From 273029d0f0367b58fd9f7bcc49fdfcc2b78dad4d Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Wed, 28 Jan 2026 18:51:49 +0530 Subject: [PATCH 01/32] fix: recalculate tax withholding during Purchase Order child update --- erpnext/controllers/accounts_controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f6195e6e09e..e39c2ffe85a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -4054,6 +4054,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.set_payment_schedule() if parent_doctype == "Purchase Order": + parent.set_tax_withholding() parent.validate_minimum_order_qty() parent.validate_budget() if parent.is_against_so(): From b740846b68dd0220b3f59bbfdff0ac048f5ce9c4 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 10 Feb 2026 13:08:35 +0530 Subject: [PATCH 02/32] refactor: update labels for tax withholding reports columns to improve clarity --- .../tax_withholding_details.py | 18 +++++++++--------- .../tds_computation_summary.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py index 78fc08614f2..b6ca8bc60ec 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -252,17 +252,11 @@ def get_columns(filters): "width": 60, }, { - "label": _("Total Amount"), + "label": _("Taxable Amount"), "fieldname": "total_amount", "fieldtype": "Float", "width": 120, }, - { - "label": _("Base Total"), - "fieldname": "base_total", - "fieldtype": "Float", - "width": 120, - }, { "label": _("TDS Amount") if filters.get("party_type") == "Supplier" else _("TCS Amount"), "fieldname": "tax_amount", @@ -270,10 +264,16 @@ def get_columns(filters): "width": 120, }, { - "label": _("Grand Total"), + "label": _("Grand Total (Company Currency)"), + "fieldname": "base_total", + "fieldtype": "Float", + "width": 150, + }, + { + "label": _("Grand Total (Transaction Currency)"), "fieldname": "grand_total", "fieldtype": "Float", - "width": 120, + "width": 170, }, {"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 130}, { diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py index e14d9320fa2..1b5292f6bde 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py @@ -128,7 +128,7 @@ def get_columns(filters): "width": 120, }, { - "label": _("Total Amount"), + "label": _("Total Taxable Amount"), "fieldname": "total_amount", "fieldtype": "Float", "width": 120, From ed42d5498912966a32e8a1999d062c85c4288d1d Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 10 Feb 2026 13:30:12 +0530 Subject: [PATCH 03/32] fix: add base_tax_withholding_net_total to tax withholding report --- .../tax_withholding_details.py | 13 ++++++++++++- .../test_tax_withholding_details.py | 15 ++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py index b6ca8bc60ec..81278211d36 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -51,7 +51,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_ entries = {} for name, details in gle_map.items(): for entry in details: - tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0 + tax_amount, total_amount, grand_total, base_total, base_tax_withholding_net_total = 0, 0, 0, 0, 0 tax_withholding_category, rate = None, None bill_no, bill_date = "", "" party = entry.party or entry.against @@ -93,12 +93,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_ grand_total = values[1] base_total = values[2] + base_tax_withholding_net_total = total_amount if voucher_type == "Purchase Invoice": + base_tax_withholding_net_total = values[0] bill_no = values[3] bill_date = values[4] + else: total_amount += entry.credit + base_tax_withholding_net_total = total_amount if tax_amount: if party_map.get(party, {}).get("party_type") == "Supplier": @@ -125,6 +129,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_ "rate": rate, "total_amount": total_amount, "grand_total": grand_total, + "base_tax_withholding_net_total": base_tax_withholding_net_total, "base_total": base_total, "tax_amount": tax_amount, "transaction_date": posting_date, @@ -251,6 +256,12 @@ def get_columns(filters): "fieldtype": "Percent", "width": 60, }, + { + "label": _("Tax Withholding Net Total"), + "fieldname": "base_tax_withholding_net_total", + "fieldtype": "Float", + "width": 150, + }, { "label": _("Taxable Amount"), "fieldname": "total_amount", diff --git a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py index 853ae71abe3..33666c912b2 100644 --- a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py @@ -35,9 +35,9 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase): result = execute(filters)[1] expected_values = [ # Check for JV totals using back calculation logic - [jv.name, "TCS", 0.075, -10000.0, -7.5, -10000.0], - [pe.name, "TCS", 0.075, 2550, 0.53, 2550.53], - [si.name, "TCS", 0.075, 1000, 0.52, 1000.52], + [jv.name, "TCS", 0.075, -10000.0, -10000.0, -7.5, -10000.0], + [pe.name, "TCS", 0.075, 2550, 2550, 0.53, 2550.53], + [si.name, "TCS", 0.075, 1000, 1000, 0.52, 1000.52], ] self.check_expected_values(result, expected_values) @@ -55,8 +55,8 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase): frappe._dict(company="_Test Company", party_type="Supplier", from_date=today(), to_date=today()) )[1] expected_values = [ - [inv_1.name, "TDS - 1", 10, 5000, 500, 5500], - [inv_2.name, "TDS - 2", 20, 5000, 1000, 6000], + [inv_1.name, "TDS - 1", 10, 5000, 5000, 500, 5500], + [inv_2.name, "TDS - 2", 20, 5000, 5000, 1000, 6000], ] self.check_expected_values(result, expected_values) @@ -107,8 +107,8 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase): )[1] expected_values = [ - [inv_1.name, "TDS - 3", 10.0, 5000, 500, 4500], - [inv_2.name, "TDS - 3", 20.0, 5000, 1000, 4000], + [inv_1.name, "TDS - 3", 10.0, 5000, 5000, 500, 4500], + [inv_2.name, "TDS - 3", 20.0, 5000, 5000, 1000, 4000], ] self.check_expected_values(result, expected_values) @@ -120,6 +120,7 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase): voucher.ref_no, voucher.section_code, voucher.rate, + voucher.base_tax_withholding_net_total, voucher.base_total, voucher.tax_amount, voucher.grand_total, From 68099a9b5c5be97036d42b11c7cd6e97b16fa69f Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 10 Feb 2026 13:53:31 +0530 Subject: [PATCH 04/32] fix: set base_tax_withholding_net_total for jv in tds report --- .../report/tax_withholding_details/tax_withholding_details.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py index 81278211d36..9fb40938d59 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -83,6 +83,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_ # back calculate total amount from rate and tax_amount base_total = min(flt(tax_amount / (rate / 100), precision=precision), values[0]) total_amount = grand_total = base_total + base_tax_withholding_net_total = total_amount else: if tax_amount and rate: From e57f3fe727e52d3879bd4dfdbd8d9e0228c7fd7c Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 10 Feb 2026 14:40:53 +0530 Subject: [PATCH 05/32] test: update expected values for tax withholding calculations in tests --- .../tax_withholding_details/test_tax_withholding_details.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py index 33666c912b2..56dba9d86d3 100644 --- a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py @@ -36,8 +36,8 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase): expected_values = [ # Check for JV totals using back calculation logic [jv.name, "TCS", 0.075, -10000.0, -10000.0, -7.5, -10000.0], - [pe.name, "TCS", 0.075, 2550, 2550, 0.53, 2550.53], - [si.name, "TCS", 0.075, 1000, 1000, 0.52, 1000.52], + [pe.name, "TCS", 0.075, 706.67, 2550.0, 0.53, 2550.53], + [si.name, "TCS", 0.075, 693.33, 1000.0, 0.52, 1000.52], ] self.check_expected_values(result, expected_values) From 8bdbb24d73deddd87cc46b9b18a0107959b3526b Mon Sep 17 00:00:00 2001 From: Dharanidharan S Date: Tue, 27 Jan 2026 14:08:16 +0530 Subject: [PATCH 06/32] fix(accounts): correct base grand total and rounded total mismatch (#51739) (cherry picked from commit d82c92a237dcc7512709897f997fa1097e3820c4) # Conflicts: # erpnext/public/js/controllers/taxes_and_totals.js --- .../public/js/controllers/taxes_and_totals.js | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index de0a7ab51bc..30c4807743d 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -598,9 +598,20 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { ? this.frm.doc["taxes"][tax_count - 1].total + grand_total_diff : this.frm.doc.net_total); - if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) { - this.frm.doc.base_grand_total = (this.frm.doc.total_taxes_and_charges) ? - flt(this.frm.doc.grand_total * this.frm.doc.conversion_rate) : this.frm.doc.base_net_total; + // total taxes and charges is calculated before adjusting base grand total + this.frm.doc.total_taxes_and_charges = flt( + this.frm.doc.grand_total - this.frm.doc.net_total - grand_total_diff, + precision("total_taxes_and_charges") + ); + + if ( + ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes( + this.frm.doc.doctype + ) + ) { + this.frm.doc.base_grand_total = this.frm.doc.total_taxes_and_charges + ? flt(this.frm.doc.grand_total * this.frm.doc.conversion_rate) + : this.frm.doc.base_net_total; } else { // other charges added/deducted this.frm.doc.taxes_and_charges_added = this.frm.doc.taxes_and_charges_deducted = 0.0; @@ -626,11 +637,6 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { ["taxes_and_charges_added", "taxes_and_charges_deducted"]); } - this.frm.doc.total_taxes_and_charges = flt(this.frm.doc.grand_total - this.frm.doc.net_total - - grand_total_diff, precision("total_taxes_and_charges")); - - this.set_in_company_currency(this.frm.doc, ["total_taxes_and_charges"]); - // Round grand total as per precision frappe.model.round_floats_in(this.frm.doc, ["grand_total", "base_grand_total"]); From 0a4198718b77f89923349a788b026381880f2412 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Wed, 17 Dec 2025 12:39:34 +0530 Subject: [PATCH 07/32] fix: allow rename for market segment doctype (cherry picked from commit f3142c4af608f893fa5b8adc4c54195c5c8074a8) # Conflicts: # erpnext/crm/doctype/market_segment/market_segment.json --- .../market_segment/market_segment.json | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/market_segment/market_segment.json b/erpnext/crm/doctype/market_segment/market_segment.json index 66cca0ed400..78ddf5722bf 100644 --- a/erpnext/crm/doctype/market_segment/market_segment.json +++ b/erpnext/crm/doctype/market_segment/market_segment.json @@ -1,4 +1,5 @@ { +<<<<<<< HEAD "allow_copy": 0, "allow_events_in_timeline": 0, "allow_guest_to_view": 0, @@ -13,6 +14,18 @@ "document_type": "", "editable_grid": 1, "engine": "InnoDB", +======= + "actions": [], + "allow_rename": 1, + "autoname": "field:market_segment", + "creation": "2018-10-01 09:59:14.479509", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "market_segment" + ], +>>>>>>> f3142c4af6 (fix: allow rename for market segment doctype) "fields": [ { "allow_bulk_edit": 0, @@ -46,6 +59,7 @@ "translatable": 0, "unique": 1 } +<<<<<<< HEAD ], "has_web_view": 0, "hide_heading": 0, @@ -63,6 +77,16 @@ "name": "Market Segment", "name_case": "", "owner": "Administrator", +======= + ], + "links": [], + "modified": "2025-12-17 12:09:34.687368", + "modified_by": "Administrator", + "module": "CRM", + "name": "Market Segment", + "naming_rule": "By fieldname", + "owner": "Administrator", +>>>>>>> f3142c4af6 (fix: allow rename for market segment doctype) "permissions": [ { "amend": 0, @@ -83,6 +107,7 @@ "submit": 0, "write": 1 } +<<<<<<< HEAD ], "quick_entry": 1, "read_only": 0, @@ -93,4 +118,15 @@ "track_changes": 1, "track_seen": 0, "track_views": 0 -} \ No newline at end of file +} +======= + ], + "quick_entry": 1, + "row_format": "Dynamic", + "sort_field": "creation", + "sort_order": "DESC", + "states": [], + "track_changes": 1, + "translated_doctype": 1 +} +>>>>>>> f3142c4af6 (fix: allow rename for market segment doctype) From b977366dcd805c22d93f58eef69838187ec522d8 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Wed, 11 Feb 2026 14:41:01 +0530 Subject: [PATCH 08/32] chore: resolve conflict --- .../market_segment/market_segment.json | 108 ++---------------- 1 file changed, 12 insertions(+), 96 deletions(-) diff --git a/erpnext/crm/doctype/market_segment/market_segment.json b/erpnext/crm/doctype/market_segment/market_segment.json index 78ddf5722bf..85200018848 100644 --- a/erpnext/crm/doctype/market_segment/market_segment.json +++ b/erpnext/crm/doctype/market_segment/market_segment.json @@ -1,20 +1,4 @@ { -<<<<<<< HEAD - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:market_segment", - "beta": 0, - "creation": "2018-10-01 09:59:14.479509", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", -======= "actions": [], "allow_rename": 1, "autoname": "field:market_segment", @@ -25,59 +9,13 @@ "field_order": [ "market_segment" ], ->>>>>>> f3142c4af6 (fix: allow rename for market segment doctype) "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "market_segment", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Market Segment", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "fieldname": "market_segment", + "fieldtype": "Data", + "label": "Market Segment", "unique": 1 } -<<<<<<< HEAD - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-10-01 09:59:14.479509", - "modified_by": "Administrator", - "module": "CRM", - "name": "Market Segment", - "name_case": "", - "owner": "Administrator", -======= ], "links": [], "modified": "2025-12-17 12:09:34.687368", @@ -86,40 +24,19 @@ "name": "Market Segment", "naming_rule": "By fieldname", "owner": "Administrator", ->>>>>>> f3142c4af6 (fix: allow rename for market segment doctype) "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "share": 1, "write": 1 } -<<<<<<< HEAD - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} -======= ], "quick_entry": 1, "row_format": "Dynamic", @@ -129,4 +46,3 @@ "track_changes": 1, "translated_doctype": 1 } ->>>>>>> f3142c4af6 (fix: allow rename for market segment doctype) From 78a3701f4c19a5a322a00758654d19fa7b89d511 Mon Sep 17 00:00:00 2001 From: dharanidharan2813 Date: Fri, 17 Oct 2025 13:16:02 +0530 Subject: [PATCH 09/32] fix: Payment Terms auto-fetched in Sales Invoice even when automatically_fetch_payment_terms is disabled (cherry picked from commit cf1d892d60f9c0622640623066819aa855028cef) # Conflicts: # erpnext/selling/doctype/sales_order/test_sales_order.py --- .../doctype/sales_order/sales_order.py | 1 - .../doctype/sales_order/test_sales_order.py | 38 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index a1047c11a96..832992198b6 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1203,7 +1203,6 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, a "doctype": "Sales Invoice", "field_map": { "party_account_currency": "party_account_currency", - "payment_terms_template": "payment_terms_template", }, "field_no_map": ["payment_terms_template"], "validation": {"docstatus": ["=", 1]}, diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index d38473e4b69..a78367900eb 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -200,10 +200,16 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): so.load_from_db() self.assertEqual(so.per_billed, 0) +<<<<<<< HEAD @change_settings( "Accounts Settings", {"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 1}, ) +======= + @IntegrationTestCase.change_settings( + "Accounts Settings", {"automatically_fetch_payment_terms": 1} + ) # Enable auto fetch +>>>>>>> cf1d892d60 (fix: Payment Terms auto-fetched in Sales Invoice even when automatically_fetch_payment_terms is disabled) def test_make_sales_invoice_with_terms(self): so = make_sales_order(do_not_submit=True) @@ -232,6 +238,38 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): si1 = make_sales_invoice(so.name) self.assertEqual(len(si1.get("items")), 0) + @IntegrationTestCase.change_settings( + "Accounts Settings", {"automatically_fetch_payment_terms": 1} + ) # Enable auto fetch + def test_auto_fetch_terms_enable(self): + so = make_sales_order(do_not_submit=True) + + so.payment_terms_template = "_Test Payment Term Template" + so.save() + so.submit() + + si = make_sales_invoice(so.name) + # Check if payment terms are copied from sales order to sales invoice + self.assertTrue(si.payment_terms_template) + si.insert() + si.submit() + + @IntegrationTestCase.change_settings( + "Accounts Settings", {"automatically_fetch_payment_terms": 0} + ) # Disable auto fetch + def test_auto_fetch_terms_disable(self): + so = make_sales_order(do_not_submit=True) + + so.payment_terms_template = "_Test Payment Term Template" + so.save() + so.submit() + + si = make_sales_invoice(so.name) + # Check if payment terms are not copied from sales order to sales invoice + self.assertFalse(si.payment_terms_template) + si.insert() + si.submit() + def test_update_qty(self): so = make_sales_order() From a503460bd5f2ac907140b79ebc1f60574f7eb94a Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Tue, 2 Dec 2025 12:20:56 +0530 Subject: [PATCH 10/32] chore: resolve conflict --- .../selling/doctype/sales_order/test_sales_order.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index a78367900eb..53832569812 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -200,16 +200,10 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): so.load_from_db() self.assertEqual(so.per_billed, 0) -<<<<<<< HEAD @change_settings( "Accounts Settings", {"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 1}, ) -======= - @IntegrationTestCase.change_settings( - "Accounts Settings", {"automatically_fetch_payment_terms": 1} - ) # Enable auto fetch ->>>>>>> cf1d892d60 (fix: Payment Terms auto-fetched in Sales Invoice even when automatically_fetch_payment_terms is disabled) def test_make_sales_invoice_with_terms(self): so = make_sales_order(do_not_submit=True) @@ -238,7 +232,7 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): si1 = make_sales_invoice(so.name) self.assertEqual(len(si1.get("items")), 0) - @IntegrationTestCase.change_settings( + @change_settings( "Accounts Settings", {"automatically_fetch_payment_terms": 1} ) # Enable auto fetch def test_auto_fetch_terms_enable(self): @@ -254,7 +248,7 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): si.insert() si.submit() - @IntegrationTestCase.change_settings( + @change_settings( "Accounts Settings", {"automatically_fetch_payment_terms": 0} ) # Disable auto fetch def test_auto_fetch_terms_disable(self): From 4da44e2c3f092104b314ba15ff53efa44d09068d Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Tue, 2 Dec 2025 12:25:39 +0530 Subject: [PATCH 11/32] chore: resolve linter issue --- erpnext/selling/doctype/sales_order/test_sales_order.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 53832569812..d56431f5d3b 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -232,9 +232,7 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): si1 = make_sales_invoice(so.name) self.assertEqual(len(si1.get("items")), 0) - @change_settings( - "Accounts Settings", {"automatically_fetch_payment_terms": 1} - ) # Enable auto fetch + @change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1}) # Enable auto fetch def test_auto_fetch_terms_enable(self): so = make_sales_order(do_not_submit=True) @@ -248,9 +246,7 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): si.insert() si.submit() - @change_settings( - "Accounts Settings", {"automatically_fetch_payment_terms": 0} - ) # Disable auto fetch + @change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 0}) # Disable auto fetch def test_auto_fetch_terms_disable(self): so = make_sales_order(do_not_submit=True) From 2c13b2cc220b7aacbc140d07016dbcfdce670d7c Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Tue, 2 Dec 2025 12:58:43 +0530 Subject: [PATCH 12/32] test: fixed test_make_sales_invoice_with_terms --- erpnext/selling/doctype/sales_order/test_sales_order.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index d56431f5d3b..cff84411231 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -202,7 +202,11 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): @change_settings( "Accounts Settings", - {"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 1}, + { + "add_taxes_from_item_tax_template": 0, + "add_taxes_from_taxes_and_charges_template": 1, + "automatically_fetch_payment_terms": 1, + }, ) def test_make_sales_invoice_with_terms(self): so = make_sales_order(do_not_submit=True) From 13239a9deecd312b8fb50192b9e14be3eecfa6cc Mon Sep 17 00:00:00 2001 From: Dharanidharan S Date: Wed, 3 Dec 2025 18:44:25 +0530 Subject: [PATCH 13/32] fix(accounts-controller): handle empty items list --- erpnext/controllers/accounts_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f6195e6e09e..cc371a9ccf9 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2580,12 +2580,12 @@ class AccountsController(TransactionBase): def get_order_details(self): if self.doctype == "Sales Invoice": - po_or_so = self.get("items")[0].get("sales_order") + po_or_so = self.get("items") and self.get("items")[0].get("sales_order") po_or_so_doctype = "Sales Order" po_or_so_doctype_name = "sales_order" else: - po_or_so = self.get("items")[0].get("purchase_order") + po_or_so = self.get("items") and self.get("items")[0].get("purchase_order") po_or_so_doctype = "Purchase Order" po_or_so_doctype_name = "purchase_order" From 383648fb592223d1cc0776e10c65e682a18ceb61 Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Wed, 11 Feb 2026 17:29:20 +0530 Subject: [PATCH 14/32] feat: show formatted currency symbol on ledger preview (cherry picked from commit 5c8cb1e7ec0aa194ccd41e6581301a0894ad6814) --- erpnext/controllers/stock_controller.py | 2 +- erpnext/public/js/utils/ledger_preview.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index a20d59333b6..3860a7106c4 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -1591,7 +1591,7 @@ def get_gl_entries_for_preview(doctype, docname, fields): def get_columns(raw_columns, fields): return [ - {"name": d.get("label"), "editable": False, "width": 110} + {"name": d.get("label"), "editable": False, "width": 110, "fieldtype": d.get("fieldtype")} for d in raw_columns if not d.get("hidden") and d.get("fieldname") in fields ] diff --git a/erpnext/public/js/utils/ledger_preview.js b/erpnext/public/js/utils/ledger_preview.js index 6a610dd5065..9a5c8d3217f 100644 --- a/erpnext/public/js/utils/ledger_preview.js +++ b/erpnext/public/js/utils/ledger_preview.js @@ -80,6 +80,14 @@ erpnext.accounts.ledger_preview = { }, get_datatable(columns, data, wrapper) { + columns.forEach((col) => { + if (col.fieldtype === "Currency") { + col.format = (value) => { + return format_currency(value); + }; + } + }); + const datatable_options = { columns: columns, data: data, From ac02af476a086d6e1a4c3bba5a551434dbac4d7b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 12 Feb 2026 12:02:44 +0530 Subject: [PATCH 15/32] refactor: use query builder for sales person commission summary (cherry picked from commit 7105e3fb69b0b2e2ed28fbdcc674231e7aecc3c0) --- .../sales_person_commission_summary.py | 71 +++++++++++-------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py b/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py index 16b2d499af2..b0e611589eb 100644 --- a/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py +++ b/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py @@ -3,7 +3,8 @@ import frappe -from frappe import _, msgprint +from frappe import _, msgprint, qb +from frappe.query_builder import Criterion def execute(filters=None): @@ -97,45 +98,53 @@ def get_columns(filters): def get_entries(filters): - date_field = filters["doc_type"] == "Sales Order" and "transaction_date" or "posting_date" + dt = qb.DocType(filters["doc_type"]) + st = qb.DocType("Sales Team") + date_field = dt["transaction_date"] if filters["doc_type"] == "Sales Order" else dt["posting_date"] - conditions, values = get_conditions(filters, date_field) - entries = frappe.db.sql( - """ - select - dt.name, dt.customer, dt.territory, dt.{} as posting_date,dt.base_net_total as base_net_amount, - st.commission_rate,st.sales_person, st.allocated_percentage, st.allocated_amount, st.incentives - from - `tab{}` dt, `tabSales Team` st - where - st.parent = dt.name and st.parenttype = {} - and dt.docstatus = 1 {} order by dt.name desc,st.sales_person - """.format(date_field, filters["doc_type"], "%s", conditions), - tuple([filters["doc_type"], *values]), - as_dict=1, + conditions = get_conditions(dt, st, filters, date_field) + entries = ( + qb.from_(dt) + .join(st) + .on(st.parent.eq(dt.name) & st.parenttype.eq(filters["doc_type"])) + .select( + dt.name, + dt.customer, + dt.territory, + date_field.as_("posting_date"), + dt.base_net_total.as_("base_net_amount"), + st.commission_rate, + st.sales_person, + st.allocated_percentage, + st.allocated_amount, + st.incentives, + ) + .where(Criterion.all(conditions)) + .orderby(dt.name, st.sales_person) + .run(as_dict=True) ) return entries -def get_conditions(filters, date_field): - conditions = [""] - values = [] +def get_conditions(dt, st, filters, date_field): + conditions = [] + + conditions.append(dt.docstatus.eq(1)) + from_dt = filters.get("from_date") + to_dt = filters.get("to_date") + if from_dt and to_dt: + conditions.append(date_field.between(from_dt, to_dt)) + elif from_dt and not to_dt: + conditions.append(date_field.gte(from_dt)) + elif not from_dt and to_dt: + conditions.append(date_field.lte(to_dt)) for field in ["company", "customer", "territory"]: if filters.get(field): - conditions.append(f"dt.{field}=%s") - values.append(filters[field]) + conditions.append(dt[field].eq(filters.get(field))) if filters.get("sales_person"): - conditions.append("st.sales_person = '{}'".format(filters.get("sales_person"))) + conditions.append(st["sales_person"].eq(filters.get("sales_person"))) - if filters.get("from_date"): - conditions.append(f"dt.{date_field}>=%s") - values.append(filters["from_date"]) - - if filters.get("to_date"): - conditions.append(f"dt.{date_field}<=%s") - values.append(filters["to_date"]) - - return " and ".join(conditions), values + return conditions From c18ed0862e6e2e1730d5c991160805d65680ca9e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 12 Feb 2026 13:36:26 +0530 Subject: [PATCH 16/32] refactor: use query builder for profitability analysis (cherry picked from commit 5e34325604acae2cf7c112b64d1ef052612fe1ac) --- .../profitability_analysis.py | 68 ++++++++++++------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py index 5f3215fe7e2..4d057636a69 100644 --- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py @@ -3,7 +3,8 @@ import frappe -from frappe import _ +from frappe import _, qb +from frappe.query_builder import Criterion from frappe.utils import cstr, flt from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions @@ -33,11 +34,19 @@ def execute(filters=None): def get_accounts_data(based_on, company): if based_on == "Cost Center": - return frappe.db.sql( - """select name, parent_cost_center as parent_account, cost_center_name as account_name, lft, rgt - from `tabCost Center` where company=%s order by name""", - company, - as_dict=True, + cc = qb.DocType("Cost Center") + return ( + qb.from_(cc) + .select( + cc.name, + cc.parent_cost_center.as_("parent_account"), + cc.cost_center_name.as_("account_name"), + cc.lft, + cc.rgt, + ) + .where(cc.company.eq(company)) + .orderby(cc.name) + .run(as_dict=True) ) elif based_on == "Project": return frappe.get_all("Project", fields=["name"], filters={"company": company}, order_by="name") @@ -206,27 +215,38 @@ def set_gl_entries_by_account( company, from_date, to_date, based_on, gl_entries_by_account, ignore_closing_entries=False ): """Returns a dict like { "account": [gl entries], ... }""" - additional_conditions = [] + gl = qb.DocType("GL Entry") + acc = qb.DocType("Account") + + conditions = [] + conditions.append(gl.company.eq(company)) + conditions.append(gl[based_on].notnull()) + conditions.append(gl.is_cancelled.eq(0)) + + if from_date and to_date: + conditions.append(gl.posting_date.between(from_date, to_date)) + elif from_date and not to_date: + conditions.append(gl.posting_date.gte(from_date)) + elif not from_date and to_date: + conditions.append(gl.posting_date.lte(to_date)) if ignore_closing_entries: - additional_conditions.append("and voucher_type !='Period Closing Voucher'") + conditions.append(gl.voucher_type.ne("Period Closing Voucher")) - if from_date: - additional_conditions.append("and posting_date >= %(from_date)s") - - gl_entries = frappe.db.sql( - """select posting_date, {based_on} as based_on, debit, credit, - is_opening, (select root_type from `tabAccount` where name = account) as type - from `tabGL Entry` where company=%(company)s - {additional_conditions} - and posting_date <= %(to_date)s - and {based_on} is not null - and is_cancelled = 0 - order by {based_on}, posting_date""".format( - additional_conditions="\n".join(additional_conditions), based_on=based_on - ), - {"company": company, "from_date": from_date, "to_date": to_date}, - as_dict=True, + root_subquery = qb.from_(acc).select(acc.root_type).where(acc.name.eq(gl.account)) + gl_entries = ( + qb.from_(gl) + .select( + gl.posting_date, + gl[based_on].as_("based_on"), + gl.debit, + gl.credit, + gl.is_opening, + root_subquery.as_("type"), + ) + .where(Criterion.all(conditions)) + .orderby(gl[based_on], gl.posting_date) + .run(as_dict=True) ) for entry in gl_entries: From e12871b4083060036497063a3c661dd33c302c23 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 15 Feb 2026 13:06:37 +0530 Subject: [PATCH 17/32] fix: total weight does not update when updating items (cherry picked from commit 63323a2611f6e57e02c68873b7b2f612eac38b3d) --- erpnext/controllers/accounts_controller.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index cc371a9ccf9..0198d5f76e5 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -4002,6 +4002,12 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil flt(d.get("conversion_factor"), conv_fac_precision) or conversion_factor ) + if child_item.get("total_weight") and child_item.get("weight_per_unit"): + child_item.total_weight = flt( + child_item.weight_per_unit * child_item.qty * child_item.conversion_factor, + child_item.precision("total_weight"), + ) + if d.get("delivery_date") and parent_doctype == "Sales Order": child_item.delivery_date = d.get("delivery_date") From 85d18fa7a4c167fa1ce740544fec20da19193709 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 16 Feb 2026 10:38:15 +0530 Subject: [PATCH 18/32] fix: better validation for negative batch (cherry picked from commit a8636e4f59d60759e3e988c4f2817c206663886e) --- .../serial_and_batch_bundle.py | 36 +++++++++++++------ .../stock_settings/stock_settings.json | 4 +-- 2 files changed, 27 insertions(+), 13 deletions(-) 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 c23ccf24083..f0abd507173 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 @@ -15,6 +15,7 @@ from frappe.utils import ( cint, cstr, flt, + format_datetime, get_datetime, get_link_to_form, getdate, @@ -1426,22 +1427,33 @@ class SerialandBatchBundle(Document): if flt(available_qty, precision) < 0: self.throw_negative_batch(d.batch_no, available_qty, precision) - def throw_negative_batch(self, batch_no, available_qty, precision): + def throw_negative_batch(self, batch_no, available_qty, precision, posting_datetime=None): from erpnext.stock.stock_ledger import NegativeStockError if frappe.db.get_single_value("Stock Settings", "allow_negative_stock_for_batch"): return + date_msg = "" + if posting_datetime: + date_msg = " " + _("as of {0}").format(format_datetime(posting_datetime)) + + msg = _( + """ + The Batch {0} of an item {1} has negative stock in the warehouse {2}{3}. + Please add a stock quantity of {4} to proceed with this entry. + If it is not possible to make an adjustment entry, please enable 'Allow Negative Stock for Batch' in Stock Settings to proceed. + However, enabling this setting may lead to negative stock in the system. + So please ensure the stock levels are adjusted as soon as possible to maintain the correct valuation rate.""" + ).format( + bold(batch_no), + bold(self.item_code), + bold(self.warehouse), + date_msg, + bold(abs(flt(available_qty, precision))), + ) + frappe.throw( - _( - """ - The Batch {0} of an item {1} has negative stock in the warehouse {2}. Please add a stock quantity of {3} to proceed with this entry.""" - ).format( - bold(batch_no), - bold(self.item_code), - bold(self.warehouse), - bold(abs(flt(available_qty, precision))), - ), + msg, title=_("Negative Stock Error"), exc=NegativeStockError, ) @@ -1464,7 +1476,9 @@ class SerialandBatchBundle(Document): available_qty[row.batch_no] = flt(row.qty) if flt(available_qty[row.batch_no], precision) < 0: - self.throw_negative_batch(row.batch_no, available_qty[row.batch_no], precision) + self.throw_negative_batch( + row.batch_no, available_qty[row.batch_no], precision, row.posting_datetime + ) return available_qty diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 9e6eb8d6f2d..ee7e652cf6e 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -542,7 +542,7 @@ }, { "default": "0", - "description": "If enabled, the system will allow negative stock entries for the batch, but this could calculate the valuation rate incorrectly, so avoid using this option.", + "description": "If enabled, the system will allow negative stock entries for the batch. But, this may lead to incorrect valuation rates, so it is recommended to avoid using this option. The system will permit negative stock only when it is caused by backdated entries and will validate and block negative stock in all other cases.", "fieldname": "allow_negative_stock_for_batch", "fieldtype": "Check", "label": "Allow Negative Stock for Batch" @@ -553,7 +553,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-02-09 15:01:12.466175", + "modified": "2026-02-16 10:36:59.921491", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", From f2a77d178dcea70771e1bb385e08effd1cc3f88d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 16 Feb 2026 19:19:30 +0530 Subject: [PATCH 19/32] fix: cancel SABB if SLE cancelled from LCV (cherry picked from commit f23a49a25ee0f7f7a479c2bb6ced9fc197e3fa44) --- erpnext/controllers/buying_controller.py | 21 +++- .../purchase_receipt/test_purchase_receipt.py | 96 +++++++++++++++++++ .../serial_and_batch_bundle.py | 3 + 3 files changed, 118 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 8c8692d0be3..06a8f25b186 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -626,7 +626,9 @@ class BuyingController(SubcontractingController): or self.is_return or (self.is_internal_transfer() and self.docstatus == 2) else self.get_package_for_target_warehouse( - d, type_of_transaction=type_of_transaction + d, + type_of_transaction=type_of_transaction, + via_landed_cost_voucher=via_landed_cost_voucher, ) ), }, @@ -714,7 +716,22 @@ class BuyingController(SubcontractingController): via_landed_cost_voucher=via_landed_cost_voucher, ) - def get_package_for_target_warehouse(self, item, warehouse=None, type_of_transaction=None) -> str: + def get_package_for_target_warehouse( + self, item, warehouse=None, type_of_transaction=None, via_landed_cost_voucher=None + ) -> str: + if via_landed_cost_voucher and item.get("warehouse"): + if sabb := frappe.db.get_value( + "Serial and Batch Bundle", + { + "voucher_detail_no": item.name, + "warehouse": item.get("warehouse"), + "docstatus": 1, + "is_cancelled": 0, + }, + "name", + ): + return sabb + if not item.serial_and_batch_bundle: return "" diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index c91b9e22632..2070b264f8f 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1015,6 +1015,102 @@ class TestPurchaseReceipt(FrappeTestCase): pr.cancel() + def test_lcv_for_internal_transfer(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import ( + make_landed_cost_voucher, + ) + + prepare_data_for_internal_transfer() + + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + + item_code = make_item( + "Test Item For LCV in Internal Transfer", + {"has_batch_no": 1, "create_new_batch": 1, "batch_naming_series": "TEST-SBATCH.###"}, + ).name + + pr1 = make_purchase_receipt( + item_code=item_code, + qty=10, + rate=100, + warehouse="Stores - TCP1", + company="_Test Company with perpetual inventory", + ) + + dn1 = create_delivery_note( + item_code=pr1.items[0].item_code, + company=company, + customer=customer, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=10, + rate=500, + warehouse="Stores - TCP1", + target_warehouse="Work In Progress - TCP1", + ) + + pr = make_inter_company_purchase_receipt(dn1.name) + pr.items[0].from_warehouse = "Work In Progress - TCP1" + pr.items[0].warehouse = "Stores - TCP1" + pr.submit() + + sle_entries = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + fields=["serial_and_batch_bundle", "actual_qty"], + ) + self.assertEqual(len(sle_entries), 2) + + inward_sabb = frappe.get_all( + "Serial and Batch Bundle", + filters={ + "voucher_type": "Purchase Receipt", + "voucher_no": pr.name, + "total_qty": (">", 0), + "docstatus": 1, + }, + pluck="name", + ) + self.assertEqual(len(inward_sabb), 1) + + original_cost = frappe.db.get_value("Serial and Batch Bundle", inward_sabb[0], "total_amount") + + make_landed_cost_voucher( + company=pr.company, + receipt_document_type="Purchase Receipt", + receipt_document=pr.name, + charges=100, + distribute_charges_based_on="Qty", + expense_account="Expenses Included In Valuation - TCP1", + ) + + sle_entries = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0}, + fields=["serial_and_batch_bundle", "actual_qty"], + ) + self.assertEqual(len(sle_entries), 2) + + new_inward_sabb = frappe.get_all( + "Serial and Batch Bundle", + filters={ + "voucher_type": "Purchase Receipt", + "voucher_no": pr.name, + "total_qty": (">", 0), + "docstatus": 1, + }, + pluck="name", + ) + self.assertEqual(len(new_inward_sabb), 1) + + new_cost = frappe.db.get_value("Serial and Batch Bundle", new_inward_sabb[0], "total_amount") + self.assertEqual(new_cost, original_cost + 100) + + self.assertTrue(new_inward_sabb[0] == inward_sabb[0]) + def test_stock_transfer_from_purchase_receipt_with_valuation(self): from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note 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 f0abd507173..93807ee7c0b 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 @@ -1541,6 +1541,9 @@ class SerialandBatchBundle(Document): return query.run(as_dict=True) def validate_voucher_no_docstatus(self): + if self.is_cancelled: + return + if self.voucher_type == "POS Invoice": return From 4fe968961afbff5825f6e8e78e91559d92f91a2f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:01:23 +0000 Subject: [PATCH 20/32] fix(pos_invoice): add correct depends on condition (backport #52689) (#52693) * fix(pos_invoice): add correct depends on condition (#52689) * fix(pos_invoice): add correct depends on condition * fix: show field in sales order * refactor: eval condition (cherry picked from commit 219cf6bc5728cda02f333ad8076530ae7b29fed2) # Conflicts: # erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json * chore: resolve conflict --------- Co-authored-by: Soham Kulkarni <77533095+sokumon@users.noreply.github.com> Co-authored-by: Diptanil Saha --- .../sales_invoice_payment/sales_invoice_payment.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json b/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json index bd59f65dd4c..1ab1680d108 100644 --- a/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json +++ b/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json @@ -26,7 +26,7 @@ }, { "default": "0", - "depends_on": "eval:parent.doctype == 'Sales Invoice'", + "depends_on": "eval: [\"POS Invoice\", \"Sales Invoice\"].includes(parent.doctype)", "fieldname": "amount", "fieldtype": "Currency", "in_list_view": 1, @@ -85,7 +85,7 @@ ], "istable": 1, "links": [], - "modified": "2024-01-23 16:20:06.436979", + "modified": "2026-02-16 20:46:34.592604", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Payment", @@ -95,4 +95,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} From ca79f6478a4c95c82753fb0bc76d776b4dfe8fa0 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 16 Feb 2026 23:25:30 +0530 Subject: [PATCH 21/32] fix: consider sle for negative stock validation --- .../serial_and_batch_bundle/serial_and_batch_bundle.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) 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 93807ee7c0b..69a82ab64cb 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 @@ -1512,17 +1512,17 @@ class SerialandBatchBundle(Document): def get_available_qty_from_sabb(self): batches = [d.batch_no for d in self.entries if d.batch_no] - parent = frappe.qb.DocType("Serial and Batch Bundle") + parent = frappe.qb.DocType("Stock Ledger Entry") child = frappe.qb.DocType("Serial and Batch Entry") query = ( frappe.qb.from_(parent) .inner_join(child) - .on(parent.name == child.parent) + .on(parent.serial_and_batch_bundle == child.parent) .select( child.batch_no, child.qty, - CombineDatetime(parent.posting_date, parent.posting_time).as_("posting_datetime"), + parent.posting_datetime, parent.creation, ) .where( @@ -1531,13 +1531,10 @@ class SerialandBatchBundle(Document): & (child.batch_no.isin(batches)) & (parent.docstatus == 1) & (parent.is_cancelled == 0) - & (parent.type_of_transaction.isin(["Inward", "Outward"])) ) .for_update() ) - query = query.where(parent.voucher_type != "Pick List") - return query.run(as_dict=True) def validate_voucher_no_docstatus(self): From 9ec30319e4232f7dffd221003cfa39e7395fe978 Mon Sep 17 00:00:00 2001 From: "ili.ad" <108145573+ili-ad@users.noreply.github.com> Date: Tue, 17 Feb 2026 00:02:53 -0500 Subject: [PATCH 22/32] fix(postgres): validate against period closing using MAX(period_end_date) (#51554) * fix(postgres): validate against period closing using MAX(period_end_date) * refactor: remove non-existent field --------- Co-authored-by: Matt Howard Co-authored-by: ruthra kumar --- erpnext/accounts/general_ledger.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 60b3efd3cbc..ab86dcfd15c 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -804,12 +804,19 @@ def validate_against_pcv(is_opening, posting_date, company): title=_("Invalid Opening Entry"), ) - last_pcv_date = frappe.db.get_value( - "Period Closing Voucher", {"docstatus": 1, "company": company}, "max(period_end_date)" - ) + # Local import so you don't have to touch file-level imports + from frappe.query_builder.functions import Max + + pcv = frappe.qb.DocType("Period Closing Voucher") + + last_pcv_date = ( + frappe.qb.from_(pcv) + .select(Max(pcv.period_end_date)) + .where((pcv.docstatus == 1) & (pcv.company == company)) + ).run(pluck=True)[0] if last_pcv_date and getdate(posting_date) <= getdate(last_pcv_date): - message = _("Books have been closed till the period ending on {0}").format(formatdate(last_pcv_date)) + message = _("Books have been closed till the period ending on {0}.").format(formatdate(last_pcv_date)) message += "
" message += _("You cannot create/amend any accounting entries till this date.") frappe.throw(message, title=_("Period Closed")) From 97a6610c0c8b349a6d10e79cb9386d2d67e2c75e Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 17 Feb 2026 10:36:25 +0530 Subject: [PATCH 23/32] fix: production plan status (cherry picked from commit b3e6b304e46b20fc9112a18db1c6a183dcfb5ce1) --- .../production_plan/production_plan.py | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index d38da9257ff..29e4f4fb38a 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -648,8 +648,8 @@ class ProductionPlan(Document): self.status = "Completed" if self.status != "Completed": - self.update_ordered_status() self.update_requested_status() + self.update_ordered_status() if close is not None: self.db_set("status", self.status) @@ -658,25 +658,17 @@ class ProductionPlan(Document): self.update_bin_qty() def update_ordered_status(self): - update_status = False - for d in self.po_items: - if d.planned_qty == d.ordered_qty: - update_status = True - - if update_status and self.status != "Completed": - self.status = "In Process" + for child_table in ["po_items", "sub_assembly_items"]: + for item in self.get(child_table): + if item.ordered_qty: + self.status = "In Process" + return def update_requested_status(self): - if not self.mr_items: - return - - update_status = True for d in self.mr_items: - if d.quantity != d.requested_qty: - update_status = False - - if update_status: - self.status = "Material Requested" + if d.requested_qty: + self.status = "Material Requested" + break def get_production_items(self): item_dict = {} From 0576752d3b59ae6fd16a97ce46c414a9a436594b Mon Sep 17 00:00:00 2001 From: Pandiyan37 Date: Tue, 17 Feb 2026 12:10:08 +0530 Subject: [PATCH 24/32] fix(manufacturing): add sales order fields in subassembly child table --- .../production_plan_sub_assembly_item.json | 22 +++++++++++++++++-- .../production_plan_sub_assembly_item.py | 2 ++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json index b5f6a3ab065..9818beaa2c8 100644 --- a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json +++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json @@ -10,6 +10,8 @@ "fg_warehouse", "parent_item_code", "schedule_date", + "sales_order", + "sales_order_item", "column_break_3", "qty", "bom_no", @@ -212,20 +214,36 @@ "label": "Ordered Qty", "no_copy": 1, "read_only": 1 + }, + { + "fieldname": "sales_order", + "fieldtype": "Link", + "label": "Sales Order", + "options": "Sales Order", + "read_only": 1 + }, + { + "fieldname": "sales_order_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Sales Order Item", + "no_copy": 1, + "print_hide": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-06-10 13:36:24.759101", + "modified": "2026-02-17 12:06:02.309032", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan Sub Assembly Item", "owner": "Administrator", "permissions": [], "quick_entry": 1, + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py index 7e29675136c..42031d437b8 100644 --- a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py +++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py @@ -33,6 +33,8 @@ class ProductionPlanSubAssemblyItem(Document): purchase_order: DF.Link | None qty: DF.Float received_qty: DF.Float + sales_order: DF.Link | None + sales_order_item: DF.Data | None schedule_date: DF.Datetime | None stock_uom: DF.Link | None supplier: DF.Link | None From 53e18a9beb551d8c025f2964eb1560e2707878d8 Mon Sep 17 00:00:00 2001 From: Pandiyan37 Date: Tue, 17 Feb 2026 12:11:05 +0530 Subject: [PATCH 25/32] fix(manufacturing): set sales order references in subassembly child table --- .../doctype/production_plan/production_plan.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 29e4f4fb38a..351ba27a43e 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -790,6 +790,8 @@ class ProductionPlan(Document): "stock_uom", "bom_level", "schedule_date", + "sales_order", + "sales_order_item", ]: if row.get(field): wo_data[field] = row.get(field) @@ -829,6 +831,8 @@ class ProductionPlan(Document): "qty", "description", "production_plan_item", + "sales_order", + "sales_order_item", ]: po_data[field] = row.get(field) @@ -1015,6 +1019,10 @@ class ProductionPlan(Document): if not is_group_warehouse: data.fg_warehouse = self.sub_assembly_warehouse + if not self.combine_sub_items: + data.sales_order = row.sales_order + data.sales_order_item = row.sales_order_item + def set_default_supplier_for_subcontracting_order(self): items = [ d.production_item for d in self.sub_assembly_items if d.type_of_manufacturing == "Subcontract" From d6333c15621dc5d6912767a63325348f74caac0f Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 17 Feb 2026 11:54:12 +0530 Subject: [PATCH 26/32] fix: do not allow plant floor company and warehouse to be updated (cherry picked from commit fd721327430210f0048f29909d1dd884e0c6a9e3) # Conflicts: # erpnext/manufacturing/doctype/plant_floor/plant_floor.json --- .../doctype/plant_floor/plant_floor.json | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/plant_floor/plant_floor.json b/erpnext/manufacturing/doctype/plant_floor/plant_floor.json index c1c167c395b..311e4fd913b 100644 --- a/erpnext/manufacturing/doctype/plant_floor/plant_floor.json +++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.json @@ -49,7 +49,8 @@ "fieldname": "warehouse", "fieldtype": "Link", "label": "Warehouse", - "options": "Warehouse" + "options": "Warehouse", + "set_only_once": 1 }, { "depends_on": "eval:!doc.__islocal && doc.warehouse", @@ -66,13 +67,18 @@ "fieldname": "company", "fieldtype": "Link", "label": "Company", - "options": "Company" + "options": "Company", + "set_only_once": 1 } ], "hide_toolbar": 1, "index_web_pages_for_search": 1, "links": [], +<<<<<<< HEAD "modified": "2024-09-19 19:06:36.481625", +======= + "modified": "2026-02-17 11:53:17.940039", +>>>>>>> fd72132743 (fix: do not allow plant floor company and warehouse to be updated) "modified_by": "Administrator", "module": "Manufacturing", "name": "Plant Floor", @@ -92,7 +98,12 @@ "write": 1 } ], +<<<<<<< HEAD "sort_field": "modified", +======= + "row_format": "Dynamic", + "sort_field": "creation", +>>>>>>> fd72132743 (fix: do not allow plant floor company and warehouse to be updated) "sort_order": "DESC", "states": [] } From e8b46d9815621568ab3a523058ad3a7fef80fb73 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 17 Feb 2026 12:13:33 +0530 Subject: [PATCH 27/32] chore: resolve conflicts --- .../manufacturing/doctype/plant_floor/plant_floor.json | 9 --------- 1 file changed, 9 deletions(-) diff --git a/erpnext/manufacturing/doctype/plant_floor/plant_floor.json b/erpnext/manufacturing/doctype/plant_floor/plant_floor.json index 311e4fd913b..556a0c33721 100644 --- a/erpnext/manufacturing/doctype/plant_floor/plant_floor.json +++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.json @@ -74,11 +74,7 @@ "hide_toolbar": 1, "index_web_pages_for_search": 1, "links": [], -<<<<<<< HEAD - "modified": "2024-09-19 19:06:36.481625", -======= "modified": "2026-02-17 11:53:17.940039", ->>>>>>> fd72132743 (fix: do not allow plant floor company and warehouse to be updated) "modified_by": "Administrator", "module": "Manufacturing", "name": "Plant Floor", @@ -98,12 +94,7 @@ "write": 1 } ], -<<<<<<< HEAD "sort_field": "modified", -======= - "row_format": "Dynamic", - "sort_field": "creation", ->>>>>>> fd72132743 (fix: do not allow plant floor company and warehouse to be updated) "sort_order": "DESC", "states": [] } From d2dc0a4c9aa97bce4c8b3ab332c37d4ff10a231e Mon Sep 17 00:00:00 2001 From: Pandiyan37 Date: Tue, 17 Feb 2026 12:15:33 +0530 Subject: [PATCH 28/32] test(manufacturing): add test to validate the sales order references for sub assembly items --- .../production_plan/test_production_plan.py | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 25fc4f26247..62aa4f6ea11 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -565,6 +565,90 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(po_doc.items[0].fg_item, fg_item) self.assertEqual(po_doc.items[0].item_code, service_item) + def test_sales_order_references_for_sub_assembly_items(self): + """ + Test that Sales Order and Sales Order Item references in Work Order and Purchase Order + are correctly propagated from the Production Plan. + """ + + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + + # Setup Test Items & BOM + fg_item = "Test FG Good Item" + sub_assembly_item1 = "Test Sub Assembly Item 1" + sub_assembly_item2 = "Test Sub Assembly Item 2" + + bom_tree = { + fg_item: { + sub_assembly_item1: {"Test Raw Material 1": {}}, + sub_assembly_item2: {"Test Raw Material 2": {}}, + } + } + + create_nested_bom(bom_tree, prefix="") + + # Create Sales Order + so = make_sales_order(item_code=fg_item, qty=10) + so_item_row = so.items[0].name + + # Create Production Plan from Sales Order + production_plan = frappe.new_doc("Production Plan") + production_plan.company = so.company + production_plan.get_items_from = "Sales Order" + production_plan.item_code = fg_item + + production_plan.get_open_sales_orders() + self.assertEqual(production_plan.sales_orders[0].sales_order, so.name) + + production_plan.get_so_items() + + production_plan.skip_available_sub_assembly_item = 0 + production_plan.get_sub_assembly_items() + + self.assertEqual(len(production_plan.sub_assembly_items), 2) + + # Validate Sales Order references in Sub Assembly Items + for row in production_plan.sub_assembly_items: + if row.production_item == sub_assembly_item1: + row.supplier = "_Test Supplier" + row.type_of_manufacturing = "Subcontract" + + self.assertEqual(row.sales_order, so.name) + self.assertEqual(row.sales_order_item, so_item_row) + + # Submit Production Plan + production_plan.save() + production_plan.submit() + production_plan.make_work_order() + + # Validate Purchase Order (Subcontracted Item) + po_items = frappe.get_all( + "Purchase Order Item", + { + "production_plan": production_plan.name, + "fg_item": sub_assembly_item1, + }, + ["sales_order", "sales_order_item"], + ) + + self.assertTrue(po_items) + self.assertEqual(po_items[0].sales_order, so.name) + self.assertEqual(po_items[0].sales_order_item, so_item_row) + + # Validate Work Order (In-house Item) + work_orders = frappe.get_all( + "Work Order", + { + "production_plan": production_plan.name, + "production_item": sub_assembly_item2, + }, + ["sales_order", "sales_order_item"], + ) + + self.assertTrue(work_orders) + self.assertEqual(work_orders[0].sales_order, so.name) + self.assertEqual(work_orders[0].sales_order_item, so_item_row) + def test_production_plan_combine_subassembly(self): """ Test combining Sub assembly items belonging to the same BOM in Prod Plan. From c425944bdff1e207f7fe8991ee347a923d46ab0b Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 17 Feb 2026 15:39:52 +0530 Subject: [PATCH 29/32] fix: allow sequence id edit in BOM if routing is not set (cherry picked from commit 08529964b4407faee3ab3f6d6890fccc17ab038d) # Conflicts: # erpnext/manufacturing/doctype/bom_operation/bom_operation.json --- .../doctype/bom_operation/bom_operation.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index 3bcc4e896bd..e79746dcc7c 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -133,7 +133,12 @@ "label": "Batch Size" }, { +<<<<<<< HEAD "depends_on": "eval:doc.parenttype == \"Routing\"", +======= + "depends_on": "eval:doc.parenttype == \"Routing\" || !parent.routing", + "description": "If you want to run operations in parallel, keep the same sequence ID for them.", +>>>>>>> 08529964b4 (fix: allow sequence id edit in BOM if routing is not set) "fieldname": "sequence_id", "fieldtype": "Int", "label": "Sequence ID" @@ -196,7 +201,11 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2025-07-31 16:17:47.287117", +======= + "modified": "2026-02-17 15:33:28.495850", +>>>>>>> 08529964b4 (fix: allow sequence id edit in BOM if routing is not set) "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", From c6682f130cf3509696499c42526d785ac77cb731 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 17 Feb 2026 16:07:49 +0530 Subject: [PATCH 30/32] chore: resolve conflicts --- .../doctype/bom_operation/bom_operation.json | 9 --------- 1 file changed, 9 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index e79746dcc7c..a40d0d714ed 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -133,12 +133,7 @@ "label": "Batch Size" }, { -<<<<<<< HEAD - "depends_on": "eval:doc.parenttype == \"Routing\"", -======= "depends_on": "eval:doc.parenttype == \"Routing\" || !parent.routing", - "description": "If you want to run operations in parallel, keep the same sequence ID for them.", ->>>>>>> 08529964b4 (fix: allow sequence id edit in BOM if routing is not set) "fieldname": "sequence_id", "fieldtype": "Int", "label": "Sequence ID" @@ -201,11 +196,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2025-07-31 16:17:47.287117", -======= "modified": "2026-02-17 15:33:28.495850", ->>>>>>> 08529964b4 (fix: allow sequence id edit in BOM if routing is not set) "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", From 79c3bc9bcdb405d20649a493d7aa8e59296c4507 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 15 Feb 2026 20:29:57 +0530 Subject: [PATCH 31/32] fix: standalone credit/debit notes should not fetch any serial or batch by default (cherry picked from commit 2017edca88639515c0d8dc4735145837d1869542) --- erpnext/public/js/utils/serial_no_batch_selector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 996ee949a13..002842cad2d 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -707,7 +707,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { } render_data() { - if (this.bundle || this.frm.doc.is_return) { + if (this.bundle || (this.frm.doc.is_return && this.frm.doc.return_against)) { frappe .call({ method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_serial_batch_ledgers", From 631363632b53281f37863978b609cba5c426488d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 17 Feb 2026 15:07:37 +0530 Subject: [PATCH 32/32] feat: Negative Batch report (cherry picked from commit 34edbed00bb4dad11deec87d7da613c1ab77dfdc) --- .../report/negative_batch_report/__init__.py | 0 .../negative_batch_report.js | 41 +++++ .../negative_batch_report.json | 53 +++++++ .../negative_batch_report.py | 145 ++++++++++++++++++ 4 files changed, 239 insertions(+) create mode 100644 erpnext/stock/report/negative_batch_report/__init__.py create mode 100644 erpnext/stock/report/negative_batch_report/negative_batch_report.js create mode 100644 erpnext/stock/report/negative_batch_report/negative_batch_report.json create mode 100644 erpnext/stock/report/negative_batch_report/negative_batch_report.py diff --git a/erpnext/stock/report/negative_batch_report/__init__.py b/erpnext/stock/report/negative_batch_report/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/report/negative_batch_report/negative_batch_report.js b/erpnext/stock/report/negative_batch_report/negative_batch_report.js new file mode 100644 index 00000000000..3bfe8fe9c85 --- /dev/null +++ b/erpnext/stock/report/negative_batch_report/negative_batch_report.js @@ -0,0 +1,41 @@ +// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.query_reports["Negative Batch Report"] = { + filters: [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_default("company"), + }, + { + fieldname: "item_code", + label: __("Item Code"), + fieldtype: "Link", + options: "Item", + get_query: function () { + return { + filters: { + has_batch_no: 1, + }, + }; + }, + }, + { + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + options: "Warehouse", + get_query: function () { + return { + filters: { + is_group: 0, + company: frappe.query_report.get_filter_value("company"), + }, + }; + }, + }, + ], +}; diff --git a/erpnext/stock/report/negative_batch_report/negative_batch_report.json b/erpnext/stock/report/negative_batch_report/negative_batch_report.json new file mode 100644 index 00000000000..cecc5716055 --- /dev/null +++ b/erpnext/stock/report/negative_batch_report/negative_batch_report.json @@ -0,0 +1,53 @@ +{ + "add_total_row": 0, + "add_translate_data": 0, + "columns": [], + "creation": "2026-02-17 11:34:21.549485", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "json": "", + "letter_head": null, + "modified": "2026-02-17 11:34:59.106045", + "modified_by": "Administrator", + "module": "Stock", + "name": "Negative Batch Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Serial and Batch Bundle", + "report_name": "Negative Batch Report", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Purchase User" + }, + { + "role": "Purchase Manager" + }, + { + "role": "Stock User" + }, + { + "role": "Stock Manager" + }, + { + "role": "Delivery User" + }, + { + "role": "Delivery Manager" + }, + { + "role": "Manufacturing User" + }, + { + "role": "Manufacturing Manager" + } + ], + "timeout": 0 +} diff --git a/erpnext/stock/report/negative_batch_report/negative_batch_report.py b/erpnext/stock/report/negative_batch_report/negative_batch_report.py new file mode 100644 index 00000000000..b12bd87d538 --- /dev/null +++ b/erpnext/stock/report/negative_batch_report/negative_batch_report.py @@ -0,0 +1,145 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.utils import add_to_date, flt, today + +from erpnext.stock.report.stock_ledger.stock_ledger import execute as stock_ledger_execute + + +def execute(filters: dict | None = None): + """Return columns and data for the report. + + This is the main entry point for the report. It accepts the filters as a + dictionary and should return columns and data. It is called by the framework + every time the report is refreshed or a filter is updated. + """ + columns = get_columns() + data = get_data(filters) + + return columns, data + + +def get_columns() -> list[dict]: + return [ + { + "label": _("Posting Datetime"), + "fieldname": "posting_date", + "fieldtype": "Datetime", + "width": 160, + }, + { + "label": _("Batch No"), + "fieldname": "batch_no", + "fieldtype": "Link", + "options": "Batch", + "width": 120, + }, + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 150, + }, + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 160, + }, + { + "label": _("Previous Qty"), + "fieldname": "previous_qty", + "fieldtype": "Float", + "width": 130, + }, + { + "label": _("Transaction Qty"), + "fieldname": "actual_qty", + "fieldtype": "Float", + "width": 130, + }, + { + "label": _("Qty After Transaction"), + "fieldname": "qty_after_transaction", + "fieldtype": "Float", + "width": 180, + }, + { + "label": _("Document Type"), + "fieldname": "voucher_type", + "fieldtype": "Data", + "width": 130, + }, + { + "label": _("Document No"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": 130, + }, + ] + + +def get_data(filters) -> list[dict]: + batches = get_batches(filters) + companies = get_companies(filters) + batch_negative_data = [] + + flt_precision = frappe.db.get_default("float_precision") or 2 + for company in companies: + for batch in batches: + _c, data = stock_ledger_execute( + frappe._dict( + { + "company": company, + "batch_no": batch, + "from_date": add_to_date(today(), years=-12), + "to_date": today(), + "segregate_serial_batch_bundle": 1, + "warehouse": filters.get("warehouse"), + "valuation_field_type": "Currency", + } + ) + ) + + previous_qty = 0 + for row in data: + if flt(row.get("qty_after_transaction"), flt_precision) < 0: + batch_negative_data.append( + { + "posting_date": row.get("date"), + "batch_no": row.get("batch_no"), + "item_code": row.get("item_code"), + "item_name": row.get("item_name"), + "warehouse": row.get("warehouse"), + "actual_qty": row.get("actual_qty"), + "qty_after_transaction": row.get("qty_after_transaction"), + "previous_qty": previous_qty, + "voucher_type": row.get("voucher_type"), + "voucher_no": row.get("voucher_no"), + } + ) + + previous_qty = row.get("qty_after_transaction") + + return batch_negative_data + + +def get_batches(filters): + batch_filters = {} + if filters.get("item_code"): + batch_filters["item"] = filters["item_code"] + + return frappe.get_all("Batch", pluck="name", filters=batch_filters) + + +def get_companies(filters): + company_filters = {} + if filters.get("company"): + company_filters["name"] = filters["company"] + + return frappe.get_all("Company", pluck="name", filters=company_filters)