From 81ce5fbee90f78301e9d3f89d324af1c690911ea Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Mon, 1 Jun 2026 12:47:31 +0530 Subject: [PATCH 01/39] fix: use fiscal year instead of calendar year in accounting dashboard number cards (cherry picked from commit e8fff2fdadad91823a55db47fdd76a4be7528a5d) # Conflicts: # erpnext/accounts/number_card/total_incoming_bills/total_incoming_bills.json # erpnext/accounts/number_card/total_incoming_payment/total_incoming_payment.json # erpnext/accounts/number_card/total_outgoing_bills/total_outgoing_bills.json # erpnext/accounts/number_card/total_outgoing_payment/total_outgoing_payment.json --- .../total_incoming_bills/total_incoming_bills.json | 9 +++++++++ .../total_incoming_payment/total_incoming_payment.json | 9 +++++++++ .../total_outgoing_bills/total_outgoing_bills.json | 9 +++++++++ .../total_outgoing_payment/total_outgoing_payment.json | 9 +++++++++ 4 files changed, 36 insertions(+) diff --git a/erpnext/accounts/number_card/total_incoming_bills/total_incoming_bills.json b/erpnext/accounts/number_card/total_incoming_bills/total_incoming_bills.json index 88c7cae3f69..a184869539e 100644 --- a/erpnext/accounts/number_card/total_incoming_bills/total_incoming_bills.json +++ b/erpnext/accounts/number_card/total_incoming_bills/total_incoming_bills.json @@ -4,14 +4,23 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Purchase Invoice", +<<<<<<< HEAD "dynamic_filters_json": "[[\"Purchase Invoice\",\"company\",\"=\",\" frappe.defaults.get_user_default(\\\"Company\\\")\"]]", "filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Purchase Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]", +======= + "dynamic_filters_json": "[[\"Purchase Invoice\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Purchase Invoice\",\"posting_date\",\"Between\",\"[frappe.boot.current_fiscal_year[1], frappe.boot.current_fiscal_year[2]]\"]]", + "filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\"]]", +>>>>>>> e8fff2fdad (fix: use fiscal year instead of calendar year in accounting dashboard number cards) "function": "Sum", "idx": 0, "is_public": 1, "is_standard": 1, "label": "Total Incoming Bills", +<<<<<<< HEAD "modified": "2024-11-20 19:08:37.043777", +======= + "modified": "2026-06-01 12:00:00.000000", +>>>>>>> e8fff2fdad (fix: use fiscal year instead of calendar year in accounting dashboard number cards) "modified_by": "Administrator", "module": "Accounts", "name": "Total Incoming Bills", diff --git a/erpnext/accounts/number_card/total_incoming_payment/total_incoming_payment.json b/erpnext/accounts/number_card/total_incoming_payment/total_incoming_payment.json index a53b222ed7d..6714b36dc15 100644 --- a/erpnext/accounts/number_card/total_incoming_payment/total_incoming_payment.json +++ b/erpnext/accounts/number_card/total_incoming_payment/total_incoming_payment.json @@ -4,14 +4,23 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Payment Entry", +<<<<<<< HEAD "dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", "filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\",false]]", +======= + "dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Payment Entry\",\"posting_date\",\"Between\",\"[frappe.boot.current_fiscal_year[1], frappe.boot.current_fiscal_year[2]]\"]]", + "filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\"],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\"]]", +>>>>>>> e8fff2fdad (fix: use fiscal year instead of calendar year in accounting dashboard number cards) "function": "Sum", "idx": 0, "is_public": 1, "is_standard": 1, "label": "Total Incoming Payment", +<<<<<<< HEAD "modified": "2020-07-22 13:06:20.237689", +======= + "modified": "2026-06-01 12:00:00.000000", +>>>>>>> e8fff2fdad (fix: use fiscal year instead of calendar year in accounting dashboard number cards) "modified_by": "Administrator", "module": "Accounts", "name": "Total Incoming Payment", diff --git a/erpnext/accounts/number_card/total_outgoing_bills/total_outgoing_bills.json b/erpnext/accounts/number_card/total_outgoing_bills/total_outgoing_bills.json index 092defd94bd..da806362b3c 100644 --- a/erpnext/accounts/number_card/total_outgoing_bills/total_outgoing_bills.json +++ b/erpnext/accounts/number_card/total_outgoing_bills/total_outgoing_bills.json @@ -4,14 +4,23 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Sales Invoice", +<<<<<<< HEAD "dynamic_filters_json": "[[\"Sales Invoice\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", "filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Sales Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]", +======= + "dynamic_filters_json": "[[\"Sales Invoice\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Sales Invoice\",\"posting_date\",\"Between\",\"[frappe.boot.current_fiscal_year[1], frappe.boot.current_fiscal_year[2]]\"]]", + "filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\"]]", +>>>>>>> e8fff2fdad (fix: use fiscal year instead of calendar year in accounting dashboard number cards) "function": "Sum", "idx": 0, "is_public": 1, "is_standard": 1, "label": "Total Outgoing Bills", +<<<<<<< HEAD "modified": "2020-07-22 13:07:19.633101", +======= + "modified": "2026-06-01 12:00:00.000000", +>>>>>>> e8fff2fdad (fix: use fiscal year instead of calendar year in accounting dashboard number cards) "modified_by": "Administrator", "module": "Accounts", "name": "Total Outgoing Bills", diff --git a/erpnext/accounts/number_card/total_outgoing_payment/total_outgoing_payment.json b/erpnext/accounts/number_card/total_outgoing_payment/total_outgoing_payment.json index d60f30f7c9a..5230e4679c9 100644 --- a/erpnext/accounts/number_card/total_outgoing_payment/total_outgoing_payment.json +++ b/erpnext/accounts/number_card/total_outgoing_payment/total_outgoing_payment.json @@ -4,14 +4,23 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Payment Entry", +<<<<<<< HEAD "dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", "filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\",false]]", +======= + "dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Payment Entry\",\"posting_date\",\"Between\",\"[frappe.boot.current_fiscal_year[1], frappe.boot.current_fiscal_year[2]]\"]]", + "filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\"],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\"]]", +>>>>>>> e8fff2fdad (fix: use fiscal year instead of calendar year in accounting dashboard number cards) "function": "Sum", "idx": 0, "is_public": 1, "is_standard": 1, "label": "Total Outgoing Payment", +<<<<<<< HEAD "modified": "2020-07-22 12:49:34.942896", +======= + "modified": "2026-06-01 12:00:00.000000", +>>>>>>> e8fff2fdad (fix: use fiscal year instead of calendar year in accounting dashboard number cards) "modified_by": "Administrator", "module": "Accounts", "name": "Total Outgoing Payment", From da1ccc2b622f8bb3ca766585ce08e25db2e3ff32 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Mon, 1 Jun 2026 13:13:29 +0530 Subject: [PATCH 02/39] fix: set a fallback value if no fiscal year set (cherry picked from commit c68918bc1823051575c5e0d0caacb229775ecae8) # Conflicts: # erpnext/accounts/number_card/total_incoming_bills/total_incoming_bills.json # erpnext/accounts/number_card/total_incoming_payment/total_incoming_payment.json # erpnext/accounts/number_card/total_outgoing_bills/total_outgoing_bills.json # erpnext/accounts/number_card/total_outgoing_payment/total_outgoing_payment.json --- .../total_incoming_bills/total_incoming_bills.json | 4 ++++ .../total_incoming_payment/total_incoming_payment.json | 4 ++++ .../total_outgoing_bills/total_outgoing_bills.json | 4 ++++ .../total_outgoing_payment/total_outgoing_payment.json | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/erpnext/accounts/number_card/total_incoming_bills/total_incoming_bills.json b/erpnext/accounts/number_card/total_incoming_bills/total_incoming_bills.json index a184869539e..5b545837905 100644 --- a/erpnext/accounts/number_card/total_incoming_bills/total_incoming_bills.json +++ b/erpnext/accounts/number_card/total_incoming_bills/total_incoming_bills.json @@ -4,11 +4,15 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Purchase Invoice", +<<<<<<< HEAD <<<<<<< HEAD "dynamic_filters_json": "[[\"Purchase Invoice\",\"company\",\"=\",\" frappe.defaults.get_user_default(\\\"Company\\\")\"]]", "filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Purchase Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]", ======= "dynamic_filters_json": "[[\"Purchase Invoice\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Purchase Invoice\",\"posting_date\",\"Between\",\"[frappe.boot.current_fiscal_year[1], frappe.boot.current_fiscal_year[2]]\"]]", +======= + "dynamic_filters_json": "[[\"Purchase Invoice\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Purchase Invoice\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]", +>>>>>>> c68918bc18 (fix: set a fallback value if no fiscal year set) "filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\"]]", >>>>>>> e8fff2fdad (fix: use fiscal year instead of calendar year in accounting dashboard number cards) "function": "Sum", diff --git a/erpnext/accounts/number_card/total_incoming_payment/total_incoming_payment.json b/erpnext/accounts/number_card/total_incoming_payment/total_incoming_payment.json index 6714b36dc15..4c13e4c1823 100644 --- a/erpnext/accounts/number_card/total_incoming_payment/total_incoming_payment.json +++ b/erpnext/accounts/number_card/total_incoming_payment/total_incoming_payment.json @@ -4,11 +4,15 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Payment Entry", +<<<<<<< HEAD <<<<<<< HEAD "dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", "filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\",false]]", ======= "dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Payment Entry\",\"posting_date\",\"Between\",\"[frappe.boot.current_fiscal_year[1], frappe.boot.current_fiscal_year[2]]\"]]", +======= + "dynamic_filters_json": "[[\"Payment Entry\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Payment Entry\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]", +>>>>>>> c68918bc18 (fix: set a fallback value if no fiscal year set) "filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\"],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\"]]", >>>>>>> e8fff2fdad (fix: use fiscal year instead of calendar year in accounting dashboard number cards) "function": "Sum", diff --git a/erpnext/accounts/number_card/total_outgoing_bills/total_outgoing_bills.json b/erpnext/accounts/number_card/total_outgoing_bills/total_outgoing_bills.json index da806362b3c..2f66ad50578 100644 --- a/erpnext/accounts/number_card/total_outgoing_bills/total_outgoing_bills.json +++ b/erpnext/accounts/number_card/total_outgoing_bills/total_outgoing_bills.json @@ -4,11 +4,15 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Sales Invoice", +<<<<<<< HEAD <<<<<<< HEAD "dynamic_filters_json": "[[\"Sales Invoice\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", "filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Sales Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]", ======= "dynamic_filters_json": "[[\"Sales Invoice\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Sales Invoice\",\"posting_date\",\"Between\",\"[frappe.boot.current_fiscal_year[1], frappe.boot.current_fiscal_year[2]]\"]]", +======= + "dynamic_filters_json": "[[\"Sales Invoice\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Sales Invoice\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]", +>>>>>>> c68918bc18 (fix: set a fallback value if no fiscal year set) "filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\"]]", >>>>>>> e8fff2fdad (fix: use fiscal year instead of calendar year in accounting dashboard number cards) "function": "Sum", diff --git a/erpnext/accounts/number_card/total_outgoing_payment/total_outgoing_payment.json b/erpnext/accounts/number_card/total_outgoing_payment/total_outgoing_payment.json index 5230e4679c9..6e583d8daf5 100644 --- a/erpnext/accounts/number_card/total_outgoing_payment/total_outgoing_payment.json +++ b/erpnext/accounts/number_card/total_outgoing_payment/total_outgoing_payment.json @@ -4,11 +4,15 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Payment Entry", +<<<<<<< HEAD <<<<<<< HEAD "dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", "filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\",false]]", ======= "dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Payment Entry\",\"posting_date\",\"Between\",\"[frappe.boot.current_fiscal_year[1], frappe.boot.current_fiscal_year[2]]\"]]", +======= + "dynamic_filters_json": "[[\"Payment Entry\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Payment Entry\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]", +>>>>>>> c68918bc18 (fix: set a fallback value if no fiscal year set) "filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\"],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\"]]", >>>>>>> e8fff2fdad (fix: use fiscal year instead of calendar year in accounting dashboard number cards) "function": "Sum", From eef075a2bac9b4b37ba73d8b58da00a6fed6c65d Mon Sep 17 00:00:00 2001 From: Lakshit Jain Date: Tue, 9 Jun 2026 17:52:19 +0530 Subject: [PATCH 03/39] fix: update round off account functions to accept document context for regional overrides (#55758) (cherry picked from commit 08129ff71c2279e803ec95564b9f4837a599c42b) # Conflicts: # erpnext/controllers/taxes_and_totals.py # erpnext/controllers/tests/test_taxes_and_totals.py # erpnext/public/js/controllers/taxes_and_totals.js --- erpnext/controllers/taxes_and_totals.py | 15 +++++++++-- .../tests/test_taxes_and_totals.py | 26 +++++++++++++++++++ .../public/js/controllers/taxes_and_totals.js | 9 +++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 54aee03e083..8b2a02ce290 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -26,9 +26,14 @@ from erpnext.utilities.regional import temporary_flag class calculate_taxes_and_totals: def __init__(self, doc: Document): self.doc = doc +<<<<<<< HEAD frappe.flags.round_off_applicable_accounts = [] frappe.flags.round_row_wise_tax = frappe.db.get_single_value( "Accounts Settings", "round_row_wise_tax" +======= + frappe.flags.round_off_applicable_accounts = ( + get_round_off_applicable_accounts(self.doc.company, [], self.doc) or [] +>>>>>>> 08129ff71c (fix: update round off account functions to accept document context for regional overrides (#55758)) ) if doc.get("round_off_applicable_accounts_for_tax_withholding"): @@ -1128,14 +1133,20 @@ def get_itemised_tax_breakup_html(doc): @frappe.whitelist() +<<<<<<< HEAD def get_round_off_applicable_accounts(company, account_list): +======= +def get_round_off_applicable_accounts( + company: str, account_list: list | str, doc: str | dict | Document | None = None +): +>>>>>>> 08129ff71c (fix: update round off account functions to accept document context for regional overrides (#55758)) # required to set correct region with temporary_flag("company", company): - return get_regional_round_off_accounts(company, account_list) + return get_regional_round_off_accounts(company, account_list, doc) @erpnext.allow_regional -def get_regional_round_off_accounts(company, account_list): +def get_regional_round_off_accounts(company, account_list, doc=None): pass diff --git a/erpnext/controllers/tests/test_taxes_and_totals.py b/erpnext/controllers/tests/test_taxes_and_totals.py index 504716f4684..b86e60a027b 100644 --- a/erpnext/controllers/tests/test_taxes_and_totals.py +++ b/erpnext/controllers/tests/test_taxes_and_totals.py @@ -5,7 +5,33 @@ from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +<<<<<<< HEAD class TestTaxesAndTotals(FrappeTestCase): +======= +class TestTaxesAndTotals(ERPNextTestSuite): + def test_regional_round_off_accounts(self): + """ + Regional overrides cannot extend the list in-place — the return + value must be assigned back to frappe.flags.round_off_applicable_accounts. + """ + test_account = "_Test Round Off Account" + + def mock_regional(company, account_list: list, doc=None) -> list: + # Simulates a regional override + account_list.extend([test_account]) + return account_list + + so = make_sales_order(do_not_save=True) + + with patch( + "erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts", + mock_regional, + ): + calculate_taxes_and_totals(so) + + self.assertIn(test_account, frappe.flags.round_off_applicable_accounts) + +>>>>>>> 08129ff71c (fix: update round off account functions to accept document context for regional overrides (#55758)) def test_disabling_rounded_total_resets_base_fields(self): """Disabling rounded total should also clear base rounded values.""" so = make_sales_order(do_not_save=True) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 7b07c13bf11..924bf4534ac 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -204,10 +204,19 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { if (me.frm.doc.company) { frappe.call({ +<<<<<<< HEAD "method": "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts", "args": { "company": me.frm.doc.company, "account_list": frappe.flags.round_off_applicable_accounts +======= + method: "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts", + args: { + company: me.frm.doc.company, + account_list: frappe.flags.round_off_applicable_accounts, + // pass the doc so regional overrides can inspect it + doc: me.frm.doc, +>>>>>>> 08129ff71c (fix: update round off account functions to accept document context for regional overrides (#55758)) }, callback(r) { if (r.message) { From 5629056ec267e9bb72b316f5f661f7a99e534e7d Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 9 Jun 2026 18:12:53 +0530 Subject: [PATCH 04/39] chore: resolve conflicts --- erpnext/controllers/taxes_and_totals.py | 13 +++---------- erpnext/controllers/tests/test_taxes_and_totals.py | 6 ++---- erpnext/public/js/controllers/taxes_and_totals.js | 12 ++---------- 3 files changed, 7 insertions(+), 24 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 8b2a02ce290..09318b6725f 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -26,14 +26,9 @@ from erpnext.utilities.regional import temporary_flag class calculate_taxes_and_totals: def __init__(self, doc: Document): self.doc = doc -<<<<<<< HEAD frappe.flags.round_off_applicable_accounts = [] frappe.flags.round_row_wise_tax = frappe.db.get_single_value( "Accounts Settings", "round_row_wise_tax" -======= - frappe.flags.round_off_applicable_accounts = ( - get_round_off_applicable_accounts(self.doc.company, [], self.doc) or [] ->>>>>>> 08129ff71c (fix: update round off account functions to accept document context for regional overrides (#55758)) ) if doc.get("round_off_applicable_accounts_for_tax_withholding"): @@ -43,7 +38,9 @@ class calculate_taxes_and_totals: self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items") - get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts) + get_round_off_applicable_accounts( + self.doc.company, frappe.flags.round_off_applicable_accounts, self.doc + ) self.calculate() def filter_rows(self): @@ -1133,13 +1130,9 @@ def get_itemised_tax_breakup_html(doc): @frappe.whitelist() -<<<<<<< HEAD -def get_round_off_applicable_accounts(company, account_list): -======= def get_round_off_applicable_accounts( company: str, account_list: list | str, doc: str | dict | Document | None = None ): ->>>>>>> 08129ff71c (fix: update round off account functions to accept document context for regional overrides (#55758)) # required to set correct region with temporary_flag("company", company): return get_regional_round_off_accounts(company, account_list, doc) diff --git a/erpnext/controllers/tests/test_taxes_and_totals.py b/erpnext/controllers/tests/test_taxes_and_totals.py index b86e60a027b..715acf8782f 100644 --- a/erpnext/controllers/tests/test_taxes_and_totals.py +++ b/erpnext/controllers/tests/test_taxes_and_totals.py @@ -1,3 +1,5 @@ +from unittest.mock import patch + import frappe from frappe.tests.utils import FrappeTestCase @@ -5,10 +7,7 @@ from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order -<<<<<<< HEAD class TestTaxesAndTotals(FrappeTestCase): -======= -class TestTaxesAndTotals(ERPNextTestSuite): def test_regional_round_off_accounts(self): """ Regional overrides cannot extend the list in-place — the return @@ -31,7 +30,6 @@ class TestTaxesAndTotals(ERPNextTestSuite): self.assertIn(test_account, frappe.flags.round_off_applicable_accounts) ->>>>>>> 08129ff71c (fix: update round off account functions to accept document context for regional overrides (#55758)) def test_disabling_rounded_total_resets_base_fields(self): """Disabling rounded total should also clear base rounded values.""" so = make_sales_order(do_not_save=True) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 924bf4534ac..0df4cabfb4f 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -204,19 +204,11 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { if (me.frm.doc.company) { frappe.call({ -<<<<<<< HEAD "method": "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts", "args": { "company": me.frm.doc.company, - "account_list": frappe.flags.round_off_applicable_accounts -======= - method: "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts", - args: { - company: me.frm.doc.company, - account_list: frappe.flags.round_off_applicable_accounts, - // pass the doc so regional overrides can inspect it - doc: me.frm.doc, ->>>>>>> 08129ff71c (fix: update round off account functions to accept document context for regional overrides (#55758)) + "account_list": frappe.flags.round_off_applicable_accounts, + "doc": me.frm.doc, }, callback(r) { if (r.message) { From 690adf10518ef1450b994b6a28b35457f926b978 Mon Sep 17 00:00:00 2001 From: S Sakthivel Murugan Date: Wed, 17 Jun 2026 12:51:35 +0530 Subject: [PATCH 05/39] fix: add dynamic links for customer and supplier dashboards --- erpnext/buying/doctype/supplier/supplier_dashboard.py | 1 + erpnext/selling/doctype/customer/customer_dashboard.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/supplier/supplier_dashboard.py b/erpnext/buying/doctype/supplier/supplier_dashboard.py index 9f424dd0c7c..198b32cda79 100644 --- a/erpnext/buying/doctype/supplier/supplier_dashboard.py +++ b/erpnext/buying/doctype/supplier/supplier_dashboard.py @@ -5,6 +5,7 @@ def get_data(): return { "fieldname": "supplier", "non_standard_fieldnames": {"Payment Entry": "party", "Bank Account": "party"}, + "dynamic_links": {"party": ["Supplier", "party_type"]}, "transactions": [ {"label": _("Procurement"), "items": ["Request for Quotation", "Supplier Quotation"]}, {"label": _("Orders"), "items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]}, diff --git a/erpnext/selling/doctype/customer/customer_dashboard.py b/erpnext/selling/doctype/customer/customer_dashboard.py index fc3c5cf1ab2..8afef385af4 100644 --- a/erpnext/selling/doctype/customer/customer_dashboard.py +++ b/erpnext/selling/doctype/customer/customer_dashboard.py @@ -11,7 +11,10 @@ def get_data(): "Bank Account": "party", "Subscription": "party", }, - "dynamic_links": {"party_name": ["Customer", "quotation_to"]}, + "dynamic_links": { + "party_name": ["Customer", "quotation_to"], + "party": ["Customer", "party_type"], + }, "transactions": [ {"label": _("Pre Sales"), "items": ["Opportunity", "Quotation"]}, {"label": _("Orders"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]}, From 19913127a73050d91ffe61e966e0b655515ca63e Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 17 Jun 2026 15:55:22 +0530 Subject: [PATCH 06/39] fix: preserve stock ageing on non-serial reconciliation --- .../stock/report/stock_ageing/stock_ageing.py | 3 +- .../report/stock_ageing/test_stock_ageing.py | 61 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index d0e96d640fe..2e11fa1664b 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -358,7 +358,7 @@ class FIFOSlots: if row.voucher_type != "Stock Reconciliation": return - if not row.batch_no or row.serial_no or row.serial_and_batch_bundle: + if row.has_serial_no and (not row.batch_no or row.serial_no or row.serial_and_batch_bundle): if row.voucher_detail_no in self.stock_reco_voucher_wise_count: # Legacy reconciliation with a single SLE has qty_after_transaction and # stock_value_difference without an outward entry, so reset the queue first. @@ -1083,6 +1083,7 @@ class FIFOSlots: (doctype.voucher_type == "Stock Reconciliation") & (doctype.docstatus < 2) & (doctype.is_cancelled == 0) + & (item.has_serial_no == 1) ) .groupby(doctype.voucher_detail_no) ) diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 59550f07aac..003d1a51d93 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -195,6 +195,67 @@ class TestStockAgeing(FrappeTestCase): self.assertEqual(queue[0][0], 20.0) self.assertEqual(queue[1][0], 20.0) + def test_non_serial_stock_reco_decrease_preserves_ageing(self): + """ + Non-serial stock reconciliation should adjust FIFO by the balance delta. + Decreasing stock consumes old slots; increasing stock adds only the new qty. + """ + + def make_sle( + posting_date, + voucher_type, + voucher_no, + actual_qty, + qty_after, + voucher_detail_no=None, + stock_value_difference=None, + ): + stock_value_difference = actual_qty if stock_value_difference is None else stock_value_difference + + return frappe._dict( + name="Flask Item", + item_name="Flask Item", + description="Flask Item", + item_group=None, + brand=None, + stock_uom="Nos", + actual_qty=actual_qty, + qty_after_transaction=qty_after, + stock_value_difference=stock_value_difference, + valuation_rate=1, + warehouse="WH 1", + posting_date=posting_date, + voucher_type=voucher_type, + voucher_no=voucher_no, + voucher_detail_no=voucher_detail_no, + has_serial_no=False, + has_batch_no=False, + serial_no=None, + batch_no=None, + serial_and_batch_bundle=None, + ) + + filters = frappe._dict(company="_Test Company", to_date="2026-02-15", ranges=["30", "60", "90"]) + sle = [ + make_sle("2025-11-30", "Stock Entry", "001", 100, 100), + make_sle("2025-12-31", "Stock Reconciliation", "002", 0, 60, "SRI-DECREASE", -40), + make_sle("2026-01-31", "Stock Reconciliation", "003", 0, 90, "SRI-INCREASE", 30), + ] + + fifo_slots = FIFOSlots(filters, sle) + + def prepare_stock_reco_voucher_wise_count(): + fifo_slots.stock_reco_voucher_wise_count = frappe._dict({"SRI-DECREASE": 100, "SRI-INCREASE": 60}) + + fifo_slots.prepare_stock_reco_voucher_wise_count = prepare_stock_reco_voucher_wise_count + + slots = fifo_slots.generate() + queue = slots["Flask Item"]["fifo_queue"] + report_data = format_report_data(filters, slots, filters.to_date) + + self.assertEqual(queue, [[60.0, "2025-11-30", 60.0], [30.0, "2026-01-31", 30.0]]) + self.assertEqual(report_data[0][7:15], [30.0, 30.0, 0.0, 0.0, 60.0, 60.0, 0.0, 0.0]) + def test_sequential_stock_reco_same_warehouse(self): """ Test back to back stock recos (same warehouse). From 3f9a88a5e29382582e06ec818e29ee1442078561 Mon Sep 17 00:00:00 2001 From: pandiyan Date: Wed, 17 Jun 2026 17:22:48 +0530 Subject: [PATCH 07/39] fix(stock): update transfer status for mixed transfer flows --- erpnext/stock/doctype/stock_entry/stock_entry.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index aebcc6dffb2..5ee35dbe3e0 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -4116,13 +4116,19 @@ def get_batchwise_serial_nos(item_code, row): def get_transferred_qty(material_request): - sed = DocType("Stock Entry Detail") + from pypika import Case + + se = frappe.qb.DocType("Stock Entry") + sed = frappe.qb.DocType("Stock Entry Detail") + completed_qty = Case().when(se.add_to_transit == 1, sed.transferred_qty).else_(sed.transfer_qty) query = ( frappe.qb.from_(sed) + .inner_join(se) + .on(se.name == sed.parent) .select( Sum(sed.transfer_qty).as_("transfer_qty"), - Sum(sed.transferred_qty).as_("transferred_qty"), + Sum(completed_qty).as_("transferred_qty"), ) .where((sed.material_request == material_request) & (sed.docstatus == 1)) ).run(as_dict=True) From c13567228ed4af3665181581ebb4d90b0e1605fc Mon Sep 17 00:00:00 2001 From: pandiyan Date: Wed, 17 Jun 2026 17:22:53 +0530 Subject: [PATCH 08/39] test(stock): validate completed status for mixed transfer methods --- .../material_request/test_material_request.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 2da1861ba21..ac11b2fb7d9 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -995,6 +995,52 @@ class TestMaterialRequest(FrappeTestCase): se.save() se.submit() + def test_mr_status_for_mixed_direct_and_transit_transfer(self): + material_request = make_material_request( + material_request_type="Material Transfer", + item_code="_Test Item Home Desktop 100", + qty=5, + ) + + in_transit_wh = get_in_transit_warehouse(material_request.company) + + # Make stock available + self._insert_stock_entry(20.0, 20.0) + + # Direct Transfer for 3 Qty + direct_transfer = make_stock_entry(material_request.name) + direct_transfer.items[0].update( + { + "qty": 3, + "transfer_qty": 3, + "s_warehouse": "_Test Warehouse 1 - _TC", + } + ) + direct_transfer.save() + direct_transfer.submit() + + # In Transit Transfer for remaining 2 Qty + transit_transfer = make_in_transit_stock_entry(material_request.name, in_transit_wh) + transit_transfer.items[0].update( + { + "qty": 2, + "s_warehouse": "_Test Warehouse 1 - _TC", + } + ) + transit_transfer.save() + transit_transfer.submit() + + # Complete End Transit + end_transit = make_stock_in_entry(transit_transfer.name) + end_transit.save() + end_transit.submit() + + material_request.reload() + + self.assertEqual(material_request.per_ordered, 100) + self.assertEqual(material_request.status, "Transferred") + self.assertEqual(material_request.transfer_status, "Completed") + def get_in_transit_warehouse(company): if not frappe.db.exists("Warehouse Type", "Transit"): From 27d574dad50d2e44e58a949effbc7700bde11e99 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 17 Jun 2026 17:04:58 +0200 Subject: [PATCH 09/39] fix(stock): propagate renamed attribute values to variant items --- erpnext/controllers/item_variant.py | 43 +++++++++++++++++++ erpnext/stock/doctype/item/test_item.py | 24 +++++++++++ .../doctype/item_attribute/item_attribute.py | 2 + 3 files changed, 69 insertions(+) diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py index 74b7dfb5c24..42692b7528c 100644 --- a/erpnext/controllers/item_variant.py +++ b/erpnext/controllers/item_variant.py @@ -129,6 +129,49 @@ def validate_is_incremental(numeric_attribute, attribute, value, item): ) +def get_attribute_value_renames(item_attribute): + """Return old to new attribute value mappings for renamed Item Attribute Value rows.""" + if item_attribute.numeric_values: + return {} + + db_value = item_attribute.get_doc_before_save() + if not db_value: + return {} + + old_values = {d.name: d.attribute_value for d in db_value.item_attribute_values} + renames = {} + + for row in item_attribute.item_attribute_values: + if row.name in old_values and old_values[row.name] != row.attribute_value: + renames[old_values[row.name]] = row.attribute_value + + return renames + + +def update_variant_attribute_values(item_attribute): + """Propagate renamed Item Attribute Values to Item Variant Attribute on variant items.""" + value_map = get_attribute_value_renames(item_attribute) + if not value_map: + return + + item_variant_table = frappe.qb.DocType("Item Variant Attribute") + item_table = frappe.qb.DocType("Item") + + for old_value, new_value in value_map.items(): + ( + frappe.qb.update(item_variant_table) + .join(item_table) + .on(item_table.name == item_variant_table.parent) + .set(item_variant_table.attribute_value, new_value) + .where(item_table.variant_of.isnotnull()) + .where(item_table.variant_of != "") + .where(item_variant_table.attribute == item_attribute.name) + .where(item_variant_table.attribute_value == old_value) + ).run() + + frappe.flags.attribute_values = None + + def validate_item_attribute_value(attributes_list, attribute, attribute_value, item, from_variant=True): allow_rename_attribute_value = frappe.db.get_single_value( "Item Variant Settings", "allow_rename_attribute_value" diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index d5f13e62a5c..91fa8bece72 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -360,6 +360,30 @@ class TestItem(FrappeTestCase): self.assertRaises(InvalidItemAttributeValueError, attribute.save) frappe.db.rollback() + def test_rename_attribute_value_updates_variants(self): + frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1) + + variant = create_variant("_Test Variant Item", {"Test Size": "Large"}) + variant.save() + + attribute = frappe.get_doc("Item Attribute", "Test Size") + for row in attribute.item_attribute_values: + if row.attribute_value == "Large": + row.attribute_value = "Larger" + break + + frappe.flags.attribute_values = None + attribute.save() + + self.assertEqual( + frappe.db.get_value( + "Item Variant Attribute", + {"parent": variant.name, "attribute": "Test Size"}, + "attribute_value", + ), + "Larger", + ) + def test_make_item_variant(self): frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1) diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.py b/erpnext/stock/doctype/item_attribute/item_attribute.py index 3b9bcf93288..14d2c6a4f12 100644 --- a/erpnext/stock/doctype/item_attribute/item_attribute.py +++ b/erpnext/stock/doctype/item_attribute/item_attribute.py @@ -9,6 +9,7 @@ from frappe.utils import flt from erpnext.controllers.item_variant import ( InvalidItemAttributeValueError, + update_variant_attribute_values, validate_is_incremental, validate_item_attribute_value, ) @@ -47,6 +48,7 @@ class ItemAttribute(Document): self.validate_duplication() def on_update(self): + update_variant_attribute_values(self) self.validate_exising_items() self.set_enabled_disabled_in_items() From 60f5de7ab80926afb710d7350185ce52e90a883c Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 17 Jun 2026 17:34:42 +0200 Subject: [PATCH 10/39] test(stock): add cleanup for item attribute value changes in tests --- erpnext/stock/doctype/item/test_item.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 91fa8bece72..b34ce5bd894 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -372,6 +372,17 @@ class TestItem(FrappeTestCase): row.attribute_value = "Larger" break + def restore_test_size_large(): + doc = frappe.get_doc("Item Attribute", "Test Size") + for row in doc.item_attribute_values: + if row.attribute_value == "Larger": + row.attribute_value = "Large" + break + frappe.flags.attribute_values = None + doc.save() + + self.addCleanup(restore_test_size_large) + frappe.flags.attribute_values = None attribute.save() From c7acd887425e42e15460d588a1735a7122e48736 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 17 Jun 2026 17:55:27 +0200 Subject: [PATCH 11/39] fix(stock): update variant attributes on value rename --- erpnext/controllers/item_variant.py | 25 +++++++------ erpnext/stock/doctype/item/test_item.py | 48 +++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py index 42692b7528c..c2c620950af 100644 --- a/erpnext/controllers/item_variant.py +++ b/erpnext/controllers/item_variant.py @@ -7,6 +7,7 @@ import json import frappe from frappe import _ +from frappe.query_builder import Case from frappe.utils import cstr, flt from erpnext.utilities.product import get_item_codes_by_attributes @@ -156,18 +157,22 @@ def update_variant_attribute_values(item_attribute): item_variant_table = frappe.qb.DocType("Item Variant Attribute") item_table = frappe.qb.DocType("Item") + attribute_value = item_variant_table.attribute_value + attribute_value_case = Case() for old_value, new_value in value_map.items(): - ( - frappe.qb.update(item_variant_table) - .join(item_table) - .on(item_table.name == item_variant_table.parent) - .set(item_variant_table.attribute_value, new_value) - .where(item_table.variant_of.isnotnull()) - .where(item_table.variant_of != "") - .where(item_variant_table.attribute == item_attribute.name) - .where(item_variant_table.attribute_value == old_value) - ).run() + attribute_value_case = attribute_value_case.when(attribute_value == old_value, new_value) + + ( + frappe.qb.update(item_variant_table) + .join(item_table) + .on(item_table.name == item_variant_table.parent) + .set(attribute_value, attribute_value_case.else_(attribute_value)) + .where(item_table.variant_of.isnotnull()) + .where(item_table.variant_of != "") + .where(item_variant_table.attribute == item_attribute.name) + .where(attribute_value.isin(list(value_map))) + ).run() frappe.flags.attribute_values = None diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index b34ce5bd894..8072437a173 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -395,6 +395,54 @@ class TestItem(FrappeTestCase): "Larger", ) + def test_swapped_attribute_value_renames_update_variants(self): + frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1) + frappe.delete_doc_if_exists("Item", "_Test Variant Item-S", force=1) + + large_variant = create_variant("_Test Variant Item", {"Test Size": "Large"}) + large_variant.save() + + small_variant = create_variant("_Test Variant Item", {"Test Size": "Small"}) + small_variant.save() + + attribute = frappe.get_doc("Item Attribute", "Test Size") + original_values = {row.name: row.attribute_value for row in attribute.item_attribute_values} + + def restore_test_size_values(): + doc = frappe.get_doc("Item Attribute", "Test Size") + for row in doc.item_attribute_values: + row.attribute_value = original_values[row.name] + frappe.flags.attribute_values = None + doc.save() + + self.addCleanup(restore_test_size_values) + + for row in attribute.item_attribute_values: + if row.attribute_value == "Large": + row.attribute_value = "Small" + elif row.attribute_value == "Small": + row.attribute_value = "Large" + + frappe.flags.attribute_values = None + attribute.save() + + self.assertEqual( + frappe.db.get_value( + "Item Variant Attribute", + {"parent": large_variant.name, "attribute": "Test Size"}, + "attribute_value", + ), + "Small", + ) + self.assertEqual( + frappe.db.get_value( + "Item Variant Attribute", + {"parent": small_variant.name, "attribute": "Test Size"}, + "attribute_value", + ), + "Large", + ) + def test_make_item_variant(self): frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1) From 98caefea8871053e7e3074faa41ecda5474f0f94 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 18 Jun 2026 11:21:13 +0530 Subject: [PATCH 12/39] refactor: posting datetime for SABB (backport #49710 + #50248) (#56072) * refactor: posting datetime for SABB (backport #49710 + #50248) Backport of #49710 "refactor: posting datetime for SABB" to version-15-hotfix, including the regression fix #50248 "fix: missed refactoring code". Replaces Serial and Batch Bundle's `posting_date`/`posting_time` with a single `posting_datetime` field and updates all readers/writers; adds a patch to populate `posting_datetime` from the linked Stock Ledger Entry and drop the now-unused single-column indexes. Adapted to version-15-hotfix where the surrounding stock code had already moved to `posting_datetime` (SLE) and diverged from develop. Co-Authored-By: Claude Opus 4.8 * chore: fix conflicts * chore: fixed formatting --------- Co-authored-by: Claude Opus 4.8 --- .../controllers/sales_and_purchase_return.py | 5 +- erpnext/controllers/selling_controller.py | 5 +- erpnext/controllers/stock_controller.py | 4 +- .../controllers/subcontracting_controller.py | 4 +- erpnext/patches.txt | 1 + ...ting_datetime_for_sabb_and_drop_indexes.py | 31 ++++ erpnext/stock/deprecated_serial_batch.py | 45 +++--- erpnext/stock/doctype/batch/batch.py | 41 +++--- .../serial_and_batch_bundle.json | 34 ++--- .../serial_and_batch_bundle.py | 134 +++++++----------- .../test_serial_and_batch_bundle.py | 8 +- .../stock/doctype/stock_entry/stock_entry.py | 8 +- .../doctype/stock_entry/stock_entry_utils.py | 8 +- .../stock_reconciliation.py | 10 +- .../serial_and_batch_summary.py | 6 +- erpnext/stock/serial_batch_bundle.py | 55 +++---- erpnext/stock/stock_ledger.py | 8 +- erpnext/stock/utils.py | 24 ++-- 18 files changed, 216 insertions(+), 215 deletions(-) create mode 100644 erpnext/patches/v16_0/set_posting_datetime_for_sabb_and_drop_indexes.py diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index ef5feafb45c..8f6edbe75ae 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -11,7 +11,7 @@ from frappe.utils import cint, flt, format_datetime, get_datetime import erpnext from erpnext.stock.serial_batch_bundle import get_batches_from_bundle from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle -from erpnext.stock.utils import get_incoming_rate, get_valuation_method, getdate +from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_valuation_method, getdate class StockOverReturnError(frappe.ValidationError): @@ -1180,8 +1180,7 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f "batches": data.get("batches"), "serial_nos_valuation": data.get("serial_nos_valuation"), "batches_valuation": data.get("batches_valuation"), - "posting_date": parent_doc.posting_date, - "posting_time": parent_doc.posting_time, + "posting_datetime": get_combine_datetime(parent_doc.posting_date, parent_doc.posting_time), "voucher_type": parent_doc.doctype, "voucher_no": parent_doc.name, "voucher_detail_no": child_doc.name, diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 3383bd51fdd..5655fea9915 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -12,7 +12,7 @@ from erpnext.controllers.sales_and_purchase_return import get_rate_for_return, i from erpnext.controllers.stock_controller import StockController from erpnext.stock.doctype.item.item import set_item_default from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor -from erpnext.stock.utils import get_incoming_rate, get_valuation_method +from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_valuation_method class SellingController(StockController): @@ -1084,8 +1084,7 @@ def get_serial_and_batch_bundle(child, parent, delivery_note_child=None): "voucher_type": parent.doctype, "voucher_no": parent.name if parent.docstatus < 2 else None, "voucher_detail_no": delivery_note_child.name if delivery_note_child else child.name, - "posting_date": parent.posting_date, - "posting_time": parent.posting_time, + "posting_datetime": get_combine_datetime(parent.posting_date, parent.posting_time), "qty": child.qty, "type_of_transaction": "Outward" if child.qty > 0 and parent.docstatus < 2 else "Inward", "company": parent.company, diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 9e681bce814..32968952fc6 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -26,6 +26,7 @@ from erpnext.stock.doctype.inventory_dimension.inventory_dimension import ( get_evaluated_inventory_dimension, ) from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + combine_datetime, get_type_of_transaction, ) from erpnext.stock.stock_ledger import get_items_to_be_repost @@ -282,8 +283,7 @@ class StockController(AccountsController): ): bundle_details = { "item_code": row.get("rm_item_code") or row.item_code, - "posting_date": self.posting_date, - "posting_time": self.posting_time, + "posting_datetime": combine_datetime(self.posting_date, self.posting_time), "voucher_type": self.doctype, "voucher_no": self.name, "voucher_detail_no": row.name, diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index c32edfaa7b2..6b08cc15838 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -13,6 +13,7 @@ from frappe.utils import cint, flt, get_link_to_form from erpnext.controllers.stock_controller import StockController from erpnext.stock.doctype.batch.batch import get_batch_qty from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + combine_datetime, get_auto_batch_nos, get_available_serial_nos, get_voucher_wise_serial_batch_from_bundle, @@ -570,8 +571,7 @@ class SubcontractingController(StockController): "qty": qty, "serial_nos": serial_nos, "batches": batches, - "posting_date": self.posting_date, - "posting_time": self.posting_time, + "posting_datetime": combine_datetime(self.posting_date, self.posting_time), "voucher_type": "Subcontracting Receipt", "do_not_submit": True, "type_of_transaction": "Outward" if qty > 0 else "Inward", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 239066f7c98..3b0ac5cf552 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -436,3 +436,4 @@ erpnext.patches.v16_0.depends_on_inv_dimensions erpnext.patches.v16_0.clear_procedures_from_receivable_report erpnext.patches.v16_0.migrate_address_contact_custom_fields erpnext.patches.v15_0.set_main_item_code_in_material_request_plan_item +erpnext.patches.v16_0.set_posting_datetime_for_sabb_and_drop_indexes diff --git a/erpnext/patches/v16_0/set_posting_datetime_for_sabb_and_drop_indexes.py b/erpnext/patches/v16_0/set_posting_datetime_for_sabb_and_drop_indexes.py new file mode 100644 index 00000000000..c4a71791671 --- /dev/null +++ b/erpnext/patches/v16_0/set_posting_datetime_for_sabb_and_drop_indexes.py @@ -0,0 +1,31 @@ +import click +import frappe + + +def execute(): + frappe.db.sql( + """ + UPDATE `tabSerial and Batch Bundle` + JOIN `tabStock Ledger Entry` + ON `tabSerial and Batch Bundle`.`name` = `tabStock Ledger Entry`.`serial_and_batch_bundle` + SET `tabSerial and Batch Bundle`.`posting_datetime` = `tabStock Ledger Entry`.`posting_datetime` + WHERE `tabStock Ledger Entry`.`is_cancelled` = 0 + """ + ) + + drop_indexes() + + +def drop_indexes(): + table = "tabSerial and Batch Bundle" + index_list = ["voucher_no_index", "item_code_index", "warehouse_index", "company_index"] + + for index in index_list: + if not frappe.db.has_index(table, index): + continue + + try: + frappe.db.sql_ddl(f"ALTER TABLE `{table}` DROP INDEX `{index}`") + click.echo(f"✓ dropped {index} index from {table}") + except Exception: + frappe.log_error("Failed to drop index") diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index 00a06e98d2e..b57b683c2d8 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -36,6 +36,11 @@ class DeprecatedSerialNoValuation: # get rate from serial nos within same company incoming_values = 0.0 + posting_datetime = self.sle.posting_datetime + + if not posting_datetime and self.sle.posting_date: + posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time) + for serial_no in serial_nos: sn_details = frappe.db.get_value("Serial No", serial_no, ["purchase_rate", "company"], as_dict=1) if ( @@ -64,10 +69,7 @@ class DeprecatedSerialNoValuation: & (table.serial_and_batch_bundle.isnull()) & (table.actual_qty > 0) & (table.is_cancelled == 0) - & ( - table.posting_datetime - <= get_combine_datetime(self.sle.posting_date, self.sle.posting_time) - ) + & (table.posting_datetime <= posting_datetime) ) .orderby(table.posting_datetime, order=Order.desc) .limit(1) @@ -98,11 +100,8 @@ class DeprecatedBatchNoValuation: sle = frappe.qb.DocType("Stock Ledger Entry") timestamp_condition = None - if self.sle.posting_date: - if self.sle.posting_time is None: - self.sle.posting_time = nowtime() - - posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time) + if self.sle.posting_datetime: + posting_datetime = self.sle.posting_datetime if not self.sle.creation: posting_datetime = posting_datetime + datetime.timedelta(milliseconds=1) @@ -202,7 +201,11 @@ class DeprecatedBatchNoValuation: sle = frappe.qb.DocType("Stock Ledger Entry") batch = frappe.qb.DocType("Batch") - posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time) + posting_datetime = self.sle.posting_datetime + + if not posting_datetime and self.sle.posting_date: + posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time) + if not self.sle.creation: posting_datetime = posting_datetime + datetime.timedelta(milliseconds=1) @@ -266,7 +269,10 @@ class DeprecatedBatchNoValuation: sle = frappe.qb.DocType("Stock Ledger Entry") - posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time) + posting_datetime = self.sle.posting_datetime + if not posting_datetime and self.sle.posting_date: + posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time) + if not self.sle.creation: posting_datetime = posting_datetime + datetime.timedelta(milliseconds=1) @@ -308,19 +314,22 @@ class DeprecatedBatchNoValuation: @deprecated def set_balance_value_from_bundle(self) -> None: + from erpnext.stock.utils import get_combine_datetime + bundle = frappe.qb.DocType("Serial and Batch Bundle") bundle_child = frappe.qb.DocType("Serial and Batch Entry") batch = frappe.qb.DocType("Batch") - timestamp_condition = CombineDatetime(bundle.posting_date, bundle.posting_time) < CombineDatetime( - self.sle.posting_date, self.sle.posting_time - ) + posting_datetime = self.sle.posting_datetime + if not posting_datetime and self.sle.posting_date: + posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time) + + timestamp_condition = bundle.posting_datetime < posting_datetime if self.sle.creation: - timestamp_condition |= ( - CombineDatetime(bundle.posting_date, bundle.posting_time) - == CombineDatetime(self.sle.posting_date, self.sle.posting_time) - ) & (bundle.creation < self.sle.creation) + timestamp_condition |= (bundle.posting_datetime == posting_datetime) & ( + bundle.creation < self.sle.creation + ) query = ( frappe.qb.from_(bundle) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 6b2b1360cf9..7f31c65dfcf 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt +import datetime from collections import OrderedDict, defaultdict import frappe @@ -10,7 +11,7 @@ from frappe.model.document import Document from frappe.model.naming import make_autoname, revert_series_if_last from frappe.query_builder.functions import CurDate, Sum from frappe.utils import cint, flt, get_link_to_form -from frappe.utils.data import add_days +from frappe.utils.data import DateTimeLikeObject, add_days class UnableToSelectBatchError(frappe.ValidationError): @@ -232,17 +233,18 @@ class Batch(Document): @frappe.whitelist() def get_batch_qty( - batch_no=None, - warehouse=None, - item_code=None, - creation=None, - posting_date=None, - posting_time=None, - ignore_voucher_nos=None, - for_stock_levels=False, - consider_negative_batches=False, - do_not_check_future_batches=False, - ignore_reserved_stock=False, + batch_no: str | None = None, + warehouse: str | None = None, + item_code: str | None = None, + creation: DateTimeLikeObject | None = None, + posting_datetime: DateTimeLikeObject | None = None, + posting_date: DateTimeLikeObject | None = None, + posting_time: datetime.timedelta | None = None, + ignore_voucher_nos: list | None = None, + for_stock_levels: bool = False, + consider_negative_batches: bool = False, + do_not_check_future_batches: bool = False, + ignore_reserved_stock: bool = False, ): """Returns batch actual qty if warehouse is passed, or returns dict of qty by warehouse if warehouse is None @@ -255,6 +257,7 @@ def get_batch_qty( :param for_stock_levels: True consider expired batches""" from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + combine_datetime, get_auto_batch_nos, ) @@ -264,8 +267,6 @@ def get_batch_qty( "item_code": item_code, "warehouse": warehouse, "creation": creation, - "posting_date": posting_date, - "posting_time": posting_time, "batch_no": batch_no, "based_on": frappe.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"), "ignore_voucher_nos": ignore_voucher_nos, @@ -276,6 +277,10 @@ def get_batch_qty( } ) + kwargs["posting_datetime"] = posting_datetime + if not kwargs.get("posting_datetime") and posting_date: + kwargs["posting_datetime"] = combine_datetime(posting_date, posting_time) + batches = get_auto_batch_nos(kwargs) if not (batch_no and warehouse): @@ -357,6 +362,7 @@ def make_batch_bundle( ): from frappe.utils import nowtime, today + from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import combine_datetime from erpnext.stock.serial_batch_bundle import SerialBatchCreation return ( @@ -364,8 +370,7 @@ def make_batch_bundle( { "item_code": item_code, "warehouse": warehouse, - "posting_date": today(), - "posting_time": nowtime(), + "posting_datetime": combine_datetime(today(), nowtime()), "voucher_type": "Stock Entry", "qty": qty, "type_of_transaction": type_of_transaction, @@ -476,9 +481,13 @@ def get_pos_reserved_batch_qty(filters): def get_available_batches(kwargs): from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + combine_datetime, get_auto_batch_nos, ) + if kwargs.get("posting_date"): + kwargs["posting_datetime"] = combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time")) + batchwise_qty = OrderedDict() batches = get_auto_batch_nos(kwargs) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json index 0a24bfb5645..864a0c51008 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json @@ -29,8 +29,7 @@ "voucher_no", "voucher_detail_no", "column_break_aouy", - "posting_date", - "posting_time", + "posting_datetime", "returned_against", "section_break_wzou", "is_cancelled", @@ -49,8 +48,7 @@ "in_list_view": 1, "label": "Company", "options": "Company", - "reqd": 1, - "search_index": 1 + "reqd": 1 }, { "fetch_from": "item_code.item_group", @@ -79,8 +77,7 @@ "in_standard_filter": 1, "label": "Item Code", "options": "Item", - "reqd": 1, - "search_index": 1 + "reqd": 1 }, { "fetch_from": "item_code.item_name", @@ -117,8 +114,7 @@ "in_standard_filter": 1, "label": "Voucher No", "no_copy": 1, - "options": "voucher_type", - "search_index": 1 + "options": "voucher_type" }, { "default": "0", @@ -188,8 +184,7 @@ "in_standard_filter": 1, "label": "Warehouse", "mandatory_depends_on": "eval:doc.type_of_transaction != \"Maintenance\"", - "options": "Warehouse", - "search_index": 1 + "options": "Warehouse" }, { "fieldname": "type_of_transaction", @@ -211,18 +206,6 @@ "fieldname": "section_break_wzou", "fieldtype": "Section Break" }, - { - "fieldname": "posting_date", - "fieldtype": "Date", - "label": "Posting Date", - "no_copy": 1 - }, - { - "fieldname": "posting_time", - "fieldtype": "Time", - "label": "Posting Time", - "no_copy": 1 - }, { "fieldname": "voucher_detail_no", "fieldtype": "Data", @@ -251,12 +234,17 @@ "label": "Naming Series", "options": "\nSABB-.########", "set_only_once": 1 + }, + { + "fieldname": "posting_datetime", + "fieldtype": "Datetime", + "label": "Posting Datetime" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-09-15 14:37:26.441742", + "modified": "2025-09-24 16:24:48.154853", "modified_by": "Administrator", "module": "Stock", "name": "Serial and Batch Bundle", 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 39e5f1425f0..ce185e40a31 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 @@ -74,8 +74,7 @@ class SerialandBatchBundle(Document): item_group: DF.Link | None item_name: DF.Data | None naming_series: DF.Literal["", "SABB-.########"] - posting_date: DF.Date | None - posting_time: DF.Time | None + posting_datetime: DF.Datetime | None returned_against: DF.Data | None total_amount: DF.Float total_qty: DF.Float @@ -280,8 +279,7 @@ class SerialandBatchBundle(Document): kwargs.update( { "voucher_no": self.voucher_no, - "posting_date": self.posting_date, - "posting_time": self.posting_time, + "posting_datetime": self.posting_datetime, } ) @@ -332,8 +330,7 @@ class SerialandBatchBundle(Document): kwargs = frappe._dict( { "item_code": self.item_code, - "posting_date": self.posting_date, - "posting_time": self.posting_time, + "posting_datetime": self.posting_datetime, "serial_nos": serial_nos, "check_serial_nos": True, } @@ -669,8 +666,7 @@ class SerialandBatchBundle(Document): def get_sle_for_outward_transaction(self): sle = frappe._dict( { - "posting_date": self.posting_date, - "posting_time": self.posting_time, + "posting_datetime": self.posting_datetime, "item_code": self.item_code, "warehouse": self.warehouse, "serial_and_batch_bundle": self.name, @@ -829,11 +825,10 @@ class SerialandBatchBundle(Document): if not self.voucher_detail_no or self.voucher_detail_no != row.name: values_to_set["voucher_detail_no"] = row.name - if parent.get("posting_date") and (not self.posting_date or self.posting_date != parent.posting_date): - values_to_set["posting_date"] = parent.posting_date or today() - - if parent.get("posting_time") and (not self.posting_time or self.posting_time != parent.posting_time): - values_to_set["posting_time"] = parent.posting_time + if parent.get("posting_date") and parent.get("posting_time"): + posting_datetime = combine_datetime(parent.posting_date, parent.posting_time) + if not self.posting_datetime or self.posting_datetime != posting_datetime: + values_to_set["posting_datetime"] = posting_datetime if row.get("doctype") == "Packed Item" and row.get("parent_detail_docname"): values_to_set["voucher_detail_no"] = row.get("parent_detail_docname") @@ -911,9 +906,7 @@ class SerialandBatchBundle(Document): parent = frappe.qb.DocType("Serial and Batch Bundle") child = frappe.qb.DocType("Serial and Batch Entry") - timestamp_condition = CombineDatetime(parent.posting_date, parent.posting_time) > CombineDatetime( - self.posting_date, self.posting_time - ) + timestamp_condition = parent.posting_datetime > self.posting_datetime future_entries = ( frappe.qb.from_(parent) @@ -2083,6 +2076,8 @@ def create_serial_batch_no_ledgers( if parent_doc.get("doctype") == "Stock Entry": warehouse = warehouse or child_row.s_warehouse or child_row.t_warehouse + posting_datetime = combine_datetime(parent_doc.get("posting_date"), parent_doc.get("posting_time")) + doc = frappe.get_doc( { "doctype": "Serial and Batch Bundle", @@ -2091,8 +2086,7 @@ def create_serial_batch_no_ledgers( "warehouse": warehouse, "is_rejected": child_row.is_rejected, "type_of_transaction": type_of_transaction, - "posting_date": parent_doc.get("posting_date"), - "posting_time": parent_doc.get("posting_time"), + "posting_datetime": posting_datetime, "company": parent_doc.get("company"), } ) @@ -2128,6 +2122,12 @@ def create_serial_batch_no_ledgers( return doc +def combine_datetime(date, time=None): + from erpnext.stock.utils import get_combine_datetime + + return get_combine_datetime(date, time) + + def get_batch(item_code): from erpnext.stock.doctype.batch.batch import make_batch @@ -2180,8 +2180,8 @@ def update_serial_batch_no_ledgers(bundle, entries, child_row, parent_doc, wareh ) doc.voucher_detail_no = child_row.name - doc.posting_date = parent_doc.posting_date - doc.posting_time = parent_doc.posting_time + doc.posting_datetime = combine_datetime(parent_doc.get("posting_date"), parent_doc.get("posting_time")) + doc.warehouse = warehouse or doc.warehouse doc.set("entries", []) @@ -2269,6 +2269,9 @@ def get_available_serial_nos(kwargs): elif kwargs.based_on == "Expiry": order_by = "amc_expiry_date" + if not kwargs.get("posting_datetime") and kwargs.get("posting_date"): + kwargs["posting_datetime"] = combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time")) + filters = {"item_code": kwargs.item_code} # ignore_warehouse is used for backdated stock transactions @@ -2285,10 +2288,8 @@ def get_available_serial_nos(kwargs): if kwargs.get("ignore_serial_nos"): ignore_serial_nos.extend(kwargs.get("ignore_serial_nos")) - if kwargs.get("posting_date"): - if kwargs.get("posting_time") is None: - kwargs.posting_time = nowtime() - + ignore_serial_nos = list(set(ignore_serial_nos)) + if kwargs.get("posting_datetime"): time_based_serial_nos = get_serial_nos_based_on_posting_date(kwargs, ignore_serial_nos) if not time_based_serial_nos: @@ -2692,6 +2693,9 @@ def get_reserved_batches_for_sre(kwargs) -> dict: def get_auto_batch_nos(kwargs): + if not kwargs.get("posting_datetime") and kwargs.get("posting_date"): + kwargs["posting_datetime"] = combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time")) + available_batches = get_available_batches(kwargs) qty = flt(kwargs.qty) @@ -2721,7 +2725,7 @@ def get_auto_batch_nos(kwargs): if kwargs.based_on == "Expiry": available_batches = sorted(available_batches, key=lambda x: x.expiry_date or getdate("9999-12-31")) - if not kwargs.get("do_not_check_future_batches") and available_batches and kwargs.get("posting_date"): + if not kwargs.get("do_not_check_future_batches") and available_batches and kwargs.get("posting_datetime"): filter_zero_near_batches(available_batches, kwargs) if not kwargs.consider_negative_batches: @@ -2737,8 +2741,7 @@ def get_auto_batch_nos(kwargs): def filter_zero_near_batches(available_batches, kwargs): kwargs.batch_no = [d.batch_no for d in available_batches] - del kwargs["posting_date"] - del kwargs["posting_time"] + del kwargs["posting_datetime"] kwargs.do_not_check_future_batches = 1 available_batches_in_future = get_auto_batch_nos(kwargs) @@ -2804,8 +2807,6 @@ def update_available_batches(available_batches, *reserved_batches) -> None: def get_available_batches(kwargs): - from erpnext.stock.utils import get_combine_datetime - stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry") batch_ledger = frappe.qb.DocType("Serial and Batch Entry") batch_table = frappe.qb.DocType("Batch") @@ -2833,23 +2834,15 @@ def get_available_batches(kwargs): if not kwargs.get("for_stock_levels"): query = query.where((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull())) - if kwargs.get("posting_date"): - if kwargs.get("posting_time") is None: - kwargs.posting_time = nowtime() - - timestamp_condition = stock_ledger_entry.posting_datetime <= get_combine_datetime( - kwargs.posting_date, kwargs.posting_time - ) + if kwargs.get("posting_datetime"): + timestamp_condition = stock_ledger_entry.posting_datetime <= kwargs.posting_datetime if kwargs.get("creation"): - timestamp_condition = stock_ledger_entry.posting_datetime < get_combine_datetime( - kwargs.posting_date, kwargs.posting_time - ) + timestamp_condition = stock_ledger_entry.posting_datetime < kwargs.posting_datetime - timestamp_condition |= ( - stock_ledger_entry.posting_datetime - == get_combine_datetime(kwargs.posting_date, kwargs.posting_time) - ) & (stock_ledger_entry.creation < kwargs.creation) + timestamp_condition |= (stock_ledger_entry.posting_datetime == kwargs.posting_datetime) & ( + stock_ledger_entry.creation < kwargs.creation + ) query = query.where(timestamp_condition) @@ -3035,15 +3028,14 @@ def get_ledgers_from_serial_batch_bundle(**kwargs) -> list[frappe._dict]: serial_batch_table.incoming_rate, bundle_table.voucher_detail_no, bundle_table.voucher_no, - bundle_table.posting_date, - bundle_table.posting_time, + bundle_table.posting_datetime, ) .where( (bundle_table.docstatus == 1) & (bundle_table.is_cancelled == 0) & (bundle_table.type_of_transaction.isin(["Inward", "Outward"])) ) - .orderby(bundle_table.posting_date, bundle_table.posting_time) + .orderby(bundle_table.posting_datetime) ) for key, val in kwargs.items(): @@ -3061,7 +3053,7 @@ def get_ledgers_from_serial_batch_bundle(**kwargs) -> list[frappe._dict]: query = query.where(bundle_table[key].isin(val)) else: query = query.where(bundle_table[key] == val) - elif key in ["posting_date", "posting_time"]: + elif key in ["posting_datetime"]: query = query.where(bundle_table[key] >= val) else: if isinstance(val, list): @@ -3073,8 +3065,6 @@ def get_ledgers_from_serial_batch_bundle(**kwargs) -> list[frappe._dict]: def get_stock_ledgers_for_serial_nos(kwargs): - from erpnext.stock.utils import get_combine_datetime - stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry") query = ( @@ -3090,23 +3080,15 @@ def get_stock_ledgers_for_serial_nos(kwargs): .orderby(stock_ledger_entry.creation) ) - if kwargs.get("posting_date"): - if kwargs.get("posting_time") is None: - kwargs.posting_time = nowtime() - - timestamp_condition = stock_ledger_entry.posting_datetime <= get_combine_datetime( - kwargs.posting_date, kwargs.posting_time - ) + if kwargs.get("posting_datetime"): + timestamp_condition = stock_ledger_entry.posting_datetime <= kwargs.posting_datetime if kwargs.get("creation"): - timestamp_condition = stock_ledger_entry.posting_datetime < get_combine_datetime( - kwargs.posting_date, kwargs.posting_time - ) + timestamp_condition = stock_ledger_entry.posting_datetime < kwargs.posting_datetime - timestamp_condition |= ( - stock_ledger_entry.posting_datetime - == get_combine_datetime(kwargs.posting_date, kwargs.posting_time) - ) & (stock_ledger_entry.creation < kwargs.creation) + timestamp_condition |= (stock_ledger_entry.posting_datetime == kwargs.posting_datetime) & ( + stock_ledger_entry.creation < kwargs.creation + ) query = query.where(timestamp_condition) @@ -3129,8 +3111,6 @@ def get_stock_ledgers_for_serial_nos(kwargs): def get_stock_ledgers_batches(kwargs): - from erpnext.stock.utils import get_combine_datetime - stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry") batch_table = frappe.qb.DocType("Batch") @@ -3164,23 +3144,15 @@ def get_stock_ledgers_batches(kwargs): if not kwargs.get("for_stock_levels"): query = query.where((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull())) - if kwargs.get("posting_date"): - if kwargs.get("posting_time") is None: - kwargs.posting_time = nowtime() - - timestamp_condition = stock_ledger_entry.posting_datetime <= get_combine_datetime( - kwargs.posting_date, kwargs.posting_time - ) + if kwargs.get("posting_datetime"): + timestamp_condition = stock_ledger_entry.posting_datetime <= kwargs.posting_datetime if kwargs.get("creation"): - timestamp_condition = stock_ledger_entry.posting_datetime < get_combine_datetime( - kwargs.posting_date, kwargs.posting_time - ) + timestamp_condition = stock_ledger_entry.posting_datetime < kwargs.posting_datetime - timestamp_condition |= ( - stock_ledger_entry.posting_datetime - == get_combine_datetime(kwargs.posting_date, kwargs.posting_time) - ) & (stock_ledger_entry.creation < kwargs.creation) + timestamp_condition |= (stock_ledger_entry.posting_datetime == kwargs.posting_datetime) & ( + stock_ledger_entry.creation < kwargs.creation + ) query = query.where(timestamp_condition) @@ -3268,3 +3240,7 @@ def get_stock_reco_details(voucher_detail_no): ], as_dict=True, ) + + +def on_doctype_update(): + frappe.db.add_index("Serial and Batch Bundle", ["item_code", "warehouse", "posting_datetime", "creation"]) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index 2c184cbb876..2cee2bb6f0a 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -10,6 +10,7 @@ from frappe.utils import flt, nowtime, today from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( add_serial_batch_ledgers, + combine_datetime, make_batch_nos, make_serial_nos, ) @@ -1271,14 +1272,17 @@ def make_serial_batch_bundle(kwargs): if kwargs.get("type_of_transaction"): type_of_transaction = kwargs.get("type_of_transaction") + posting_datetime = None + if kwargs.get("posting_date"): + posting_datetime = combine_datetime(kwargs.posting_date, kwargs.posting_time or nowtime()) + sb = SerialBatchCreation( { "item_code": kwargs.item_code, "warehouse": kwargs.warehouse, "voucher_type": kwargs.voucher_type, "voucher_no": kwargs.voucher_no, - "posting_date": kwargs.posting_date, - "posting_time": kwargs.posting_time, + "posting_datetime": posting_datetime, "qty": kwargs.qty, "avg_rate": kwargs.rate, "batches": kwargs.batches, diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 5ee35dbe3e0..3ed696e0a02 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -52,7 +52,7 @@ from erpnext.stock.serial_batch_bundle import ( get_serial_or_batch_items, ) from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle, get_valuation_rate -from erpnext.stock.utils import get_bin, get_incoming_rate +from erpnext.stock.utils import get_bin, get_combine_datetime, get_incoming_rate class FinishedGoodError(frappe.ValidationError): @@ -1527,8 +1527,7 @@ class StockEntry(StockController): { "item_code": row.item_code, "warehouse": row.s_warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, + "posting_datetime": get_combine_datetime(self.posting_date, self.posting_time), "voucher_type": self.doctype, "voucher_detail_no": row.name, "qty": row.transfer_qty * -1, @@ -4058,8 +4057,7 @@ def create_serial_and_batch_bundle(parent_doc, row, child, type_of_transaction=N "item_code": child.item_code, "warehouse": child.warehouse, "type_of_transaction": type_of_transaction, - "posting_date": parent_doc.posting_date, - "posting_time": parent_doc.posting_time, + "posting_datetime": get_combine_datetime(parent_doc.posting_date, parent_doc.posting_time), } ) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index e5f4e73e443..1b4c3d7407f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -8,6 +8,7 @@ import frappe from frappe.utils import cint, flt import erpnext +from erpnext.stock.utils import get_combine_datetime if TYPE_CHECKING: from erpnext.stock.doctype.stock_entry.stock_entry import StockEntry @@ -140,6 +141,10 @@ def make_stock_entry(**args): elif args.batches: batches = args.batches + posting_datetime = None + if args.posting_date and args.posting_time: + posting_datetime = get_combine_datetime(args.posting_date, args.posting_time) + bundle_id = ( SerialBatchCreation( { @@ -151,8 +156,7 @@ def make_stock_entry(**args): "serial_nos": args.serial_no, "type_of_transaction": "Outward" if args.source else "Inward", "company": s.company, - "posting_date": s.posting_date, - "posting_time": s.posting_time, + "posting_datetime": posting_datetime, "rate": args.rate or args.basic_rate, "do_not_submit": True, } diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index b3e330a0a85..67f9b57c172 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -13,6 +13,7 @@ from erpnext.controllers.stock_controller import StockController from erpnext.stock.doctype.batch.batch import get_available_batches, get_batch_qty from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + combine_datetime, get_available_serial_nos, ) from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -136,8 +137,7 @@ class StockReconciliation(StockController): { "item_code": row.item_code, "warehouse": row.warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, + "posting_datetime": combine_datetime(self.posting_date, self.posting_time), "voucher_type": self.doctype, "voucher_no": self.name, "voucher_detail_no": row.name, @@ -243,8 +243,7 @@ class StockReconciliation(StockController): "doctype": "Serial and Batch Bundle", "item_code": item.item_code, "warehouse": item.warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, + "posting_datetime": combine_datetime(self.posting_date, self.posting_time), "voucher_type": self.doctype, "type_of_transaction": "Outward", } @@ -262,8 +261,7 @@ class StockReconciliation(StockController): { "item_code": item.item_code, "warehouse": item.warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, + "posting_datetime": combine_datetime(self.posting_date, self.posting_time), "ignore_warehouse": 1, } ) diff --git a/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py index 486828af1cc..f821899e209 100644 --- a/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py +++ b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py @@ -19,7 +19,7 @@ def get_data(filters): "Serial and Batch Bundle", fields=[ "`tabSerial and Batch Bundle`.`voucher_type`", - "`tabSerial and Batch Bundle`.`posting_date`", + "`tabSerial and Batch Bundle`.`posting_datetime` as posting_date", "`tabSerial and Batch Bundle`.`name`", "`tabSerial and Batch Bundle`.`company`", "`tabSerial and Batch Bundle`.`voucher_no`", @@ -33,7 +33,7 @@ def get_data(filters): "`tabSerial and Batch Entry`.`qty`", ], filters=filter_conditions, - order_by="posting_date", + order_by="posting_datetime", ) @@ -54,7 +54,7 @@ def get_filter_conditions(filters): filter_conditions.append( [ "Serial and Batch Bundle", - "posting_date", + "posting_datetime", "between", [filters.get("from_date"), filters.get("to_date")], ] diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index b2eba994538..1b030dbe2fa 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -5,7 +5,7 @@ from frappe import _, bold from frappe.model.naming import NamingSeries, make_autoname, parse_naming_series from frappe.query_builder import Case from frappe.query_builder.functions import CombineDatetime, Max, Sum, Timestamp -from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, now, nowtime, today +from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, now, nowtime, today from pypika import Order from pypika.terms import ExistsCriterion @@ -140,8 +140,7 @@ class SerialBatchBundle: { "item_code": self.item_code, "warehouse": self.warehouse, - "posting_date": self.sle.posting_date, - "posting_time": self.sle.posting_time, + "posting_datetime": self.sle.posting_datetime, "voucher_type": self.sle.voucher_type, "voucher_no": self.sle.voucher_no, "voucher_detail_no": self.sle.voucher_detail_no, @@ -484,7 +483,7 @@ class SerialBatchBundle: if status == "Delivered": warranty_period = frappe.get_cached_value("Item", sle.item_code, "warranty_period") if warranty_period: - warranty_expiry_date = add_days(sle.posting_date, cint(warranty_period)) + warranty_expiry_date = add_days(getdate(sle.posting_datetime), cint(warranty_period)) query = query.set(sn_table.warranty_expiry_date, warranty_expiry_date) query = query.set(sn_table.warranty_period, warranty_period) else: @@ -509,7 +508,7 @@ class SerialBatchBundle: sle_doctype.voucher_no, sle_doctype.is_cancelled, sle_doctype.item_code, - sle_doctype.posting_date, + sle_doctype.posting_datetime, sle_doctype.company, ) .where( @@ -663,7 +662,7 @@ class SerialNoValuation(DeprecatedSerialNoValuation): .on(bundle.name == bundle_child.parent) .select( bundle_child.serial_no, - Max(CombineDatetime(bundle.posting_date, bundle.posting_time)).as_("max_posting_dt"), + Max(bundle.posting_datetime).as_("max_posting_dt"), ) .where( (bundle.is_cancelled == 0) @@ -681,13 +680,8 @@ class SerialNoValuation(DeprecatedSerialNoValuation): if self.sle.voucher_no: latest_posting = latest_posting.where(bundle.voucher_no != self.sle.voucher_no) - if self.sle.posting_date: - if self.sle.posting_time is None: - self.sle.posting_time = nowtime() - - timestamp_condition = CombineDatetime( - bundle.posting_date, bundle.posting_time - ) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time) + if self.sle.posting_datetime: + timestamp_condition = bundle.posting_datetime <= self.sle.posting_datetime latest_posting = latest_posting.where(timestamp_condition) @@ -704,10 +698,7 @@ class SerialNoValuation(DeprecatedSerialNoValuation): .join(latest_posting) .on( (latest_posting.serial_no == bundle_child.serial_no) - & ( - latest_posting.max_posting_dt - == CombineDatetime(bundle.posting_date, bundle.posting_time) - ) + & (latest_posting.max_posting_dt == bundle.posting_datetime) ) .select( bundle_child.serial_no, @@ -839,19 +830,13 @@ class BatchNoValuation(DeprecatedBatchNoValuation): child = frappe.qb.DocType("Serial and Batch Entry") timestamp_condition = "" - if self.sle.posting_date: - if self.sle.posting_time is None: - self.sle.posting_time = nowtime() - - timestamp_condition = CombineDatetime(parent.posting_date, parent.posting_time) < CombineDatetime( - self.sle.posting_date, self.sle.posting_time - ) + if self.sle.posting_datetime: + timestamp_condition = parent.posting_datetime < self.sle.posting_datetime if self.sle.creation: - timestamp_condition |= ( - CombineDatetime(parent.posting_date, parent.posting_time) - == CombineDatetime(self.sle.posting_date, self.sle.posting_time) - ) & (parent.creation < self.sle.creation) + timestamp_condition |= (parent.posting_datetime == self.sle.posting_datetime) & ( + parent.creation < self.sle.creation + ) query = ( frappe.qb.from_(parent) @@ -1074,9 +1059,9 @@ class SerialBatchCreation: self.__dict__.update(item_details) def set_other_details(self): - if not self.get("posting_date"): - self.posting_date = today() - self.__dict__["posting_date"] = self.posting_date + if not self.get("posting_datetime"): + self.posting_datetime = now() + self.__dict__["posting_datetime"] = self.posting_datetime if not self.get("actual_qty"): qty = self.get("qty") or self.get("total_qty") @@ -1101,8 +1086,7 @@ class SerialBatchCreation: new_package.docstatus = 0 new_package.warehouse = self.warehouse new_package.voucher_no = "" - new_package.posting_date = self.posting_date if hasattr(self, "posting_date") else today() - new_package.posting_time = self.posting_time if hasattr(self, "posting_time") else nowtime() + new_package.posting_datetime = self.posting_datetime if hasattr(self, "posting_datetime") else now() new_package.type_of_transaction = self.type_of_transaction new_package.returned_against = self.get("returned_against") @@ -1242,9 +1226,8 @@ class SerialBatchCreation: elif self.has_serial_no and not self.get("serial_nos"): self.serial_nos = get_serial_nos_for_outward(kwargs) elif not self.has_serial_no and self.has_batch_no and not self.get("batches"): - if self.get("posting_date"): - kwargs["posting_date"] = self.get("posting_date") - kwargs["posting_time"] = self.get("posting_time") + if self.get("posting_datetime"): + kwargs["posting_datetime"] = self.get("posting_datetime") self.batches = get_available_batches(kwargs) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 7ae0a2fda49..bcc5b3e8a26 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1975,9 +1975,6 @@ def get_valuation_rate( # Get moving average rate of a specific batch number if warehouse and serial_and_batch_bundle: - sabb = frappe.db.get_value( - "Serial and Batch Bundle", serial_and_batch_bundle, ["posting_date", "posting_time"], as_dict=True - ) batch_obj = BatchNoValuation( sle=frappe._dict( { @@ -1985,8 +1982,9 @@ def get_valuation_rate( "warehouse": warehouse, "actual_qty": -1, "serial_and_batch_bundle": serial_and_batch_bundle, - "posting_date": sabb.posting_date, - "posting_time": sabb.posting_time, + "posting_datetime": frappe.get_value( + "Serial and Batch Bundle", serial_and_batch_bundle, "posting_datetime" + ), } ) ) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 8dfcf5a833e..92a42ec714c 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -2,12 +2,14 @@ # License: GNU General Public License v3. See license.txt +import datetime import json import frappe from frappe import _ from frappe.query_builder.functions import CombineDatetime, IfNull, Sum from frappe.utils import cstr, flt, get_link_to_form, get_time, getdate, nowdate, nowtime +from frappe.utils.data import DateTimeLikeObject import erpnext from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions @@ -94,13 +96,13 @@ def get_stock_value_on( @frappe.whitelist() def get_stock_balance( - item_code, - warehouse, - posting_date=None, - posting_time=None, - with_valuation_rate=False, - with_serial_no=False, - inventory_dimensions_dict=None, + item_code: str, + warehouse: str | None, + posting_date: DateTimeLikeObject | None = None, + posting_time: DateTimeLikeObject | datetime.timedelta | None = None, + with_valuation_rate: bool = False, + with_serial_no: bool = False, + inventory_dimensions_dict: dict | None = None, ): """Returns stock balance quantity at given warehouse on given posting date or current date. @@ -146,8 +148,7 @@ def get_stock_balance( { "item_code": item_code, "warehouse": warehouse, - "posting_date": posting_date, - "posting_time": posting_time, + "posting_datetime": get_combine_datetime(posting_date, posting_time), "ignore_warehouse": 1, } ) @@ -247,13 +248,16 @@ def _create_bin(item_code, warehouse): @frappe.whitelist() -def get_incoming_rate(args, raise_error_if_no_rate=True, fallbacks: bool = True): +def get_incoming_rate(args: dict | str, raise_error_if_no_rate: bool = True, fallbacks: bool = True): """Get Incoming Rate based on valuation method""" from erpnext.stock.stock_ledger import get_previous_sle, get_valuation_rate if isinstance(args, str): args = json.loads(args) + if not args.get("posting_datetime") and args.get("posting_date"): + args["posting_datetime"] = get_combine_datetime(args.get("posting_date"), args.get("posting_time")) + in_rate = None item_details = frappe.get_cached_value( From 5c4f19ebdc10ca97337babf3c366d44d44931d33 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 18 Jun 2026 13:35:33 +0530 Subject: [PATCH 13/39] feat: add batch-level option to allow negative stock for batch --- erpnext/stock/doctype/batch/batch.json | 11 ++++++++++- erpnext/stock/doctype/batch/batch.py | 1 + .../serial_and_batch_bundle.py | 17 +++++++++++++++-- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.json b/erpnext/stock/doctype/batch/batch.json index e318f113713..00541250531 100644 --- a/erpnext/stock/doctype/batch/batch.json +++ b/erpnext/stock/doctype/batch/batch.json @@ -11,6 +11,7 @@ "disabled", "column_break_24", "use_batchwise_valuation", + "allow_negative_stock_for_batch", "sb_batch", "batch_id", "item", @@ -202,6 +203,14 @@ "label": "Use Batch-wise Valuation", "read_only": 1, "set_only_once": 1 + }, + { + "default": "0", + "description": "If enabled, the system will allow negative stock entries for this batch, overriding the 'Allow negative stock for Batch' setting in Stock Settings. This may lead to incorrect valuation rates, so it is recommended to avoid using this option.", + "fieldname": "allow_negative_stock_for_batch", + "fieldtype": "Check", + "label": "Allow Negative Stock for Batch", + "no_copy": 1 } ], "icon": "fa fa-archive", @@ -209,7 +218,7 @@ "image_field": "image", "links": [], "max_attachments": 5, - "modified": "2026-06-16 16:01:26.556324", + "modified": "2026-06-17 12:17:28.339975", "modified_by": "Administrator", "module": "Stock", "name": "Batch", diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 7f31c65dfcf..b3a85b0bdd6 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -95,6 +95,7 @@ class Batch(Document): if TYPE_CHECKING: from frappe.types import DF + allow_negative_stock_for_batch: DF.Check batch_id: DF.Data batch_qty: DF.Float description: DF.SmallText | None 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 ce185e40a31..f8facea5f78 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 @@ -1508,7 +1508,7 @@ class SerialandBatchBundle(Document): 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"): + if allow_negative_stock_for_batch(batch_no): return date_msg = "" @@ -1519,7 +1519,7 @@ class SerialandBatchBundle(Document): """ 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. + If it is not possible to make an adjustment entry, please enable 'Allow Negative Stock for Batch' in the batch {0} or 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( @@ -2128,6 +2128,19 @@ def combine_datetime(date, time=None): return get_combine_datetime(date, time) +def allow_negative_stock_for_batch(batch_no): + """Return whether negative stock is allowed for the given batch. + + The batch-level setting takes priority: if `allow_negative_stock_for_batch` + is enabled on the Batch, negative stock is allowed regardless of Stock Settings. + Otherwise, fall back to the `allow_negative_stock_for_batch` Stock Setting. + """ + if batch_no and frappe.db.get_value("Batch", batch_no, "allow_negative_stock_for_batch"): + return True + + return bool(frappe.db.get_single_value("Stock Settings", "allow_negative_stock_for_batch")) + + def get_batch(item_code): from erpnext.stock.doctype.batch.batch import make_batch From ce8fce78f15694bb4ccb668138471d26ff8eb555 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:32:54 +0000 Subject: [PATCH 14/39] fix: Add likely missing escapes (backport #55574) (#55580) * fix: Add likely missing escaps (#55574) (cherry picked from commit b72cde73ba355a0451a0236df1303b3c8602cd28) # Conflicts: # erpnext/accounts/doctype/budget/budget.py # erpnext/controllers/website_list_for_contact.py * chore: conflicts --------- Co-authored-by: Ankush Menat --- erpnext/accounts/doctype/budget/budget.py | 7 +++---- .../doctype/journal_entry/journal_entry.py | 6 +++++- .../bulk_transaction_log.py | 3 ++- erpnext/controllers/status_updater.py | 17 ++++++++------- .../controllers/website_list_for_contact.py | 2 +- .../inactive_customers/inactive_customers.py | 3 +++ .../report/sales_analytics/sales_analytics.py | 21 ++++++++++++------- .../authorization_control.py | 8 ++++--- .../material_request/material_request.py | 2 +- erpnext/stock/stock_balance.py | 2 +- 10 files changed, 44 insertions(+), 27 deletions(-) diff --git a/erpnext/accounts/doctype/budget/budget.py b/erpnext/accounts/doctype/budget/budget.py index 32225fff93d..4ef1f164aaa 100644 --- a/erpnext/accounts/doctype/budget/budget.py +++ b/erpnext/accounts/doctype/budget/budget.py @@ -425,11 +425,11 @@ def get_ordered_amount(args): def get_other_condition(args, for_doc): - condition = "expense_account = '%s'" % (args.expense_account) + condition = f"expense_account = {frappe.db.escape(args.expense_account)}" budget_against_field = args.get("budget_against_field") if budget_against_field and args.get(budget_against_field): - condition += f" and child.{budget_against_field} = '{args.get(budget_against_field)}'" + condition += f" and child.{budget_against_field} = {frappe.db.escape(args.get(budget_against_field))}" if args.get("fiscal_year"): date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date" @@ -437,8 +437,7 @@ def get_other_condition(args, for_doc): "Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"] ) - condition += f""" and parent.{date_field} - between '{start_date}' and '{end_date}' """ + condition += f" and parent.{date_field} between {frappe.db.escape(str(start_date))} and {frappe.db.escape(str(end_date))}" return condition diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 502a4f9e015..aa048a71ff2 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -1184,7 +1184,11 @@ class JournalEntry(AccountsController): self.validate_total_debit_and_credit() def get_values(self): - cond = f" and outstanding_amount <= {self.write_off_amount}" if flt(self.write_off_amount) > 0 else "" + cond = ( + f" and outstanding_amount <= {flt(self.write_off_amount)}" + if flt(self.write_off_amount) > 0 + else "" + ) if self.write_off_based_on == "Accounts Receivable": return frappe.db.sql( diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py index 2733d07a476..fbe9d7fcf7d 100644 --- a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py +++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py @@ -31,7 +31,8 @@ class BulkTransactionLog(Document): log_detail = qb.DocType("Bulk Transaction Log Detail") has_records = frappe.db.sql( - f"select exists (select * from `tabBulk Transaction Log Detail` where date = '{self.name}');" + "select exists (select * from `tabBulk Transaction Log Detail` where date = %s);", + (self.name,), )[0][0] if not has_records: raise frappe.DoesNotExistError diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index c3d3627dfe6..21e6d3ea8b9 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -394,9 +394,9 @@ class StatusUpdater(Document): for args in self.status_updater: # condition to include current record (if submit or no if cancel) if self.docstatus == 1: - args["cond"] = " or parent='%s'" % self.name.replace('"', '"') + args["cond"] = " or parent=%s" % frappe.db.escape(self.name) else: - args["cond"] = " and parent!='%s'" % self.name.replace('"', '"') + args["cond"] = " and parent!=%s" % frappe.db.escape(self.name) self._update_children(args, update_modified) @@ -426,9 +426,10 @@ class StatusUpdater(Document): args["second_source_condition"] = frappe.db.sql( """ select ifnull((select sum({second_source_field}) from `tab{second_source_dt}` - where `{second_join_field}`='{detail_id}' + where `{second_join_field}`=%(detail_id)s and (`tab{second_source_dt}`.docstatus=1) - {second_source_extra_cond}), 0) """.format(**args) + {second_source_extra_cond}), 0) """.format(**args), + {"detail_id": args["detail_id"]}, )[0][0] if args["detail_id"]: @@ -439,9 +440,10 @@ class StatusUpdater(Document): frappe.db.sql( """ (select ifnull(sum({source_field}), 0) - from `tab{source_dt}` where `{join_field}`='{detail_id}' + from `tab{source_dt}` where `{join_field}`=%(detail_id)s and (docstatus=1 {cond}) {extra_cond}) - """.format(**args) + """.format(**args), + {"detail_id": args["detail_id"]}, )[0][0] or 0.0 ) @@ -452,7 +454,8 @@ class StatusUpdater(Document): frappe.db.sql( """update `tab{target_dt}` set {target_field} = {source_dt_value} {update_modified} - where name='{detail_id}'""".format(**args) + where name=%(detail_id)s""".format(**args), + {"detail_id": args["detail_id"]}, ) def _update_percent_field_in_targets(self, args, update_modified=True): diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py index f3f1f1b0651..ea7b47bd487 100644 --- a/erpnext/controllers/website_list_for_contact.py +++ b/erpnext/controllers/website_list_for_contact.py @@ -7,7 +7,7 @@ import json import frappe from frappe import _ from frappe.modules.utils import get_module_app -from frappe.utils import flt, has_common +from frappe.utils import cint, flt, has_common from frappe.utils.user import is_website_user diff --git a/erpnext/selling/report/inactive_customers/inactive_customers.py b/erpnext/selling/report/inactive_customers/inactive_customers.py index 7e4ddc128ac..d21d11b2447 100644 --- a/erpnext/selling/report/inactive_customers/inactive_customers.py +++ b/erpnext/selling/report/inactive_customers/inactive_customers.py @@ -14,6 +14,9 @@ def execute(filters=None): days_since_last_order = filters.get("days_since_last_order") doctype = filters.get("doctype") + if doctype not in ("Sales Order", "Sales Invoice"): + frappe.throw(_("Invalid value {0} for 'Doctype'").format(doctype)) + if cint(days_since_last_order) <= 0: frappe.throw(_("'Days Since Last Order' must be greater than or equal to zero")) diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index 5786adc6881..5d4478f8dd6 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -427,14 +427,16 @@ class Analytics: break def get_groups(self): - if self.filters.tree_type == "Territory": - parent = "parent_territory" - if self.filters.tree_type == "Customer Group": - parent = "parent_customer_group" - if self.filters.tree_type == "Item Group": - parent = "parent_item_group" - if self.filters.tree_type == "Supplier Group": - parent = "parent_supplier_group" + parent_field_map = { + "Territory": "parent_territory", + "Customer Group": "parent_customer_group", + "Item Group": "parent_item_group", + "Supplier Group": "parent_supplier_group", + } + if self.filters.tree_type not in parent_field_map: + frappe.throw(_("Invalid Tree Type {0}").format(self.filters.tree_type)) + + parent = parent_field_map[self.filters.tree_type] self.depth_map = frappe._dict() @@ -453,6 +455,9 @@ class Analytics: def get_teams(self): self.depth_map = frappe._dict() + if not frappe.db.exists("DocType", self.filters.doc_type): + frappe.throw(_("Invalid Document Type {0}").format(self.filters.doc_type)) + self.group_entries = frappe.db.sql( f""" select * from (select "Order Types" as name, 0 as lft, 2 as rgt, '' as parent union select distinct order_type as name, 1 as lft, 1 as rgt, "Order Types" as parent diff --git a/erpnext/setup/doctype/authorization_control/authorization_control.py b/erpnext/setup/doctype/authorization_control/authorization_control.py index 4298ffc9ec6..cfa2eef8915 100644 --- a/erpnext/setup/doctype/authorization_control/authorization_control.py +++ b/erpnext/setup/doctype/authorization_control/authorization_control.py @@ -120,7 +120,9 @@ class AuthorizationControl(TransactionBase): if val == 1: add_cond += " and system_user = {}".format(frappe.db.escape(session["user"])) elif val == 2: - add_cond += " and system_role IN %s" % ("('" + "','".join(frappe.get_roles()) + "')") + add_cond += " and system_role IN (%s)" % ", ".join( + frappe.db.escape(r) for r in frappe.get_roles() + ) else: add_cond += " and ifnull(system_user,'') = '' and ifnull(system_role,'') = ''" @@ -203,8 +205,8 @@ class AuthorizationControl(TransactionBase): and docstatus != 2 """.format( "%s", - "'" + "','".join(frappe.get_roles()) + "'", - "'" + "','".join(final_based_on) + "'", + ", ".join(frappe.db.escape(r) for r in frappe.get_roles()), + ", ".join(frappe.db.escape(b) for b in final_based_on), "%s", ), (doctype_name, company), diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 068daeae4f1..ffd37d91df4 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -209,7 +209,7 @@ class MaterialRequest(BuyingController): def check_modified_date(self): mod_db = frappe.db.sql("""select modified from `tabMaterial Request` where name = %s""", self.name) - date_diff = frappe.db.sql(f"""select TIMEDIFF('{mod_db[0][0]}', '{cstr(self.modified)}')""") + date_diff = frappe.db.sql("""select TIMEDIFF(%s, %s)""", (mod_db[0][0], cstr(self.modified))) if date_diff and date_diff[0][0]: frappe.throw(_("{0} {1} has been modified. Please refresh.").format(_(self.doctype), self.name)) diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index c3f5086fbc5..d11a2f55398 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -283,7 +283,7 @@ def set_stock_balance_as_per_serial_no( if not posting_time: posting_time = nowtime() - condition = " and item.name='%s'" % item_code.replace("'", "'") if item_code else "" + condition = " and item.name=%s" % frappe.db.escape(item_code, percent=False) if item_code else "" bin = frappe.db.sql( """select bin.item_code, bin.warehouse, bin.actual_qty, item.stock_uom From e4370ab332cea148446b3a5aca9597c559e2e583 Mon Sep 17 00:00:00 2001 From: pandiyan Date: Wed, 17 Jun 2026 18:01:14 +0530 Subject: [PATCH 15/39] fix: disable is_debit_note while creating credit note (cherry picked from commit 279c8dea0630215553bacdd2792f97a7725ac36b) --- erpnext/controllers/sales_and_purchase_return.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 8f6edbe75ae..e1e3ba3e84e 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -380,6 +380,8 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai doc.pricing_rules = [] doc.return_against = source.name doc.set_warehouse = "" + if doctype == "Sales Invoice": + doc.is_debit_note = 0 if doctype == "Sales Invoice" or doctype == "POS Invoice": doc.is_pos = source.is_pos From 90fd057fb3a9ec577d65677928a7a7bdbea790de Mon Sep 17 00:00:00 2001 From: pandiyan Date: Thu, 18 Jun 2026 18:05:46 +0530 Subject: [PATCH 16/39] fix: apply docstatus filter to exclude cancelled Work Orders in Serial No (cherry picked from commit 3ba8f690a462107252bbae3c0b6cb5a1568761a3) --- erpnext/stock/doctype/serial_no/serial_no.js | 40 +++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.js b/erpnext/stock/doctype/serial_no/serial_no.js index b12bbbdb27c..6e8363287a0 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.js +++ b/erpnext/stock/doctype/serial_no/serial_no.js @@ -1,26 +1,30 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -cur_frm.add_fetch("customer", "customer_name", "customer_name"); -cur_frm.add_fetch("supplier", "supplier_name", "supplier_name"); - -cur_frm.add_fetch("item_code", "item_name", "item_name"); -cur_frm.add_fetch("item_code", "description", "description"); -cur_frm.add_fetch("item_code", "item_group", "item_group"); -cur_frm.add_fetch("item_code", "brand", "brand"); - -cur_frm.cscript.onload = function () { - cur_frm.set_query("item_code", function () { - return erpnext.queries.item({ is_stock_item: 1, has_serial_no: 1 }); - }); -}; - -frappe.ui.form.on("Serial No", "refresh", function (frm) { - frm.toggle_enable("item_code", frm.doc.__islocal); -}); - frappe.ui.form.on("Serial No", { + setup(frm) { + frm.add_fetch("customer", "customer_name", "customer_name"); + frm.add_fetch("supplier", "supplier_name", "supplier_name"); + frm.add_fetch("item_code", "item_name", "item_name"); + frm.add_fetch("item_code", "description", "description"); + frm.add_fetch("item_code", "item_group", "item_group"); + frm.add_fetch("item_code", "brand", "brand"); + + frm.set_query("item_code", function () { + return erpnext.queries.item({ is_stock_item: 1, has_serial_no: 1 }); + }); + + frm.set_query("work_order", () => { + return { + filters: { + docstatus: 1, + }, + }; + }); + }, + refresh(frm) { + frm.toggle_enable("item_code", frm.doc.__islocal); frm.trigger("view_ledgers"); }, From 37dffa72738133dcbccbb8e3ec8cfcf5c1a9ed97 Mon Sep 17 00:00:00 2001 From: vorasmit Date: Thu, 18 Jun 2026 20:15:03 +0530 Subject: [PATCH 17/39] fix: tax.base_tax_amount as none when payment entry created using API (cherry picked from commit b9b402f2eca1fca77451fa860bc55b7a65cd8496) --- .../doctype/payment_entry/payment_entry.py | 4 ++-- .../payment_entry/test_payment_entry.py | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 26c980a1946..148dd4edcc5 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1197,9 +1197,9 @@ class PaymentEntry(AccountsController): continue if tax.add_deduct_tax == "Add": - included_taxes += tax.base_tax_amount + included_taxes += flt(tax.base_tax_amount) else: - included_taxes -= tax.base_tax_amount + included_taxes -= flt(tax.base_tax_amount) return included_taxes diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 79bd66f735a..c52193cc469 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1118,6 +1118,27 @@ class TestPaymentEntry(FrappeTestCase): self.assertEqual(gl_entries, expected_gl_entries) + def test_payment_entry_with_inclusive_tax(self): + # inclusive tax built server-side: base_tax_amount is None until apply_taxes() + payment_entry = create_payment_entry(paid_amount=1180) + payment_entry.append( + "taxes", + { + "account_head": "_Test Account Service Tax - _TC", + "charge_type": "On Paid Amount", + "rate": 18, + "included_in_paid_amount": 1, + "add_deduct_tax": "Add", + "description": "Service Tax", + }, + ) + payment_entry.save() + payment_entry.submit() + + # 1180 incl 18% => 1000 base + 180 tax + self.assertEqual(flt(payment_entry.total_taxes_and_charges, 2), 180.0) + self.assertEqual(flt(payment_entry.unallocated_amount, 2), 1000.0) + def test_payment_entry_against_onhold_purchase_invoice(self): pi = make_purchase_invoice() From e556cbbe6a36c094ed33e1cf484c21bc792b3c6c Mon Sep 17 00:00:00 2001 From: nareshkannasln Date: Tue, 26 May 2026 12:39:51 +0530 Subject: [PATCH 18/39] fix: update reference doctype mapping and field visibility in bank guarantee (cherry picked from commit b1de654dfd71cfa5c86e5f57d13da8d4f6d90007) # Conflicts: # erpnext/accounts/doctype/bank_guarantee/bank_guarantee.json --- .../doctype/bank_guarantee/bank_guarantee.js | 7 +++++++ .../doctype/bank_guarantee/bank_guarantee.json | 12 ++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js index c34e0f9099c..8d36c731d37 100644 --- a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js +++ b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js @@ -9,6 +9,13 @@ cur_frm.add_fetch("bank", "swift_number", "swift_number"); frappe.ui.form.on("Bank Guarantee", { setup: function (frm) { + frm.set_query("reference_doctype", function () { + return { + filters: { + name: ["in", ["Sales Order", "Purchase Order"]], + }, + }; + }); frm.set_query("bank_account", function () { return { filters: { diff --git a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.json b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.json index c255ce7ba8f..42e92613ea8 100644 --- a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.json +++ b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_bulk_edit": 1, "autoname": "ACC-BG-.YYYY.-.#####", "creation": "2016-12-17 10:43:35.731631", "doctype": "DocType", @@ -50,8 +51,7 @@ "fieldname": "reference_doctype", "fieldtype": "Link", "label": "Reference Document Type", - "options": "DocType", - "read_only": 1 + "options": "DocType" }, { "fieldname": "reference_docname", @@ -60,14 +60,14 @@ "options": "reference_doctype" }, { - "depends_on": "eval: doc.bg_type == \"Receiving\"", + "depends_on": "eval: doc.reference_doctype == \"Sales Order\"", "fieldname": "customer", "fieldtype": "Link", "label": "Customer", "options": "Customer" }, { - "depends_on": "eval: doc.bg_type == \"Providing\"", + "depends_on": "eval: doc.reference_doctype == \"Purchase Order\"", "fieldname": "supplier", "fieldtype": "Link", "label": "Supplier", @@ -217,11 +217,11 @@ ], "is_submittable": 1, "links": [], - "modified": "2025-09-26 00:38:17.584694", + "modified": "2026-05-25 18:12:10.768835", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Guarantee", - "naming_rule": "Expression (old style)", + "naming_rule": "Expression", "owner": "Administrator", "permissions": [ { From 041a9adbbf7f211ad75d84e9c79c7b94b319815e Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Sat, 20 Jun 2026 19:25:58 +0530 Subject: [PATCH 19/39] fix(err): add missing permission check on `get_account_details` (cherry picked from commit 9bf1e847d2cf97d2700e5a1e9b1c339c083fd712) --- .../exchange_rate_revaluation/exchange_rate_revaluation.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index 96272c0d901..41249662624 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -619,6 +619,10 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party): def get_account_details( company, posting_date, account, party_type=None, party=None, rounding_loss_allowance: float | None = None ): + if not account: + return + frappe.has_permission("Account", doc=account, throw=True) + if not (company and posting_date): frappe.throw(_("Company and Posting Date is mandatory")) From 0efebf5d8ce73524057d0634683b3e618922600b Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Sat, 20 Jun 2026 19:28:20 +0530 Subject: [PATCH 20/39] fix(report_utils): remove unnecessary whitelist decorator on `get_invoiced_item_gross_margin` (cherry picked from commit e29535f29c50db187138585d11d5fabf3428598c) # Conflicts: # erpnext/accounts/report/utils.py --- erpnext/accounts/report/utils.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index bf604a36db0..64943db96ba 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -146,8 +146,17 @@ def get_appropriate_company(filters): return company +<<<<<<< HEAD @frappe.whitelist() def get_invoiced_item_gross_margin(sales_invoice=None, item_code=None, company=None, with_item_data=False): +======= +def get_invoiced_item_gross_margin( + sales_invoice: str | None = None, + item_code: str | None = None, + company: str | None = None, + with_item_data: bool = False, +): +>>>>>>> e29535f29c (fix(report_utils): remove unnecessary whitelist decorator on `get_invoiced_item_gross_margin`) from erpnext.accounts.report.gross_profit.gross_profit import GrossProfitGenerator sales_invoice = sales_invoice or frappe.form_dict.get("sales_invoice") From 7a04f031d9db0e3dda69060dc041323c95f728f4 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Sat, 20 Jun 2026 19:58:49 +0530 Subject: [PATCH 21/39] chore: resolve conflicts --- erpnext/accounts/report/utils.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index 64943db96ba..8d1730ab294 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -146,17 +146,7 @@ def get_appropriate_company(filters): return company -<<<<<<< HEAD -@frappe.whitelist() def get_invoiced_item_gross_margin(sales_invoice=None, item_code=None, company=None, with_item_data=False): -======= -def get_invoiced_item_gross_margin( - sales_invoice: str | None = None, - item_code: str | None = None, - company: str | None = None, - with_item_data: bool = False, -): ->>>>>>> e29535f29c (fix(report_utils): remove unnecessary whitelist decorator on `get_invoiced_item_gross_margin`) from erpnext.accounts.report.gross_profit.gross_profit import GrossProfitGenerator sales_invoice = sales_invoice or frappe.form_dict.get("sales_invoice") From acc1444c030ab2bf38baefb101c9fdd9e7a1ed1b Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Sat, 20 Jun 2026 23:50:50 +0530 Subject: [PATCH 22/39] fix(stock): apply precision to the additional cost amount in stock entry (cherry picked from commit 6ac699d3bb44cfb9d96536a5271e2e5654d221c3) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 3ed696e0a02..69d8e96f6f2 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2005,6 +2005,8 @@ class StockEntry(StockController): ] += flt(t.base_amount * multiply_based_on) / divide_based_on if item_account_wise_additional_cost: + precision = self.get_debit_field_precision() + for d in self.get("items"): for account, amount in item_account_wise_additional_cost.get( (d.item_code, d.name), {} @@ -2012,6 +2014,9 @@ class StockEntry(StockController): if not amount: continue + amount["amount"] = flt(amount["amount"], precision) + amount["base_amount"] = flt(amount["base_amount"], precision) + gl_entries.append( self.get_gl_dict( { From deb11745e5795a91bf9228788c4c489ad3a2597a Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Sat, 20 Jun 2026 23:51:16 +0530 Subject: [PATCH 23/39] test(stock): add test to validate the precision for additional cost amount (cherry picked from commit 20b14395e30e6b97520cd418f8dbdd4644c84600) --- .../doctype/stock_entry/test_stock_entry.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index d0d60276a97..ea231ff466c 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -547,6 +547,60 @@ class TestStockEntry(FrappeTestCase): ), ) + def test_additional_cost_no_rounding_residual_on_stock_adjustment(self): + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") + warehouse = "Stores - TCP1" + items = [ + make_item(f"_Test Addl Cost Rounding {x}", {"is_stock_item": 1}).name for x in ("A", "B", "C") + ] + + for item_code in items: + make_stock_entry(item_code=item_code, target=warehouse, company=company, qty=100, basic_rate=10) + + transfer = make_stock_entry(company=company, purpose="Material Transfer", do_not_save=True) + transfer.from_warehouse = warehouse + transfer.to_warehouse = warehouse + transfer.items = [] + for item_code in items: + transfer.append( + "items", + { + "item_code": item_code, + "qty": 100, + "s_warehouse": warehouse, + "t_warehouse": warehouse, + "uom": "Nos", + "conversion_factor": 1, + }, + ) + transfer.append( + "additional_costs", + { + "expense_account": "Expenses Included In Valuation - TCP1", + "description": "freight", + "amount": 100, + }, + ) + transfer.insert() + transfer.submit() + + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_type": "Stock Entry", "voucher_no": transfer.name}, + fields=["account", "debit", "credit"], + ) + gl_map = {} + for row in gl_entries: + account = gl_map.setdefault(row.account, frappe._dict(debit=0.0, credit=0.0)) + account.debit += row.debit + account.credit += row.credit + + self.assertNotIn("Stock Adjustment - TCP1", gl_map) + + stock_in_hand_account = get_inventory_account(company, warehouse) + self.assertEqual(flt(gl_map[stock_in_hand_account].debit, 2), 99.99) + self.assertEqual(flt(gl_map["Expenses Included In Valuation - TCP1"].credit, 2), 99.99) + def check_stock_ledger_entries(self, voucher_type, voucher_no, expected_sle): expected_sle.sort(key=lambda x: x[1]) From 37f847e730aafae2fc026c008bd391c15feace1a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 21 Jun 2026 22:07:38 +0530 Subject: [PATCH 24/39] fix(stock): update voucher valuaion rate in sle (backport #55960) (#56262) fix(stock): update voucher valuaion rate in sle (#55960) (cherry picked from commit 130c2594e1e999708c5b70708ba108201ab83931) Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com> --- erpnext/stock/stock_ledger.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index bcc5b3e8a26..9ee458ce9c7 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -908,6 +908,16 @@ class update_entries_after: and not has_dimensions ): # assert + if ( + sle.voucher_detail_no + and self.repost_doc + and self.repost_doc.get("recalculate_valuation_rate") + ): + source_rate = frappe.get_cached_value( + "Stock Reconciliation Item", sle.voucher_detail_no, "valuation_rate" + ) + if source_rate: + sle.valuation_rate = source_rate self.wh_data.valuation_rate = sle.valuation_rate self.wh_data.qty_after_transaction = sle.qty_after_transaction self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt( From a858d7746141cd511de8519d3c324d9f3f111817 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Sun, 21 Jun 2026 00:52:21 +0530 Subject: [PATCH 25/39] fix(stock): allow partial raw material picking/transfer from work order (cherry picked from commit 8e3fbab94ae6fbc19b378777a149b49e4f40175b) # Conflicts: # erpnext/manufacturing/doctype/work_order/work_order.py --- .../doctype/work_order/work_order.js | 25 +++++--- .../doctype/work_order/work_order.py | 30 ++++++++++ erpnext/stock/doctype/pick_list/pick_list.py | 2 +- .../stock/doctype/stock_entry/stock_entry.py | 59 ++++++++++++++++++- 4 files changed, 104 insertions(+), 12 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 3b3448333d9..5905d40b2f3 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -979,17 +979,24 @@ erpnext.work_order = { }, create_pick_list: function (frm, purpose = "Material Transfer for Manufacture") { - this.show_prompt_for_qty_input(frm, purpose) - .then((data) => { - return frappe.xcall("erpnext.manufacturing.doctype.work_order.work_order.create_pick_list", { + const max = this.get_max_transferable_qty(frm, purpose); + + const get_pick_list = (for_qty) => + frappe + .xcall("erpnext.manufacturing.doctype.work_order.work_order.create_pick_list", { source_name: frm.doc.name, - for_qty: data.qty, + for_qty: for_qty, + }) + .then((pick_list) => { + frappe.model.sync(pick_list); + frappe.set_route("Form", pick_list.doctype, pick_list.name); }); - }) - .then((pick_list) => { - frappe.model.sync(pick_list); - frappe.set_route("Form", pick_list.doctype, pick_list.name); - }); + + if (max <= 0) { + get_pick_list(frm.doc.qty); + } else { + this.show_prompt_for_qty_input(frm, purpose).then((data) => get_pick_list(data.qty)); + } }, make_consumption_se: function (frm, backflush_raw_materials_based_on) { diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 9d3100302e7..d6764005a80 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1241,6 +1241,36 @@ class WorkOrder(Document): "transferred_qty", (transferred_items.get(row.item_code) or 0.0), update_modified=False ) + self.recompute_material_transferred_for_manufacturing(transferred_items) + + def recompute_material_transferred_for_manufacturing(self, transferred_items): + """Set material_transferred_for_manufacturing based on actual item-level transfers, not fg_completed_qty.""" + # When fg_completed_qty > 0 (direct stock entries, excess transfer), preserve the + # SUM(fg_completed_qty) approach so excess-transfer tracking works correctly. + sum_fg_completed_qty = self.get_transferred_or_manufactured_qty("Material Transfer for Manufacture") + if sum_fg_completed_qty: + self.db_set("material_transferred_for_manufacturing", sum_fg_completed_qty) + return + + # Pick list flow sets fg_completed_qty=0; use min-fraction of actual item transfers + # so partial availability does not prematurely mark the work order as fully transferred. + required_by_item = {} + for row in self.required_items: + if not row.include_item_in_manufacturing or flt(row.required_qty) <= 0: + continue + required_by_item[row.item_code] = required_by_item.get(row.item_code, 0.0) + flt(row.required_qty) + + if not required_by_item: + return + + min_fraction = min( + flt(transferred_items.get(item_code) or 0) / required_qty + for item_code, required_qty in required_by_item.items() + ) + min_fraction = min(min_fraction, 1.0) + material_transferred = min_fraction * flt(self.qty) + self.db_set("material_transferred_for_manufacturing", material_transferred) + def update_returned_qty(self): ste = frappe.qb.DocType("Stock Entry") ste_child = frappe.qb.DocType("Stock Entry Detail") diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 41624477882..6d42f51a8d6 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -1577,7 +1577,7 @@ def update_stock_entry_based_on_work_order(pick_list, stock_entry): stock_entry.from_bom = 1 stock_entry.bom_no = work_order.bom_no stock_entry.use_multi_level_bom = work_order.use_multi_level_bom - stock_entry.fg_completed_qty = pick_list.for_qty + stock_entry.fg_completed_qty = 0 if work_order.bom_no: stock_entry.inspection_required = frappe.db.get_value("BOM", work_order.bom_no, "inspection_required") diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 69d8e96f6f2..3aea5271d3d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1146,10 +1146,12 @@ class StockEntry(StockController): if self.purpose not in ["Manufacture", "Material Transfer for Manufacture"]: return - if not frappe.db.get_single_value("Manufacturing Settings", "validate_components_quantities_per_bom"): + if not self.fg_completed_qty: + if self.work_order and self.purpose == "Material Transfer for Manufacture": + self._validate_no_excess_transfer() return - if not self.fg_completed_qty: + if not frappe.db.get_single_value("Manufacturing Settings", "validate_components_quantities_per_bom"): return raw_materials = self.get_bom_raw_materials(self.fg_completed_qty) @@ -1174,6 +1176,59 @@ class StockEntry(StockController): title=_("Missing Item"), ) + def _validate_no_excess_transfer(self): + if self.is_return: + return + + if ( + frappe.db.get_single_value("Manufacturing Settings", "backflush_raw_materials_based_on") + == "Material Transferred for Manufacture" + ): + return + + wo = self.pro_doc + if not wo: + return + + pending_by_item = {} + for r in wo.required_items: + pending_by_item[r.item_code] = ( + pending_by_item.get(r.item_code, 0.0) + flt(r.required_qty) - flt(r.transferred_qty) + ) + + transfer_by_item = {} + first_row_by_item = {} + for item in self.items: + if not item.s_warehouse: + continue + + key = ( + item.item_code if item.item_code in pending_by_item else getattr(item, "original_item", None) + ) + if key not in pending_by_item: + continue + + transfer_by_item[key] = transfer_by_item.get(key, 0.0) + flt(item.qty) + first_row_by_item.setdefault(key, item) + + for key, transfer_qty in transfer_by_item.items(): + pending_qty = max(0.0, pending_by_item[key]) + if transfer_qty > pending_qty: + item = first_row_by_item[key] + frappe.throw( + _( + "Row #{0}: Cannot transfer {1} {2} of Item {3}. " + "Maximum transferable quantity is {4} {2}." + ).format( + item.idx, + transfer_qty, + item.uom, + frappe.bold(item.item_code), + pending_qty, + ), + title=_("Excess Material Transfer"), + ) + def validate_same_source_target_warehouse_during_material_transfer(self): """ Validate Material Transfer entries where source and target warehouses are identical. From e6d45e35c752592223f3c64cec4d4ed7d327aa3d Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Sun, 21 Jun 2026 00:53:53 +0530 Subject: [PATCH 26/39] test(stock): add test to validate the partial transfer of raw material (cherry picked from commit 4d055d374a5ca82b2dacc99b51cfe0c383401d5e) --- .../doctype/work_order/test_work_order.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index bf56182ee41..2679d6e29fe 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -1461,6 +1461,68 @@ class TestWorkOrder(FrappeTestCase): self.assertEqual(work_order.required_items[0].transferred_qty, 1) self.assertEqual(work_order.required_items[1].transferred_qty, 2) + def test_material_transferred_min_fraction_on_partial_pick_list(self): + """Pick-list flow (fg_completed_qty = 0): 'Material Transferred for Manufacturing' + must reflect the least-transferred required item (the bottleneck), instead of being + marked fully transferred prematurely when only some materials are transferred. + """ + work_order = make_wo_order_test_record(planned_start_date=now(), qty=2) + test_stock_entry.make_stock_entry( + item_code="_Test Item", target="_Test Warehouse - _TC", qty=10, basic_rate=5000.0 + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=10, basic_rate=1000.0 + ) + + required_qty = {row.item_code: flt(row.required_qty) for row in work_order.required_items} + + # pick-list transfer: For Quantity = 0 + transfer_entry = frappe.get_doc( + make_stock_entry(work_order.name, "Material Transfer for Manufacture", 0) + ) + self.assertEqual(transfer_entry.fg_completed_qty, 0.0) + + for item in transfer_entry.items: + full_qty = required_qty[item.item_code] + item.qty = full_qty if item.item_code == "_Test Item" else full_qty / 2 + item.transfer_qty = item.qty + transfer_entry.submit() + + work_order.reload() + transferred_qty = {row.item_code: flt(row.transferred_qty) for row in work_order.required_items} + self.assertEqual(transferred_qty["_Test Item"], required_qty["_Test Item"]) + self.assertEqual( + transferred_qty["_Test Item Home Desktop 100"], + required_qty["_Test Item Home Desktop 100"] / 2, + ) + # bottleneck fraction = 0.5 -> 0.5 * qty(2) = 1.0 + self.assertEqual(work_order.material_transferred_for_manufacturing, 1.0) + + def test_material_transferred_full_via_pick_list_flow(self): + """Pick-list flow with every required item fully transferred marks the work order + as fully transferred (min fraction = 1.0).""" + work_order = make_wo_order_test_record(planned_start_date=now(), qty=2) + test_stock_entry.make_stock_entry( + item_code="_Test Item", target="_Test Warehouse - _TC", qty=10, basic_rate=5000.0 + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=10, basic_rate=1000.0 + ) + + required_qty = {row.item_code: flt(row.required_qty) for row in work_order.required_items} + + transfer_entry = frappe.get_doc( + make_stock_entry(work_order.name, "Material Transfer for Manufacture", 0) + ) + self.assertEqual(transfer_entry.fg_completed_qty, 0.0) + for item in transfer_entry.items: + item.qty = required_qty[item.item_code] + item.transfer_qty = item.qty + transfer_entry.submit() + + work_order.reload() + self.assertEqual(work_order.material_transferred_for_manufacturing, 2.0) + def test_backflushed_batch_raw_materials_based_on_transferred(self): frappe.db.set_single_value( "Manufacturing Settings", From 42af4ce7b005f1dc28d5590fc637478bcb540a2b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 03:00:50 +0530 Subject: [PATCH 27/39] fix: escape user image url on various templates (backport #56269) (#56270) Co-authored-by: Diptanil Saha fix: escape user image url on various templates (#56269) --- erpnext/templates/emails/daily_project_summary.html | 2 +- erpnext/templates/includes/projects/project_row.html | 2 +- erpnext/templates/includes/projects/project_tasks.html | 2 +- erpnext/templates/includes/projects/project_timesheets.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/templates/emails/daily_project_summary.html b/erpnext/templates/emails/daily_project_summary.html index 5ccc6101665..689a503a7d1 100644 --- a/erpnext/templates/emails/daily_project_summary.html +++ b/erpnext/templates/emails/daily_project_summary.html @@ -10,7 +10,7 @@ {% if user.image %} - + {% else %}
{{ user.full_name[0] }} diff --git a/erpnext/templates/includes/projects/project_row.html b/erpnext/templates/includes/projects/project_row.html index ccb306afcdb..7c8d23f056d 100644 --- a/erpnext/templates/includes/projects/project_row.html +++ b/erpnext/templates/includes/projects/project_row.html @@ -34,7 +34,7 @@ ], as_dict = True) %} {% if user_details.user_image %} - + {% else %} diff --git a/erpnext/templates/includes/projects/project_tasks.html b/erpnext/templates/includes/projects/project_tasks.html index 4069d3dcca5..43fa591cd47 100644 --- a/erpnext/templates/includes/projects/project_tasks.html +++ b/erpnext/templates/includes/projects/project_tasks.html @@ -27,7 +27,7 @@ as_dict = True)%} {% if user_details.user_image %} - + {% else %} diff --git a/erpnext/templates/includes/projects/project_timesheets.html b/erpnext/templates/includes/projects/project_timesheets.html index b8e06829192..6c3f8011ecd 100644 --- a/erpnext/templates/includes/projects/project_timesheets.html +++ b/erpnext/templates/includes/projects/project_timesheets.html @@ -15,7 +15,7 @@ %} {% if user_details.user_image %} - + {% else %} From 37d26222d709d013d184f55bf50dedac9bd94aa1 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 05:17:49 +0530 Subject: [PATCH 28/39] fix: clear stale payment rows on non-POS returns so they don't surface in bank reconciliation (backport #55903) (#56169) fix: clear stale payment rows on non-POS returns so they don't surface in bank reconciliation (#55903) (cherry picked from commit 322d4dff25fb3e73da70e5bd5c49478b97866854) # Conflicts: # erpnext/accounts/doctype/sales_invoice/sales_invoice.py # erpnext/accounts/doctype/sales_invoice/services/pos.py Co-authored-by: Jatin3128 <140256508+Jatin3128@users.noreply.github.com> --- .../doctype/sales_invoice/sales_invoice.py | 9 ++++++++- .../doctype/sales_invoice/test_sales_invoice.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 5d6676aa537..97b2afd7751 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -455,8 +455,8 @@ class SalesInvoice(SellingController): self.calculate_taxes_and_totals() def before_save(self): - self.set_account_for_mode_of_payment() self.set_paid_amount() + self.set_account_for_mode_of_payment() def before_submit(self): self.add_remarks() @@ -791,6 +791,13 @@ class SalesInvoice(SellingController): def set_paid_amount(self): paid_amount = 0.0 base_paid_amount = 0.0 + + if not cint(self.is_pos) and self.is_return: + self.set("payments", []) + self.paid_amount = paid_amount + self.base_paid_amount = base_paid_amount + return + for data in self.payments: data.base_amount = flt(data.amount * self.conversion_rate, self.precision("base_paid_amount")) paid_amount += data.amount diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index e210903c450..6f51e27f532 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1049,6 +1049,21 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(pos_return.get("payments")[0].amount, -500) self.assertEqual(pos_return.get("payments")[1].amount, -500) + def test_non_pos_return_clears_payment_rows(self): + from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return + + si = create_sales_invoice(do_not_save=True) + si.append("payments", {"mode_of_payment": "Cash", "amount": 100}) + si.insert() + si.submit() + + si_return = make_sales_return(si.name) + si_return.insert() + + self.assertEqual(si_return.is_pos, 0) + self.assertEqual(si_return.get("payments"), []) + self.assertEqual(si_return.paid_amount, 0) + def test_pos_change_amount(self): make_pos_profile( company="_Test Company with perpetual inventory", From 1f5283da5891be08983ec092cc6e9e9fe8ed5bb2 Mon Sep 17 00:00:00 2001 From: pandiyan Date: Mon, 22 Jun 2026 11:47:36 +0530 Subject: [PATCH 29/39] fix: add partially transferred status and fix button visibility for partial material transfer on job card --- .../doctype/job_card/job_card.js | 12 ++++------ .../doctype/job_card/job_card.json | 4 ++-- .../doctype/job_card/job_card.py | 23 +++++++++++++++++-- .../doctype/job_card/job_card_list.js | 1 + 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index e096c73cc61..cc8bdf04176 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -72,8 +72,9 @@ frappe.ui.form.on("Job Card", { frm.toggle_enable("for_quantity", !has_stock_entry); if (!frm.is_new() && has_items && frm.doc.docstatus < 2) { - let to_request = frm.doc.for_quantity > frm.doc.transferred_qty; - let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer; + const excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer; + const to_transfer = frm.doc.items.some((row) => flt(row.transferred_qty) < flt(row.required_qty)); + const to_request = to_transfer; if (to_request || excess_transfer_allowed) { frm.add_custom_button( @@ -85,10 +86,6 @@ frappe.ui.form.on("Job Card", { ); } - // check if any row has untransferred materials - // in case of multiple items in JC - let to_transfer = frm.doc.items.some((row) => row.transferred_qty < row.required_qty); - if (to_transfer || excess_transfer_allowed) { frm.add_custom_button( __("Material Transfer"), @@ -120,7 +117,8 @@ frappe.ui.form.on("Job Card", { frm.doc.docstatus == 0 && !frm.is_new() && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) && - (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty) + (!frm.doc.items.length || + !frm.doc.items.some((row) => flt(row.transferred_qty) < flt(row.required_qty))) ) { // if Job Card is link to Work Order, the job card must not be able to start if Work Order not "Started" // and if stock mvt for WIP is required diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index ba680df99f9..45ddb9cf609 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -234,7 +234,7 @@ "fieldtype": "Select", "label": "Status", "no_copy": 1, - "options": "Open\nWork In Progress\nMaterial Transferred\nOn Hold\nSubmitted\nCancelled\nCompleted", + "options": "Open\nWork In Progress\nPartially Transferred\nMaterial Transferred\nOn Hold\nSubmitted\nCancelled\nCompleted", "read_only": 1 }, { @@ -513,7 +513,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2026-05-12 12:17:17.750857", + "modified": "2026-06-22 11:51:16.526778", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index ae60bcb1ca8..9fddeda3e96 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -107,6 +107,7 @@ class JobCard(Document): status: DF.Literal[ "Open", "Work In Progress", + "Partially Transferred", "Material Transferred", "On Hold", "Submitted", @@ -927,6 +928,8 @@ class JobCard(Document): frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty)) + self.set_status(update_status=True) + def set_transferred_qty(self, update_status=False): "Set total FG Qty in Job Card for which RM was transferred." if not self.items: @@ -980,7 +983,22 @@ class JobCard(Document): self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0] if self.docstatus < 2: - if flt(self.for_quantity) <= flt(self.transferred_qty): + if self.items: + item_data = frappe.get_all( + "Job Card Item", + filters={"parent": self.name}, + fields=["transferred_qty", "required_qty"], + ) + all_transferred = item_data and all( + flt(d.transferred_qty) >= flt(d.required_qty) for d in item_data + ) + any_transferred = any(flt(d.transferred_qty) > 0 for d in item_data) + + if all_transferred: + self.status = "Material Transferred" + elif any_transferred: + self.status = "Partially Transferred" + elif flt(self.for_quantity) <= flt(self.transferred_qty): self.status = "Material Transferred" if self.time_logs: @@ -1224,12 +1242,13 @@ def time_diff_in_minutes(string_ed_date, string_st_date): @frappe.whitelist() -def get_job_details(start, end, filters=None): +def get_job_details(start: str, end: str, filters: str | None = None): events = [] event_color = { "Completed": "#cdf5a6", "Material Transferred": "#ffdd9e", + "Partially Transferred": "#ffe5b4", "Work In Progress": "#D3D3D3", } diff --git a/erpnext/manufacturing/doctype/job_card/job_card_list.js b/erpnext/manufacturing/doctype/job_card/job_card_list.js index e417b7f576d..bddb11d00bb 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card_list.js +++ b/erpnext/manufacturing/doctype/job_card/job_card_list.js @@ -7,6 +7,7 @@ frappe.listview_settings["Job Card"] = { Completed: "green", Cancelled: "red", "Material Transferred": "blue", + "Partially Transferred": "yellow", Open: "red", }; const status = doc.status || "Open"; From 7bea925230e179e4bda2862abe219736f3877821 Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Sat, 30 May 2026 18:21:11 +0530 Subject: [PATCH 30/39] fix: add validation and tests for set_status (cherry picked from commit b5a84c5e65f566c98d5b3d31c362e5b3983a839c) --- erpnext/support/doctype/issue/issue.py | 1 - erpnext/support/doctype/issue/test_issue.py | 32 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index c35b76cf37d..a08c9b22c23 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -226,7 +226,6 @@ def set_multiple_status(names, status): @frappe.whitelist() def set_status(name, status): frappe.has_permission("Issue", "write", name, throw=True) - frappe.db.set_value("Issue", name, "status", status) diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index c334a2251b3..09ae0d516bb 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -524,6 +524,38 @@ class TestFirstResponseTime(TestSetUp): ) self.assertEqual(issue.first_response_time, 1.0) + def _get_no_perm_user(self): + email = "test_no_issue_perm@example.com" + if not frappe.db.exists("User", email): + user = frappe.new_doc("User") + user.email = email + user.first_name = "No Perm" + user.send_welcome_email = 0 + user.insert(ignore_permissions=True) + return email + + def test_set_status_requires_write_permission(self): + from erpnext.support.doctype.issue.issue import set_status + + issue = frappe.new_doc("Issue") + issue.subject = "_Test Permission Issue" + issue.insert(ignore_permissions=True) + frappe.set_user(self._get_no_perm_user()) + self.assertRaises(frappe.PermissionError, set_status, issue.name, "Closed") + frappe.set_user("Administrator") + + def test_set_multiple_status_requires_write_permission(self): + import json + + from erpnext.support.doctype.issue.issue import set_multiple_status + + issue = frappe.new_doc("Issue") + issue.subject = "_Test Permission Issue" + issue.insert(ignore_permissions=True) + frappe.set_user(self._get_no_perm_user()) + self.assertRaises(frappe.PermissionError, set_multiple_status, json.dumps([issue.name]), "Closed") + frappe.set_user("Administrator") + def create_issue_and_communication(issue_creation, first_responded_on): issue = make_issue(issue_creation, index=1) From 42121f2e36d314166ee3f7e92e37cc3a5a2dfa3b Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 22 Jun 2026 17:40:57 +0530 Subject: [PATCH 31/39] fix: simplify get_round_off_applicable_accounts function signature --- erpnext/controllers/taxes_and_totals.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 09318b6725f..1fc8128d6ea 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -1130,9 +1130,7 @@ def get_itemised_tax_breakup_html(doc): @frappe.whitelist() -def get_round_off_applicable_accounts( - company: str, account_list: list | str, doc: str | dict | Document | None = None -): +def get_round_off_applicable_accounts(company, account_list, doc=None): # required to set correct region with temporary_flag("company", company): return get_regional_round_off_accounts(company, account_list, doc) From 506658c3a6941eff92afb7bbe15c68603b836c26 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:49:58 +0530 Subject: [PATCH 32/39] fix: attribute error because of missing margin_type field in Supplier Quotation (backport #48089) * fix: add missing margin related fields (cherry picked from commit 67f96c66e40ecce72b1131e05ef7dd17c3aa6201) # Conflicts: # erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json * chore: fix conflict --------- Co-authored-by: iamkhanraheel Co-authored-by: Sagar Vora <16315650+sagarvora@users.noreply.github.com> --- .../supplier_quotation_item.json | 51 ++++++++++++++++--- .../supplier_quotation_item.py | 3 ++ 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json index 91019104949..a41638966f1 100644 --- a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json +++ b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json @@ -30,11 +30,15 @@ "stock_qty", "sec_break_price_list", "price_list_rate", + "base_price_list_rate", + "discount_and_margin_section", + "margin_type", + "margin_rate_or_amount", + "rate_with_margin", + "col_break_6", "discount_percentage", "discount_amount", "distributed_discount_amount", - "col_break_price_list", - "base_price_list_rate", "sec_break1", "rate", "amount", @@ -531,10 +535,6 @@ "fieldname": "sec_break_price_list", "fieldtype": "Section Break" }, - { - "fieldname": "col_break_price_list", - "fieldtype": "Column Break" - }, { "collapsible": 1, "fieldname": "ad_sec_break", @@ -572,13 +572,48 @@ "fieldtype": "Currency", "label": "Distributed Discount Amount", "options": "currency" + }, + { + "depends_on": "price_list_rate", + "fieldname": "margin_type", + "fieldtype": "Select", + "label": "Margin Type", + "options": "\nPercentage\nAmount", + "print_hide": 1 + }, + { + "depends_on": "eval:doc.margin_type && doc.price_list_rate", + "fieldname": "margin_rate_or_amount", + "fieldtype": "Float", + "label": "Margin Rate or Amount", + "print_hide": 1 + }, + { + "collapsible": 1, + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount", + "fieldname": "discount_and_margin_section", + "fieldtype": "Section Break", + "label": "Discount and Margin" + }, + { + "depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount", + "fieldname": "rate_with_margin", + "fieldtype": "Currency", + "label": "Rate With Margin", + "options": "currency", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "col_break_6", + "fieldtype": "Column Break" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-06-02 06:22:18.864822", + "modified": "2025-06-17 12:05:52.441645", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation Item", @@ -589,4 +624,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.py b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.py index a51b9500fd8..ac313cbf182 100644 --- a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.py +++ b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.py @@ -38,6 +38,8 @@ class SupplierQuotationItem(Document): lead_time_days: DF.Int manufacturer: DF.Link | None manufacturer_part_no: DF.Data | None + margin_rate_or_amount: DF.Float + margin_type: DF.Literal["", "Percentage", "Amount"] material_request: DF.Link | None material_request_item: DF.Data | None net_amount: DF.Currency @@ -52,6 +54,7 @@ class SupplierQuotationItem(Document): project: DF.Link | None qty: DF.Float rate: DF.Currency + rate_with_margin: DF.Currency request_for_quotation: DF.Link | None request_for_quotation_item: DF.Data | None sales_order: DF.Link | None From f106513005265c6b0bc06c79eed46e5cfc44d931 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Tue, 23 Jun 2026 16:32:16 +0530 Subject: [PATCH 33/39] fix: resolve backport conflict in accounting dashboard number cards Resolved Mergify backport conflicts (PR #55548 / backport of #55484) by taking the final upstream state: fiscal-year posting_date Between filter with calendar-year fallback, Timespan filter removed. Co-Authored-By: Claude Opus 4.8 --- .../total_incoming_bills/total_incoming_bills.json | 13 ------------- .../total_incoming_payment.json | 13 ------------- .../total_outgoing_bills/total_outgoing_bills.json | 13 ------------- .../total_outgoing_payment.json | 13 ------------- 4 files changed, 52 deletions(-) diff --git a/erpnext/accounts/number_card/total_incoming_bills/total_incoming_bills.json b/erpnext/accounts/number_card/total_incoming_bills/total_incoming_bills.json index 5b545837905..34e42ac7cfe 100644 --- a/erpnext/accounts/number_card/total_incoming_bills/total_incoming_bills.json +++ b/erpnext/accounts/number_card/total_incoming_bills/total_incoming_bills.json @@ -4,27 +4,14 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Purchase Invoice", -<<<<<<< HEAD -<<<<<<< HEAD - "dynamic_filters_json": "[[\"Purchase Invoice\",\"company\",\"=\",\" frappe.defaults.get_user_default(\\\"Company\\\")\"]]", - "filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Purchase Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]", -======= - "dynamic_filters_json": "[[\"Purchase Invoice\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Purchase Invoice\",\"posting_date\",\"Between\",\"[frappe.boot.current_fiscal_year[1], frappe.boot.current_fiscal_year[2]]\"]]", -======= "dynamic_filters_json": "[[\"Purchase Invoice\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Purchase Invoice\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]", ->>>>>>> c68918bc18 (fix: set a fallback value if no fiscal year set) "filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\"]]", ->>>>>>> e8fff2fdad (fix: use fiscal year instead of calendar year in accounting dashboard number cards) "function": "Sum", "idx": 0, "is_public": 1, "is_standard": 1, "label": "Total Incoming Bills", -<<<<<<< HEAD - "modified": "2024-11-20 19:08:37.043777", -======= "modified": "2026-06-01 12:00:00.000000", ->>>>>>> e8fff2fdad (fix: use fiscal year instead of calendar year in accounting dashboard number cards) "modified_by": "Administrator", "module": "Accounts", "name": "Total Incoming Bills", diff --git a/erpnext/accounts/number_card/total_incoming_payment/total_incoming_payment.json b/erpnext/accounts/number_card/total_incoming_payment/total_incoming_payment.json index 4c13e4c1823..d0f125df5bf 100644 --- a/erpnext/accounts/number_card/total_incoming_payment/total_incoming_payment.json +++ b/erpnext/accounts/number_card/total_incoming_payment/total_incoming_payment.json @@ -4,27 +4,14 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Payment Entry", -<<<<<<< HEAD -<<<<<<< HEAD - "dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", - "filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\",false]]", -======= - "dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Payment Entry\",\"posting_date\",\"Between\",\"[frappe.boot.current_fiscal_year[1], frappe.boot.current_fiscal_year[2]]\"]]", -======= "dynamic_filters_json": "[[\"Payment Entry\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Payment Entry\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]", ->>>>>>> c68918bc18 (fix: set a fallback value if no fiscal year set) "filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\"],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\"]]", ->>>>>>> e8fff2fdad (fix: use fiscal year instead of calendar year in accounting dashboard number cards) "function": "Sum", "idx": 0, "is_public": 1, "is_standard": 1, "label": "Total Incoming Payment", -<<<<<<< HEAD - "modified": "2020-07-22 13:06:20.237689", -======= "modified": "2026-06-01 12:00:00.000000", ->>>>>>> e8fff2fdad (fix: use fiscal year instead of calendar year in accounting dashboard number cards) "modified_by": "Administrator", "module": "Accounts", "name": "Total Incoming Payment", diff --git a/erpnext/accounts/number_card/total_outgoing_bills/total_outgoing_bills.json b/erpnext/accounts/number_card/total_outgoing_bills/total_outgoing_bills.json index 2f66ad50578..5eff4005fda 100644 --- a/erpnext/accounts/number_card/total_outgoing_bills/total_outgoing_bills.json +++ b/erpnext/accounts/number_card/total_outgoing_bills/total_outgoing_bills.json @@ -4,27 +4,14 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Sales Invoice", -<<<<<<< HEAD -<<<<<<< HEAD - "dynamic_filters_json": "[[\"Sales Invoice\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", - "filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Sales Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]", -======= - "dynamic_filters_json": "[[\"Sales Invoice\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Sales Invoice\",\"posting_date\",\"Between\",\"[frappe.boot.current_fiscal_year[1], frappe.boot.current_fiscal_year[2]]\"]]", -======= "dynamic_filters_json": "[[\"Sales Invoice\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Sales Invoice\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]", ->>>>>>> c68918bc18 (fix: set a fallback value if no fiscal year set) "filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\"]]", ->>>>>>> e8fff2fdad (fix: use fiscal year instead of calendar year in accounting dashboard number cards) "function": "Sum", "idx": 0, "is_public": 1, "is_standard": 1, "label": "Total Outgoing Bills", -<<<<<<< HEAD - "modified": "2020-07-22 13:07:19.633101", -======= "modified": "2026-06-01 12:00:00.000000", ->>>>>>> e8fff2fdad (fix: use fiscal year instead of calendar year in accounting dashboard number cards) "modified_by": "Administrator", "module": "Accounts", "name": "Total Outgoing Bills", diff --git a/erpnext/accounts/number_card/total_outgoing_payment/total_outgoing_payment.json b/erpnext/accounts/number_card/total_outgoing_payment/total_outgoing_payment.json index 6e583d8daf5..a78f73c1dc5 100644 --- a/erpnext/accounts/number_card/total_outgoing_payment/total_outgoing_payment.json +++ b/erpnext/accounts/number_card/total_outgoing_payment/total_outgoing_payment.json @@ -4,27 +4,14 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Payment Entry", -<<<<<<< HEAD -<<<<<<< HEAD - "dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", - "filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\",false]]", -======= - "dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Payment Entry\",\"posting_date\",\"Between\",\"[frappe.boot.current_fiscal_year[1], frappe.boot.current_fiscal_year[2]]\"]]", -======= "dynamic_filters_json": "[[\"Payment Entry\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Payment Entry\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]", ->>>>>>> c68918bc18 (fix: set a fallback value if no fiscal year set) "filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\"],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\"]]", ->>>>>>> e8fff2fdad (fix: use fiscal year instead of calendar year in accounting dashboard number cards) "function": "Sum", "idx": 0, "is_public": 1, "is_standard": 1, "label": "Total Outgoing Payment", -<<<<<<< HEAD - "modified": "2020-07-22 12:49:34.942896", -======= "modified": "2026-06-01 12:00:00.000000", ->>>>>>> e8fff2fdad (fix: use fiscal year instead of calendar year in accounting dashboard number cards) "modified_by": "Administrator", "module": "Accounts", "name": "Total Outgoing Payment", From 1b4da9dc961e1aba8091342ba9230fac6f33daed Mon Sep 17 00:00:00 2001 From: pandiyan Date: Tue, 23 Jun 2026 16:54:23 +0530 Subject: [PATCH 34/39] fix(manufacturing): make item_code mandatory in Job Card Item The item_code field in the Job Card Item child table was optional, allowing job cards to be saved without a raw material item linked. Set reqd=1 in the JSON and update the Python type annotation accordingly. (cherry picked from commit d7e9a97f8ad67596e34fb947a42cd8ef08e638d1) --- .../manufacturing/doctype/job_card_item/job_card_item.json | 5 +++-- erpnext/manufacturing/doctype/job_card_item/job_card_item.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json index 93a0b8960e5..f21e94ebbe0 100644 --- a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json +++ b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json @@ -26,7 +26,8 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Item Code", - "options": "Item" + "options": "Item", + "reqd": 1 }, { "fieldname": "source_warehouse", @@ -107,7 +108,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-05-12 12:22:18.506904", + "modified": "2026-06-23 16:52:37.669110", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card Item", diff --git a/erpnext/manufacturing/doctype/job_card_item/job_card_item.py b/erpnext/manufacturing/doctype/job_card_item/job_card_item.py index ecf082392a3..f1b0f53e9c4 100644 --- a/erpnext/manufacturing/doctype/job_card_item/job_card_item.py +++ b/erpnext/manufacturing/doctype/job_card_item/job_card_item.py @@ -16,7 +16,7 @@ class JobCardItem(Document): allow_alternative_item: DF.Check description: DF.Text | None - item_code: DF.Link | None + item_code: DF.Link item_group: DF.Link | None item_name: DF.Data | None parent: DF.Data From 9b6adc42b6ca2d4b954aa69000b6cee922a3073a Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Tue, 23 Jun 2026 00:07:53 +0530 Subject: [PATCH 35/39] fix: show contextual balance label on party dashboard for net balances (cherry picked from commit 3251b403658f0f3f851728fb689ed150ef469d56) --- erpnext/accounts/party.py | 9 +++++++++ erpnext/public/js/utils.js | 31 ++++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index d02230f0ee7..b39c5a7dc62 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -922,6 +922,15 @@ def get_dashboard_info(party_type, party, loyalty_program=None): if party_type == "Supplier": info["total_unpaid"] = -1 * info["total_unpaid"] + if info["total_unpaid"] < 0: + info["balance_label"] = ( + "Total Advance Paid" if party_type == "Supplier" else "Total Advance Received" + ) + info["balance_amount"] = abs(info["total_unpaid"]) + else: + info["balance_label"] = "Total Unpaid" + info["balance_amount"] = info["total_unpaid"] + company_wise_info.append(info) return company_wise_info diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 3ae0b3d1835..8746aa822aa 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -93,11 +93,19 @@ $.extend(erpnext.utils, { ]), "blue" ); + var info = company_wise_info[0]; + var is_advance = info.balance_label !== "Total Unpaid"; + var indicator_label = + info.balance_label === "Total Advance Paid" + ? __("Total Advance Paid: {0}", [format_currency(info.balance_amount, info.currency)]) + : info.balance_label === "Total Advance Received" + ? __("Total Advance Received: {0}", [ + format_currency(info.balance_amount, info.currency), + ]) + : __("Total Unpaid: {0}", [format_currency(info.balance_amount, info.currency)]); frm.dashboard.add_indicator( - __("Total Unpaid: {0}", [ - format_currency(company_wise_info[0].total_unpaid, company_wise_info[0].currency), - ]), - company_wise_info[0].total_unpaid ? "orange" : "green" + indicator_label, + is_advance ? "green" : info.balance_amount ? "orange" : "green" ); if (company_wise_info[0].loyalty_points) { @@ -140,7 +148,14 @@ $.extend(erpnext.utils, { frm.dashboard.stats_area_row.addClass("flex"); frm.dashboard.stats_area_row.css("flex-wrap", "wrap"); - var color = info.total_unpaid ? "orange" : "green"; + var is_advance = info.balance_label !== "Total Unpaid"; + var color = is_advance ? "green" : info.balance_amount ? "orange" : "green"; + var balance_label_text = + info.balance_label === "Total Advance Paid" + ? __("Total Advance Paid") + : info.balance_label === "Total Advance Received" + ? __("Total Advance Received") + : __("Total Unpaid"); var indicator = $( '
' + @@ -154,8 +169,10 @@ $.extend(erpnext.utils, { '" + "
" ).appendTo(frm.dashboard.stats_area_row); From 66d0a60140888720dfc11838db923667328186a7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 22:02:47 +0530 Subject: [PATCH 36/39] feat(crm_settings)!: enable frappe crm data synchronization (backport #56268) (#56383) * feat(crm_settings): enable frappe crm data synchronization (#56268) (cherry picked from commit 0ab812c3ec08f7188c166411f05521fe626cbfb3) # Conflicts: # erpnext/crm/frappe_crm_api.py * chore: resolve conflicts --------- Co-authored-by: Diptanil Saha --- .../doctype/crm_settings/crm_settings.json | 42 ++++++++++++++++- .../crm/doctype/crm_settings/crm_settings.py | 46 ++++++++++++++++++ .../frappe_crm_allowed_user/__init__.py | 0 .../frappe_crm_allowed_user.json | 36 ++++++++++++++ .../frappe_crm_allowed_user.py | 23 +++++++++ erpnext/crm/frappe_crm_api.py | 47 +++++++++---------- 6 files changed, 167 insertions(+), 27 deletions(-) create mode 100644 erpnext/crm/doctype/frappe_crm_allowed_user/__init__.py create mode 100644 erpnext/crm/doctype/frappe_crm_allowed_user/frappe_crm_allowed_user.json create mode 100644 erpnext/crm/doctype/frappe_crm_allowed_user/frappe_crm_allowed_user.py diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.json b/erpnext/crm/doctype/crm_settings/crm_settings.json index 28d43c97d29..8822dd7ea02 100644 --- a/erpnext/crm/doctype/crm_settings/crm_settings.json +++ b/erpnext/crm/doctype/crm_settings/crm_settings.json @@ -20,7 +20,11 @@ "section_break_13", "carry_forward_communication_and_comments", "column_break_junk", - "update_timestamp_on_new_communication" + "update_timestamp_on_new_communication", + "frappe_crm_section", + "enable_frappe_crm_data_synchronization", + "column_break_jbzj", + "allowed_users" ], "fields": [ { @@ -105,13 +109,37 @@ "fieldname": "enable_opportunity_creation_from_contact_us", "fieldtype": "Check", "label": "Enable Opportunity Creation from Contact Us" + }, + { + "fieldname": "frappe_crm_section", + "fieldtype": "Section Break", + "label": "Frappe CRM" + }, + { + "fieldname": "column_break_jbzj", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.enable_frappe_crm_data_synchronization === 1;", + "fieldname": "allowed_users", + "fieldtype": "Table MultiSelect", + "label": "Allowed Users", + "options": "Frappe CRM Allowed User", + "permlevel": 1 + }, + { + "default": "0", + "fieldname": "enable_frappe_crm_data_synchronization", + "fieldtype": "Check", + "label": "Enable Frappe CRM Data Synchronization", + "permlevel": 1 } ], "icon": "fa fa-cog", "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-06-11 23:09:49.750381", + "modified": "2026-06-22 01:26:13.474915", "modified_by": "Administrator", "module": "CRM", "name": "CRM Settings", @@ -145,6 +173,16 @@ "role": "Sales Master Manager", "share": 1, "write": 1 + }, + { + "delete": 1, + "email": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 } ], "sort_field": "modified", diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.py b/erpnext/crm/doctype/crm_settings/crm_settings.py index 01cdaf41bde..04e5a402add 100644 --- a/erpnext/crm/doctype/crm_settings/crm_settings.py +++ b/erpnext/crm/doctype/crm_settings/crm_settings.py @@ -3,6 +3,7 @@ import frappe from frappe import _ +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields, delete_custom_fields from frappe.model.document import Document @@ -15,12 +16,16 @@ class CRMSettings(Document): if TYPE_CHECKING: from frappe.types import DF + from erpnext.crm.doctype.frappe_crm_allowed_user.frappe_crm_allowed_user import FrappeCRMAllowedUser + allow_lead_duplication_based_on_emails: DF.Check + allowed_users: DF.TableMultiSelect[FrappeCRMAllowedUser] auto_creation_of_contact: DF.Check campaign_naming_by: DF.Literal["Campaign Name", "Naming Series"] carry_forward_communication_and_comments: DF.Check close_opportunity_after_days: DF.Int default_valid_till: DF.Data | None + enable_frappe_crm_data_synchronization: DF.Check enable_opportunity_creation_from_contact_us: DF.Check update_timestamp_on_new_communication: DF.Check # end: auto-generated types @@ -28,6 +33,7 @@ class CRMSettings(Document): def validate(self): frappe.db.set_default("campaign_naming_by", self.get("campaign_naming_by", "")) self.validate_enable_opportunity_creation_from_contact_us() + self.validate_allowed_users() def validate_enable_opportunity_creation_from_contact_us(self): contact_disabled = frappe.get_single_value("Contact Us Settings", "is_disabled") @@ -38,3 +44,43 @@ class CRMSettings(Document): "Cannot enable Opportunity creation from Contact Us because the Contact Us form is disabled." ) ) + + def validate_allowed_users(self): + if self.enable_frappe_crm_data_synchronization and not self.allowed_users: + frappe.throw( + _( + "Please add atleast one user on Allowed Users to allow Data Synchronization from Frappe CRM site." + ) + ) + + def before_save(self): + self.clear_allowed_users() + + def on_update(self): + self.custom_fields_for_frappe_crm_data_sync() + + def clear_allowed_users(self): + if not self.enable_frappe_crm_data_synchronization: + self.allowed_users = [] + + def custom_fields_for_frappe_crm_data_sync(self): + custom_fields = { + "Quotation": [ + { + "fieldname": "crm_deal", + "fieldtype": "Data", + "label": "Frappe CRM Deal", + "insert_after": "party_name", + } + ], + "Customer": [ + { + "fieldname": "crm_deal", + "fieldtype": "Data", + "label": "Frappe CRM Deal", + "insert_after": "prospect_name", + } + ], + } + + create_custom_fields(custom_fields, ignore_validate=True) diff --git a/erpnext/crm/doctype/frappe_crm_allowed_user/__init__.py b/erpnext/crm/doctype/frappe_crm_allowed_user/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/crm/doctype/frappe_crm_allowed_user/frappe_crm_allowed_user.json b/erpnext/crm/doctype/frappe_crm_allowed_user/frappe_crm_allowed_user.json new file mode 100644 index 00000000000..403a5573460 --- /dev/null +++ b/erpnext/crm/doctype/frappe_crm_allowed_user/frappe_crm_allowed_user.json @@ -0,0 +1,36 @@ +{ + "actions": [], + "allow_bulk_edit": 1, + "allow_rename": 1, + "creation": "2026-06-22 00:47:12.265968", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "user" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "label": "User", + "options": "User", + "reqd": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2026-06-22 01:49:54.586410", + "modified_by": "Administrator", + "module": "CRM", + "name": "Frappe CRM Allowed User", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/erpnext/crm/doctype/frappe_crm_allowed_user/frappe_crm_allowed_user.py b/erpnext/crm/doctype/frappe_crm_allowed_user/frappe_crm_allowed_user.py new file mode 100644 index 00000000000..0cbc5b8ab76 --- /dev/null +++ b/erpnext/crm/doctype/frappe_crm_allowed_user/frappe_crm_allowed_user.py @@ -0,0 +1,23 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class FrappeCRMAllowedUser(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + user: DF.Link + # end: auto-generated types + + _DOCTYPE_NAME = "Frappe CRM Allowed User" diff --git a/erpnext/crm/frappe_crm_api.py b/erpnext/crm/frappe_crm_api.py index 586ebe91eed..5db9b7dc652 100644 --- a/erpnext/crm/frappe_crm_api.py +++ b/erpnext/crm/frappe_crm_api.py @@ -2,35 +2,12 @@ import json import frappe from frappe import _ -from frappe.custom.doctype.custom_field.custom_field import create_custom_fields - - -@frappe.whitelist() -def create_custom_fields_for_frappe_crm(): - frappe.only_for("System Manager") - custom_fields = { - "Quotation": [ - { - "fieldname": "crm_deal", - "fieldtype": "Data", - "label": "Frappe CRM Deal", - "insert_after": "party_name", - } - ], - "Customer": [ - { - "fieldname": "crm_deal", - "fieldtype": "Data", - "label": "Frappe CRM Deal", - "insert_after": "prospect_name", - } - ], - } - create_custom_fields(custom_fields, ignore_validate=True) @frappe.whitelist() def create_prospect_against_crm_deal(): + validate_frappe_crm_sync() + doc = frappe.form_dict prospect = frappe.new_doc("Prospect") prospect.company_name = doc.organization or doc.lead_name @@ -161,6 +138,8 @@ CUSTOMER_ALLOWED_FIELDS = { @frappe.whitelist() def create_customer(customer_data=None): + validate_frappe_crm_sync() + if not customer_data: customer_data = frappe.form_dict @@ -181,3 +160,21 @@ def create_customer(customer_data=None): except Exception: frappe.log_error(frappe.get_traceback(), "Error while creating customer against Frappe CRM Deal") pass + + +def validate_frappe_crm_sync(): + CRMSettings = frappe.get_single("CRM Settings") + if not CRMSettings.enable_frappe_crm_data_synchronization: + frappe.throw( + _("Frappe CRM data synchronization is not enabled on ERPNext. Contact System Manager of ERPNext.") + ) + + allowed_users = [d.user for d in CRMSettings.allowed_users] + + if frappe.session.user not in allowed_users: + frappe.throw( + _( + "User not allowed to synchronize data from Frappe CRM on ERPNext. Contact System Manager of ERPNext." + ), + exc=frappe.PermissionError, + ) From 53a11229ec4dd2971375ec701ae240cac2331674 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 19:49:07 +0000 Subject: [PATCH 37/39] fix(budget): ambiguous error message for budget assignment validation (backport #56390) (#56391) Co-authored-by: Wolfram Schmidt Co-authored-by: Diptanil Saha fix(budget): ambiguous error message for budget assignment validation (#56390) --- erpnext/accounts/doctype/budget/budget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/budget/budget.py b/erpnext/accounts/doctype/budget/budget.py index 4ef1f164aaa..e1038b6af33 100644 --- a/erpnext/accounts/doctype/budget/budget.py +++ b/erpnext/accounts/doctype/budget/budget.py @@ -103,8 +103,8 @@ class Budget(Document): elif account_details.report_type != "Profit and Loss": frappe.throw( _( - "Budget cannot be assigned against {0}, as it's not an Income or Expense account" - ).format(d.account) + "Budget cannot be assigned against {0}, as its Root Type is not of Income or Expense" + ).format(self.account) ) if d.account in account_list: From cef608d043b0198aa57fd55f6bff7d85b7cea7b6 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 02:10:54 +0530 Subject: [PATCH 38/39] fix(payment_entry): recompute base amount when exchange rate changes (backport #56136) (#56397) Co-authored-by: Ravibharathi <131471282+ravibharathi656@users.noreply.github.com> Co-authored-by: ervishnucs fix(payment_entry): recompute base amount when exchange rate changes (#56136) --- .../doctype/payment_entry/payment_entry.js | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 14fc2b51b19..5a5815ae4c7 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -769,17 +769,21 @@ frappe.ui.form.on("Payment Entry", { frm.set_paid_amount_based_on_received_amount = true; let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; - if (frm.doc.base_received_amount && frm.doc.source_exchange_rate) { - frm.set_value("base_paid_amount", frm.doc.base_received_amount); + if (frm.doc.paid_amount && frm.doc.source_exchange_rate) { + frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate)); + frm.set_value("base_received_amount", frm.doc.base_paid_amount); // target exchange rate should always be same as source if both account currencies is same if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) { frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate); + frm.set_value("received_amount", frm.doc.paid_amount); } else { - frm.set_value( - "paid_amount", - flt(frm.doc.base_paid_amount) / flt(frm.doc.source_exchange_rate) - ); + const target_rate = + flt(frm.doc.target_exchange_rate) || + (company_currency == frm.doc.paid_to_account_currency ? 1 : 0); + if (target_rate) { + frm.set_value("received_amount", flt(frm.doc.base_received_amount) / target_rate); + } } // set_unallocated_amount is called by below method, @@ -795,18 +799,23 @@ frappe.ui.form.on("Payment Entry", { target_exchange_rate: function (frm) { let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; - if (frm.doc.base_paid_amount && frm.doc.target_exchange_rate) { - frm.set_value("base_received_amount", frm.doc.base_paid_amount); - if ( - !frm.doc.source_exchange_rate && - frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency - ) { + if (frm.doc.received_amount && frm.doc.target_exchange_rate) { + frm.set_value( + "base_received_amount", + flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate) + ); + frm.set_value("base_paid_amount", frm.doc.base_received_amount); + + if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) { frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate); + frm.set_value("paid_amount", frm.doc.received_amount); } else { - frm.set_value( - "received_amount", - flt(frm.doc.base_received_amount) / flt(frm.doc.target_exchange_rate) - ); + const source_rate = + flt(frm.doc.source_exchange_rate) || + (company_currency == frm.doc.paid_from_account_currency ? 1 : 0); + if (source_rate) { + frm.set_value("paid_amount", flt(frm.doc.base_paid_amount) / source_rate); + } } // set_unallocated_amount is called by below method, From 334a0b2137d75a5cc182b4c261617e0f26b770be Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 02:24:29 +0530 Subject: [PATCH 39/39] fix(pos): remove redundant opening balance dialog onchange handler (backport #54591) (#56402) Co-authored-by: Ravibharathi <131471282+ravibharathi656@users.noreply.github.com> fix(pos): remove redundant opening balance dialog onchange handler (#54591) --- erpnext/selling/page/point_of_sale/pos_controller.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index e232464b53a..f042718045c 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -40,15 +40,6 @@ erpnext.PointOfSale.Controller = class { in_list_view: 1, label: __("Opening Amount"), options: "company:company_currency", - onchange: function () { - dialog.fields_dict.balance_details.df.data.some((d) => { - if (d.idx == this.doc.idx) { - d.opening_amount = this.value; - dialog.fields_dict.balance_details.grid.refresh(); - return true; - } - }); - }, }, ]; const fetch_pos_payment_methods = () => {