Merge pull request #31414 from frappe/version-13-hotfix

chore: weekly version-13 release
This commit is contained in:
Ankush Menat
2022-06-21 16:39:04 +05:30
committed by GitHub
22 changed files with 350 additions and 106 deletions

View File

@@ -36,10 +36,15 @@ class PricingRule(Document):
self.margin_rate_or_amount = 0.0 self.margin_rate_or_amount = 0.0
def validate_duplicate_apply_on(self): def validate_duplicate_apply_on(self):
field = apply_on_dict.get(self.apply_on) if self.apply_on != "Transaction":
values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field) if field] apply_on_table = apply_on_dict.get(self.apply_on)
if len(values) != len(set(values)): if not apply_on_table:
frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on)) return
apply_on_field = frappe.scrub(self.apply_on)
values = [d.get(apply_on_field) for d in self.get(apply_on_table) if d.get(apply_on_field)]
if len(values) != len(set(values)):
frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
def validate_mandatory(self): def validate_mandatory(self):
for apply_on, field in apply_on_dict.items(): for apply_on, field in apply_on_dict.items():

View File

@@ -161,17 +161,6 @@ class PurchaseInvoice(BuyingController):
super(PurchaseInvoice, self).set_missing_values(for_validate) super(PurchaseInvoice, self).set_missing_values(for_validate)
def check_conversion_rate(self):
default_currency = erpnext.get_company_currency(self.company)
if not default_currency:
throw(_("Please enter default currency in Company Master"))
if (
(self.currency == default_currency and flt(self.conversion_rate) != 1.00)
or not self.conversion_rate
or (self.currency != default_currency and flt(self.conversion_rate) == 1.00)
):
throw(_("Conversion rate cannot be 0 or 1"))
def validate_credit_to_acc(self): def validate_credit_to_acc(self):
if not self.credit_to: if not self.credit_to:
self.credit_to = get_party_account("Supplier", self.supplier, self.company) self.credit_to = get_party_account("Supplier", self.supplier, self.company)

View File

