From 16fbf8299f0a90e0faf5620019c28e28cea25ed2 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 27 May 2026 00:23:04 +0000 Subject: [PATCH 001/109] chore(release): Bumped to Version 15.109.0 # [15.109.0](https://github.com/frappe/erpnext/compare/v15.108.3...v15.109.0) (2026-05-27) ### Bug Fixes * consider batchwise valuation in stock ageing report (backport [#54919](https://github.com/frappe/erpnext/issues/54919)) ([#55229](https://github.com/frappe/erpnext/issues/55229)) ([418a7fb](https://github.com/frappe/erpnext/commit/418a7fb3015553884ecb739126793dfd809e00b5)) * consumed operation cost calculation (backport [#54858](https://github.com/frappe/erpnext/issues/54858)) ([#55132](https://github.com/frappe/erpnext/issues/55132)) ([46d5395](https://github.com/frappe/erpnext/commit/46d5395148747f8b4259a418fab98035bc4897b5)) * default use_for_shopping_cart to 0 in set_taxes ([960be3e](https://github.com/frappe/erpnext/commit/960be3e081959f6dff9537d5cab58b466020a5da)) * edit stock uom qty for purchase documents (backport [#55135](https://github.com/frappe/erpnext/issues/55135)) ([#55178](https://github.com/frappe/erpnext/issues/55178)) ([425e6c5](https://github.com/frappe/erpnext/commit/425e6c52f411c1bd69ea18fb3ce5e16cf04b6398)) * ERPNextTestSuite to change_settings ([76078a7](https://github.com/frappe/erpnext/commit/76078a7fb941ba2b820e1af9ac8c040f6a774654)) * faster range calculation on process period closing voucher ([bf27f2d](https://github.com/frappe/erpnext/commit/bf27f2d869417789a1c4d74c30c6ff3bc5a3d06e)) * fg valuation rate in repack entry when multiple FGs ([238f168](https://github.com/frappe/erpnext/commit/238f1685f1fcf33f6bef6a6a18b5c2b02ab27b11)) * **general-ledger:** show raw GL entries when categorize_by is empty (backport [#54816](https://github.com/frappe/erpnext/issues/54816)) ([#54829](https://github.com/frappe/erpnext/issues/54829)) ([b972b7c](https://github.com/frappe/erpnext/commit/b972b7c307cf01f754d02ee872b6f949908e9a68)) * import change_settings ([9d21199](https://github.com/frappe/erpnext/commit/9d211990c3a3782a938fb2ad7c24d2941783a73e)) * inclusive tax amount not considered while setting LCV from purchase invoice ([cba4c9f](https://github.com/frappe/erpnext/commit/cba4c9f0ee64f882657ed4bbea27f2dc5475f61a)) * incoming rate for legacy serial no ([6e6ef83](https://github.com/frappe/erpnext/commit/6e6ef83d60d3f42b32b2d578889e8bd5f0f2e62b)) * incorrect error message string in sales order (backport [#55090](https://github.com/frappe/erpnext/issues/55090)) ([#55094](https://github.com/frappe/erpnext/issues/55094)) ([04e28f9](https://github.com/frappe/erpnext/commit/04e28f95564e1579e25b25f87214a5916a069b87)) * invalid filter on item_group (backport [#55186](https://github.com/frappe/erpnext/issues/55186)) ([#55187](https://github.com/frappe/erpnext/issues/55187)) ([25739ae](https://github.com/frappe/erpnext/commit/25739ae217bee2411dd3d8564fd431f1c80ce6b5)) * merge conflicts ([59e9f51](https://github.com/frappe/erpnext/commit/59e9f5192c56d4bff7c10290e01fb30f8c3f301e)) * **payment_entry:** sync paid/received amounts for cross-currency entries (backport [#55270](https://github.com/frappe/erpnext/issues/55270)) ([#55271](https://github.com/frappe/erpnext/issues/55271)) ([d31a051](https://github.com/frappe/erpnext/commit/d31a051c747923d76559207e5642c02d706d7b28)) * prevent AttributeError in batch query filters (backport [#55257](https://github.com/frappe/erpnext/issues/55257)) ([#55278](https://github.com/frappe/erpnext/issues/55278)) ([4f89f3a](https://github.com/frappe/erpnext/commit/4f89f3a85646061ef39730eb658b9904f7b3b0b3)) * **project:** update customer and sales order as no copy ([9145760](https://github.com/frappe/erpnext/commit/914576040eb4d30b2f85aa4733268e8b9474315a)) * removed redundant code ([259f499](https://github.com/frappe/erpnext/commit/259f499e2550dcaa71907ce2f32965a817a6f376)) * set bin details when adding item using update items (backport [#55096](https://github.com/frappe/erpnext/issues/55096)) ([#55097](https://github.com/frappe/erpnext/issues/55097)) ([aa79247](https://github.com/frappe/erpnext/commit/aa79247c390d0f8506764035434f75f0ffb2ee94)) * single variant creation error (backport [#55286](https://github.com/frappe/erpnext/issues/55286)) ([#55288](https://github.com/frappe/erpnext/issues/55288)) ([937eb87](https://github.com/frappe/erpnext/commit/937eb87932e9fd6fdbacca33638e9012a4f15f77)) * **stock:** apply posting datetime filters while fetching available batches (backport [#54976](https://github.com/frappe/erpnext/issues/54976)) ([#55184](https://github.com/frappe/erpnext/issues/55184)) ([ff442cd](https://github.com/frappe/erpnext/commit/ff442cd8e7611d62d5f187376d3dc6160fd91992)) * **stock:** remove precision for valuation rate while creating sle (backport [#55249](https://github.com/frappe/erpnext/issues/55249)) ([#55259](https://github.com/frappe/erpnext/issues/55259)) ([8b241b4](https://github.com/frappe/erpnext/commit/8b241b45e2c6768ee33515eff4d537c5513158d1)) * **stock:** remove recalculate current qty function ([#55121](https://github.com/frappe/erpnext/issues/55121)) ([1c90c3b](https://github.com/frappe/erpnext/commit/1c90c3bbc210a1280fbedc1cac0f2061b97f9428)) * update import ([31c251d](https://github.com/frappe/erpnext/commit/31c251d9561d8149bc8d79e0e2c4a41bec18e7f6)) * use passed posting date in make_reverse_gl_entries ([4436585](https://github.com/frappe/erpnext/commit/4436585aa07a82ab3704d8091fd99482854aa854)) ### Features * add get_parent_supplier_groups using query builder ([6517ed7](https://github.com/frappe/erpnext/commit/6517ed72b4816b69f080259c6a7135bd82e30fe9)) ### Performance Improvements * skip delink_original_entry during cancellation when Immutable Ledger is enabled ([#55130](https://github.com/frappe/erpnext/issues/55130)) ([034e159](https://github.com/frappe/erpnext/commit/034e159ee4cb474d45fd76f44b7cfb95fea4460c)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index dbda07fc37e..36897538fc3 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from frappe.utils.user import is_website_user -__version__ = "15.108.3" +__version__ = "15.109.0" def get_default_company(user=None): From bfdf1e43f9f2bf4e855190bce96b543c3f1afe01 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 17:35:20 +0530 Subject: [PATCH 002/109] fix: material transfer in transit issue (backport #55320) (backport #55324) (#55404) Co-authored-by: Rohit Waghchaure fix: material transfer in transit issue (backport #55320) (#55324) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index f0d7e10ad73..e6d6dab7426 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -3132,12 +3132,12 @@ class StockEntry(StockController): args = { "source_dt": "Stock Entry Detail", "target_field": "transferred_qty", - "target_ref_field": "qty", + "target_ref_field": "transfer_qty", "target_dt": "Stock Entry Detail", "join_field": "ste_detail", "target_parent_dt": "Stock Entry", "target_parent_field": "per_transferred", - "source_field": "qty", + "source_field": "transfer_qty", "percent_join_field": "against_stock_entry", } From 9e7b03173d0d16823e2261dbc1cfcc935ee47c0b Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Fri, 29 May 2026 12:07:01 +0000 Subject: [PATCH 003/109] chore(release): Bumped to Version 15.109.1 ## [15.109.1](https://github.com/frappe/erpnext/compare/v15.109.0...v15.109.1) (2026-05-29) ### Bug Fixes * material transfer in transit issue (backport [#55320](https://github.com/frappe/erpnext/issues/55320)) (backport [#55324](https://github.com/frappe/erpnext/issues/55324)) ([#55404](https://github.com/frappe/erpnext/issues/55404)) ([bfdf1e4](https://github.com/frappe/erpnext/commit/bfdf1e43f9f2bf4e855190bce96b543c3f1afe01)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 36897538fc3..19e050c3069 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from frappe.utils.user import is_website_user -__version__ = "15.109.0" +__version__ = "15.109.1" def get_default_company(user=None): From c2063c4707eccbf67e1ec63b00587a4f8e6044b3 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 29 May 2026 19:17:34 +0530 Subject: [PATCH 004/109] fix: billing address does not belongs to the company error (cherry picked from commit 9df07b367a29eceae700614d5a98485cf48df080) (cherry picked from commit 5c392d61236176c367544dc336468b82b38eff5b) --- erpnext/public/js/controllers/buying.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 01d458d6b16..ff7218d78ae 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -176,14 +176,9 @@ erpnext.buying = { callback: (r) => { if (!r.message) return; - if (!this.frm.doc.billing_address) { - this.frm.set_value("billing_address", r.message.primary_address || ""); - } + this.frm.set_value("billing_address", r.message.primary_address || ""); - if ( - frappe.meta.has_field(this.frm.doc.doctype, "shipping_address") && - !this.frm.doc.shipping_address - ) { + if (frappe.meta.has_field(this.frm.doc.doctype, "shipping_address")) { this.frm.set_value("shipping_address", r.message.shipping_address || ""); } }, From 1c5220b86f136e7f0faec020d057ada68e282c85 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Mon, 1 Jun 2026 06:08:30 +0000 Subject: [PATCH 005/109] chore(release): Bumped to Version 15.109.2 ## [15.109.2](https://github.com/frappe/erpnext/compare/v15.109.1...v15.109.2) (2026-06-01) ### Bug Fixes * billing address does not belongs to the company error ([c2063c4](https://github.com/frappe/erpnext/commit/c2063c4707eccbf67e1ec63b00587a4f8e6044b3)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 19e050c3069..6c6d7c3894d 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from frappe.utils.user import is_website_user -__version__ = "15.109.1" +__version__ = "15.109.2" def get_default_company(user=None): From 4a6af25d11fd2d352a44005d7cf6bd277cf2a2e7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 1 Jun 2026 12:13:49 +0530 Subject: [PATCH 006/109] fix: only consider non-opening balance for Balance sheet accounts (cherry picked from commit a2b83340461de901f097a99910de8e79052f8b67) --- .../process_period_closing_voucher.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py index e85ebd29e52..e23751a8200 100644 --- a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py @@ -559,6 +559,9 @@ def process_individual_date(docname: str, date, report_type, parentfield): if parentfield == "z_opening_balances": query = query.where(gle.is_opening.eq("Yes")) + else: + # Keep balances aligned with legacy PCV logic (non-opening transactions only) + query = query.where(gle.is_opening.eq("No")) query = query.groupby(gle.account) for dim in dimensions: From 94e15ae9efacd91ae417a3f7ac4dda74c5722c21 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 1 Jun 2026 12:17:44 +0530 Subject: [PATCH 007/109] refactor: tabbed view for process period closing voucher (cherry picked from commit 1960c816194d8f3ce91c1508b2db23430ef481c1) --- .../process_period_closing_voucher.json | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.json b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.json index a06a16f156c..0a30b6564b5 100644 --- a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.json +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_bulk_edit": 1, "autoname": "format:Process-PCV-{###}", "creation": "2025-09-25 15:44:03.534699", "doctype": "DocType", @@ -7,11 +8,13 @@ "field_order": [ "parent_pcv", "status", + "amended_from", + "section_normal_balances", "p_l_closing_balance", - "normal_balances", "bs_closing_balance", - "z_opening_balances", - "amended_from" + "normal_balances", + "section_opening_balances", + "z_opening_balances" ], "fields": [ { @@ -64,17 +67,27 @@ "fieldname": "bs_closing_balance", "fieldtype": "JSON", "label": "Balance Sheet Closing Balance" + }, + { + "fieldname": "section_normal_balances", + "fieldtype": "Tab Break", + "label": "Normal Balances" + }, + { + "fieldname": "section_opening_balances", + "fieldtype": "Tab Break", + "label": "Opening Balances" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-11-05 11:40:24.996403", + "modified": "2026-06-01 12:16:37.374412", "modified_by": "Administrator", "module": "Accounts", "name": "Process Period Closing Voucher", - "naming_rule": "Expression", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { From e314d0cfc5dfd5a22264d38abacaa2c77377f537 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 1 Jun 2026 12:51:04 +0530 Subject: [PATCH 008/109] refactor: color coded status in list view (cherry picked from commit cfeffbb3543ae05dd4fcd8f644c63e7a33144b72) --- .../process_period_closing_voucher_list.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher_list.js diff --git a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher_list.js b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher_list.js new file mode 100644 index 00000000000..4b117b8fbcf --- /dev/null +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher_list.js @@ -0,0 +1,17 @@ +// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// License: GNU General Public License v3. See license.txt + +// render +frappe.listview_settings["Process Period Closing Voucher"] = { + add_fields: ["status"], + get_indicator: function (doc) { + const status_colors = { + Queued: "blue", + Running: "orange", + Paused: "gray", + Completed: "green", + Cancelled: "red", + }; + return [__(doc.status), status_colors[doc.status], "status,=," + doc.status]; + }, +}; From 13eeddd1f6a691987018a3d6fe41c4b89ae135d7 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Mon, 1 Jun 2026 14:01:43 +0000 Subject: [PATCH 009/109] chore(release): Bumped to Version 15.109.3 ## [15.109.3](https://github.com/frappe/erpnext/compare/v15.109.2...v15.109.3) (2026-06-01) ### Bug Fixes * only consider non-opening balance for Balance sheet accounts ([4a6af25](https://github.com/frappe/erpnext/commit/4a6af25d11fd2d352a44005d7cf6bd277cf2a2e7)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 6c6d7c3894d..a74faef55c6 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from frappe.utils.user import is_website_user -__version__ = "15.109.2" +__version__ = "15.109.3" def get_default_company(user=None): From 81ce5fbee90f78301e9d3f89d324af1c690911ea Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Mon, 1 Jun 2026 12:47:31 +0530 Subject: [PATCH 010/109] 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 011/109] 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 d6b2fb2f96a32018ee79cdeb89e39321bfb959f1 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 2 Jun 2026 16:55:43 +0000 Subject: [PATCH 012/109] chore(release): Bumped to Version 15.110.0 # [15.110.0](https://github.com/frappe/erpnext/compare/v15.109.3...v15.110.0) (2026-06-02) ### Bug Fixes * billing address does not belongs to the company error ([5c392d6](https://github.com/frappe/erpnext/commit/5c392d61236176c367544dc336468b82b38eff5b)) * **book_appointment:** when scheduling is disabled, block API endpoints (backport [#55455](https://github.com/frappe/erpnext/issues/55455)) ([#55456](https://github.com/frappe/erpnext/issues/55456)) ([2a12ae1](https://github.com/frappe/erpnext/commit/2a12ae1afe368c122a63d56a5153271f23fa311f)) * check perm for account (backport [#55479](https://github.com/frappe/erpnext/issues/55479)) ([#55482](https://github.com/frappe/erpnext/issues/55482)) ([1238aeb](https://github.com/frappe/erpnext/commit/1238aeb30a4a48e7e6830cfaf8d81db74ad4b0a3)) * **issue:** check permission before issue status modification (backport [#55458](https://github.com/frappe/erpnext/issues/55458)) ([#55459](https://github.com/frappe/erpnext/issues/55459)) ([338feb3](https://github.com/frappe/erpnext/commit/338feb31e1410bde26c887fe56d7fa6aeaaea273)) * **je:** preserve account on duplicate row when party row exists (backport [#55180](https://github.com/frappe/erpnext/issues/55180)) ([#55513](https://github.com/frappe/erpnext/issues/55513)) ([741216d](https://github.com/frappe/erpnext/commit/741216d3eb726d5429b12512e3aefaebf00e094c)) * **manufacturing:** allow to edit batch size while creating a work order ([#55332](https://github.com/frappe/erpnext/issues/55332)) ([41bf2f3](https://github.com/frappe/erpnext/commit/41bf2f32fd223b1074988bea0a5dcecfaee651d7)) * material transfer in transit issue (backport [#55320](https://github.com/frappe/erpnext/issues/55320)) ([#55324](https://github.com/frappe/erpnext/issues/55324)) ([067c23f](https://github.com/frappe/erpnext/commit/067c23f20e5541eacb37a188be379121c2e7ddfb)) * new bom version should not recalculate operations through routing (backport [#55370](https://github.com/frappe/erpnext/issues/55370)) ([#55371](https://github.com/frappe/erpnext/issues/55371)) ([4669ff2](https://github.com/frappe/erpnext/commit/4669ff295f6beab550fd05980bc6927297ee36a7)) * pick correct name when creating user from RFQ (backport [#55468](https://github.com/frappe/erpnext/issues/55468)) ([#55471](https://github.com/frappe/erpnext/issues/55471)) ([e429e60](https://github.com/frappe/erpnext/commit/e429e608c2ab9451bf307b46776563dc8a3d2a44)) * **pos:** escape html output in pos page templates (backport [#55527](https://github.com/frappe/erpnext/issues/55527)) ([#55528](https://github.com/frappe/erpnext/issues/55528)) ([689a3f5](https://github.com/frappe/erpnext/commit/689a3f50ae114c2e9751fe9dba2184b40d6674d9)) * **pos:** escape item data on pos item selector (backport [#55503](https://github.com/frappe/erpnext/issues/55503)) ([#55523](https://github.com/frappe/erpnext/issues/55523)) ([96bd97d](https://github.com/frappe/erpnext/commit/96bd97dd6d90b6d0c2186fb83d018621197b57da)) * **pos:** preserve contacts and enforce permissions in set_customer_info (backport [#55463](https://github.com/frappe/erpnext/issues/55463)) ([#55465](https://github.com/frappe/erpnext/issues/55465)) ([0353262](https://github.com/frappe/erpnext/commit/03532624b847598e6b27e7fd222f38bb748bbe5f)) * **ppr:** make default_advance_account optional ([aa94c3f](https://github.com/frappe/erpnext/commit/aa94c3ff22b6f1cb92b1b49c7bbd3b441228f707)) * **quotation:** made customer contact column visible (backport [#55433](https://github.com/frappe/erpnext/issues/55433)) ([#55434](https://github.com/frappe/erpnext/issues/55434)) ([a2d924c](https://github.com/frappe/erpnext/commit/a2d924c48f4192eccba4be010b04dd6f68b3ea44)) * **regional:** Japanese CT Rate (backport [#54998](https://github.com/frappe/erpnext/issues/54998)) ([#55437](https://github.com/frappe/erpnext/issues/55437)) ([2a52ea6](https://github.com/frappe/erpnext/commit/2a52ea6850e84e45338456d93c226c8085962746)) * replace get_query with get_list for permission-aware queries in v15 ([ad511b8](https://github.com/frappe/erpnext/commit/ad511b80c05b8f2efddc1b03494b9a473808f7b9)) * stock reco for legacy serial nos ([93dcba4](https://github.com/frappe/erpnext/commit/93dcba40ec3819a175c9c088d885e37159f848d8)) * **stock:** add warning message to notify the user to configure the inspection ([42e2fd5](https://github.com/frappe/erpnext/commit/42e2fd5fc98c4881aadfa9959a0fb725e287109c)) * **stock:** allow to create quality inspection after purchase/delivery ([10664b7](https://github.com/frappe/erpnext/commit/10664b7b9539f822e126014c9e8dae418ce818d6)) * **stock:** change qb to qb get_query to fix filter issues (backport [#55443](https://github.com/frappe/erpnext/issues/55443)) ([#55444](https://github.com/frappe/erpnext/issues/55444)) ([75d00ef](https://github.com/frappe/erpnext/commit/75d00ef173c24d4230d20f8311325520277f90e4)) * **stock:** change valuation rate column label in stock ledger entry/report (backport [#55323](https://github.com/frappe/erpnext/issues/55323)) ([#55393](https://github.com/frappe/erpnext/issues/55393)) ([94fd15e](https://github.com/frappe/erpnext/commit/94fd15e550bc2a058463c9002a5ba043b7c677cf)) * **stock:** get_actual_qty during cancellations (backport [#55388](https://github.com/frappe/erpnext/issues/55388)) ([#55391](https://github.com/frappe/erpnext/issues/55391)) ([ad6e3a4](https://github.com/frappe/erpnext/commit/ad6e3a45d224b420a7b3ff4ed20c72cb40783e7f)) * update default_advance_account type ([7200c22](https://github.com/frappe/erpnext/commit/7200c228900b2033a681a7f3e65417c98626b6b7)) * use get_query instead of get_all for data fetching ([264433b](https://github.com/frappe/erpnext/commit/264433b23d61ffa41a75adcc9c554941e1de4e29)) ### Features * **payment-entry:** warn user before cancelling reconciled payment entry ([87c6ad4](https://github.com/frappe/erpnext/commit/87c6ad4f85f1401fdf5cd804c4b130398d4ad80b)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index a74faef55c6..4be8b579505 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from frappe.utils.user import is_website_user -__version__ = "15.109.3" +__version__ = "15.110.0" def get_default_company(user=None): From eef075a2bac9b4b37ba73d8b58da00a6fed6c65d Mon Sep 17 00:00:00 2001 From: Lakshit Jain Date: Tue, 9 Jun 2026 17:52:19 +0530 Subject: [PATCH 013/109] 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 014/109] 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 2b3e3dfd83558539f79bc6a5ecd5462f6678dd21 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 10 Jun 2026 00:24:00 +0000 Subject: [PATCH 015/109] chore(release): Bumped to Version 15.111.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [15.111.0](https://github.com/frappe/erpnext/compare/v15.110.0...v15.111.0) (2026-06-10) ### Bug Fixes * **accounts:** include asset items in purchase receipt validation ([#55150](https://github.com/frappe/erpnext/issues/55150)) ([611849f](https://github.com/frappe/erpnext/commit/611849f9534068d621f35cec155f83d9eebcade4)) * add permission checks in accounts whitelisted methods ([2d1c0dc](https://github.com/frappe/erpnext/commit/2d1c0dcb53e4ff1069ceb39f67063cb954f578dd)) * bypass project permission check when updating consumed material … (backport [#55645](https://github.com/frappe/erpnext/issues/55645)) ([#55706](https://github.com/frappe/erpnext/issues/55706)) ([cedd0a1](https://github.com/frappe/erpnext/commit/cedd0a19035f6e37d17c907478fcb7587cf6f4fb)) * **cheque_print_template:** print format creation from cheque print template requires system manager (backport [#55708](https://github.com/frappe/erpnext/issues/55708)) ([#55711](https://github.com/frappe/erpnext/issues/55711)) ([2f5b93e](https://github.com/frappe/erpnext/commit/2f5b93e308446f0842c413cf9e2c2eb1d9de0d42)) * correct field order in Address and Contacts report ([bde46cf](https://github.com/frappe/erpnext/commit/bde46cffd0e2321860435eceeaa6c59589ef315c)) * correct field order in Address and Contacts report ([08f3cf9](https://github.com/frappe/erpnext/commit/08f3cf98f9e0738a01ca8a5809fdbfb1b7bb6104)) * do not allow to make changes in SABB after submit ([07b6111](https://github.com/frappe/erpnext/commit/07b61113af40c5a247b1cb51c6bdb4afc8e83eeb)) * drop ignore_permissions handling from add_ac ([2d0e3fd](https://github.com/frappe/erpnext/commit/2d0e3fd9af521e407be35fd09e9427176609513f)) * duplicating a Customer/Supplier shouldn't inherit the source's primary contact and address (backport [#55421](https://github.com/frappe/erpnext/issues/55421)) ([#55608](https://github.com/frappe/erpnext/issues/55608)) ([013bd1a](https://github.com/frappe/erpnext/commit/013bd1a5664a7f3a5b65f26b77a63644e06f68eb)) * handle multi-select stock ageing filters ([#55776](https://github.com/frappe/erpnext/issues/55776)) ([95f46df](https://github.com/frappe/erpnext/commit/95f46dfc015517ca390b29ad27c46fa4575f7fb2)) * **item:** format integer numeric variant attributes without decimals (backport [#55561](https://github.com/frappe/erpnext/issues/55561)) ([#55563](https://github.com/frappe/erpnext/issues/55563)) ([4772799](https://github.com/frappe/erpnext/commit/4772799db22292cf236a4a8759938f726a691b59)) * naming series issue ([e7eaa87](https://github.com/frappe/erpnext/commit/e7eaa87a77db77b0d421b1aa3c70a5024c4e378f)) * persist main item code for MR plan items ([#55623](https://github.com/frappe/erpnext/issues/55623)) ([e8e0514](https://github.com/frappe/erpnext/commit/e8e0514a30e3c8514c25c85e74e7d36ad612dc67)) * prevent leakage of party-derived fields in cross doctype transactions (backport [#55336](https://github.com/frappe/erpnext/issues/55336)) ([#55578](https://github.com/frappe/erpnext/issues/55578)) ([0e64acb](https://github.com/frappe/erpnext/commit/0e64acb0fa04042268805e11fa4f7b4a082708aa)) * prevent selling items from sample retention warehouse (backport [#55613](https://github.com/frappe/erpnext/issues/55613)) ([#55633](https://github.com/frappe/erpnext/issues/55633)) ([c15012c](https://github.com/frappe/erpnext/commit/c15012cd51c80baff894081aa25472a6c6d53959)) * **process statement of accounts:** validate pdf_name and validate permission before triggering send_auto_email (backport [#55781](https://github.com/frappe/erpnext/issues/55781)) ([#55782](https://github.com/frappe/erpnext/issues/55782)) ([18ca96c](https://github.com/frappe/erpnext/commit/18ca96c36ba65362bf1c25abb8eab32c64c6c7dd)) * remove item name from update items dialog item code column (backport [#55718](https://github.com/frappe/erpnext/issues/55718)) ([#55722](https://github.com/frappe/erpnext/issues/55722)) ([09453f8](https://github.com/frappe/erpnext/commit/09453f883ba8ea1a355d044d3f416f8925d663df)) * resolve conflict ([271ddb6](https://github.com/frappe/erpnext/commit/271ddb6add5f062fdc33750c21e124ae275d7bc3)) * restrict already invoiced qty in intercompany purchase invoice ([#55754](https://github.com/frappe/erpnext/issues/55754)) ([a5c23a3](https://github.com/frappe/erpnext/commit/a5c23a3d16dedcb05bf05e924b8630e1dec44e41)) * **selling:** consider delivered qty (backport [#55597](https://github.com/frappe/erpnext/issues/55597)) ([#55606](https://github.com/frappe/erpnext/issues/55606)) ([e8267e3](https://github.com/frappe/erpnext/commit/e8267e32377e264e44e829fdf2e27b610f5fe28b)) * simplify New Zealand sales accounts ([eebb37f](https://github.com/frappe/erpnext/commit/eebb37f9fddf00b559f33de8d285627cd07cc9e3)) * sql injection ([a94e362](https://github.com/frappe/erpnext/commit/a94e362b8ce33144b69afb1fcfc94a261374a551)) * **stock:** add validation for work order seial nos and batch nos ([6d3f9d3](https://github.com/frappe/erpnext/commit/6d3f9d3c6f37930567deb02c20d7649499c18f2e)) * update items respects workflow "Only Allow Edit For" role (backport [#55667](https://github.com/frappe/erpnext/issues/55667)) ([#55705](https://github.com/frappe/erpnext/issues/55705)) ([7852ea6](https://github.com/frappe/erpnext/commit/7852ea65af4ec5208b90d63d60fb7f6be8e9206a)) * use new_doc with field allowlist in CRM integration endpoints ([45b232d](https://github.com/frappe/erpnext/commit/45b232d369eb1624383986d7047d5625c7e4f08f)) * validate fg and materials qty in the disassemble entry ([ba19a24](https://github.com/frappe/erpnext/commit/ba19a245266f280dceb7e9d550d4b7d76e27f26c)) * work order status should be in process if material transfer is skipped (backport [#55641](https://github.com/frappe/erpnext/issues/55641) to version-15-hotfix) ([#55643](https://github.com/frappe/erpnext/issues/55643)) ([55b0715](https://github.com/frappe/erpnext/commit/55b07153104482f1edabe9615821adc3ca95404a)) ### Features * add New Zealand chart of accounts ([f8a123e](https://github.com/frappe/erpnext/commit/f8a123e79dcecdaddb635a55f38af2caf2dfb2bf)) ### Performance Improvements * **transaction:** exit early before backend query (backport [#55556](https://github.com/frappe/erpnext/issues/55556)) ([#55557](https://github.com/frappe/erpnext/issues/55557)) ([ccbca57](https://github.com/frappe/erpnext/commit/ccbca57420b23207c8c0edf7b9e92f0a79f7588c)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 4be8b579505..5b488095239 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from frappe.utils.user import is_website_user -__version__ = "15.110.0" +__version__ = "15.111.0" def get_default_company(user=None): From e27016339f3c50eacbdc26842ce0f7d9f72cd8b5 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 16 Jun 2026 21:53:42 +0000 Subject: [PATCH 016/109] chore(release): Bumped to Version 15.112.0 # [15.112.0](https://github.com/frappe/erpnext/compare/v15.111.0...v15.112.0) (2026-06-16) ### Bug Fixes * **accounts:** removed whitelist on `get_balance_on` (backport [#55956](https://github.com/frappe/erpnext/issues/55956)) ([#55964](https://github.com/frappe/erpnext/issues/55964)) ([64bbc01](https://github.com/frappe/erpnext/commit/64bbc019aded28862892a0e94b16301464404971)) * Add authorization checks on internal functions (backport [#55709](https://github.com/frappe/erpnext/issues/55709)) (backport [#55726](https://github.com/frappe/erpnext/issues/55726)) ([#55736](https://github.com/frappe/erpnext/issues/55736)) ([8c7a313](https://github.com/frappe/erpnext/commit/8c7a313a38dfe38c9e35ca41e91389bcfaed2404)) * added doctype filter validation for sales person wise transaction summary report (backport [#55812](https://github.com/frappe/erpnext/issues/55812)) ([#55817](https://github.com/frappe/erpnext/issues/55817)) ([f1fc9e3](https://github.com/frappe/erpnext/commit/f1fc9e3261a154a9f97c4a3354573c78432e8f33)) * apply user permissions to receivable/payable reports ([c03a66a](https://github.com/frappe/erpnext/commit/c03a66a1bf48a53c42d01c9d936d9b22aa013e11)) * BOM Creator by removing redundant permissions ([08d9b82](https://github.com/frappe/erpnext/commit/08d9b8275daa1763dfc0bb05e56d4929a522b43c)) * **bom:** fetch routing operations when Routing is selected ([#55813](https://github.com/frappe/erpnext/issues/55813)) ([02e38e8](https://github.com/frappe/erpnext/commit/02e38e80a7fe964b03f470dc58e44ff202483f66)) * **budget:** add root_type filter on account field ([#55934](https://github.com/frappe/erpnext/issues/55934)) ([e1fbf78](https://github.com/frappe/erpnext/commit/e1fbf78409d788e34c3e2021eebb0bb0e760596a)) * clearance date update with permission checks ([46b3e0c](https://github.com/frappe/erpnext/commit/46b3e0c385658e1292f1c7c01bfd3fd356f923ad)) * **company:** replaced "this company" with company name on delete transactions dialog (backport [#56021](https://github.com/frappe/erpnext/issues/56021)) ([#56022](https://github.com/frappe/erpnext/issues/56022)) ([8a9461f](https://github.com/frappe/erpnext/commit/8a9461ff45f990a8f4740f3defb34a1ac7407a93)) * converted whitelist non class methods to class methods ([7c78aa6](https://github.com/frappe/erpnext/commit/7c78aa6e5d5ddbd4fd98bab4c741fc30ba556638)) * **get_exchange_rate:** using get_single_value to fetch `disabled` value from `currency_exchange_settings` ([9405b49](https://github.com/frappe/erpnext/commit/9405b49e93b9ce1c04bc3cca78b848325fe7a6dc)) * **Lead:** stop storing Gravatar image URLs for Leads (backport [#55880](https://github.com/frappe/erpnext/issues/55880)) ([#55881](https://github.com/frappe/erpnext/issues/55881)) ([8b3a0fe](https://github.com/frappe/erpnext/commit/8b3a0fe04571c9f48f74bf68fb3ff160269b5151)) * **manufacturing:** fetch raw materials from work order ([7e9c1ef](https://github.com/frappe/erpnext/commit/7e9c1efab72057cd1fad10ef2e027df99ef102e7)) * multiple issues related to BOM Creator ([f4e6f14](https://github.com/frappe/erpnext/commit/f4e6f143421ce92639668c70d2ac1d1aaa500d9e)) * opportunity creation from contact us page (backport [#55841](https://github.com/frappe/erpnext/issues/55841)) ([#55866](https://github.com/frappe/erpnext/issues/55866)) ([d460577](https://github.com/frappe/erpnext/commit/d4605771dae7d9171052cecf271bd3c716bfc93b)) * pass source cost center to target cost center ([209977f](https://github.com/frappe/erpnext/commit/209977f6a30551daa1dfaedbc12deae49c9551a3)) * pemission for whitelist functions ([559c95c](https://github.com/frappe/erpnext/commit/559c95c8a8cb2b41b71c1a86372175fb9fd665d9)) * permission in bom compare tool ([ef3046d](https://github.com/frappe/erpnext/commit/ef3046dca2f3571f5c76cf1e8c82bd7515ce5374)) * permissions in workstation file ([808ca06](https://github.com/frappe/erpnext/commit/808ca06801f0b20dffdb0a4ffc26fecadf2b8ff2)) * prefetch batchwise valuations before streaming SLEs in stock ageing ([c617650](https://github.com/frappe/erpnext/commit/c6176500d269cba563b3d00ae5e53ed8f0a5f375)) * prevent exchange rate flow from transaction to payment ([72c7b79](https://github.com/frappe/erpnext/commit/72c7b79933c2569fa11f329c822316345151fbc5)) * provision to recalculate valuation rate during reposting ([df0c8ee](https://github.com/frappe/erpnext/commit/df0c8ee21eccccbbafd724f8fcb48983e95b741c)) * recalculate incoming rate in SLE for purchase documents during repost ([081887b](https://github.com/frappe/erpnext/commit/081887bec5b7daead28377b9b8b0d7f338d6b2ab)) * regression issues related to security fixes (backport [#55902](https://github.com/frappe/erpnext/issues/55902)) ([#55925](https://github.com/frappe/erpnext/issues/55925)) ([1f47b24](https://github.com/frappe/erpnext/commit/1f47b2417bbaffa59b0d2ebcc84352e539cf682d)) * restricting currency_exchange_settings write permission only to system manager ([7b65206](https://github.com/frappe/erpnext/commit/7b6520664c90c510cb94e98af5172c6d19c77a1e)) * show company name in delete transactions confirmation dialog ([3356583](https://github.com/frappe/erpnext/commit/3356583865708b8ec1bbd595c958b9bef6ac6c79)) * show user disable audit log ([559585f](https://github.com/frappe/erpnext/commit/559585fb7b4f8651d0d799c4059e22f88edddeed)) * **stock:** enable quality inspection for all Stock Entry purposes ([1ffef19](https://github.com/frappe/erpnext/commit/1ffef19957b359619f5a29c6b674286b482c964d)), closes [#55830](https://github.com/frappe/erpnext/issues/55830) [#70945](https://github.com/frappe/erpnext/issues/70945) * **stock:** make uom mandatory in item uom table ([c3a3eb3](https://github.com/frappe/erpnext/commit/c3a3eb3df313f07c2df7c72688fb44f9ca6cc829)) * **stock:** show only batched items in batch item selector ([7f30a2c](https://github.com/frappe/erpnext/commit/7f30a2cfb6d85f0a513935d1db75752e04bc1792)) * **stock:** update stock value calculation in stock balance report ([18ad323](https://github.com/frappe/erpnext/commit/18ad3238286ecf540232f46e9c6ab917c6e74820)) * sync employee user status after save ([e40999c](https://github.com/frappe/erpnext/commit/e40999c8792c542951d6292df0a547cbd1555cb7)) * update system manager permissions ([81e05fc](https://github.com/frappe/erpnext/commit/81e05fc1f61ff8ba0e6168830082edb3c725f902)) * updated role based permission for terms and conditions doctype (backport [#55674](https://github.com/frappe/erpnext/issues/55674)) ([#55675](https://github.com/frappe/erpnext/issues/55675)) ([5f5b2a6](https://github.com/frappe/erpnext/commit/5f5b2a6ae2f5047e9dbfb3e316ccdcbfbc68d9dd)) * use frankfurter v2 by default for new install ([d62985d](https://github.com/frappe/erpnext/commit/d62985d9a76ca474b138a71b97cd9a98d342b7e1)) ### Features * **currency exchange settings:** frankfurter v2 support ([70628c0](https://github.com/frappe/erpnext/commit/70628c06c99d279b59acc5619ab0e77d4077349a)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 5b488095239..bfb1c251270 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from frappe.utils.user import is_website_user -__version__ = "15.111.0" +__version__ = "15.112.0" def get_default_company(user=None): From 144483765323f4197085bf183ff973a533d70269 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 18 Jun 2026 11:21:13 +0530 Subject: [PATCH 017/109] 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 (cherry picked from commit 98caefea8871053e7e3074faa41ecda5474f0f94) --- .../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 aebcc6dffb2..5a32a7bb641 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 2ed7c84d0af613faf42699b029804efd1e051005 Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Thu, 18 Jun 2026 15:42:28 +0530 Subject: [PATCH 018/109] fix(journal entry): validate opening entry against pcv on save (cherry picked from commit f8aa4c730c6b3eb4b02108251b8dab836aaefbef) # Conflicts: # erpnext/accounts/doctype/journal_entry/journal_entry.py # erpnext/accounts/general_ledger.py --- .../doctype/journal_entry/journal_entry.py | 4 ++++ erpnext/accounts/general_ledger.py | 17 ++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index aa048a71ff2..762585601e5 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -21,6 +21,7 @@ from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import ( get_party_tax_withholding_details, ) +from erpnext.accounts.general_ledger import validate_opening_entry_against_pcv from erpnext.accounts.party import get_party_account from erpnext.accounts.utils import ( cancel_exchange_gain_loss_journal, @@ -123,6 +124,9 @@ class JournalEntry(AccountsController): if not self.is_opening: self.is_opening = "No" + if self.is_opening == "Yes": + validate_opening_entry_against_pcv(self.company) + self.clearance_date = None self.validate_party() diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 599173c99f5..8d01a9e2843 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -802,13 +802,24 @@ def check_freezing_date(posting_date, adv_adj=False): ) -def validate_against_pcv(is_opening, posting_date, company): - if is_opening and frappe.db.exists("Period Closing Voucher", {"docstatus": 1, "company": company}): +def validate_opening_entry_against_pcv(company): + if frappe.db.exists("Period Closing Voucher", {"docstatus": 1, "company": company}): frappe.throw( - _("Opening Entry can not be created after Period Closing Voucher is created."), + _( + "A Period Closing Voucher is already submitted and an Opening Entry can no longer be created. {0} to learn more." + ).format( + '' + + _("Read the docs") + + "" + ), title=_("Invalid Opening Entry"), ) + +def validate_against_pcv(is_opening, posting_date, company): + if is_opening: + validate_opening_entry_against_pcv(company) + # Local import so you don't have to touch file-level imports from frappe.query_builder.functions import Max From 041a9adbbf7f211ad75d84e9c79c7b94b319815e Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Sat, 20 Jun 2026 19:25:58 +0530 Subject: [PATCH 019/109] 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 020/109] 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 021/109] 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 022/109] 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 023/109] 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 024/109] 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 025/109] 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 026/109] 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 027/109] 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 028/109] 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 029/109] 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 030/109] 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 031/109] 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 032/109] 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 033/109] 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 034/109] 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 035/109] 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 036/109] 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 037/109] 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 038/109] 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 039/109] 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 = () => { From c97be8abe1b6ca1115130776b52065d6eddf914c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 24 Jun 2026 12:57:31 +0530 Subject: [PATCH 040/109] feat(accounts): add configurable job timeout for Process Period Closing Voucher Adds a `pcv_job_timeout` Int field (default 3600s) to Accounts Settings so admins can tune the enqueue timeout for PCV background jobs without a code change. All three `frappe.enqueue` calls in `process_period_closing_voucher.py` now read this value at runtime. Co-Authored-By: Claude Sonnet 4.6 (cherry picked from commit 13b6c4a165db29d95d05cdcd01d79cdeaca0accc) # Conflicts: # erpnext/accounts/doctype/accounts_settings/accounts_settings.json --- .../accounts_settings/accounts_settings.json | 16 ++++++++++++++++ .../process_period_closing_voucher.py | 10 +++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 18a0a86fd52..bfde962fe62 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -79,6 +79,7 @@ "acc_frozen_upto", "ignore_account_closing_balance", "use_legacy_controller_for_pcv", + "pcv_job_timeout", "column_break_25", "frozen_accounts_modifier", "tab_break_dpet", @@ -635,6 +636,7 @@ "label": "Use Legacy Controller For Period Closing Voucher" }, { +<<<<<<< HEAD "fieldname": "payment_entry_settings", "fieldtype": "Section Break", "label": "Payment Entry Settings" @@ -650,6 +652,20 @@ "fieldname": "show_party_balance", "fieldtype": "Check", "label": "Show Party Balance" +======= + "default": "3600", + "description": "Timeout (in seconds) for each background job enqueued by Process Period Closing Voucher", + "fieldname": "pcv_job_timeout", + "fieldtype": "Int", + "label": "PCV Job Timeout (seconds)" + }, + { + "description": "Users with this role will be notified if the asset depreciation gets failed", + "fieldname": "role_to_notify_on_depreciation_failure", + "fieldtype": "Link", + "label": "Role to Notify on Depreciation Failure", + "options": "Role" +>>>>>>> 13b6c4a165 (feat(accounts): add configurable job timeout for Process Period Closing Voucher) }, { "default": "30, 60, 90, 120", diff --git a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py index e6a330a4d14..096b085cf0e 100644 --- a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py @@ -92,6 +92,8 @@ def start_pcv_processing(docname: str): frappe.has_permission("Process Payment Reconciliation", "write", doc=docname, throw=True) frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running") + timeout = frappe.db.get_single_value("Accounts Settings", "pcv_job_timeout") or 3600 + ppcvd = qb.DocType("Process Period Closing Voucher Detail") if normal_balances := ( qb.from_(ppcvd) @@ -118,7 +120,7 @@ def start_pcv_processing(docname: str): frappe.enqueue( method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date", queue="long", - timeout="3600", + timeout=timeout, is_async=True, enqueue_after_commit=True, docname=docname, @@ -244,6 +246,8 @@ def get_gle_for_closing_account(pcv, dimension_balance, dimensions): @frappe.whitelist() def schedule_next_date(docname: str): + timeout = frappe.db.get_single_value("Accounts Settings", "pcv_job_timeout") or 3600 + ppcvd = qb.DocType("Process Period Closing Voucher Detail") if to_process := ( qb.from_(ppcvd) @@ -269,7 +273,7 @@ def schedule_next_date(docname: str): frappe.enqueue( method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date", queue="long", - timeout="3600", + timeout=timeout, is_async=True, enqueue_after_commit=True, docname=docname, @@ -299,7 +303,7 @@ def schedule_next_date(docname: str): frappe.enqueue( method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.summarize_and_post_ledger_entries", queue="long", - timeout="3600", + timeout=timeout, is_async=True, job_name=job_name, enqueue_after_commit=True, From c33d7e5d7be12f103ccae69354ab75d309af3d2d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 24 Jun 2026 13:01:42 +0530 Subject: [PATCH 041/109] refactor: patch, display depends on and json changes (cherry picked from commit 3da7eefebb37bb1f1e6cc4c7c0c2add735e0e2c5) # Conflicts: # erpnext/accounts/doctype/accounts_settings/accounts_settings.json # erpnext/accounts/doctype/accounts_settings/accounts_settings.py # erpnext/patches.txt --- .../doctype/accounts_settings/accounts_settings.json | 5 +++++ .../doctype/accounts_settings/accounts_settings.py | 5 +++++ erpnext/patches.txt | 10 ++++++++++ 3 files changed, 20 insertions(+) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index bfde962fe62..6dd3c5fcc6e 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -654,6 +654,7 @@ "label": "Show Party Balance" ======= "default": "3600", + "depends_on": "eval: !doc.use_legacy_controller_for_pcv", "description": "Timeout (in seconds) for each background job enqueued by Process Period Closing Voucher", "fieldname": "pcv_job_timeout", "fieldtype": "Int", @@ -679,7 +680,11 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], +<<<<<<< HEAD "modified": "2026-05-18 12:16:33.679345", +======= + "modified": "2026-06-24 12:59:41.868865", +>>>>>>> 3da7eefebb (refactor: patch, display depends on and json changes) "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 5cd4955cdd7..69e3dde73af 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -59,7 +59,12 @@ class AccountsSettings(Document): make_payment_via_journal_entry: DF.Check merge_similar_account_heads: DF.Check over_billing_allowance: DF.Currency +<<<<<<< HEAD post_change_gl_entries: DF.Check +======= + pcv_job_timeout: DF.Int + preview_mode: DF.Check +>>>>>>> 3da7eefebb (refactor: patch, display depends on and json changes) receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"] receivable_payable_remarks_length: DF.Int reconciliation_queue_size: DF.Int diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 3b0ac5cf552..fb3279255e2 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -435,5 +435,15 @@ erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po 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 +<<<<<<< HEAD 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 +======= +erpnext.patches.v16_0.rename_secondary_item_type_field +erpnext.patches.v16_0.submit_existing_product_bundles #1 +erpnext.patches.v16_0.migrate_subscription_generate_invoice_at +erpnext.patches.v16_0.rename_subscription_billing_period_fields +erpnext.patches.v16_0.drop_redundant_serial_no_index_from_sabb +erpnext.patches.v16_0.set_default_close_opportunity_after_days +execute:frappe.db.set_single_value("Accounts Settings", "pcv_job_timeout", 3600) +>>>>>>> 3da7eefebb (refactor: patch, display depends on and json changes) From df3c821f98045f9ac038b1bafbd35113949db783 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 24 Jun 2026 15:05:41 +0530 Subject: [PATCH 042/109] chore: resolve conflicts --- .../accounts_settings/accounts_settings.json | 16 ++-------------- .../accounts_settings/accounts_settings.py | 4 ---- erpnext/patches.txt | 9 --------- 3 files changed, 2 insertions(+), 27 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 6dd3c5fcc6e..62b6d5c0b6a 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -636,7 +636,6 @@ "label": "Use Legacy Controller For Period Closing Voucher" }, { -<<<<<<< HEAD "fieldname": "payment_entry_settings", "fieldtype": "Section Break", "label": "Payment Entry Settings" @@ -652,7 +651,8 @@ "fieldname": "show_party_balance", "fieldtype": "Check", "label": "Show Party Balance" -======= + }, + { "default": "3600", "depends_on": "eval: !doc.use_legacy_controller_for_pcv", "description": "Timeout (in seconds) for each background job enqueued by Process Period Closing Voucher", @@ -660,14 +660,6 @@ "fieldtype": "Int", "label": "PCV Job Timeout (seconds)" }, - { - "description": "Users with this role will be notified if the asset depreciation gets failed", - "fieldname": "role_to_notify_on_depreciation_failure", - "fieldtype": "Link", - "label": "Role to Notify on Depreciation Failure", - "options": "Role" ->>>>>>> 13b6c4a165 (feat(accounts): add configurable job timeout for Process Period Closing Voucher) - }, { "default": "30, 60, 90, 120", "fieldname": "default_ageing_range", @@ -680,11 +672,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], -<<<<<<< HEAD - "modified": "2026-05-18 12:16:33.679345", -======= "modified": "2026-06-24 12:59:41.868865", ->>>>>>> 3da7eefebb (refactor: patch, display depends on and json changes) "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 69e3dde73af..83ece261895 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -59,12 +59,8 @@ class AccountsSettings(Document): make_payment_via_journal_entry: DF.Check merge_similar_account_heads: DF.Check over_billing_allowance: DF.Currency -<<<<<<< HEAD post_change_gl_entries: DF.Check -======= pcv_job_timeout: DF.Int - preview_mode: DF.Check ->>>>>>> 3da7eefebb (refactor: patch, display depends on and json changes) receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"] receivable_payable_remarks_length: DF.Int reconciliation_queue_size: DF.Int diff --git a/erpnext/patches.txt b/erpnext/patches.txt index fb3279255e2..b48f16a7550 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -435,15 +435,6 @@ erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po 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 -<<<<<<< HEAD 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 -======= -erpnext.patches.v16_0.rename_secondary_item_type_field -erpnext.patches.v16_0.submit_existing_product_bundles #1 -erpnext.patches.v16_0.migrate_subscription_generate_invoice_at -erpnext.patches.v16_0.rename_subscription_billing_period_fields -erpnext.patches.v16_0.drop_redundant_serial_no_index_from_sabb -erpnext.patches.v16_0.set_default_close_opportunity_after_days execute:frappe.db.set_single_value("Accounts Settings", "pcv_job_timeout", 3600) ->>>>>>> 3da7eefebb (refactor: patch, display depends on and json changes) From 25857125007861e121e505eef841f5b8e0233f90 Mon Sep 17 00:00:00 2001 From: Loic Oberle Date: Sat, 23 May 2026 07:59:09 +0200 Subject: [PATCH 043/109] =?UTF-8?q?refactor(sales=5Fperson=5Fwise=5Ftransa?= =?UTF-8?q?ction=5Fsummary):=20Replace=20SQL=20with=20que=E2=80=A6=20(#551?= =?UTF-8?q?91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit df3d0859a13b00d9aa9377dac48cfc365e557b20) --- .../sales_person_wise_transaction_summary.py | 94 ++++++++++--------- 1 file changed, 52 insertions(+), 42 deletions(-) diff --git a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py index 405159215cd..23ed83cca84 100644 --- a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py +++ b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py @@ -4,7 +4,7 @@ import frappe from frappe import _, msgprint, qb -from frappe.query_builder import Criterion +from frappe.query_builder import Case, Criterion from erpnext import get_company_currency @@ -155,50 +155,60 @@ def get_columns(filters): def get_entries(filters): - date_field = filters["doc_type"] == "Sales Order" and "transaction_date" or "posting_date" - if filters["doc_type"] == "Sales Order": - qty_field = "delivered_qty" - else: - qty_field = "qty" - conditions, values = get_conditions(filters, date_field) + doc_type = filters["doc_type"] - entries = frappe.db.sql( - """ - SELECT - dt.name, dt.customer, dt.territory, dt.{} as posting_date, dt_item.item_code, - st.sales_person, st.allocated_percentage, dt_item.warehouse, - CASE - WHEN dt.status = "Closed" THEN dt_item.{} * dt_item.conversion_factor - ELSE dt_item.stock_qty - END as stock_qty, - CASE - WHEN dt.status = "Closed" THEN (dt_item.base_net_rate * dt_item.{} * dt_item.conversion_factor) - ELSE dt_item.base_net_amount - END as base_net_amount, - CASE - WHEN dt.status = "Closed" THEN ((dt_item.base_net_rate * dt_item.{} * dt_item.conversion_factor) * st.allocated_percentage/100) - ELSE dt_item.base_net_amount * st.allocated_percentage/100 - END as contribution_amt - FROM - `tab{}` dt, `tab{} Item` dt_item, `tabSales Team` st - WHERE - st.parent = dt.name and dt.name = dt_item.parent and st.parenttype = {} - and dt.docstatus = 1 {} order by st.sales_person, dt.name desc - """.format( - date_field, - qty_field, - qty_field, - qty_field, - filters["doc_type"], - filters["doc_type"], - "%s", - conditions, - ), - tuple([filters["doc_type"], *values]), - as_dict=1, + date_field = "transaction_date" if doc_type == "Sales Order" else "posting_date" + qty_field = "delivered_qty" if doc_type == "Sales Order" else "qty" + + dt = frappe.qb.DocType(doc_type) + dt_item = frappe.qb.DocType(f"{doc_type} Item") + st = frappe.qb.DocType("Sales Team") + + calc_qty = dt_item[qty_field] * dt_item.conversion_factor + calc_net_amount = dt_item.base_net_rate * calc_qty + + stock_qty_case = Case().when(dt.status == "Closed", calc_qty).else_(dt_item.stock_qty).as_("stock_qty") + + base_net_amount_case = ( + Case() + .when(dt.status == "Closed", calc_net_amount) + .else_(dt_item.base_net_amount) + .as_("base_net_amount") ) - return entries + contribution_amt_case = ( + Case() + .when(dt.status == "Closed", (calc_net_amount * st.allocated_percentage / 100)) + .else_(dt_item.base_net_amount * st.allocated_percentage / 100) + .as_("contribution_amt") + ) + + query = ( + frappe.get_query(dt, filters=filters, ignore_permissions=False) + .join(dt_item) + .on(dt.name == dt_item.parent) + .join(st) + .on(dt.name == st.parent) + .select( + dt.name, + dt.customer, + dt.territory, + dt[date_field].as_("posting_date"), + dt_item.item_code, + st.sales_person, + st.allocated_percentage, + dt_item.warehouse, + stock_qty_case, + base_net_amount_case, + contribution_amt_case, + ) + .where(st.parenttype == doc_type) + .where(dt.docstatus == 1) + ) + + query = query.orderby(st.sales_person).orderby(dt.name, order=frappe.qb.desc) + + return query.run(as_dict=True) def get_conditions(filters, date_field): From db8a26a3afe971355df746c12d9377e853e6731a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 18:30:14 +0530 Subject: [PATCH 044/109] fix: precision issue causing COGS in inter transfer PR (backport #56420) (#56424) fix: precision issue causing COGS in inter transfer PR (#56420) (cherry picked from commit 9b0e1b61f202e33beaff8b8a146d3336001dc861) Co-authored-by: rohitwaghchaure --- erpnext/controllers/taxes_and_totals.py | 2 +- .../doctype/repost_item_valuation/repost_item_valuation.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 1fc8128d6ea..e218e9a44cb 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -185,7 +185,7 @@ class calculate_taxes_and_totals: return if not self.discount_amount_applied: - do_not_round_fields = ["valuation_rate", "incoming_rate"] + do_not_round_fields = ["valuation_rate", "incoming_rate", "sales_incoming_rate"] for item in self.doc.items: self.doc.round_floats_in(item, do_not_round_fields=do_not_round_fields) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index ef42da67668..98a2720e6d2 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -306,6 +306,9 @@ class RepostItemValuation(Document): def _recalculate_valuation_rate(self): doc = frappe.get_doc(self.voucher_type, self.voucher_no) + if doc.get("is_internal_supplier"): + doc.set_sales_incoming_rate_for_internal_transfer() + doc.update_valuation_rate() for item in doc.items: item.db_set("valuation_rate", item.valuation_rate) From 414b3665c1cc444e66eb3a3cbb09682d4bf04f78 Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Thu, 25 Jun 2026 00:49:27 +0530 Subject: [PATCH 045/109] fix(company): using `String.prototype.bold` method instead of `frappe.utils.bold` on company deletion prompt (#56441) --- erpnext/setup/doctype/company/company.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 33a7ecc8714..799686fbbb1 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -208,7 +208,7 @@ frappe.ui.form.on("Company", { reqd: 1, description: __( "Please make sure you really want to delete all the transactions for {0}. Your master data will remain as it is. This action cannot be undone.", - [frappe.utils.bold(frm.doc.name)] + [frappe.utils.escape_html(frm.doc.name).bold()] ), }, function (data) { @@ -228,7 +228,9 @@ frappe.ui.form.on("Company", { }, }); }, - __("Delete all the Transactions for {0}", [frappe.utils.bold(frm.doc.name)]), + __("Delete all the Transactions for {0}", [ + frappe.utils.escape_html(frm.doc.name).bold(), + ]), __("Delete") ); d.get_primary_btn().addClass("btn-danger"); From b176fb292b8d0ef3a9f14ec0007578881a1f45ff Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Thu, 25 Jun 2026 14:55:47 +0530 Subject: [PATCH 046/109] fix: remove frappe.utils from jinja context in process statement of accounts (cherry picked from commit 37ec2d0edd439a6dd49c4506d889b0977dcfb183) --- .../process_statement_of_accounts.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index bf130db8e22..48349a4bd99 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -390,7 +390,6 @@ def get_context(customer, doc): return { "doc": template_doc, "customer": frappe.get_doc("Customer", customer), - "frappe": frappe.utils, } From 28e4bca4f2ee9394dc8b4a7266b641d0ad58cec9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 10:41:41 +0000 Subject: [PATCH 047/109] fix(crm): using `get_list` instead of `get_all` in `get_opportunities` (backport #56463) (#56465) Co-authored-by: Diptanil Saha fix(crm): using `get_list` instead of `get_all` in `get_opportunities` (#56463) --- erpnext/crm/doctype/prospect/prospect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/prospect/prospect.py b/erpnext/crm/doctype/prospect/prospect.py index 7ecbe637f04..02bd2a33be4 100644 --- a/erpnext/crm/doctype/prospect/prospect.py +++ b/erpnext/crm/doctype/prospect/prospect.py @@ -136,7 +136,7 @@ def make_opportunity(source_name, target_doc=None): @frappe.whitelist() def get_opportunities(prospect): - return frappe.get_all( + return frappe.get_list( "Opportunity", filters={"opportunity_from": "Prospect", "party_name": prospect}, fields=[ From 9adfab19dc8c67a682e6df79be867c9a4ca01ea0 Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Thu, 25 Jun 2026 17:32:46 +0530 Subject: [PATCH 048/109] fix: handle missing serial and batch bundle in print format (cherry picked from commit 548d90df4f58cd0a7a885cdd3adfd85af2e81de7) --- erpnext/stock/serial_batch_bundle.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 1b030dbe2fa..d0cf769100f 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -588,10 +588,16 @@ def get_serial_nos_from_bundle(serial_and_batch_bundle, serial_nos=None): def get_serial_or_batch_nos(bundle): # For print format + if not bundle: + return "" + bundle_data = frappe.get_cached_value( "Serial and Batch Bundle", bundle, ["has_serial_no", "has_batch_no"], as_dict=True ) + if not bundle_data: + return bundle + fields = [] if bundle_data.has_serial_no: fields.append("serial_no") From 3365373ec9e2c030cf72e76f77d7841993958721 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 26 Jun 2026 10:08:10 +0530 Subject: [PATCH 049/109] Revert "fix: handle missing serial and batch bundle in print format (backport #56497)" --- erpnext/stock/serial_batch_bundle.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index d0cf769100f..1b030dbe2fa 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -588,16 +588,10 @@ def get_serial_nos_from_bundle(serial_and_batch_bundle, serial_nos=None): def get_serial_or_batch_nos(bundle): # For print format - if not bundle: - return "" - bundle_data = frappe.get_cached_value( "Serial and Batch Bundle", bundle, ["has_serial_no", "has_batch_no"], as_dict=True ) - if not bundle_data: - return bundle - fields = [] if bundle_data.has_serial_no: fields.append("serial_no") From 9e6edea818ad3a82fdf7cf66c562cfd6f32dfd3b Mon Sep 17 00:00:00 2001 From: Mohd Haris Date: Fri, 19 Jun 2026 10:51:36 +0530 Subject: [PATCH 050/109] fix: allow rename for Quality Inspection Parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Quality Inspection Parameter DocType did not have `allow_rename` enabled, so the "Rename" action was hidden from the form's menu (the 3-dots / ⋮ options). Since the DocType is auto-named from the `parameter` field (`autoname: field:parameter`), users had no way to correct or change a parameter's name once created. Enable `allow_rename` so users can rename a Quality Inspection Parameter from the form menu. Co-Authored-By: Claude Opus 4.8 (cherry picked from commit adfef48a65cbbe8019e97ac850d873f843d9f923) # Conflicts: # erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json --- .../quality_inspection_parameter.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json index 418b4825f2f..8915a580777 100644 --- a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json +++ b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_rename": 1, "autoname": "field:parameter", "creation": "2020-12-28 17:06:00.254129", "doctype": "DocType", @@ -34,7 +35,11 @@ ], "index_web_pages_for_search": 1, "links": [], +<<<<<<< HEAD "modified": "2021-02-19 20:33:30.657406", +======= + "modified": "2026-06-19 10:55:00.000000", +>>>>>>> adfef48a65 (fix: allow rename for Quality Inspection Parameter) "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection Parameter", From f2b7319462db6d6a137db0e16255fa1502ed538e Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 26 Jun 2026 13:46:30 +0530 Subject: [PATCH 051/109] chore: resolve conflicts --- .../quality_inspection_parameter.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json index 8915a580777..f95c4a69482 100644 --- a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json +++ b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json @@ -35,11 +35,7 @@ ], "index_web_pages_for_search": 1, "links": [], -<<<<<<< HEAD - "modified": "2021-02-19 20:33:30.657406", -======= "modified": "2026-06-19 10:55:00.000000", ->>>>>>> adfef48a65 (fix: allow rename for Quality Inspection Parameter) "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection Parameter", @@ -98,4 +94,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} From 0f9429fd3dca54021f184c400b6397d92f830724 Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Fri, 26 Jun 2026 15:00:54 +0530 Subject: [PATCH 052/109] fix: remove dead bundle helper call from purchase receipt print format (cherry picked from commit b9f5a77fa7ae7e151a667a38fc5569b4e2cecedd) --- .../purchase_receipt_serial_and_batch_bundle_print.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/print_format/purchase_receipt_serial_and_batch_bundle_print/purchase_receipt_serial_and_batch_bundle_print.json b/erpnext/stock/print_format/purchase_receipt_serial_and_batch_bundle_print/purchase_receipt_serial_and_batch_bundle_print.json index a8ab8f6ac7d..9b5a71aae02 100644 --- a/erpnext/stock/print_format/purchase_receipt_serial_and_batch_bundle_print/purchase_receipt_serial_and_batch_bundle_print.json +++ b/erpnext/stock/print_format/purchase_receipt_serial_and_batch_bundle_print/purchase_receipt_serial_and_batch_bundle_print.json @@ -8,7 +8,7 @@ "docstatus": 0, "doctype": "Print Format", "font_size": 14, - "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"
\\t\\t\\t\\t

