From a7bf55b4bf05725b5caa6715f0c3a4c20bd48be4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 04:59:04 +0000 Subject: [PATCH 01/30] chore: make supplier data expanded by default in PI (backport #53565) (#53578) * chore: make supplier data expanded by default in PI (#53565) (cherry picked from commit b433852f8a2a613637a33b1b55d9a7308412ab51) # Conflicts: # erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json * chore: resolve conflicts --------- Co-authored-by: Mihir Kandoi --- .../accounts/doctype/purchase_invoice/purchase_invoice.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 81c9ff09ccb..88428e57879 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -382,7 +382,7 @@ }, { "collapsible": 1, - "collapsible_depends_on": "bill_no", + "collapsible_depends_on": "posting_date", "fieldname": "supplier_invoice_details", "fieldtype": "Section Break", "label": "Supplier Invoice" @@ -1660,7 +1660,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2026-02-05 20:45:16.964500", + "modified": "2026-03-17 20:44:00.221219", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", From c09c5999dc1225d92ff8977756a37f16692a6f50 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 05:04:52 +0000 Subject: [PATCH 02/30] fix(stock): add company filter while fetching batches (backport #53369) (#53580) * fix(stock): add company filter while fetching batches (#53369) (cherry picked from commit 31d14df37ba270989c9e1b5851c05b1629d30ebf) # Conflicts: # erpnext/manufacturing/doctype/work_order/work_order.py # erpnext/stock/doctype/pick_list/pick_list.py # erpnext/stock/doctype/pick_list_item/pick_list_item.json * chore: resolve conflicts * chore: resolve conflicts * chore: resolve conflicts * chore: resolve conflicts --------- Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com> Co-authored-by: Mihir Kandoi --- erpnext/manufacturing/doctype/bom/bom.js | 1 + .../doctype/work_order/work_order.py | 7 +- erpnext/stock/doctype/pick_list/pick_list.py | 39 ++++++ .../stock/doctype/pick_list/test_pick_list.py | 114 ++++++++++++++++++ .../pick_list_item/pick_list_item.json | 3 +- .../serial_and_batch_bundle.py | 60 ++++++--- 6 files changed, 204 insertions(+), 20 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index c48c56df3d5..525b6aecee7 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -192,6 +192,7 @@ frappe.ui.form.on("BOM", { bom_no: frm.doc.name, item: item, qty: data.qty || 0.0, + company: frm.doc.company, project: frm.doc.project, variant_items: variant_items, use_multi_level_bom: use_multi_level_bom, diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 590708ac275..04974e2e2ba 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1372,7 +1372,11 @@ def get_item_details(item, project=None, skip_bom_info=False, throw=True): @frappe.whitelist() -def make_work_order(bom_no, item, qty=0, project=None, variant_items=None, use_multi_level_bom=None): +def make_work_order( + bom_no, item, qty=0, company=None, project=None, variant_items=None, use_multi_level_bom=None +): + from erpnext import get_default_company + if not frappe.has_permission("Work Order", "write"): frappe.throw(_("Not permitted"), frappe.PermissionError) @@ -1387,6 +1391,7 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None, use_m wo_doc = frappe.new_doc("Work Order") wo_doc.production_item = item + wo_doc.company = company or get_default_company() wo_doc.update(item_details) wo_doc.bom_no = bom_no wo_doc.use_multi_level_bom = cint(use_multi_level_bom) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index f2766852b64..1b444971659 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -29,6 +29,15 @@ from erpnext.stock.serial_batch_bundle import ( ) from erpnext.utilities.transaction_base import TransactionBase + +class MissingWarehouseValidationError(frappe.ValidationError): + pass + + +class IncorrectWarehouseValidationError(frappe.ValidationError): + pass + + # TODO: Prioritize SO or WO group warehouse @@ -94,6 +103,7 @@ class PickList(TransactionBase): if self.get("locations"): self.validate_sales_order_percentage() + self.validate_warehouses() def validate_stock_qty(self): from erpnext.stock.doctype.batch.batch import get_batch_qty @@ -138,6 +148,31 @@ class PickList(TransactionBase): title=_("Insufficient Stock"), ) + def validate_warehouses(self): + for location in self.locations: + if not location.warehouse: + frappe.throw( + _("Row {0}: Warehouse is required").format(location.idx), + title=_("Missing Warehouse"), + exc=MissingWarehouseValidationError, + ) + + company = frappe.get_cached_value("Warehouse", location.warehouse, "company") + + if company != self.company: + frappe.throw( + _( + "Row {0}: Warehouse {1} is linked to company {2}. Please select a warehouse belonging to company {3}." + ).format( + location.idx, + frappe.bold(location.warehouse), + frappe.bold(company), + frappe.bold(self.company), + ), + title=_("Incorrect Warehouse"), + exc=IncorrectWarehouseValidationError, + ) + def check_serial_no_status(self): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -949,6 +984,7 @@ def get_available_item_locations( locations = get_available_item_locations_for_batched_item( item_code, from_warehouses, + company, consider_rejected_warehouses=consider_rejected_warehouses, ) else: @@ -1049,6 +1085,7 @@ def get_available_item_locations_for_serial_and_batched_item( locations = get_available_item_locations_for_batched_item( item_code, from_warehouses, + company, consider_rejected_warehouses=consider_rejected_warehouses, ) @@ -1129,6 +1166,7 @@ def get_available_item_locations_for_serialized_item( def get_available_item_locations_for_batched_item( item_code, from_warehouses, + company, consider_rejected_warehouses=False, ): locations = [] @@ -1138,6 +1176,7 @@ def get_available_item_locations_for_batched_item( "item_code": item_code, "warehouse": from_warehouses, "based_on": frappe.db.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"), + "company": company, } ) ) diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 6e8c7abec7c..4d3c6b45a6a 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -211,6 +211,7 @@ class TestPickList(FrappeTestCase): "qty": 1000, "stock_qty": 1000, "conversion_factor": 1, + "warehouse": "_Test Warehouse - _TC", "sales_order": so.name, "sales_order_item": so.items[0].name, } @@ -268,6 +269,119 @@ class TestPickList(FrappeTestCase): pr1.cancel() pr2.cancel() + def test_pick_list_warehouse_for_batched_item(self): + """ + Test that pick list respects company based warehouse assignment for batched items. + + This test verifies that when creating a pick list for a batched item, + the system correctly identifies and assigns the appropriate warehouse + based on the company. + """ + from erpnext.stock.doctype.batch.test_batch import make_new_batch + + batch_company = frappe.get_doc( + {"doctype": "Company", "company_name": "Batch Company", "default_currency": "INR"} + ) + batch_company.insert() + + batch_warehouse = frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": "Batch Warehouse", + "company": batch_company.name, + } + ) + batch_warehouse.insert() + + batch_item = frappe.db.exists("Item", "Batch Warehouse Item") + if not batch_item: + batch_item = create_item("Batch Warehouse Item") + batch_item.has_batch_no = 1 + batch_item.create_new_batch = 1 + batch_item.save() + else: + batch_item = frappe.get_doc("Item", "Batch Warehouse Item") + + batch_no = make_new_batch(item_code=batch_item.name, batch_id="B-WH-ITEM-001") + + make_stock_entry( + item_code=batch_item.name, + qty=5, + company=batch_company.name, + to_warehouse=batch_warehouse.name, + batch_no=batch_no.name, + rate=100.0, + ) + make_stock_entry( + item_code=batch_item.name, + qty=5, + to_warehouse="_Test Warehouse - _TC", + batch_no=batch_no.name, + rate=100.0, + ) + + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": batch_company.name, + "purpose": "Material Transfer", + "locations": [ + { + "item_code": batch_item.name, + "qty": 10, + "stock_qty": 10, + "conversion_factor": 1, + } + ], + } + ) + + pick_list.set_item_locations() + self.assertEqual(len(pick_list.locations), 1) + self.assertEqual(pick_list.locations[0].qty, 5) + self.assertEqual(pick_list.locations[0].batch_no, batch_no.name) + self.assertEqual(pick_list.locations[0].warehouse, batch_warehouse.name) + + def test_pick_list_warehouse_validation(self): + """check if the warehouse validations are triggered""" + from erpnext.stock.doctype.pick_list.pick_list import ( + IncorrectWarehouseValidationError, + MissingWarehouseValidationError, + ) + + warehouse_item = create_item("Warehouse Item") + temp_company = frappe.get_doc( + {"doctype": "Company", "company_name": "Temp Company", "default_currency": "INR"} + ).insert() + temp_warehouse = frappe.get_doc( + {"doctype": "Warehouse", "warehouse_name": "Temp Warehouse", "company": temp_company.name} + ).insert() + + make_stock_entry(item_code=warehouse_item.name, qty=10, rate=100.0, to_warehouse=temp_warehouse.name) + + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": temp_company.name, + "purpose": "Material Transfer", + "pick_manually": 1, + "locations": [ + { + "item_code": warehouse_item.name, + "qty": 5, + "stock_qty": 5, + "conversion_factor": 1, + } + ], + } + ) + + self.assertRaises(MissingWarehouseValidationError, pick_list.insert) + pick_list.locations[0].warehouse = "_Test Warehouse - _TC" + self.assertRaises(IncorrectWarehouseValidationError, pick_list.insert) + pick_list.locations[0].warehouse = temp_warehouse.name + pick_list.insert() + def test_pick_list_for_batched_and_serialised_item(self): # check if oldest batch no and serial nos are picked item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"}) diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json index a33123e3e16..adac858acac 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json @@ -62,6 +62,7 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Warehouse", + "mandatory_depends_on": "eval: parent.pick_manually", "options": "Warehouse", "read_only": 1 }, @@ -283,7 +284,7 @@ ], "istable": 1, "links": [], - "modified": "2025-09-23 00:02:57.817040", + "modified": "2026-03-17 16:25:10.358013", "modified_by": "Administrator", "module": "Stock", "name": "Pick List Item", diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index b5618bda08e..c3423a5cb59 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -2524,26 +2524,38 @@ def get_reserved_batches_for_pos(kwargs) -> dict: """Returns a dict of `Batch No` followed by the `Qty` reserved in POS Invoices.""" pos_batches = frappe._dict() - pos_invoices = frappe.get_all( - "POS Invoice", - fields=[ - "`tabPOS Invoice Item`.batch_no", - "`tabPOS Invoice Item`.qty", - "`tabPOS Invoice`.is_return", - "`tabPOS Invoice Item`.warehouse", - "`tabPOS Invoice Item`.name as child_docname", - "`tabPOS Invoice`.name as parent_docname", - "`tabPOS Invoice Item`.use_serial_batch_fields", - "`tabPOS Invoice Item`.serial_and_batch_bundle", - ], - filters=[ - ["POS Invoice", "consolidated_invoice", "is", "not set"], - ["POS Invoice", "docstatus", "=", 1], - ["POS Invoice Item", "item_code", "=", kwargs.item_code], - ["POS Invoice", "name", "not in", kwargs.ignore_voucher_nos], - ], + POS_Invoice = frappe.qb.DocType("POS Invoice") + POS_Invoice_Item = frappe.qb.DocType("POS Invoice Item") + + pos_invoices = ( + frappe.qb.from_(POS_Invoice) + .inner_join(POS_Invoice_Item) + .on(POS_Invoice.name == POS_Invoice_Item.parent) + .select( + POS_Invoice_Item.batch_no, + POS_Invoice_Item.qty, + POS_Invoice.is_return, + POS_Invoice_Item.warehouse, + POS_Invoice_Item.name.as_("child_docname"), + POS_Invoice.name.as_("parent_docname"), + POS_Invoice_Item.use_serial_batch_fields, + POS_Invoice_Item.serial_and_batch_bundle, + ) + .where( + (POS_Invoice.consolidated_invoice.isnull()) + & (POS_Invoice.docstatus == 1) + & (POS_Invoice_Item.item_code == kwargs.item_code) + ) ) + if kwargs.get("company"): + pos_invoices = pos_invoices.where(POS_Invoice.company == kwargs.get("company")) + + if kwargs.get("ignore_voucher_nos"): + pos_invoices = pos_invoices.where(POS_Invoice.name.notin(kwargs.get("ignore_voucher_nos"))) + + pos_invoices = pos_invoices.run(as_dict=True) + ids = [ pos_invoice.serial_and_batch_bundle for pos_invoice in pos_invoices @@ -2607,6 +2619,9 @@ def get_reserved_batches_for_sre(kwargs) -> dict: .groupby(sb_entry.batch_no, sre.warehouse) ) + if kwargs.get("company"): + query = query.where(sre.company == kwargs.get("company")) + if kwargs.batch_no: if isinstance(kwargs.batch_no, list): query = query.where(sb_entry.batch_no.isin(kwargs.batch_no)) @@ -2769,6 +2784,9 @@ def get_available_batches(kwargs): .groupby(batch_ledger.batch_no, batch_ledger.warehouse) ) + if kwargs.get("company"): + query = query.where(stock_ledger_entry.company == kwargs.get("company")) + if not kwargs.get("for_stock_levels"): query = query.where((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull())) @@ -2886,6 +2904,9 @@ def get_picked_batches(kwargs) -> dict[str, dict]: ) ) + if kwargs.get("company"): + query = query.where(table.company == kwargs.get("company")) + if kwargs.get("item_code"): query = query.where(table.item_code == kwargs.get("item_code")) @@ -3085,6 +3106,9 @@ def get_stock_ledgers_batches(kwargs): .groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse) ) + if kwargs.get("company"): + query = query.where(stock_ledger_entry.company == kwargs.get("company")) + for field in ["warehouse", "item_code", "batch_no"]: if not kwargs.get(field): continue From e17b5dfe61bcb8fec45c4e91658ad70cf26f6de7 Mon Sep 17 00:00:00 2001 From: sudarshan-g Date: Mon, 9 Mar 2026 13:32:59 +0530 Subject: [PATCH 03/30] feat: add cost center field to the stock entry accounting dimension tab (cherry picked from commit 47772f4e7763161d01c6987207327cbf4d428107) # Conflicts: # erpnext/stock/doctype/stock_entry/stock_entry.json --- .../stock/doctype/stock_entry/stock_entry.js | 4 +- .../doctype/stock_entry/stock_entry.json | 52 +++++++++++++++++++ .../stock/doctype/stock_entry/stock_entry.py | 1 + 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index b1a1cdece37..5e9a749d672 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -514,7 +514,9 @@ frappe.ui.form.on("Stock Entry", { frm.fields_dict.items.grid.refresh(); frm.cscript.toggle_related_fields(frm.doc); }, - + cost_center(frm, cdt, cdn) { + erpnext.utils.copy_value_in_all_rows(frm.doc, cdt, cdn, "items", "cost_center"); + }, validate_purpose_consumption: function (frm) { frappe .call({ diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 023dca5bdf2..315893e2129 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -70,6 +70,8 @@ "address_display", "accounting_dimensions_section", "project", + "column_break_wgvc", + "cost_center", "other_info_tab", "printing_settings", "select_print_heading", @@ -699,6 +701,52 @@ "fieldtype": "Data", "is_virtual": 1, "label": "Last Scanned Warehouse" +<<<<<<< HEAD +======= + }, + { + "default": "0", + "fieldname": "is_additional_transfer_entry", + "fieldtype": "Check", + "hidden": 1, + "label": "Is Additional Transfer Entry", + "read_only": 1 + }, + { + "depends_on": "subcontracting_inward_order", + "fieldname": "subcontracting_inward_order", + "fieldtype": "Link", + "label": "Subcontracting Inward Order", + "options": "Subcontracting Inward Order", + "read_only": 1 + }, + { + "fieldname": "reference_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_jabv", + "fieldtype": "Column Break" + }, + { + "fieldname": "reference_details_section", + "fieldtype": "Section Break", + "label": "Reference" + }, + { + "fieldname": "column_break_qpvo", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_wgvc", + "fieldtype": "Column Break" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" +>>>>>>> 47772f4e77 (feat: add cost center field to the stock entry accounting dimension tab) } ], "icon": "fa fa-file-text", @@ -706,7 +754,11 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2025-08-04 19:21:03.338958", +======= + "modified": "2026-03-04 19:03:23.426082", +>>>>>>> 47772f4e77 (feat: add cost center field to the stock entry accounting dimension tab) "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 24aad0ddf79..7819189f082 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -103,6 +103,7 @@ class StockEntry(StockController): asset_repair: DF.Link | None bom_no: DF.Link | None company: DF.Link + cost_center: DF.Link | None credit_note: DF.Link | None delivery_note_no: DF.Link | None fg_completed_qty: DF.Float From 4c2dba98da25fc0f5227502b1c5cd8c537bbb5c0 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 18 Mar 2026 16:32:56 +0530 Subject: [PATCH 04/30] chore: fix conflicts Removed several fields related to additional transfer entries and subcontracting inward orders from the stock entry JSON. --- .../doctype/stock_entry/stock_entry.json | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 315893e2129..f37a2785252 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -701,41 +701,6 @@ "fieldtype": "Data", "is_virtual": 1, "label": "Last Scanned Warehouse" -<<<<<<< HEAD -======= - }, - { - "default": "0", - "fieldname": "is_additional_transfer_entry", - "fieldtype": "Check", - "hidden": 1, - "label": "Is Additional Transfer Entry", - "read_only": 1 - }, - { - "depends_on": "subcontracting_inward_order", - "fieldname": "subcontracting_inward_order", - "fieldtype": "Link", - "label": "Subcontracting Inward Order", - "options": "Subcontracting Inward Order", - "read_only": 1 - }, - { - "fieldname": "reference_section", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_jabv", - "fieldtype": "Column Break" - }, - { - "fieldname": "reference_details_section", - "fieldtype": "Section Break", - "label": "Reference" - }, - { - "fieldname": "column_break_qpvo", - "fieldtype": "Column Break" }, { "fieldname": "column_break_wgvc", @@ -746,7 +711,6 @@ "fieldtype": "Link", "label": "Cost Center", "options": "Cost Center" ->>>>>>> 47772f4e77 (feat: add cost center field to the stock entry accounting dimension tab) } ], "icon": "fa fa-file-text", @@ -754,11 +718,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], -<<<<<<< HEAD - "modified": "2025-08-04 19:21:03.338958", -======= "modified": "2026-03-04 19:03:23.426082", ->>>>>>> 47772f4e77 (feat: add cost center field to the stock entry accounting dimension tab) "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", From 7acd4358352ccf34f807687c0d51ea1219024502 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 18 Mar 2026 18:49:30 +0530 Subject: [PATCH 05/30] fix: incorrect sle calculation when doc has project (#53599) (cherry picked from commit 6cb6a52ded9addd3e0e54b0b59ebd6d34d357b4a) --- erpnext/stock/report/stock_ledger/stock_ledger.py | 1 + erpnext/stock/stock_ledger.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index b9dadfbb62d..45af9474d1f 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -686,6 +686,7 @@ def get_opening_balance(filters, columns, sl_entries, inv_dimension_wise_value=N "posting_time": "00:00:00", "project": project, }, + for_report=True, ) # check if any SLEs are actually Opening Stock Reconciliation diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 5134bd29116..e6a42d28b15 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1766,7 +1766,7 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc return sle[0] if sle else frappe._dict() -def get_previous_sle(args, for_update=False, extra_cond=None): +def get_previous_sle(args, for_update=False, extra_cond=None, for_report=False): """ get the last sle on or before the current time-bucket, to get actual qty before transaction, this function @@ -1782,7 +1782,7 @@ def get_previous_sle(args, for_update=False, extra_cond=None): """ args["name"] = args.get("sle", None) or "" sle = get_stock_ledger_entries( - args, "<=", "desc", "limit 1", for_update=for_update, extra_cond=extra_cond + args, "<=", "desc", "limit 1", for_update=for_update, extra_cond=extra_cond, for_report=for_report ) return sle and sle[0] or {} @@ -1796,6 +1796,7 @@ def get_stock_ledger_entries( debug=False, check_serial_no=True, extra_cond=None, + for_report=False, ): """get stock ledger entries filtered by specific posting datetime conditions""" conditions = f" and posting_datetime {operator} %(posting_datetime)s" @@ -1850,7 +1851,7 @@ def get_stock_ledger_entries( if extra_cond: conditions += f"{extra_cond}" - if previous_sle.get("project"): + if for_report and previous_sle.get("project"): conditions += " and project = %(project)s" # nosemgrep From e0f1e757f3f6002e5c08ac8d05da24f9de935228 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 04:38:20 +0000 Subject: [PATCH 06/30] fix: check posting_date in args (backport #53303) (#53611) Co-authored-by: Vishnu Priya Baskaran <145791817+ervishnucs@users.noreply.github.com> fix: check posting_date in args (#53303) --- erpnext/accounts/doctype/pricing_rule/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index c6875b53ed8..86a1adcb994 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -243,8 +243,10 @@ def get_other_conditions(conditions, values, args): if group_condition: conditions += " and " + group_condition - date = args.get("transaction_date") or frappe.get_value( - args.get("doctype"), args.get("name"), "posting_date", ignore=True + date = ( + args.get("transaction_date") + or args.get("posting_date") + or frappe.get_value(args.get("doctype"), args.get("name"), "posting_date", ignore=True) ) if date: conditions += """ and %(transaction_date)s between ifnull(`tabPricing Rule`.valid_from, '2000-01-01') From 562f93e75c743ec8f01a0f39b46842173f31fdc0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 04:41:10 +0000 Subject: [PATCH 07/30] fix: ignore cost center (backport #53063) (#53613) Co-authored-by: Sowmya <106989392+SowmyaArunachalam@users.noreply.github.com> fix: ignore cost center (#53063) --- .../doctype/sales_invoice/sales_invoice.py | 2 +- .../sales_invoice/test_sales_invoice.py | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 96eca4f52cd..8616379c56d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2516,7 +2516,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): "doctype": target_doctype, "postprocess": update_details, "set_target_warehouse": "set_from_warehouse", - "field_no_map": ["taxes_and_charges", "set_warehouse", "shipping_address"], + "field_no_map": ["taxes_and_charges", "set_warehouse", "shipping_address", "cost_center"], }, doctype + " Item": item_field_map, }, diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index f9b6ab4f07c..c515322348a 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -4835,6 +4835,33 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(stock_ledger_entry.incoming_rate, 0.0) + def test_inter_company_transaction_cost_center(self): + si = create_sales_invoice( + company="Wind Power LLC", + customer="_Test Internal Customer", + debit_to="Debtors - WP", + warehouse="Stores - WP", + income_account="Sales - WP", + expense_account="Cost of Goods Sold - WP", + parent_cost_center="Main - WP", + cost_center="Main - WP", + currency="USD", + do_not_save=1, + ) + + si.selling_price_list = "_Test Price List Rest of the World" + si.submit() + + cost_center = frappe.db.get_value("Company", "_Test Company 1", "cost_center") + frappe.db.set_value("Company", "_Test Company 1", "cost_center", None) + + target_doc = make_inter_company_transaction("Sales Invoice", si.name) + + self.assertEqual(target_doc.cost_center, None) + self.assertEqual(target_doc.items[0].cost_center, None) + + frappe.db.set_value("Company", "_Test Company 1", "cost_center", cost_center) + def make_item_for_si(item_code, properties=None): from erpnext.stock.doctype.item.test_item import make_item From 5a3bc27e2cc9948a1bf39aec7be949bea5114da7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 05:13:36 +0000 Subject: [PATCH 08/30] fix: python error in manufacture entry if transfer against is job card (backport #53615) (#53617) Co-authored-by: Mihir Kandoi fix: python error in manufacture entry if transfer against is job card (#53615) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 7819189f082..5bb50d079af 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2137,7 +2137,7 @@ class StockEntry(StockController): self.to_warehouse if self.purpose == "Send to Subcontractor" else "" ) - if original_item != item.get("item_code"): + if isinstance(original_item, str) and original_item != item.get("item_code"): item["original_item"] = original_item self.add_to_stock_entry_detail(item_dict) From af86fd3cb4230ada1b3e2a9370be0218caac20eb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:52:44 +0530 Subject: [PATCH 09/30] fix: consider returned qty in subcontracting report (backport #53616) (#53620) Co-authored-by: Mihir Kandoi fix: consider returned qty in subcontracting report (#53616) --- .../subcontracted_raw_materials_to_be_transferred.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py index ef28eda62a5..4526bbf1703 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py @@ -41,6 +41,7 @@ def get_columns(filters): "fieldname": "transferred_qty", "width": 200, }, + {"label": _("Returned Quantity"), "fieldtype": "Float", "fieldname": "returned_qty", "width": 150}, {"label": _("Pending Quantity"), "fieldtype": "Float", "fieldname": "p_qty", "width": 150}, ] @@ -50,7 +51,7 @@ def get_data(filters): data = [] for row in order_rm_item_details: - transferred_qty = row.get("transferred_qty") or 0 + transferred_qty = (row.get("transferred_qty") or 0) - (row.get("returned_qty") or 0) if transferred_qty < row.get("reqd_qty", 0): pending_qty = frappe.utils.flt(row.get("reqd_qty", 0) - transferred_qty) row.p_qty = pending_qty if pending_qty > 0 else 0 @@ -86,6 +87,7 @@ def get_order_items_to_supply(filters): f"`tab{supplied_items_table}`.rm_item_code as rm_item_code", f"`tab{supplied_items_table}`.required_qty as reqd_qty", f"`tab{supplied_items_table}`.supplied_qty as transferred_qty", + f"`tab{supplied_items_table}`.returned_qty as returned_qty", ], filters=record_filters, ) From 4f39dfd642eae780f084facc8809285d7c60a470 Mon Sep 17 00:00:00 2001 From: Sakthivel Murugan S <129778327+ssakthivelmurugan@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:04:31 +0530 Subject: [PATCH 10/30] fix: set customer details on customer creation at login (#53509) (cherry picked from commit 256d267a3b94369303927130b8da3f179ef0e79c) # Conflicts: # erpnext/portal/utils.py --- erpnext/portal/utils.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/erpnext/portal/utils.py b/erpnext/portal/utils.py index 903d4a6196c..14c6abe2f4b 100644 --- a/erpnext/portal/utils.py +++ b/erpnext/portal/utils.py @@ -47,8 +47,8 @@ def create_customer_or_supplier(): if party_exists(doctype, user): return - party = frappe.new_doc(doctype) fullname = frappe.utils.get_fullname(user) +<<<<<<< HEAD if not doctype == "Customer": party.update( @@ -62,6 +62,9 @@ def create_customer_or_supplier(): party.flags.ignore_mandatory = True party.insert(ignore_permissions=True) +======= + party = create_party(doctype, fullname) +>>>>>>> 256d267a3b (fix: set customer details on customer creation at login (#53509)) alternate_doctype = "Customer" if doctype == "Supplier" else "Supplier" if party_exists(alternate_doctype, user): @@ -69,6 +72,22 @@ def create_customer_or_supplier(): fullname += "-" + doctype create_party_contact(doctype, fullname, user, party.name) + return party + + +def create_party(doctype, fullname): + party = frappe.new_doc(doctype) + # Can't set parent party as group + + party.update( + { + f"{doctype.lower()}_name": fullname, + f"{doctype.lower()}_type": "Individual", + } + ) + + party.flags.ignore_mandatory = True + party.insert(ignore_permissions=True) return party From 9c243e8dd0fb09dd72ee88543824c7a0022bfb52 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 06:50:54 +0000 Subject: [PATCH 11/30] refactor: remove test file import in stock ageing report (backport #53619) (#53625) Co-authored-by: Mihir Kandoi --- erpnext/stock/report/stock_ageing/stock_ageing.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 46f655c8b11..e0c2bf2bcfc 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -243,10 +243,7 @@ class FIFOSlots: consumed/updated and maintained via FIFO. ** } """ - - from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import ( - get_serial_nos_from_bundle, - ) + from erpnext.stock.serial_batch_bundle import get_serial_nos_from_bundle stock_ledger_entries = self.sle @@ -271,7 +268,7 @@ class FIFOSlots: if bundle_wise_serial_nos: serial_nos = bundle_wise_serial_nos.get(d.serial_and_batch_bundle) or [] else: - serial_nos = get_serial_nos_from_bundle(d.serial_and_batch_bundle) or [] + serial_nos = sorted(get_serial_nos_from_bundle(d.serial_and_batch_bundle)) or [] serial_nos = self.uppercase_serial_nos(serial_nos) if d.actual_qty > 0: From e2c8dc538659c2bb9b1ee66f7cd67047ab7f9a11 Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Thu, 19 Mar 2026 13:06:16 +0530 Subject: [PATCH 12/30] chore: resolve conflict --- erpnext/portal/utils.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/erpnext/portal/utils.py b/erpnext/portal/utils.py index 14c6abe2f4b..cfdf8e18191 100644 --- a/erpnext/portal/utils.py +++ b/erpnext/portal/utils.py @@ -48,23 +48,7 @@ def create_customer_or_supplier(): return fullname = frappe.utils.get_fullname(user) -<<<<<<< HEAD - - if not doctype == "Customer": - party.update( - { - "supplier_name": fullname, - "supplier_group": "All Supplier Groups", - "supplier_type": "Individual", - } - ) - - party.flags.ignore_mandatory = True - party.insert(ignore_permissions=True) - -======= party = create_party(doctype, fullname) ->>>>>>> 256d267a3b (fix: set customer details on customer creation at login (#53509)) alternate_doctype = "Customer" if doctype == "Supplier" else "Supplier" if party_exists(alternate_doctype, user): From 6ea3d569725f3a0f389a6cbaad6a34d840ad8cd8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 07:39:30 +0000 Subject: [PATCH 13/30] fix(stock): fix email error message (backport #53606) (#53632) Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com> fix(stock): fix email error message (#53606) --- erpnext/stock/reorder_item.py | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py index 570dc3a3405..1f527e7071a 100644 --- a/erpnext/stock/reorder_item.py +++ b/erpnext/stock/reorder_item.py @@ -7,7 +7,7 @@ from math import ceil import frappe from frappe import _ -from frappe.utils import add_days, cint, flt, nowdate +from frappe.utils import add_days, cint, escape_html, flt, nowdate import erpnext @@ -219,15 +219,6 @@ def create_material_request(material_requests): mr_list = [] exceptions_list = [] - def _log_exception(mr): - if frappe.local.message_log: - exceptions_list.extend(frappe.local.message_log) - frappe.local.message_log = [] - else: - exceptions_list.append(frappe.get_traceback(with_context=True)) - - mr.log_error("Unable to create material request") - company_wise_mr = frappe._dict({}) for request_type in material_requests: for company in material_requests[request_type]: @@ -297,8 +288,9 @@ def create_material_request(material_requests): company_wise_mr.setdefault(company, []).append(mr) - except Exception: - _log_exception(mr) + except Exception as exception: + exceptions_list.append(exception) + mr.log_error("Unable to create material request") if company_wise_mr: if getattr(frappe.local, "reorder_email_notify", None) is None: @@ -383,10 +375,7 @@ def notify_errors(exceptions_list): for exception in exceptions_list: try: - exception = json.loads(exception) - error_message = """
{}

""".format( - _(exception.get("message")) - ) + error_message = f"
{escape_html(str(exception))}

" content += error_message except Exception: pass From f8ab56ecc96ae7c28b9f7b8e79488ff2c47cc810 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Thu, 19 Mar 2026 14:18:48 +0530 Subject: [PATCH 14/30] fix(sales_invoice): using `msgprint` and removed condition checking for `is_created_using_pos` to refetch payment methods (#53636) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 8616379c56d..6bc24633ca9 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2745,7 +2745,7 @@ def update_multi_mode_option(doc, pos_profile): payment.account = payment_mode.default_account payment.type = payment_mode.type - mop_refetched = bool(doc.payments) and not doc.is_created_using_pos + mop_refetched = bool(doc.payments) doc.set("payments", []) invalid_modes = [] @@ -2769,9 +2769,8 @@ def update_multi_mode_option(doc, pos_profile): frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account")) if mop_refetched: - frappe.toast( - _("Payment methods refreshed. Please review before proceeding."), - indicator="orange", + frappe.msgprint( + _("Payment methods refreshed. Please review before proceeding."), indicator="orange", alert=True ) From 7707a79d4481292465da217c3851545f05f2eebe Mon Sep 17 00:00:00 2001 From: nishkagosalia Date: Thu, 19 Mar 2026 16:21:40 +0530 Subject: [PATCH 15/30] fix: Adding validation for operation time in BOM (cherry picked from commit 7f70e62c30d3939b975b40f956bbeccc81a16fa9) # Conflicts: # erpnext/manufacturing/doctype/job_card/test_job_card.py --- erpnext/manufacturing/doctype/bom/bom.py | 6 + erpnext/manufacturing/doctype/bom/test_bom.py | 9 + .../doctype/job_card/test_job_card.py | 377 ++++++++++++++++++ 3 files changed, 392 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index a6fad4ee1aa..510d4f511dd 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1019,6 +1019,12 @@ class BOM(WebsiteGenerator): "Row {0}: Workstation or Workstation Type is mandatory for an operation {1}" ).format(d.idx, d.operation) ) + if not d.time_in_mins or d.time_in_mins <= 0: + frappe.throw( + _("Row {0}: Operation time should be greater than 0 for operation {1}").format( + d.idx, d.operation + ) + ) def get_tree_representation(self) -> BOMTree: """Get a complete tree representation preserving order of child items.""" diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 83e722cce50..6dc2a82ff1a 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -132,6 +132,15 @@ class TestBOM(FrappeTestCase): self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost) self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost) + @timeout + def test_bom_no_operation_time_validation(self): + bom = frappe.copy_doc(self.globalTestRecords["BOM"][2]) + bom.docstatus = 0 + for op_row in bom.operations: + op_row.time_in_mins = 0 + + self.assertRaises(frappe.ValidationError, bom.save) + @timeout def test_bom_cost_with_batch_size(self): bom = frappe.copy_doc(test_records[2]) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 12205a80a2b..fb4a62ff8b8 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -55,8 +55,82 @@ class TestJobCard(FrappeTestCase): basic_rate=100, ) +<<<<<<< HEAD def tearDown(self): frappe.db.rollback() +======= + def test_quality_inspection_mandatory_check(self): + from erpnext.manufacturing.doctype.operation.test_operation import make_operation + + raw = create_item("Fabric-Raw") + cut_fg = create_item("Cut-Fabric-SFG") + stitch_fg = create_item("Stitched-TShirt-SFG") + final = create_item("Finished-TShirt") + + row = {"operation": "Cutting", "workstation": "_Test Workstation 1"} + + cutting = make_operation(row) + stitching = make_operation({"operation": "Stitching", "workstation": "_Test Workstation 1"}) + ironing = make_operation({"operation": "Ironing", "workstation": "_Test Workstation 1"}) + + cut_bom = create_semi_fg_bom(cut_fg.name, raw.name, inspection_required=1) + stitch_bom = create_semi_fg_bom(stitch_fg.name, cut_fg.name, inspection_required=0) + final_bom = frappe.new_doc( + "BOM", + item=final.name, + quantity=1, + with_operations=1, + track_semi_finished_goods=1, + company="_Test Company", + ) + final_bom.append("items", {"item_code": raw.name, "qty": 1}) + final_bom.append( + "operations", + { + "operation": cutting.name, + "workstation": "_Test Workstation 1", + "bom_no": cut_bom, + "skip_material_transfer": 1, + "time_in_mins": 60, + }, + ) + final_bom.append( + "operations", + { + "operation": stitching.name, + "workstation": "_Test Workstation 1", + "bom_no": stitch_bom, + "skip_material_transfer": 1, + "time_in_mins": 60, + }, + ) + final_bom.append( + "operations", + { + "operation": ironing.name, + "workstation": "_Test Workstation 1", + "bom_no": final_bom.name, + "is_final_finished_good": 1, + "skip_material_transfer": 1, + "time_in_mins": 60, + }, + ) + final_bom.append("items", {"item_code": stitch_fg.name, "qty": 1, "operation_row_id": 3}) + final_bom.insert() + final_bom.submit() + work_order = make_work_order(final_bom.name, final.name, 1, variant_items=[], use_multi_level_bom=0) + work_order.company = "_Test Company" + work_order.wip_warehouse = "Work In Progress - _TC" + work_order.fg_warehouse = "Finished Goods - _TC" + work_order.scrap_warehouse = "All Warehouses - _TC" + for operation in work_order.operations: + operation.time_in_mins = 60 + + work_order.submit() + job_card = frappe.get_all("Job Card", filters={"work_order": work_order.name, "operation": "Cutting"}) + job_card_doc = frappe.get_doc("Job Card", job_card[0].name) + self.assertRaises(frappe.ValidationError, job_card_doc.submit) +>>>>>>> 7f70e62c30 (fix: Adding validation for operation time in BOM) def test_job_card_operations(self): job_cards = frappe.get_all( @@ -697,6 +771,309 @@ class TestJobCard(FrappeTestCase): self.assertEqual(wo_doc.process_loss_qty, 2) self.assertEqual(wo_doc.status, "Completed") +<<<<<<< HEAD +======= + def test_op_cost_calculation(self): + from erpnext.manufacturing.doctype.routing.test_routing import ( + create_routing, + setup_bom, + setup_operations, + ) + from erpnext.manufacturing.doctype.work_order.work_order import make_job_card + from erpnext.manufacturing.doctype.work_order.work_order import ( + make_stock_entry as make_stock_entry_for_wo, + ) + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + make_workstation(workstation_name="Test Workstation Z", hour_rate_rent=240) + operations = [ + {"operation": "Test Operation A1", "workstation": "Test Workstation Z", "time_in_mins": 30}, + ] + + warehouse = create_warehouse("Test Warehouse 123 for Job Card") + setup_operations(operations) + + item_code = "Test Job Card Process Qty Item" + for item in [item_code, item_code + "RM 1", item_code + "RM 2"]: + if not frappe.db.exists("Item", item): + make_item( + item, + { + "item_name": item, + "stock_uom": "Nos", + "is_stock_item": 1, + }, + ) + + routing_doc = create_routing(routing_name="Testing Route", operations=operations) + bom_doc = setup_bom( + item_code=item_code, + routing=routing_doc.name, + raw_materials=[item_code + "RM 1", item_code + "RM 2"], + source_warehouse=warehouse, + ) + + for row in bom_doc.items: + make_stock_entry( + item_code=row.item_code, + target=row.source_warehouse, + qty=10, + basic_rate=100, + ) + + wo_doc = make_wo_order_test_record( + production_item=item_code, + bom_no=bom_doc.name, + qty=10, + skip_transfer=1, + wip_warehouse=warehouse, + source_warehouse=warehouse, + ) + + first_job_card = frappe.get_all( + "Job Card", + filters={"work_order": wo_doc.name, "sequence_id": 1}, + fields=["name"], + order_by="sequence_id", + limit=1, + )[0].name + + jc = frappe.get_doc("Job Card", first_job_card) + for _ in jc.scheduled_time_logs: + jc.append( + "time_logs", + { + "from_time": now(), + "to_time": add_to_date(now(), minutes=1), + "completed_qty": 4, + }, + ) + jc.for_quantity = 4 + jc.save() + jc.submit() + + s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 4)) + s.submit() + + self.assertEqual(s.additional_costs[0].amount, 4) + + make_job_card( + wo_doc.name, + [ + { + "name": wo_doc.operations[0].name, + "operation": "Test Operation A1", + "qty": 6, + "pending_qty": 6, + } + ], + ) + + job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name}) + job_card.append( + "time_logs", + { + "from_time": add_to_date(now(), hours=1), + "to_time": add_to_date(now(), hours=1, minutes=2), + "completed_qty": 6, + }, + ) + job_card.for_quantity = 6 + job_card.save() + job_card.submit() + + s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 6)) + self.assertEqual(s.additional_costs[0].amount, 8) + + def test_co_by_product_for_sfg_flow(self): + from erpnext.manufacturing.doctype.operation.test_operation import make_operation + + frappe.db.set_value("UOM", "Nos", "must_be_whole_number", 0) + + def create_bom(raw_material, finished_good, scrap_item, submit=True): + bom = frappe.new_doc("BOM") + bom.company = "_Test Company" + bom.item = finished_good + bom.quantity = 1 + bom.append("items", {"item_code": raw_material, "qty": 1}) + bom.append( + "secondary_items", + { + "item_code": scrap_item, + "qty": 1, + "process_loss_per": 10, + "cost_allocation_per": 5, + "type": "Scrap", + }, + ) + if submit: + bom.insert() + bom.submit() + + return bom + + rm1 = create_item("RM 1") + scrap1 = create_item("Scrap 1") + sfg = create_item("SFG 1") + sfg_bom = create_bom(rm1.name, sfg.name, scrap1.name) + + rm2 = create_item("RM 2") + fg1 = create_item("FG 1") + scrap2 = create_item("Scrap 2") + scrap_extra = create_item("Scrap Extra") + fg_bom = create_bom(rm2.name, fg1.name, scrap2.name, submit=False) + fg_bom.with_operations = 1 + fg_bom.track_semi_finished_goods = 1 + + operation1 = { + "operation": "Test Operation A", + "workstation": "_Test Workstation A", + "finished_good": sfg.name, + "bom_no": sfg_bom.name, + "finished_good_qty": 1, + "sequence_id": 1, + "time_in_mins": 60, + } + operation2 = { + "operation": "Test Operation B", + "workstation": "_Test Workstation A", + "finished_good": fg1.name, + "bom_no": fg_bom.name, + "finished_good_qty": 1, + "is_final_finished_good": 1, + "sequence_id": 2, + "time_in_mins": 60, + } + + make_workstation(operation1) + make_operation(operation1) + make_operation(operation2) + + fg_bom.append("operations", operation1) + fg_bom.append("operations", operation2) + fg_bom.append("items", {"item_code": sfg.name, "qty": 1, "uom": "Nos", "operation_row_id": 2}) + fg_bom.insert() + fg_bom.save() + fg_bom.submit() + + work_order = make_wo_order_test_record( + item=fg1.name, + qty=10, + source_warehouse="Stores - _TC", + fg_warehouse="Finished Goods - _TC", + bom_no=fg_bom.name, + skip_transfer=1, + do_not_save=True, + ) + + work_order.operations[0].time_in_mins = 60 + work_order.operations[1].time_in_mins = 60 + work_order.save() + work_order.submit() + + job_card = frappe.get_doc( + "Job Card", + frappe.db.get_value( + "Job Card", {"work_order": work_order.name, "operation": "Test Operation A"}, "name" + ), + ) + job_card.append( + "time_logs", + { + "from_time": "2009-01-01 12:06:25", + "to_time": "2009-01-01 12:37:25", + "completed_qty": job_card.for_quantity, + }, + ) + job_card.append( + "secondary_items", {"item_code": scrap_extra.name, "stock_qty": 5, "type": "Co-Product"} + ) + job_card.submit() + + for row in sfg_bom.items: + make_stock_entry( + item_code=row.item_code, + target="Stores - _TC", + qty=10, + basic_rate=100, + ) + + manufacturing_entry = frappe.get_doc(job_card.make_stock_entry_for_semi_fg_item()) + manufacturing_entry.submit() + + self.assertEqual(manufacturing_entry.items[2].item_code, scrap1.name) + self.assertEqual(manufacturing_entry.items[2].qty, 9) + self.assertEqual(flt(manufacturing_entry.items[2].basic_rate, 3), 5.556) + self.assertEqual(manufacturing_entry.items[3].item_code, scrap_extra.name) + self.assertEqual(manufacturing_entry.items[3].type, "Co-Product") + self.assertEqual(manufacturing_entry.items[3].qty, 5) + self.assertEqual(manufacturing_entry.items[3].basic_rate, 0) + + job_card = frappe.get_doc( + "Job Card", + frappe.db.get_value( + "Job Card", {"work_order": work_order.name, "operation": "Test Operation B"}, "name" + ), + ) + job_card.append( + "time_logs", + { + "from_time": "2009-02-01 12:06:25", + "to_time": "2009-02-01 12:37:25", + "completed_qty": job_card.for_quantity, + }, + ) + job_card.submit() + + for row in fg_bom.items: + make_stock_entry( + item_code=row.item_code, + target="Stores - _TC", + qty=10, + basic_rate=100, + ) + + manufacturing_entry = frappe.get_doc(job_card.make_stock_entry_for_semi_fg_item()) + manufacturing_entry.submit() + + self.assertEqual(manufacturing_entry.items[2].item_code, scrap2.name) + self.assertEqual(manufacturing_entry.items[2].qty, 9) + self.assertEqual(flt(manufacturing_entry.items[2].basic_rate, 3), 5.556) + + def test_secondary_items_without_sfg(self): + for row in frappe.get_doc("BOM", self.work_order.bom_no).items: + make_stock_entry( + item_code=row.item_code, + target="_Test Warehouse - _TC", + qty=10, + basic_rate=100, + ) + + job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name}) + job_card.append("secondary_items", {"item_code": "_Test Item", "stock_qty": 2, "type": "Scrap"}) + job_card.append( + "time_logs", + { + "from_time": "2009-01-01 12:06:25", + "to_time": "2009-01-01 12:37:25", + "completed_qty": job_card.for_quantity, + }, + ) + job_card.save() + job_card.submit() + + from erpnext.manufacturing.doctype.work_order.work_order import ( + make_stock_entry as make_stock_entry_for_wo, + ) + + s = frappe.get_doc(make_stock_entry_for_wo(self.work_order.name, "Manufacture")) + s.submit() + + self.assertEqual(s.items[3].item_code, "_Test Item") + self.assertEqual(s.items[3].transfer_qty, 2) + +>>>>>>> 7f70e62c30 (fix: Adding validation for operation time in BOM) def create_bom_with_multiple_operations(): "Create a BOM with multiple operations and Material Transfer against Job Card" From b3f0e2a00dc5a54902c515c102eb50feb1a37cde Mon Sep 17 00:00:00 2001 From: Nishka Gosalia Date: Thu, 19 Mar 2026 18:40:29 +0530 Subject: [PATCH 16/30] fix: merge conflicts --- erpnext/manufacturing/doctype/bom/test_bom.py | 2 +- .../doctype/job_card/test_job_card.py | 377 ------------------ 2 files changed, 1 insertion(+), 378 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 6dc2a82ff1a..cc942d59c74 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -134,7 +134,7 @@ class TestBOM(FrappeTestCase): @timeout def test_bom_no_operation_time_validation(self): - bom = frappe.copy_doc(self.globalTestRecords["BOM"][2]) + bom = frappe.copy_doc(test_records[2]) bom.docstatus = 0 for op_row in bom.operations: op_row.time_in_mins = 0 diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index fb4a62ff8b8..12205a80a2b 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -55,82 +55,8 @@ class TestJobCard(FrappeTestCase): basic_rate=100, ) -<<<<<<< HEAD def tearDown(self): frappe.db.rollback() -======= - def test_quality_inspection_mandatory_check(self): - from erpnext.manufacturing.doctype.operation.test_operation import make_operation - - raw = create_item("Fabric-Raw") - cut_fg = create_item("Cut-Fabric-SFG") - stitch_fg = create_item("Stitched-TShirt-SFG") - final = create_item("Finished-TShirt") - - row = {"operation": "Cutting", "workstation": "_Test Workstation 1"} - - cutting = make_operation(row) - stitching = make_operation({"operation": "Stitching", "workstation": "_Test Workstation 1"}) - ironing = make_operation({"operation": "Ironing", "workstation": "_Test Workstation 1"}) - - cut_bom = create_semi_fg_bom(cut_fg.name, raw.name, inspection_required=1) - stitch_bom = create_semi_fg_bom(stitch_fg.name, cut_fg.name, inspection_required=0) - final_bom = frappe.new_doc( - "BOM", - item=final.name, - quantity=1, - with_operations=1, - track_semi_finished_goods=1, - company="_Test Company", - ) - final_bom.append("items", {"item_code": raw.name, "qty": 1}) - final_bom.append( - "operations", - { - "operation": cutting.name, - "workstation": "_Test Workstation 1", - "bom_no": cut_bom, - "skip_material_transfer": 1, - "time_in_mins": 60, - }, - ) - final_bom.append( - "operations", - { - "operation": stitching.name, - "workstation": "_Test Workstation 1", - "bom_no": stitch_bom, - "skip_material_transfer": 1, - "time_in_mins": 60, - }, - ) - final_bom.append( - "operations", - { - "operation": ironing.name, - "workstation": "_Test Workstation 1", - "bom_no": final_bom.name, - "is_final_finished_good": 1, - "skip_material_transfer": 1, - "time_in_mins": 60, - }, - ) - final_bom.append("items", {"item_code": stitch_fg.name, "qty": 1, "operation_row_id": 3}) - final_bom.insert() - final_bom.submit() - work_order = make_work_order(final_bom.name, final.name, 1, variant_items=[], use_multi_level_bom=0) - work_order.company = "_Test Company" - work_order.wip_warehouse = "Work In Progress - _TC" - work_order.fg_warehouse = "Finished Goods - _TC" - work_order.scrap_warehouse = "All Warehouses - _TC" - for operation in work_order.operations: - operation.time_in_mins = 60 - - work_order.submit() - job_card = frappe.get_all("Job Card", filters={"work_order": work_order.name, "operation": "Cutting"}) - job_card_doc = frappe.get_doc("Job Card", job_card[0].name) - self.assertRaises(frappe.ValidationError, job_card_doc.submit) ->>>>>>> 7f70e62c30 (fix: Adding validation for operation time in BOM) def test_job_card_operations(self): job_cards = frappe.get_all( @@ -771,309 +697,6 @@ class TestJobCard(FrappeTestCase): self.assertEqual(wo_doc.process_loss_qty, 2) self.assertEqual(wo_doc.status, "Completed") -<<<<<<< HEAD -======= - def test_op_cost_calculation(self): - from erpnext.manufacturing.doctype.routing.test_routing import ( - create_routing, - setup_bom, - setup_operations, - ) - from erpnext.manufacturing.doctype.work_order.work_order import make_job_card - from erpnext.manufacturing.doctype.work_order.work_order import ( - make_stock_entry as make_stock_entry_for_wo, - ) - from erpnext.stock.doctype.item.test_item import make_item - from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse - - make_workstation(workstation_name="Test Workstation Z", hour_rate_rent=240) - operations = [ - {"operation": "Test Operation A1", "workstation": "Test Workstation Z", "time_in_mins": 30}, - ] - - warehouse = create_warehouse("Test Warehouse 123 for Job Card") - setup_operations(operations) - - item_code = "Test Job Card Process Qty Item" - for item in [item_code, item_code + "RM 1", item_code + "RM 2"]: - if not frappe.db.exists("Item", item): - make_item( - item, - { - "item_name": item, - "stock_uom": "Nos", - "is_stock_item": 1, - }, - ) - - routing_doc = create_routing(routing_name="Testing Route", operations=operations) - bom_doc = setup_bom( - item_code=item_code, - routing=routing_doc.name, - raw_materials=[item_code + "RM 1", item_code + "RM 2"], - source_warehouse=warehouse, - ) - - for row in bom_doc.items: - make_stock_entry( - item_code=row.item_code, - target=row.source_warehouse, - qty=10, - basic_rate=100, - ) - - wo_doc = make_wo_order_test_record( - production_item=item_code, - bom_no=bom_doc.name, - qty=10, - skip_transfer=1, - wip_warehouse=warehouse, - source_warehouse=warehouse, - ) - - first_job_card = frappe.get_all( - "Job Card", - filters={"work_order": wo_doc.name, "sequence_id": 1}, - fields=["name"], - order_by="sequence_id", - limit=1, - )[0].name - - jc = frappe.get_doc("Job Card", first_job_card) - for _ in jc.scheduled_time_logs: - jc.append( - "time_logs", - { - "from_time": now(), - "to_time": add_to_date(now(), minutes=1), - "completed_qty": 4, - }, - ) - jc.for_quantity = 4 - jc.save() - jc.submit() - - s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 4)) - s.submit() - - self.assertEqual(s.additional_costs[0].amount, 4) - - make_job_card( - wo_doc.name, - [ - { - "name": wo_doc.operations[0].name, - "operation": "Test Operation A1", - "qty": 6, - "pending_qty": 6, - } - ], - ) - - job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name}) - job_card.append( - "time_logs", - { - "from_time": add_to_date(now(), hours=1), - "to_time": add_to_date(now(), hours=1, minutes=2), - "completed_qty": 6, - }, - ) - job_card.for_quantity = 6 - job_card.save() - job_card.submit() - - s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 6)) - self.assertEqual(s.additional_costs[0].amount, 8) - - def test_co_by_product_for_sfg_flow(self): - from erpnext.manufacturing.doctype.operation.test_operation import make_operation - - frappe.db.set_value("UOM", "Nos", "must_be_whole_number", 0) - - def create_bom(raw_material, finished_good, scrap_item, submit=True): - bom = frappe.new_doc("BOM") - bom.company = "_Test Company" - bom.item = finished_good - bom.quantity = 1 - bom.append("items", {"item_code": raw_material, "qty": 1}) - bom.append( - "secondary_items", - { - "item_code": scrap_item, - "qty": 1, - "process_loss_per": 10, - "cost_allocation_per": 5, - "type": "Scrap", - }, - ) - if submit: - bom.insert() - bom.submit() - - return bom - - rm1 = create_item("RM 1") - scrap1 = create_item("Scrap 1") - sfg = create_item("SFG 1") - sfg_bom = create_bom(rm1.name, sfg.name, scrap1.name) - - rm2 = create_item("RM 2") - fg1 = create_item("FG 1") - scrap2 = create_item("Scrap 2") - scrap_extra = create_item("Scrap Extra") - fg_bom = create_bom(rm2.name, fg1.name, scrap2.name, submit=False) - fg_bom.with_operations = 1 - fg_bom.track_semi_finished_goods = 1 - - operation1 = { - "operation": "Test Operation A", - "workstation": "_Test Workstation A", - "finished_good": sfg.name, - "bom_no": sfg_bom.name, - "finished_good_qty": 1, - "sequence_id": 1, - "time_in_mins": 60, - } - operation2 = { - "operation": "Test Operation B", - "workstation": "_Test Workstation A", - "finished_good": fg1.name, - "bom_no": fg_bom.name, - "finished_good_qty": 1, - "is_final_finished_good": 1, - "sequence_id": 2, - "time_in_mins": 60, - } - - make_workstation(operation1) - make_operation(operation1) - make_operation(operation2) - - fg_bom.append("operations", operation1) - fg_bom.append("operations", operation2) - fg_bom.append("items", {"item_code": sfg.name, "qty": 1, "uom": "Nos", "operation_row_id": 2}) - fg_bom.insert() - fg_bom.save() - fg_bom.submit() - - work_order = make_wo_order_test_record( - item=fg1.name, - qty=10, - source_warehouse="Stores - _TC", - fg_warehouse="Finished Goods - _TC", - bom_no=fg_bom.name, - skip_transfer=1, - do_not_save=True, - ) - - work_order.operations[0].time_in_mins = 60 - work_order.operations[1].time_in_mins = 60 - work_order.save() - work_order.submit() - - job_card = frappe.get_doc( - "Job Card", - frappe.db.get_value( - "Job Card", {"work_order": work_order.name, "operation": "Test Operation A"}, "name" - ), - ) - job_card.append( - "time_logs", - { - "from_time": "2009-01-01 12:06:25", - "to_time": "2009-01-01 12:37:25", - "completed_qty": job_card.for_quantity, - }, - ) - job_card.append( - "secondary_items", {"item_code": scrap_extra.name, "stock_qty": 5, "type": "Co-Product"} - ) - job_card.submit() - - for row in sfg_bom.items: - make_stock_entry( - item_code=row.item_code, - target="Stores - _TC", - qty=10, - basic_rate=100, - ) - - manufacturing_entry = frappe.get_doc(job_card.make_stock_entry_for_semi_fg_item()) - manufacturing_entry.submit() - - self.assertEqual(manufacturing_entry.items[2].item_code, scrap1.name) - self.assertEqual(manufacturing_entry.items[2].qty, 9) - self.assertEqual(flt(manufacturing_entry.items[2].basic_rate, 3), 5.556) - self.assertEqual(manufacturing_entry.items[3].item_code, scrap_extra.name) - self.assertEqual(manufacturing_entry.items[3].type, "Co-Product") - self.assertEqual(manufacturing_entry.items[3].qty, 5) - self.assertEqual(manufacturing_entry.items[3].basic_rate, 0) - - job_card = frappe.get_doc( - "Job Card", - frappe.db.get_value( - "Job Card", {"work_order": work_order.name, "operation": "Test Operation B"}, "name" - ), - ) - job_card.append( - "time_logs", - { - "from_time": "2009-02-01 12:06:25", - "to_time": "2009-02-01 12:37:25", - "completed_qty": job_card.for_quantity, - }, - ) - job_card.submit() - - for row in fg_bom.items: - make_stock_entry( - item_code=row.item_code, - target="Stores - _TC", - qty=10, - basic_rate=100, - ) - - manufacturing_entry = frappe.get_doc(job_card.make_stock_entry_for_semi_fg_item()) - manufacturing_entry.submit() - - self.assertEqual(manufacturing_entry.items[2].item_code, scrap2.name) - self.assertEqual(manufacturing_entry.items[2].qty, 9) - self.assertEqual(flt(manufacturing_entry.items[2].basic_rate, 3), 5.556) - - def test_secondary_items_without_sfg(self): - for row in frappe.get_doc("BOM", self.work_order.bom_no).items: - make_stock_entry( - item_code=row.item_code, - target="_Test Warehouse - _TC", - qty=10, - basic_rate=100, - ) - - job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name}) - job_card.append("secondary_items", {"item_code": "_Test Item", "stock_qty": 2, "type": "Scrap"}) - job_card.append( - "time_logs", - { - "from_time": "2009-01-01 12:06:25", - "to_time": "2009-01-01 12:37:25", - "completed_qty": job_card.for_quantity, - }, - ) - job_card.save() - job_card.submit() - - from erpnext.manufacturing.doctype.work_order.work_order import ( - make_stock_entry as make_stock_entry_for_wo, - ) - - s = frappe.get_doc(make_stock_entry_for_wo(self.work_order.name, "Manufacture")) - s.submit() - - self.assertEqual(s.items[3].item_code, "_Test Item") - self.assertEqual(s.items[3].transfer_qty, 2) - ->>>>>>> 7f70e62c30 (fix: Adding validation for operation time in BOM) def create_bom_with_multiple_operations(): "Create a BOM with multiple operations and Material Transfer against Job Card" From defa1d4a766f3fbfc86982885ba4def73a9bba32 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:49:53 +0530 Subject: [PATCH 17/30] fix: validate permission before updating status (backport #53651) (#53652) * fix: validate permission before updating status (#53651) (cherry picked from commit 8e17c722fbb2c685d215d7daf309eb856282050c) # Conflicts: # erpnext/buying/doctype/purchase_order/purchase_order.py # erpnext/selling/doctype/sales_order/sales_order.py # erpnext/stock/doctype/purchase_receipt/purchase_receipt.py * chore: resolve conflicts --------- Co-authored-by: diptanilsaha --- erpnext/buying/doctype/purchase_order/purchase_order.py | 2 ++ erpnext/selling/doctype/sales_order/sales_order.py | 2 ++ erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 5d1513df9b2..815ffd2a609 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -912,6 +912,8 @@ def get_list_context(context=None): @frappe.whitelist() def update_status(status, name): + frappe.has_permission("Purchase Order", "write", name, throw=True) + po = frappe.get_doc("Purchase Order", name) po.update_status(status) po.update_delivered_qty_in_sales_order() diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index dbd7f406432..68fa73fd0e4 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1681,6 +1681,8 @@ def make_work_orders(items, sales_order, company, project=None): @frappe.whitelist() def update_status(status, name): + frappe.has_permission("Sales Order", "write", name, throw=True) + so = frappe.get_doc("Sales Order", name) so.update_status(status) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index b2e2e7dac84..7e9036f51e7 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1414,6 +1414,8 @@ def make_purchase_return(source_name, target_doc=None): @frappe.whitelist() def update_purchase_receipt_status(docname, status): + frappe.has_permission("Purchase Receipt", "write", docname, throw=True) + pr = frappe.get_doc("Purchase Receipt", docname) pr.update_status(status) From 90e4f9026db8eef72ba9657f345cf938435c9df4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:41:39 +0000 Subject: [PATCH 18/30] fix: do not overwrite expense account in stock entry (backport #53658) (#53660) * fix: do not overwrite expense account in stock entry (#53658) (cherry picked from commit fa35fbdb8ed47203f7da167364c0b725c6f332df) # Conflicts: # erpnext/stock/doctype/stock_entry/stock_entry.py * chore: resolve conflicts --------- Co-authored-by: Mihir Kandoi --- erpnext/stock/doctype/stock_entry/stock_entry.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 5bb50d079af..24e704e07b2 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -590,9 +590,6 @@ class StockEntry(StockController): flt(item.qty) * flt(item.conversion_factor), self.precision("transfer_qty", item) ) - if self.purpose == "Manufacture": - item.set("expense_account", item_details.get("expense_account")) - def validate_fg_completed_qty(self): item_wise_qty = {} if self.purpose == "Manufacture" and self.work_order: From d96590c4d916aaa317fd10af07115ea3d58d9f5e Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Fri, 20 Mar 2026 15:33:36 +0530 Subject: [PATCH 19/30] fix(budget-variance-report): validate 'budget_against' filter (backport #53079) (#53663) --- .../budget_variance_report.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index db42d23a839..f4cb3083961 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -8,6 +8,7 @@ import frappe from frappe import _ from frappe.utils import flt, formatdate +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions from erpnext.controllers.trends import get_period_date_ranges, get_period_month_ranges @@ -15,6 +16,8 @@ def execute(filters=None): if not filters: filters = {} + validate_filters(filters) + columns = get_columns(filters) if filters.get("budget_against_filter"): dimensions = filters.get("budget_against_filter") @@ -35,6 +38,21 @@ def execute(filters=None): return columns, data, None, chart +def validate_filters(filters): + validate_budget_dimensions(filters) + + +def validate_budget_dimensions(filters): + dimensions = [d.get("document_type") for d in get_dimensions(with_cost_center_and_project=True)[0]] + if filters.get("budget_against") and filters.get("budget_against") not in dimensions: + frappe.throw( + title=_("Invalid Accounting Dimension"), + msg=_("{0} is not a valid Accounting Dimension.").format( + frappe.bold(filters.get("budget_against")) + ), + ) + + def get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation): for account, monthwise_data in dimension_items.items(): row = [dimension, account] From 540a8540d6d05195da100e71219225ad0bc6a3bd Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 19 Mar 2026 14:13:56 +0530 Subject: [PATCH 20/30] fix: deadlock issue for SLE (cherry picked from commit f48b03c6ece7916c107ff80e64cf837b986d6f0d) --- erpnext/stock/stock_ledger.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index e6a42d28b15..d50d7fc5dba 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -2058,7 +2058,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): where item_code = %(item_code)s and warehouse = %(warehouse)s - and voucher_no != %(voucher_no)s and is_cancelled = 0 and ( posting_datetime > %(posting_datetime)s From 461bc1733f41de570a4663b0804eb1301d1a346e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 22 Mar 2026 12:06:43 +0530 Subject: [PATCH 21/30] fix: stock queue for SABB (cherry picked from commit 3fcf308ed80b3f5a1f1fb5a258fa252fe7b10cb0) --- .../serial_and_batch_bundle/serial_and_batch_bundle.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index c3423a5cb59..4de6ebc6a00 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -748,11 +748,16 @@ class SerialandBatchBundle(Document): precision = frappe.get_precision("Serial and Batch Entry", "incoming_rate") for d in self.entries: + fifo_batch_wise_val = True + if valuation_method == "FIFO" and d.batch_no in batches: + fifo_batch_wise_val = False + if self.is_rejected and not set_valuation_rate_for_rejected_materials: rate = 0.0 elif ( (flt(d.incoming_rate, precision) == flt(rate, precision)) and not stock_queue + and fifo_batch_wise_val and d.qty and d.stock_value_difference ): From f5bd85b4dc6d9a25f4a29d0652c894e051a217f2 Mon Sep 17 00:00:00 2001 From: nishkagosalia Date: Thu, 19 Mar 2026 18:28:21 +0530 Subject: [PATCH 22/30] chore: Adding new argument in status updater to skip qty validation (cherry picked from commit dcd05090893a25c50026d5bcf67f91c84fd43737) --- erpnext/controllers/status_updater.py | 4 +-- .../purchase_receipt/purchase_receipt.py | 1 + .../purchase_receipt/test_purchase_receipt.py | 35 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 290d8eb5d4b..c28a8ff44fc 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -234,8 +234,8 @@ class StatusUpdater(Document): self.global_amount_allowance = None for args in self.status_updater: - if "target_ref_field" not in args: - # if target_ref_field is not specified, the programmer does not want to validate qty / amount + if "target_ref_field" not in args or args.get("validate_qty") is False: + # if target_ref_field is not specified or validate_qty is explicitly set to False, skip validation continue # get unique transactions to update diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 7e9036f51e7..fb6b94a7090 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -185,6 +185,7 @@ class PurchaseReceipt(BuyingController): "target_ref_field": "stock_qty", "source_field": "stock_qty", "percent_join_field": "material_request", + "validate_qty": False, }, { "source_dt": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 2070b264f8f..d2e0397a7ce 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -15,6 +15,7 @@ from erpnext.controllers.accounts_controller import InvalidQtyError from erpnext.controllers.buying_controller import QtyMismatchError from erpnext.stock import get_warehouse_account_map from erpnext.stock.doctype.item.test_item import create_item, make_item +from erpnext.stock.doctype.material_request.material_request import make_purchase_order from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( SerialNoDuplicateError, @@ -33,6 +34,40 @@ class TestPurchaseReceipt(FrappeTestCase): def setUp(self): frappe.db.set_single_value("Buying Settings", "allow_multiple_items", 1) + def test_purchase_receipt_skips_validation(self): + """ + Test that validation is skipped when over delivery receipt allowance is reduced after PO submission + and PR can be submitted with higher qty than MR. + """ + item = create_item("Test item for validation") + mr = frappe.new_doc("Material Request") + mr.material_request_type = "Purchase" + mr.company = "_Test Company" + mr.price_list = "_Test Price List" + mr.append( + "items", + { + "item_code": item.name, + "item_name": item.item_name, + "item_group": item.item_group, + "schedule_date": add_days(today(), 1), + "qty": 100, + "uom": item.stock_uom, + }, + ) + mr.insert() + mr.submit() + frappe.db.set_value("Item", item.name, "over_delivery_receipt_allowance", 200) + po = make_purchase_order(mr.name) + po.supplier = "_Test Supplier" + po.items[0].qty = 300 + po.save() + po.submit() + frappe.db.set_value("Item", item.name, "over_delivery_receipt_allowance", 20) + pr = make_purchase_receipt(qty=300, item_code=item.name, do_not_save=True) + pr.save() + pr.submit() + def test_purchase_receipt_qty(self): pr = make_purchase_receipt(qty=0, rejected_qty=0, do_not_save=True) with self.assertRaises(InvalidQtyError): From 5e767ea595f88af566b5d0d6a6afc028a1c59705 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:38:32 +0530 Subject: [PATCH 23/30] fix: shipping rule applied twice on non stock items (backport #53655) (#53686) Co-authored-by: Mihir Kandoi fix: shipping rule applied twice on non stock items (#53655) --- .../accounts/doctype/shipping_rule/shipping_rule.py | 4 +++- erpnext/controllers/accounts_controller.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py index 0c4fe4a4855..abbb6a58119 100644 --- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py +++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py @@ -152,7 +152,9 @@ class ShippingRule(Document): frappe.throw(_("Shipping rule only applicable for Buying")) shipping_charge["doctype"] = "Purchase Taxes and Charges" - shipping_charge["category"] = "Valuation and Total" + shipping_charge["category"] = ( + "Valuation and Total" if doc.get_stock_items() or doc.get_asset_items() else "Total" + ) shipping_charge["add_deduct_tax"] = "Add" existing_shipping_charge = doc.get("taxes", filters=shipping_charge) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index ec2b5caf9d2..ae2fae8051d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2297,6 +2297,16 @@ class AccountsController(TransactionBase): return stock_items + def get_asset_items(self): + asset_items = [] + item_codes = list(set(item.item_code for item in self.get("items"))) + if item_codes: + asset_items = frappe.db.get_values( + "Item", {"name": ["in", item_codes], "is_fixed_asset": 1}, pluck="name", cache=True + ) + + return asset_items + def calculate_total_advance_from_ledger(self): adv = frappe.qb.DocType("Advance Payment Ledger Entry") return ( From 974755b224e43b1db088b35c0fb280c1d1b4e8e4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:42:03 +0530 Subject: [PATCH 24/30] fix(trends): added validation for `period_based_on` filter (backport #53690) (#53691) Co-authored-by: diptanilsaha fix(trends): added validation for `period_based_on` filter (#53690) --- erpnext/controllers/trends.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/controllers/trends.py b/erpnext/controllers/trends.py index 476bde248cc..bc4e2b346d4 100644 --- a/erpnext/controllers/trends.py +++ b/erpnext/controllers/trends.py @@ -55,6 +55,14 @@ def validate_filters(filters): if filters.get("based_on") == filters.get("group_by"): frappe.throw(_("'Based On' and 'Group By' can not be same")) + if filters.get("period_based_on") and filters.period_based_on not in ["bill_date", "posting_date"]: + frappe.throw( + msg=_("{0} can be either {1} or {2}.").format( + frappe.bold("Period based On"), frappe.bold("Posting Date"), frappe.bold("Billing Date") + ), + title=_("Invalid Filter"), + ) + def get_data(filters, conditions): data = [] From 04d74ad6eb09707274036cedda57c8ef6066c467 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:30:20 +0000 Subject: [PATCH 25/30] fix: PO should not be required for internal transfers (backport #53681) (#53683) * fix: PO should not be required for internal transfers (#53681) (cherry picked from commit 51541024687bff5cca2891cc0837084aeb104e26) # Conflicts: # erpnext/stock/doctype/purchase_receipt/purchase_receipt.py * chore: resolve conflicts * chore: resolve conflicts * chore: resolve conflicts --------- Co-authored-by: Mihir Kandoi --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index fb6b94a7090..d8dbfc2772f 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -328,7 +328,7 @@ class PurchaseReceipt(BuyingController): ) def po_required(self): - if frappe.db.get_value("Buying Settings", None, "po_required") == "Yes": + if frappe.db.get_single_value("Buying Settings", "po_required") == "Yes" and not self.is_internal_transfer(): for d in self.get("items"): if not d.purchase_order: frappe.throw(_("Purchase Order number required for Item {0}").format(d.item_code)) From eaf5494502468056dbfe533fa0b0762ccec02e97 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 23 Mar 2026 15:49:41 +0530 Subject: [PATCH 26/30] chore: linter (#53696) --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index d8dbfc2772f..d011994985c 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -328,7 +328,10 @@ class PurchaseReceipt(BuyingController): ) def po_required(self): - if frappe.db.get_single_value("Buying Settings", "po_required") == "Yes" and not self.is_internal_transfer(): + if ( + frappe.db.get_single_value("Buying Settings", "po_required") == "Yes" + and not self.is_internal_transfer() + ): for d in self.get("items"): if not d.purchase_order: frappe.throw(_("Purchase Order number required for Item {0}").format(d.item_code)) From c0ce34e12cb17c94af0192084897ea2a962a2f4a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:26:29 +0000 Subject: [PATCH 27/30] fix(manufacturing): update non-stock item dict (backport #53689) (#53698) Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com> Co-authored-by: Mihir Kandoi fix(manufacturing): update non-stock item dict (#53689) --- erpnext/manufacturing/doctype/bom/bom.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 510d4f511dd..0fa17d34d51 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1335,9 +1335,10 @@ def add_non_stock_items_cost(stock_entry, work_order, expense_account): bom = frappe.get_doc("BOM", work_order.bom_no) table = "exploded_items" if work_order.get("use_multi_level_bom") else "items" - items = {} + items = frappe._dict() for d in bom.get(table): - items.setdefault(d.item_code, d.amount) + items.setdefault(d.item_code, 0) + items[d.item_code] += flt(d.amount) non_stock_items = frappe.get_all( "Item", From 46e784d0941ab02124f74fc43eb1837cd406e871 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Mon, 23 Mar 2026 16:03:50 +0530 Subject: [PATCH 28/30] fix: check for `submit` permissions instead of `write` permissions when updating status (backport #53697) (#53702) --- erpnext/buying/doctype/purchase_order/purchase_order.py | 2 +- erpnext/selling/doctype/sales_order/sales_order.py | 2 +- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 815ffd2a609..c4427da7135 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -912,7 +912,7 @@ def get_list_context(context=None): @frappe.whitelist() def update_status(status, name): - frappe.has_permission("Purchase Order", "write", name, throw=True) + frappe.has_permission("Purchase Order", "submit", name, throw=True) po = frappe.get_doc("Purchase Order", name) po.update_status(status) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 68fa73fd0e4..85f9e246b04 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1681,7 +1681,7 @@ def make_work_orders(items, sales_order, company, project=None): @frappe.whitelist() def update_status(status, name): - frappe.has_permission("Sales Order", "write", name, throw=True) + frappe.has_permission("Sales Order", "submit", name, throw=True) so = frappe.get_doc("Sales Order", name) so.update_status(status) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index d011994985c..e9291e10baf 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1418,7 +1418,7 @@ def make_purchase_return(source_name, target_doc=None): @frappe.whitelist() def update_purchase_receipt_status(docname, status): - frappe.has_permission("Purchase Receipt", "write", docname, throw=True) + frappe.has_permission("Purchase Receipt", "submit", docname, throw=True) pr = frappe.get_doc("Purchase Receipt", docname) pr.update_status(status) From 32c0532dec0244cf6ac28fdbda49bdbe137e1754 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 23 Mar 2026 16:29:47 +0530 Subject: [PATCH 29/30] fix: batch validation for subcontracting receipt (cherry picked from commit b8d201658ac805858b5be8838cd1bcf952dfd90b) --- erpnext/controllers/subcontracting_controller.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 66109479d8c..c32edfaa7b2 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -989,6 +989,12 @@ class SubcontractingController(StockController): if self.doctype not in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]: return + if ( + frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on") + == "BOM" + ): + return + for row in self.get(self.raw_material_table): key = (row.rm_item_code, row.main_item_code, row.get(self.subcontract_data.order_field)) if not self.__transferred_items or not self.__transferred_items.get(key): From 119195c6fa7f2eb20cae13cc18c78d2115189b44 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 18:05:54 +0530 Subject: [PATCH 30/30] fix: initialize all tax columns to resolve Key error in `item_wise_sales_register` and `item_wise_purchase_register` reports (backport #53323) (#53583) Co-authored-by: Lakshit Jain fix: initialize all tax columns to resolve Key error in `item_wise_sales_register` and `item_wise_purchase_register` reports (#53323) --- .../item_wise_purchase_register.py | 6 ++ .../item_wise_sales_register.py | 6 ++ .../test_item_wise_sales_register.py | 66 ++++++++++++++++++- 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index 1ed08d79cc6..c4269beda85 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -31,6 +31,7 @@ def _execute(filters=None, additional_table_columns=None): item_list = get_items(filters, additional_table_columns) aii_account_map = get_aii_accounts() + default_taxes = {} if item_list: itemised_tax, tax_columns = get_tax_accounts( item_list, @@ -39,6 +40,9 @@ def _execute(filters=None, additional_table_columns=None): doctype="Purchase Invoice", tax_doctype="Purchase Taxes and Charges", ) + for tax in tax_columns: + default_taxes[f"{tax}_rate"] = 0 + default_taxes[f"{tax}_amount"] = 0 po_pr_map = get_purchase_receipts_against_purchase_order(item_list) @@ -85,6 +89,8 @@ def _execute(filters=None, additional_table_columns=None): } total_tax = 0 + row.update(default_taxes.copy()) + for tax in tax_columns: item_tax = itemised_tax.get(d.name, {}).get(tax, {}) row.update( diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index 78aa90ab947..aa431372f75 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -29,8 +29,12 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions= company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency") item_list = get_items(filters, additional_table_columns, additional_conditions) + default_taxes = {} if item_list: itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency) + for tax in tax_columns: + default_taxes[f"{tax}_rate"] = 0 + default_taxes[f"{tax}_amount"] = 0 mode_of_payments = get_mode_of_payments(set(d.parent for d in item_list)) so_dn_map = get_delivery_notes_against_sales_order(item_list) @@ -88,6 +92,8 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions= total_tax = 0 total_other_charges = 0 + row.update(default_taxes.copy()) + for tax in tax_columns: item_tax = itemised_tax.get(d.name, {}).get(tax, {}) row.update( diff --git a/erpnext/accounts/report/item_wise_sales_register/test_item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/test_item_wise_sales_register.py index 4dfdf3058e4..7d074f338dd 100644 --- a/erpnext/accounts/report/item_wise_sales_register/test_item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/test_item_wise_sales_register.py @@ -17,9 +17,11 @@ class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase): def tearDown(self): frappe.db.rollback() - def create_sales_invoice(self, do_not_submit=False): + def create_sales_invoice(self, item=None, taxes=None, do_not_submit=False): si = create_sales_invoice( - item=self.item, + item=item or self.item, + item_name=item or self.item, + description=item or self.item, company=self.company, customer=self.customer, debit_to=self.debit_to, @@ -30,6 +32,19 @@ class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase): price_list_rate=100, do_not_save=1, ) + + for tax in taxes or []: + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": tax["account_head"], + "cost_center": self.cost_center, + "description": tax["description"], + "rate": tax["rate"], + }, + ) + si = si.save() if not do_not_submit: si = si.submit() @@ -63,3 +78,50 @@ class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase): report_output = {k: v for k, v in report[1][0].items() if k in expected_result} self.assertDictEqual(report_output, expected_result) + + def test_grouped_report_handles_different_tax_descriptions(self): + self.create_item(item_name="_Test Item Tax Description A") + first_item = self.item + self.create_item(item_name="_Test Item Tax Description B") + second_item = self.item + + first_tax_description = "Tax Description A" + second_tax_description = "Tax Description B" + first_tax_amount_field = f"{frappe.scrub(first_tax_description)}_amount" + second_tax_amount_field = f"{frappe.scrub(second_tax_description)}_amount" + + self.create_sales_invoice( + item=first_item, + taxes=[ + { + "account_head": "_Test Account VAT - _TC", + "description": first_tax_description, + "rate": 5, + } + ], + ) + self.create_sales_invoice( + item=second_item, + taxes=[ + { + "account_head": "_Test Account Service Tax - _TC", + "description": second_tax_description, + "rate": 2, + } + ], + ) + + filters = frappe._dict( + { + "from_date": today(), + "to_date": today(), + "company": self.company, + "group_by": "Customer", + } + ) + _, data, _, _, _, _ = execute(filters) + + grand_total_row = next(row for row in data if row.get("bold") and row.get("item_code") == "Total") + + self.assertEqual(grand_total_row[first_tax_amount_field], 5.0) + self.assertEqual(grand_total_row[second_tax_amount_field], 2.0)