@@ -1582,6 +1582,26 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
company.enable_provisional_accounting_for_non_stock_items = 0 company.enable_provisional_accounting_for_non_stock_items = 0
company.save() company.save()
def test_item_less_defaults(self):
pi = frappe.new_doc("Purchase Invoice")
pi.supplier = "_Test Supplier"
pi.company = "_Test Company"
pi.append(
"items",
{
"item_name": "Opening item",
"qty": 1,
"uom": "Tonne",
"stock_uom": "Kg",
"rate": 1000,
"expense_account": "Stock Received But Not Billed - _TC",
},
)
pi.save()
self.assertEqual(pi.items[0].conversion_factor, 1000)
def check_gl_entries(doc, voucher_no, expected_gle, posting_date): def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql( gl_entries = frappe.db.sql(

View File

@@ -116,6 +116,7 @@ class SalesInvoice(SellingController):
self.set_income_account_for_fixed_assets() self.set_income_account_for_fixed_assets()
self.validate_item_cost_centers() self.validate_item_cost_centers()
self.validate_income_account() self.validate_income_account()
self.check_conversion_rate()
validate_inter_company_party( validate_inter_company_party(
self.doctype, self.customer, self.company, self.inter_company_invoice_reference self.doctype, self.customer, self.company, self.inter_company_invoice_reference

View File

@@ -1612,6 +1612,17 @@ class TestSalesInvoice(unittest.TestCase):
self.assertTrue(gle) self.assertTrue(gle)
def test_invoice_exchange_rate(self):
si = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=1,
do_not_save=1,
)
self.assertRaises(frappe.ValidationError, si.save)
def test_invalid_currency(self): def test_invalid_currency(self):
# Customer currency = USD # Customer currency = USD

View File

@@ -62,8 +62,8 @@ class TestUtils(unittest.TestCase):
stock_entry = {"item": item, "to_warehouse": "_Test Warehouse - _TC", "qty": 1, "rate": 10} stock_entry = {"item": item, "to_warehouse": "_Test Warehouse - _TC", "qty": 1, "rate": 10}
se1 = make_stock_entry(posting_date="2022-01-01", **stock_entry) se1 = make_stock_entry(posting_date="2022-01-01", **stock_entry)
se2 = make_stock_entry(posting_date="2022-02-01", **stock_entry)
se3 = make_stock_entry(posting_date="2022-03-01", **stock_entry) se3 = make_stock_entry(posting_date="2022-03-01", **stock_entry)
se2 = make_stock_entry(posting_date="2022-02-01", **stock_entry)
for doc in (se1, se2, se3): for doc in (se1, se2, se3):
vouchers.append((doc.doctype, doc.name)) vouchers.append((doc.doctype, doc.name))

View File

@@ -3,13 +3,23 @@
from json import loads from json import loads
from typing import List, Tuple from typing import TYPE_CHECKING, List, Optional, Tuple
import frappe import frappe
import frappe.defaults import frappe.defaults
from frappe import _, throw from frappe import _, throw
from frappe.model.meta import get_field_precision from frappe.model.meta import get_field_precision
from frappe.utils import cint, cstr, flt, formatdate, get_number_format_info, getdate, now, nowdate from frappe.utils import (
cint,
create_batch,
cstr,
flt,
formatdate,
get_number_format_info,
getdate,
now,
nowdate,
)
from six import string_types from six import string_types
import erpnext import erpnext
@@ -19,6 +29,9 @@ from erpnext.accounts.doctype.account.account import get_account_currency # noq
from erpnext.stock import get_warehouse_account_map from erpnext.stock import get_warehouse_account_map
from erpnext.stock.utils import get_stock_value_on from erpnext.stock.utils import get_stock_value_on
if TYPE_CHECKING:
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import RepostItemValuation
class FiscalYearError(frappe.ValidationError): class FiscalYearError(frappe.ValidationError):
pass pass
@@ -28,6 +41,9 @@ class PaymentEntryUnlinkError(frappe.ValidationError):
pass pass
GL_REPOSTING_CHUNK = 100
@frappe.whitelist() @frappe.whitelist()
def get_fiscal_year( def get_fiscal_year(
date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False
@@ -1122,38 +1138,55 @@ def update_gl_entries_after(
def repost_gle_for_stock_vouchers( def repost_gle_for_stock_vouchers(
stock_vouchers, posting_date, company=None, warehouse_account=None stock_vouchers: List[Tuple[str, str]],
posting_date: str,
company: Optional[str] = None,
warehouse_account=None,
repost_doc: Optional["RepostItemValuation"] = None,
): ):
if not stock_vouchers: if not stock_vouchers:
return return
def _delete_gl_entries(voucher_type, voucher_no):
frappe.db.sql(
"""delete from `tabGL Entry`
where voucher_type=%s and voucher_no=%s""",
(voucher_type, voucher_no),
)
stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers)
if not warehouse_account: if not warehouse_account:
warehouse_account = get_warehouse_account_map(company) warehouse_account = get_warehouse_account_map(company)
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2 precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2
gle = get_voucherwise_gl_entries(stock_vouchers, posting_date) stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers)
for voucher_type, voucher_no in stock_vouchers: if repost_doc and repost_doc.gl_reposting_index:
existing_gle = gle.get((voucher_type, voucher_no), []) # Restore progress
voucher_obj = frappe.get_cached_doc(voucher_type, voucher_no) stock_vouchers = stock_vouchers[cint(repost_doc.gl_reposting_index) :]
expected_gle = voucher_obj.get_gl_entries(warehouse_account)
if expected_gle: for stock_vouchers_chunk in create_batch(stock_vouchers, GL_REPOSTING_CHUNK):
if not existing_gle or not compare_existing_and_expected_gle( gle = get_voucherwise_gl_entries(stock_vouchers_chunk, posting_date)
existing_gle, expected_gle, precision for voucher_type, voucher_no in stock_vouchers_chunk:
): existing_gle = gle.get((voucher_type, voucher_no), [])
voucher_obj = frappe.get_doc(voucher_type, voucher_no)
expected_gle = voucher_obj.get_gl_entries(warehouse_account)
if expected_gle:
if not existing_gle or not compare_existing_and_expected_gle(
existing_gle, expected_gle, precision
):
_delete_gl_entries(voucher_type, voucher_no)
voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
else:
_delete_gl_entries(voucher_type, voucher_no) _delete_gl_entries(voucher_type, voucher_no)
voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
else: if not frappe.flags.in_test:
_delete_gl_entries(voucher_type, voucher_no) frappe.db.commit()
if repost_doc:
repost_doc.db_set(
"gl_reposting_index", cint(repost_doc.gl_reposting_index) + len(stock_vouchers_chunk)
)
def _delete_gl_entries(voucher_type, voucher_no):
frappe.db.sql(
"""delete from `tabGL Entry`
where voucher_type=%s and voucher_no=%s""",
(voucher_type, voucher_no),
)
def sort_stock_vouchers_by_posting_date( def sort_stock_vouchers_by_posting_date(
@@ -1167,6 +1200,9 @@ def sort_stock_vouchers_by_posting_date(
.select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation) .select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation)
.where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos))) .where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos)))
.groupby(sle.voucher_type, sle.voucher_no) .groupby(sle.voucher_type, sle.voucher_no)
.orderby(sle.posting_date)
.orderby(sle.posting_time)
.orderby(sle.creation)
).run(as_dict=True) ).run(as_dict=True)
sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles] sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles]

