diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index db6ec82c83d..6ea244003c3 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -36,10 +36,15 @@ class PricingRule(Document): self.margin_rate_or_amount = 0.0 def validate_duplicate_apply_on(self): - field = apply_on_dict.get(self.apply_on) - values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field) if field] - if len(values) != len(set(values)): - frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on)) + if self.apply_on != "Transaction": + apply_on_table = apply_on_dict.get(self.apply_on) + if not apply_on_table: + 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): for apply_on, field in apply_on_dict.items(): diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 3c1dc80970e..a3558ab8b40 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -161,17 +161,6 @@ class PurchaseInvoice(BuyingController): 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): if not self.credit_to: self.credit_to = get_party_account("Supplier", self.supplier, self.company) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 038508b14a8..1fdadea8bed 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1582,6 +1582,26 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): company.enable_provisional_accounting_for_non_stock_items = 0 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): gl_entries = frappe.db.sql( diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 954a8207780..069148c1199 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -116,6 +116,7 @@ class SalesInvoice(SellingController): self.set_income_account_for_fixed_assets() self.validate_item_cost_centers() self.validate_income_account() + self.check_conversion_rate() validate_inter_company_party( self.doctype, self.customer, self.company, self.inter_company_invoice_reference diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 068d1ffd89d..16210e78bdd 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1612,6 +1612,17 @@ class TestSalesInvoice(unittest.TestCase): 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): # Customer currency = USD diff --git a/erpnext/accounts/test/test_utils.py b/erpnext/accounts/test/test_utils.py index 77c40bae2d9..882cd694a32 100644 --- a/erpnext/accounts/test/test_utils.py +++ b/erpnext/accounts/test/test_utils.py @@ -62,8 +62,8 @@ class TestUtils(unittest.TestCase): stock_entry = {"item": item, "to_warehouse": "_Test Warehouse - _TC", "qty": 1, "rate": 10} 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) + se2 = make_stock_entry(posting_date="2022-02-01", **stock_entry) for doc in (se1, se2, se3): vouchers.append((doc.doctype, doc.name)) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 0bf2939336a..47de0eff359 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -3,13 +3,23 @@ from json import loads -from typing import List, Tuple +from typing import TYPE_CHECKING, List, Optional, Tuple import frappe import frappe.defaults from frappe import _, throw 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 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.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): pass @@ -28,6 +41,9 @@ class PaymentEntryUnlinkError(frappe.ValidationError): pass +GL_REPOSTING_CHUNK = 100 + + @frappe.whitelist() def get_fiscal_year( 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( - 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: 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: warehouse_account = get_warehouse_account_map(company) precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2 - gle = get_voucherwise_gl_entries(stock_vouchers, posting_date) - for voucher_type, voucher_no in stock_vouchers: - existing_gle = gle.get((voucher_type, voucher_no), []) - voucher_obj = frappe.get_cached_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 - ): + stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers) + if repost_doc and repost_doc.gl_reposting_index: + # Restore progress + stock_vouchers = stock_vouchers[cint(repost_doc.gl_reposting_index) :] + + for stock_vouchers_chunk in create_batch(stock_vouchers, GL_REPOSTING_CHUNK): + gle = get_voucherwise_gl_entries(stock_vouchers_chunk, posting_date) + 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) - voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True) - else: - _delete_gl_entries(voucher_type, voucher_no) + + if not frappe.flags.in_test: + 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( @@ -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) .where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos))) .groupby(sle.voucher_type, sle.voucher_no) + .orderby(sle.posting_date) + .orderby(sle.posting_time) + .orderby(sle.creation) ).run(as_dict=True) sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles] diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 127d1094a3d..71901c52bf6 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -47,6 +47,7 @@ from erpnext.controllers.print_settings import ( from erpnext.controllers.sales_and_purchase_return import validate_return from erpnext.exceptions import InvalidCurrency 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.get_item_details import ( _get_item_tax_template, @@ -549,6 +550,15 @@ class AccountsController(TransactionBase): if ret.get("pricing_rules"): self.apply_pricing_rule_on_items(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": self.set_expense_account(for_validate) @@ -1836,6 +1846,17 @@ class AccountsController(TransactionBase): jv.save() 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() def get_tax_rate(account_head): diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index feec42f43a3..e90a4f62411 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -166,7 +166,7 @@ class StockController(AccountsController): "against": warehouse_account[sle.warehouse]["account"], "cost_center": item_row.cost_center, "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"), "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No", }, diff --git a/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json b/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json index 83b54d326cb..d204e8e6de9 100644 --- a/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json +++ b/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json @@ -1,6 +1,6 @@ { "actions": [], - "autoname": "autoincrement", + "autoname": "hash", "creation": "2022-05-31 17:34:39.825537", "doctype": "DocType", "engine": "InnoDB", @@ -42,7 +42,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-06-06 14:50:35.161062", + "modified": "2022-06-20 15:10:15.826571", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Update Batch", @@ -50,6 +50,5 @@ "owner": "Administrator", "permissions": [], "sort_field": "modified", - "sort_order": "DESC", - "states": [] + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 2d6e15b15f7..3a51dd1a261 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -572,7 +572,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ is_pos: cint(me.frm.doc.is_pos), is_return: cint(me.frm.doc.is_return), 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, doctype: me.frm.doc.doctype, name: me.frm.doc.name, diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 871bf9027c1..0d53e702d8e 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -56,6 +56,9 @@ def validate_eligibility(doc): return False 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 [ "Registered Regular", "Registered Composition", @@ -72,6 +75,7 @@ def validate_eligibility(doc): if ( invalid_company + or invalid_company_gstin or invalid_supply_type or company_transaction or no_taxes_applied diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 0c1c25da475..5c65cb68ef5 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -377,6 +377,12 @@ def create_internal_customer( if not allowed_to_interact_with: 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): customer = frappe.get_doc( { diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index eeaa4fda585..09fd310040c 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -331,7 +331,7 @@ "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", "fieldtype": "Column Break", "show_days": 1, @@ -357,7 +357,7 @@ "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", "fieldtype": "Link", "hidden": 1, @@ -1174,7 +1174,7 @@ "idx": 82, "is_submittable": 1, "links": [], - "modified": "2022-06-11 20:35:32.635804", + "modified": "2022-06-15 20:35:32.635804", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index ceb922c0e53..d1530395d8a 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -116,7 +116,7 @@ class Quotation(SellingController): @frappe.whitelist() 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"]) lost_reasons_lst = [reason.get("name") for reason in get_lost_reasons] frappe.db.set(self, "status", "Lost") diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index edd7d26d0bc..4bbeb21a1f2 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -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.setup.doctype.item_group.item_group import get_item_group_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 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 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 + if not for_raw_material_request: total_work_order_qty = flt( frappe.db.sql( @@ -508,32 +510,19 @@ class SalesOrder(SellingController): pending_qty = stock_qty if pending_qty and i.item_code not in product_bundle_parents: - if bom: - items.append( - dict( - name=i.name, - item_code=i.item_code, - description=i.description, - bom=bom, - warehouse=i.warehouse, - pending_qty=pending_qty, - required_qty=pending_qty if for_raw_material_request else 0, - 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, - ) + items.append( + dict( + name=i.name, + item_code=i.item_code, + description=i.description, + bom=bom or "", + 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 def on_recurring(self, reference_doc, auto_repeat_doc): @@ -1237,13 +1226,6 @@ def update_status(status, name): 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() def make_raw_material_request(items, company, sales_order, project=None): if not frappe.has_permission("Sales Order", "write"): diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 292562beebb..81545fac14f 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1374,6 +1374,59 @@ class TestSalesOrder(FrappeTestCase): except Exception: 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): item = make_item( "_Test Finished Item", diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index fffcdca3802..6bcab737b37 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1064,6 +1064,33 @@ class TestDeliveryNote(FrappeTestCase): 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): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json index 8c13149252a..2c97d0f5173 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -25,7 +25,8 @@ "items_to_be_repost", "affected_transactions", "distinct_item_and_warehouse", - "current_index" + "current_index", + "gl_reposting_index" ], "fields": [ { @@ -181,12 +182,20 @@ "label": "Affected Transactions", "no_copy": 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, "is_submittable": 1, "links": [], - "modified": "2022-04-18 14:08:08.821602", + "modified": "2022-06-13 12:20:22.182322", "modified_by": "Administrator", "module": "Stock", "name": "Repost Item Valuation", diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index b068d5fe3c3..ee4d7bd8643 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -87,6 +87,7 @@ class RepostItemValuation(Document): self.current_index = 0 self.distinct_item_and_warehouse = None self.items_to_be_repost = None + self.gl_reposting_index = 0 self.db_update() def deduplicate_similar_repost(self): @@ -192,6 +193,7 @@ def repost_gl_entries(doc): directly_dependent_transactions + list(repost_affected_transaction), doc.posting_date, doc.company, + repost_doc=doc, ) diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py index 3184f69aa45..edd2553d5d1 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -2,10 +2,14 @@ # See license.txt +from unittest.mock import MagicMock, call + import frappe from frappe.tests.utils import FrappeTestCase 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.stock.doctype.item.test_item import make_item 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, ) 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 -class TestRepostItemValuation(FrappeTestCase): +class TestRepostItemValuation(FrappeTestCase, StockTestMixin): def tearDown(self): frappe.flags.dont_execute_stock_reposts = False @@ -193,3 +198,77 @@ class TestRepostItemValuation(FrappeTestCase): [["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"}, + ) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 78e809a6fd2..4db12dcb98b 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -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) validate_item_details(args, item) - out = get_basic_details(args, item, overwrite_warehouse) - if isinstance(doc, string_types): doc = json.loads(doc) - if doc and doc.get("doctype") == "Purchase Invoice": - args["bill_date"] = doc.get("bill_date") - if doc: - args["posting_date"] = doc.get("posting_date") - args["transaction_date"] = doc.get("transaction_date") + args["transaction_date"] = doc.get("transaction_date") or doc.get("posting_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) out["item_tax_rate"] = get_item_tax_map( 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: # In purchase Invoice first preference will be given to supplier invoice date # if supplier date is not present then posting date - validation_date = ( - args.get("transaction_date") or args.get("bill_date") or args.get("posting_date") - ) + validation_date = args.get("bill_date") or args.get("transaction_date") if getdate(tax.valid_from) <= getdate(validation_date) and is_within_valid_range(args, 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 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( """ select name, price_list_rate, uom from `tabItem Price` {conditions} @@ -911,7 +903,6 @@ def get_price_list_rate_for(args, item_code): "supplier": args.get("supplier"), "uom": args.get("uom"), "transaction_date": args.get("transaction_date"), - "posting_date": args.get("posting_date"), "batch_no": args.get("batch_no"), } @@ -1342,12 +1333,22 @@ def get_price_list_currency_and_exchange_rate(args): @frappe.whitelist() def get_default_bom(item_code=None): - if item_code: - bom = frappe.db.get_value( - "BOM", {"docstatus": 1, "is_default": 1, "is_active": 1, "item": item_code} + def _get_bom(item): + bom = frappe.get_all( + "BOM", dict(item=item, is_active=True, is_default=True, docstatus=1), limit=1 ) - if bom: - return bom + return bom[0].name if bom else None + + 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()