Purchase Receipt

{{ doc.name }}\\t\\t\\t\\t

\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"supplier_name\", \"print_hide\": 0, \"label\": \"Supplier Name\"}, {\"fieldname\": \"supplier_delivery_note\", \"print_hide\": 0, \"label\": \"Supplier Delivery Note\"}, {\"fieldname\": \"rack\", \"print_hide\": 0, \"label\": \"Rack\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"posting_date\", \"print_hide\": 0, \"label\": \"Date\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"apply_putaway_rule\", \"print_hide\": 0, \"label\": \"Apply Putaway Rule\"}, {\"fieldtype\": \"Section Break\", \"label\": \"Accounting Dimensions\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"region\", \"print_hide\": 0, \"label\": \"Region\"}, {\"fieldname\": \"function\", \"print_hide\": 0, \"label\": \"Function\"}, {\"fieldname\": \"depot\", \"print_hide\": 0, \"label\": \"Depot\"}, {\"fieldname\": \"cost_center\", \"print_hide\": 0, \"label\": \"Cost Center\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"location\", \"print_hide\": 0, \"label\": \"Location\"}, {\"fieldname\": \"country\", \"print_hide\": 0, \"label\": \"Country\"}, {\"fieldname\": \"project\", \"print_hide\": 0, \"label\": \"Project\"}, {\"fieldtype\": \"Section Break\", \"label\": \"Items\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"scan_barcode\", \"print_hide\": 0, \"label\": \"Scan Barcode\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"set_from_warehouse\", \"print_hide\": 0, \"label\": \"Set From Warehouse\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"\\n\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t{%- for row in doc.items -%}\\n\\t\\t\\n\\t\\t {% set bundle_data = get_serial_or_batch_nos(row.serial_and_batch_bundle) %}\\n\\t\\t {% set serial_nos = [] %}\\n {% set batches = {} %}\\n\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t{%- endfor -%}\\n\\t\\n
SrItem NameDescriptionQtyRateAmount
{{ row.idx }}\\n\\t\\t\\t\\t{{ row.item_name }}\\n\\t\\t\\t\\t{% if row.item_code != row.item_name -%}\\n\\t\\t\\t\\t
Item Code: {{ row.item_code}}\\n\\t\\t\\t\\t{%- endif %}\\n\\t\\t\\t
\\n\\t\\t\\t\\t
{{ row.description }}
{{ row.qty }} {{ row.uom or row.stock_uom }}{{\\n\\t\\t\\t\\trow.get_formatted(\\\"rate\\\", doc) }}{{\\n\\t\\t\\t\\trow.get_formatted(\\\"amount\\\", doc) }}
\\n\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"total_qty\", \"print_hide\": 0, \"label\": \"Total Quantity\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"total\", \"print_hide\": 0, \"label\": \"Total\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"taxes\", \"print_hide\": 0, \"label\": \"Purchase Taxes and Charges\", \"visible_columns\": [{\"fieldname\": \"category\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"add_deduct_tax\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"charge_type\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"row_id\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"included_in_print_rate\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"included_in_paid_amount\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"account_head\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"description\", \"print_width\": \"300px\", \"print_hide\": 0}, {\"fieldname\": \"rate\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"region\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"function\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"location\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"cost_center\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"depot\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"country\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"account_currency\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"tax_amount\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"total\", \"print_width\": \"\", \"print_hide\": 0}]}, {\"fieldtype\": \"Section Break\", \"label\": \"Totals\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"grand_total\", \"print_hide\": 0, \"label\": \"Grand Total\"}, {\"fieldname\": \"rounded_total\", \"print_hide\": 0, \"label\": \"Rounded Total\"}, {\"fieldname\": \"in_words\", \"print_hide\": 0, \"label\": \"In Words\"}, {\"fieldname\": \"disable_rounded_total\", \"print_hide\": 0, \"label\": \"Disable Rounded Total\"}, {\"fieldtype\": \"Section Break\", \"label\": \"Supplier Address\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"address_display\", \"print_hide\": 0, \"label\": \"Address\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"contact_display\", \"print_hide\": 0, \"label\": \"Contact\"}, {\"fieldname\": \"contact_mobile\", \"print_hide\": 0, \"label\": \"Mobile No\"}, {\"fieldtype\": \"Section Break\", \"label\": \"Company Billing Address\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"billing_address\", \"print_hide\": 0, \"label\": \"Billing Address\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"billing_address_display\", \"print_hide\": 0, \"label\": \"Billing Address\"}, {\"fieldname\": \"terms\", \"print_hide\": 0, \"label\": \"Terms and Conditions\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"\\n\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t{%- for row in doc.items -%}\\n\\t\\t\\n\\t\\t {% set bundle_data = frappe.get_all(\\\"Serial and Batch Entry\\\", \\n\\t\\t fields=[\\\"serial_no\\\", \\\"batch_no\\\", \\\"qty\\\"], \\n\\t\\t filters={\\\"parent\\\": row.serial_and_batch_bundle}) %}\\n\\t\\t {% set serial_nos = [] %}\\n {% set batches = {} %}\\n \\n {% if bundle_data %}\\n\\t\\t\\t {% for data in bundle_data %}\\n\\t\\t\\t {% if data.serial_no %}\\n\\t\\t\\t {{ serial_nos.append(data.serial_no) or \\\"\\\" }}\\n\\t\\t\\t {% endif %}\\n\\t\\t\\t \\n\\t\\t\\t {% if data.batch_no %}\\n\\t\\t\\t {{ batches.update({data.batch_no: data.qty}) or \\\"\\\" }}\\n\\t\\t\\t {% endif %}\\n\\t\\t\\t {% endfor %}\\n\\t\\t\\t{% endif %}\\n\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t{%- endfor -%}\\n\\t\\n
SrItem NameQtySerial NosBatch Nos (Qty)
{{ row.idx }}\\n\\t\\t\\t\\t{{ row.item_name }}\\n\\t\\t\\t\\t{% if row.item_code != row.item_name -%}\\n\\t\\t\\t\\t
Item Code: {{ row.item_code}}\\n\\t\\t\\t\\t{%- endif %}\\n\\t\\t\\t
{{ row.qty }} {{ row.uom or row.stock_uom }}{{ serial_nos|join(',') }}\\n\\t\\t\\t {% if batches %}\\n {% for batch_no, qty in batches.items() %}\\n

{{batch_no}} : {{qty}} {{ row.uom or row.stock_uom }}

\\n {% endfor %}\\n {% endif %}\\n\\t\\t\\t
\\n\"}]", + "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"
\\t\\t\\t\\t

Purchase Receipt

{{ doc.name }}\\t\\t\\t\\t

\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"supplier_name\", \"print_hide\": 0, \"label\": \"Supplier Name\"}, {\"fieldname\": \"supplier_delivery_note\", \"print_hide\": 0, \"label\": \"Supplier Delivery Note\"}, {\"fieldname\": \"rack\", \"print_hide\": 0, \"label\": \"Rack\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"posting_date\", \"print_hide\": 0, \"label\": \"Date\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"apply_putaway_rule\", \"print_hide\": 0, \"label\": \"Apply Putaway Rule\"}, {\"fieldtype\": \"Section Break\", \"label\": \"Accounting Dimensions\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"region\", \"print_hide\": 0, \"label\": \"Region\"}, {\"fieldname\": \"function\", \"print_hide\": 0, \"label\": \"Function\"}, {\"fieldname\": \"depot\", \"print_hide\": 0, \"label\": \"Depot\"}, {\"fieldname\": \"cost_center\", \"print_hide\": 0, \"label\": \"Cost Center\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"location\", \"print_hide\": 0, \"label\": \"Location\"}, {\"fieldname\": \"country\", \"print_hide\": 0, \"label\": \"Country\"}, {\"fieldname\": \"project\", \"print_hide\": 0, \"label\": \"Project\"}, {\"fieldtype\": \"Section Break\", \"label\": \"Items\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"scan_barcode\", \"print_hide\": 0, \"label\": \"Scan Barcode\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"set_from_warehouse\", \"print_hide\": 0, \"label\": \"Set From Warehouse\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"\\n\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t{%- for row in doc.items -%}\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t{%- endfor -%}\\n\\t\\n
SrItem NameDescriptionQtyRateAmount
{{ row.idx }}\\n\\t\\t\\t\\t{{ row.item_name }}\\n\\t\\t\\t\\t{% if row.item_code != row.item_name -%}\\n\\t\\t\\t\\t
Item Code: {{ row.item_code}}\\n\\t\\t\\t\\t{%- endif %}\\n\\t\\t\\t
\\n\\t\\t\\t\\t
{{ row.description }}
{{ row.qty }} {{ row.uom or row.stock_uom }}{{\\n\\t\\t\\t\\trow.get_formatted(\\\"rate\\\", doc) }}{{\\n\\t\\t\\t\\trow.get_formatted(\\\"amount\\\", doc) }}
\\n\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"total_qty\", \"print_hide\": 0, \"label\": \"Total Quantity\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"total\", \"print_hide\": 0, \"label\": \"Total\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"taxes\", \"print_hide\": 0, \"label\": \"Purchase Taxes and Charges\", \"visible_columns\": [{\"fieldname\": \"category\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"add_deduct_tax\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"charge_type\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"row_id\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"included_in_print_rate\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"included_in_paid_amount\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"account_head\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"description\", \"print_width\": \"300px\", \"print_hide\": 0}, {\"fieldname\": \"rate\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"region\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"function\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"location\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"cost_center\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"depot\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"country\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"account_currency\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"tax_amount\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"total\", \"print_width\": \"\", \"print_hide\": 0}]}, {\"fieldtype\": \"Section Break\", \"label\": \"Totals\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"grand_total\", \"print_hide\": 0, \"label\": \"Grand Total\"}, {\"fieldname\": \"rounded_total\", \"print_hide\": 0, \"label\": \"Rounded Total\"}, {\"fieldname\": \"in_words\", \"print_hide\": 0, \"label\": \"In Words\"}, {\"fieldname\": \"disable_rounded_total\", \"print_hide\": 0, \"label\": \"Disable Rounded Total\"}, {\"fieldtype\": \"Section Break\", \"label\": \"Supplier Address\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"address_display\", \"print_hide\": 0, \"label\": \"Address\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"contact_display\", \"print_hide\": 0, \"label\": \"Contact\"}, {\"fieldname\": \"contact_mobile\", \"print_hide\": 0, \"label\": \"Mobile No\"}, {\"fieldtype\": \"Section Break\", \"label\": \"Company Billing Address\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"billing_address\", \"print_hide\": 0, \"label\": \"Billing Address\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"billing_address_display\", \"print_hide\": 0, \"label\": \"Billing Address\"}, {\"fieldname\": \"terms\", \"print_hide\": 0, \"label\": \"Terms and Conditions\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"\\n\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t{%- for row in doc.items -%}\\n\\t\\t\\n\\t\\t {% set bundle_data = frappe.get_all(\\\"Serial and Batch Entry\\\", \\n\\t\\t fields=[\\\"serial_no\\\", \\\"batch_no\\\", \\\"qty\\\"], \\n\\t\\t filters={\\\"parent\\\": row.serial_and_batch_bundle}) %}\\n\\t\\t {% set serial_nos = [] %}\\n {% set batches = {} %}\\n \\n {% if bundle_data %}\\n\\t\\t\\t {% for data in bundle_data %}\\n\\t\\t\\t {% if data.serial_no %}\\n\\t\\t\\t {{ serial_nos.append(data.serial_no) or \\\"\\\" }}\\n\\t\\t\\t {% endif %}\\n\\t\\t\\t \\n\\t\\t\\t {% if data.batch_no %}\\n\\t\\t\\t {{ batches.update({data.batch_no: data.qty}) or \\\"\\\" }}\\n\\t\\t\\t {% endif %}\\n\\t\\t\\t {% endfor %}\\n\\t\\t\\t{% endif %}\\n\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t{%- endfor -%}\\n\\t\\n
SrItem NameQtySerial NosBatch Nos (Qty)
{{ row.idx }}\\n\\t\\t\\t\\t{{ row.item_name }}\\n\\t\\t\\t\\t{% if row.item_code != row.item_name -%}\\n\\t\\t\\t\\t
Item Code: {{ row.item_code}}\\n\\t\\t\\t\\t{%- endif %}\\n\\t\\t\\t
{{ row.qty }} {{ row.uom or row.stock_uom }}{{ serial_nos|join(',') }}\\n\\t\\t\\t {% if batches %}\\n {% for batch_no, qty in batches.items() %}\\n

{{batch_no}} : {{qty}} {{ row.uom or row.stock_uom }}

\\n {% endfor %}\\n {% endif %}\\n\\t\\t\\t
\\n\"}]", "idx": 0, "line_breaks": 0, "margin_bottom": 15.0, @@ -27,4 +27,4 @@ "raw_printing": 0, "show_section_headings": 0, "standard": "Yes" -} \ No newline at end of file +} From 4aedf98c7ccff4ead45947219e2e5825f5432e8f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 17:06:34 +0530 Subject: [PATCH 053/109] ci: bump pre-commit actions to v3.0.1 (backport #56562) (#56566) Co-authored-by: Diptanil Saha --- .github/workflows/linters.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 94b76b12ce7..f55d6bf844e 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -18,7 +18,7 @@ jobs: cache: pip - name: Install and Run Pre-commit - uses: pre-commit/action@v3.0.0 + uses: pre-commit/action@v3.0.1 - name: Download Semgrep rules run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules From 5ed946b3b96c888dc8d66b5595b6b75523abe279 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 19:42:28 +0530 Subject: [PATCH 054/109] fix: ignored posting time 00:00:00 in RIV (backport #56571) (#56572) fix: ignored posting time 00:00:00 in RIV (#56571) (cherry picked from commit 31f89b72b48110a2245791b85994b491c6fda2e5) Co-authored-by: rohitwaghchaure --- .../doctype/repost_item_valuation/repost_item_valuation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 98a2720e6d2..f7cc4b90c36 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -86,7 +86,7 @@ class RepostItemValuation(Document): self.validate_recreate_stock_ledgers() def set_default_posting_time(self): - if not self.posting_time: + if self.posting_time is None: self.posting_time = nowtime() if not self.posting_date: From af05b8a30dba71ca964f90167db24015bd1ca6d5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 02:31:22 +0530 Subject: [PATCH 055/109] chore: removing `controllers` from pre-commit eslint hooks exclude list (backport #56575) (#56578) Co-authored-by: Diptanil Saha --- .pre-commit-config.yaml | 1 - erpnext/public/js/controllers/accounts.js | 14 ++++++----- .../public/js/controllers/taxes_and_totals.js | 12 +++++---- erpnext/public/js/controllers/transaction.js | 25 +++++++++---------- 4 files changed, 27 insertions(+), 25 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 13cbf66a5af..c09e5cdedb9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,7 +50,6 @@ repos: cypress/.*| .*node_modules.*| .*boilerplate.*| - erpnext/public/js/controllers/.*| erpnext/templates/pages/order.js| erpnext/templates/includes/.* )$ diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index dec0f1c024d..1b1c45e38ea 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -16,13 +16,15 @@ erpnext.accounts.taxes = { } }); }, - onload: function(frm) { - if(frm.get_field("taxes")) { - frm.set_query("account_head", "taxes", function(doc) { - if(frm.cscript.tax_table == "Sales Taxes and Charges") { - var account_type = ["Tax", "Chargeable", "Expense Account"]; + onload: function (frm) { + if (frm.get_field("taxes")) { + frm.set_query("account_head", "taxes", function (doc) { + let account_type = ["Tax", "Chargeable"]; + + if (frm.cscript.tax_table == "Sales Taxes and Charges") { + account_type.push("Expense Account"); } else { - var account_type = ["Tax", "Chargeable", "Income Account", "Expenses Included In Valuation"]; + account_type.push("Income Account", "Expenses Included In Valuation"); } return { diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 0df4cabfb4f..1f091f3934d 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -498,7 +498,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } else if(tax.charge_type == "On Net Total") { if (tax.account_head in item_tax_map) { current_net_amount = item.net_amount - }; + } current_tax_amount = (tax_rate / 100.0) * item.net_amount; } else if(tax.charge_type == "On Previous Row Amount") { current_net_amount = this.frm.doc["taxes"][cint(tax.row_id) - 1].tax_amount_for_current_item @@ -862,12 +862,13 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { if(["Sales Invoice", "POS Invoice", "Purchase Invoice"].includes(this.frm.doc.doctype)) { let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total; let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total; + let total_amount_to_pay; if(this.frm.doc.party_account_currency == this.frm.doc.currency) { - var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance + total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance - this.frm.doc.write_off_amount), precision("grand_total")); } else { - var total_amount_to_pay = flt( + total_amount_to_pay = flt( (flt(base_grand_total, precision("base_grand_total")) - this.frm.doc.total_advance - this.frm.doc.base_write_off_amount), precision("base_grand_total") @@ -901,14 +902,15 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { async set_total_amount_to_default_mop() { let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total; let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total; + let total_amount_to_pay; if (this.frm.doc.party_account_currency == this.frm.doc.currency) { - var total_amount_to_pay = flt( + total_amount_to_pay = flt( grand_total - this.frm.doc.total_advance - this.frm.doc.write_off_amount, precision("grand_total") ); } else { - var total_amount_to_pay = flt( + total_amount_to_pay = flt( ( flt( base_grand_total, diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 5fce7783c38..88dc01d5845 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1007,13 +1007,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe var set_party_account = function(set_pricing) { if (["Sales Invoice", "Purchase Invoice"].includes(me.frm.doc.doctype)) { - if(me.frm.doc.doctype=="Sales Invoice") { - var party_type = "Customer"; - var party_account_field = 'debit_to'; - } else { - var party_type = "Supplier"; - var party_account_field = 'credit_to'; - } + let party_type = me.frm.doc.doctype == "Sales Invoice" ? "Customer" : "Supplier"; + let party_account_field = me.frm.doc.doctype == "Sales Invoice" ? "debit_to" : "credit_to"; var party = me.frm.doc[frappe.model.scrub(party_type)]; if(party && me.frm.doc.company && (!me.frm.doc.__onload?.load_after_mapping || !me.frm.doc[party_account_field])) { @@ -1427,7 +1422,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe let first_row = this.frm.doc.items[0]; if (!first_row) { return false - }; + } let mapped_rows = mappped_fields.filter(d => first_row[d]) @@ -1599,7 +1594,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe this.frm.set_currency_labels(["operating_cost", "hour_rate"], this.frm.doc.currency, "operations"); this.frm.set_currency_labels(["base_operating_cost", "base_hour_rate"], company_currency, "operations"); - var item_grid = this.frm.fields_dict["operations"].grid; + let item_grid = this.frm.fields_dict["operations"].grid; $.each(["base_operating_cost", "base_hour_rate"], function(i, fname) { if(frappe.meta.get_docfield(item_grid.doctype, fname)) item_grid.set_column_disp(fname, me.frm.doc.currency != company_currency); @@ -1610,7 +1605,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe this.frm.set_currency_labels(["rate", "amount"], this.frm.doc.currency, "scrap_items"); this.frm.set_currency_labels(["base_rate", "base_amount"], company_currency, "scrap_items"); - var item_grid = this.frm.fields_dict["scrap_items"].grid; + let item_grid = this.frm.fields_dict["scrap_items"].grid; $.each(["base_rate", "base_amount"], function(i, fname) { if(frappe.meta.get_docfield(item_grid.doctype, fname)) item_grid.set_column_disp(fname, me.frm.doc.currency != company_currency); @@ -2005,7 +2000,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe row_to_modify[key] = pr_row[key]; } - if (this.frm.doc.hasOwnProperty("is_pos") && this.frm.doc.is_pos) { + if (Object.prototype.hasOwnProperty.call(this.frm.doc, "is_pos") && this.frm.doc.is_pos) { let r = await frappe.db.get_value("POS Profile", this.frm.doc.pos_profile, "cost_center"); if (r.message.cost_center) { row_to_modify["cost_center"] = r.message.cost_center; @@ -2237,8 +2232,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }, callback: function(r) { if (!r.exc) { - $.each(me.frm.doc.items || [], function(i, item) { - if (item.name && r.message.hasOwnProperty(item.name) && r.message[item.name].item_tax_template) { + $.each(me.frm.doc.items || [], function (i, item) { + if ( + item.name && + Object.prototype.hasOwnProperty.call(r.message, item.name) && + r.message[item.name].item_tax_template + ) { item.item_tax_template = r.message[item.name].item_tax_template; item.item_tax_rate = r.message[item.name].item_tax_rate; me.add_taxes_from_item_tax_template(item.item_tax_rate); From 257b4225ecfe531cc88c5b4a0c41c17d9045f482 Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Sat, 27 Jun 2026 02:34:24 +0530 Subject: [PATCH 056/109] ci: separate job for semgrep (#56580) --- .github/workflows/linters.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index f55d6bf844e..3b9d953db1c 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -20,6 +20,18 @@ jobs: - name: Install and Run Pre-commit uses: pre-commit/action@v3.0.1 + semgrep: + name: semgrep + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + cache: pip + - name: Download Semgrep rules run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules From afddf709053183030a3e26bd27e00531b3f0939e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 02:37:22 +0530 Subject: [PATCH 057/109] fix(lead): added missing read permission check on `get_lead_details` (backport #56272) (#56273) Co-authored-by: Diptanil Saha fix(lead): added missing read permission check on `get_lead_details` (#56272) --- erpnext/crm/doctype/lead/lead.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 94e99a612e8..42b82395719 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -438,6 +438,7 @@ def get_lead_details(lead, posting_date=None, company=None, doctype=None): out = frappe._dict() lead_doc = frappe.get_doc("Lead", lead) + lead_doc.check_permission() lead = lead_doc out.update( From 54fcd09995469f89e114402eebbbe95455f0468b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 12:04:38 +0530 Subject: [PATCH 058/109] fix: do not allow closing the accounting period for future dates (backport #56551) (#56576) fix: do not allow closing the accounting period for future dates (#56551) (cherry picked from commit 5e60e4faa7a7a2e5238746275995fc1bb7625a48) Co-authored-by: rohitwaghchaure --- .../doctype/accounting_period/accounting_period.py | 13 +++++++++++++ .../accounting_period/test_accounting_period.py | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_period/accounting_period.py b/erpnext/accounts/doctype/accounting_period/accounting_period.py index 300d216618e..426a4d57064 100644 --- a/erpnext/accounts/doctype/accounting_period/accounting_period.py +++ b/erpnext/accounts/doctype/accounting_period/accounting_period.py @@ -5,6 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document +from frappe.utils import getdate, nowdate class OverlapError(frappe.ValidationError): @@ -34,8 +35,20 @@ class AccountingPeriod(Document): # end: auto-generated types def validate(self): + self.validate_dates() self.validate_overlap() + def validate_dates(self): + if getdate(self.start_date) > getdate(self.end_date): + frappe.throw(_("Start Date cannot be after End Date")) + + if getdate(self.end_date) > getdate(nowdate()): + frappe.throw( + _( + "Accounting Period cannot be created for a future date. End Date {0} is after today." + ).format(frappe.bold(frappe.format(self.end_date, "Date"))) + ) + def before_insert(self): self.bootstrap_doctypes_for_closing() diff --git a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py index 16cae9683f9..671a28e3956 100644 --- a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py +++ b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py @@ -4,7 +4,7 @@ import unittest import frappe -from frappe.utils import add_months, nowdate +from frappe.utils import nowdate from erpnext.accounts.doctype.accounting_period.accounting_period import ( ClosedAccountingPeriod, @@ -47,7 +47,7 @@ def create_accounting_period(**args): accounting_period = frappe.new_doc("Accounting Period") accounting_period.start_date = args.start_date or nowdate() - accounting_period.end_date = args.end_date or add_months(nowdate(), 1) + accounting_period.end_date = args.end_date or nowdate() accounting_period.company = args.company or "_Test Company" accounting_period.period_name = args.period_name or "_Test_Period_Name_1" accounting_period.append("closed_documents", {"document_type": "Sales Invoice", "closed": 1}) From d992f6f5c8ca5676d7fc9365729a195fa50d22ac Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:13:07 +0000 Subject: [PATCH 059/109] chore(crm_settings): remove unused `delete_custom_fields` import (backport #56558) (#56582) Co-authored-by: Diptanil Saha --- erpnext/crm/doctype/crm_settings/crm_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.py b/erpnext/crm/doctype/crm_settings/crm_settings.py index 04e5a402add..7ca341adb77 100644 --- a/erpnext/crm/doctype/crm_settings/crm_settings.py +++ b/erpnext/crm/doctype/crm_settings/crm_settings.py @@ -3,7 +3,7 @@ import frappe from frappe import _ -from frappe.custom.doctype.custom_field.custom_field import create_custom_fields, delete_custom_fields +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.model.document import Document From d3df0bf387c452d831e2d2cd7acf7074380307a3 Mon Sep 17 00:00:00 2001 From: pandiyan Date: Sat, 27 Jun 2026 23:20:11 +0530 Subject: [PATCH 060/109] fix: reserve projected stock for production plan based on BOM qty --- .../doctype/production_plan/production_plan.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 67323d42d40..cb8f24fc9f1 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -9,6 +9,7 @@ from collections import defaultdict import frappe from frappe import _, msgprint from frappe.model.document import Document +from frappe.query_builder import Case from frappe.query_builder.functions import IfNull, Sum from frappe.utils import ( add_days, @@ -1375,7 +1376,7 @@ def get_material_request_items( get_conversion_factor(row.item_code, item_details.purchase_uom).get("conversion_factor") or 1.0 ) - if required_qty > 0: + if flt(row.get("qty")) > 0: return { "item_code": row.item_code, "item_name": row.item_name, @@ -1880,7 +1881,12 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): frappe.qb.from_(table) .inner_join(child) .on(table.name == child.parent) - .select(Sum(child.quantity * child.conversion_factor)) + .select( + Sum( + (Case().when(child.quantity == 0, child.required_bom_qty).else_(child.quantity)) + * child.conversion_factor + ) + ) .where( (table.docstatus == 1) & (child.item_code == item_code) From b39024e0e3380c67e9c2a48d4b77dbeb3c6adc32 Mon Sep 17 00:00:00 2001 From: pandiyan Date: Sat, 27 Jun 2026 23:20:11 +0530 Subject: [PATCH 061/109] test: update cascading test to assert zero-qty reservation for stock-covered items --- .../production_plan/test_production_plan.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 5af1fdb36b5..e64a8c16b84 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -212,13 +212,15 @@ class TestProductionPlan(FrappeTestCase): quantities = [d["quantity"] for d in mr_items] rm_qty = sum(quantities) - # Only 2 MR item created - the first SO's requirement is fully covered by stock (v15 behaviour) - self.assertEqual(len(mr_items), 2) - self.assertEqual(rm_qty, 2, "Cascading failed: total MR qty should be 2 (3 needed - 1 in stock)") + # 3 MR items: SO1's requirement is covered by stock (qty=0 but reserved), SO2 and SO3 need 1 each + self.assertEqual(len(mr_items), 3) + self.assertEqual( + rm_qty, 2, "Cascading failed: total purchase qty should be 2 (3 needed - 1 in stock)" + ) self.assertEqual( quantities, - [1, 1], - "Cascading failed: only second and third SO should need procurement (qty=1) since first SO consumed stock", + [0, 1, 1], + "SO1 stock-covered item should appear with qty=0 for reservation; SO2 and SO3 need qty=1", ) sr.cancel() @@ -251,11 +253,13 @@ class TestProductionPlan(FrappeTestCase): pln = create_production_plan( item_code="Test Production Item 1", use_multi_level_bom=0, ignore_existing_ordered_qty=0 ) - self.assertFalse(len(pln.mr_items)) + items_needing_purchase = [row.item_code for row in pln.mr_items if row.quantity > 0] + self.assertFalse(len(items_needing_purchase)) + + pln.cancel() sr1.cancel() sr2.cancel() - pln.cancel() def test_production_plan_sales_orders(self): "Test if previously fulfilled SO (with WO) is pulled into Prod Plan." From bba7fe98313c44c1478d5dca72aadc9ba555aa7e Mon Sep 17 00:00:00 2001 From: pandiyan Date: Wed, 24 Jun 2026 18:43:33 +0530 Subject: [PATCH 062/109] fix: skip qty over-allowance check for non-stock items only --- erpnext/controllers/status_updater.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 21e6d3ea8b9..50d272d7efb 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -275,6 +275,12 @@ class StatusUpdater(Document): item["idx"] = d.idx item["target_ref_field"] = args["target_ref_field"].replace("_", " ") + # skip qty over-allowance check for non-stock items + if "qty" in args.get("target_ref_field", "") and not frappe.get_cached_value( + "Item", item["item_code"], "is_stock_item" + ): + continue + # if not item[args['target_ref_field']]: # msgprint(_("Note: System will not check over-delivery and over-booking for Item {0} as quantity or amount is 0").format(item.item_code)) if args.get("no_allowance"): From 3f00a0ffa9c03e891643f17b86a9ca9182c6e7f6 Mon Sep 17 00:00:00 2001 From: pandiyan Date: Wed, 24 Jun 2026 18:43:39 +0530 Subject: [PATCH 063/109] test: add tests for non stock item over billing against so/po --- .../purchase_invoice/test_purchase_invoice.py | 18 ++++++++ .../sales_invoice/test_sales_invoice.py | 45 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index bccd29c822a..5aa2faed1a1 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -2924,6 +2924,24 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): # Test 4 - Since this PI is overbilled by 130% and only 120% is allowed, it will fail self.assertRaises(frappe.ValidationError, pi.submit) + @change_settings("Accounts Settings", {"over_billing_allowance": 0}) + def test_non_stock_item_over_billing_against_po_is_blocked(self): + service_item = create_item( + "_Test Service Item Non Stock PI", + is_stock_item=0, + is_purchase_item=1, + ).name + + po = create_purchase_order(item_code=service_item, qty=5, rate=100, do_not_save=False) + po.submit() + + pi = make_pi_from_po(po.name) + pi.items[0].qty = 10 # overbill by 100 % + pi.save() + + with self.assertRaises(frappe.ValidationError): + pi.submit() + def test_discount_percentage_not_set_when_amount_is_manually_set(self): pi = make_purchase_invoice(do_not_save=True) discount_amount = 7 diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 6f51e27f532..0b1f1e922bf 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3700,6 +3700,51 @@ class TestSalesInvoice(FrappeTestCase): self.assertTrue("cannot overbill" in str(err.exception).lower()) dn.cancel() + @change_settings("Accounts Settings", {"over_billing_allowance": 0}) + def test_non_stock_item_over_billing_against_so_is_blocked(self): + from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice as make_si_from_so + from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + + service_item = create_item( + "_Test Service Item Non Stock SI", + is_stock_item=0, + ).name + + so = make_sales_order(item_code=service_item, qty=5, rate=100) + so.submit() + + si = make_si_from_so(so.name) + si.items[0].qty = 10 # overbill by 100 % + si.save() + + with self.assertRaises(frappe.ValidationError): + si.submit() + + @change_settings("Accounts Settings", {"over_billing_allowance": 0}) + def test_non_stock_item_over_billing_against_so_from_quotation_is_blocked(self): + from erpnext.selling.doctype.quotation.quotation import make_sales_order as make_so_from_quotation + from erpnext.selling.doctype.quotation.test_quotation import make_quotation + from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice as make_si_from_so + + service_item = create_item( + "_Test Service Item Non Stock SI Quot", + is_stock_item=0, + ).name + + quotation = make_quotation(item_code=service_item, qty=5, rate=100) + + so = make_so_from_quotation(quotation.name) + so.delivery_date = frappe.utils.add_days(frappe.utils.today(), 7) + so.insert() + so.submit() + + si = make_si_from_so(so.name) + si.items[0].qty = 10 # overbill by 100 % + si.save() + + with self.assertRaises(frappe.ValidationError): + si.submit() + @change_settings( "Accounts Settings", { From 562563553c3dafc5f5e97c3995db0ac22dcd1fab Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 28 Jun 2026 12:16:40 +0530 Subject: [PATCH 064/109] fix: sync Stock Reconciliation difference amount with GL after reposting (backport #56574) (#56584) * fix: sync Stock Reconciliation difference amount with GL after reposting (#56574) * fix: sync Stock Reconciliation difference amount with GL after reposting * fix: placement of recalculate differece amount function (cherry picked from commit c7ef42ef98b9e1da549f67e73291fb25431aa46e) # Conflicts: # erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py # erpnext/stock/stock_ledger.py * chore: fix conflicts Removed unused imports and fixed import structure. * chore: fix conflicts Refactor update_rate_on_stock_reconciliation method to use recalculation from ledger instead of manual updates for stock reconciliation items. * chore: fix conflicts --------- Co-authored-by: rohitwaghchaure --- .../stock_reconciliation.py | 113 ++++++++++++ .../test_stock_reconciliation.py | 166 ++++++++++++++++++ erpnext/stock/stock_ledger.py | 45 ++--- 3 files changed, 291 insertions(+), 33 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 67f9b57c172..ab1358e8293 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -993,6 +993,102 @@ class StockReconciliation(StockController): d.quantity_difference = flt(d.qty) - flt(d.current_qty) d.amount_difference = flt(d.amount) - flt(d.current_amount) + def recalculate_difference_amount_from_ledger(self): + """Sync the displayed current qty/rate and difference amount with the (reposted) ledger. + + Submitted reconciliations freeze ``difference_amount`` and the per-row current values at + submit time, but reposting/backdated transactions recompute the reconciliation's Stock Ledger + Entries and rebuild the GL from them. Without this sync the document keeps showing stale figures + that no longer match the GL entries. Anchoring ``amount_difference`` to the row's summed + ``stock_value_difference`` keeps the document and the GL consistent by construction. + """ + difference_amount = 0.0 + + for row in self.items: + stock_value_difference = flt(get_row_stock_value_difference(self.doctype, self.name, row.name)) + + amount = flt(flt(row.qty) * flt(row.valuation_rate), row.precision("amount")) + amount_difference = flt(stock_value_difference, row.precision("amount_difference")) + current_amount = flt(amount - amount_difference, row.precision("current_amount")) + + current_qty = self.get_current_qty_from_ledger(row) + current_valuation_rate = ( + flt(current_amount / current_qty, row.precision("current_valuation_rate")) + if current_qty + else 0.0 + ) + + row.db_set( + { + "amount": amount, + "current_qty": current_qty, + "current_valuation_rate": current_valuation_rate, + "current_amount": current_amount, + "quantity_difference": flt(row.qty) - current_qty, + "amount_difference": amount_difference, + }, + update_modified=False, + ) + + difference_amount += amount_difference + + self.db_set( + "difference_amount", + flt(difference_amount, self.precision("difference_amount")), + update_modified=False, + ) + + def get_current_qty_from_ledger(self, row): + """Current (pre-reconciliation) qty for a row, recomputed from the ledger after reposting. + + Serial/batch rows cannot have backdated qty changes inserted before a future reconciliation + (blocked by ``check_future_entries_exists``), so their current qty is frozen and read straight + from the current bundle. Non-serial rows can float, so read the ledger balance just before the + reconciliation, excluding the reconciliation's own entries. + """ + if row.current_serial_and_batch_bundle: + total_qty = frappe.db.get_value( + "Serial and Batch Bundle", row.current_serial_and_batch_bundle, "total_qty" + ) + return abs(flt(total_qty, row.precision("current_qty"))) + + reco_sle = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": self.doctype, + "voucher_no": self.name, + "voucher_detail_no": row.name, + "is_cancelled": 0, + }, + ["posting_datetime", "creation"], + as_dict=True, + ) + if not reco_sle: + return flt(row.current_qty, row.precision("current_qty")) + + sle = frappe.qb.DocType("Stock Ledger Entry") + previous_sle = ( + frappe.qb.from_(sle) + .select(sle.qty_after_transaction) + .where( + (sle.item_code == row.item_code) + & (sle.warehouse == row.warehouse) + & (sle.is_cancelled == 0) + & ( + (sle.posting_datetime < reco_sle.posting_datetime) + | ( + (sle.posting_datetime == reco_sle.posting_datetime) + & (sle.creation < reco_sle.creation) + ) + ) + ) + .orderby(sle.posting_datetime, order=frappe.qb.desc) + .orderby(sle.creation, order=frappe.qb.desc) + .limit(1) + ).run() + + return flt(previous_sle[0][0], row.precision("current_qty")) if previous_sle else 0.0 + def submit(self): if len(self.items) > 100: msgprint( @@ -1179,6 +1275,23 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None): return itemwise_batch_data +def get_row_stock_value_difference(voucher_type: str, voucher_no: str, voucher_detail_no: str): + """Net stock value change posted to the GL by a reconciliation row (sum of its SLEs).""" + sle = frappe.qb.DocType("Stock Ledger Entry") + result = ( + frappe.qb.from_(sle) + .select(Sum(sle.stock_value_difference)) + .where( + (sle.voucher_type == voucher_type) + & (sle.voucher_no == voucher_no) + & (sle.voucher_detail_no == voucher_detail_no) + & (sle.is_cancelled == 0) + ) + ).run() + + return flt(result[0][0]) if result and result[0][0] else 0.0 + + @frappe.whitelist() def get_stock_balance_for( item_code: str, diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 795ea870cf1..11d7850913e 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -782,6 +782,172 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): sr1.load_from_db() self.assertEqual(sr1.difference_amount, 10000) + def assert_reco_difference_matches_gl(self, reco_name): + """The displayed Difference Amount (doc and per-row) must equal the reposted GL impact, + i.e. the sum of the reconciliation's Stock Ledger Entry ``stock_value_difference``.""" + from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( + get_row_stock_value_difference, + ) + + reco = frappe.get_doc("Stock Reconciliation", reco_name) + total_difference = 0.0 + + for row in reco.items: + row_difference = flt( + get_row_stock_value_difference("Stock Reconciliation", reco_name, row.name), + row.precision("amount_difference"), + ) + + self.assertEqual(flt(row.amount_difference), row_difference) + total_difference += row_difference + + self.assertEqual( + flt(reco.difference_amount, reco.precision("difference_amount")), + flt(total_difference, reco.precision("difference_amount")), + ) + + def test_difference_amount_synced_with_gl_after_repost_non_serialized(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = self.make_item().name + warehouse = "_Test Warehouse - _TC" + + # Opening stock => 100 * 100 = 10000 + make_stock_entry( + item_code=item_code, + target=warehouse, + qty=100, + basic_rate=100, + posting_date=add_days(nowdate(), -5), + posting_time="10:00:00", + ) + + # Reconcile to 100 @ 200 => difference 20000 - 10000 = 10000 + reco = create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=100, + rate=200, + posting_date=add_days(nowdate(), -2), + ) + self.assertEqual(reco.difference_amount, 10000) + self.assert_reco_difference_matches_gl(reco.name) + + # Backdated reconciliation lowers the pre-reco stock value to 50 * 50 = 2500 + create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=50, + rate=50, + posting_date=add_days(nowdate(), -3), + ) + + reco.load_from_db() + # Current is now 2500 => difference 20000 - 2500 = 17500 + self.assertEqual(reco.difference_amount, 17500) + self.assert_reco_difference_matches_gl(reco.name) + + def test_difference_amount_synced_with_gl_after_repost_batched(self): + from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import ( + make_landed_cost_voucher, + ) + + item_code = self.make_item( + "Test Batch Item Reco Difference Sync", + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TEST-BATCH-DIFFSYNC-.###", + }, + ).name + warehouse = "_Test Warehouse - _TC" + + # Receive 10 @ 100 (batch value 1000) + pr = make_purchase_receipt( + item_code=item_code, + warehouse=warehouse, + qty=10, + rate=100, + posting_date=add_days(nowdate(), -5), + ) + batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) + + # Reconcile the batch to 10 @ 500 => difference 5000 - 1000 = 4000 + reco = create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=10, + rate=500, + batch_no=batch_no, + use_serial_batch_fields=1, + posting_date=add_days(nowdate(), -2), + ) + difference_on_submit = reco.difference_amount + self.assert_reco_difference_matches_gl(reco.name) + + # Landed cost retroactively raises the receipt (and batch) valuation, reposting the reco + make_landed_cost_voucher( + receipt_document_type="Purchase Receipt", + receipt_document=pr.name, + charges=1000, + company="_Test Company", + ) + + reco.load_from_db() + self.assertNotEqual(reco.difference_amount, difference_on_submit) + self.assert_reco_difference_matches_gl(reco.name) + + def test_difference_amount_synced_with_gl_after_repost_serialized(self): + from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import ( + make_landed_cost_voucher, + ) + + item_code = self.make_item( + "Test Serial Item Reco Difference Sync", + { + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "TSIRDS.####", + }, + ).name + warehouse = "_Test Warehouse - _TC" + + # Receive 5 serial nos @ 100 (value 500) + pr = make_purchase_receipt( + item_code=item_code, + warehouse=warehouse, + qty=5, + rate=100, + posting_date=add_days(nowdate(), -5), + ) + serial_nos = get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle) + + # Reconcile the serial nos to 5 @ 500 => difference 2500 - 500 = 2000 + reco = create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=5, + rate=500, + serial_no="\n".join(serial_nos), + use_serial_batch_fields=1, + posting_date=add_days(nowdate(), -2), + ) + difference_on_submit = reco.difference_amount + self.assert_reco_difference_matches_gl(reco.name) + + # Landed cost retroactively raises the receipt (and serial) valuation, reposting the reco + make_landed_cost_voucher( + receipt_document_type="Purchase Receipt", + receipt_document=pr.name, + charges=1000, + company="_Test Company", + ) + + reco.load_from_db() + self.assertNotEqual(reco.difference_amount, difference_on_submit) + self.assert_reco_difference_matches_gl(reco.name) + def test_make_stock_zero_for_serial_batch_item(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 9ee458ce9c7..26dcb5f024b 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1329,6 +1329,11 @@ class update_entries_after: Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount """ + if sle.voucher_type == "Stock Reconciliation": + if flt(sle.actual_qty) <= 0 and not self.args.get("sle_id"): + self.update_rate_on_stock_reconciliation(sle) + return + if sle.actual_qty and sle.voucher_detail_no: outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty) @@ -1340,8 +1345,6 @@ class update_entries_after: self.update_rate_on_purchase_receipt(sle, outgoing_rate) elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt": self.update_rate_on_subcontracting_receipt(sle, outgoing_rate) - elif sle.voucher_type == "Stock Reconciliation": - self.update_rate_on_stock_reconciliation(sle) def update_rate_on_stock_entry(self, sle, outgoing_rate): frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate) @@ -1435,37 +1438,13 @@ class update_entries_after: d.db_update() def update_rate_on_stock_reconciliation(self, sle): - if not sle.serial_no and not sle.batch_no: - sr = frappe.get_doc("Stock Reconciliation", sle.voucher_no, for_update=True) - - for item in sr.items: - # Skip for Serial and Batch Items - if item.name != sle.voucher_detail_no or item.serial_no or item.batch_no: - continue - - previous_sle = get_previous_sle( - { - "item_code": item.item_code, - "warehouse": item.warehouse, - "posting_date": sr.posting_date, - "posting_time": sr.posting_time, - "sle": sle.name, - } - ) - - item.current_qty = previous_sle.get("qty_after_transaction") or 0.0 - item.current_valuation_rate = previous_sle.get("valuation_rate") or 0.0 - item.current_amount = flt(item.current_qty) * flt(item.current_valuation_rate) - - item.amount = flt(item.qty) * flt(item.valuation_rate) - item.quantity_difference = item.qty - item.current_qty - item.amount_difference = item.amount - item.current_amount - else: - sr.difference_amount = sum([item.amount_difference for item in sr.items]) - sr.db_update() - - for item in sr.items: - item.db_update() + # Refresh the reconciliation's difference amount and per-row current qty/rate from the reposted + # ledger so the document keeps matching the GL entries. Handles serialized, batched and + # non-serialized items uniformly (the document method reads the current bundle for serial/batch + # rows and the pre-reconciliation ledger balance for non-serial rows). + frappe.get_lazy_doc( + "Stock Reconciliation", sle.voucher_no, for_update=True + ).recalculate_difference_amount_from_ledger() def get_incoming_value_for_serial_nos(self, sle, serial_nos): # get rate from serial nos within same company From 8abc27863ab0653a3af9c02dd034c2049b084d39 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 29 Jun 2026 15:06:21 +0530 Subject: [PATCH 065/109] fix: extract stock ageing data helper in stock balance (backport #55774) The v15 backport (#55776) of #55774 dropped the stock_balance.py changes. A separate backport left an inline version that sorts/filters the FIFO queue by itemgetter(1) *before* normalizing, so for batchwise valuation slots itemgetter(1) reads the count flag instead of the posting date. The queue is then ordered wrong and earliest_age/latest_age come out swapped. Extract get_stock_ageing_data, which normalizes before sorting/filtering so itemgetter(1) is always the posting date, and add the regression test. Co-Authored-By: Claude Opus 4.8 --- .../report/stock_balance/stock_balance.py | 28 +++++++++++-------- .../stock_balance/test_stock_balance.py | 18 +++++++++++- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 6bceb0483f8..2c25b2c5b2b 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -100,8 +100,6 @@ class StockBalanceReport: self.filters["show_warehouse_wise_stock"] = True item_wise_fifo_queue = FIFOSlots(self.filters, self.sle_entries).generate() - _func = itemgetter(1) - del self.sle_entries sre_details = self.get_sre_reserved_qty_details() @@ -126,16 +124,7 @@ class StockBalanceReport: stock_ageing_data = {"average_age": 0, "earliest_age": 0, "latest_age": 0} if opening_fifo_queue: - fifo_queue = sorted(filter(_func, opening_fifo_queue), key=_func) - fifo_queue = normalize_fifo_queue(fifo_queue) - if not fifo_queue: - continue - - to_date = self.to_date - stock_ageing_data["average_age"] = get_average_age(fifo_queue, to_date) - stock_ageing_data["earliest_age"] = date_diff(to_date, fifo_queue[0][1]) - stock_ageing_data["latest_age"] = date_diff(to_date, fifo_queue[-1][1]) - stock_ageing_data["fifo_queue"] = fifo_queue + stock_ageing_data.update(get_stock_ageing_data(opening_fifo_queue, self.to_date)) report_data.update(stock_ageing_data) @@ -694,6 +683,21 @@ class StockBalanceReport: return opening_fifo_queue +def get_stock_ageing_data(fifo_queue: list, to_date: str) -> dict: + stock_ageing_data = {"average_age": 0, "earliest_age": 0, "latest_age": 0} + fifo_queue = sorted(filter(itemgetter(1), normalize_fifo_queue(fifo_queue)), key=itemgetter(1)) + + if not fifo_queue: + return stock_ageing_data + + stock_ageing_data["average_age"] = get_average_age(fifo_queue, to_date) + stock_ageing_data["earliest_age"] = date_diff(to_date, fifo_queue[0][1]) + stock_ageing_data["latest_age"] = date_diff(to_date, fifo_queue[-1][1]) + stock_ageing_data["fifo_queue"] = fifo_queue + + return stock_ageing_data + + def filter_items_with_no_transactions( iwb_map, float_precision: float, inventory_dimensions: list | None = None ): diff --git a/erpnext/stock/report/stock_balance/test_stock_balance.py b/erpnext/stock/report/stock_balance/test_stock_balance.py index 0985e4783c3..347c14c7eb0 100644 --- a/erpnext/stock/report/stock_balance/test_stock_balance.py +++ b/erpnext/stock/report/stock_balance/test_stock_balance.py @@ -7,7 +7,7 @@ from frappe.utils import today from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -from erpnext.stock.report.stock_balance.stock_balance import execute +from erpnext.stock.report.stock_balance.stock_balance import execute, get_stock_ageing_data def stock_balance(filters): @@ -168,3 +168,19 @@ class TestStockBalance(FrappeTestCase): rows = stock_balance(self.filters.update({"show_variant_attributes": 1, "item_code": [variant.name]})) self.assertPartialDictEq(attributes, rows[0]) self.assertInvariants(rows) + + def test_stock_ageing_data_accepts_batchwise_valuation_slots(self): + fifo_queue = [ + ["SA-BATCH-NEWER", 1, 2.0, "2021-12-05", 20.0], + ["SA-BATCH-OLDER", 1, 3.0, "2021-12-01", 30.0], + ] + + stock_ageing_data = get_stock_ageing_data(fifo_queue, "2021-12-10") + + self.assertEqual(stock_ageing_data["average_age"], 7.4) + self.assertEqual(stock_ageing_data["earliest_age"], 9) + self.assertEqual(stock_ageing_data["latest_age"], 5) + self.assertEqual( + stock_ageing_data["fifo_queue"], + [[3.0, "2021-12-01", 30.0], [2.0, "2021-12-05", 20.0]], + ) From ab98d19f26f82f34f89a2dcddfa285e8c55cff74 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 29 Jun 2026 17:20:12 +0530 Subject: [PATCH 066/109] fix: negative stock issue (#56616) --- erpnext/stock/stock_ledger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 26dcb5f024b..06159345577 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -134,6 +134,7 @@ def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_vou "sle_id": args.get("name"), "creation": args.get("creation"), "reserved_stock": args.get("reserved_stock"), + "cancelled": args.get("is_cancelled"), }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher, From f50e529f8ad6bcc267925fd550ab3f5baa9a4134 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 29 Jun 2026 17:21:17 +0530 Subject: [PATCH 067/109] fix: update_qty_in_future_sle skips SLEs with same posting datetime (#56615) --- .../test_stock_ledger_entry.py | 101 ++++++++++++++++++ erpnext/stock/stock_ledger.py | 80 ++++++++------ 2 files changed, 149 insertions(+), 32 deletions(-) diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index 917aba9803e..ac3d4a01d11 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -1257,6 +1257,107 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): self.assertEqual(sle[0].qty_after_transaction, 105) self.assertEqual(sle[0].actual_qty, 100) + def test_update_qty_in_future_sle_shifts_same_timestamp_later_entry(self): + # update_qty_in_future_sle treats "future" as strictly after the current entry in the + # (posting_datetime, creation) order. An entry sharing the exact posting timestamp but created + # later must still have its running balance shifted; comparing posting_datetime alone would skip + # it. The current entry itself (same timestamp, same creation) must not be shifted. + from erpnext.stock.stock_ledger import update_qty_in_future_sle + + item = make_item().name + warehouse = "_Test Warehouse - _TC" + + receipt1 = make_purchase_receipt( + item_code=item, + warehouse=warehouse, + qty=10, + rate=10, + posting_date="2021-01-01", + posting_time="02:00:00", + ) + time.sleep(1) + receipt2 = make_purchase_receipt( + item_code=item, + warehouse=warehouse, + qty=20, + rate=10, + posting_date="2021-01-01", + posting_time="02:00:00", # identical timestamp, later creation + ) + + def sle(voucher): + return frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": voucher.name, "is_cancelled": 0}, + ["name", "posting_date", "posting_time", "creation", "qty_after_transaction"], + as_dict=True, + ) + + sle1, sle2 = sle(receipt1), sle(receipt2) + self.assertEqual(sle1.qty_after_transaction, 10) + self.assertEqual(sle2.qty_after_transaction, 30) + + # Simulate a +5 qty shift originating at receipt1's ledger position. + args = frappe._dict( + { + "item_code": item, + "warehouse": warehouse, + "voucher_type": "Purchase Receipt", + "voucher_no": receipt1.name, + "posting_date": sle1.posting_date, + "posting_time": sle1.posting_time, + "creation": sle1.creation, + "actual_qty": 5, + } + ) + update_qty_in_future_sle(args, allow_negative_stock=True) + + # receipt2 (same timestamp, later creation) is shifted; receipt1 (the current entry) is not. + self.assertEqual(frappe.db.get_value("Stock Ledger Entry", sle2.name, "qty_after_transaction"), 35) + self.assertEqual(frappe.db.get_value("Stock Ledger Entry", sle1.name, "qty_after_transaction"), 10) + + def test_get_next_stock_reco_respects_creation_order(self): + # A stock reco sharing the exact posting timestamp of the current entry must only count as the + # "next" reco when it was created after that entry. A reco created before it actually precedes + # the entry and must not bound (truncate) the qty-shift range. + from erpnext.stock.stock_ledger import get_next_stock_reco + + item = make_item().name + warehouse = "_Test Warehouse - _TC" + + reco = create_stock_reconciliation( + item_code=item, + warehouse=warehouse, + qty=10, + rate=100, + posting_date="2021-01-01", + posting_time="02:00:00", + ) + reco_sle = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": reco.name, "is_cancelled": 0}, + ["posting_date", "posting_time", "creation"], + as_dict=True, + ) + + base_kwargs = { + "item_code": item, + "warehouse": warehouse, + "voucher_no": "SOME-OTHER-VOUCHER", + "posting_date": reco_sle.posting_date, + "posting_time": reco_sle.posting_time, + } + + # Current entry created AFTER the reco at the same timestamp -> reco precedes it -> not returned. + after = {**base_kwargs, "creation": add_to_date(reco_sle.creation, seconds=5)} + self.assertFalse(get_next_stock_reco(after)) + + # Current entry created BEFORE the reco at the same timestamp -> reco follows it -> returned. + before = {**base_kwargs, "creation": add_to_date(reco_sle.creation, seconds=-5)} + result = get_next_stock_reco(before) + self.assertTrue(result) + self.assertEqual(result[0].voucher_no, reco.name) + @change_settings("System Settings", {"float_precision": 3, "currency_precision": 2}) def test_transfer_invariants(self): """Extact stock value should be transferred.""" diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 06159345577..0d83119a0fc 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -2042,36 +2042,44 @@ def get_valuation_rate( def update_qty_in_future_sle(args, allow_negative_stock=False): """Recalculate Qty after Transaction in future SLEs based on current SLE.""" - datetime_limit_condition = "" qty_shift = args.actual_qty - args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"]) + posting_datetime = get_combine_datetime(args["posting_date"], args["posting_time"]) + args["posting_datetime"] = posting_datetime # find difference/shift in qty caused by stock reconciliation if args.voucher_type == "Stock Reconciliation": qty_shift = get_stock_reco_qty_shift(args) + sle = frappe.qb.DocType("Stock Ledger Entry") + + # SLEs are ordered by (posting_datetime, creation). "Future" therefore means strictly after the + # current entry in that tuple order: a later posting_datetime, or the same posting_datetime with a + # later creation. Comparing posting_datetime alone would skip same-timestamp entries created after + # this one (e.g. the same item repeated in a voucher, or another voucher posted in the same second). + future_condition = sle.posting_datetime > posting_datetime + if args.get("creation"): + future_condition = future_condition | ( + (sle.posting_datetime == posting_datetime) & (sle.creation > args.get("creation")) + ) + + query = ( + frappe.qb.update(sle) + .set(sle.qty_after_transaction, sle.qty_after_transaction + qty_shift) + .where( + (sle.item_code == args.get("item_code")) + & (sle.warehouse == args.get("warehouse")) + & (sle.is_cancelled == 0) + & future_condition + ) + ) + # find the next nearest stock reco so that we only recalculate SLEs till that point next_stock_reco_detail = get_next_stock_reco(args) if next_stock_reco_detail: - detail = next_stock_reco_detail[0] - datetime_limit_condition = get_datetime_limit_condition(detail) + query = query.where(get_datetime_limit_condition(sle, next_stock_reco_detail[0])) - frappe.db.sql( # nosemgrep - f""" - update `tabStock Ledger Entry` - set qty_after_transaction = qty_after_transaction + {qty_shift} - where - item_code = %(item_code)s - and warehouse = %(warehouse)s - and is_cancelled = 0 - and ( - posting_datetime > %(posting_datetime)s - ) - {datetime_limit_condition} - """, - args, - ) + query.run() validate_negative_qty_in_future_sle(args, allow_negative_stock) @@ -2106,6 +2114,22 @@ def get_stock_reco_qty_shift(args): return stock_reco_qty_shift +def get_next_reco_datetime_condition(sle, kwargs): + """Match stock recos that come strictly after the current entry in (posting_datetime, creation) + order. Using posting_datetime alone (>=) could pick a reco sharing this exact timestamp but created + earlier — i.e. one that actually precedes this entry — and wrongly truncate the qty-shift range.""" + current_datetime = get_combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time")) + + creation = kwargs.get("creation") + if not creation: + # No creation tiebreaker available; fall back to the posting_datetime-only bound. + return sle.posting_datetime >= current_datetime + + return (sle.posting_datetime > current_datetime) | ( + (sle.posting_datetime == current_datetime) & (sle.creation > creation) + ) + + def get_next_stock_reco(kwargs): """Returns next nearest stock reconciliaton's details.""" @@ -2131,10 +2155,7 @@ def get_next_stock_reco(kwargs): & (sle.voucher_type == "Stock Reconciliation") & (sle.voucher_no != kwargs.get("voucher_no")) & (sle.is_cancelled == 0) - & ( - sle.posting_datetime - >= get_combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time")) - ) + & get_next_reco_datetime_condition(sle, kwargs) ) .orderby(sle.posting_datetime) .orderby(sle.creation) @@ -2147,17 +2168,12 @@ def get_next_stock_reco(kwargs): return query.run(as_dict=True) -def get_datetime_limit_condition(detail): +def get_datetime_limit_condition(sle, detail): posting_datetime = get_combine_datetime(detail.posting_date, detail.posting_time) - return f""" - and - (posting_datetime < '{posting_datetime}' - or ( - posting_datetime = '{posting_datetime}' - and creation < '{detail.creation}' - ) - )""" + return (sle.posting_datetime < posting_datetime) | ( + (sle.posting_datetime == posting_datetime) & (sle.creation < detail.creation) + ) def validate_negative_qty_in_future_sle(args, allow_negative_stock=False): From 52d04ad8341fa5653de593feb7c0dbb4f1ecf6b5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 19:04:44 +0530 Subject: [PATCH 068/109] fix: Use correct doctype name for PCV perm-check (backport #56606) (#56610) fix: Use correct doctype name for PCV perm-check (#56606) closes https://github.com/frappe/erpnext/issues/56593 (cherry picked from commit 747374e767a1b790d1cd2d83cab8ae3bdb3580ae) Co-authored-by: Ankush Menat --- .../process_period_closing_voucher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py index 096b085cf0e..b5ca3331b71 100644 --- a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py @@ -89,7 +89,7 @@ class ProcessPeriodClosingVoucher(Document): @frappe.whitelist() def start_pcv_processing(docname: str): if frappe.db.get_value("Process Period Closing Voucher", docname, "status") in ["Queued", "Running"]: - frappe.has_permission("Process Payment Reconciliation", "write", doc=docname, throw=True) + frappe.has_permission("Process Period Closing Voucher", "write", doc=docname, throw=True) frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running") timeout = frappe.db.get_single_value("Accounts Settings", "pcv_job_timeout") or 3600 From 83e8d1eb2f74c34e555b149c61f2e660387601a9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 23:12:20 +0530 Subject: [PATCH 069/109] fix: carry item-level project to Purchase Receipt GL entries (backport #56568) (#56618) fix: carry item-level project to Purchase Receipt GL entries (#56568) Purchase Receipt stock and asset GL lines used the item row's cost center but always fell back to the document-level project, unlike Purchase Invoice which uses the item-level project. add_gl_entry accepted a project argument but never wrote it to the GL dict, so the inward, Stock Received But Not Billed, landed cost, divisional loss, sub-contracting and exchange rate lines dropped the row's project. Write project into the GL dict and pass project=item.project on the entries that were missing it, so project behaves like cost center and matches Purchase Invoice. Ticket: 72523 (cherry picked from commit 6f97c7199c4f65e5e2fda31d99905077f2d2a2bc) Co-authored-by: Jatin3128 <140256508+Jatin3128@users.noreply.github.com> --- erpnext/controllers/stock_controller.py | 3 +++ erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 32968952fc6..f58c831922a 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -1490,6 +1490,9 @@ class StockController(AccountsController): "remarks": remarks, } + if project: + gl_entry.update({"project": project}) + if voucher_detail_no: gl_entry.update({"voucher_detail_no": voucher_detail_no}) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index c27e2e40f30..10099631a75 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -493,6 +493,7 @@ class PurchaseReceipt(BuyingController): remarks=remarks, against_account=stock_asset_rbnb, account_currency=account_currency, + project=item.project, item=item, ) @@ -535,6 +536,7 @@ class PurchaseReceipt(BuyingController): against_account=stock_asset_account_name, debit_in_account_currency=-1 * flt(outgoing_amount, item.precision("base_net_amount")), account_currency=account_currency, + project=item.project, item=item, ) @@ -559,6 +561,7 @@ class PurchaseReceipt(BuyingController): against_account=self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, account_currency=account_currency, + project=item.project, item=item, ) @@ -572,6 +575,7 @@ class PurchaseReceipt(BuyingController): against_account=self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, account_currency=account_currency, + project=item.project, item=item, ) @@ -634,6 +638,7 @@ class PurchaseReceipt(BuyingController): remarks=remarks, against_account=stock_asset_account_name, account_currency=supplier_warehouse_account_currency, + project=item.project, item=item, ) From 9125ab6c7734fb2a97060a717851e3a7c9051284 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 21 Jun 2026 10:14:42 +0530 Subject: [PATCH 070/109] fix(controllers): fix supplier-RFQ portal list query (wrong column + Postgres DISTINCT) rfq_transaction_list had two defects introduced when it was converted to the query builder: 1. `party.supplier == party[0]` compared supplier to a column literally named "0" (a stray index on the DocType, not the intended `parties[0]` value). This renders as `supplier = \`0\`` / `supplier = "0"` and errors on BOTH engines (MariaDB: Unknown column '0'; Postgres: column "0" does not exist), so the supplier portal RFQ list was completely broken. 2. SELECT DISTINCT ordered by `creation`, which is not in the select list. Postgres rejects this ("for SELECT DISTINCT, ORDER BY expressions must appear in select list"). Compare against `parties[0]` and add `creation` to the select list. Co-Authored-By: Claude Opus 4.8 (1M context) (cherry picked from commit a7d9078bf450141a4e10fcb2f406f6ba272fe803) # Conflicts: # erpnext/controllers/tests/test_website_list_for_contact.py --- .../tests/test_website_list_for_contact.py | 36 +++++++++++++++++++ .../controllers/website_list_for_contact.py | 5 +-- 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 erpnext/controllers/tests/test_website_list_for_contact.py diff --git a/erpnext/controllers/tests/test_website_list_for_contact.py b/erpnext/controllers/tests/test_website_list_for_contact.py new file mode 100644 index 00000000000..d62254d8a0c --- /dev/null +++ b/erpnext/controllers/tests/test_website_list_for_contact.py @@ -0,0 +1,36 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import json + +from frappe.tests.utils import FrappeTestCase + + +class TestWebsiteListForContact(FrappeTestCase): + def test_get_list_context_currency_symbols(self): + # get_list_context builds the enabled-currency symbol map via frappe.get_all (converted from + # raw SQL). Exercises that query and asserts a known enabled currency is present. + from erpnext.controllers.website_list_for_contact import get_list_context + + context = get_list_context() + + symbols = json.loads(context["currency_symbols"]) + self.assertIsInstance(symbols, dict) + self.assertIn("USD", symbols) + + def test_rfq_transaction_list_returns_supplier_rfq(self): + # rfq_transaction_list filters RFQs by the supplier (parties[0]) and uses SELECT DISTINCT with + # ORDER BY creation -- both must be valid on Postgres, and the supplier filter must compare to the + # party value (not a stray `party[0]` column reference). + from erpnext.buying.doctype.request_for_quotation.test_request_for_quotation import ( + make_request_for_quotation, + ) + from erpnext.controllers.website_list_for_contact import rfq_transaction_list + + rfq = make_request_for_quotation() + supplier = rfq.suppliers[0].supplier + + rows = rfq_transaction_list( + "Request for Quotation Supplier", "Request for Quotation", [supplier], 0, 20 + ) + self.assertIn(rfq.name, [row.name for row in rows]) diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py index ea7b47bd487..a62fccc752c 100644 --- a/erpnext/controllers/website_list_for_contact.py +++ b/erpnext/controllers/website_list_for_contact.py @@ -181,9 +181,10 @@ def rfq_transaction_list(parties_doctype, doctype, parties, limit_start, limit_p party = frappe.qb.DocType(parties_doctype) data = ( frappe.qb.from_(party) - .select(party.parent.as_("name"), party.supplier) + # creation must be selected: Postgres requires SELECT DISTINCT order-by exprs in the select list + .select(party.parent.as_("name"), party.supplier, party.creation) .distinct() - .where((party.supplier == party[0]) & (party.docstatus == 1)) + .where((party.supplier == parties[0]) & (party.docstatus == 1)) .orderby(party.creation, order=frappe.qb.desc) .limit(limit_page_length) .offset(limit_start) From 25764520c2981460ffa713c8ff5770246667b695 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 14:47:33 +0530 Subject: [PATCH 071/109] fix: update qty in future SLEs when cancelling documents (backport #56638) (#56644) fix: update qty in future SLEs when cancelling documents (#56638) (cherry picked from commit 01374db8dab4979309b4d1fa446c4af0a008ddfc) Co-authored-by: rohitwaghchaure --- .../test_stock_ledger_entry.py | 41 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 5 ++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index ac3d4a01d11..6aea0ef5337 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -1316,6 +1316,47 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): self.assertEqual(frappe.db.get_value("Stock Ledger Entry", sle2.name, "qty_after_transaction"), 35) self.assertEqual(frappe.db.get_value("Stock Ledger Entry", sle1.name, "qty_after_transaction"), 10) + def test_cancel_first_of_two_same_timestamp_entries(self): + # Two receipts of the same item+warehouse at the exact same posting timestamp: balances 10 -> 20. + # Cancelling the first must leave the second standing alone on a zero base (qty 10), not + # double-decremented. The same-timestamp sibling is corrected by the cancellation reprocessing, + # so update_qty_in_future_sle must not shift it again. + item = make_item().name + warehouse = "_Test Warehouse - _TC" + + receipt1 = make_purchase_receipt( + item_code=item, + warehouse=warehouse, + qty=10, + rate=10, + posting_date="2026-06-01", + posting_time="10:00:00", + ) + time.sleep(1) + receipt2 = make_purchase_receipt( + item_code=item, + warehouse=warehouse, + qty=10, + rate=10, + posting_date="2026-06-01", + posting_time="10:00:00", # identical timestamp, later creation + ) + + def qty_after(voucher): + return frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": voucher.name, "is_cancelled": 0}, + "qty_after_transaction", + ) + + self.assertEqual(qty_after(receipt1), 10) + self.assertEqual(qty_after(receipt2), 20) + + receipt1.cancel() + + # receipt2 now sits on a zero base -> 10 (not 0 from a double shift, nor a negative-stock error). + self.assertEqual(qty_after(receipt2), 10) + def test_get_next_stock_reco_respects_creation_order(self): # A stock reco sharing the exact posting timestamp of the current entry must only count as the # "next" reco when it was created after that entry. A reco created before it actually precedes diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 0d83119a0fc..93ee2eaa651 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -2057,8 +2057,11 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): # current entry in that tuple order: a later posting_datetime, or the same posting_datetime with a # later creation. Comparing posting_datetime alone would skip same-timestamp entries created after # this one (e.g. the same item repeated in a voucher, or another voucher posted in the same second). + # On cancellation `args` is a freshly inserted reversal entry, so its `creation` is the cancel time + # (not the original entry's position) and same-timestamp siblings are already recomputed by the + # cancelled path in update_entries_after; applying the tiebreaker here would double-shift them. future_condition = sle.posting_datetime > posting_datetime - if args.get("creation"): + if args.get("creation") and not args.get("is_cancelled"): future_condition = future_condition | ( (sle.posting_datetime == posting_datetime) & (sle.creation > args.get("creation")) ) From 3479d65bd4fe0b5efcebad0e0c6eb0bf94033f00 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:33:00 +0530 Subject: [PATCH 072/109] fix(stock): value batch/serial return from ledger when original receipt has no bundle (backport #56631) (#56645) fix(stock): value batch/serial return from ledger when original receipt has no bundle (#56631) * fix(stock): value batch/serial return from ledger when original receipt has no bundle * test(stock): add test to validate the valuation of serial/batch for return when original receipt has no bundle (cherry picked from commit 6184c057dbb4547f6cbb30f08a16a811fe811b0c) Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com> --- .../serial_and_batch_bundle.py | 52 +++++++++++- .../test_serial_and_batch_bundle.py | 85 +++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) 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 f8facea5f78..4fa630fb8a8 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 @@ -513,10 +513,12 @@ class SerialandBatchBundle(Document): ] # Added to handle rejected warehouse case + return_warehouse = None if self.voucher_type in ["Purchase Receipt", "Purchase Invoice"]: warehouses = get_warehouses_for_return(self.voucher_type, return_against_voucher_detail_no) if self.warehouse in warehouses: - filters.append(["Serial and Batch Entry", "warehouse", "=", self.warehouse]) + return_warehouse = self.warehouse + filters.append(["Serial and Batch Entry", "warehouse", "=", return_warehouse]) bundle_data = frappe.get_all( "Serial and Batch Bundle", @@ -529,6 +531,11 @@ class SerialandBatchBundle(Document): order_by="`tabSerial and Batch Bundle`.`creation`, `tabSerial and Batch Entry`.`idx`", ) + if not bundle_data: + bundle_data = self.get_legacy_valuation_rate_for_return_entry( + return_against, return_against_voucher_detail_no, return_warehouse + ) + if not bundle_data: return {} @@ -540,6 +547,49 @@ class SerialandBatchBundle(Document): return valuation_details + def get_legacy_valuation_rate_for_return_entry( + self, return_against, return_against_voucher_detail_no, return_warehouse=None + ): + """Return the original line's incoming rate per serial no / batch from the SLE, for legacy receipts with no bundle.""" + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + if not (self.has_serial_no or self.has_batch_no): + return [] + + sle = frappe.qb.DocType("Stock Ledger Entry") + query = ( + frappe.qb.from_(sle) + .select(sle.serial_no, sle.batch_no, sle.incoming_rate) + .where( + (sle.voucher_no == return_against) + & (sle.voucher_detail_no == return_against_voucher_detail_no) + & (sle.item_code == self.item_code) + & (sle.is_cancelled == 0) + & (sle.serial_and_batch_bundle.isnull()) + ) + ) + + if return_warehouse: + query = query.where(sle.warehouse == return_warehouse) + + data = [] + for d in query.run(as_dict=True): + if d.serial_no: + for serial_no in get_serial_nos(d.serial_no): + data.append( + frappe._dict( + {"serial_no": serial_no, "batch_no": d.batch_no, "incoming_rate": d.incoming_rate} + ) + ) + elif d.batch_no: + data.append( + frappe._dict( + {"serial_no": None, "batch_no": d.batch_no, "incoming_rate": d.incoming_rate} + ) + ) + + return data + def calculate_total_qty(self, save=True): self.total_qty = 0.0 for d in self.entries: 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 2cee2bb6f0a..100e62deafb 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 @@ -1246,6 +1246,91 @@ class TestSerialandBatchBundle(FrappeTestCase): self.assertEqual(frappe.get_value("Serial No", serial_no, "purchase_document_no"), se1.name) + def _assert_legacy_return_valuation(self, item_code, props, batch_no=None): + """Return against a legacy serial/batch receipt (no Serial and Batch Bundle) must value outgoing stock from the original ledger rate.""" + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + + make_item(item_code, props) + if batch_no and not frappe.db.exists("Batch", batch_no): + frappe.get_doc({"doctype": "Batch", "batch_id": batch_no, "item": item_code}).insert() + + pr = make_purchase_receipt( + item_code=item_code, qty=10, rate=100, batch_no=batch_no, use_serial_batch_fields=True + ) + + # Simulate a receipt migrated from an older version: serial nos / batch tracked via the + # deprecated fields on the Stock Ledger Entry, with no Serial and Batch Bundle. + serial_nos = [] + for row in pr.items: + if row.serial_and_batch_bundle: + serial_nos = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + pluck="serial_no", + ) + frappe.db.delete("Serial and Batch Bundle", {"name": row.serial_and_batch_bundle}) + frappe.db.set_value("Purchase Receipt Item", row.name, "serial_and_batch_bundle", None) + + serial_nos = [sn for sn in serial_nos if sn] + legacy = {"serial_and_batch_bundle": None} + if batch_no: + legacy["batch_no"] = batch_no + if serial_nos: + legacy["serial_no"] = "\n".join(serial_nos) + for sle in frappe.get_all("Stock Ledger Entry", filters={"voucher_no": pr.name}, pluck="name"): + frappe.db.set_value("Stock Ledger Entry", sle, legacy) + + rt = make_return_doc("Purchase Receipt", pr.name) + rt.items[0].qty = -4 + rt.items[0].received_qty = -4 + rt.items[0].use_serial_batch_fields = 1 + if batch_no: + rt.items[0].batch_no = batch_no + if serial_nos: + rt.items[0].serial_no = "\n".join(serial_nos[:4]) + rt.submit() + + difference_in_stock_value = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": rt.name, "is_cancelled": 0, "voucher_type": "Purchase Receipt"}, + "stock_value_difference", + ) + # 4 units returned at the original ledger rate of 100 -> -400 (must not be zero) + self.assertEqual(flt(difference_in_stock_value, 2), -400.0) + + def test_return_valuation_for_legacy_batch_without_bundle(self): + self._assert_legacy_return_valuation( + "Test Legacy Batch Return Valuation", + { + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "LBRV-.#####", + "is_stock_item": 1, + }, + batch_no="LBRV-BATCH-0001", + ) + + def test_return_valuation_for_legacy_serial_without_bundle(self): + self._assert_legacy_return_valuation( + "Test Legacy Serial Return Valuation", + {"has_serial_no": 1, "serial_no_series": "LSRV-.#####", "is_stock_item": 1}, + ) + + def test_return_valuation_for_legacy_serial_and_batch_without_bundle(self): + self._assert_legacy_return_valuation( + "Test Legacy Serial Batch Return Valuation", + { + "has_serial_no": 1, + "serial_no_series": "LSBRV-.#####", + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "LSBRVB-.#####", + "is_stock_item": 1, + }, + batch_no="LSBRV-BATCH-0001", + ) + def get_batch_from_bundle(bundle): from erpnext.stock.serial_batch_bundle import get_batch_nos From 53bdccee5f79df7cf2112a70e2ec0472b6102fbc Mon Sep 17 00:00:00 2001 From: pandiyan Date: Tue, 30 Jun 2026 16:09:00 +0530 Subject: [PATCH 073/109] fix: set mr status to received when per_received is 100 even if per_ordered < 100 --- erpnext/controllers/status_updater.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 50d272d7efb..c695d17e80f 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -135,7 +135,7 @@ status_map = { ], [ "Partially Ordered", - "eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1 and self.material_request_type != 'Material Transfer'", + "eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.per_received < 100 and self.docstatus == 1 and self.material_request_type not in ['Material Transfer', 'Customer Provided']", ], ], "POS Opening Entry": [ From c2b77184554c2d6bf959cb59f7807377f372c06b Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 30 Jun 2026 20:23:14 +0530 Subject: [PATCH 074/109] fix: use correct variable to fetch valuation method (cherry picked from commit 1492c9fbc3fb7c0e8b2434d4ffeb7320838a70c6) # Conflicts: # erpnext/controllers/sales_and_purchase_return.py --- erpnext/controllers/sales_and_purchase_return.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index e1e3ba3e84e..afa4f5c6d00 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -143,7 +143,11 @@ def validate_returned_items(doc): ref.rate and flt(d.rate) > ref.rate and doc.doctype in ("Delivery Note", "Sales Invoice") +<<<<<<< HEAD and get_valuation_method(ref.item_code) != "Moving Average" +======= + and get_valuation_method(d.item_code, doc.company) != "Moving Average" +>>>>>>> 1492c9fbc3 (fix: use correct variable to fetch valuation method) ): frappe.throw( _("Row # {0}: Rate cannot be greater than the rate used in {1} {2}").format( From 04a6831645f511edb574b140d0cf91dd0aba972d Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 30 Jun 2026 20:35:44 +0530 Subject: [PATCH 075/109] chore: resolve conflicts --- erpnext/controllers/sales_and_purchase_return.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index afa4f5c6d00..c58580739e3 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -143,11 +143,7 @@ def validate_returned_items(doc): ref.rate and flt(d.rate) > ref.rate and doc.doctype in ("Delivery Note", "Sales Invoice") -<<<<<<< HEAD - and get_valuation_method(ref.item_code) != "Moving Average" -======= - and get_valuation_method(d.item_code, doc.company) != "Moving Average" ->>>>>>> 1492c9fbc3 (fix: use correct variable to fetch valuation method) + and get_valuation_method(d.item_code) != "Moving Average" ): frappe.throw( _("Row # {0}: Rate cannot be greater than the rate used in {1} {2}").format( From b5735531c15521c411d441e72f8ca08e1e23e344 Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Tue, 30 Jun 2026 21:46:03 +0530 Subject: [PATCH 076/109] refactor: port bulk_transaction_log existence check to the query builder (#56667) Co-authored-by: Mihir Kandoi Co-authored-by: Claude Opus 4.8 (1M context) --- .../doctype/bulk_transaction_log/bulk_transaction_log.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 fbe9d7fcf7d..6ffb23659c8 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 @@ -30,10 +30,7 @@ class BulkTransactionLog(Document): def load_from_db(self): log_detail = qb.DocType("Bulk Transaction Log Detail") - has_records = frappe.db.sql( - "select exists (select * from `tabBulk Transaction Log Detail` where date = %s);", - (self.name,), - )[0][0] + has_records = frappe.db.exists("Bulk Transaction Log Detail", {"date": self.name}) if not has_records: raise frappe.DoesNotExistError From 345587754f8b1b4906fe47884ee3b47a5468b97b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 16:43:26 +0000 Subject: [PATCH 077/109] fix: add permission checks in whitelisted functions (backport #53103) (#56668) * fix: add permission checks in whitelisted functions (#53103) (cherry picked from commit a6e78c2eea71df011fbb59ca1cafeda15f96bd78) # Conflicts: # erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py * chore: resolve conflict --------- Co-authored-by: Priyal Rawal <135015851+Priyal208@users.noreply.github.com> Co-authored-by: Diptanil Saha --- .../bank_reconciliation_tool.py | 3 +- .../payment_request/payment_request.py | 1 + .../process_statement_of_accounts.py | 3 ++ .../stock_qty_vs_batch_qty.js | 44 ++++++++++--------- .../stock_qty_vs_batch_qty.py | 1 + 5 files changed, 30 insertions(+), 22 deletions(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 9ea87ef0ae7..f249cf9c19d 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -57,7 +57,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None): filters.append(["date", "<=", to_date]) if from_date: filters.append(["date", ">=", from_date]) - transactions = frappe.get_all( + transactions = frappe.get_list( "Bank Transaction", fields=[ "date", @@ -82,6 +82,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None): @frappe.whitelist() def get_account_balance(bank_account, till_date, company): # returns account balance till the specified date + frappe.has_permission("Bank Account", "read", bank_account, throw=True) account = frappe.db.get_value("Bank Account", bank_account, "account") filters = frappe._dict( { diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index e341490ed7f..f13569e0d9b 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -834,6 +834,7 @@ def resend_payment_email(docname): @frappe.whitelist() def make_payment_entry(docname): doc = frappe.get_doc("Payment Request", docname) + doc.check_permission("read") return doc.create_payment_entry(submit=False).as_dict() diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 48349a4bd99..9f0680de3ee 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -439,6 +439,8 @@ def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=Tr when Is Billing Contact checked and Primary email- email with Is Primary checked""" + frappe.has_permission("Customer", "read", customer_name, throw=True) + billing_email = frappe.db.sql( """ SELECT @@ -482,6 +484,7 @@ def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=Tr @frappe.whitelist() def download_statements(document_name): doc = frappe.get_doc("Process Statement Of Accounts", document_name) + doc.check_permission("read") report = get_report_pdf(doc) if report: frappe.local.response.filename = doc.name + ".pdf" diff --git a/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.js b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.js index f80126bcb0a..40ad8843871 100644 --- a/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.js +++ b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.js @@ -28,28 +28,30 @@ frappe.query_reports["Stock Qty vs Batch Qty"] = { }, ], onload: function (report) { - report.page.add_inner_button(__("Update Batch Qty"), function () { - let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows(); - let selected_rows = indexes - .map((i) => frappe.query_report.data[i]) - .filter((row) => row.difference != 0); + if (frappe.model.can_write("Batch")) { + report.page.add_inner_button(__("Update Batch Qty"), function () { + let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows(); + let selected_rows = indexes + .map((i) => frappe.query_report.data[i]) + .filter((row) => row.difference != 0); - if (selected_rows.length) { - frappe.call({ - method: "erpnext.stock.report.stock_qty_vs_batch_qty.stock_qty_vs_batch_qty.update_batch_qty", - args: { - selected_batches: selected_rows, - }, - callback: function (r) { - if (!r.exc) { - report.refresh(); - } - }, - }); - } else { - frappe.msgprint(__("Please select at least one row with difference value")); - } - }); + if (selected_rows.length) { + frappe.call({ + method: "erpnext.stock.report.stock_qty_vs_batch_qty.stock_qty_vs_batch_qty.update_batch_qty", + args: { + selected_batches: selected_rows, + }, + callback: function (r) { + if (!r.exc) { + report.refresh(); + } + }, + }); + } else { + frappe.msgprint(__("Please select at least one row with difference value")); + } + }); + } }, formatter: function (value, row, column, data, default_formatter) { diff --git a/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py index 87c5e1419cc..e9ccde483ae 100644 --- a/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py +++ b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py @@ -101,6 +101,7 @@ def get_data(filters=None): @frappe.whitelist() def update_batch_qty(selected_batches=None): + frappe.has_permission("Batch", "write", throw=True, ignore_share_permissions=True) if not selected_batches: return From 17733a564159ddf220fc58ba91ac53e48d6a0841 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Wed, 1 Jul 2026 08:02:45 +0530 Subject: [PATCH 078/109] fix: gross profit calculation with rate adjustment entries (cherry picked from commit b9f330a1589baa03acc60ba08fbda9874e5bc9b5) --- erpnext/accounts/report/gross_profit/gross_profit.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 21f999197c2..8ecfe51d244 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -562,7 +562,12 @@ class GrossProfitGenerator: row.base_amount = packed_item.base_amount # get buying amount - if row.item_code in product_bundles: + if row.is_debit_note: + # Rate adjustment debit notes have no stock movement, so buying amount is zero + if not grouped_by_invoice: + row.qty = 0 + row.buying_amount = 0 + elif row.item_code in product_bundles: row.buying_amount = flt( self.get_buying_amount_from_product_bundle(row, product_bundles[row.item_code]), self.currency_precision, @@ -925,6 +930,7 @@ class GrossProfitGenerator: SalesInvoice.customer_group, SalesInvoice.customer_name, SalesInvoice.territory, + SalesInvoice.is_debit_note, SalesInvoiceItem.item_code, SalesInvoice.base_net_total.as_("invoice_base_net_total"), SalesInvoiceItem.item_name, @@ -1104,6 +1110,7 @@ class GrossProfitGenerator: "posting_time": row.posting_time, "project": row.project, "update_stock": row.update_stock, + "is_debit_note": row.is_debit_note, "customer": row.customer, "customer_group": row.customer_group, "customer_name": row.customer_name, @@ -1142,6 +1149,7 @@ class GrossProfitGenerator: "description": item.description, "warehouse": item.warehouse or row.warehouse, "update_stock": row.update_stock, + "is_debit_note": row.is_debit_note, "item_group": "", "brand": "", "dn_detail": row.dn_detail, From fa82da509ba3a9572d31a0b68cbd005cc06d9ca2 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Wed, 1 Jul 2026 08:17:42 +0530 Subject: [PATCH 079/109] test(gross_profit): added test cases for rate adjustment entry (cherry picked from commit 17ef5d6034b5dd2191813b3e57201e02fd65b0dd) --- .../report/gross_profit/test_gross_profit.py | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/erpnext/accounts/report/gross_profit/test_gross_profit.py b/erpnext/accounts/report/gross_profit/test_gross_profit.py index 9a0a9cc5174..d24d472710d 100644 --- a/erpnext/accounts/report/gross_profit/test_gross_profit.py +++ b/erpnext/accounts/report/gross_profit/test_gross_profit.py @@ -727,6 +727,160 @@ class TestGrossProfit(FrappeTestCase): self.assertEqual(total[7], 1000.0) self.assertEqual(total[8], 100.0) + def create_rate_adjustment_debit_note(self, against_invoice, adjustment_rate, item_code=None): + """Create a rate adjustment debit note with no stock movement.""" + dn = self.create_sales_invoice(qty=1, rate=adjustment_rate, do_not_save=True, do_not_submit=True) + if item_code: + dn.items[0].item_code = item_code + dn.items[0].item_name = item_code + dn.is_debit_note = 1 + dn.return_against = against_invoice.name + dn.items[0].allow_zero_valuation_rate = 1 + return dn.save().submit() + + def test_debit_note_has_zero_buying_amount_and_full_gross_profit(self): + """ + Rate adjustment debit note (is_debit_note=1) should show buying_amount=0 + since there is no stock movement. Gross profit equals the adjustment amount + and gross profit % equals 100%. + """ + make_stock_entry( + company=self.company, + item_code=self.item, + target=self.warehouse, + qty=1, + basic_rate=100, + ) + + sinv = self.create_sales_invoice(qty=1, rate=200, do_not_submit=True) + sinv.update_stock = 1 + sinv = sinv.save().submit() + + debit_note = self.create_rate_adjustment_debit_note(sinv, adjustment_rate=20) + + filters = frappe._dict( + company=self.company, + from_date=nowdate(), + to_date=nowdate(), + group_by="Invoice", + ) + + columns, data = execute(filters=filters) + + dn_item_rows = [ + x for x in data if x.get("parent_invoice") == debit_note.name and x.get("indent") == 1.0 + ] + self.assertEqual(len(dn_item_rows), 1) + + dn_row = dn_item_rows[0] + self.assertEqual(dn_row.buying_amount, 0.0) + self.assertEqual(dn_row.selling_amount, 20.0) + self.assertEqual(dn_row.gross_profit, 20.0) + self.assertEqual(dn_row["gross_profit_%"], 100.0) + + def test_original_invoice_unaffected_by_rate_adjustment_debit_note(self): + """ + The original invoice's GP should be derived solely from its own selling + amount and COGS — the rate adjustment debit note must not alter it. + """ + make_stock_entry( + company=self.company, + item_code=self.item, + target=self.warehouse, + qty=1, + basic_rate=100, + ) + + sinv = self.create_sales_invoice(qty=1, rate=200, do_not_submit=True) + sinv.update_stock = 1 + sinv = sinv.save().submit() + + self.create_rate_adjustment_debit_note(sinv, adjustment_rate=20) + + filters = frappe._dict( + company=self.company, + from_date=nowdate(), + to_date=nowdate(), + group_by="Invoice", + ) + + columns, data = execute(filters=filters) + + sinv_item_rows = [x for x in data if x.get("parent_invoice") == sinv.name and x.get("indent") == 1.0] + self.assertEqual(len(sinv_item_rows), 1) + + sinv_row = sinv_item_rows[0] + self.assertEqual(sinv_row.selling_amount, 200.0) + self.assertEqual(sinv_row.buying_amount, 100.0) + self.assertEqual(sinv_row.gross_profit, 100.0) + self.assertEqual(sinv_row["gross_profit_%"], 50.0) + + def test_debit_note_qty_not_inflated_in_grouped_report(self): + """ + When grouped by Item Code, the debit note (qty=0) must not inflate + the group's qty or buying_amount. The selling amount and average + selling rate correctly reflect the rate adjustment. + """ + item = create_item("_Test Rate Adjustment Debit Note Item") + + make_stock_entry( + company=self.company, + item_code=item.item_code, + target=self.warehouse, + qty=1, + basic_rate=100, + ) + + sinv = create_sales_invoice( + qty=1, + rate=200, + company=self.company, + customer=self.customer, + item_code=item.item_code, + item_name=item.item_code, + cost_center=self.cost_center, + warehouse=self.warehouse, + debit_to=self.debit_to, + parent_cost_center=self.cost_center, + update_stock=1, + currency="INR", + income_account=self.income_account, + expense_account=self.expense_account, + ) + + self.create_rate_adjustment_debit_note(sinv, adjustment_rate=20, item_code=item.item_code) + + filters = frappe._dict( + company=self.company, + from_date=nowdate(), + to_date=nowdate(), + group_by="Item Code", + ) + + columns, data = execute(filters=filters) + + # group_by="Item Code" column order: + # [item_code, item_name, brand, description, qty, base_rate, + # buying_rate, base_amount, buying_amount, gross_profit, gross_profit_percent, currency] + item_row = next((row for row in data if row[0] == item.item_code), None) + self.assertIsNotNone(item_row) + + qty, base_rate, buying_amount, base_amount, gross_profit, gp_percent = ( + item_row[4], + item_row[5], + item_row[8], + item_row[7], + item_row[9], + item_row[10], + ) + + self.assertEqual(qty, 1.0) # debit note adds qty=0, not inflated + self.assertEqual(buying_amount, 100.0) # only original invoice COGS + self.assertEqual(base_amount, 220.0) # 200 (original) + 20 (adjustment) + self.assertEqual(base_rate, 220.0) # avg selling rate = 220/1 + self.assertEqual(gross_profit, 120.0) # 220 - 100 + self.assertAlmostEqual(gp_percent, 54.545, places=2) # 120/220 * 100 + def make_sales_person(sales_person_name="_Test Sales Person"): if not frappe.db.exists("Sales Person", {"sales_person_name": sales_person_name}): From 63e85c655182982d35012e50c4b8a9d4ab5f002c Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Tue, 30 Jun 2026 22:22:17 +0530 Subject: [PATCH 080/109] fix(selling): update sales order per billed on credit note submission (cherry picked from commit 224cf19f3331976178e89990fe92e12ad0db633f) --- erpnext/stock/doctype/delivery_note/delivery_note.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 0ad8bc781a5..1f119e9aa50 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -9,6 +9,7 @@ from frappe import _ from frappe.contacts.doctype.address.address import get_company_address from frappe.contacts.doctype.contact.contact import get_default_contact from frappe.desk.notifications import clear_doctype_notifications +from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc from frappe.model.utils import get_fetch_values from frappe.query_builder import DocType @@ -910,7 +911,9 @@ def get_returned_qty_map(delivery_note): @frappe.whitelist() -def make_sales_invoice(source_name, target_doc=None, args=None): +def make_sales_invoice( + source_name: str, target_doc: Document | str | None = None, args: dict | str | None = None +): if args is None: args = {} if isinstance(args, str): @@ -1015,7 +1018,12 @@ def make_sales_invoice(source_name, target_doc=None, args=None): frappe.db.get_single_value("Accounts Settings", "automatically_fetch_payment_terms") ) - if not doc.is_return: + if doc.is_return: + # A credit note made from a return Delivery Note should roll back the billed + # amount on the linked Sales Order too, so that per_billed stays consistent with + # per_delivered (which the return already reset). + doc.update_billed_amount_in_sales_order = True + else: so, doctype, fieldname = doc.get_order_details() if ( doc.linked_order_has_payment_terms(so, fieldname, doctype) From a6d790f321245574ef9369c246219f2f28f5c11a Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Tue, 30 Jun 2026 22:23:43 +0530 Subject: [PATCH 081/109] test(selling): add test to validate the per billed after credit note submission (cherry picked from commit 710e0216382f4c596da2c074c103ec8c4d591f1e) --- .../delivery_note/test_delivery_note.py | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 90bae7f68f5..e77940b1661 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -2599,6 +2599,92 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(dn.per_returned, 100) self.assertEqual(returned.status, "Return") + def _assert_credit_note_from_return_dn_resets_per_billed(self, so, dn): + """Given a fully billed Sales Order and a submitted Delivery Note that delivers it, + a credit note made from the return of that Delivery Note must reset per_billed to 0 + while leaving the delivery quantities exactly as the return already set them.""" + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + so.load_from_db() + self.assertEqual(so.per_delivered, 100) + self.assertEqual(so.per_billed, 100) + + return_dn = make_sales_return(dn.name) + return_dn.insert() + return_dn.submit() + + # the return reverses the delivery quantities + so.load_from_db() + self.assertEqual(so.per_delivered, 0) + self.assertEqual(so.items[0].delivered_qty, 0) + + credit_note = make_sales_invoice(return_dn.name) + self.assertTrue(credit_note.is_return) + self.assertTrue(credit_note.update_billed_amount_in_sales_order) + # A Delivery Note-linked invoice can't update stock (validate_delivery_note), so the + # credit note only rolls back billing and never re-reverses the delivery quantities. + self.assertFalse(credit_note.update_stock) + credit_note.insert() + credit_note.submit() + + # per_billed is reset, and the delivery state stays exactly as the return left it + so.load_from_db() + self.assertEqual(so.per_billed, 0) + self.assertEqual(so.per_delivered, 0) + self.assertEqual(so.items[0].delivered_qty, 0) + self.assertEqual(so.items[0].returned_qty, 0) + + # Cancelling the credit note should restore the billed amount on the Sales Order. + credit_note.cancel() + so.load_from_db() + self.assertEqual(so.per_billed, 100) + + def test_sales_order_per_billed_after_credit_note_from_return_dn(self): + # Reported flow: SO -> SI (from SO) -> DN (from SI) -> return DN -> credit note. + # The DN carries si_detail in this path. + from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note + from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice as make_si_from_so + + make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=10, basic_rate=100) + + so = make_sales_order(qty=2) + + si = make_si_from_so(so.name) + si.insert() + si.submit() + + dn = make_delivery_note(si.name) + dn.insert() + dn.submit() + + self._assert_credit_note_from_return_dn_resets_per_billed(so, dn) + + def test_sales_order_per_billed_after_credit_note_from_so_derived_dn(self): + # SO billed and delivered separately (SO -> SI, SO -> DN), then return DN -> credit note. + # SO per_billed rolls back via the status_updater in update_prevdoc_status. + from erpnext.selling.doctype.sales_order.sales_order import ( + make_delivery_note as make_dn_from_so, + ) + from erpnext.selling.doctype.sales_order.sales_order import ( + make_sales_invoice as make_si_from_so, + ) + + make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=10, basic_rate=100) + + so = make_sales_order(qty=2) + + si = make_si_from_so(so.name) + si.insert() + si.submit() + + dn = make_dn_from_so(so.name) + dn.insert() + dn.submit() + + self.assertIsNone(dn.items[0].si_detail) + + self._assert_credit_note_from_return_dn_resets_per_billed(so, dn) + def test_sales_return_for_product_bundle(self): from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return From 38b7042481bf6ad654e1ca416a90b58b04952694 Mon Sep 17 00:00:00 2001 From: Pandiyan P Date: Wed, 3 Jun 2026 13:10:26 +0530 Subject: [PATCH 082/109] fix(stock): set stock received but not billed account for purchase (#55149) (cherry picked from commit c4d28a261271c69853efb51ecd481b9aa94fd961) --- erpnext/stock/get_item_details.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 8c205a10874..e342396d631 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -12,6 +12,7 @@ from frappe.model.utils import get_fetch_values from frappe.query_builder.functions import IfNull, Sum from frappe.utils import add_days, add_months, cint, cstr, flt, get_link_to_form, getdate, parse_json +import erpnext from erpnext import get_company_currency from erpnext.accounts.doctype.pricing_rule.pricing_rule import ( get_pricing_rule_for_item, @@ -518,10 +519,21 @@ def get_basic_details(args, item, overwrite_warehouse=True): args.name, args.conversion_rate, item.name, out.conversion_factor ) + expense_account_field = "default_expense_account" + if ( + item.is_stock_item + and erpnext.is_perpetual_inventory_enabled(args.company) + and ( + args.doctype == "Purchase Receipt" + or (args.doctype == "Purchase Invoice" and args.get("update_stock")) + ) + ): + expense_account_field = "stock_received_but_not_billed" + # if default specified in item is for another company, fetch from company for d in [ ["Account", "income_account", "default_income_account"], - ["Account", "expense_account", "default_expense_account"], + ["Account", "expense_account", expense_account_field], ["Cost Center", "cost_center", "cost_center"], ["Warehouse", "warehouse", ""], ]: From f13946685136736fc154263ad44a551669861a6e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 2 Jul 2026 11:54:27 +0530 Subject: [PATCH 083/109] refactor: update title for process statement of accounts (cherry picked from commit 04468c3c33b37201810e742ee1ae8cdec2a24c74) --- .../process_statement_of_accounts.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html index cd1e357e3bc..c60de4c29c7 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html @@ -13,7 +13,7 @@
{% endif %} -

{{ _("GENERAL LEDGER") }}

+

{{ _("STATEMENT OF ACCOUNTS") }}

{% if filters.party[0] == filters.party_name[0] %}
{{ _("Customer: ") }} {{ filters.party_name[0] }}
From 71a2d6e43dbd957f41e242a021831bb9e0bff26f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:27:20 +0530 Subject: [PATCH 084/109] fix: validate reverse GL entries on current date under immutable ledger (backport #56709) (#56751) * fix: validate reverse GL entries on current date under immutable ledger (#56709) * fix: validate reverse GL entries on current date under immutable ledger When Immutable Ledger is enabled, the reverse GL entry is posted on the current date, but the closed-period checks in make_reverse_gl_entries still validate against the original (backdated) posting date. This blocks cancelling a backdated voucher, such as a suspense Journal Entry for a migrated NPA loan, with a books-closed error even though the reverse entry lands in an open period. Validate both check_freezing_date and validate_against_pcv against the current date when Immutable Ledger is enabled. When it is disabled, behaviour is unchanged. Follow-up to #55268. * test: reset frozen till date after reverse entry test The freeze date set on the company was not reset, so it leaked into the next test which posts entries in that period. Reset it in a finally block. * fix: prefer explicit posting_date under immutable ledger Prefer the posting_date argument before frappe.form_dict and getdate, at both the validation and the GL entry site, so an explicit date passed by the caller is honoured and validation still matches the posted date. (cherry picked from commit cab1b129c064bdbef8e01763f0f1201495d0d233) # Conflicts: # erpnext/accounts/general_ledger.py * fix: resolved conflicts Removed outdated check for freezing date in general ledger validation. * fix: Update check_freezing_date function call parameters * fix: accounts_frozen_till_date to acc_frozen_upto * test: acc_frozen_upto setting in test_period_closing_voucher --------- Co-authored-by: Nihantra C. Patel <141945075+Nihantra-Patel@users.noreply.github.com> --- .../test_period_closing_voucher.py | 15 +++++++++------ erpnext/accounts/general_ledger.py | 12 +++++++----- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py index e4e31a9adf4..e9bad6d7494 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py @@ -379,12 +379,15 @@ class TestPeriodClosingVoucher(unittest.TestCase): self.make_period_closing_voucher(posting_date="2021-03-31") - # Passed posting_date is after PCV end date, so cancellation should not fail. - make_reverse_gl_entries( - voucher_type="Journal Entry", - voucher_no=jv.name, - posting_date="2022-01-01", - ) + frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", "2021-12-31") + + try: + make_reverse_gl_entries( + voucher_type="Journal Entry", + voucher_no=jv.name, + ) + finally: + frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) totals_after_cancel = frappe.db.sql( """ diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 599173c99f5..38242a57eae 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -697,13 +697,15 @@ def make_reverse_gl_entries( partial_cancel=partial_cancel, ) validate_accounting_period(gl_entries) - check_freezing_date(gl_entries[0]["posting_date"], adv_adj) is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries) - # For reverse entries, use the posting_date parameter if provided and valid - # Otherwise fall back to original posting_date - validation_date = posting_date if posting_date else gl_entries[0]["posting_date"] + if immutable_ledger_enabled: + validation_date = posting_date or frappe.form_dict.get("posting_date") or getdate() + else: + validation_date = posting_date if posting_date else gl_entries[0]["posting_date"] + + check_freezing_date(validation_date, adv_adj) validate_against_pcv(is_opening, validation_date, gl_entries[0]["company"]) if partial_cancel: @@ -770,7 +772,7 @@ def make_reverse_gl_entries( if immutable_ledger_enabled: new_gle["is_cancelled"] = 0 - new_gle["posting_date"] = frappe.form_dict.get("posting_date") or getdate() + new_gle["posting_date"] = posting_date or frappe.form_dict.get("posting_date") or getdate() elif posting_date: new_gle["posting_date"] = posting_date From a9330e8900c6384558bc2ff833ed8c5300eb525a Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 2 Jul 2026 12:25:52 +0530 Subject: [PATCH 085/109] fix: don't treat batch slot at FIFO queue head as qty slot An incoming SLE without resolvable serial/batch details hit the negative-head branch in _compute_incoming_stock even when the head was a batch slot, because flt() on the batch number string returns 0.0. _add_to_negative_fifo_head then crashed with "TypeError: can only concatenate str (not 'float') to str". Guard the branch with is_qty_slot, mirroring the existing check in _add_transfer_slot_to_fifo_queue. Co-Authored-By: Claude Fable 5 (cherry picked from commit c47a95a4d259863baa911f6161b5128df95e42d4) --- .../stock/report/stock_ageing/stock_ageing.py | 2 +- .../report/stock_ageing/test_stock_ageing.py | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 2e11fa1664b..b9ec3e7204f 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -492,7 +492,7 @@ class FIFOSlots: self._add_serial_fifo_slots(row, fifo_queue, serial_nos) elif batch_nos and row.get("has_batch_no"): self._add_batch_fifo_slots(row, fifo_queue, batch_nos) - elif fifo_queue and flt(fifo_queue[0][FIFO_QTY_INDEX]) <= 0: + elif fifo_queue and is_qty_slot(fifo_queue[0]) and flt(fifo_queue[0][FIFO_QTY_INDEX]) <= 0: self._add_to_negative_fifo_head(row, fifo_queue) else: fifo_queue.append([flt(row.actual_qty), row.posting_date, flt(row.stock_value_difference)]) diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 003d1a51d93..b21a89572c4 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -1438,6 +1438,47 @@ class TestStockAgeing(FrappeTestCase): self.assertEqual(item_result["total_qty"], -4.0) self.assertEqual(item_result["fifo_queue"], [[batch_no, 1, -4.0, "2021-11-10", -40.0]]) + def test_untagged_receipt_with_negative_batch_head(self): + """An incoming SLE without batch details must not treat a negative + batch slot at the queue head as a qty slot (TypeError: str += float).""" + sle = [ + frappe._dict( + name="Enclosure Item", + actual_qty=-10, + qty_after_transaction=-10, + stock_value_difference=-100, + warehouse="WH 1", + posting_date="2021-12-01", + voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, + has_batch_no=True, + serial_no=None, + batch_no="QI-06448", + ), + frappe._dict( + name="Enclosure Item", + actual_qty=45, + qty_after_transaction=35, + stock_value_difference=1051.65, + warehouse="WH 1", + posting_date="2021-12-05", + voucher_type="Purchase Receipt", + voucher_no="002", + has_serial_no=False, + serial_no=None, + batch_no=None, + serial_and_batch_bundle="SABB-00001294", + ), + ] + + slots = FIFOSlots(self.filters, sle).generate() + queue = slots["Enclosure Item"]["fifo_queue"] + + self.assertEqual(slots["Enclosure Item"]["total_qty"], 35.0) + self.assertEqual(queue[0][:3], ["QI-06448", None, -10.0]) + self.assertEqual(queue[1], [45.0, "2021-12-05", 1051.65]) + def test_batchwise_valuation_stock_reconciliation_with_bundle(self): from frappe.utils import add_days, getdate, nowdate From 4a6587b48eea1dcb55f323dd2d4606c5bce9c875 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 2 Jul 2026 12:31:24 +0530 Subject: [PATCH 086/109] test: assert full negative batch slot in ageing regression test Co-Authored-By: Claude Fable 5 (cherry picked from commit 8928b42d5d7952053aa191cfe9321015b8a102a1) --- erpnext/stock/report/stock_ageing/test_stock_ageing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index b21a89572c4..54f9ef9241e 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -1476,7 +1476,7 @@ class TestStockAgeing(FrappeTestCase): queue = slots["Enclosure Item"]["fifo_queue"] self.assertEqual(slots["Enclosure Item"]["total_qty"], 35.0) - self.assertEqual(queue[0][:3], ["QI-06448", None, -10.0]) + self.assertEqual(queue[0], ["QI-06448", None, -10.0, "2021-12-01", -100.0]) self.assertEqual(queue[1], [45.0, "2021-12-05", 1051.65]) def test_batchwise_valuation_stock_reconciliation_with_bundle(self): From 5f14f2ccd6b57c2c3d682ac3fe6efee75893a757 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 12:54:27 +0530 Subject: [PATCH 087/109] fix(accounts): clear clearance date when amending reconciled voucher (backport #55947) When a reconciled voucher (Payment Entry / Journal Entry / Purchase Invoice / Sales Invoice) is cancelled, amended and resubmitted, the stale clearance date was carried into the amended draft because the framework ignores `no_copy` while amending. Add a shared `before_insert` hook on AccountsController that clears `clearance_date` (top-level field and Sales Invoice `payments` rows) on amendment. Reconciliation still sets the value post-submit via `frappe.db.set_value`, which bypasses this path. Manual backport of #55947 to version-15-hotfix (Mergify's auto-backport #55972 committed conflict markers; this replaces it). --- .../bank_transaction/test_bank_transaction.py | 30 +++++++++++++++++++ erpnext/controllers/accounts_controller.py | 20 +++++++++++++ 2 files changed, 50 insertions(+) diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index 4294c4462b1..05a9c055078 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -115,6 +115,36 @@ class TestBankTransaction(FrappeTestCase): self.assertEqual(bank_transaction.unallocated_amount, 1700) self.assertEqual(bank_transaction.payment_entries, []) + # Amending a reconciled payment entry must not carry over its clearance date + def test_clearance_date_cleared_on_amend(self): + bank_transaction = frappe.get_doc( + "Bank Transaction", + dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"), + ) + payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700)) + vouchers = json.dumps( + [ + { + "payment_doctype": "Payment Entry", + "payment_name": payment.name, + "amount": bank_transaction.unallocated_amount, + } + ] + ) + reconcile_vouchers(bank_transaction.name, vouchers) + + self.assertTrue(frappe.db.get_value("Payment Entry", payment.name, "clearance_date")) + + payment.reload() + payment.cancel() + + amended = frappe.copy_doc(payment) + amended.amended_from = payment.name + amended.docstatus = 0 + amended.insert() + + self.assertFalse(amended.clearance_date) + # Check if ERPNext can correctly filter a linked payments based on the debit/credit amount def test_debit_credit_output(self): bank_transaction = frappe.get_doc( diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 0d3f13dde8c..365e481890f 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -141,6 +141,26 @@ class AccountsController(TransactionBase): if self.doctype in relevant_docs: self.set_payment_schedule() + def before_insert(self): + self.clear_clearance_date_on_amend() + + def clear_clearance_date_on_amend(self): + """Drop the bank reconciliation clearance date copied over while amending. + + The framework copies `no_copy` fields when amending, so a reconciled + voucher would carry a stale clearance date into its amendment even though + the linked bank transaction gets unreconciled on cancellation. + """ + if not self.get("amended_from"): + return + + if self.meta.has_field("clearance_date"): + self.clearance_date = None + + for payment in self.get("payments") or []: + if payment.meta.has_field("clearance_date"): + payment.clearance_date = None + def remove_bundle_for_non_stock_invoices(self): has_sabb = False if self.doctype in ("Sales Invoice", "Purchase Invoice") and not self.update_stock: From ac2e69fbfd628abcb8f47bd14c5fb9b1771abcc5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 8 May 2026 12:27:32 +0530 Subject: [PATCH 088/109] fix: disallow editing on reversal journals (cherry picked from commit 26ca7445eb8c122351833105f7d4dcf57cb245c2) --- erpnext/accounts/doctype/journal_entry/journal_entry.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index ae3ee00e535..232c33d4def 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -40,6 +40,10 @@ frappe.ui.form.on("Journal Entry", { }, refresh: function (frm) { + if (frm.doc.reversal_of && (frm.is_new() || frm.doc.docstatus == 0)) { + frm.set_read_only(); + } + erpnext.toggle_naming_series(); if (frm.doc.docstatus > 0) { From 44a7013ab45c4567da37c8126f1a050e877e88f7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:26:04 +0000 Subject: [PATCH 089/109] fix: block serialized to non-serialized item change when SABB exists (backport #56773) (#56774) * fix: block serialized to non-serialized item change when SABB exists (#56773) (cherry picked from commit 0e8ae7548d12dbee6b50a02730d962f30b89765e) # Conflicts: # erpnext/stock/doctype/item/item.py # erpnext/stock/doctype/item/test_item.py * chore: fix conflicts * chore: fix conflicts Remove test for variant UOM mismatch and related logic. --------- Co-authored-by: rohitwaghchaure --- erpnext/stock/doctype/item/item.py | 20 ++++++++++++ erpnext/stock/doctype/item/test_item.py | 41 +++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 05e8a2f3779..7f077cfd4dd 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -217,6 +217,7 @@ class Item(Document): self.validate_item_defaults() self.validate_auto_reorder_enabled_in_stock_settings() self.cant_change() + self.validate_serialized_change_with_bundle() self.validate_item_tax_net_rate_range() if not self.is_new(): @@ -1074,6 +1075,25 @@ class Item(Document): frappe.throw(msg, title=_("Linked with submitted documents")) + def validate_serialized_change_with_bundle(self): + """Block turning a serialized item non-serialized while any Serial and Batch Bundle still exists + for it. Such bundles carry the item's serial numbers; the user must delete or cancel them first.""" + if self.is_new() or self.has_serial_no or not self._doc_before_save: + return + + # Only relevant when the item was serialized before and is now being unset. + if not self._doc_before_save.has_serial_no: + return + + # Draft (docstatus 0) or submitted (docstatus 1) bundles block the change; cancelled ones don't. + if frappe.db.count("Serial and Batch Bundle", {"item_code": self.name, "docstatus": ("<", 2)}): + frappe.throw( + _( + "Cannot change Item {0} from serialized to non-serialized because a Serial and Batch Bundle exists for it. Please delete or cancel the Serial and Batch Bundle first." + ).format(frappe.bold(self.name)), + title=_("Serial and Batch Bundle Exists"), + ) + def _get_linked_submitted_documents(self, changed_fields: list[str]) -> dict[str, str] | None: linked_doctypes = [ "Delivery Note Item", diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 8072437a173..a2319ca9488 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -966,6 +966,47 @@ class TestItem(FrappeTestCase): self.assertRaises(frappe.ValidationError, item_doc.save) + def test_cannot_unset_serialized_while_bundle_exists(self): + from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import ( + make_serial_batch_bundle, + ) + + item = make_item( + properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "TSN-UNSET-.####"} + ).name + + serial_no = f"{item}-SN-01" + frappe.get_doc( + {"doctype": "Serial No", "serial_no": serial_no, "item_code": item, "company": "_Test Company"} + ).insert() + + # A draft (unsubmitted) Serial and Batch Bundle for the item must block the change. + bundle = make_serial_batch_bundle( + { + "item_code": item, + "warehouse": "_Test Warehouse - _TC", + "company": "_Test Company", + "qty": 1, + "rate": 100, + "voucher_type": "Stock Entry", + "serial_nos": [serial_no], + "type_of_transaction": "Inward", + "do_not_submit": True, + "ignore_sabb_validation": True, + } + ) + + doc = frappe.get_doc("Item", item) + doc.has_serial_no = 0 + self.assertRaises(frappe.ValidationError, doc.save) + + # Once the bundle is removed, the item can be made non-serialized. + frappe.delete_doc("Serial and Batch Bundle", bundle.name, force=True) + doc = frappe.get_doc("Item", item) + doc.has_serial_no = 0 + doc.save() + self.assertEqual(frappe.db.get_value("Item", item, "has_serial_no"), 0) + def set_item_variant_settings(fields): doc = frappe.get_doc("Item Variant Settings") From b7e0331d56bc6aaa7bbc4df9aa73137ce5c9a7b5 Mon Sep 17 00:00:00 2001 From: jatin3128 Date: Thu, 2 Jul 2026 16:38:02 +0530 Subject: [PATCH 090/109] fix: fetch asset category expense account on purchase receipt Backport of #41024. Fixed-asset items now resolve expense_account from the asset category (CWIP account when CWIP accounting is enabled, otherwise the fixed asset account) for Purchase Receipt, Purchase Order and Material Request, matching v16 behavior. v15 previously did this only for Purchase Invoice. --- erpnext/stock/get_item_details.py | 24 +++++++--- erpnext/stock/tests/test_get_item_details.py | 46 ++++++++++++++++++++ 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 312b8e129f8..35fd63ddcd2 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -410,12 +410,26 @@ def get_basic_details(args, item, overwrite_warehouse=True): expense_account = None - if args.get("doctype") == "Purchase Invoice" and item.is_fixed_asset: - from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account + if item.is_fixed_asset: + from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled - expense_account = get_asset_category_account( - fieldname="fixed_asset_account", item=args.item_code, company=args.company - ) + if is_cwip_accounting_enabled(item.asset_category): + expense_account = get_asset_account( + "capital_work_in_progress_account", + asset_category=item.asset_category, + company=args.company, + ) + elif args.get("doctype") in ( + "Purchase Invoice", + "Purchase Receipt", + "Purchase Order", + "Material Request", + ): + from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account + + expense_account = get_asset_category_account( + fieldname="fixed_asset_account", item=args.item_code, company=args.company + ) # Set the UOM to the Default Sales UOM or Default Purchase UOM if configured in the Item Master if not args.get("uom"): diff --git a/erpnext/stock/tests/test_get_item_details.py b/erpnext/stock/tests/test_get_item_details.py index fc19bac0a44..af98aa43980 100644 --- a/erpnext/stock/tests/test_get_item_details.py +++ b/erpnext/stock/tests/test_get_item_details.py @@ -35,6 +35,52 @@ class TestGetItemDetail(FrappeTestCase): details = get_item_details(args) self.assertEqual(details.get("price_list_rate"), 100) + def test_fetch_asset_category_expense_account_on_purchase_receipt(self): + from erpnext.stock.doctype.item.test_item import make_item + + asset_category = "Test Expense Account Asset Category" + if not frappe.db.exists("Asset Category", asset_category): + frappe.get_doc( + { + "doctype": "Asset Category", + "asset_category_name": asset_category, + "enable_cwip_accounting": 0, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 12, + "frequency_of_depreciation": 1, + "accounts": [ + { + "company_name": "_Test Company", + "fixed_asset_account": "_Test Fixed Asset - _TC", + "accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC", + "depreciation_expense_account": "_Test Depreciations - _TC", + } + ], + } + ).insert() + + asset_item = make_item( + "Test Expense Account Asset Item", + {"is_stock_item": 0, "is_fixed_asset": 1, "asset_category": asset_category}, + ).item_code + + args = frappe._dict( + { + "item_code": asset_item, + "company": "_Test Company", + "conversion_rate": 1.0, + "price_list_currency": "USD", + "plc_conversion_rate": 1.0, + "doctype": "Purchase Receipt", + "supplier": "_Test Supplier", + "price_list": "_Test Buying Price List", + "ignore_pricing_rule": 1, + "qty": 1, + } + ) + details = get_item_details(args) + self.assertEqual(details.get("expense_account"), "_Test Fixed Asset - _TC") + # making this test in get_item_details test file as feat/fix is present in that method def test_fetch_price_from_list_rate_on_doc_save(self): # create item From 5666fd13343ad2bc4682b9938e8f5957487d94e8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 13:02:09 +0000 Subject: [PATCH 091/109] fix(company): ignore user permissions for link fields having link to `Account` and `Cost Center` (backport #56748) (#56794) Co-authored-by: Diptanil Saha --- erpnext/setup/doctype/company/company.json | 45 +++++++++++++++++++++- erpnext/setup/doctype/company/company.py | 1 + 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index fc6533a1e89..380320f0399 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -330,33 +330,48 @@ "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "round_off_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Round Off Account", + "no_copy": 1, "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "round_off_cost_center", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Round Off Cost Center", + "no_copy": 1, "options": "Cost Center" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "write_off_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Write Off Account", + "no_copy": 1, "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "exchange_gain_loss_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Exchange Gain / Loss Account", + "no_copy": 1, "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "unrealized_exchange_gain_loss_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Unrealized Exchange Gain/Loss Account", + "no_copy": 1, "options": "Account" }, { @@ -482,6 +497,7 @@ "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "expenses_included_in_valuation", "fieldtype": "Link", "ignore_user_permissions": 1, @@ -490,15 +506,19 @@ "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "accumulated_depreciation_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Accumulated Depreciation Account", "no_copy": 1, "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "depreciation_expense_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Depreciation Expense Account", "no_copy": 1, "options": "Account" @@ -519,29 +539,39 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "disposal_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Gain/Loss Account on Asset Disposal", "no_copy": 1, "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "depreciation_cost_center", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Asset Depreciation Cost Center", "no_copy": 1, "options": "Cost Center" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "capital_work_in_progress_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Capital Work In Progress Account", + "no_copy": 1, "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "asset_received_but_not_billed", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Asset Received But Not Billed", + "no_copy": 1, "options": "Account" }, { @@ -673,15 +703,21 @@ "options": "Warehouse" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "unrealized_profit_loss_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Unrealized Profit / Loss Account", + "no_copy": 1, "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "default_discount_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Default Payment Discount Account", + "no_copy": 1, "options": "Account" }, { @@ -723,8 +759,10 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account", "fieldname": "default_advance_received_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Default Advance Received Account", "mandatory_depends_on": "book_advance_payments_as_liability", + "no_copy": 1, "options": "Account" }, { @@ -733,8 +771,10 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account", "fieldname": "default_advance_paid_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Default Advance Paid Account", "mandatory_depends_on": "book_advance_payments_as_liability", + "no_copy": 1, "options": "Account" }, { @@ -814,9 +854,12 @@ "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "round_off_for_opening", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Round Off for Opening", + "no_copy": 1, "options": "Account" }, { @@ -865,7 +908,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2025-11-16 16:51:27.624096", + "modified": "2026-07-02 07:21:21.794533", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 299ae82cb69..c53dfa5fb40 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -74,6 +74,7 @@ class Company(NestedSet): default_operating_cost_account: DF.Link | None default_payable_account: DF.Link | None default_provisional_account: DF.Link | None + default_purchase_price_variance_account: DF.Link | None default_receivable_account: DF.Link | None default_sales_contact: DF.Link | None default_selling_terms: DF.Link | None From b0446b6a096d912baf37043e53500a8786eace75 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 10:35:17 +0530 Subject: [PATCH 092/109] fix: don't attempt to create SABB for non-serialized / non-batch items (backport #56627) (#56802) * fix: don't attempt to create SABB for non-serialized / non-batch items (#56627) * fix: don't attempt to create SABB for non-serialized / non-batch items * fix(stock): skip serial batch lookup for rows without item code (cherry picked from commit 5b738b7b0d21289569c92f0f3a85c7a57f3a2981) # Conflicts: # erpnext/stock/services/serial_batch_bundle_service.py * chore: resolve conflicts --------- Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/controllers/stock_controller.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index f58c831922a..c48eb2bd620 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -263,6 +263,10 @@ class StockController(AccountsController): parent_details = self.get_parent_details_for_packed_items() for row in self.get(table_name): + item_code = row.get("rm_item_code") or row.get("item_code") + if not item_code or not self.is_serial_batch_item(item_code): + continue + if ( not via_landed_cost_voucher and row.serial_and_batch_bundle From 1cf94f55017eda065b745735a25792486b415aae Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 12:37:11 +0530 Subject: [PATCH 093/109] fix: replay immutable SLE qty for serial/batch bundle valuation (backport #56814) (#56834) fix: replay immutable SLE qty for serial/batch bundle valuation (#56814) (cherry picked from commit ecc8ec672bab513619ae35cdb7dde49b264cc722) Co-authored-by: rohitwaghchaure --- erpnext/stock/stock_ledger.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 93ee2eaa651..53d2153c44d 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1080,7 +1080,11 @@ class update_entries_after: self.wh_data.stock_queue = json.loads(stock_queue[0]) if stock_queue else [] self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount) - self.wh_data.qty_after_transaction += flt(doc.total_qty, self.flt_precision) + # Replay the immutable qty recorded on the SLE at submission, not the bundle's recomputed + # total_qty. A valuation repost must never rewrite physical quantities; if the bundle's child + # rows were edited after submission, doc.total_qty would silently corrupt qty_after_transaction + # (and every downstream balance). sle.actual_qty is the frozen movement for this entry. + self.wh_data.qty_after_transaction += flt(sle.actual_qty, self.flt_precision) if flt(self.wh_data.qty_after_transaction, self.flt_precision): self.wh_data.valuation_rate = flt(self.wh_data.stock_value, self.flt_precision) / flt( self.wh_data.qty_after_transaction, self.flt_precision From 19fc8b8e6ffacdb9acb13fdd5369122192e46610 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 3 Jul 2026 11:29:44 +0530 Subject: [PATCH 094/109] fix: FIFO queue checks and incorrect entries filter in stock ledger reports - 'Show Incorrect Entries' always returned an empty result (regression from #43619); now returns entries from one row before the first incorrect one - FIFO queue columns were computed for serialized/batched SLEs that don't maintain a stock queue, showing false differences; left empty for such rows - compare value/valuation differences at currency precision, qty at float precision (cherry picked from commit 94ab09e4a3c6af85e086c6393518a432f11a86c7) # Conflicts: # erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py --- .../stock_ledger_invariant_check.py | 90 ++++++++++--------- .../test_stock_ledger_invariant_check.py | 76 ++++++++++++++++ .../stock_ledger_variance.py | 32 ++++--- 3 files changed, 147 insertions(+), 51 deletions(-) create mode 100644 erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py index 954acf998d8..421529c90e6 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py @@ -20,6 +20,7 @@ SLE_FIELDS = ( "outgoing_rate", "stock_queue", "batch_no", + "serial_no", "stock_value", "stock_value_difference", "valuation_rate", @@ -52,16 +53,16 @@ def add_invariant_check_fields(sles, filters): balance_qty = 0.0 balance_stock_value = 0.0 - incorrect_idx = 0 - precision = frappe.get_precision("Stock Ledger Entry", "actual_qty") + incorrect_idx = None + float_precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) or 3 + currency_precision = ( + cint(frappe.db.get_single_value("System Settings", "currency_precision")) or float_precision + ) for idx, sle in enumerate(sles): - queue = json.loads(sle.stock_queue) if sle.stock_queue else [] - - fifo_qty = 0.0 - fifo_value = 0.0 - for qty, rate in queue: - fifo_qty += qty - fifo_value += qty * rate + if sle.batch_no: + sle.use_batchwise_valuation = frappe.db.get_value( + "Batch", sle.batch_no, "use_batchwise_valuation", cache=True + ) if sle.actual_qty < 0: sle.consumption_rate = sle.stock_value_difference / sle.actual_qty @@ -77,57 +78,66 @@ def add_invariant_check_fields(sles, filters): if balance_qty is None: balance_qty = sle.qty_after_transaction - sle.fifo_queue_qty = fifo_qty - sle.fifo_stock_value = fifo_value - sle.fifo_valuation_rate = fifo_value / fifo_qty if fifo_qty else None sle.balance_value_by_qty = ( sle.stock_value / sle.qty_after_transaction if sle.qty_after_transaction else None ) sle.expected_qty_after_transaction = balance_qty sle.stock_value_from_diff = balance_stock_value - # set difference fields sle.difference_in_qty = sle.qty_after_transaction - sle.expected_qty_after_transaction - sle.fifo_qty_diff = sle.qty_after_transaction - fifo_qty - sle.fifo_value_diff = sle.stock_value - fifo_value - sle.fifo_valuation_diff = ( - sle.valuation_rate - sle.fifo_valuation_rate if sle.fifo_valuation_rate else None - ) sle.valuation_diff = ( sle.valuation_rate - sle.balance_value_by_qty if sle.balance_value_by_qty else None ) sle.diff_value_diff = sle.stock_value_from_diff - sle.stock_value - if not incorrect_idx and filters.get("show_incorrect_entries"): - if is_sle_has_correct_data(sle, precision): - continue - else: - incorrect_idx = idx + if maintains_fifo_queue(sle): + add_fifo_fields(sle, sles[idx - 1] if idx else None) - if idx > 0: - sle.fifo_stock_diff = sle.fifo_stock_value - sles[idx - 1].fifo_stock_value - sle.fifo_difference_diff = sle.fifo_stock_diff - sle.stock_value_difference - - if sle.batch_no: - sle.use_batchwise_valuation = frappe.db.get_value( - "Batch", sle.batch_no, "use_batchwise_valuation", cache=True - ) + if incorrect_idx is None and not is_sle_has_correct_data(sle, float_precision, currency_precision): + incorrect_idx = idx if filters.get("show_incorrect_entries"): - if incorrect_idx > 0: - sles = sles[cint(incorrect_idx) - 1 :] - - return [] + if incorrect_idx is None: + return [] + return sles[max(incorrect_idx - 1, 0) :] return sles -def is_sle_has_correct_data(sle, precision): - if flt(sle.difference_in_qty, precision) != 0.0 or flt(sle.diff_value_diff, precision) != 0: - print(flt(sle.difference_in_qty, precision), flt(sle.diff_value_diff, precision)) - return False +def maintains_fifo_queue(sle): + # no queue is maintained for serialized/batchwise-valued stock + return not ( + sle.serial_and_batch_bundle or sle.serial_no or (sle.batch_no and sle.use_batchwise_valuation) + ) - return True + +def add_fifo_fields(sle, prev_sle): + queue = json.loads(sle.stock_queue) if sle.stock_queue else [] + + fifo_qty = 0.0 + fifo_value = 0.0 + for qty, rate in queue: + fifo_qty += qty + fifo_value += qty * rate + + sle.fifo_queue_qty = fifo_qty + sle.fifo_stock_value = fifo_value + sle.fifo_valuation_rate = fifo_value / fifo_qty if fifo_qty else None + sle.fifo_qty_diff = sle.qty_after_transaction - fifo_qty + sle.fifo_value_diff = sle.stock_value - fifo_value + sle.fifo_valuation_diff = ( + sle.valuation_rate - sle.fifo_valuation_rate if sle.fifo_valuation_rate else None + ) + if prev_sle and prev_sle.fifo_stock_value is not None: + sle.fifo_stock_diff = sle.fifo_stock_value - prev_sle.fifo_stock_value + sle.fifo_difference_diff = sle.fifo_stock_diff - sle.stock_value_difference + + +def is_sle_has_correct_data(sle, float_precision, currency_precision): + return ( + flt(sle.difference_in_qty, float_precision) == 0.0 + and flt(sle.diff_value_diff, currency_precision) == 0.0 + ) def get_columns(): diff --git a/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py new file mode 100644 index 00000000000..0f71a8834b2 --- /dev/null +++ b/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py @@ -0,0 +1,76 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check import execute +from erpnext.tests.utils import ERPNextTestSuite + +WAREHOUSE = "Stores - _TC" +COMPANY = "_Test Company" +ITEM = "_Test Item" + + +class TestStockLedgerInvariantCheck(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict({"company": COMPANY, "warehouse": WAREHOUSE}) + filters.update(extra) + return execute(filters)[1] + + def make_movements(self) -> str: + frappe.db.set_value("Item", ITEM, "valuation_method", "FIFO") + make_stock_entry(item_code=ITEM, to_warehouse=WAREHOUSE, qty=10, rate=100, posting_date="2026-06-01") + make_stock_entry(item_code=ITEM, to_warehouse=WAREHOUSE, qty=5, rate=120, posting_date="2026-06-02") + make_stock_entry(item_code=ITEM, from_warehouse=WAREHOUSE, qty=4, rate=0, posting_date="2026-06-03") + return ITEM + + def test_diagnostic_rows_have_no_discrepancy(self): + item = self.make_movements() + + data = self.run_report(item_code=item) + + self.assertEqual(len(data), 3) + for row in data: + self.assertLess(abs(row.difference_in_qty), 0.01) + self.assertLess(abs(row.fifo_qty_diff), 0.01) + self.assertLess(abs(row.diff_value_diff), 0.01) + + def test_running_balance_matches(self): + item = self.make_movements() + + data = self.run_report(item_code=item) + + self.assertEqual(data[-1].qty_after_transaction, 11) + + def test_show_incorrect_entries(self): + item = self.make_movements() + + self.assertEqual(self.run_report(item_code=item, show_incorrect_entries=1), []) + + sle = frappe.get_last_doc( + "Stock Ledger Entry", {"item_code": item, "warehouse": WAREHOUSE, "is_cancelled": 0} + ) + frappe.db.set_value( + "Stock Ledger Entry", sle.name, "qty_after_transaction", sle.qty_after_transaction + 5 + ) + + data = self.run_report(item_code=item, show_incorrect_entries=1) + self.assertEqual(len(data), 2) # incorrect entry + one before it for context + self.assertEqual(data[-1].name, sle.name) + + def test_batch_item_skips_fifo_queue_checks(self): + from erpnext.stock.doctype.item.test_item import make_item + + item = make_item( + properties={"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "SLIC-BAT-.####"} + ).name + make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=10, rate=100) + + data = self.run_report(item_code=item) + self.assertTrue(data) + for row in data: + self.assertIsNone(row.fifo_qty_diff) + self.assertIsNone(row.fifo_value_diff) + + self.assertEqual(self.run_report(item_code=item, show_incorrect_entries=1), []) diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py index 327f158e3f6..f02f630d716 100644 --- a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py +++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py @@ -205,7 +205,10 @@ def get_data(filters=None): data = [] if item_warehouse_map: - precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) + float_precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) or 3 + currency_precision = ( + cint(frappe.db.get_single_value("System Settings", "currency_precision")) or float_precision + ) for item_warehouse in item_warehouse_map: report_data = stock_ledger_invariant_check(item_warehouse) @@ -215,7 +218,11 @@ def get_data(filters=None): for row in report_data: if has_difference( - row, precision, filters.difference_in, item_warehouse.valuation_method or valuation_method + row, + float_precision, + currency_precision, + filters.difference_in, + item_warehouse.valuation_method or valuation_method, ): row.update( { @@ -261,23 +268,26 @@ def get_item_warehouse_combinations(filters: dict | None = None) -> dict: return query.run(as_dict=1) -def has_difference(row, precision, difference_in, valuation_method): +def has_difference(row, float_precision, currency_precision, difference_in, valuation_method): if valuation_method == "Moving Average": - qty_diff = flt(row.difference_in_qty, precision) - value_diff = flt(row.diff_value_diff, precision) - valuation_diff = flt(row.valuation_diff, precision) + qty_diff = flt(row.difference_in_qty, float_precision) + value_diff = flt(row.diff_value_diff, currency_precision) + valuation_diff = flt(row.valuation_diff, currency_precision) else: - qty_diff = flt(row.difference_in_qty, precision) - value_diff = flt(row.diff_value_diff, precision) + qty_diff = flt(row.difference_in_qty, float_precision) + value_diff = flt(row.diff_value_diff, currency_precision) if row.stock_queue and json.loads(row.stock_queue): value_diff = value_diff or ( - flt(row.fifo_value_diff, precision) or flt(row.fifo_difference_diff, precision) + flt(row.fifo_value_diff, currency_precision) + or flt(row.fifo_difference_diff, currency_precision) ) - qty_diff = qty_diff or flt(row.fifo_qty_diff, precision) + qty_diff = qty_diff or flt(row.fifo_qty_diff, float_precision) - valuation_diff = flt(row.valuation_diff, precision) or flt(row.fifo_valuation_diff, precision) + valuation_diff = flt(row.valuation_diff, currency_precision) or flt( + row.fifo_valuation_diff, currency_precision + ) if difference_in == "Qty" and qty_diff: return True From 3c47adb0215bb027540b081f5a6bbebfe8553a2e Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 3 Jul 2026 11:39:51 +0530 Subject: [PATCH 095/109] fix: address review comments - restore mutated SLE after test via addCleanup - explicit return False in has_difference - comment the fifo_stock_diff guard for non-queue predecessors (cherry picked from commit ef5f47fafdc7e81122f0e335be7d2faff8a730a9) --- .../stock_ledger_invariant_check.py | 1 + .../test_stock_ledger_invariant_check.py | 7 +++++++ .../report/stock_ledger_variance/stock_ledger_variance.py | 2 ++ 3 files changed, 10 insertions(+) diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py index 421529c90e6..aef9fec6414 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py @@ -128,6 +128,7 @@ def add_fifo_fields(sle, prev_sle): sle.fifo_valuation_diff = ( sle.valuation_rate - sle.fifo_valuation_rate if sle.fifo_valuation_rate else None ) + # prev row may not maintain a queue; H and H - F stay blank across the gap if prev_sle and prev_sle.fifo_stock_value is not None: sle.fifo_stock_diff = sle.fifo_stock_value - prev_sle.fifo_stock_value sle.fifo_difference_diff = sle.fifo_stock_diff - sle.stock_value_difference diff --git a/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py index 0f71a8834b2..49504b31207 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py @@ -51,6 +51,13 @@ class TestStockLedgerInvariantCheck(ERPNextTestSuite): sle = frappe.get_last_doc( "Stock Ledger Entry", {"item_code": item, "warehouse": WAREHOUSE, "is_cancelled": 0} ) + self.addCleanup( + frappe.db.set_value, + "Stock Ledger Entry", + sle.name, + "qty_after_transaction", + sle.qty_after_transaction, + ) frappe.db.set_value( "Stock Ledger Entry", sle.name, "qty_after_transaction", sle.qty_after_transaction + 5 ) diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py index f02f630d716..04b888d85fe 100644 --- a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py +++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py @@ -297,3 +297,5 @@ def has_difference(row, float_precision, currency_precision, difference_in, valu return True elif difference_in not in ["Qty", "Value", "Valuation"] and (qty_diff or value_diff or valuation_diff): return True + + return False From f3dd90bb0afe3ef695149a497278e1c5528aaf92 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 3 Jul 2026 12:12:46 +0530 Subject: [PATCH 096/109] test: drop redundant cleanup, db rolls back after each test (cherry picked from commit 3b1e57966e14160ab620d3b60d99ad25e95db5b9) --- .../test_stock_ledger_invariant_check.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py index 49504b31207..0f71a8834b2 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py @@ -51,13 +51,6 @@ class TestStockLedgerInvariantCheck(ERPNextTestSuite): sle = frappe.get_last_doc( "Stock Ledger Entry", {"item_code": item, "warehouse": WAREHOUSE, "is_cancelled": 0} ) - self.addCleanup( - frappe.db.set_value, - "Stock Ledger Entry", - sle.name, - "qty_after_transaction", - sle.qty_after_transaction, - ) frappe.db.set_value( "Stock Ledger Entry", sle.name, "qty_after_transaction", sle.qty_after_transaction + 5 ) From db4136b258a5a45a19eebc786a65427eeb0fcafa Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 3 Jul 2026 13:54:07 +0530 Subject: [PATCH 097/109] fix: use correct import --- .../test_stock_ledger_invariant_check.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py index 0f71a8834b2..ca5ba6acec1 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py @@ -5,14 +5,14 @@ import frappe from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check import execute -from erpnext.tests.utils import ERPNextTestSuite +from erpnext.tests.utils import FrappeTestCase WAREHOUSE = "Stores - _TC" COMPANY = "_Test Company" ITEM = "_Test Item" -class TestStockLedgerInvariantCheck(ERPNextTestSuite): +class TestStockLedgerInvariantCheck(FrappeTestCase): def run_report(self, **extra): filters = frappe._dict({"company": COMPANY, "warehouse": WAREHOUSE}) filters.update(extra) From b170913880d4010b792a79633c360c387dce4619 Mon Sep 17 00:00:00 2001 From: pandiyan Date: Fri, 3 Jul 2026 12:29:25 +0530 Subject: [PATCH 098/109] fix: remove company default on cost center in stock entry detail the ":company" default pre-filled every row before set_default_cost_center() ran, so its "if not row.cost_center" guard was always false and the project/item group/brand priority chain in get_default_cost_center() never ran. (cherry picked from commit edfa0a7a1d599a9f20406b3b114f8c666148e4f0) # Conflicts: # erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json --- .../stock/doctype/stock_entry_detail/stock_entry_detail.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 4e422e320b9..2a63a37ffa7 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -337,7 +337,6 @@ "print_hide": 1 }, { - "default": ":Company", "depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))", "fieldname": "cost_center", "fieldtype": "Link", @@ -616,7 +615,11 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2026-04-27 11:40:38.294196", +======= + "modified": "2026-07-03 12:11:53.714931", +>>>>>>> edfa0a7a1d (fix: remove company default on cost center in stock entry detail) "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", From 1a52c586661149d851f88d783dac4fcc3e778cdd Mon Sep 17 00:00:00 2001 From: pandiyan Date: Fri, 3 Jul 2026 13:12:35 +0530 Subject: [PATCH 099/109] test: cover cost center fallback to item group default in manufacture entry the existing test_cost_center_for_manufacture only checks a raw material row against an item-level override, which is set independently of the ":company" default guard and never exercised the bug. (cherry picked from commit a168bb7ea49f669597aadcf898c35e1fd0cb2dbd) --- .../doctype/work_order/test_work_order.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 2679d6e29fe..629a8e717f5 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -687,6 +687,28 @@ class TestWorkOrder(FrappeTestCase): ste = make_stock_entry(wo_order.name, "Material Transfer for Manufacture", wo_order.qty) self.assertEqual(ste.get("items")[0].get("cost_center"), "_Test Cost Center - _TC") + @ERPNextTestSuite.change_settings("Manufacturing Settings", {"make_serial_no_batch_from_work_order": 0}) + def test_cost_center_for_manufacture_falls_back_to_item_group_default(self): + # "_Test Item Group" is master data with buying_cost_center already set to + # "_Test Cost Center 2 - _TC" for "_Test Company"; only the FG item and its + # BOM need to be created, since no existing item in that group has one. + fg_item = make_item( + "_Test FG Item For Item Group Cost Center", + {"is_stock_item": 1, "item_group": "_Test Item Group", "include_item_in_manufacturing": 1}, + ) + + if not frappe.db.exists("BOM", {"item": fg_item.name, "is_active": 1, "is_default": 1}): + make_bom(item=fg_item.name, raw_materials=["_Test Item"]) + + wo_order = make_wo_order_test_record( + production_item=fg_item.name, skip_transfer=1, source_warehouse="_Test Warehouse - _TC" + ) + ste = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", wo_order.qty)) + ste.insert() + + fg_row = next(d for d in ste.items if d.is_finished_item) + self.assertEqual(fg_row.cost_center, "_Test Cost Center 2 - _TC") + def test_operation_time_with_batch_size(self): fg_item = "Test Batch Size Item For BOM" rm1 = "Test Batch Size Item RM 1 For BOM" From cecfcf82e0aa6b03c4dfce504a4a28e60339f692 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 3 Jul 2026 21:14:43 +0530 Subject: [PATCH 100/109] fix: incorrect import path for FrappeTestCase --- .../test_stock_ledger_invariant_check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py index ca5ba6acec1..372f91aaaa0 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py @@ -5,7 +5,7 @@ import frappe from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check import execute -from erpnext.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase WAREHOUSE = "Stores - _TC" COMPANY = "_Test Company" From 0ab94a929afbd449d1a4092f89c4860e097350e5 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 3 Jul 2026 21:20:32 +0530 Subject: [PATCH 101/109] chore: formatting --- .../test_stock_ledger_invariant_check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py index 372f91aaaa0..0ac0ce79216 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py @@ -2,10 +2,10 @@ # See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check import execute -from frappe.tests.utils import FrappeTestCase WAREHOUSE = "Stores - _TC" COMPANY = "_Test Company" From 93f33cc5cf2d195f64257e0bd36acb08781b1681 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 3 Jul 2026 21:42:58 +0530 Subject: [PATCH 102/109] test: fresh item per test, v15 only rolls back at class teardown --- .../test_stock_ledger_invariant_check.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py index 0ac0ce79216..b82e341c84a 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py @@ -4,12 +4,12 @@ import frappe from frappe.tests.utils import FrappeTestCase +from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check import execute WAREHOUSE = "Stores - _TC" COMPANY = "_Test Company" -ITEM = "_Test Item" class TestStockLedgerInvariantCheck(FrappeTestCase): @@ -19,11 +19,12 @@ class TestStockLedgerInvariantCheck(FrappeTestCase): return execute(filters)[1] def make_movements(self) -> str: - frappe.db.set_value("Item", ITEM, "valuation_method", "FIFO") - make_stock_entry(item_code=ITEM, to_warehouse=WAREHOUSE, qty=10, rate=100, posting_date="2026-06-01") - make_stock_entry(item_code=ITEM, to_warehouse=WAREHOUSE, qty=5, rate=120, posting_date="2026-06-02") - make_stock_entry(item_code=ITEM, from_warehouse=WAREHOUSE, qty=4, rate=0, posting_date="2026-06-03") - return ITEM + # fresh item per test: db is only rolled back at class teardown on v15 + item = make_item(properties={"valuation_method": "FIFO"}).name + make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=10, rate=100, posting_date="2026-06-01") + make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=5, rate=120, posting_date="2026-06-02") + make_stock_entry(item_code=item, from_warehouse=WAREHOUSE, qty=4, rate=0, posting_date="2026-06-03") + return item def test_diagnostic_rows_have_no_discrepancy(self): item = self.make_movements() @@ -60,8 +61,6 @@ class TestStockLedgerInvariantCheck(FrappeTestCase): self.assertEqual(data[-1].name, sle.name) def test_batch_item_skips_fifo_queue_checks(self): - from erpnext.stock.doctype.item.test_item import make_item - item = make_item( properties={"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "SLIC-BAT-.####"} ).name From ece1c9872fad55bdebeec36405a60ed7a106dc22 Mon Sep 17 00:00:00 2001 From: Shllokkk <140623894+Shllokkk@users.noreply.github.com> Date: Fri, 3 Jul 2026 22:49:33 +0530 Subject: [PATCH 103/109] fix: use live source warehouse valuation for internal transfer purchse receipts (#56812) fix: use live source warehouse valuation for internal transfer purchase receipts --- .../purchase_receipt/test_purchase_receipt.py | 87 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 21 ++++- 2 files changed, 105 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 3799e773a7d..edde28a04e6 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1662,6 +1662,93 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(query[0].value, 0) + def test_internal_transfer_pr_incoming_sle_anchored_to_dn_rate(self): + """Internal-transfer PR's inward SLE must use DN.incoming_rate even when + PR.item.valuation_rate was wrong at submit, so divisional_loss does not + leak to COGS.""" + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + from erpnext.stock.stock_ledger import update_entries_after + + prepare_data_for_internal_transfer() + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + + from_warehouse = create_warehouse("_Test Drift From", company=company) + transit_warehouse = create_warehouse("_Test Drift Transit", company=company) + to_warehouse = create_warehouse("_Test Drift Receiver", company=company) + item_doc = create_item("Test Internal Drift Item") + + make_purchase_receipt( + item_code=item_doc.name, + company=company, + posting_date=add_days(today(), -1), + warehouse=from_warehouse, + qty=10, + rate=100, + ) + + dn = create_delivery_note( + item_code=item_doc.name, + company=company, + customer=customer, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=1, + rate=100, + warehouse=from_warehouse, + target_warehouse=transit_warehouse, + ) + self.assertEqual(flt(dn.items[0].incoming_rate), 100.0) + + pr = make_inter_company_purchase_receipt(dn.name) + pr.items[0].warehouse = to_warehouse + pr.submit() + + inward_sle = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": pr.name, + "warehouse": to_warehouse, + "is_cancelled": 0, + }, + ["name", "item_code", "warehouse", "posting_date", "posting_time", "creation", "incoming_rate"], + as_dict=True, + ) + self.assertEqual(flt(inward_sle.incoming_rate), 100.0) + + frappe.db.set_value( + "Purchase Receipt Item", + pr.items[0].name, + {"sales_incoming_rate": 0, "valuation_rate": 80}, + ) + frappe.db.set_value( + "Stock Ledger Entry", + inward_sle.name, + {"incoming_rate": 80, "stock_value_difference": 80}, + ) + + update_entries_after( + { + "item_code": inward_sle.item_code, + "warehouse": inward_sle.warehouse, + "posting_date": inward_sle.posting_date, + "posting_time": inward_sle.posting_time, + "sle_id": inward_sle.name, + "creation": inward_sle.creation, + } + ) + + refreshed = frappe.db.get_value( + "Stock Ledger Entry", + inward_sle.name, + ["incoming_rate", "stock_value_difference"], + as_dict=True, + ) + self.assertEqual(flt(refreshed.incoming_rate), 100.0) + self.assertEqual(flt(refreshed.stock_value_difference), 100.0) + def test_backdated_transaction_for_internal_transfer_in_trasit_warehouse_for_purchase_invoice( self, ): diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 53d2153c44d..ba578f69814 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -870,10 +870,16 @@ class update_entries_after: if ( sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and sle.voucher_detail_no - and sle.actual_qty < 0 and is_internal_transfer(sle) ): - sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle) + # Anchor both legs of an internal-transfer PR/PI to the DN/SI incoming_rate; + # otherwise an inward SLE that inherits a stale PR.valuation_rate leaks the + # gap to COGS via divisional_loss. + rate = get_incoming_rate_for_inter_company_transfer(sle) + if sle.actual_qty < 0: + sle.outgoing_rate = rate + elif rate: + sle.incoming_rate = rate dimensions = get_inventory_dimensions() has_dimensions = False @@ -2430,7 +2436,16 @@ def get_incoming_rate_for_inter_company_transfer(sle) -> float: if lcv_amount: lcv_rate = flt(lcv_amount / abs(sle.actual_qty)) - return rate + lcv_rate + charges_rate = 0.0 + if flt(sle.actual_qty) > 0: + charge_fields = ["item_tax_amount", "rm_supp_cost"] + charges = frappe.db.get_value( + f"{sle.voucher_type} Item", sle.voucher_detail_no, charge_fields, as_dict=True + ) + if charges: + charges_rate = flt(sum(flt(charges.get(f)) for f in charge_fields)) / abs(sle.actual_qty) + + return rate + lcv_rate + charges_rate def is_internal_transfer(sle): From da623eb124bd453cbfabebe95807ffcfa8395d2b Mon Sep 17 00:00:00 2001 From: Pandiyan P Date: Sat, 4 Jul 2026 12:45:09 +0530 Subject: [PATCH 104/109] fix: resolve conflicts --- .../stock/doctype/stock_entry_detail/stock_entry_detail.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 2a63a37ffa7..ec73d0c6fee 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -615,11 +615,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2026-04-27 11:40:38.294196", -======= "modified": "2026-07-03 12:11:53.714931", ->>>>>>> edfa0a7a1d (fix: remove company default on cost center in stock entry detail) "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", From 5cedc50c55bcd00a4ba033fbfdf1c16a49634fc1 Mon Sep 17 00:00:00 2001 From: pandiyan Date: Sat, 4 Jul 2026 12:53:57 +0530 Subject: [PATCH 105/109] fix: use change_settings decorator matching version-15-hotfix test utils ERPNextTestSuite doesn't exist on this branch; the cherry-picked test used the develop-branch decorator style, breaking ruff (F821) in CI. --- erpnext/manufacturing/doctype/work_order/test_work_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 629a8e717f5..3d599e311bb 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -687,7 +687,7 @@ class TestWorkOrder(FrappeTestCase): ste = make_stock_entry(wo_order.name, "Material Transfer for Manufacture", wo_order.qty) self.assertEqual(ste.get("items")[0].get("cost_center"), "_Test Cost Center - _TC") - @ERPNextTestSuite.change_settings("Manufacturing Settings", {"make_serial_no_batch_from_work_order": 0}) + @change_settings("Manufacturing Settings", {"make_serial_no_batch_from_work_order": 0}) def test_cost_center_for_manufacture_falls_back_to_item_group_default(self): # "_Test Item Group" is master data with buying_cost_center already set to # "_Test Cost Center 2 - _TC" for "_Test Company"; only the FG item and its From ae43f47c39c240d422692f681ea8ac278f4d66cc Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 3 Jul 2026 11:29:18 +0530 Subject: [PATCH 106/109] fix: race condition and repeatable read in process pcv - Update using child table name to avoid scanning whole table, which eventually leads to mariadb 1020 (REPEATABLE READ). - Avoid race condition in final summarization (cherry picked from commit ff6881764b843a3046934fddbbbfd8a907637ef8) --- .../process_period_closing_voucher.py | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py index b5ca3331b71..43813fb98af 100644 --- a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py @@ -97,7 +97,7 @@ def start_pcv_processing(docname: str): ppcvd = qb.DocType("Process Period Closing Voucher Detail") if normal_balances := ( qb.from_(ppcvd) - .select(ppcvd.processing_date, ppcvd.report_type, ppcvd.parentfield) + .select(ppcvd.name, ppcvd.processing_date, ppcvd.report_type, ppcvd.parentfield) .where(ppcvd.parent.eq(docname) & ppcvd.status.eq("Queued")) .orderby(ppcvd.parentfield, ppcvd.idx, ppcvd.processing_date) .limit(4) @@ -108,12 +108,7 @@ def start_pcv_processing(docname: str): for x in normal_balances: frappe.db.set_value( "Process Period Closing Voucher Detail", - { - "processing_date": x.processing_date, - "parent": docname, - "report_type": x.report_type, - "parentfield": x.parentfield, - }, + x.name, "status", "Running", ) @@ -124,10 +119,12 @@ def start_pcv_processing(docname: str): is_async=True, enqueue_after_commit=True, docname=docname, + row_name=x.name, date=x.processing_date, report_type=x.report_type, parentfield=x.parentfield, ) + frappe.db.commit() else: frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed") @@ -251,7 +248,7 @@ def schedule_next_date(docname: str): ppcvd = qb.DocType("Process Period Closing Voucher Detail") if to_process := ( qb.from_(ppcvd) - .select(ppcvd.processing_date, ppcvd.report_type, ppcvd.parentfield) + .select(ppcvd.name, ppcvd.processing_date, ppcvd.report_type, ppcvd.parentfield) .where(ppcvd.parent.eq(docname) & ppcvd.status.eq("Queued")) .orderby(ppcvd.parentfield, ppcvd.idx, ppcvd.processing_date) .limit(1) @@ -261,15 +258,11 @@ def schedule_next_date(docname: str): if not is_scheduler_inactive(): frappe.db.set_value( "Process Period Closing Voucher Detail", - { - "processing_date": to_process[0].processing_date, - "parent": docname, - "report_type": to_process[0].report_type, - "parentfield": to_process[0].parentfield, - }, + to_process[0].name, "status", "Running", ) + frappe.db.commit() frappe.enqueue( method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date", queue="long", @@ -277,6 +270,7 @@ def schedule_next_date(docname: str): is_async=True, enqueue_after_commit=True, docname=docname, + row_name=to_process[0].name, date=to_process[0].processing_date, report_type=to_process[0].report_type, parentfield=to_process[0].parentfield, @@ -441,6 +435,8 @@ def summarize_and_post_ledger_entries(docname): make_closing_entries(closing_entries, pcv.name, pcv.company, pcv.period_end_date) + frappe.db.commit() + frappe.db.set_value("Period Closing Voucher", pcv.name, "gle_processing_status", "Completed") frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed") @@ -526,10 +522,10 @@ def build_dimension_wise_balance_dict(gl_entries): return dimension_balances -def process_individual_date(docname: str, date, report_type, parentfield): +def process_individual_date(docname: str, row_name, date, report_type, parentfield): current_date_status = frappe.db.get_value( "Process Period Closing Voucher Detail", - {"processing_date": date, "report_type": report_type, "parentfield": parentfield}, + row_name, "status", ) if current_date_status != "Running": @@ -576,17 +572,18 @@ def process_individual_date(docname: str, date, report_type, parentfield): # save results frappe.db.set_value( "Process Period Closing Voucher Detail", - {"processing_date": date, "parent": docname, "report_type": report_type, "parentfield": parentfield}, + row_name, "closing_balance", frappe.json.dumps(res), ) frappe.db.set_value( "Process Period Closing Voucher Detail", - {"processing_date": date, "parent": docname, "report_type": report_type, "parentfield": parentfield}, + row_name, "status", "Completed", ) + frappe.db.commit() # chain call schedule_next_date(docname) From 1ee8904a94b21f60d46ee925de02cf0321ce5193 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 3 Jul 2026 13:00:00 +0530 Subject: [PATCH 107/109] fix: prevent repeatable read related concurrency errors Process Period Closing Voucher and Process Period Closing Voucher Details are trackers how the jobs are processed. Keep transactions on them very short. (cherry picked from commit 7e4045e8282714928989453529d679ff3bf4b6eb) --- .../process_period_closing_voucher.py | 99 +++++++++++-------- 1 file changed, 58 insertions(+), 41 deletions(-) diff --git a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py index 43813fb98af..a0e59d96be4 100644 --- a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py @@ -86,47 +86,55 @@ class ProcessPeriodClosingVoucher(Document): cancel_pcv_processing(self.name) +def initialize_parallel_threads(docname: str): + threads = 4 + timeout = frappe.db.get_single_value("Accounts Settings", "pcv_job_timeout") or 3600 + ppcvd = qb.DocType("Process Period Closing Voucher Detail") + + frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running") + + if normal_balances := ( + qb.from_(ppcvd) + .select(ppcvd.name, ppcvd.processing_date, ppcvd.report_type, ppcvd.parentfield) + .where(ppcvd.parent.eq(docname) & ppcvd.status.eq("Queued")) + .orderby(ppcvd.parentfield, ppcvd.idx, ppcvd.processing_date) + .limit(threads) + .for_update(skip_locked=True) + .run(as_dict=True) + ): + if not is_scheduler_inactive(): + for x in normal_balances: + frappe.db.set_value( + "Process Period Closing Voucher Detail", + x.name, + "status", + "Running", + ) + frappe.enqueue( + method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date", + queue="long", + timeout=timeout, + is_async=True, + enqueue_after_commit=True, + docname=docname, + row_name=x.name, + date=x.processing_date, + report_type=x.report_type, + parentfield=x.parentfield, + ) + # keep transaction on PPCV and PPCVD short + # prevents concurrency errors - REPEATABLE READ + if not frappe.in_test: + frappe.db.commit() + else: + frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed") + + @frappe.whitelist() def start_pcv_processing(docname: str): if frappe.db.get_value("Process Period Closing Voucher", docname, "status") in ["Queued", "Running"]: frappe.has_permission("Process Period Closing Voucher", "write", doc=docname, throw=True) - frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running") - - timeout = frappe.db.get_single_value("Accounts Settings", "pcv_job_timeout") or 3600 - - ppcvd = qb.DocType("Process Period Closing Voucher Detail") - if normal_balances := ( - qb.from_(ppcvd) - .select(ppcvd.name, ppcvd.processing_date, ppcvd.report_type, ppcvd.parentfield) - .where(ppcvd.parent.eq(docname) & ppcvd.status.eq("Queued")) - .orderby(ppcvd.parentfield, ppcvd.idx, ppcvd.processing_date) - .limit(4) - .for_update(skip_locked=True) - .run(as_dict=True) - ): - if not is_scheduler_inactive(): - for x in normal_balances: - frappe.db.set_value( - "Process Period Closing Voucher Detail", - x.name, - "status", - "Running", - ) - frappe.enqueue( - method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date", - queue="long", - timeout=timeout, - is_async=True, - enqueue_after_commit=True, - docname=docname, - row_name=x.name, - date=x.processing_date, - report_type=x.report_type, - parentfield=x.parentfield, - ) - frappe.db.commit() - else: - frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed") + initialize_parallel_threads(docname) @frappe.whitelist() @@ -244,8 +252,8 @@ def get_gle_for_closing_account(pcv, dimension_balance, dimensions): @frappe.whitelist() def schedule_next_date(docname: str): timeout = frappe.db.get_single_value("Accounts Settings", "pcv_job_timeout") or 3600 - ppcvd = qb.DocType("Process Period Closing Voucher Detail") + if to_process := ( qb.from_(ppcvd) .select(ppcvd.name, ppcvd.processing_date, ppcvd.report_type, ppcvd.parentfield) @@ -262,7 +270,11 @@ def schedule_next_date(docname: str): "status", "Running", ) - frappe.db.commit() + # keep transaction on PPCV and PPCVD short + # prevents concurrency errors - REPEATABLE READ + if not frappe.in_test: + frappe.db.commit() + frappe.enqueue( method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date", queue="long", @@ -435,7 +447,10 @@ def summarize_and_post_ledger_entries(docname): make_closing_entries(closing_entries, pcv.name, pcv.company, pcv.period_end_date) - frappe.db.commit() + # keep transaction on PPCV and PPCVD short + # prevents concurrency errors - REPEATABLE READ + if not frappe.in_test: + frappe.db.commit() frappe.db.set_value("Period Closing Voucher", pcv.name, "gle_processing_status", "Completed") frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed") @@ -583,7 +598,9 @@ def process_individual_date(docname: str, row_name, date, report_type, parentfie "status", "Completed", ) - frappe.db.commit() + # commit heavy computation before touching PPCV or PPCVD + if not frappe.in_test: + frappe.db.commit() # chain call schedule_next_date(docname) From 6945e1292b58348210280b4519f1fc3f37d5cea0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 3 Jul 2026 15:51:18 +0530 Subject: [PATCH 108/109] refactor: prevent whole table scan while scheduling next date - helps in concurrency isolation (cherry picked from commit 21f4603144d3ffbe5e1b871dbaa02631e61919c2) --- .../process_period_closing_voucher_detail.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.py b/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.py index f3a8302ac5b..0e0b905c96a 100644 --- a/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.py +++ b/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.py @@ -1,7 +1,7 @@ # Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -# import frappe +import frappe from frappe.model.document import Document @@ -24,3 +24,10 @@ class ProcessPeriodClosingVoucherDetail(Document): # end: auto-generated types pass + + +def on_doctype_update(): + frappe.db.add_index( + "Process Period Closing Voucher Detail", + ["parent", "status", "parentfield", "idx", "processing_date"], + ) From dcea09ea8f2a30f621cb364482899432bd96ab2d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 3 Jul 2026 17:04:51 +0530 Subject: [PATCH 109/109] chore: linter fix (cherry picked from commit a9ffdac8062de9b2e2e68a9c457e9e96e8ced37b) --- .../process_period_closing_voucher.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py index a0e59d96be4..6315560b89f 100644 --- a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py @@ -125,7 +125,7 @@ def initialize_parallel_threads(docname: str): # keep transaction on PPCV and PPCVD short # prevents concurrency errors - REPEATABLE READ if not frappe.in_test: - frappe.db.commit() + frappe.db.commit() # nosemgrep else: frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed") @@ -273,7 +273,7 @@ def schedule_next_date(docname: str): # keep transaction on PPCV and PPCVD short # prevents concurrency errors - REPEATABLE READ if not frappe.in_test: - frappe.db.commit() + frappe.db.commit() # nosemgrep frappe.enqueue( method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date", @@ -450,7 +450,7 @@ def summarize_and_post_ledger_entries(docname): # keep transaction on PPCV and PPCVD short # prevents concurrency errors - REPEATABLE READ if not frappe.in_test: - frappe.db.commit() + frappe.db.commit() # nosemgrep frappe.db.set_value("Period Closing Voucher", pcv.name, "gle_processing_status", "Completed") frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed") @@ -600,7 +600,7 @@ def process_individual_date(docname: str, row_name, date, report_type, parentfie ) # commit heavy computation before touching PPCV or PPCVD if not frappe.in_test: - frappe.db.commit() + frappe.db.commit() # nosemgrep # chain call schedule_next_date(docname)