View File

@@ -47,6 +47,7 @@ from erpnext.controllers.print_settings import (
from erpnext.controllers.sales_and_purchase_return import validate_return from erpnext.controllers.sales_and_purchase_return import validate_return
from erpnext.exceptions import InvalidCurrency from erpnext.exceptions import InvalidCurrency
from erpnext.setup.utils import get_exchange_rate from erpnext.setup.utils import get_exchange_rate
from erpnext.stock.doctype.item.item import get_uom_conv_factor
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
from erpnext.stock.get_item_details import ( from erpnext.stock.get_item_details import (
_get_item_tax_template, _get_item_tax_template,
@@ -549,6 +550,15 @@ class AccountsController(TransactionBase):
if ret.get("pricing_rules"): if ret.get("pricing_rules"):
self.apply_pricing_rule_on_items(item, ret) self.apply_pricing_rule_on_items(item, ret)
self.set_pricing_rule_details(item, ret) self.set_pricing_rule_details(item, ret)
else:
# Transactions line item without item code
uom = item.get("uom")
stock_uom = item.get("stock_uom")
if bool(uom) != bool(stock_uom): # xor
item.stock_uom = item.uom = uom or stock_uom
item.conversion_factor = get_uom_conv_factor(item.get("uom"), item.get("stock_uom"))
if self.doctype == "Purchase Invoice": if self.doctype == "Purchase Invoice":
self.set_expense_account(for_validate) self.set_expense_account(for_validate)
@@ -1836,6 +1846,17 @@ class AccountsController(TransactionBase):
jv.save() jv.save()
jv.submit() jv.submit()
def check_conversion_rate(self):
default_currency = erpnext.get_company_currency(self.company)
if not default_currency:
throw(_("Please enter default currency in Company Master"))
if (
(self.currency == default_currency and flt(self.conversion_rate) != 1.00)
or not self.conversion_rate
or (self.currency != default_currency and flt(self.conversion_rate) == 1.00)
):
throw(_("Conversion rate cannot be 0 or 1"))
@frappe.whitelist() @frappe.whitelist()
def get_tax_rate(account_head): def get_tax_rate(account_head):

View File

@@ -166,7 +166,7 @@ class StockController(AccountsController):
"against": warehouse_account[sle.warehouse]["account"], "against": warehouse_account[sle.warehouse]["account"],
"cost_center": item_row.cost_center, "cost_center": item_row.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"), "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(sle.stock_value_difference, precision), "debit": -1 * flt(sle.stock_value_difference, precision),
"project": item_row.get("project") or self.get("project"), "project": item_row.get("project") or self.get("project"),
"is_opening": item_row.get("is_opening") or self.get("is_opening") or "No", "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No",
}, },

View File

@@ -1,6 +1,6 @@
{ {
"actions": [], "actions": [],
"autoname": "autoincrement", "autoname": "hash",
"creation": "2022-05-31 17:34:39.825537", "creation": "2022-05-31 17:34:39.825537",
"doctype": "DocType", "doctype": "DocType",
"engine": "InnoDB", "engine": "InnoDB",
@@ -42,7 +42,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-06-06 14:50:35.161062", "modified": "2022-06-20 15:10:15.826571",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM Update Batch", "name": "BOM Update Batch",
@@ -50,6 +50,5 @@
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC"
"states": []
} }

View File

@@ -572,7 +572,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
is_pos: cint(me.frm.doc.is_pos), is_pos: cint(me.frm.doc.is_pos),
is_return: cint(me.frm.doc.is_return), is_return: cint(me.frm.doc.is_return),
is_subcontracted: me.frm.doc.is_subcontracted, is_subcontracted: me.frm.doc.is_subcontracted,
transaction_date: me.frm.doc.transaction_date || me.frm.doc.posting_date,
ignore_pricing_rule: me.frm.doc.ignore_pricing_rule, ignore_pricing_rule: me.frm.doc.ignore_pricing_rule,
doctype: me.frm.doc.doctype, doctype: me.frm.doc.doctype,
name: me.frm.doc.name, name: me.frm.doc.name,

View File

@@ -56,6 +56,9 @@ def validate_eligibility(doc):
return False return False
invalid_company = not frappe.db.get_value("E Invoice User", {"company": doc.get("company")}) invalid_company = not frappe.db.get_value("E Invoice User", {"company": doc.get("company")})
invalid_company_gstin = not frappe.db.get_value(
"E Invoice User", {"gstin": doc.get("company_gstin")}
)
invalid_supply_type = doc.get("gst_category") not in [ invalid_supply_type = doc.get("gst_category") not in [
"Registered Regular", "Registered Regular",
"Registered Composition", "Registered Composition",
@@ -72,6 +75,7 @@ def validate_eligibility(doc):
if ( if (
invalid_company invalid_company
or invalid_company_gstin
or invalid_supply_type or invalid_supply_type
or company_transaction or company_transaction
or no_taxes_applied or no_taxes_applied

View File

@@ -377,6 +377,12 @@ def create_internal_customer(
if not allowed_to_interact_with: if not allowed_to_interact_with:
allowed_to_interact_with = represents_company allowed_to_interact_with = represents_company
exisiting_representative = frappe.db.get_value(
"Customer", {"represents_company": represents_company}
)
if exisiting_representative:
return exisiting_representative
if not frappe.db.exists("Customer", customer_name): if not frappe.db.exists("Customer", customer_name):
customer = frappe.get_doc( customer = frappe.get_doc(
{ {

View File

@@ -331,7 +331,7 @@
"show_seconds": 1 "show_seconds": 1
}, },
{ {
"depends_on": "eval:doc.quotaion_to=='Customer' && doc.party_name", "depends_on": "eval:doc.quotation_to=='Customer' && doc.party_name",
"fieldname": "col_break98", "fieldname": "col_break98",
"fieldtype": "Column Break", "fieldtype": "Column Break",
"show_days": 1, "show_days": 1,
@@ -357,7 +357,7 @@
"show_seconds": 1 "show_seconds": 1
}, },
{ {
"depends_on": "eval:doc.quotaion_to=='Customer' && doc.party_name", "depends_on": "eval:doc.quotation_to=='Customer' && doc.party_name",
"fieldname": "customer_group", "fieldname": "customer_group",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 1, "hidden": 1,
@@ -1174,7 +1174,7 @@
"idx": 82, "idx": 82,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-06-11 20:35:32.635804", "modified": "2022-06-15 20:35:32.635804",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Quotation", "name": "Quotation",

View File

@@ -116,7 +116,7 @@ class Quotation(SellingController):
@frappe.whitelist() @frappe.whitelist()
def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None): def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None):
if not self.has_sales_order(): if not (self.is_fully_ordered() or self.is_partially_ordered()):
get_lost_reasons = frappe.get_list("Quotation Lost Reason", fields=["name"]) get_lost_reasons = frappe.get_list("Quotation Lost Reason", fields=["name"])
lost_reasons_lst = [reason.get("name") for reason in get_lost_reasons] lost_reasons_lst = [reason.get("name") for reason in get_lost_reasons]
frappe.db.set(self, "status", "Lost") frappe.db.set(self, "status", "Lost")

View File

@@ -26,6 +26,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import (
from erpnext.selling.doctype.customer.customer import check_credit_limit from erpnext.selling.doctype.customer.customer import check_credit_limit
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.doctype.item.item import get_item_defaults from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.stock.get_item_details import get_default_bom
from erpnext.stock.stock_balance import get_reserved_qty, update_bin_qty from erpnext.stock.stock_balance import get_reserved_qty, update_bin_qty
form_grid_templates = {"items": "templates/form_grid/item_grid.html"} form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@@ -493,8 +494,9 @@ class SalesOrder(SellingController):
for table in [self.items, self.packed_items]: for table in [self.items, self.packed_items]:
for i in table: for i in table:
bom = get_default_bom_item(i.item_code) bom = get_default_bom(i.item_code)
stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty
if not for_raw_material_request: if not for_raw_material_request:
total_work_order_qty = flt( total_work_order_qty = flt(
frappe.db.sql( frappe.db.sql(
@@ -508,32 +510,19 @@ class SalesOrder(SellingController):
pending_qty = stock_qty pending_qty = stock_qty
if pending_qty and i.item_code not in product_bundle_parents: if pending_qty and i.item_code not in product_bundle_parents:
if bom: items.append(
items.append( dict(
dict( name=i.name,
name=i.name, item_code=i.item_code,
item_code=i.item_code, description=i.description,
description=i.description, bom=bom or "",
bom=bom, warehouse=i.warehouse,
warehouse=i.warehouse, pending_qty=pending_qty,
pending_qty=pending_qty, required_qty=pending_qty if for_raw_material_request else 0,
required_qty=pending_qty if for_raw_material_request else 0, sales_order_item=i.name,
sales_order_item=i.name,
)
)
else:
items.append(
dict(
name=i.name,
item_code=i.item_code,
description=i.description,
bom="",
warehouse=i.warehouse,
pending_qty=pending_qty,
required_qty=pending_qty if for_raw_material_request else 0,
sales_order_item=i.name,
)
) )
)
return items return items
def on_recurring(self, reference_doc, auto_repeat_doc): def on_recurring(self, reference_doc, auto_repeat_doc):
@@ -1237,13 +1226,6 @@ def update_status(status, name):
so.update_status(status) so.update_status(status)
def get_default_bom_item(item_code):
bom = frappe.get_all("BOM", dict(item=item_code, is_active=True), order_by="is_default desc")
bom = bom[0].name if bom else None
return bom
@frappe.whitelist() @frappe.whitelist()
def make_raw_material_request(items, company, sales_order, project=None): def make_raw_material_request(items, company, sales_order, project=None):
if not frappe.has_permission("Sales Order", "write"): if not frappe.has_permission("Sales Order", "write"):

View File

@@ -1374,6 +1374,59 @@ class TestSalesOrder(FrappeTestCase):
except Exception: except Exception:
self.fail("Can not cancel sales order with linked cancelled payment entry") self.fail("Can not cancel sales order with linked cancelled payment entry")
def test_work_order_pop_up_from_sales_order(self):
"Test `get_work_order_items` in Sales Order picks the right BOM for items to manufacture."
from erpnext.controllers.item_variant import create_variant
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
make_item( # template item
"Test-WO-Tshirt",
{
"has_variant": 1,
"variant_based_on": "Item Attribute",
"attributes": [{"attribute": "Test Colour"}],
},
)
make_item("Test-RM-Cotton") # RM for BOM
for colour in (
"Red",
"Green",
):
variant = create_variant("Test-WO-Tshirt", {"Test Colour": colour})
variant.save()
template_bom = make_bom(item="Test-WO-Tshirt", rate=100, raw_materials=["Test-RM-Cotton"])
red_var_bom = make_bom(item="Test-WO-Tshirt-R", rate=100, raw_materials=["Test-RM-Cotton"])
so = make_sales_order(
**{
"item_list": [
{
"item_code": "Test-WO-Tshirt-R",
"qty": 1,
"rate": 1000,
"warehouse": "_Test Warehouse - _TC",
},
{
"item_code": "Test-WO-Tshirt-G",
"qty": 1,
"rate": 1000,
"warehouse": "_Test Warehouse - _TC",
},
]
}
)
wo_items = so.get_work_order_items()
self.assertEqual(wo_items[0].get("item_code"), "Test-WO-Tshirt-R")
self.assertEqual(wo_items[0].get("bom"), red_var_bom.name)
# Must pick Template Item BOM for Test-WO-Tshirt-G as it has no BOM
self.assertEqual(wo_items[1].get("item_code"), "Test-WO-Tshirt-G")
self.assertEqual(wo_items[1].get("bom"), template_bom.name)
def test_request_for_raw_materials(self): def test_request_for_raw_materials(self):
item = make_item( item = make_item(
"_Test Finished Item", "_Test Finished Item",

View File

@@ -1064,6 +1064,33 @@ class TestDeliveryNote(FrappeTestCase):
self.assertEqual(dn.items[0].rate, rate) self.assertEqual(dn.items[0].rate, rate)
def test_internal_transfer_precision_gle(self):
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
item = make_item(properties={"valuation_method": "Moving Average"}).name
company = "_Test Company with perpetual inventory"
warehouse = "Stores - TCP1"
target = "Finished Goods - TCP1"
customer = create_internal_customer(represents_company=company)
# average rate = 128.015
rates = [101.45, 150.46, 138.25, 121.9]
for rate in rates:
make_stock_entry(item_code=item, target=warehouse, qty=1, rate=rate)
dn = create_delivery_note(
item_code=item,
company=company,
customer=customer,
qty=4,
warehouse=warehouse,
target_warehouse=target,
)
self.assertFalse(
frappe.db.exists("GL Entry", {"voucher_no": dn.name, "voucher_type": dn.doctype})
)
def create_delivery_note(**args): def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note") dn = frappe.new_doc("Delivery Note")

View File

@@ -25,7 +25,8 @@
"items_to_be_repost", "items_to_be_repost",
"affected_transactions", "affected_transactions",
"distinct_item_and_warehouse", "distinct_item_and_warehouse",
"current_index" "current_index",
"gl_reposting_index"
], ],
"fields": [ "fields": [
{ {
@@ -181,12 +182,20 @@
"label": "Affected Transactions", "label": "Affected Transactions",
"no_copy": 1, "no_copy": 1,
"read_only": 1 "read_only": 1
},
{
"default": "0",
"fieldname": "gl_reposting_index",
"fieldtype": "Int",
"hidden": 1,
"label": "GL reposting index",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-04-18 14:08:08.821602", "modified": "2022-06-13 12:20:22.182322",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Repost Item Valuation", "name": "Repost Item Valuation",

View File

@@ -87,6 +87,7 @@ class RepostItemValuation(Document):
self.current_index = 0 self.current_index = 0
self.distinct_item_and_warehouse = None self.distinct_item_and_warehouse = None
self.items_to_be_repost = None self.items_to_be_repost = None
self.gl_reposting_index = 0
self.db_update() self.db_update()
def deduplicate_similar_repost(self): def deduplicate_similar_repost(self):
@@ -192,6 +193,7 @@ def repost_gl_entries(doc):
directly_dependent_transactions + list(repost_affected_transaction), directly_dependent_transactions + list(repost_affected_transaction),
doc.posting_date, doc.posting_date,
doc.company, doc.company,
repost_doc=doc,
) )

View File

@@ -2,10 +2,14 @@
# See license.txt # See license.txt
from unittest.mock import MagicMock, call
import frappe import frappe
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from frappe.utils import nowdate from frappe.utils import nowdate
from frappe.utils.data import add_to_date, today
from erpnext.accounts.utils import repost_gle_for_stock_vouchers
from erpnext.controllers.stock_controller import create_item_wise_repost_entries from erpnext.controllers.stock_controller import create_item_wise_repost_entries
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
@@ -13,10 +17,11 @@ from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import (
in_configured_timeslot, in_configured_timeslot,
) )
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.tests.test_utils import StockTestMixin
from erpnext.stock.utils import PendingRepostingError from erpnext.stock.utils import PendingRepostingError
class TestRepostItemValuation(FrappeTestCase): class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
def tearDown(self): def tearDown(self):
frappe.flags.dont_execute_stock_reposts = False frappe.flags.dont_execute_stock_reposts = False
@@ -193,3 +198,77 @@ class TestRepostItemValuation(FrappeTestCase):
[["a", "b"], ["c", "d"]], [["a", "b"], ["c", "d"]],
sorted(frappe.parse_json(frappe.as_json(set([("a", "b"), ("c", "d")])))), sorted(frappe.parse_json(frappe.as_json(set([("a", "b"), ("c", "d")])))),
) )
def test_gl_repost_progress(self):
from erpnext.accounts import utils
# lower numbers to simplify test
orig_chunk_size = utils.GL_REPOSTING_CHUNK
utils.GL_REPOSTING_CHUNK = 1
self.addCleanup(setattr, utils, "GL_REPOSTING_CHUNK", orig_chunk_size)
doc = frappe.new_doc("Repost Item Valuation")
doc.db_set = MagicMock()
vouchers = []
company = "_Test Company with perpetual inventory"
posting_date = today()
for _ in range(3):
se = make_stock_entry(company=company, qty=1, rate=2, target="Stores - TCP1")
vouchers.append((se.doctype, se.name))
repost_gle_for_stock_vouchers(stock_vouchers=vouchers, posting_date=posting_date, repost_doc=doc)
self.assertIn(call("gl_reposting_index", 1), doc.db_set.mock_calls)
doc.db_set.reset_mock()
doc.gl_reposting_index = 1
repost_gle_for_stock_vouchers(stock_vouchers=vouchers, posting_date=posting_date, repost_doc=doc)
self.assertNotIn(call("gl_reposting_index", 1), doc.db_set.mock_calls)
def test_gl_complete_gl_reposting(self):
from erpnext.accounts import utils
# lower numbers to simplify test
orig_chunk_size = utils.GL_REPOSTING_CHUNK
utils.GL_REPOSTING_CHUNK = 2
self.addCleanup(setattr, utils, "GL_REPOSTING_CHUNK", orig_chunk_size)
item = self.make_item().name
company = "_Test Company with perpetual inventory"
for _ in range(10):
make_stock_entry(item=item, company=company, qty=1, rate=10, target="Stores - TCP1")
# consume
consumption = make_stock_entry(item=item, company=company, qty=1, source="Stores - TCP1")
self.assertGLEs(
consumption,
[{"credit": 10, "debit": 0}],
gle_filters={"account": "Stock In Hand - TCP1"},
)
# backdated receipt
backdated_receipt = make_stock_entry(
item=item,
company=company,
qty=1,
rate=50,
target="Stores - TCP1",
posting_date=add_to_date(today(), days=-1),
)
self.assertGLEs(
backdated_receipt,
[{"credit": 0, "debit": 50}],
gle_filters={"account": "Stock In Hand - TCP1"},
)
# check that original consumption GLe is updated
self.assertGLEs(
consumption,
[{"credit": 50, "debit": 0}],
gle_filters={"account": "Stock In Hand - TCP1"},
)

View File

@@ -63,18 +63,16 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
item = frappe.get_cached_doc("Item", args.item_code) item = frappe.get_cached_doc("Item", args.item_code)
validate_item_details(args, item) validate_item_details(args, item)
out = get_basic_details(args, item, overwrite_warehouse)
if isinstance(doc, string_types): if isinstance(doc, string_types):
doc = json.loads(doc) doc = json.loads(doc)
if doc and doc.get("doctype") == "Purchase Invoice":
args["bill_date"] = doc.get("bill_date")
if doc: if doc:
args["posting_date"] = doc.get("posting_date") args["transaction_date"] = doc.get("transaction_date") or doc.get("posting_date")
args["transaction_date"] = doc.get("transaction_date")
if doc.get("doctype") == "Purchase Invoice":
args["bill_date"] = doc.get("bill_date")
out = get_basic_details(args, item, overwrite_warehouse)
get_item_tax_template(args, item, out) get_item_tax_template(args, item, out)
out["item_tax_rate"] = get_item_tax_map( out["item_tax_rate"] = get_item_tax_map(
args.company, args.company,
@@ -586,9 +584,7 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False):
if tax.valid_from or tax.maximum_net_rate: if tax.valid_from or tax.maximum_net_rate:
# In purchase Invoice first preference will be given to supplier invoice date # In purchase Invoice first preference will be given to supplier invoice date
# if supplier date is not present then posting date # if supplier date is not present then posting date
validation_date = ( validation_date = args.get("bill_date") or args.get("transaction_date")
args.get("transaction_date") or args.get("bill_date") or args.get("posting_date")
)
if getdate(tax.valid_from) <= getdate(validation_date) and is_within_valid_range(args, tax): if getdate(tax.valid_from) <= getdate(validation_date) and is_within_valid_range(args, tax):
taxes_with_validity.append(tax) taxes_with_validity.append(tax)
@@ -881,10 +877,6 @@ def get_item_price(args, item_code, ignore_party=False):
conditions += """ and %(transaction_date)s between conditions += """ and %(transaction_date)s between
ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')""" ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')"""
if args.get("posting_date"):
conditions += """ and %(posting_date)s between
ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')"""
return frappe.db.sql( return frappe.db.sql(
""" select name, price_list_rate, uom """ select name, price_list_rate, uom
from `tabItem Price` {conditions} from `tabItem Price` {conditions}
@@ -911,7 +903,6 @@ def get_price_list_rate_for(args, item_code):
"supplier": args.get("supplier"), "supplier": args.get("supplier"),
"uom": args.get("uom"), "uom": args.get("uom"),
"transaction_date": args.get("transaction_date"), "transaction_date": args.get("transaction_date"),
"posting_date": args.get("posting_date"),
"batch_no": args.get("batch_no"), "batch_no": args.get("batch_no"),
} }
@@ -1342,12 +1333,22 @@ def get_price_list_currency_and_exchange_rate(args):
@frappe.whitelist() @frappe.whitelist()
def get_default_bom(item_code=None): def get_default_bom(item_code=None):
if item_code: def _get_bom(item):
bom = frappe.db.get_value( bom = frappe.get_all(
"BOM", {"docstatus": 1, "is_default": 1, "is_active": 1, "item": item_code} "BOM", dict(item=item, is_active=True, is_default=True, docstatus=1), limit=1
) )
if bom: return bom[0].name if bom else None
return bom
if not item_code:
return
bom_name = _get_bom(item_code)
template_item = frappe.db.get_value("Item", item_code, "variant_of")
if not bom_name and template_item:
bom_name = _get_bom(template_item)
return bom_name
@frappe.whitelist() @frappe.whitelist()