From 670c6dcdd7cdf017f52bd4ba595aa570999b567f Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Tue, 4 Nov 2025 15:51:53 +0530 Subject: [PATCH 01/86] fix(material request): set default buying price list if not exists (cherry picked from commit 9c0ff140601bb158585978165700ef77af187bbc) --- erpnext/stock/doctype/material_request/material_request.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 8a813dfadf3..243ba7adac8 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -79,7 +79,9 @@ frappe.ui.form.on("Material Request", { }); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); - frm.doc.buying_price_list = frappe.defaults.get_default("buying_price_list"); + if (!frm.doc.buying_price_list) { + frm.doc.buying_price_list = frappe.defaults.get_default("buying_price_list"); + } }, company: function (frm) { From c8e3da0a713efda19a2a3b457fb487d08cdd06db Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 25 Sep 2025 16:35:54 +0530 Subject: [PATCH 02/86] feat: process period closing voucher (cherry picked from commit 7a93630629518fcdc6a12545a2155277e12ac637) --- .../__init__.py | 0 .../process_period_closing_voucher.js | 8 ++++ .../process_period_closing_voucher.json | 42 +++++++++++++++++++ .../process_period_closing_voucher.py | 19 +++++++++ .../test_process_period_closing_voucher.py | 20 +++++++++ 5 files changed, 89 insertions(+) create mode 100644 erpnext/accounts/doctype/process_period_closing_voucher/__init__.py create mode 100644 erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js create mode 100644 erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.json create mode 100644 erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py create mode 100644 erpnext/accounts/doctype/process_period_closing_voucher/test_process_period_closing_voucher.py diff --git a/erpnext/accounts/doctype/process_period_closing_voucher/__init__.py b/erpnext/accounts/doctype/process_period_closing_voucher/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js new file mode 100644 index 00000000000..8633cb78f8d --- /dev/null +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Process Period Closing Voucher", { +// refresh(frm) { + +// }, +// }); 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 new file mode 100644 index 00000000000..0c1c7a996cf --- /dev/null +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.json @@ -0,0 +1,42 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-09-25 15:44:03.534699", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "section_break_zqqu" + ], + "fields": [ + { + "fieldname": "section_break_zqqu", + "fieldtype": "Section Break" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-09-25 15:44:03.534699", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Process Period Closing Voucher", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} 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 new file mode 100644 index 00000000000..da38291b16c --- /dev/null +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py @@ -0,0 +1,19 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class ProcessPeriodClosingVoucher(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 + + # end: auto-generated types + + pass diff --git a/erpnext/accounts/doctype/process_period_closing_voucher/test_process_period_closing_voucher.py b/erpnext/accounts/doctype/process_period_closing_voucher/test_process_period_closing_voucher.py new file mode 100644 index 00000000000..17ce2c13b09 --- /dev/null +++ b/erpnext/accounts/doctype/process_period_closing_voucher/test_process_period_closing_voucher.py @@ -0,0 +1,20 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class IntegrationTestProcessPeriodClosingVoucher(IntegrationTestCase): + """ + Integration tests for ProcessPeriodClosingVoucher. + Use this class for testing interactions between multiple components. + """ + + pass From bfc0044d23830bd1f2b134c09784b549755b5956 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 1 Oct 2025 15:20:07 +0530 Subject: [PATCH 03/86] refactor: checkbox for pcv controller (cherry picked from commit 4888461be215e9aadf11e6bfa6e7279768ee7e41) # Conflicts: # erpnext/accounts/doctype/accounts_settings/accounts_settings.json # erpnext/accounts/doctype/accounts_settings/accounts_settings.py --- .../accounts_settings/accounts_settings.json | 20 +++++++++++++++++++ .../accounts_settings/accounts_settings.py | 5 +++++ 2 files changed, 25 insertions(+) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index abb9b96020a..dad7165b665 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -75,6 +75,7 @@ "period_closing_settings_section", "acc_frozen_upto", "ignore_account_closing_balance", + "use_legacy_controller_for_pcv", "column_break_25", "frozen_accounts_modifier", "tab_break_dpet", @@ -629,6 +630,21 @@ "fieldname": "fetch_valuation_rate_for_internal_transaction", "fieldtype": "Check", "label": "Fetch Valuation Rate for Internal Transaction" +<<<<<<< HEAD +======= + }, + { + "default": "0", + "fieldname": "use_legacy_budget_controller", + "fieldtype": "Check", + "label": "Use Legacy Budget Controller" + }, + { + "default": "0", + "fieldname": "use_legacy_controller_for_pcv", + "fieldtype": "Check", + "label": "Use Legacy Controller For Period Closing Voucher" +>>>>>>> 4888461be2 (refactor: checkbox for pcv controller) } ], "icon": "icon-cog", @@ -636,7 +652,11 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], +<<<<<<< HEAD "modified": "2025-07-18 13:56:47.192437", +======= + "modified": "2025-10-01 15:17:47.168354", +>>>>>>> 4888461be2 (refactor: checkbox for pcv controller) "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 f5a5eb70f96..7ec9da682dc 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -73,6 +73,11 @@ class AccountsSettings(Document): submit_journal_entries: DF.Check unlink_advance_payment_on_cancelation_of_order: DF.Check unlink_payment_on_cancellation_of_invoice: DF.Check +<<<<<<< HEAD +======= + use_legacy_budget_controller: DF.Check + use_legacy_controller_for_pcv: DF.Check +>>>>>>> 4888461be2 (refactor: checkbox for pcv controller) # end: auto-generated types def validate(self): From 902ce45a36267c8caf404b15efa00029f70ea7ef Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 3 Oct 2025 11:17:29 +0530 Subject: [PATCH 04/86] refactor: more data structure changes (cherry picked from commit a15578f8f47134c3efec451647934d92a80fce4c) --- .../period_closing_voucher.py | 16 +++++++++++----- .../process_period_closing_voucher.json | 18 ++++++++++++++---- .../process_period_closing_voucher.py | 2 ++ 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index fba261a1484..97c503f5885 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -131,8 +131,11 @@ class PeriodClosingVoucher(AccountsController): frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency)) def on_submit(self): - self.db_set("gle_processing_status", "In Progress") - self.make_gl_entries() + if frappe.get_single_value("Accounts Settings", "use_legacy_controller_for_pcv"): + self.db_set("gle_processing_status", "In Progress") + self.make_gl_entries() + else: + print("submit") def on_cancel(self): self.ignore_linked_doctypes = ( @@ -141,9 +144,12 @@ class PeriodClosingVoucher(AccountsController): "Payment Ledger Entry", "Account Closing Balance", ) - self.block_if_future_closing_voucher_exists() - self.db_set("gle_processing_status", "In Progress") - self.cancel_gl_entries() + if frappe.get_single_value("Accounts Settings", "use_legacy_controller_for_pcv"): + self.block_if_future_closing_voucher_exists() + self.db_set("gle_processing_status", "In Progress") + self.cancel_gl_entries() + else: + print("cancel") def make_gl_entries(self): if frappe.db.estimate_count("GL Entry") > 100_000: 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 0c1c7a996cf..5368138a52b 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 @@ -5,18 +5,28 @@ "doctype": "DocType", "engine": "InnoDB", "field_order": [ - "section_break_zqqu" + "parent_pcv", + "status" ], "fields": [ { - "fieldname": "section_break_zqqu", - "fieldtype": "Section Break" + "fieldname": "parent_pcv", + "fieldtype": "Link", + "label": "PCV", + "options": "Period Closing Voucher" + }, + { + "default": "Queued", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Queued\nRunning\nCompleted" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2025-09-25 15:44:03.534699", + "modified": "2025-10-01 15:57:36.832943", "modified_by": "Administrator", "module": "Accounts", "name": "Process Period Closing Voucher", 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 da38291b16c..784ae36ae2f 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 @@ -14,6 +14,8 @@ class ProcessPeriodClosingVoucher(Document): if TYPE_CHECKING: from frappe.types import DF + parent_pcv: DF.Link | None + status: DF.Literal["Queued", "Running", "Completed"] # end: auto-generated types pass From b484db3ffd18ddd0d02506327724a9b7ee2636ab Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 3 Oct 2025 14:24:35 +0530 Subject: [PATCH 05/86] refactor: child table in process pcv (cherry picked from commit 0d09d21d2e12a5cc7cecf4c18389fa45051e79b9) --- .../process_period_closing_voucher.json | 11 ++++- .../process_period_closing_voucher.py | 5 +++ .../__init__.py | 0 ...process_period_closing_voucher_detail.json | 40 +++++++++++++++++++ .../process_period_closing_voucher_detail.py | 24 +++++++++++ 5 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 erpnext/accounts/doctype/process_period_closing_voucher_detail/__init__.py create mode 100644 erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json create mode 100644 erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.py 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 5368138a52b..cc85b6fb908 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 @@ -6,7 +6,8 @@ "engine": "InnoDB", "field_order": [ "parent_pcv", - "status" + "status", + "dates_to_process" ], "fields": [ { @@ -21,12 +22,18 @@ "fieldtype": "Select", "label": "Status", "options": "Queued\nRunning\nCompleted" + }, + { + "fieldname": "dates_to_process", + "fieldtype": "Table", + "label": "Dates to Process", + "options": "Process Period Closing Voucher Detail" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2025-10-01 15:57:36.832943", + "modified": "2025-10-03 14:29:55.584225", "modified_by": "Administrator", "module": "Accounts", "name": "Process Period Closing Voucher", 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 784ae36ae2f..62410b52efb 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 @@ -14,6 +14,11 @@ class ProcessPeriodClosingVoucher(Document): if TYPE_CHECKING: from frappe.types import DF + from erpnext.accounts.doctype.process_period_closing_voucher_detail.process_period_closing_voucher_detail import ( + ProcessPeriodClosingVoucherDetail, + ) + + dates_to_process: DF.Table[ProcessPeriodClosingVoucherDetail] parent_pcv: DF.Link | None status: DF.Literal["Queued", "Running", "Completed"] # end: auto-generated types diff --git a/erpnext/accounts/doctype/process_period_closing_voucher_detail/__init__.py b/erpnext/accounts/doctype/process_period_closing_voucher_detail/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json b/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json new file mode 100644 index 00000000000..32aa85702e0 --- /dev/null +++ b/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json @@ -0,0 +1,40 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-10-01 15:58:17.544153", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "processing_date", + "status" + ], + "fields": [ + { + "fieldname": "processing_date", + "fieldtype": "Date", + "label": "Processing Date" + }, + { + "default": "Queued", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Queued\nRunning\nCompleted" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2025-10-01 16:00:02.221411", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Process Period Closing Voucher Detail", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} 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 new file mode 100644 index 00000000000..445451ee7c2 --- /dev/null +++ b/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.py @@ -0,0 +1,24 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class ProcessPeriodClosingVoucherDetail(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 + processing_date: DF.Date | None + status: DF.Literal["Queued", "Running", "Completed"] + # end: auto-generated types + + pass From bc07de8c120d77f8167bdf7a9fc03f6c8a7568c0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 8 Oct 2025 10:23:24 +0530 Subject: [PATCH 06/86] refactor: temporarily save balances in JSON (cherry picked from commit f44c908a8de6aad79dceccf89c014a77a920d42f) --- .../process_period_closing_voucher_detail.json | 10 ++++++++-- .../process_period_closing_voucher_detail.py | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json b/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json index 32aa85702e0..780a1d5da15 100644 --- a/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json +++ b/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json @@ -6,7 +6,8 @@ "engine": "InnoDB", "field_order": [ "processing_date", - "status" + "status", + "closing_balance" ], "fields": [ { @@ -20,13 +21,18 @@ "fieldtype": "Select", "label": "Status", "options": "Queued\nRunning\nCompleted" + }, + { + "fieldname": "closing_balance", + "fieldtype": "JSON", + "label": "Closing Balance" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-10-01 16:00:02.221411", + "modified": "2025-10-08 10:19:29.928526", "modified_by": "Administrator", "module": "Accounts", "name": "Process Period Closing Voucher Detail", 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 445451ee7c2..3ec46f1b181 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 @@ -14,6 +14,7 @@ class ProcessPeriodClosingVoucherDetail(Document): if TYPE_CHECKING: from frappe.types import DF + closing_balance: DF.JSON | None parent: DF.Data parentfield: DF.Data parenttype: DF.Data From 19b911120ca757f25f3792e6c87deea15204eb03 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 8 Oct 2025 11:02:52 +0530 Subject: [PATCH 07/86] refactor: barebones functions (cherry picked from commit 1c92b015423fd89b609bd653739610eea9bae8bf) --- .../process_period_closing_voucher.js | 24 +++++++++++--- .../process_period_closing_voucher.json | 20 +++++++++-- .../process_period_closing_voucher.py | 33 +++++++++++++++++-- ...process_period_closing_voucher_detail.json | 5 ++- 4 files changed, 71 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js index 8633cb78f8d..fcd6ee11b80 100644 --- a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js @@ -1,8 +1,24 @@ // Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -// frappe.ui.form.on("Process Period Closing Voucher", { -// refresh(frm) { +frappe.ui.form.on("Process Period Closing Voucher", { + refresh(frm) { + if (frm.doc.docstatus == 1 && ["Queued"].find((x) => x == frm.doc.status)) { + let execute_btn = __("Start"); -// }, -// }); + frm.add_custom_button(execute_btn, () => { + frm.call({ + method: "erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.start_pcv_processing", + args: { + docname: frm.doc.name, + }, + }).then((r) => { + if (!r.exc) { + frappe.show_alert(__("Job Started")); + frm.reload_doc(); + } + }); + }); + } + }, +}); 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 cc85b6fb908..131d7b7a53a 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 @@ -7,14 +7,17 @@ "field_order": [ "parent_pcv", "status", - "dates_to_process" + "dates_to_process", + "amended_from" ], "fields": [ { "fieldname": "parent_pcv", "fieldtype": "Link", + "in_list_view": 1, "label": "PCV", - "options": "Period Closing Voucher" + "options": "Period Closing Voucher", + "reqd": 1 }, { "default": "Queued", @@ -28,12 +31,23 @@ "fieldtype": "Table", "label": "Dates to Process", "options": "Process Period Closing Voucher Detail" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Process Period Closing Voucher", + "print_hide": 1, + "read_only": 1, + "search_index": 1 } ], "grid_page_length": 50, "index_web_pages_for_search": 1, + "is_submittable": 1, "links": [], - "modified": "2025-10-03 14:29:55.584225", + "modified": "2025-10-08 10:33:43.742974", "modified_by": "Administrator", "module": "Accounts", "name": "Process Period Closing Voucher", 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 62410b52efb..39d6b1b0874 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 @@ -1,8 +1,11 @@ # Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -# import frappe +from datetime import timedelta + +import frappe from frappe.model.document import Document +from frappe.utils import add_days, get_datetime class ProcessPeriodClosingVoucher(Document): @@ -18,9 +21,33 @@ class ProcessPeriodClosingVoucher(Document): ProcessPeriodClosingVoucherDetail, ) + amended_from: DF.Link | None dates_to_process: DF.Table[ProcessPeriodClosingVoucherDetail] - parent_pcv: DF.Link | None + parent_pcv: DF.Link status: DF.Literal["Queued", "Running", "Completed"] # end: auto-generated types - pass + def validate(self): + self.status = "Queued" + self.populate_processing_table() + + def populate_processing_table(self): + self.dates_to_process = [] + pcv = frappe.get_doc("Period Closing Voucher", self.parent_pcv) + start = get_datetime(pcv.period_start_date) + end = get_datetime(pcv.period_end_date) + dates = [start + timedelta(days=x) for x in range((end - start).days + 1)] + for x in dates: + self.append("dates_to_process", {"processing_date": x, "status": "Queued"}) + + +@frappe.whitelist() +def start_pcv_processing(docname: str): + if frappe.db.get_value("Process Period Closing Voucher", docname, "status") == "Queued": + dates_to_process = frappe.db.get_all( + "Process Period Closing Voucher Detail", + filters={"parent": docname, "status": "Queued"}, + fields=["processing_date"], + order_by="processing_date", + limit=4, + ) diff --git a/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json b/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json index 780a1d5da15..ab0963417c0 100644 --- a/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json +++ b/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json @@ -13,18 +13,21 @@ { "fieldname": "processing_date", "fieldtype": "Date", + "in_list_view": 1, "label": "Processing Date" }, { "default": "Queued", "fieldname": "status", "fieldtype": "Select", + "in_list_view": 1, "label": "Status", "options": "Queued\nRunning\nCompleted" }, { "fieldname": "closing_balance", "fieldtype": "JSON", + "in_list_view": 1, "label": "Closing Balance" } ], @@ -32,7 +35,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-10-08 10:19:29.928526", + "modified": "2025-10-08 10:47:50.050341", "modified_by": "Administrator", "module": "Accounts", "name": "Process Period Closing Voucher Detail", From 43acfdff82c291e9d7b99d3605441cecf885f237 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 10 Oct 2025 10:50:03 +0530 Subject: [PATCH 08/86] refactor: stable start, pause, resume and completion stages (cherry picked from commit c839ebf593082cf44820e92fa6397f633bc011a7) --- .../process_period_closing_voucher.js | 20 +++++++- .../process_period_closing_voucher.py | 48 ++++++++++++++++++- ...process_period_closing_voucher_detail.json | 4 +- .../process_period_closing_voucher_detail.py | 2 +- 4 files changed, 68 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js index fcd6ee11b80..3b797476128 100644 --- a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js @@ -3,7 +3,7 @@ frappe.ui.form.on("Process Period Closing Voucher", { refresh(frm) { - if (frm.doc.docstatus == 1 && ["Queued"].find((x) => x == frm.doc.status)) { + if (frm.doc.docstatus == 1 && ["Queued", "Paused"].find((x) => x == frm.doc.status)) { let execute_btn = __("Start"); frm.add_custom_button(execute_btn, () => { @@ -20,5 +20,23 @@ frappe.ui.form.on("Process Period Closing Voucher", { }); }); } + + if (frm.doc.docstatus == 1 && ["Running"].find((x) => x == frm.doc.status)) { + let execute_btn = __("Pause"); + + frm.add_custom_button(execute_btn, () => { + frm.call({ + method: "erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.pause_pcv_processing", + args: { + docname: frm.doc.name, + }, + }).then((r) => { + if (!r.exc) { + frappe.show_alert(__("Job Started")); + frm.reload_doc(); + } + }); + }); + } }, }); 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 39d6b1b0874..8a72dca2245 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 @@ -6,6 +6,7 @@ from datetime import timedelta import frappe from frappe.model.document import Document from frappe.utils import add_days, get_datetime +from frappe.utils.scheduler import is_scheduler_inactive class ProcessPeriodClosingVoucher(Document): @@ -43,11 +44,54 @@ class ProcessPeriodClosingVoucher(Document): @frappe.whitelist() def start_pcv_processing(docname: str): - if frappe.db.get_value("Process Period Closing Voucher", docname, "status") == "Queued": - dates_to_process = frappe.db.get_all( + if frappe.db.get_value("Process Period Closing Voucher", docname, "status") in ["Queued", "Paused"]: + frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running") + if dates_to_process := frappe.db.get_all( "Process Period Closing Voucher Detail", filters={"parent": docname, "status": "Queued"}, fields=["processing_date"], order_by="processing_date", limit=4, + ): + if not is_scheduler_inactive(): + for x in dates_to_process: + frappe.enqueue( + method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date", + queue="long", + is_async=True, + enqueue_after_commit=True, + docname=docname, + date=x.processing_date, + ) + else: + frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed") + + +@frappe.whitelist() +def pause_pcv_processing(docname: str): + frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Paused") + + +def process_individual_date(docname: str, date: str): + if frappe.db.get_value("Process Period Closing Voucher", docname, "status") == "Running": + frappe.db.set_value( + "Process Period Closing Voucher Detail", {"processing_date": date}, "status", "Completed" ) + if next_date_to_process := frappe.db.get_all( + "Process Period Closing Voucher Detail", + filters={"parent": docname, "status": "Queued"}, + fields=["processing_date"], + order_by="processing_date", + limit=1, + ): + if not is_scheduler_inactive(): + frappe.enqueue( + method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date", + queue="long", + is_async=True, + enqueue_after_commit=True, + docname=docname, + date=next_date_to_process[0].processing_date, + ) + else: + frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed") diff --git a/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json b/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json index ab0963417c0..6d452c18fc0 100644 --- a/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json +++ b/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json @@ -22,7 +22,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Status", - "options": "Queued\nRunning\nCompleted" + "options": "Queued\nRunning\nPaused\nCompleted" }, { "fieldname": "closing_balance", @@ -35,7 +35,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-10-08 10:47:50.050341", + "modified": "2025-10-09 16:46:37.778199", "modified_by": "Administrator", "module": "Accounts", "name": "Process Period Closing Voucher Detail", 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 3ec46f1b181..f15d917a295 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 @@ -19,7 +19,7 @@ class ProcessPeriodClosingVoucherDetail(Document): parentfield: DF.Data parenttype: DF.Data processing_date: DF.Date | None - status: DF.Literal["Queued", "Running", "Completed"] + status: DF.Literal["Queued", "Running", "Paused", "Completed"] # end: auto-generated types pass From 1999de0b7591cd5454272008017f2b27bb3f923a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 13 Oct 2025 13:40:40 +0530 Subject: [PATCH 09/86] refactor: store closing balance as JSON (cherry picked from commit 1a31825409a211225e7c139fb3779b200efe15ee) --- .../process_period_closing_voucher.py | 103 ++++++++++++++---- 1 file changed, 79 insertions(+), 24 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 8a72dca2245..e68aaa6b8cb 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 @@ -4,10 +4,14 @@ from datetime import timedelta import frappe +from frappe import qb from frappe.model.document import Document +from frappe.query_builder.functions import Sum from frappe.utils import add_days, get_datetime from frappe.utils.scheduler import is_scheduler_inactive +BACKGROUND = True + class ProcessPeriodClosingVoucher(Document): # begin: auto-generated types @@ -55,43 +59,94 @@ def start_pcv_processing(docname: str): ): if not is_scheduler_inactive(): for x in dates_to_process: - frappe.enqueue( - method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date", - queue="long", - is_async=True, - enqueue_after_commit=True, - docname=docname, - date=x.processing_date, - ) + if BACKGROUND: + frappe.enqueue( + method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date", + queue="long", + is_async=True, + enqueue_after_commit=True, + docname=docname, + date=x.processing_date, + ) + else: + process_individual_date(docname, x.processing_date) else: frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed") @frappe.whitelist() def pause_pcv_processing(docname: str): - frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Paused") + queued_dates = frappe.db.get_all( + "Process Period Closing Voucher Detail", + filters={"parent": docname, "status": "Queued"}, + fields=["name"], + ) + ppcv = qb.DocType("Process Period Closing Voucher") + qb.update(ppcv).set(ppcv.status, "Paused").where(ppcv.name.isin(queued_dates)).run() -def process_individual_date(docname: str, date: str): - if frappe.db.get_value("Process Period Closing Voucher", docname, "status") == "Running": - frappe.db.set_value( - "Process Period Closing Voucher Detail", {"processing_date": date}, "status", "Completed" - ) - if next_date_to_process := frappe.db.get_all( - "Process Period Closing Voucher Detail", - filters={"parent": docname, "status": "Queued"}, - fields=["processing_date"], - order_by="processing_date", - limit=1, - ): - if not is_scheduler_inactive(): +def call_next_date(docname: str): + if next_date_to_process := frappe.db.get_all( + "Process Period Closing Voucher Detail", + filters={"parent": docname, "status": "Queued"}, + fields=["processing_date"], + order_by="processing_date", + limit=1, + ): + next_date_to_process = next_date_to_process[0].processing_date + if not is_scheduler_inactive(): + frappe.db.set_value( + "Process Period Closing Voucher Detail", + {"processing_date": next_date_to_process, "parent": docname}, + "status", + "Running", + ) + if BACKGROUND: frappe.enqueue( method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date", queue="long", is_async=True, enqueue_after_commit=True, docname=docname, - date=next_date_to_process[0].processing_date, + date=next_date_to_process, ) - else: + else: + process_individual_date(docname, next_date_to_process) + else: + running = frappe.db.get_all( + "Process Period Closing Voucher Detail", + filters={"parent": docname, "status": "Running"}, + fields=["processing_date"], + order_by="processing_date", + limit=1, + ) + if not running: frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed") + + +def process_individual_date(docname: str, date: str): + if frappe.db.get_value("Process Period Closing Voucher", docname, "status") == "Running": + pcv_name = frappe.db.get_value("Process Period Closing Voucher", docname, "parent_pcv") + pcv = frappe.get_doc("Period Closing Voucher", pcv_name) + gle = qb.DocType("GL Entry") + res = ( + qb.from_(gle) + .select(gle.account, Sum(gle.debit).as_("debit"), Sum(gle.credit).as_("credit")) + .where((gle.company.eq(pcv.company)) & (gle.is_cancelled.eq(False)) & (gle.posting_date.eq(date))) + .groupby(gle.account) + .run(as_dict=True) + ) + frappe.db.set_value( + "Process Period Closing Voucher Detail", + {"processing_date": date, "parent": docname}, + "status", + "Completed", + ) + frappe.db.set_value( + "Process Period Closing Voucher Detail", + {"processing_date": date, "parent": docname}, + "closing_balance", + frappe.json.dumps(res), + ) + + call_next_date(docname) From 76bdf7944c4ed9d79ffd133edfab952252ef58c7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 14 Oct 2025 16:31:46 +0530 Subject: [PATCH 10/86] refactor: store daily balances based on dimensions key dimensions key is manually converted to string (cherry picked from commit 1846de0d493494311555b956865649ba1dd2de47) --- .../process_period_closing_voucher.py | 98 ++++++++++++++++--- 1 file changed, 85 insertions(+), 13 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 e68aaa6b8cb..5786c319e39 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 @@ -7,10 +7,10 @@ import frappe from frappe import qb from frappe.model.document import Document from frappe.query_builder.functions import Sum -from frappe.utils import add_days, get_datetime +from frappe.utils import add_days, flt, get_datetime from frappe.utils.scheduler import is_scheduler_inactive -BACKGROUND = True +BACKGROUND = False class ProcessPeriodClosingVoucher(Document): @@ -76,13 +76,16 @@ def start_pcv_processing(docname: str): @frappe.whitelist() def pause_pcv_processing(docname: str): + ppcv = qb.DocType("Process Period Closing Voucher") + qb.update(ppcv).set(ppcv.status, "Paused").where(ppcv.name.eq(docname)).run() + queued_dates = frappe.db.get_all( "Process Period Closing Voucher Detail", filters={"parent": docname, "status": "Queued"}, - fields=["name"], + pluck="name", ) - ppcv = qb.DocType("Process Period Closing Voucher") - qb.update(ppcv).set(ppcv.status, "Paused").where(ppcv.name.isin(queued_dates)).run() + ppcvd = qb.DocType("Process Period Closing Voucher Detail") + qb.update(ppcvd).set(ppcvd.status, "Paused").where(ppcvd.name.isin(queued_dates)).run() def call_next_date(docname: str): @@ -121,32 +124,101 @@ def call_next_date(docname: str): limit=1, ) if not running: + # TODO: Generate GL and Account Closing Balance frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed") +def get_dimensions(): + from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_accounting_dimensions, + ) + + default_dimensions = ["cost_center", "finance_book", "project"] + dimensions = default_dimensions + get_accounting_dimensions() + return dimensions + + +def get_key(res): + return tuple([res.get(dimension) for dimension in get_dimensions()]) + + def process_individual_date(docname: str, date: str): if frappe.db.get_value("Process Period Closing Voucher", docname, "status") == "Running": pcv_name = frappe.db.get_value("Process Period Closing Voucher", docname, "parent_pcv") pcv = frappe.get_doc("Period Closing Voucher", pcv_name) - gle = qb.DocType("GL Entry") - res = ( - qb.from_(gle) - .select(gle.account, Sum(gle.debit).as_("debit"), Sum(gle.credit).as_("credit")) - .where((gle.company.eq(pcv.company)) & (gle.is_cancelled.eq(False)) & (gle.posting_date.eq(date))) - .groupby(gle.account) - .run(as_dict=True) + + dimensions = get_dimensions() + + p_l_accounts = frappe.db.get_all( + "Account", filters={"company": pcv.company, "report_type": "Profit and Loss"}, pluck="name" ) + + gle = qb.DocType("GL Entry") + query = qb.from_(gle).select(gle.account) + for dim in dimensions: + query = query.select(gle[dim]) + + query = query.select( + Sum(gle.debit).as_("debit"), + Sum(gle.credit).as_("credit"), + Sum(gle.debit_in_account_currency).as_("debit_in_account_currency"), + Sum(gle.credit_in_account_currency).as_("credit_in_account_currency"), + ).where( + (gle.company.eq(pcv.company)) + & (gle.is_cancelled.eq(0)) + & (gle.posting_date.eq(date)) + & (gle.account.isin(p_l_accounts)) + ) + + query = query.groupby(gle.account) + for dim in dimensions: + query = query.groupby(gle[dim]) + + res = query.run(as_dict=True) + + dimension_wise_acc_balances = frappe._dict() + for x in res: + dimension_key = get_key(x) + dimension_wise_acc_balances.setdefault(dimension_key, frappe._dict()).setdefault( + x.account, + frappe._dict( + { + "debit_in_account_currency": 0, + "credit_in_account_currency": 0, + "debit": 0, + "credit": 0, + "account_currency": x.account_currency, + } + ), + ) + dimension_wise_acc_balances[dimension_key][x.account].debit_in_account_currency += flt( + x.debit_in_account_currency + ) + dimension_wise_acc_balances[dimension_key][x.account].credit_in_account_currency += flt( + x.credit_in_account_currency + ) + dimension_wise_acc_balances[dimension_key][x.account].debit += flt(x.debit) + dimension_wise_acc_balances[dimension_key][x.account].credit += flt(x.credit) + frappe.db.set_value( "Process Period Closing Voucher Detail", {"processing_date": date, "parent": docname}, "status", "Completed", ) + + # convert dict keys to json compliant json dictionary keys + json_dict = {} + for k, v in dimension_wise_acc_balances.items(): + str_key = [str(x) for x in k] + str_key = ",".join(str_key) + json_dict[str_key] = v + frappe.db.set_value( "Process Period Closing Voucher Detail", {"processing_date": date, "parent": docname}, "closing_balance", - frappe.json.dumps(res), + frappe.json.dumps(json_dict), ) call_next_date(docname) From b6b552422839d2455fc73f828eb534503a7c60c8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 15 Oct 2025 13:09:29 +0530 Subject: [PATCH 11/86] refactor: build and post gl entries (cherry picked from commit e88074ddec52cd6a99b1c8afeaf4bd387ebc9acd) --- .../process_period_closing_voucher.js | 18 ++ .../process_period_closing_voucher.json | 11 +- .../process_period_closing_voucher.py | 166 +++++++++++++++++- 3 files changed, 191 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js index 3b797476128..8f231f241be 100644 --- a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js @@ -38,5 +38,23 @@ frappe.ui.form.on("Process Period Closing Voucher", { }); }); } + + if (frm.doc.docstatus == 1 && ["Completed"].find((x) => x == frm.doc.status)) { + let execute_btn = __("Call Next"); + + frm.add_custom_button(execute_btn, () => { + frm.call({ + method: "erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.call_next_date", + args: { + docname: frm.doc.name, + }, + }).then((r) => { + if (!r.exc) { + frappe.show_alert(__("Call next date for processing")); + frm.reload_doc(); + } + }); + }); + } }, }); 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 131d7b7a53a..5db50764153 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 @@ -7,6 +7,7 @@ "field_order": [ "parent_pcv", "status", + "total", "dates_to_process", "amended_from" ], @@ -24,12 +25,14 @@ "fieldname": "status", "fieldtype": "Select", "label": "Status", + "no_copy": 1, "options": "Queued\nRunning\nCompleted" }, { "fieldname": "dates_to_process", "fieldtype": "Table", "label": "Dates to Process", + "no_copy": 1, "options": "Process Period Closing Voucher Detail" }, { @@ -41,13 +44,19 @@ "print_hide": 1, "read_only": 1, "search_index": 1 + }, + { + "fieldname": "total", + "fieldtype": "JSON", + "label": "Total", + "no_copy": 1 } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-10-08 10:33:43.742974", + "modified": "2025-10-15 12:46:03.627166", "modified_by": "Administrator", "module": "Accounts", "name": "Process Period Closing Voucher", 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 5786c319e39..f9c668769bb 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 @@ -30,6 +30,7 @@ class ProcessPeriodClosingVoucher(Document): dates_to_process: DF.Table[ProcessPeriodClosingVoucherDetail] parent_pcv: DF.Link status: DF.Literal["Queued", "Running", "Completed"] + total: DF.JSON | None # end: auto-generated types def validate(self): @@ -88,6 +89,69 @@ def pause_pcv_processing(docname: str): qb.update(ppcvd).set(ppcvd.status, "Paused").where(ppcvd.name.isin(queued_dates)).run() +def get_gle_for_pl_account(pcv, acc, balances, dimensions): + balance_in_account_currency = flt(balances.debit_in_account_currency) - flt( + balances.credit_in_account_currency + ) + balance_in_company_currency = flt(balances.debit) - flt(balances.credit) + gl_entry = frappe._dict( + { + "company": pcv.company, + "posting_date": pcv.period_end_date, + "account": acc, + "account_currency": balances.account_currency, + "debit_in_account_currency": abs(balance_in_account_currency) + if balance_in_account_currency < 0 + else 0, + "debit": abs(balance_in_company_currency) if balance_in_company_currency < 0 else 0, + "credit_in_account_currency": abs(balance_in_account_currency) + if balance_in_account_currency > 0 + else 0, + "credit": abs(balance_in_company_currency) if balance_in_company_currency > 0 else 0, + "is_period_closing_voucher_entry": 1, + "voucher_type": "Period Closing Voucher", + "voucher_no": pcv.name, + "fiscal_year": pcv.fiscal_year, + "remarks": pcv.remarks, + "is_opening": "No", + } + ) + # update dimensions + for i, dimension in enumerate(dimensions): + gl_entry[dimension] = dimensions[i] + return gl_entry + + +def get_gle_for_closing_account(pcv, dimension_balance, dimensions): + balance_in_company_currency = flt(dimension_balance.balance_in_company_currency) + debit = balance_in_company_currency if balance_in_company_currency > 0 else 0 + credit = abs(balance_in_company_currency) if balance_in_company_currency < 0 else 0 + + gl_entry = frappe._dict( + { + "company": pcv.company, + "posting_date": pcv.period_end_date, + "account": pcv.closing_account_head, + "account_currency": frappe.db.get_value("Account", pcv.closing_account_head, "account_currency"), + "debit_in_account_currency": debit, + "debit": debit, + "credit_in_account_currency": credit, + "credit": credit, + "is_period_closing_voucher_entry": 1, + "voucher_type": "Period Closing Voucher", + "voucher_no": pcv.name, + "fiscal_year": pcv.fiscal_year, + "remarks": pcv.remarks, + "is_opening": "No", + } + ) + # update dimensions + for i, dimension in enumerate(dimensions): + gl_entry[dimension] = dimensions[i] + return gl_entry + + +@frappe.whitelist() def call_next_date(docname: str): if next_date_to_process := frappe.db.get_all( "Process Period Closing Voucher Detail", @@ -123,8 +187,82 @@ def call_next_date(docname: str): order_by="processing_date", limit=1, ) + # TODO: ensure all dates are processed if not running: - # TODO: Generate GL and Account Closing Balance + # Calculate total balances for PCV period + # Build dictionary back + dimension_wise_acc_balances = {} + ppcv = frappe.get_doc("Process Period Closing Voucher", docname) + for x in [x.closing_balance for x in ppcv.dates_to_process]: + bal = frappe.json.loads(x) + for dimensions, account_balances in bal.items(): + dim_key = tuple([None if x == "None" else x for x in dimensions.split(",")]) + obj = dimension_wise_acc_balances.setdefault(dim_key, frappe._dict()) + + for acc, bal in account_balances.items(): + if acc != "balances": + bal_dict = obj.setdefault( + acc, + frappe._dict( + { + "debit_in_account_currency": 0, + "credit_in_account_currency": 0, + "debit": 0, + "credit": 0, + "account_currency": bal["account_currency"], + } + ), + ) + bal_dict["debit_in_account_currency"] += bal["debit_in_account_currency"] + bal_dict["credit_in_account_currency"] += bal["credit_in_account_currency"] + bal_dict["debit"] += bal["debit"] + bal_dict["credit"] += bal["credit"] + else: + bal_dict = obj.setdefault( + "balances", + frappe._dict( + { + "balance_in_company_currency": 0, + "balance_in_account_currency": 0, + } + ), + ) + bal_dict["balance_in_company_currency"] += bal["balance_in_company_currency"] + bal_dict["balance_in_account_currency"] += bal["balance_in_account_currency"] + + # convert dict keys to json compliant json dictionary keys + json_dict = {} + for k, v in dimension_wise_acc_balances.items(): + str_key = [str(x) for x in k] + str_key = ",".join(str_key) + json_dict[str_key] = v + + frappe.db.set_value( + "Process Period Closing Voucher", docname, "total", frappe.json.dumps(json_dict) + ) + + # Build GL map + pcv = frappe.get_doc("Period Closing Voucher", ppcv.parent_pcv) + pl_accounts_reverse_gle = [] + closing_account_gle = [] + + for dimensions, account_balances in dimension_wise_acc_balances.items(): + for acc, balances in account_balances.items(): + balance_in_company_currency = flt(balances.debit) - flt(balances.credit) + if balance_in_company_currency: + pl_accounts_reverse_gle.append(get_gle_for_pl_account(pcv, acc, balances, dimensions)) + + # closing liability account + closing_account_gle.append( + get_gle_for_closing_account(pcv, account_balances["balances"], dimensions) + ) + + gl_entries = pl_accounts_reverse_gle + closing_account_gle + from erpnext.accounts.general_ledger import make_gl_entries + + if gl_entries: + make_gl_entries(gl_entries, merge_entries=False) + frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed") @@ -138,7 +276,7 @@ def get_dimensions(): return dimensions -def get_key(res): +def get_dimension_key(res): return tuple([res.get(dimension) for dimension in get_dimensions()]) @@ -163,6 +301,7 @@ def process_individual_date(docname: str, date: str): Sum(gle.credit).as_("credit"), Sum(gle.debit_in_account_currency).as_("debit_in_account_currency"), Sum(gle.credit_in_account_currency).as_("credit_in_account_currency"), + gle.account_currency, ).where( (gle.company.eq(pcv.company)) & (gle.is_cancelled.eq(0)) @@ -178,7 +317,7 @@ def process_individual_date(docname: str, date: str): dimension_wise_acc_balances = frappe._dict() for x in res: - dimension_key = get_key(x) + dimension_key = get_dimension_key(x) dimension_wise_acc_balances.setdefault(dimension_key, frappe._dict()).setdefault( x.account, frappe._dict( @@ -200,6 +339,27 @@ def process_individual_date(docname: str, date: str): dimension_wise_acc_balances[dimension_key][x.account].debit += flt(x.debit) dimension_wise_acc_balances[dimension_key][x.account].credit += flt(x.credit) + # dimension-wise total balances + dimension_wise_acc_balances[dimension_key].setdefault( + "balances", + frappe._dict( + { + "balance_in_account_currency": 0, + "balance_in_company_currency": 0, + } + ), + ) + + balance_in_account_currency = flt(x.debit_in_account_currency) - flt(x.credit_in_account_currency) + balance_in_company_currency = flt(x.debit) - flt(x.credit) + + dimension_wise_acc_balances[dimension_key][ + "balances" + ].balance_in_account_currency += balance_in_account_currency + dimension_wise_acc_balances[dimension_key][ + "balances" + ].balance_in_company_currency += balance_in_company_currency + frappe.db.set_value( "Process Period Closing Voucher Detail", {"processing_date": date, "parent": docname}, From e8f8abd6855f7e54b48263c5b21a05b31b19fa64 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 16 Oct 2025 11:54:17 +0530 Subject: [PATCH 12/86] refactor: store results as is and convert at the end (cherry picked from commit f25ee3c53f1cf8302c43970edf504da7ac1389bf) --- .../process_period_closing_voucher.py | 148 +++++++----------- 1 file changed, 57 insertions(+), 91 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 f9c668769bb..ce82cbb53cf 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 @@ -190,45 +190,16 @@ def call_next_date(docname: str): # TODO: ensure all dates are processed if not running: # Calculate total balances for PCV period - # Build dictionary back - dimension_wise_acc_balances = {} - ppcv = frappe.get_doc("Process Period Closing Voucher", docname) - for x in [x.closing_balance for x in ppcv.dates_to_process]: - bal = frappe.json.loads(x) - for dimensions, account_balances in bal.items(): - dim_key = tuple([None if x == "None" else x for x in dimensions.split(",")]) - obj = dimension_wise_acc_balances.setdefault(dim_key, frappe._dict()) - for acc, bal in account_balances.items(): - if acc != "balances": - bal_dict = obj.setdefault( - acc, - frappe._dict( - { - "debit_in_account_currency": 0, - "credit_in_account_currency": 0, - "debit": 0, - "credit": 0, - "account_currency": bal["account_currency"], - } - ), - ) - bal_dict["debit_in_account_currency"] += bal["debit_in_account_currency"] - bal_dict["credit_in_account_currency"] += bal["credit_in_account_currency"] - bal_dict["debit"] += bal["debit"] - bal_dict["credit"] += bal["credit"] - else: - bal_dict = obj.setdefault( - "balances", - frappe._dict( - { - "balance_in_company_currency": 0, - "balance_in_account_currency": 0, - } - ), - ) - bal_dict["balance_in_company_currency"] += bal["balance_in_company_currency"] - bal_dict["balance_in_account_currency"] += bal["balance_in_account_currency"] + ppcv = frappe.get_doc("Process Period Closing Voucher", docname) + + gl_entries = [] + for x in ppcv.dates_to_process: + closing_balances = [frappe._dict(gle) for gle in frappe.json.loads(x.closing_balance)] + gl_entries.extend(closing_balances) + + # Build dimension wise dictionary from all GLE's + dimension_wise_acc_balances = build_dimension_wise_balance_dict(gl_entries) # convert dict keys to json compliant json dictionary keys json_dict = {} @@ -280,6 +251,53 @@ def get_dimension_key(res): return tuple([res.get(dimension) for dimension in get_dimensions()]) +def build_dimension_wise_balance_dict(gl_entries): + dimension_balances = frappe._dict() + for x in gl_entries: + dimension_key = get_dimension_key(x) + dimension_balances.setdefault(dimension_key, frappe._dict()).setdefault( + x.account, + frappe._dict( + { + "debit_in_account_currency": 0, + "credit_in_account_currency": 0, + "debit": 0, + "credit": 0, + "account_currency": x.account_currency, + } + ), + ) + dimension_balances[dimension_key][x.account].debit_in_account_currency += flt( + x.debit_in_account_currency + ) + dimension_balances[dimension_key][x.account].credit_in_account_currency += flt( + x.credit_in_account_currency + ) + dimension_balances[dimension_key][x.account].debit += flt(x.debit) + dimension_balances[dimension_key][x.account].credit += flt(x.credit) + + # dimension-wise total balances + dimension_balances[dimension_key].setdefault( + "balances", + frappe._dict( + { + "balance_in_account_currency": 0, + "balance_in_company_currency": 0, + } + ), + ) + balance_in_account_currency = flt(x.debit_in_account_currency) - flt(x.credit_in_account_currency) + balance_in_company_currency = flt(x.debit) - flt(x.credit) + dimension_balances[dimension_key][ + "balances" + ].balance_in_account_currency += balance_in_account_currency + dimension_balances[dimension_key][ + "balances" + ].balance_in_company_currency += balance_in_company_currency + + return dimension_balances + + def process_individual_date(docname: str, date: str): if frappe.db.get_value("Process Period Closing Voucher", docname, "status") == "Running": pcv_name = frappe.db.get_value("Process Period Closing Voucher", docname, "parent_pcv") @@ -315,51 +333,6 @@ def process_individual_date(docname: str, date: str): res = query.run(as_dict=True) - dimension_wise_acc_balances = frappe._dict() - for x in res: - dimension_key = get_dimension_key(x) - dimension_wise_acc_balances.setdefault(dimension_key, frappe._dict()).setdefault( - x.account, - frappe._dict( - { - "debit_in_account_currency": 0, - "credit_in_account_currency": 0, - "debit": 0, - "credit": 0, - "account_currency": x.account_currency, - } - ), - ) - dimension_wise_acc_balances[dimension_key][x.account].debit_in_account_currency += flt( - x.debit_in_account_currency - ) - dimension_wise_acc_balances[dimension_key][x.account].credit_in_account_currency += flt( - x.credit_in_account_currency - ) - dimension_wise_acc_balances[dimension_key][x.account].debit += flt(x.debit) - dimension_wise_acc_balances[dimension_key][x.account].credit += flt(x.credit) - - # dimension-wise total balances - dimension_wise_acc_balances[dimension_key].setdefault( - "balances", - frappe._dict( - { - "balance_in_account_currency": 0, - "balance_in_company_currency": 0, - } - ), - ) - - balance_in_account_currency = flt(x.debit_in_account_currency) - flt(x.credit_in_account_currency) - balance_in_company_currency = flt(x.debit) - flt(x.credit) - - dimension_wise_acc_balances[dimension_key][ - "balances" - ].balance_in_account_currency += balance_in_account_currency - dimension_wise_acc_balances[dimension_key][ - "balances" - ].balance_in_company_currency += balance_in_company_currency - frappe.db.set_value( "Process Period Closing Voucher Detail", {"processing_date": date, "parent": docname}, @@ -367,18 +340,11 @@ def process_individual_date(docname: str, date: str): "Completed", ) - # convert dict keys to json compliant json dictionary keys - json_dict = {} - for k, v in dimension_wise_acc_balances.items(): - str_key = [str(x) for x in k] - str_key = ",".join(str_key) - json_dict[str_key] = v - frappe.db.set_value( "Process Period Closing Voucher Detail", {"processing_date": date, "parent": docname}, "closing_balance", - frappe.json.dumps(json_dict), + frappe.json.dumps(res), ) call_next_date(docname) From 633ccef2ff8a3a6d9cd60716c54a72de4eced499 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 16 Oct 2025 13:13:33 +0530 Subject: [PATCH 13/86] refactor: for better readability (cherry picked from commit 8ba199016ab742e4848170d85f33011549e4f086) --- .../process_period_closing_voucher.js | 2 +- .../process_period_closing_voucher.py | 194 +++++++++--------- 2 files changed, 100 insertions(+), 96 deletions(-) diff --git a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js index 8f231f241be..40c760af647 100644 --- a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js @@ -44,7 +44,7 @@ frappe.ui.form.on("Process Period Closing Voucher", { frm.add_custom_button(execute_btn, () => { frm.call({ - method: "erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.call_next_date", + method: "erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.schedule_next_date", args: { docname: frm.doc.name, }, 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 ce82cbb53cf..bdfe63b6806 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 @@ -50,6 +50,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", "Paused"]: + # TODO: move this inside if block frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running") if dates_to_process := frappe.db.get_all( "Process Period Closing Voucher Detail", @@ -152,19 +153,19 @@ def get_gle_for_closing_account(pcv, dimension_balance, dimensions): @frappe.whitelist() -def call_next_date(docname: str): - if next_date_to_process := frappe.db.get_all( +def schedule_next_date(docname: str): + if to_process := frappe.db.get_all( "Process Period Closing Voucher Detail", filters={"parent": docname, "status": "Queued"}, fields=["processing_date"], order_by="processing_date", limit=1, ): - next_date_to_process = next_date_to_process[0].processing_date + next_date = to_process[0].processing_date if not is_scheduler_inactive(): frappe.db.set_value( "Process Period Closing Voucher Detail", - {"processing_date": next_date_to_process, "parent": docname}, + {"processing_date": next_date, "parent": docname}, "status", "Running", ) @@ -175,66 +176,68 @@ def call_next_date(docname: str): is_async=True, enqueue_after_commit=True, docname=docname, - date=next_date_to_process, + date=next_date, ) else: - process_individual_date(docname, next_date_to_process) + process_individual_date(docname, next_date) else: - running = frappe.db.get_all( - "Process Period Closing Voucher Detail", - filters={"parent": docname, "status": "Running"}, - fields=["processing_date"], - order_by="processing_date", - limit=1, - ) - # TODO: ensure all dates are processed - if not running: - # Calculate total balances for PCV period + # summarize, build and post GL + summarize_and_post_ledger_entries(docname) - ppcv = frappe.get_doc("Process Period Closing Voucher", docname) - gl_entries = [] - for x in ppcv.dates_to_process: - closing_balances = [frappe._dict(gle) for gle in frappe.json.loads(x.closing_balance)] - gl_entries.extend(closing_balances) +def summarize_and_post_ledger_entries(docname): + # TODO: ensure all dates are processed + running = frappe.db.get_all( + "Process Period Closing Voucher Detail", + filters={"parent": docname, "status": "Running"}, + fields=["processing_date"], + order_by="processing_date", + limit=1, + ) + if not running: + # calculate balances for whole PCV period + ppcv = frappe.get_doc("Process Period Closing Voucher", docname) - # Build dimension wise dictionary from all GLE's - dimension_wise_acc_balances = build_dimension_wise_balance_dict(gl_entries) + gl_entries = [] + for x in ppcv.dates_to_process: + closing_balances = [frappe._dict(gle) for gle in frappe.json.loads(x.closing_balance)] + gl_entries.extend(closing_balances) - # convert dict keys to json compliant json dictionary keys - json_dict = {} - for k, v in dimension_wise_acc_balances.items(): - str_key = [str(x) for x in k] - str_key = ",".join(str_key) - json_dict[str_key] = v + # build dimension wise dictionary from all GLE's + dimension_wise_acc_balances = build_dimension_wise_balance_dict(gl_entries) - frappe.db.set_value( - "Process Period Closing Voucher", docname, "total", frappe.json.dumps(json_dict) + # convert tuple key to str to make it json compliant + json_dict = {} + for k, v in dimension_wise_acc_balances.items(): + str_key = [str(x) for x in k] + str_key = ",".join(str_key) + json_dict[str_key] = v + + # save + frappe.db.set_value("Process Period Closing Voucher", docname, "total", frappe.json.dumps(json_dict)) + + # build gl map + pcv = frappe.get_doc("Period Closing Voucher", ppcv.parent_pcv) + pl_accounts_reverse_gle = [] + closing_account_gle = [] + + for dimensions, account_balances in dimension_wise_acc_balances.items(): + for acc, balances in account_balances.items(): + balance_in_company_currency = flt(balances.debit) - flt(balances.credit) + if balance_in_company_currency: + pl_accounts_reverse_gle.append(get_gle_for_pl_account(pcv, acc, balances, dimensions)) + + closing_account_gle.append( + get_gle_for_closing_account(pcv, account_balances["balances"], dimensions) ) - # Build GL map - pcv = frappe.get_doc("Period Closing Voucher", ppcv.parent_pcv) - pl_accounts_reverse_gle = [] - closing_account_gle = [] + gl_entries = pl_accounts_reverse_gle + closing_account_gle + from erpnext.accounts.general_ledger import make_gl_entries - for dimensions, account_balances in dimension_wise_acc_balances.items(): - for acc, balances in account_balances.items(): - balance_in_company_currency = flt(balances.debit) - flt(balances.credit) - if balance_in_company_currency: - pl_accounts_reverse_gle.append(get_gle_for_pl_account(pcv, acc, balances, dimensions)) + if gl_entries: + make_gl_entries(gl_entries, merge_entries=False) - # closing liability account - closing_account_gle.append( - get_gle_for_closing_account(pcv, account_balances["balances"], dimensions) - ) - - gl_entries = pl_accounts_reverse_gle + closing_account_gle - from erpnext.accounts.general_ledger import make_gl_entries - - if gl_entries: - make_gl_entries(gl_entries, merge_entries=False) - - frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed") + frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed") def get_dimensions(): @@ -299,52 +302,53 @@ def build_dimension_wise_balance_dict(gl_entries): def process_individual_date(docname: str, date: str): - if frappe.db.get_value("Process Period Closing Voucher", docname, "status") == "Running": - pcv_name = frappe.db.get_value("Process Period Closing Voucher", docname, "parent_pcv") - pcv = frappe.get_doc("Period Closing Voucher", pcv_name) + if frappe.db.get_value("Process Period Closing Voucher", docname, "status") != "Running": + return - dimensions = get_dimensions() + pcv_name = frappe.db.get_value("Process Period Closing Voucher", docname, "parent_pcv") + company = frappe.db.get_value("Period Closing Voucher", pcv_name, "company") - p_l_accounts = frappe.db.get_all( - "Account", filters={"company": pcv.company, "report_type": "Profit and Loss"}, pluck="name" - ) + dimensions = get_dimensions() - gle = qb.DocType("GL Entry") - query = qb.from_(gle).select(gle.account) - for dim in dimensions: - query = query.select(gle[dim]) + p_l_accounts = frappe.db.get_all( + "Account", filters={"company": company, "report_type": "Profit and Loss"}, pluck="name" + ) - query = query.select( - Sum(gle.debit).as_("debit"), - Sum(gle.credit).as_("credit"), - Sum(gle.debit_in_account_currency).as_("debit_in_account_currency"), - Sum(gle.credit_in_account_currency).as_("credit_in_account_currency"), - gle.account_currency, - ).where( - (gle.company.eq(pcv.company)) - & (gle.is_cancelled.eq(0)) - & (gle.posting_date.eq(date)) - & (gle.account.isin(p_l_accounts)) - ) + # summarize + gle = qb.DocType("GL Entry") + query = qb.from_(gle).select(gle.account) + for dim in dimensions: + query = query.select(gle[dim]) + query = query.select( + Sum(gle.debit).as_("debit"), + Sum(gle.credit).as_("credit"), + Sum(gle.debit_in_account_currency).as_("debit_in_account_currency"), + Sum(gle.credit_in_account_currency).as_("credit_in_account_currency"), + gle.account_currency, + ).where( + (gle.company.eq(company)) + & (gle.is_cancelled.eq(0)) + & (gle.posting_date.eq(date)) + & (gle.account.isin(p_l_accounts)) + ) + query = query.groupby(gle.account) + for dim in dimensions: + query = query.groupby(gle[dim]) + res = query.run(as_dict=True) - query = query.groupby(gle.account) - for dim in dimensions: - query = query.groupby(gle[dim]) + # save results + frappe.db.set_value( + "Process Period Closing Voucher Detail", + {"processing_date": date, "parent": docname}, + "closing_balance", + frappe.json.dumps(res), + ) + frappe.db.set_value( + "Process Period Closing Voucher Detail", + {"processing_date": date, "parent": docname}, + "status", + "Completed", + ) - res = query.run(as_dict=True) - - frappe.db.set_value( - "Process Period Closing Voucher Detail", - {"processing_date": date, "parent": docname}, - "status", - "Completed", - ) - - frappe.db.set_value( - "Process Period Closing Voucher Detail", - {"processing_date": date, "parent": docname}, - "closing_balance", - frappe.json.dumps(res), - ) - - call_next_date(docname) + # chain call + schedule_next_date(docname) From 908f8ed462e60c2a055817c339fb00cb45412a8d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 16 Oct 2025 15:34:08 +0530 Subject: [PATCH 14/86] refactor: process on submit (cherry picked from commit c738b6d3563a758314ac9d73a0b5faef93ce5409) --- .../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 bdfe63b6806..3d3c022298d 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 @@ -46,6 +46,9 @@ class ProcessPeriodClosingVoucher(Document): for x in dates: self.append("dates_to_process", {"processing_date": x, "status": "Queued"}) + def on_submit(self): + start_pcv_processing(self.name) + @frappe.whitelist() def start_pcv_processing(docname: str): From d911e1dab282f452d192746e11dfd1bf06cc8b79 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 16 Oct 2025 17:05:43 +0530 Subject: [PATCH 15/86] refactor: more stable pause and resume (cherry picked from commit 9e93298f12f5841a1865e73a1f41ce890cc76eaf) --- .../process_period_closing_voucher.js | 12 ++-- .../process_period_closing_voucher.py | 57 ++++++++++++++++--- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js index 40c760af647..23d410e9bb0 100644 --- a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js @@ -3,7 +3,7 @@ frappe.ui.form.on("Process Period Closing Voucher", { refresh(frm) { - if (frm.doc.docstatus == 1 && ["Queued", "Paused"].find((x) => x == frm.doc.status)) { + if (frm.doc.docstatus == 1 && ["Queued"].find((x) => x == frm.doc.status)) { let execute_btn = __("Start"); frm.add_custom_button(execute_btn, () => { @@ -32,25 +32,25 @@ frappe.ui.form.on("Process Period Closing Voucher", { }, }).then((r) => { if (!r.exc) { - frappe.show_alert(__("Job Started")); + frappe.show_alert(__("PCV Paused")); frm.reload_doc(); } }); }); } - if (frm.doc.docstatus == 1 && ["Completed"].find((x) => x == frm.doc.status)) { - let execute_btn = __("Call Next"); + if (frm.doc.docstatus == 1 && ["Paused"].find((x) => x == frm.doc.status)) { + let execute_btn = __("Resume"); frm.add_custom_button(execute_btn, () => { frm.call({ - method: "erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.schedule_next_date", + method: "erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.resume_pcv_processing", args: { docname: frm.doc.name, }, }).then((r) => { if (!r.exc) { - frappe.show_alert(__("Call next date for processing")); + frappe.show_alert(__("PCV Resumed")); frm.reload_doc(); } }); 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 3d3c022298d..f3697c7acb3 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 @@ -6,11 +6,11 @@ from datetime import timedelta import frappe from frappe import qb from frappe.model.document import Document -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import Count, Sum from frappe.utils import add_days, flt, get_datetime from frappe.utils.scheduler import is_scheduler_inactive -BACKGROUND = False +BACKGROUND = True class ProcessPeriodClosingVoucher(Document): @@ -52,7 +52,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", "Paused"]: + if frappe.db.get_value("Process Period Closing Voucher", docname, "status") in ["Queued", "Running"]: # TODO: move this inside if block frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running") if dates_to_process := frappe.db.get_all( @@ -64,6 +64,13 @@ def start_pcv_processing(docname: str): ): if not is_scheduler_inactive(): for x in dates_to_process: + frappe.db.set_value( + "Process Period Closing Voucher Detail", + {"processing_date": x.processing_date, "parent": docname}, + "status", + "Running", + ) + if BACKGROUND: frappe.enqueue( method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date", @@ -84,13 +91,28 @@ def pause_pcv_processing(docname: str): ppcv = qb.DocType("Process Period Closing Voucher") qb.update(ppcv).set(ppcv.status, "Paused").where(ppcv.name.eq(docname)).run() - queued_dates = frappe.db.get_all( + if queued_dates := frappe.db.get_all( "Process Period Closing Voucher Detail", filters={"parent": docname, "status": "Queued"}, pluck="name", - ) - ppcvd = qb.DocType("Process Period Closing Voucher Detail") - qb.update(ppcvd).set(ppcvd.status, "Paused").where(ppcvd.name.isin(queued_dates)).run() + ): + ppcvd = qb.DocType("Process Period Closing Voucher Detail") + qb.update(ppcvd).set(ppcvd.status, "Paused").where(ppcvd.name.isin(queued_dates)).run() + + +@frappe.whitelist() +def resume_pcv_processing(docname: str): + ppcv = qb.DocType("Process Period Closing Voucher") + qb.update(ppcv).set(ppcv.status, "Running").where(ppcv.name.eq(docname)).run() + + if paused_dates := frappe.db.get_all( + "Process Period Closing Voucher Detail", + filters={"parent": docname, "status": "Paused"}, + pluck="name", + ): + ppcvd = qb.DocType("Process Period Closing Voucher Detail") + qb.update(ppcvd).set(ppcvd.status, "Queued").where(ppcvd.name.isin(paused_dates)).run() + start_pcv_processing(docname) def get_gle_for_pl_account(pcv, acc, balances, dimensions): @@ -185,7 +207,18 @@ def schedule_next_date(docname: str): process_individual_date(docname, next_date) else: # summarize, build and post GL - summarize_and_post_ledger_entries(docname) + ppcvd = qb.DocType("Process Period Closing Voucher Detail") + total_no_of_dates = ( + qb.from_(ppcvd).select(Count(ppcvd.star)).where(ppcvd.parent.eq(docname)).run()[0][0] + ) + completed = ( + qb.from_(ppcvd) + .select(Count(ppcvd.star)) + .where(ppcvd.parent.eq(docname) & ppcvd.status.eq("Completed")) + .run()[0][0] + ) + if total_no_of_dates == completed: + summarize_and_post_ledger_entries(docname) def summarize_and_post_ledger_entries(docname): @@ -305,7 +338,12 @@ def build_dimension_wise_balance_dict(gl_entries): def process_individual_date(docname: str, date: str): - if frappe.db.get_value("Process Period Closing Voucher", docname, "status") != "Running": + current_date_status = frappe.db.get_value( + "Process Period Closing Voucher Detail", + {"processing_date": date, "parent": docname}, + "status", + ) + if current_date_status != "Running": return pcv_name = frappe.db.get_value("Process Period Closing Voucher", docname, "parent_pcv") @@ -346,6 +384,7 @@ def process_individual_date(docname: str, date: str): "closing_balance", frappe.json.dumps(res), ) + frappe.db.set_value( "Process Period Closing Voucher Detail", {"processing_date": date, "parent": docname}, From 6c94ca664f4b30bd93ee4b96f6b08786a2f7eabe Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Oct 2025 11:27:03 +0530 Subject: [PATCH 16/86] refactor: maintain report type on each date (cherry picked from commit 186d540502130cb66aa53b2aaa2d3855b8561616) --- .../process_period_closing_voucher_detail.json | 11 ++++++++++- .../process_period_closing_voucher_detail.py | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json b/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json index 6d452c18fc0..b64871b2ff0 100644 --- a/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json +++ b/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "processing_date", + "report_type", "status", "closing_balance" ], @@ -29,13 +30,21 @@ "fieldtype": "JSON", "in_list_view": 1, "label": "Closing Balance" + }, + { + "default": "Profit and Loss", + "fieldname": "report_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Report Type", + "options": "Profit and Loss\nBalance Sheet" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-10-09 16:46:37.778199", + "modified": "2025-10-17 11:28:34.775743", "modified_by": "Administrator", "module": "Accounts", "name": "Process Period Closing Voucher Detail", 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 f15d917a295..6bec6622ee0 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 @@ -19,6 +19,7 @@ class ProcessPeriodClosingVoucherDetail(Document): parentfield: DF.Data parenttype: DF.Data processing_date: DF.Date | None + report_type: DF.Literal["Profit and Loss", "Balance Sheet"] status: DF.Literal["Queued", "Running", "Paused", "Completed"] # end: auto-generated types From 87e297e8997d1279defef3210c758401a9b5c138 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Oct 2025 11:49:15 +0530 Subject: [PATCH 17/86] refactor: balances for both P&L and Balance sheet accounts (cherry picked from commit 324bebfd44e4351c04c90072cc7719fa97c6981a) --- .../process_period_closing_voucher.py | 50 ++++++++++++------- 1 file changed, 32 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 f3697c7acb3..bb31b2fdcca 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 @@ -44,7 +44,13 @@ class ProcessPeriodClosingVoucher(Document): end = get_datetime(pcv.period_end_date) dates = [start + timedelta(days=x) for x in range((end - start).days + 1)] for x in dates: - self.append("dates_to_process", {"processing_date": x, "status": "Queued"}) + self.append( + "dates_to_process", + {"processing_date": x, "status": "Queued", "report_type": "Profit and Loss"}, + ) + self.append( + "dates_to_process", {"processing_date": x, "status": "Queued", "report_type": "Balance Sheet"} + ) def on_submit(self): start_pcv_processing(self.name) @@ -58,15 +64,19 @@ def start_pcv_processing(docname: str): if dates_to_process := frappe.db.get_all( "Process Period Closing Voucher Detail", filters={"parent": docname, "status": "Queued"}, - fields=["processing_date"], + fields=["processing_date", "report_type"], order_by="processing_date", - limit=4, + limit=1, ): if not is_scheduler_inactive(): for x in dates_to_process: frappe.db.set_value( "Process Period Closing Voucher Detail", - {"processing_date": x.processing_date, "parent": docname}, + { + "processing_date": x.processing_date, + "parent": docname, + "report_type": x.report_type, + }, "status", "Running", ) @@ -79,9 +89,10 @@ def start_pcv_processing(docname: str): enqueue_after_commit=True, docname=docname, date=x.processing_date, + report_type=x.report_type, ) else: - process_individual_date(docname, x.processing_date) + process_individual_date(docname, x.processing_date, x.report_type) else: frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed") @@ -182,15 +193,16 @@ def schedule_next_date(docname: str): if to_process := frappe.db.get_all( "Process Period Closing Voucher Detail", filters={"parent": docname, "status": "Queued"}, - fields=["processing_date"], + fields=["processing_date", "report_type"], order_by="processing_date", - limit=1, + limit=4, ): next_date = to_process[0].processing_date + report_type = to_process[0].report_type if not is_scheduler_inactive(): frappe.db.set_value( "Process Period Closing Voucher Detail", - {"processing_date": next_date, "parent": docname}, + {"processing_date": next_date, "parent": docname, "report_type": report_type}, "status", "Running", ) @@ -202,9 +214,10 @@ def schedule_next_date(docname: str): enqueue_after_commit=True, docname=docname, date=next_date, + report_type=report_type, ) else: - process_individual_date(docname, next_date) + process_individual_date(docname, next_date, report_type) else: # summarize, build and post GL ppcvd = qb.DocType("Process Period Closing Voucher Detail") @@ -236,8 +249,9 @@ def summarize_and_post_ledger_entries(docname): gl_entries = [] for x in ppcv.dates_to_process: - closing_balances = [frappe._dict(gle) for gle in frappe.json.loads(x.closing_balance)] - gl_entries.extend(closing_balances) + if x.report_type == "Profit and Loss": + closing_balances = [frappe._dict(gle) for gle in frappe.json.loads(x.closing_balance)] + gl_entries.extend(closing_balances) # build dimension wise dictionary from all GLE's dimension_wise_acc_balances = build_dimension_wise_balance_dict(gl_entries) @@ -337,10 +351,10 @@ def build_dimension_wise_balance_dict(gl_entries): return dimension_balances -def process_individual_date(docname: str, date: str): +def process_individual_date(docname: str, date: str, report_type): current_date_status = frappe.db.get_value( "Process Period Closing Voucher Detail", - {"processing_date": date, "parent": docname}, + {"processing_date": date, "parent": docname, "report_type": report_type}, "status", ) if current_date_status != "Running": @@ -351,8 +365,8 @@ def process_individual_date(docname: str, date: str): dimensions = get_dimensions() - p_l_accounts = frappe.db.get_all( - "Account", filters={"company": company, "report_type": "Profit and Loss"}, pluck="name" + accounts = frappe.db.get_all( + "Account", filters={"company": company, "report_type": report_type}, pluck="name" ) # summarize @@ -370,7 +384,7 @@ def process_individual_date(docname: str, date: str): (gle.company.eq(company)) & (gle.is_cancelled.eq(0)) & (gle.posting_date.eq(date)) - & (gle.account.isin(p_l_accounts)) + & (gle.account.isin(accounts)) ) query = query.groupby(gle.account) for dim in dimensions: @@ -380,14 +394,14 @@ def process_individual_date(docname: str, date: str): # save results frappe.db.set_value( "Process Period Closing Voucher Detail", - {"processing_date": date, "parent": docname}, + {"processing_date": date, "parent": docname, "report_type": report_type}, "closing_balance", frappe.json.dumps(res), ) frappe.db.set_value( "Process Period Closing Voucher Detail", - {"processing_date": date, "parent": docname}, + {"processing_date": date, "parent": docname, "report_type": report_type}, "status", "Completed", ) From 2c880dd6099bd5e2733045e03965e295a80e496d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Oct 2025 13:05:03 +0530 Subject: [PATCH 18/86] chore: rename closing balance field (cherry picked from commit cef879bb3b6d50f4c61ddd01d63d774d32a7cb58) --- .../process_period_closing_voucher.json | 8 ++++---- .../process_period_closing_voucher.py | 6 ++++-- 2 files changed, 8 insertions(+), 6 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 5db50764153..aebdfd1fb75 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 @@ -7,7 +7,7 @@ "field_order": [ "parent_pcv", "status", - "total", + "p_l_closing_balance", "dates_to_process", "amended_from" ], @@ -46,9 +46,9 @@ "search_index": 1 }, { - "fieldname": "total", + "fieldname": "p_l_closing_balance", "fieldtype": "JSON", - "label": "Total", + "label": "P&L Closing Balance", "no_copy": 1 } ], @@ -56,7 +56,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-10-15 12:46:03.627166", + "modified": "2025-10-17 13:04:26.353250", "modified_by": "Administrator", "module": "Accounts", "name": "Process Period Closing Voucher", 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 bb31b2fdcca..523a3b23bf5 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 @@ -28,9 +28,9 @@ class ProcessPeriodClosingVoucher(Document): amended_from: DF.Link | None dates_to_process: DF.Table[ProcessPeriodClosingVoucherDetail] + p_l_closing_balance: DF.JSON | None parent_pcv: DF.Link status: DF.Literal["Queued", "Running", "Completed"] - total: DF.JSON | None # end: auto-generated types def validate(self): @@ -264,7 +264,9 @@ def summarize_and_post_ledger_entries(docname): json_dict[str_key] = v # save - frappe.db.set_value("Process Period Closing Voucher", docname, "total", frappe.json.dumps(json_dict)) + frappe.db.set_value( + "Process Period Closing Voucher", docname, "p_l_closing_balance", frappe.json.dumps(json_dict) + ) # build gl map pcv = frappe.get_doc("Period Closing Voucher", ppcv.parent_pcv) From f381b99b14007e4a5cbcd80b15713b960263c26c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Oct 2025 13:16:56 +0530 Subject: [PATCH 19/86] refactor: populate opening balances calculation table (cherry picked from commit 86edacb781107211d99604fc9c32312ba72dd3d4) --- .../process_period_closing_voucher.json | 10 ++++- .../process_period_closing_voucher.py | 37 +++++++++++++++---- 2 files changed, 39 insertions(+), 8 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 aebdfd1fb75..6ad27c880b1 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 @@ -9,6 +9,7 @@ "status", "p_l_closing_balance", "dates_to_process", + "opening_balances", "amended_from" ], "fields": [ @@ -50,13 +51,20 @@ "fieldtype": "JSON", "label": "P&L Closing Balance", "no_copy": 1 + }, + { + "fieldname": "opening_balances", + "fieldtype": "Table", + "label": "Opening Balances", + "no_copy": 1, + "options": "Process Period Closing Voucher Detail" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-10-17 13:04:26.353250", + "modified": "2025-10-17 13:10:04.024903", "modified_by": "Administrator", "module": "Accounts", "name": "Process Period Closing Voucher", 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 523a3b23bf5..9939b61dc1e 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 @@ -6,7 +6,7 @@ from datetime import timedelta import frappe from frappe import qb from frappe.model.document import Document -from frappe.query_builder.functions import Count, Sum +from frappe.query_builder.functions import Count, Max, Min, Sum from frappe.utils import add_days, flt, get_datetime from frappe.utils.scheduler import is_scheduler_inactive @@ -28,6 +28,7 @@ class ProcessPeriodClosingVoucher(Document): amended_from: DF.Link | None dates_to_process: DF.Table[ProcessPeriodClosingVoucherDetail] + opening_balances: DF.Table[ProcessPeriodClosingVoucherDetail] p_l_closing_balance: DF.JSON | None parent_pcv: DF.Link status: DF.Literal["Queued", "Running", "Completed"] @@ -35,14 +36,20 @@ class ProcessPeriodClosingVoucher(Document): def validate(self): self.status = "Queued" - self.populate_processing_table() + self.populate_processing_tables() - def populate_processing_table(self): + def populate_processing_tables(self): + self.generate_pcv_dates() + self.generate_opening_balances_dates() + + def get_dates(self, start, end): + return [start + timedelta(days=x) for x in range((end - start).days + 1)] + + def generate_pcv_dates(self): self.dates_to_process = [] pcv = frappe.get_doc("Period Closing Voucher", self.parent_pcv) - start = get_datetime(pcv.period_start_date) - end = get_datetime(pcv.period_end_date) - dates = [start + timedelta(days=x) for x in range((end - start).days + 1)] + + dates = self.get_dates(get_datetime(pcv.period_start_date), get_datetime(pcv.period_end_date)) for x in dates: self.append( "dates_to_process", @@ -52,6 +59,22 @@ class ProcessPeriodClosingVoucher(Document): "dates_to_process", {"processing_date": x, "status": "Queued", "report_type": "Balance Sheet"} ) + def generate_opening_balances_dates(self): + self.opening_balances = [] + + pcv = frappe.get_doc("Period Closing Voucher", self.parent_pcv) + if pcv.is_first_period_closing_voucher(): + gl = qb.DocType("GL Entry") + min = qb.from_(gl).select(Min(gl.posting_date)).where(gl.company.eq(pcv.company)).run()[0][0] + max = qb.from_(gl).select(Max(gl.posting_date)).where(gl.company.eq(pcv.company)).run()[0][0] + + dates = self.get_dates(get_datetime(min), get_datetime(max)) + for x in dates: + self.append( + "opening_balances", + {"processing_date": x, "status": "Queued", "report_type": "Balance Sheet"}, + ) + def on_submit(self): start_pcv_processing(self.name) @@ -66,7 +89,7 @@ def start_pcv_processing(docname: str): filters={"parent": docname, "status": "Queued"}, fields=["processing_date", "report_type"], order_by="processing_date", - limit=1, + limit=4, ): if not is_scheduler_inactive(): for x in dates_to_process: From 55222468f9812998c79285189ae22f5889f29d8d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Oct 2025 14:42:29 +0530 Subject: [PATCH 20/86] refactor: calculate both balances from single queue (cherry picked from commit 643e1fdce8b30d532ff7bf4128b385d34e2aa047) --- .../process_period_closing_voucher.json | 22 +++--- .../process_period_closing_voucher.py | 67 ++++++++++++------- 2 files changed, 52 insertions(+), 37 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 6ad27c880b1..9b89b405eed 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 @@ -8,8 +8,8 @@ "parent_pcv", "status", "p_l_closing_balance", - "dates_to_process", - "opening_balances", + "normal_balances", + "z_opening_balances", "amended_from" ], "fields": [ @@ -29,13 +29,6 @@ "no_copy": 1, "options": "Queued\nRunning\nCompleted" }, - { - "fieldname": "dates_to_process", - "fieldtype": "Table", - "label": "Dates to Process", - "no_copy": 1, - "options": "Process Period Closing Voucher Detail" - }, { "fieldname": "amended_from", "fieldtype": "Link", @@ -53,7 +46,14 @@ "no_copy": 1 }, { - "fieldname": "opening_balances", + "fieldname": "normal_balances", + "fieldtype": "Table", + "label": "Dates to Process", + "no_copy": 1, + "options": "Process Period Closing Voucher Detail" + }, + { + "fieldname": "z_opening_balances", "fieldtype": "Table", "label": "Opening Balances", "no_copy": 1, @@ -64,7 +64,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-10-17 13:10:04.024903", + "modified": "2025-10-17 13:44:33.397172", "modified_by": "Administrator", "module": "Accounts", "name": "Process Period Closing Voucher", 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 9939b61dc1e..54363ec2889 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 @@ -27,11 +27,11 @@ class ProcessPeriodClosingVoucher(Document): ) amended_from: DF.Link | None - dates_to_process: DF.Table[ProcessPeriodClosingVoucherDetail] - opening_balances: DF.Table[ProcessPeriodClosingVoucherDetail] + normal_balances: DF.Table[ProcessPeriodClosingVoucherDetail] p_l_closing_balance: DF.JSON | None parent_pcv: DF.Link status: DF.Literal["Queued", "Running", "Completed"] + z_opening_balances: DF.Table[ProcessPeriodClosingVoucherDetail] # end: auto-generated types def validate(self): @@ -46,21 +46,21 @@ class ProcessPeriodClosingVoucher(Document): return [start + timedelta(days=x) for x in range((end - start).days + 1)] def generate_pcv_dates(self): - self.dates_to_process = [] + self.normal_balances = [] pcv = frappe.get_doc("Period Closing Voucher", self.parent_pcv) dates = self.get_dates(get_datetime(pcv.period_start_date), get_datetime(pcv.period_end_date)) for x in dates: self.append( - "dates_to_process", + "normal_balances", {"processing_date": x, "status": "Queued", "report_type": "Profit and Loss"}, ) self.append( - "dates_to_process", {"processing_date": x, "status": "Queued", "report_type": "Balance Sheet"} + "normal_balances", {"processing_date": x, "status": "Queued", "report_type": "Balance Sheet"} ) def generate_opening_balances_dates(self): - self.opening_balances = [] + self.z_opening_balances = [] pcv = frappe.get_doc("Period Closing Voucher", self.parent_pcv) if pcv.is_first_period_closing_voucher(): @@ -71,7 +71,7 @@ class ProcessPeriodClosingVoucher(Document): dates = self.get_dates(get_datetime(min), get_datetime(max)) for x in dates: self.append( - "opening_balances", + "z_opening_balances", {"processing_date": x, "status": "Queued", "report_type": "Balance Sheet"}, ) @@ -84,21 +84,22 @@ def start_pcv_processing(docname: str): if frappe.db.get_value("Process Period Closing Voucher", docname, "status") in ["Queued", "Running"]: # TODO: move this inside if block frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running") - if dates_to_process := frappe.db.get_all( + if normal_balances := frappe.db.get_all( "Process Period Closing Voucher Detail", filters={"parent": docname, "status": "Queued"}, - fields=["processing_date", "report_type"], - order_by="processing_date", + fields=["processing_date", "report_type", "parentfield"], + order_by="parentfield, idx, processing_date", limit=4, ): if not is_scheduler_inactive(): - for x in dates_to_process: + 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, }, "status", "Running", @@ -113,9 +114,10 @@ def start_pcv_processing(docname: str): docname=docname, date=x.processing_date, report_type=x.report_type, + parentfield=x.parentfield, ) else: - process_individual_date(docname, x.processing_date, x.report_type) + process_individual_date(docname, x.processing_date, x.report_type, x.parentfield) else: frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed") @@ -216,16 +218,19 @@ def schedule_next_date(docname: str): if to_process := frappe.db.get_all( "Process Period Closing Voucher Detail", filters={"parent": docname, "status": "Queued"}, - fields=["processing_date", "report_type"], - order_by="processing_date", - limit=4, + fields=["processing_date", "report_type", "parentfield"], + order_by="parentfield, idx, processing_date", + limit=1, ): - next_date = to_process[0].processing_date - report_type = to_process[0].report_type if not is_scheduler_inactive(): frappe.db.set_value( "Process Period Closing Voucher Detail", - {"processing_date": next_date, "parent": docname, "report_type": report_type}, + { + "processing_date": to_process[0].processing_date, + "parent": docname, + "report_type": to_process[0].report_type, + "parentfield": to_process[0].parentfield, + }, "status", "Running", ) @@ -236,11 +241,17 @@ def schedule_next_date(docname: str): is_async=True, enqueue_after_commit=True, docname=docname, - date=next_date, - report_type=report_type, + date=to_process[0].processing_date, + report_type=to_process[0].report_type, + parentfield=to_process[0].parentfield, ) else: - process_individual_date(docname, next_date, report_type) + process_individual_date( + docname, + to_process[0].processing_date, + to_process[0].report_type, + to_process[0].parentfield, + ) else: # summarize, build and post GL ppcvd = qb.DocType("Process Period Closing Voucher Detail") @@ -271,7 +282,7 @@ def summarize_and_post_ledger_entries(docname): ppcv = frappe.get_doc("Process Period Closing Voucher", docname) gl_entries = [] - for x in ppcv.dates_to_process: + for x in ppcv.normal_balances: if x.report_type == "Profit and Loss": closing_balances = [frappe._dict(gle) for gle in frappe.json.loads(x.closing_balance)] gl_entries.extend(closing_balances) @@ -376,10 +387,10 @@ def build_dimension_wise_balance_dict(gl_entries): return dimension_balances -def process_individual_date(docname: str, date: str, report_type): +def process_individual_date(docname: str, date, report_type, parentfield): current_date_status = frappe.db.get_value( "Process Period Closing Voucher Detail", - {"processing_date": date, "parent": docname, "report_type": report_type}, + {"processing_date": date, "report_type": report_type, "parentfield": parentfield}, "status", ) if current_date_status != "Running": @@ -411,6 +422,10 @@ def process_individual_date(docname: str, date: str, report_type): & (gle.posting_date.eq(date)) & (gle.account.isin(accounts)) ) + + if parentfield == "z_opening_balances": + query = query.where(gle.is_opening.eq("Yes")) + query = query.groupby(gle.account) for dim in dimensions: query = query.groupby(gle[dim]) @@ -419,14 +434,14 @@ def process_individual_date(docname: str, date: str, report_type): # save results frappe.db.set_value( "Process Period Closing Voucher Detail", - {"processing_date": date, "parent": docname, "report_type": report_type}, + {"processing_date": date, "parent": docname, "report_type": report_type, "parentfield": parentfield}, "closing_balance", frappe.json.dumps(res), ) frappe.db.set_value( "Process Period Closing Voucher Detail", - {"processing_date": date, "parent": docname, "report_type": report_type}, + {"processing_date": date, "parent": docname, "report_type": report_type, "parentfield": parentfield}, "status", "Completed", ) From 449fa05d7d62bbbbf8f2ca6d9ad4576fb3463e14 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Oct 2025 15:17:26 +0530 Subject: [PATCH 21/86] refactor: store closing balance for Balnace sheet accounts (cherry picked from commit 09e37bc98c0b27cbbc621c66ed47f0e753b3b28b) --- .../process_period_closing_voucher.json | 8 ++++- .../process_period_closing_voucher.py | 31 +++++++++++++++++-- 2 files changed, 35 insertions(+), 4 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 9b89b405eed..fd0186557dc 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 @@ -9,6 +9,7 @@ "status", "p_l_closing_balance", "normal_balances", + "bs_closing_balance", "z_opening_balances", "amended_from" ], @@ -58,13 +59,18 @@ "label": "Opening Balances", "no_copy": 1, "options": "Process Period Closing Voucher Detail" + }, + { + "fieldname": "bs_closing_balance", + "fieldtype": "JSON", + "label": "Balance Sheet Closing Balance" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-10-17 13:44:33.397172", + "modified": "2025-10-17 15:16:26.324369", "modified_by": "Administrator", "module": "Accounts", "name": "Process Period Closing Voucher", 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 54363ec2889..7905465f9dd 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 @@ -27,6 +27,7 @@ class ProcessPeriodClosingVoucher(Document): ) amended_from: DF.Link | None + bs_closing_balance: DF.JSON | None normal_balances: DF.Table[ProcessPeriodClosingVoucherDetail] p_l_closing_balance: DF.JSON | None parent_pcv: DF.Link @@ -258,6 +259,7 @@ def schedule_next_date(docname: str): total_no_of_dates = ( qb.from_(ppcvd).select(Count(ppcvd.star)).where(ppcvd.parent.eq(docname)).run()[0][0] ) + # consider both normal and opening balance completed = ( qb.from_(ppcvd) .select(Count(ppcvd.star)) @@ -281,6 +283,7 @@ def summarize_and_post_ledger_entries(docname): # calculate balances for whole PCV period ppcv = frappe.get_doc("Process Period Closing Voucher", docname) + # P&L Accounts gl_entries = [] for x in ppcv.normal_balances: if x.report_type == "Profit and Loss": @@ -288,11 +291,11 @@ def summarize_and_post_ledger_entries(docname): gl_entries.extend(closing_balances) # build dimension wise dictionary from all GLE's - dimension_wise_acc_balances = build_dimension_wise_balance_dict(gl_entries) + pl_dimension_wise_acc_balance = build_dimension_wise_balance_dict(gl_entries) # convert tuple key to str to make it json compliant json_dict = {} - for k, v in dimension_wise_acc_balances.items(): + for k, v in pl_dimension_wise_acc_balance.items(): str_key = [str(x) for x in k] str_key = ",".join(str_key) json_dict[str_key] = v @@ -302,12 +305,34 @@ def summarize_and_post_ledger_entries(docname): "Process Period Closing Voucher", docname, "p_l_closing_balance", frappe.json.dumps(json_dict) ) + # Balance Sheet Accounts + gl_entries = [] + for x in ppcv.normal_balances + ppcv.z_opening_balances: + if x.report_type == "Balance Sheet": + closing_balances = [frappe._dict(gle) for gle in frappe.json.loads(x.closing_balance)] + gl_entries.extend(closing_balances) + + # build dimension wise dictionary from all GLE's + bs_dimension_wise_acc_balance = build_dimension_wise_balance_dict(gl_entries) + + # convert tuple key to str to make it json compliant + json_dict = {} + for k, v in bs_dimension_wise_acc_balance.items(): + str_key = [str(x) for x in k] + str_key = ",".join(str_key) + json_dict[str_key] = v + + # save + frappe.db.set_value( + "Process Period Closing Voucher", docname, "bs_closing_balance", frappe.json.dumps(json_dict) + ) + # build gl map pcv = frappe.get_doc("Period Closing Voucher", ppcv.parent_pcv) pl_accounts_reverse_gle = [] closing_account_gle = [] - for dimensions, account_balances in dimension_wise_acc_balances.items(): + for dimensions, account_balances in pl_dimension_wise_acc_balance.items(): for acc, balances in account_balances.items(): balance_in_company_currency = flt(balances.debit) - flt(balances.credit) if balance_in_company_currency: From 3406e44b037ee09f0e5aa4605a0a8466fe888622 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Oct 2025 15:35:32 +0530 Subject: [PATCH 22/86] refactor: make Accounts Closing Balance as well (cherry picked from commit 6e32769e37b94dd268374fbba9658f82de97aaf2) --- .../process_period_closing_voucher.py | 84 +++++++++++++++---- 1 file changed, 69 insertions(+), 15 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 7905465f9dd..63e901b8af4 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 @@ -1,6 +1,7 @@ # Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +import copy from datetime import timedelta import frappe @@ -10,6 +11,10 @@ from frappe.query_builder.functions import Count, Max, Min, Sum from frappe.utils import add_days, flt, get_datetime from frappe.utils.scheduler import is_scheduler_inactive +from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import ( + make_closing_entries, +) + BACKGROUND = True @@ -305,6 +310,27 @@ def summarize_and_post_ledger_entries(docname): "Process Period Closing Voucher", docname, "p_l_closing_balance", frappe.json.dumps(json_dict) ) + # build gl map + pcv = frappe.get_doc("Period Closing Voucher", ppcv.parent_pcv) + pl_accounts_reverse_gle = [] + closing_account_gle = [] + + for dimensions, account_balances in pl_dimension_wise_acc_balance.items(): + for acc, balances in account_balances.items(): + balance_in_company_currency = flt(balances.debit) - flt(balances.credit) + if balance_in_company_currency: + pl_accounts_reverse_gle.append(get_gle_for_pl_account(pcv, acc, balances, dimensions)) + + closing_account_gle.append( + get_gle_for_closing_account(pcv, account_balances["balances"], dimensions) + ) + + gl_entries = pl_accounts_reverse_gle + closing_account_gle + from erpnext.accounts.general_ledger import make_gl_entries + + if gl_entries: + make_gl_entries(gl_entries, merge_entries=False) + # Balance Sheet Accounts gl_entries = [] for x in ppcv.normal_balances + ppcv.z_opening_balances: @@ -327,30 +353,58 @@ def summarize_and_post_ledger_entries(docname): "Process Period Closing Voucher", docname, "bs_closing_balance", frappe.json.dumps(json_dict) ) - # build gl map - pcv = frappe.get_doc("Period Closing Voucher", ppcv.parent_pcv) - pl_accounts_reverse_gle = [] - closing_account_gle = [] + # make closing entries + pl_closing_entries = copy.deepcopy(pl_accounts_reverse_gle) + for d in pl_accounts_reverse_gle: + # reverse debit and credit + gle_copy = copy.deepcopy(d) + gle_copy.debit = d.credit + gle_copy.credit = d.debit + gle_copy.debit_in_account_currency = d.credit_in_account_currency + gle_copy.credit_in_account_currency = d.debit_in_account_currency + gle_copy.is_period_closing_voucher_entry = 0 + gle_copy.period_closing_voucher = pcv.name + pl_closing_entries.append(gle_copy) - for dimensions, account_balances in pl_dimension_wise_acc_balance.items(): + bs_closing_entries = [] + for dimensions, account_balances in bs_dimension_wise_acc_balance.items(): for acc, balances in account_balances.items(): balance_in_company_currency = flt(balances.debit) - flt(balances.credit) - if balance_in_company_currency: - pl_accounts_reverse_gle.append(get_gle_for_pl_account(pcv, acc, balances, dimensions)) + if acc != "balances" and balance_in_company_currency: + bs_closing_entries.append(get_closing_entry(pcv, acc, balances, dimensions)) - closing_account_gle.append( - get_gle_for_closing_account(pcv, account_balances["balances"], dimensions) - ) + closing_entries_for_closing_account = copy.deepcopy(closing_account_gle) + for d in closing_entries_for_closing_account: + d.period_closing_voucher = pcv.name - gl_entries = pl_accounts_reverse_gle + closing_account_gle - from erpnext.accounts.general_ledger import make_gl_entries - - if gl_entries: - make_gl_entries(gl_entries, merge_entries=False) + closing_entries = pl_closing_entries + bs_closing_entries + closing_entries_for_closing_account + make_closing_entries(closing_entries, pcv.name, pcv.company, pcv.period_end_date) + # TODO: Update processing status on PCV and Process document frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed") +def get_closing_entry(pcv, account, balances, dimensions): + closing_entry = frappe._dict( + { + "company": pcv.company, + "closing_date": pcv.period_end_date, + "period_closing_voucher": pcv.name, + "account": account, + "account_currency": balances.account_currency, + "debit_in_account_currency": flt(balances.debit_in_account_currency), + "debit": flt(balances.debit), + "credit_in_account_currency": flt(balances.credit_in_account_currency), + "credit": flt(balances.credit), + "is_period_closing_voucher_entry": 0, + } + ) + # update dimensions + for i, dimension in enumerate(dimensions): + closing_entry[dimension] = dimensions[i] + return closing_entry + + def get_dimensions(): from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, From d139db296a6581cd8153a87da146db863da40fcf Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Oct 2025 15:40:53 +0530 Subject: [PATCH 23/86] refactor: cleanup and for better readability (cherry picked from commit 653ae84b3e6431616cb72525fc455624de61fef6) --- .../process_period_closing_voucher.py | 188 +++++++++--------- 1 file changed, 89 insertions(+), 99 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 63e901b8af4..bff371a46dd 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 @@ -157,6 +157,11 @@ def resume_pcv_processing(docname: str): start_pcv_processing(docname) +def update_default_dimensions(dimension_fields, gl_entry, dimension_values): + for i, dimension in enumerate(dimension_fields): + gl_entry[dimension] = dimension_values[i] + + def get_gle_for_pl_account(pcv, acc, balances, dimensions): balance_in_account_currency = flt(balances.debit_in_account_currency) - flt( balances.credit_in_account_currency @@ -185,8 +190,7 @@ def get_gle_for_pl_account(pcv, acc, balances, dimensions): } ) # update dimensions - for i, dimension in enumerate(dimensions): - gl_entry[dimension] = dimensions[i] + update_default_dimensions(get_dimensions(), gl_entry, dimensions) return gl_entry @@ -214,8 +218,7 @@ def get_gle_for_closing_account(pcv, dimension_balance, dimensions): } ) # update dimensions - for i, dimension in enumerate(dimensions): - gl_entry[dimension] = dimensions[i] + update_default_dimensions(get_dimensions(), gl_entry, dimensions) return gl_entry @@ -259,129 +262,117 @@ def schedule_next_date(docname: str): to_process[0].parentfield, ) else: - # summarize, build and post GL ppcvd = qb.DocType("Process Period Closing Voucher Detail") total_no_of_dates = ( qb.from_(ppcvd).select(Count(ppcvd.star)).where(ppcvd.parent.eq(docname)).run()[0][0] ) - # consider both normal and opening balance completed = ( qb.from_(ppcvd) .select(Count(ppcvd.star)) .where(ppcvd.parent.eq(docname) & ppcvd.status.eq("Completed")) .run()[0][0] ) + # Ensure both normal and opening balances are processed for all dates if total_no_of_dates == completed: summarize_and_post_ledger_entries(docname) def summarize_and_post_ledger_entries(docname): - # TODO: ensure all dates are processed - running = frappe.db.get_all( - "Process Period Closing Voucher Detail", - filters={"parent": docname, "status": "Running"}, - fields=["processing_date"], - order_by="processing_date", - limit=1, + # calculate balances for whole PCV period + ppcv = frappe.get_doc("Process Period Closing Voucher", docname) + + # P&L Accounts + gl_entries = [] + for x in ppcv.normal_balances: + if x.report_type == "Profit and Loss": + closing_balances = [frappe._dict(gle) for gle in frappe.json.loads(x.closing_balance)] + gl_entries.extend(closing_balances) + + # build dimension wise dictionary from all GLE's + pl_dimension_wise_acc_balance = build_dimension_wise_balance_dict(gl_entries) + + # convert tuple key to str to make it json compliant + json_dict = {} + for k, v in pl_dimension_wise_acc_balance.items(): + str_key = [str(x) for x in k] + str_key = ",".join(str_key) + json_dict[str_key] = v + + # save + frappe.db.set_value( + "Process Period Closing Voucher", docname, "p_l_closing_balance", frappe.json.dumps(json_dict) ) - if not running: - # calculate balances for whole PCV period - ppcv = frappe.get_doc("Process Period Closing Voucher", docname) - # P&L Accounts - gl_entries = [] - for x in ppcv.normal_balances: - if x.report_type == "Profit and Loss": - closing_balances = [frappe._dict(gle) for gle in frappe.json.loads(x.closing_balance)] - gl_entries.extend(closing_balances) + # build gl map + pcv = frappe.get_doc("Period Closing Voucher", ppcv.parent_pcv) + pl_accounts_reverse_gle = [] + closing_account_gle = [] - # build dimension wise dictionary from all GLE's - pl_dimension_wise_acc_balance = build_dimension_wise_balance_dict(gl_entries) + for dimensions, account_balances in pl_dimension_wise_acc_balance.items(): + for acc, balances in account_balances.items(): + balance_in_company_currency = flt(balances.debit) - flt(balances.credit) + if balance_in_company_currency: + pl_accounts_reverse_gle.append(get_gle_for_pl_account(pcv, acc, balances, dimensions)) - # convert tuple key to str to make it json compliant - json_dict = {} - for k, v in pl_dimension_wise_acc_balance.items(): - str_key = [str(x) for x in k] - str_key = ",".join(str_key) - json_dict[str_key] = v + closing_account_gle.append(get_gle_for_closing_account(pcv, account_balances["balances"], dimensions)) - # save - frappe.db.set_value( - "Process Period Closing Voucher", docname, "p_l_closing_balance", frappe.json.dumps(json_dict) - ) + gl_entries = pl_accounts_reverse_gle + closing_account_gle + from erpnext.accounts.general_ledger import make_gl_entries - # build gl map - pcv = frappe.get_doc("Period Closing Voucher", ppcv.parent_pcv) - pl_accounts_reverse_gle = [] - closing_account_gle = [] + if gl_entries: + make_gl_entries(gl_entries, merge_entries=False) - for dimensions, account_balances in pl_dimension_wise_acc_balance.items(): - for acc, balances in account_balances.items(): - balance_in_company_currency = flt(balances.debit) - flt(balances.credit) - if balance_in_company_currency: - pl_accounts_reverse_gle.append(get_gle_for_pl_account(pcv, acc, balances, dimensions)) + # Balance Sheet Accounts + gl_entries = [] + for x in ppcv.normal_balances + ppcv.z_opening_balances: + if x.report_type == "Balance Sheet": + closing_balances = [frappe._dict(gle) for gle in frappe.json.loads(x.closing_balance)] + gl_entries.extend(closing_balances) - closing_account_gle.append( - get_gle_for_closing_account(pcv, account_balances["balances"], dimensions) - ) + # build dimension wise dictionary from all GLE's + bs_dimension_wise_acc_balance = build_dimension_wise_balance_dict(gl_entries) - gl_entries = pl_accounts_reverse_gle + closing_account_gle - from erpnext.accounts.general_ledger import make_gl_entries + # convert tuple key to str to make it json compliant + json_dict = {} + for k, v in bs_dimension_wise_acc_balance.items(): + str_key = [str(x) for x in k] + str_key = ",".join(str_key) + json_dict[str_key] = v - if gl_entries: - make_gl_entries(gl_entries, merge_entries=False) + # save + frappe.db.set_value( + "Process Period Closing Voucher", docname, "bs_closing_balance", frappe.json.dumps(json_dict) + ) - # Balance Sheet Accounts - gl_entries = [] - for x in ppcv.normal_balances + ppcv.z_opening_balances: - if x.report_type == "Balance Sheet": - closing_balances = [frappe._dict(gle) for gle in frappe.json.loads(x.closing_balance)] - gl_entries.extend(closing_balances) + # make closing entries + pl_closing_entries = copy.deepcopy(pl_accounts_reverse_gle) + for d in pl_accounts_reverse_gle: + # reverse debit and credit + gle_copy = copy.deepcopy(d) + gle_copy.debit = d.credit + gle_copy.credit = d.debit + gle_copy.debit_in_account_currency = d.credit_in_account_currency + gle_copy.credit_in_account_currency = d.debit_in_account_currency + gle_copy.is_period_closing_voucher_entry = 0 + gle_copy.period_closing_voucher = pcv.name + pl_closing_entries.append(gle_copy) - # build dimension wise dictionary from all GLE's - bs_dimension_wise_acc_balance = build_dimension_wise_balance_dict(gl_entries) + bs_closing_entries = [] + for dimensions, account_balances in bs_dimension_wise_acc_balance.items(): + for acc, balances in account_balances.items(): + balance_in_company_currency = flt(balances.debit) - flt(balances.credit) + if acc != "balances" and balance_in_company_currency: + bs_closing_entries.append(get_closing_entry(pcv, acc, balances, dimensions)) - # convert tuple key to str to make it json compliant - json_dict = {} - for k, v in bs_dimension_wise_acc_balance.items(): - str_key = [str(x) for x in k] - str_key = ",".join(str_key) - json_dict[str_key] = v + closing_entries_for_closing_account = copy.deepcopy(closing_account_gle) + for d in closing_entries_for_closing_account: + d.period_closing_voucher = pcv.name - # save - frappe.db.set_value( - "Process Period Closing Voucher", docname, "bs_closing_balance", frappe.json.dumps(json_dict) - ) + closing_entries = pl_closing_entries + bs_closing_entries + closing_entries_for_closing_account + make_closing_entries(closing_entries, pcv.name, pcv.company, pcv.period_end_date) - # make closing entries - pl_closing_entries = copy.deepcopy(pl_accounts_reverse_gle) - for d in pl_accounts_reverse_gle: - # reverse debit and credit - gle_copy = copy.deepcopy(d) - gle_copy.debit = d.credit - gle_copy.credit = d.debit - gle_copy.debit_in_account_currency = d.credit_in_account_currency - gle_copy.credit_in_account_currency = d.debit_in_account_currency - gle_copy.is_period_closing_voucher_entry = 0 - gle_copy.period_closing_voucher = pcv.name - pl_closing_entries.append(gle_copy) - - bs_closing_entries = [] - for dimensions, account_balances in bs_dimension_wise_acc_balance.items(): - for acc, balances in account_balances.items(): - balance_in_company_currency = flt(balances.debit) - flt(balances.credit) - if acc != "balances" and balance_in_company_currency: - bs_closing_entries.append(get_closing_entry(pcv, acc, balances, dimensions)) - - closing_entries_for_closing_account = copy.deepcopy(closing_account_gle) - for d in closing_entries_for_closing_account: - d.period_closing_voucher = pcv.name - - closing_entries = pl_closing_entries + bs_closing_entries + closing_entries_for_closing_account - make_closing_entries(closing_entries, pcv.name, pcv.company, pcv.period_end_date) - - # TODO: Update processing status on PCV and Process document - frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed") + # TODO: Update processing status on PCV and Process document + frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed") def get_closing_entry(pcv, account, balances, dimensions): @@ -400,8 +391,7 @@ def get_closing_entry(pcv, account, balances, dimensions): } ) # update dimensions - for i, dimension in enumerate(dimensions): - closing_entry[dimension] = dimensions[i] + update_default_dimensions(get_dimensions(), closing_entry, dimensions) return closing_entry From 0151f5f191bd8864b06b3f8a72bda5aa65a5be75 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Oct 2025 19:48:35 +0530 Subject: [PATCH 24/86] refactor: utility to convert tuple key to str (cherry picked from commit 5b464ae4c15fc3a17070bfdb36d284e23f18b314) --- .../process_period_closing_voucher.py | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 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 bff371a46dd..4d0e4271e78 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 @@ -277,6 +277,20 @@ def schedule_next_date(docname: str): summarize_and_post_ledger_entries(docname) +def make_dict_json_compliant(dimension_wise_balance) -> dict: + """ + convert tuple -> str + JSON doesn't support dictionary with tuple keys + """ + converted_dict = {} + for k, v in dimension_wise_balance.items(): + str_key = [str(x) for x in k] + str_key = ",".join(str_key) + converted_dict[str_key] = v + + return converted_dict + + def summarize_and_post_ledger_entries(docname): # calculate balances for whole PCV period ppcv = frappe.get_doc("Process Period Closing Voucher", docname) @@ -291,14 +305,8 @@ def summarize_and_post_ledger_entries(docname): # build dimension wise dictionary from all GLE's pl_dimension_wise_acc_balance = build_dimension_wise_balance_dict(gl_entries) - # convert tuple key to str to make it json compliant - json_dict = {} - for k, v in pl_dimension_wise_acc_balance.items(): - str_key = [str(x) for x in k] - str_key = ",".join(str_key) - json_dict[str_key] = v - # save + json_dict = make_dict_json_compliant(pl_dimension_wise_acc_balance) frappe.db.set_value( "Process Period Closing Voucher", docname, "p_l_closing_balance", frappe.json.dumps(json_dict) ) @@ -332,14 +340,8 @@ def summarize_and_post_ledger_entries(docname): # build dimension wise dictionary from all GLE's bs_dimension_wise_acc_balance = build_dimension_wise_balance_dict(gl_entries) - # convert tuple key to str to make it json compliant - json_dict = {} - for k, v in bs_dimension_wise_acc_balance.items(): - str_key = [str(x) for x in k] - str_key = ",".join(str_key) - json_dict[str_key] = v - # save + json_dict = make_dict_json_compliant(bs_dimension_wise_acc_balance) frappe.db.set_value( "Process Period Closing Voucher", docname, "bs_closing_balance", frappe.json.dumps(json_dict) ) From 5071cad1618332c4de74c6a9b9fd5ed2975bd07f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Oct 2025 19:55:09 +0530 Subject: [PATCH 25/86] refactor: utility to consolidate results from all dates (cherry picked from commit 7406d8326097435446498bb653ec916c853ffea7) --- .../process_period_closing_voucher.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 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 4d0e4271e78..0b163e555c5 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 @@ -291,16 +291,21 @@ def make_dict_json_compliant(dimension_wise_balance) -> dict: return converted_dict +def get_consolidated_gles(balances, report_type) -> list: + gl_entries = [] + for x in balances: + if x.report_type == report_type: + closing_balances = [frappe._dict(gle) for gle in frappe.json.loads(x.closing_balance)] + gl_entries.extend(closing_balances) + return gl_entries + + def summarize_and_post_ledger_entries(docname): # calculate balances for whole PCV period ppcv = frappe.get_doc("Process Period Closing Voucher", docname) # P&L Accounts - gl_entries = [] - for x in ppcv.normal_balances: - if x.report_type == "Profit and Loss": - closing_balances = [frappe._dict(gle) for gle in frappe.json.loads(x.closing_balance)] - gl_entries.extend(closing_balances) + gl_entries = get_consolidated_gles(ppcv.normal_balances, "Profit and Loss") # build dimension wise dictionary from all GLE's pl_dimension_wise_acc_balance = build_dimension_wise_balance_dict(gl_entries) @@ -331,11 +336,7 @@ def summarize_and_post_ledger_entries(docname): make_gl_entries(gl_entries, merge_entries=False) # Balance Sheet Accounts - gl_entries = [] - for x in ppcv.normal_balances + ppcv.z_opening_balances: - if x.report_type == "Balance Sheet": - closing_balances = [frappe._dict(gle) for gle in frappe.json.loads(x.closing_balance)] - gl_entries.extend(closing_balances) + gl_entries = get_consolidated_gles(ppcv.normal_balances + ppcv.z_opening_balances, "Balance Sheet") # build dimension wise dictionary from all GLE's bs_dimension_wise_acc_balance = build_dimension_wise_balance_dict(gl_entries) From d94000fecfebd163712a5e1a8f3e3bc18129a6e1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Oct 2025 20:27:22 +0530 Subject: [PATCH 26/86] refactor: smaller methods (cherry picked from commit fa3bd6f5a7d1199d1fb1e4d32d11bef267b3cc7a) --- .../process_period_closing_voucher.py | 65 ++++++++++++++----- 1 file changed, 49 insertions(+), 16 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 0b163e555c5..3940a26ad1b 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 @@ -300,14 +300,14 @@ def get_consolidated_gles(balances, report_type) -> list: return gl_entries -def summarize_and_post_ledger_entries(docname): - # calculate balances for whole PCV period +def get_gl_entries(docname): + """ + Calculate total closing balance of all P&L accounts across PCV start and end date + """ ppcv = frappe.get_doc("Process Period Closing Voucher", docname) - # P&L Accounts + # calculate balance gl_entries = get_consolidated_gles(ppcv.normal_balances, "Profit and Loss") - - # build dimension wise dictionary from all GLE's pl_dimension_wise_acc_balance = build_dimension_wise_balance_dict(gl_entries) # save @@ -329,13 +329,15 @@ def summarize_and_post_ledger_entries(docname): closing_account_gle.append(get_gle_for_closing_account(pcv, account_balances["balances"], dimensions)) - gl_entries = pl_accounts_reverse_gle + closing_account_gle - from erpnext.accounts.general_ledger import make_gl_entries + return pl_accounts_reverse_gle, closing_account_gle - if gl_entries: - make_gl_entries(gl_entries, merge_entries=False) - # Balance Sheet Accounts +def calculate_balance_sheet_balance(docname): + """ + Calculate total closing balance of all P&L accounts across PCV start and end date. + If it is first PCV, opening entries are also considered + """ + ppcv = frappe.get_doc("Process Period Closing Voucher", docname) gl_entries = get_consolidated_gles(ppcv.normal_balances + ppcv.z_opening_balances, "Balance Sheet") # build dimension wise dictionary from all GLE's @@ -346,10 +348,12 @@ def summarize_and_post_ledger_entries(docname): frappe.db.set_value( "Process Period Closing Voucher", docname, "bs_closing_balance", frappe.json.dumps(json_dict) ) + return bs_dimension_wise_acc_balance - # make closing entries - pl_closing_entries = copy.deepcopy(pl_accounts_reverse_gle) - for d in pl_accounts_reverse_gle: + +def get_p_l_closing_entries(pl_gles, pcv): + pl_closing_entries = copy.deepcopy(pl_gles) + for d in pl_gles: # reverse debit and credit gle_copy = copy.deepcopy(d) gle_copy.debit = d.credit @@ -360,18 +364,47 @@ def summarize_and_post_ledger_entries(docname): gle_copy.period_closing_voucher = pcv.name pl_closing_entries.append(gle_copy) - bs_closing_entries = [] - for dimensions, account_balances in bs_dimension_wise_acc_balance.items(): + return pl_closing_entries + + +def get_bs_closing_entries(dimension_wise_balance, pcv): + closing_entries = [] + for dimensions, account_balances in dimension_wise_balance.items(): for acc, balances in account_balances.items(): balance_in_company_currency = flt(balances.debit) - flt(balances.credit) if acc != "balances" and balance_in_company_currency: - bs_closing_entries.append(get_closing_entry(pcv, acc, balances, dimensions)) + closing_entries.append(get_closing_entry(pcv, acc, balances, dimensions)) + return closing_entries + + +def get_closing_account_closing_entry(closing_account_gle, pcv): closing_entries_for_closing_account = copy.deepcopy(closing_account_gle) for d in closing_entries_for_closing_account: d.period_closing_voucher = pcv.name + return closing_entries_for_closing_account + +def summarize_and_post_ledger_entries(docname): + # P&L accounts + pl_accounts_reverse_gle, closing_account_gle = get_gl_entries(docname) + gl_entries = pl_accounts_reverse_gle + closing_account_gle + from erpnext.accounts.general_ledger import make_gl_entries + + if gl_entries: + make_gl_entries(gl_entries, merge_entries=False) + + pcv_name = frappe.db.get_value("Process Period Closing Voucher", docname, "parent_pcv") + pcv = frappe.get_doc("Period Closing Voucher", pcv_name) + + # Balance sheet accounts + bs_dimension_wise_acc_balance = calculate_balance_sheet_balance(docname) + + pl_closing_entries = get_p_l_closing_entries(pl_accounts_reverse_gle, pcv) + bs_closing_entries = get_bs_closing_entries(bs_dimension_wise_acc_balance, pcv) + closing_entries_for_closing_account = get_closing_account_closing_entry(closing_account_gle, pcv) closing_entries = pl_closing_entries + bs_closing_entries + closing_entries_for_closing_account + make_closing_entries(closing_entries, pcv.name, pcv.company, pcv.period_end_date) # TODO: Update processing status on PCV and Process document From 29e8801b7f3b003245a79f1487de5541549ae399 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 20 Oct 2025 10:59:59 +0530 Subject: [PATCH 27/86] refactor: more changes 1. 'Accounts Manager' has access to submit, cancel and delete 2. cancel and delete operation of PCV is linked with Proces PCV (cherry picked from commit cae123785949415fa0d4cc95df13a5e03a80a7c8) --- .../period_closing_voucher.js | 2 ++ .../period_closing_voucher.py | 30 ++++++++++++++----- .../process_period_closing_voucher.json | 21 +++++++++++-- .../process_period_closing_voucher.py | 2 +- 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js index 15de8cec243..f07d90b6ef3 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js @@ -4,6 +4,8 @@ frappe.ui.form.on("Period Closing Voucher", { onload: function (frm) { if (!frm.doc.transaction_date) frm.doc.transaction_date = frappe.datetime.obj_to_str(new Date()); + + frm.ignore_doctypes_on_cancel_all = ["Process Period Closing Voucher"]; }, setup: function (frm) { diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index 97c503f5885..40ed4230a5c 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -135,7 +135,8 @@ class PeriodClosingVoucher(AccountsController): self.db_set("gle_processing_status", "In Progress") self.make_gl_entries() else: - print("submit") + ppcv = frappe.get_doc({"doctype": "Process Period Closing Voucher", "parent_pcv": self.name}) + ppcv.save().submit() def on_cancel(self): self.ignore_linked_doctypes = ( @@ -143,13 +144,28 @@ class PeriodClosingVoucher(AccountsController): "Stock Ledger Entry", "Payment Ledger Entry", "Account Closing Balance", + "Process Period Closing Voucher", ) - if frappe.get_single_value("Accounts Settings", "use_legacy_controller_for_pcv"): - self.block_if_future_closing_voucher_exists() - self.db_set("gle_processing_status", "In Progress") - self.cancel_gl_entries() - else: - print("cancel") + self.block_if_future_closing_voucher_exists() + + if not frappe.get_single_value("Accounts Settings", "use_legacy_controller_for_pcv"): + self.cancel_process_pcv_docs() + + self.db_set("gle_processing_status", "In Progress") + self.cancel_gl_entries() + + def cancel_process_pcv_docs(self): + ppcvs = frappe.db.get_all("Process Period Closing Voucher", {"parent_pcv": self.name, "docstatus": 1}) + for x in ppcvs: + frappe.get_doc("Process Period Closing Voucher", x.name).cancel() + + def on_trash(self): + super().on_trash() + ppcvs = frappe.db.get_all( + "Process Period Closing Voucher", {"parent_pcv": self.name, "docstatus": ["in", [1, 2]]} + ) + for x in ppcvs: + frappe.delete_doc("Process Period Closing Voucher", x.name) def make_gl_entries(self): if frappe.db.estimate_count("GL Entry") > 100_000: 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 fd0186557dc..fc6ccda4488 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,6 +1,6 @@ { "actions": [], - "allow_rename": 1, + "autoname": "format:Process-PCV-{###}", "creation": "2025-09-25 15:44:03.534699", "doctype": "DocType", "engine": "InnoDB", @@ -70,13 +70,15 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-10-17 15:16:26.324369", + "modified": "2025-10-20 08:06:26.786490", "modified_by": "Administrator", "module": "Accounts", "name": "Process Period Closing Voucher", + "naming_rule": "Expression", "owner": "Administrator", "permissions": [ { + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -86,6 +88,21 @@ "report": 1, "role": "System Manager", "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "submit": 1, "write": 1 } ], 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 3940a26ad1b..9990f58ff55 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 @@ -407,7 +407,7 @@ def summarize_and_post_ledger_entries(docname): make_closing_entries(closing_entries, pcv.name, pcv.company, pcv.period_end_date) - # TODO: Update processing status on PCV and Process document + frappe.db.set_value("Period Closing Voucher", pcv.name, "gle_processing_status", "Completed") frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed") From 5cc6a1771d2603eb891ad8daa61ae67233843c22 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 20 Oct 2025 12:20:02 +0530 Subject: [PATCH 28/86] refactor: abort processing of all tasks upon cancellation (cherry picked from commit 090e155fd0d65eabc240e6588e7d1439e6ba5777) --- .../process_period_closing_voucher.json | 4 ++-- .../process_period_closing_voucher.py | 19 ++++++++++++++++++- ...process_period_closing_voucher_detail.json | 4 ++-- .../process_period_closing_voucher_detail.py | 2 +- 4 files changed, 23 insertions(+), 6 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 fc6ccda4488..71e6cdd659a 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 @@ -28,7 +28,7 @@ "fieldtype": "Select", "label": "Status", "no_copy": 1, - "options": "Queued\nRunning\nCompleted" + "options": "Queued\nRunning\nCompleted\nCancelled" }, { "fieldname": "amended_from", @@ -70,7 +70,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-10-20 08:06:26.786490", + "modified": "2025-10-20 12:06:32.613247", "modified_by": "Administrator", "module": "Accounts", "name": "Process Period Closing Voucher", 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 9990f58ff55..f4a2a76f790 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 @@ -36,7 +36,7 @@ class ProcessPeriodClosingVoucher(Document): normal_balances: DF.Table[ProcessPeriodClosingVoucherDetail] p_l_closing_balance: DF.JSON | None parent_pcv: DF.Link - status: DF.Literal["Queued", "Running", "Completed"] + status: DF.Literal["Queued", "Running", "Completed", "Cancelled"] z_opening_balances: DF.Table[ProcessPeriodClosingVoucherDetail] # end: auto-generated types @@ -84,6 +84,9 @@ class ProcessPeriodClosingVoucher(Document): def on_submit(self): start_pcv_processing(self.name) + def on_cancel(self): + cancel_pcv_processing(self.name) + @frappe.whitelist() def start_pcv_processing(docname: str): @@ -142,6 +145,20 @@ def pause_pcv_processing(docname: str): qb.update(ppcvd).set(ppcvd.status, "Paused").where(ppcvd.name.isin(queued_dates)).run() +@frappe.whitelist() +def cancel_pcv_processing(docname: str): + ppcv = qb.DocType("Process Period Closing Voucher") + qb.update(ppcv).set(ppcv.status, "Cancelled").where(ppcv.name.eq(docname)).run() + + if queued_dates := frappe.db.get_all( + "Process Period Closing Voucher Detail", + filters={"parent": docname, "status": "Queued"}, + pluck="name", + ): + ppcvd = qb.DocType("Process Period Closing Voucher Detail") + qb.update(ppcvd).set(ppcvd.status, "Cancelled").where(ppcvd.name.isin(queued_dates)).run() + + @frappe.whitelist() def resume_pcv_processing(docname: str): ppcv = qb.DocType("Process Period Closing Voucher") diff --git a/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json b/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json index b64871b2ff0..8d881ade663 100644 --- a/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json +++ b/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json @@ -23,7 +23,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Status", - "options": "Queued\nRunning\nPaused\nCompleted" + "options": "Queued\nRunning\nPaused\nCompleted\nCancelled" }, { "fieldname": "closing_balance", @@ -44,7 +44,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-10-17 11:28:34.775743", + "modified": "2025-10-20 12:03:59.106931", "modified_by": "Administrator", "module": "Accounts", "name": "Process Period Closing Voucher Detail", 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 6bec6622ee0..f3a8302ac5b 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 @@ -20,7 +20,7 @@ class ProcessPeriodClosingVoucherDetail(Document): parenttype: DF.Data processing_date: DF.Date | None report_type: DF.Literal["Profit and Loss", "Balance Sheet"] - status: DF.Literal["Queued", "Running", "Paused", "Completed"] + status: DF.Literal["Queued", "Running", "Paused", "Completed", "Cancelled"] # end: auto-generated types pass From 4ebddc591f0e2e40ba0bc0100864705a8ea256a4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 20 Oct 2025 12:22:45 +0530 Subject: [PATCH 29/86] chore: remove scaffolding (cherry picked from commit 191c0e65a1a95fd457f7a2510a4182af5a7dbbd1) --- .../process_period_closing_voucher.py | 55 +++++++------------ 1 file changed, 20 insertions(+), 35 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 f4a2a76f790..b52ac54b6b8 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 @@ -15,8 +15,6 @@ from erpnext.accounts.doctype.account_closing_balance.account_closing_balance im make_closing_entries, ) -BACKGROUND = True - class ProcessPeriodClosingVoucher(Document): # begin: auto-generated types @@ -91,7 +89,6 @@ 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"]: - # TODO: move this inside if block frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running") if normal_balances := frappe.db.get_all( "Process Period Closing Voucher Detail", @@ -113,20 +110,16 @@ def start_pcv_processing(docname: str): "status", "Running", ) - - if BACKGROUND: - frappe.enqueue( - method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date", - queue="long", - is_async=True, - enqueue_after_commit=True, - docname=docname, - date=x.processing_date, - report_type=x.report_type, - parentfield=x.parentfield, - ) - else: - process_individual_date(docname, x.processing_date, x.report_type, x.parentfield) + frappe.enqueue( + method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date", + queue="long", + is_async=True, + enqueue_after_commit=True, + docname=docname, + date=x.processing_date, + report_type=x.report_type, + parentfield=x.parentfield, + ) else: frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed") @@ -260,24 +253,16 @@ def schedule_next_date(docname: str): "status", "Running", ) - if BACKGROUND: - frappe.enqueue( - method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date", - queue="long", - is_async=True, - enqueue_after_commit=True, - docname=docname, - date=to_process[0].processing_date, - report_type=to_process[0].report_type, - parentfield=to_process[0].parentfield, - ) - else: - process_individual_date( - docname, - to_process[0].processing_date, - to_process[0].report_type, - to_process[0].parentfield, - ) + frappe.enqueue( + method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date", + queue="long", + is_async=True, + enqueue_after_commit=True, + docname=docname, + date=to_process[0].processing_date, + report_type=to_process[0].report_type, + parentfield=to_process[0].parentfield, + ) else: ppcvd = qb.DocType("Process Period Closing Voucher Detail") total_no_of_dates = ( From 10df19227591ee3bf04452483734b20d60e3ac91 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 20 Oct 2025 12:41:15 +0530 Subject: [PATCH 30/86] chore: progress bar (cherry picked from commit 0b88f98a861ad07f592cdf9c6b8530e57cbb88c8) --- .../process_period_closing_voucher.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js index 23d410e9bb0..a5b186826e6 100644 --- a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js @@ -56,5 +56,16 @@ frappe.ui.form.on("Process Period Closing Voucher", { }); }); } + // progress bar + let progress = 0; + + let normal_finished = frm.doc.normal_balances.filter((x) => x.status == "Completed").length; + let opening_finished = frm.doc.z_opening_balances.filter((x) => x.status == "Completed").length; + + progress = + ((normal_finished + opening_finished) / + (frm.doc.normal_balances.length + frm.doc.z_opening_balances.length)) * + 100; + frm.dashboard.add_progress("Books closure progress", progress, ""); }, }); From d5a36fe8aa4b685453798c6709470b69149b29b9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 20 Oct 2025 13:16:18 +0530 Subject: [PATCH 31/86] refactor: enable legacy controller by default for pcv (cherry picked from commit fe39ce03bbb2b6419452ced07aad510b861dfa69) # Conflicts: # erpnext/accounts/doctype/accounts_settings/accounts_settings.json # erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py --- .../doctype/accounts_settings/accounts_settings.json | 6 +++++- .../test_period_closing_voucher.py | 8 ++++++++ erpnext/patches.txt | 1 + .../v15_0/toggle_legacy_controller_for_period_closing.py | 9 +++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v15_0/toggle_legacy_controller_for_period_closing.py diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index dad7165b665..6a640d3464c 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -640,7 +640,7 @@ "label": "Use Legacy Budget Controller" }, { - "default": "0", + "default": "1", "fieldname": "use_legacy_controller_for_pcv", "fieldtype": "Check", "label": "Use Legacy Controller For Period Closing Voucher" @@ -652,11 +652,15 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], +<<<<<<< HEAD <<<<<<< HEAD "modified": "2025-07-18 13:56:47.192437", ======= "modified": "2025-10-01 15:17:47.168354", >>>>>>> 4888461be2 (refactor: checkbox for pcv controller) +======= + "modified": "2025-10-20 14:06:08.870427", +>>>>>>> fe39ce03bb (refactor: enable legacy controller by default for pcv) "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", 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 34e2fdd9082..809e447aef4 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 @@ -13,7 +13,15 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal from erpnext.accounts.utils import get_fiscal_year +<<<<<<< HEAD class TestPeriodClosingVoucher(unittest.TestCase): +======= +class TestPeriodClosingVoucher(IntegrationTestCase): + def setUp(self): + super().setUp() + frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1) + +>>>>>>> fe39ce03bb (refactor: enable legacy controller by default for pcv) def test_closing_entry(self): frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'") frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'") diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 4e05b974d16..bb3d5f07ef8 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -423,3 +423,4 @@ erpnext.patches.v15_0.add_company_payment_gateway_account erpnext.patches.v15_0.update_uae_zero_rated_fetch erpnext.patches.v15_0.update_fieldname_in_accounting_dimension_filter erpnext.patches.v15_0.set_asset_status_if_not_already_set +erpnext.patches.v15_0.toggle_legacy_controller_for_period_closing diff --git a/erpnext/patches/v15_0/toggle_legacy_controller_for_period_closing.py b/erpnext/patches/v15_0/toggle_legacy_controller_for_period_closing.py new file mode 100644 index 00000000000..7e7cb2f7b49 --- /dev/null +++ b/erpnext/patches/v15_0/toggle_legacy_controller_for_period_closing.py @@ -0,0 +1,9 @@ +import frappe + + +def execute(): + """ + Description: + Enable Legacy controller for Period Closing Voucher + """ + frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1) From d8e50754242a89711b8e94110dabddd788660cd1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 3 Nov 2025 16:44:26 +0530 Subject: [PATCH 32/86] refactor: minor changes on status 1. set to 'In Progress' on start of both legacy and new controller 2. force delete to avoid permission issues 3. default to 1hr timeout (cherry picked from commit 9c13edc0b9e34c28a067871f260ebef866640558) --- .../doctype/period_closing_voucher/period_closing_voucher.py | 4 ++-- .../process_period_closing_voucher.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index 40ed4230a5c..210dbc5bbf5 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -131,8 +131,8 @@ class PeriodClosingVoucher(AccountsController): frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency)) def on_submit(self): + self.db_set("gle_processing_status", "In Progress") if frappe.get_single_value("Accounts Settings", "use_legacy_controller_for_pcv"): - self.db_set("gle_processing_status", "In Progress") self.make_gl_entries() else: ppcv = frappe.get_doc({"doctype": "Process Period Closing Voucher", "parent_pcv": self.name}) @@ -165,7 +165,7 @@ class PeriodClosingVoucher(AccountsController): "Process Period Closing Voucher", {"parent_pcv": self.name, "docstatus": ["in", [1, 2]]} ) for x in ppcvs: - frappe.delete_doc("Process Period Closing Voucher", x.name) + frappe.delete_doc("Process Period Closing Voucher", x.name, force=True, ignore_permissions=True) def make_gl_entries(self): if frappe.db.estimate_count("GL Entry") > 100_000: 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 b52ac54b6b8..e6f9913dcb2 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 @@ -113,6 +113,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", is_async=True, enqueue_after_commit=True, docname=docname, @@ -256,6 +257,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", is_async=True, enqueue_after_commit=True, docname=docname, From e09ee63d32cb170703fcd0fd4a0567c054d17035 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 5 Nov 2025 11:42:31 +0530 Subject: [PATCH 33/86] refactor: add paused to select option (cherry picked from commit fca7abf4d61a3a05dfaeb68afa28ec095e5f1dcb) --- .../process_period_closing_voucher.json | 4 ++-- .../process_period_closing_voucher.py | 2 +- 2 files changed, 3 insertions(+), 3 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 71e6cdd659a..a06a16f156c 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 @@ -28,7 +28,7 @@ "fieldtype": "Select", "label": "Status", "no_copy": 1, - "options": "Queued\nRunning\nCompleted\nCancelled" + "options": "Queued\nRunning\nPaused\nCompleted\nCancelled" }, { "fieldname": "amended_from", @@ -70,7 +70,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-10-20 12:06:32.613247", + "modified": "2025-11-05 11:40:24.996403", "modified_by": "Administrator", "module": "Accounts", "name": "Process Period Closing Voucher", 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 e6f9913dcb2..c63c5652cb3 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 @@ -34,7 +34,7 @@ class ProcessPeriodClosingVoucher(Document): normal_balances: DF.Table[ProcessPeriodClosingVoucherDetail] p_l_closing_balance: DF.JSON | None parent_pcv: DF.Link - status: DF.Literal["Queued", "Running", "Completed", "Cancelled"] + status: DF.Literal["Queued", "Running", "Paused", "Completed", "Cancelled"] z_opening_balances: DF.Table[ProcessPeriodClosingVoucherDetail] # end: auto-generated types From 5c6cc1ea2a1353565dee8dbbb4fdcf82312e86ef Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 5 Nov 2025 17:05:51 +0530 Subject: [PATCH 34/86] chore: resolve conflicts --- .../accounts_settings/accounts_settings.json | 17 ----------------- .../accounts_settings/accounts_settings.py | 4 ---- .../test_period_closing_voucher.py | 4 ---- .../test_process_period_closing_voucher.py | 16 ---------------- 4 files changed, 41 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 6a640d3464c..b6032bba1fe 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -630,21 +630,12 @@ "fieldname": "fetch_valuation_rate_for_internal_transaction", "fieldtype": "Check", "label": "Fetch Valuation Rate for Internal Transaction" -<<<<<<< HEAD -======= - }, - { - "default": "0", - "fieldname": "use_legacy_budget_controller", - "fieldtype": "Check", - "label": "Use Legacy Budget Controller" }, { "default": "1", "fieldname": "use_legacy_controller_for_pcv", "fieldtype": "Check", "label": "Use Legacy Controller For Period Closing Voucher" ->>>>>>> 4888461be2 (refactor: checkbox for pcv controller) } ], "icon": "icon-cog", @@ -652,15 +643,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], -<<<<<<< HEAD -<<<<<<< HEAD - "modified": "2025-07-18 13:56:47.192437", -======= - "modified": "2025-10-01 15:17:47.168354", ->>>>>>> 4888461be2 (refactor: checkbox for pcv controller) -======= "modified": "2025-10-20 14:06:08.870427", ->>>>>>> fe39ce03bb (refactor: enable legacy controller by default for pcv) "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 7ec9da682dc..5144e87a3a2 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -73,11 +73,7 @@ class AccountsSettings(Document): submit_journal_entries: DF.Check unlink_advance_payment_on_cancelation_of_order: DF.Check unlink_payment_on_cancellation_of_invoice: DF.Check -<<<<<<< HEAD -======= - use_legacy_budget_controller: DF.Check use_legacy_controller_for_pcv: DF.Check ->>>>>>> 4888461be2 (refactor: checkbox for pcv controller) # end: auto-generated types def validate(self): 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 809e447aef4..2aa484d05a1 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 @@ -13,15 +13,11 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal from erpnext.accounts.utils import get_fiscal_year -<<<<<<< HEAD class TestPeriodClosingVoucher(unittest.TestCase): -======= -class TestPeriodClosingVoucher(IntegrationTestCase): def setUp(self): super().setUp() frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1) ->>>>>>> fe39ce03bb (refactor: enable legacy controller by default for pcv) def test_closing_entry(self): frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'") frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'") diff --git a/erpnext/accounts/doctype/process_period_closing_voucher/test_process_period_closing_voucher.py b/erpnext/accounts/doctype/process_period_closing_voucher/test_process_period_closing_voucher.py index 17ce2c13b09..e695b11bcb5 100644 --- a/erpnext/accounts/doctype/process_period_closing_voucher/test_process_period_closing_voucher.py +++ b/erpnext/accounts/doctype/process_period_closing_voucher/test_process_period_closing_voucher.py @@ -2,19 +2,3 @@ # See license.txt # import frappe -from frappe.tests import IntegrationTestCase - -# On IntegrationTestCase, the doctype test records and all -# link-field test record dependencies are recursively loaded -# Use these module variables to add/remove to/from that list -EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] -IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] - - -class IntegrationTestProcessPeriodClosingVoucher(IntegrationTestCase): - """ - Integration tests for ProcessPeriodClosingVoucher. - Use this class for testing interactions between multiple components. - """ - - pass From c48647100fea5f1584b045d27d6bfe907e3894c4 Mon Sep 17 00:00:00 2001 From: Rehan Ansari Date: Tue, 4 Nov 2025 22:08:03 +0530 Subject: [PATCH 35/86] feat: add asset name column (cherry picked from commit f3eda02972cdfa6867f560cddb16407e6cf3364f) # Conflicts: # erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py --- .../asset_depreciation_ledger.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py index a21103c719d..891a06d4a0c 100644 --- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py +++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py @@ -119,7 +119,12 @@ def get_assets_details(assets): fields = [ "name as asset", +<<<<<<< HEAD "gross_purchase_amount", +======= + "asset_name", + "net_purchase_amount", +>>>>>>> f3eda02972 (feat: add asset name column) "opening_accumulated_depreciation", "asset_category", "status", @@ -143,6 +148,12 @@ def get_columns(): "options": "Asset", "width": 120, }, + { + "label": _("Asset Name"), + "fieldname": "asset_name", + "fieldtype": "Data", + "width": 140, + }, { "label": _("Depreciation Date"), "fieldname": "depreciation_date", From 7d593dd3db06eb66ed9eea083092400d2f32d07b Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Thu, 6 Nov 2025 00:51:41 +0530 Subject: [PATCH 36/86] fix: resolve conflict --- .../asset_depreciation_ledger/asset_depreciation_ledger.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py index 891a06d4a0c..b23051bd79c 100644 --- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py +++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py @@ -119,12 +119,8 @@ def get_assets_details(assets): fields = [ "name as asset", -<<<<<<< HEAD - "gross_purchase_amount", -======= "asset_name", - "net_purchase_amount", ->>>>>>> f3eda02972 (feat: add asset name column) + "gross_purchase_amount", "opening_accumulated_depreciation", "asset_category", "status", From 3fad90ebb9c4872fdbdfbc46edf6ce85a9f72218 Mon Sep 17 00:00:00 2001 From: rehansari26 Date: Wed, 5 Nov 2025 16:46:00 +0530 Subject: [PATCH 37/86] fix: set company before creating asset movement to avoid permission error (cherry picked from commit 8c49c9e500b98b7b8756b5b432c23329d6716a96) --- erpnext/assets/doctype/asset/asset.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 6fef1b21825..9f8895b6e7e 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -458,6 +458,7 @@ class Asset(AccountsController): "asset_name": self.asset_name, "target_location": self.location, "to_employee": self.custodian, + "company": self.company, } ] asset_movement = frappe.get_doc( From 56bb88d281913efac37cd5244776c4357680cd26 Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Wed, 5 Nov 2025 10:26:18 +0000 Subject: [PATCH 38/86] fix: hide total row in general ledger report (cherry picked from commit ef38b26a730f1cc538376fba9fe804a88f826ff9) --- erpnext/accounts/report/general_ledger/general_ledger.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.json b/erpnext/accounts/report/general_ledger/general_ledger.json index 172f322de22..a49f6356122 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.json +++ b/erpnext/accounts/report/general_ledger/general_ledger.json @@ -1,5 +1,5 @@ { - "add_total_row": 1, + "add_total_row": 0, "add_translate_data": 0, "columns": [], "creation": "2013-12-06 13:22:23", @@ -10,7 +10,7 @@ "idx": 3, "is_standard": "Yes", "letterhead": null, - "modified": "2025-08-13 12:47:27.645023", + "modified": "2025-11-05 15:47:59.597853", "modified_by": "Administrator", "module": "Accounts", "name": "General Ledger", From 32182d7cc7b30c62f1c7697c5c687fc24a5b406d Mon Sep 17 00:00:00 2001 From: rethik Date: Tue, 4 Nov 2025 19:53:44 +0530 Subject: [PATCH 39/86] fix: ignore Department doctype (cherry picked from commit fff6f1fb23598ff6a93a654f9067932cfef14c03) --- .../transaction_deletion_record/transaction_deletion_record.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index 227df38e116..ae4b1db1232 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -474,6 +474,7 @@ def get_doctypes_to_be_ignored(): "Item Default", "Customer", "Supplier", + "Department", ] doctypes_to_be_ignored.extend(frappe.get_hooks("company_data_to_be_ignored") or []) From 90500f0ffc2e3965fac96fbfdf1050dde8db1564 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Thu, 6 Nov 2025 18:35:56 +0530 Subject: [PATCH 40/86] feat(account settings): add checkbox to show balances in payment entry --- .../accounts_settings/accounts_settings.json | 28 ++++++++++++++-- .../accounts_settings/accounts_settings.py | 15 +++++++++ .../doctype/payment_entry/payment_entry.py | 33 ++++++++++++++----- erpnext/patches.txt | 2 ++ 4 files changed, 67 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index b6032bba1fe..35b33b7a195 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -56,6 +56,9 @@ "reconciliation_queue_size", "column_break_resa", "exchange_gain_loss_posting_date", + "payment_entry_settings", + "show_account_balance", + "show_party_balance", "invoicing_settings_tab", "accounts_transactions_settings_section", "over_billing_allowance", @@ -95,7 +98,8 @@ "legacy_section", "ignore_is_opening_check_for_reporting", "payment_request_settings", - "create_pr_in_draft_status" + "create_pr_in_draft_status", + "column_break_xrnd" ], "fields": [ { @@ -636,6 +640,23 @@ "fieldname": "use_legacy_controller_for_pcv", "fieldtype": "Check", "label": "Use Legacy Controller For Period Closing Voucher" + }, + { + "fieldname": "payment_entry_settings", + "fieldtype": "Section Break", + "label": "Payment Entry Settings" + }, + { + "default": "0", + "fieldname": "show_account_balance", + "fieldtype": "Check", + "label": "Show Account Balance" + }, + { + "default": "0", + "fieldname": "show_party_balance", + "fieldtype": "Check", + "label": "Show Party Balance" } ], "icon": "icon-cog", @@ -643,7 +664,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-10-20 14:06:08.870427", + "modified": "2025-11-06 17:48:07.682837", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", @@ -668,8 +689,9 @@ } ], "quick_entry": 1, + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "ASC", "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 5144e87a3a2..c54ae9bac65 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -65,8 +65,10 @@ class AccountsSettings(Document): role_allowed_to_over_bill: DF.Link | None role_to_override_stop_action: DF.Link | None round_row_wise_tax: DF.Check + show_account_balance: DF.Check show_balance_in_coa: DF.Check show_inclusive_tax_in_print: DF.Check + show_party_balance: DF.Check show_payment_schedule_in_print: DF.Check show_taxes_as_table_in_print: DF.Check stale_days: DF.Int @@ -105,6 +107,7 @@ class AccountsSettings(Document): frappe.clear_cache() self.validate_and_sync_auto_reconcile_config() + self.hide_or_show_party_and_account_balance() def validate_stale_days(self): if not self.allow_stale and cint(self.stale_days) <= 0: @@ -112,6 +115,18 @@ class AccountsSettings(Document): _("Stale Days should start from 1."), title="Error", indicator="red", raise_exception=1 ) + def hide_or_show_party_and_account_balance(self): + def set_property(fieldname, value): + make_property_setter("Payment Entry", fieldname, "hidden", value, "Check") + + if self.has_value_changed("show_party_balance"): + set_property("party_balance", not self.show_party_balance) + + if self.has_value_changed("show_account_balance"): + account_fields = ["paid_from_account_balance", "paid_to_account_balance"] + for field in account_fields: + set_property(field, not self.show_account_balance) + def enable_payment_schedule_in_print(self): show_in_print = cint(self.show_payment_schedule_in_print) for doctype in ("Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"): diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index cc0da8e7b09..4196b81c9ba 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -449,7 +449,7 @@ class PaymentEntry(AccountsController): self.contact_person = get_default_contact(self.party_type, self.party) complete_contact_details(self) - if not self.party_balance: + if not self.party_balance and frappe.get_single_value("Accounts Settings", "show_party_balance"): self.party_balance = get_balance_on( party_type=self.party_type, party=self.party, date=self.posting_date, company=self.company ) @@ -2684,11 +2684,17 @@ def get_party_details(company, party_type, party, date, cost_center=None): party_account = get_party_account(party_type, party, company) account_currency = get_account_currency(party_account) - account_balance = get_balance_on(party_account, date, cost_center=cost_center) + account_balance = ( + get_balance_on(party_account, date, cost_center=cost_center) + if frappe.get_single_value("Accounts Settings", "show_account_balance") + else 0 + ) _party_name = "title" if party_type == "Shareholder" else party_type.lower() + "_name" party_name = frappe.db.get_value(party_type, party, _party_name) - party_balance = get_balance_on( - party_type=party_type, party=party, company=company, cost_center=cost_center + party_balance = ( + get_balance_on(party_type=party_type, party=party, company=company, cost_center=cost_center) + if frappe.get_single_value("Accounts Settings", "show_party_balance") + else 0 ) if party_type in ["Customer", "Supplier"]: party_bank_account = get_party_bank_account(party_type, party) @@ -2717,7 +2723,11 @@ def get_account_details(account, date, cost_center=None): if not account_list: frappe.throw(_("Account: {0} is not permitted under Payment Entry").format(account)) - account_balance = get_balance_on(account, date, cost_center=cost_center, ignore_account_permission=True) + account_balance = ( + get_balance_on(account, date, cost_center=cost_center, ignore_account_permission=True) + if frappe.get_single_value("Accounts Settings", "show_account_balance") + else 0 + ) return frappe._dict( { @@ -3529,11 +3539,18 @@ def get_paid_amount(dt, dn, party_type, party, account, due_date): def get_party_and_account_balance( company, date, paid_from=None, paid_to=None, ptype=None, pty=None, cost_center=None ): + show_account_balance = frappe.get_single_value("Accounts Settings", "show_account_balance") return frappe._dict( { - "party_balance": get_balance_on(party_type=ptype, party=pty, cost_center=cost_center), - "paid_from_account_balance": get_balance_on(paid_from, date, cost_center=cost_center), - "paid_to_account_balance": get_balance_on(paid_to, date=date, cost_center=cost_center), + "party_balance": get_balance_on(party_type=ptype, party=pty, cost_center=cost_center) + if frappe.get_single_value("Accounts Settings", "show_party_balance") + else 0, + "paid_from_account_balance": get_balance_on(paid_from, date, cost_center=cost_center) + if show_account_balance + else 0, + "paid_to_account_balance": get_balance_on(paid_to, date=date, cost_center=cost_center) + if show_account_balance + else 0, } ) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index bb3d5f07ef8..33c1e6c79df 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -424,3 +424,5 @@ erpnext.patches.v15_0.update_uae_zero_rated_fetch erpnext.patches.v15_0.update_fieldname_in_accounting_dimension_filter erpnext.patches.v15_0.set_asset_status_if_not_already_set erpnext.patches.v15_0.toggle_legacy_controller_for_period_closing +execute:frappe.db.set_single_value("Accounts Settings", "show_party_balance", 1) +execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1) \ No newline at end of file From 7e3f30baad922a565ff21c6172fb5376e7ef59d3 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Fri, 7 Nov 2025 12:08:22 +0530 Subject: [PATCH 41/86] fix: trends report total mismatch with group filters (cherry picked from commit f7d09f87608bfc87123da56fbb3530fdcce1bd52) --- .../purchase_invoice_trends.json | 47 ++++++++++-------- .../sales_invoice_trends.json | 43 +++++++++------- .../purchase_order_trends.json | 45 +++++++++-------- erpnext/controllers/trends.py | 26 ++++++++++ .../quotation_trends/quotation_trends.json | 47 ++++++++++-------- .../sales_order_trends.json | 49 ++++++++++--------- .../delivery_note_trends.json | 47 ++++++++++-------- .../purchase_receipt_trends.json | 47 ++++++++++-------- 8 files changed, 206 insertions(+), 145 deletions(-) diff --git a/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.json b/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.json index dfae7803472..2080f51933a 100644 --- a/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.json +++ b/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.json @@ -1,32 +1,37 @@ { - "add_total_row": 1, - "apply_user_permissions": 1, - "creation": "2013-06-13 18:46:55", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2018-02-21 01:28:31.261299", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Purchase Invoice Trends", - "owner": "Administrator", - "ref_doctype": "Purchase Invoice", - "report_name": "Purchase Invoice Trends", - "report_type": "Script Report", + "add_total_row": 0, + "add_translate_data": 0, + "columns": [], + "creation": "2013-06-13 18:46:55", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 3, + "is_standard": "Yes", + "letterhead": null, + "modified": "2025-11-05 11:55:49.950442", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Purchase Invoice Trends", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Purchase Invoice", + "report_name": "Purchase Invoice Trends", + "report_type": "Script Report", "roles": [ { "role": "Accounts User" - }, + }, { "role": "Purchase User" - }, + }, { "role": "Accounts Manager" - }, + }, { "role": "Auditor" } - ] -} \ No newline at end of file + ], + "timeout": 0 +} diff --git a/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.json b/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.json index 7437550cc26..1ed34ff4c36 100644 --- a/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.json +++ b/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.json @@ -1,26 +1,31 @@ { - "add_total_row": 1, - "apply_user_permissions": 1, - "creation": "2013-06-13 18:44:21", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2018-02-21 01:28:03.622485", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Sales Invoice Trends", - "owner": "Administrator", - "ref_doctype": "Sales Invoice", - "report_name": "Sales Invoice Trends", - "report_type": "Script Report", + "add_total_row": 0, + "add_translate_data": 0, + "columns": [], + "creation": "2013-06-13 18:44:21", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 3, + "is_standard": "Yes", + "letterhead": null, + "modified": "2025-11-05 11:55:50.070651", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Sales Invoice Trends", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Sales Invoice", + "report_name": "Sales Invoice Trends", + "report_type": "Script Report", "roles": [ { "role": "Accounts Manager" - }, + }, { "role": "Accounts User" } - ] -} \ No newline at end of file + ], + "timeout": 0 +} diff --git a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.json b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.json index ca58c4ed44d..0047d6ecbe5 100644 --- a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.json +++ b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.json @@ -1,29 +1,34 @@ { - "add_total_row": 1, - "apply_user_permissions": 1, - "creation": "2013-06-13 18:45:01", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2018-02-21 01:28:37.416562", - "modified_by": "Administrator", - "module": "Buying", - "name": "Purchase Order Trends", - "owner": "Administrator", - "ref_doctype": "Purchase Order", - "report_name": "Purchase Order Trends", - "report_type": "Script Report", + "add_total_row": 0, + "add_translate_data": 0, + "columns": [], + "creation": "2013-06-13 18:45:01", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 3, + "is_standard": "Yes", + "letterhead": null, + "modified": "2025-11-05 11:55:50.058154", + "modified_by": "Administrator", + "module": "Buying", + "name": "Purchase Order Trends", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Purchase Order", + "report_name": "Purchase Order Trends", + "report_type": "Script Report", "roles": [ { "role": "Stock User" - }, + }, { "role": "Purchase Manager" - }, + }, { "role": "Purchase User" } - ] -} \ No newline at end of file + ], + "timeout": 0 +} diff --git a/erpnext/controllers/trends.py b/erpnext/controllers/trends.py index 81d5621de0e..476bde248cc 100644 --- a/erpnext/controllers/trends.py +++ b/erpnext/controllers/trends.py @@ -191,6 +191,9 @@ def get_data(filters, conditions): des[j + inc] = row1[0][j] data.append(des) + + total_row = calculate_total_row(data1, conditions["columns"]) + data.append(total_row) else: data = frappe.db.sql( """ select {} from `tab{}` t1, `tab{} Item` t2 {} @@ -214,9 +217,32 @@ def get_data(filters, conditions): as_list=1, ) + total_row = calculate_total_row(data, conditions["columns"]) + data.append(total_row) + return data +def calculate_total_row(data, columns): + def wrap_in_quotes(label): + return f"'{label}'" + + total_values = {} + for i, col in enumerate(columns): + if "Float" in col or "Currency/currency" in col: + total_values[i] = 0 + + for row in data: + for i in total_values.keys(): + total_values[i] += row[i] if row[i] is not None else 0 + + total_row = [wrap_in_quotes(_("Total"))] + for i in range(1, len(columns)): + total_row.append(total_values.get(i, None)) + + return total_row + + def get_mon(dt): return getdate(dt).strftime("%b") diff --git a/erpnext/selling/report/quotation_trends/quotation_trends.json b/erpnext/selling/report/quotation_trends/quotation_trends.json index 5587cc68b4a..a4011db4041 100644 --- a/erpnext/selling/report/quotation_trends/quotation_trends.json +++ b/erpnext/selling/report/quotation_trends/quotation_trends.json @@ -1,32 +1,37 @@ { - "add_total_row": 1, - "apply_user_permissions": 1, - "creation": "2013-06-07 16:01:16", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2018-02-21 01:28:14.928929", - "modified_by": "Administrator", - "module": "Selling", - "name": "Quotation Trends", - "owner": "Administrator", - "ref_doctype": "Quotation", - "report_name": "Quotation Trends", - "report_type": "Script Report", + "add_total_row": 0, + "add_translate_data": 0, + "columns": [], + "creation": "2013-06-07 16:01:16", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 3, + "is_standard": "Yes", + "letterhead": null, + "modified": "2025-11-05 11:55:50.127020", + "modified_by": "Administrator", + "module": "Selling", + "name": "Quotation Trends", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Quotation", + "report_name": "Quotation Trends", + "report_type": "Script Report", "roles": [ { "role": "Sales User" - }, + }, { "role": "Sales Manager" - }, + }, { "role": "Maintenance Manager" - }, + }, { "role": "Maintenance User" } - ] -} \ No newline at end of file + ], + "timeout": 0 +} diff --git a/erpnext/selling/report/sales_order_trends/sales_order_trends.json b/erpnext/selling/report/sales_order_trends/sales_order_trends.json index 9dfc7d0789c..dedec06bcf9 100644 --- a/erpnext/selling/report/sales_order_trends/sales_order_trends.json +++ b/erpnext/selling/report/sales_order_trends/sales_order_trends.json @@ -1,35 +1,40 @@ { - "add_total_row": 1, - "apply_user_permissions": 1, - "creation": "2013-06-13 18:43:30", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2018-02-20 08:05:46.191588", - "modified_by": "Administrator", - "module": "Selling", - "name": "Sales Order Trends", - "owner": "Administrator", - "ref_doctype": "Sales Order", - "report_name": "Sales Order Trends", - "report_type": "Script Report", + "add_total_row": 0, + "add_translate_data": 0, + "columns": [], + "creation": "2013-06-13 18:43:30", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 3, + "is_standard": "Yes", + "letterhead": null, + "modified": "2025-11-05 11:55:50.096303", + "modified_by": "Administrator", + "module": "Selling", + "name": "Sales Order Trends", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Sales Order", + "report_name": "Sales Order Trends", + "report_type": "Script Report", "roles": [ { "role": "Sales User" - }, + }, { "role": "Sales Manager" - }, + }, { "role": "Maintenance User" - }, + }, { "role": "Accounts User" - }, + }, { "role": "Stock User" } - ] -} \ No newline at end of file + ], + "timeout": 0 +} diff --git a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.json b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.json index fe133e9d5ab..cef82c5912d 100644 --- a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.json +++ b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.json @@ -1,32 +1,37 @@ { - "add_total_row": 1, - "apply_user_permissions": 1, - "creation": "2013-06-13 18:42:11", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2018-02-21 01:28:47.049042", - "modified_by": "Administrator", - "module": "Stock", - "name": "Delivery Note Trends", - "owner": "Administrator", - "ref_doctype": "Delivery Note", - "report_name": "Delivery Note Trends", - "report_type": "Script Report", + "add_total_row": 0, + "add_translate_data": 0, + "columns": [], + "creation": "2013-06-13 18:42:11", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 3, + "is_standard": "Yes", + "letterhead": null, + "modified": "2025-11-05 11:55:50.114173", + "modified_by": "Administrator", + "module": "Stock", + "name": "Delivery Note Trends", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Delivery Note", + "report_name": "Delivery Note Trends", + "report_type": "Script Report", "roles": [ { "role": "Stock User" - }, + }, { "role": "Stock Manager" - }, + }, { "role": "Sales User" - }, + }, { "role": "Accounts User" } - ] -} \ No newline at end of file + ], + "timeout": 0 +} diff --git a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.json b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.json index 696f81675cc..03c2a09f3bb 100644 --- a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.json +++ b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.json @@ -1,32 +1,37 @@ { - "add_total_row": 1, - "apply_user_permissions": 1, - "creation": "2013-06-13 18:45:44", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2018-02-21 01:28:22.682161", - "modified_by": "Administrator", - "module": "Stock", - "name": "Purchase Receipt Trends", - "owner": "Administrator", - "ref_doctype": "Purchase Receipt", - "report_name": "Purchase Receipt Trends", - "report_type": "Script Report", + "add_total_row": 0, + "add_translate_data": 0, + "columns": [], + "creation": "2013-06-13 18:45:44", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 3, + "is_standard": "Yes", + "letterhead": null, + "modified": "2025-11-05 11:55:49.983683", + "modified_by": "Administrator", + "module": "Stock", + "name": "Purchase Receipt Trends", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Purchase Receipt", + "report_name": "Purchase Receipt Trends", + "report_type": "Script Report", "roles": [ { "role": "Stock Manager" - }, + }, { "role": "Stock User" - }, + }, { "role": "Purchase User" - }, + }, { "role": "Accounts User" } - ] -} \ No newline at end of file + ], + "timeout": 0 +} From 3380deab02382fb043daa19c90d84204ad560e40 Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Tue, 4 Nov 2025 08:30:54 +0000 Subject: [PATCH 42/86] fix: validate is_group for parent task (cherry picked from commit ed1a1099cb53d336413b1e4df95cb5d46bddd5a2) # Conflicts: # erpnext/projects/doctype/task/task.py --- erpnext/projects/doctype/task/task.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 5eae55d71c5..5434b509624 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -17,6 +17,10 @@ class CircularReferenceError(frappe.ValidationError): pass +class ParentIsGroupError(frappe.ValidationError): + pass + + class Task(NestedSet): # begin: auto-generated types # This code is auto-generated. Do not modify anything in this block. @@ -83,6 +87,11 @@ class Task(NestedSet): self.update_depends_on() self.validate_dependencies_for_template_task() self.validate_completed_on() +<<<<<<< HEAD +======= + self.set_default_end_date_if_missing() + self.validate_parent_is_group() +>>>>>>> ed1a1099cb (fix: validate is_group for parent task) def validate_dates(self): self.validate_from_to_dates("exp_start_date", "exp_end_date") @@ -167,6 +176,14 @@ class Task(NestedSet): if self.completed_on and getdate(self.completed_on) > getdate(): frappe.throw(_("Completed On cannot be greater than Today")) + def validate_parent_is_group(self): + if self.parent_task: + if not frappe.db.get_value("Task", self.parent_task, "is_group"): + parent_task_format = f"""{self.parent_task}""" + frappe.throw( + _("Parent Task {0} must be a Group Task").format(parent_task_format), ParentIsGroupError + ) + def update_depends_on(self): depends_on_tasks = "" for d in self.depends_on: From dc5b8367c52810a6e7dc9518e951fd45aa82a497 Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Tue, 4 Nov 2025 08:31:41 +0000 Subject: [PATCH 43/86] test: add test for parent task is_group validation (cherry picked from commit 291f0c71613236cdbc92aff9979d13c4bca23817) # Conflicts: # erpnext/projects/doctype/task/test_task.py --- erpnext/projects/doctype/task/test_task.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index b0194b08dfa..0cb4348a2cb 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -6,7 +6,12 @@ import unittest import frappe from frappe.utils import add_days, getdate, nowdate +<<<<<<< HEAD from erpnext.projects.doctype.task.task import CircularReferenceError +======= +from erpnext.projects.doctype.task.task import CircularReferenceError, ParentIsGroupError +from erpnext.tests.utils import ERPNextTestSuite +>>>>>>> 291f0c7161 (test: add test for parent task is_group validation) class TestTask(unittest.TestCase): @@ -109,6 +114,20 @@ class TestTask(unittest.TestCase): self.assertEqual(frappe.db.get_value("Task", task.name, "status"), "Overdue") + def test_parent_task_must_be_group(self): + parent_task = create_task( + subject="_Test Parent Task Non Group", + is_group=0, + ) + + child_task = create_task( + subject="_Test Child Task", + parent_task=parent_task.name, + save=False, + ) + + self.assertRaises(ParentIsGroupError, child_task.save) + def create_task( subject, From 635fe427fe51de98486a95a74bb322eba2cec6b1 Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Thu, 6 Nov 2025 11:22:58 +0000 Subject: [PATCH 44/86] refactor(task): use get_link_to_form for validation error messages (cherry picked from commit 4cf02b4d78980c34b0de66aaac169681a4651577) --- erpnext/projects/doctype/task/task.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 5434b509624..ce324513951 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -162,15 +162,21 @@ class Task(NestedSet): def validate_parent_template_task(self): if self.parent_task: if not frappe.db.get_value("Task", self.parent_task, "is_template"): - parent_task_format = f"""{self.parent_task}""" - frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format)) + frappe.throw( + _("Parent Task {0} is not a Template Task").format( + get_link_to_form("Task", self.parent_task) + ) + ) def validate_depends_on_tasks(self): if self.depends_on: for task in self.depends_on: if not frappe.db.get_value("Task", task.task, "is_template"): - dependent_task_format = f"""{task.task}""" - frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format)) + frappe.throw( + _("Dependent Task {0} is not a Template Task").format( + get_link_to_form("Task", task.task) + ) + ) def validate_completed_on(self): if self.completed_on and getdate(self.completed_on) > getdate(): @@ -179,9 +185,11 @@ class Task(NestedSet): def validate_parent_is_group(self): if self.parent_task: if not frappe.db.get_value("Task", self.parent_task, "is_group"): - parent_task_format = f"""{self.parent_task}""" frappe.throw( - _("Parent Task {0} must be a Group Task").format(parent_task_format), ParentIsGroupError + _("Parent Task {0} must be a Group Task").format( + get_link_to_form("Task", self.parent_task) + ), + ParentIsGroupError, ) def update_depends_on(self): From 29c976e9ae9d3a7b3e455de4026df1b618cc843b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ei=C3=9Fler?= <77415730+PatrickDEissler@users.noreply.github.com> Date: Fri, 7 Nov 2025 08:56:44 +0100 Subject: [PATCH 45/86] fix(Timesheet): don't use billing_hours for costing amount (#50394) --- erpnext/projects/doctype/timesheet/timesheet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index c4ddbcaa8b1..0b4b99ba35b 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -285,7 +285,7 @@ class Timesheet(Document): if data.activity_type or data.is_billable: rate = get_activity_cost(self.employee, data.activity_type) hours = data.billing_hours or 0 - costing_hours = data.billing_hours or data.hours or 0 + costing_hours = data.hours or 0 if rate: data.billing_rate = ( flt(rate.get("billing_rate")) if flt(data.billing_rate) == 0 else data.billing_rate From 79b3af6d3efcf4e21ff181f08785bbed25915116 Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:49:59 +0530 Subject: [PATCH 46/86] fix: check warehouse account before accessing --- .../doctype/subcontracting_receipt/subcontracting_receipt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index d2ceb90ad52..520d7bb0e16 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -599,7 +599,7 @@ class SubcontractingReceipt(SubcontractingController): for item in self.items: if flt(item.rate) and flt(item.qty): - if warehouse_account.get(item.warehouse): + if warehouse_account and warehouse_account.get(item.warehouse): stock_value_diff = frappe.db.get_value( "Stock Ledger Entry", { From 080e9a3d73d4a9be2e0b4777918058548ccb79ff Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 7 Nov 2025 16:39:13 +0530 Subject: [PATCH 47/86] fix: removed the validation (cherry picked from commit 10131333b202fefa75cf0174f910e6d288a212ab) --- .../doctype/stock_settings/stock_settings.py | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 922bdee6e47..c05b100b3fc 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -186,26 +186,6 @@ class StockSettings(Document): ) ) - else: - # Don't allow if there are negative stock - from frappe.query_builder.functions import Round - - precision = frappe.db.get_single_value("System Settings", "float_precision") or 3 - bin = frappe.qb.DocType("Bin") - bin_with_negative_stock = ( - frappe.qb.from_(bin) - .select(bin.name) - .where(Round(bin.actual_qty, precision) < 0) - .limit(1) - ).run() - - if bin_with_negative_stock: - frappe.throw( - _("As there are negative stock, you can not enable {0}.").format( - frappe.bold(_("Stock Reservation")) - ) - ) - # Enable -> Disable else: # Don't allow if there are open Stock Reservation Entries From 9d979e34abbc99045b0d11464db8150d11ce5172 Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Fri, 7 Nov 2025 11:16:58 +0530 Subject: [PATCH 48/86] fix: handle partial dn against reserved stock --- .../doctype/delivery_note/delivery_note.py | 65 +++++++++++++++++++ .../serial_and_batch_bundle.py | 17 +++++ .../stock_reservation_entry.py | 38 +++++++++-- erpnext/stock/stock_ledger.py | 42 +++++++----- 4 files changed, 140 insertions(+), 22 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 792bbf902fc..263d45599ed 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -17,6 +17,7 @@ from frappe.utils import cint, flt from erpnext.accounts.party import get_due_date from erpnext.controllers.accounts_controller import get_taxes_and_charges, merge_taxes from erpnext.controllers.selling_controller import SellingController +from erpnext.stock.stock_ledger import validate_reserved_stock form_grid_templates = {"items": "templates/form_grid/item_grid.html"} @@ -469,6 +470,10 @@ class DeliveryNote(SellingController): self.make_bundle_using_old_serial_batch_fields(table_name) self.validate_standalone_serial_nos_customer() + + if not self.is_return: + self.validate_reserved_stock() + self.update_stock_reservation_entries() # Updating stock ledger should always be called after updating prevdoc status, @@ -506,6 +511,66 @@ class DeliveryNote(SellingController): self.delete_auto_created_batches() + def validate_reserved_stock(self): + from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( + get_sre_against_so_for_dn, + ) + + if not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"): + return + + # fetch reserved stock data from bin + reserved_stocks = self.get_reserved_stock_details() + + for row in self.items: + if reserved_stocks.get((row.item_code, row.warehouse)) > 0: + args = frappe._dict( + { + "item_code": row.item_code, + "warehouse": row.warehouse, + "batch_nos": [row.batch_no] if row.batch_no else [], + "serial_nos": row.serial_no.split("\n") if row.serial_no else [], + "serial_and_batch_bundle": row.serial_and_batch_bundle, + "voucher_type": self.doctype, + "voucher_no": self.name, + "voucher_detail_no": row.name, + "actual_qty": row.qty * -1, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + } + ) + + if row.against_sales_order and row.so_detail: + args.ignore_voucher_nos = get_sre_against_so_for_dn( + row.against_sales_order, row.so_detail + ) + + validate_reserved_stock(args) + + def get_reserved_stock_details(self): + """ + Create dict from bin based on item and warehouse: + {(item_code, warehouse): reserved_stock} + + Use: to quickly retrieve/check reserved stock value instead of looping n times + """ + item_codes = set() + warehouses = set() + + for row in self.items: + item_codes.add(row.item_code) + warehouses.add(row.warehouse) + + bins = frappe.db.get_all( + "Bin", + {"item_code": ["in", item_codes], "warehouse": ["in", warehouses]}, + ["item_code", "warehouse", "reserved_stock"], + ) + + reserved_stock_lookup = {(b.item_code, b.warehouse): flt(b.reserved_stock) for b in bins} + + return reserved_stock_lookup + def validate_against_stock_reservation_entries(self): """Validates if Stock Reservation Entries are available for the Sales Order Item reference.""" 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 1992b5dc49f..8d216171641 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 @@ -253,6 +253,9 @@ class SerialandBatchBundle(Document): } ) + if self.voucher_type == "Delivery Note": + kwargs["ignore_voucher_nos"] = self.get_sre_against_dn() + available_serial_nos = get_available_serial_nos(frappe._dict(kwargs)) serial_no_warehouse = {} @@ -1380,6 +1383,20 @@ class SerialandBatchBundle(Document): self.set("entries", []) + def get_sre_against_dn(self): + from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( + get_sre_against_so_for_dn, + ) + + so_name, so_detail_no = frappe.db.get_value( + "Delivery Note Item", self.voucher_detail_no, ["against_sales_order", "so_detail"] + ) + + if so_name and so_detail_no: + sre_names = get_sre_against_so_for_dn(so_name, so_detail_no) + + return sre_names + @frappe.whitelist() def download_blank_csv_template(content): diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index 5d6aa9f68f8..a6a69f3271e 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -738,7 +738,7 @@ def get_sre_reserved_qty_for_voucher_detail_no( def get_sre_reserved_serial_nos_details( - item_code: str, warehouse: str, serial_nos: list | None = None + item_code: str, warehouse: str, serial_nos: list | None = None, ignore_voucher_nos: list | None = None ) -> dict: """Returns a dict of `Serial No` reserved in Stock Reservation Entry. The dict is like {serial_no: sre_name, ...}""" @@ -753,8 +753,7 @@ def get_sre_reserved_serial_nos_details( (sre.docstatus == 1) & (sre.item_code == item_code) & (sre.warehouse == warehouse) - & (sre.reserved_qty > sre.delivered_qty) - & (sre.status.notin(["Delivered", "Cancelled"])) + & (sre.delivered_qty < sre.reserved_qty) & (sre.reservation_based_on == "Serial and Batch") ) .orderby(sb_entry.creation) @@ -763,10 +762,15 @@ def get_sre_reserved_serial_nos_details( if serial_nos: query = query.where(sb_entry.serial_no.isin(serial_nos)) + if ignore_voucher_nos: + query = query.where(sre.name.notin(ignore_voucher_nos)) + return frappe._dict(query.run()) -def get_sre_reserved_batch_nos_details(item_code: str, warehouse: str, batch_nos: list | None = None) -> dict: +def get_sre_reserved_batch_nos_details( + item_code: str, warehouse: str, batch_nos: list | None = None, ignore_voucher_nos: list | None = None +) -> dict: """Returns a dict of `Batch Qty` reserved in Stock Reservation Entry. The dict is like {batch_no: qty, ...}""" sre = frappe.qb.DocType("Stock Reservation Entry") @@ -784,7 +788,7 @@ def get_sre_reserved_batch_nos_details(item_code: str, warehouse: str, batch_nos & (sre.item_code == item_code) & (sre.warehouse == warehouse) & ((sre.reserved_qty - sre.delivered_qty) > 0) - & (sre.status.notin(["Delivered", "Cancelled"])) + & (sre.delivered_qty < sre.reserved_qty) & (sre.reservation_based_on == "Serial and Batch") ) .groupby(sb_entry.batch_no) @@ -794,6 +798,9 @@ def get_sre_reserved_batch_nos_details(item_code: str, warehouse: str, batch_nos if batch_nos: query = query.where(sb_entry.batch_no.isin(batch_nos)) + if ignore_voucher_nos: + query = query.where(sre.name.notin(ignore_voucher_nos)) + return frappe._dict(query.run()) @@ -1175,3 +1182,24 @@ def get_stock_reservation_entries_for_voucher( query = query.where(sre.status.notin(["Delivered", "Cancelled"])) return query.run(as_dict=True) + + +@frappe.request_cache +def get_sre_against_so_for_dn(so_name: str, so_detail_no: str) -> list[str]: + """Returns list of Stock Reservation Entries against Delivery Note with Sales Order Reference.""" + + sre = frappe.qb.DocType("Stock Reservation Entry") + query = ( + frappe.qb.from_(sre) + .select(sre.name) + .where( + (sre.docstatus == 1) + & (sre.voucher_type == "Sales Order") + & (sre.voucher_no == so_name) + & (sre.voucher_detail_no == so_detail_no) + ) + ) + + result = query.run(as_list=True) + + return result[0] if result else [] diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index bc54f88687e..34b442572a0 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -2166,7 +2166,7 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False): ) frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch")) - if args.reserved_stock: + if args.reserved_stock and args.voucher_type != "Delivery Note": validate_reserved_stock(args) @@ -2236,11 +2236,10 @@ def get_future_sle_with_negative_batch_qty(sle_args): def validate_reserved_stock(kwargs): if kwargs.serial_no: - serial_nos = kwargs.serial_no.split("\n") - validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos) + validate_reserved_serial_nos(kwargs) elif kwargs.batch_no: - validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, [kwargs.batch_no]) + validate_reserved_batch_nos(kwargs) elif kwargs.serial_and_batch_bundle: sbb_entries = frappe.db.get_all( @@ -2254,9 +2253,11 @@ def validate_reserved_stock(kwargs): ) if serial_nos := [entry.serial_no for entry in sbb_entries if entry.serial_no]: - validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos) + kwargs.serial_nos = serial_nos + validate_reserved_serial_nos(kwargs) elif batch_nos := [entry.batch_no for entry in sbb_entries if entry.batch_no]: - validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, batch_nos) + kwargs.batch_nos = batch_nos + validate_reserved_batch_nos(kwargs) # Qty based validation for non-serial-batch items OR SRE with Reservation Based On Qty. precision = cint(frappe.db.get_default("float_precision")) or 2 @@ -2274,9 +2275,13 @@ def validate_reserved_stock(kwargs): frappe.throw(msg, title=_("Reserved Stock")) -def validate_reserved_serial_nos(item_code, warehouse, serial_nos): - if reserved_serial_nos_details := get_sre_reserved_serial_nos_details(item_code, warehouse, serial_nos): - if common_serial_nos := list(set(serial_nos).intersection(set(reserved_serial_nos_details.keys()))): +def validate_reserved_serial_nos(kwargs): + if reserved_serial_nos_details := get_sre_reserved_serial_nos_details( + kwargs.item_code, kwargs.warehouse, kwargs.serial_nos, kwargs.ignore_voucher_nos + ): + if common_serial_nos := list( + set(kwargs.serial_nos).intersection(set(reserved_serial_nos_details.keys())) + ): msg = _( "Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding." ) @@ -2290,22 +2295,25 @@ def validate_reserved_serial_nos(item_code, warehouse, serial_nos): frappe.throw(msg, title=_("Reserved Serial No.")) -def validate_reserved_batch_nos(item_code, warehouse, batch_nos): - if reserved_batches_map := get_sre_reserved_batch_nos_details(item_code, warehouse, batch_nos): +def validate_reserved_batch_nos(kwargs): + if reserved_batches_map := get_sre_reserved_batch_nos_details( + kwargs.item_code, kwargs.warehouse, kwargs.batch_nos, kwargs.ignore_voucher_nos + ): available_batches = get_auto_batch_nos( frappe._dict( { - "item_code": item_code, - "warehouse": warehouse, - "posting_date": nowdate(), - "posting_time": nowtime(), + "item_code": kwargs.item_code, + "warehouse": kwargs.warehouse, + "posting_date": kwargs.posting_date, + "posting_time": kwargs.posting_time, + "ignore_voucher_nos": kwargs.ignore_voucher_nos, } ) ) available_batches_map = {row.batch_no: row.qty for row in available_batches} precision = cint(frappe.db.get_default("float_precision")) or 2 - for batch_no in batch_nos: + for batch_no in kwargs.batch_nos: diff = flt( available_batches_map.get(batch_no, 0) - reserved_batches_map.get(batch_no, 0), precision ) @@ -2313,7 +2321,7 @@ def validate_reserved_batch_nos(item_code, warehouse, batch_nos): msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format( abs(diff), frappe.get_desk_link("Batch", batch_no), - frappe.get_desk_link("Warehouse", warehouse), + frappe.get_desk_link("Warehouse", kwargs.warehouse), nowdate(), nowtime(), ) From ef719fe7296d4621acae73aaa014b40109e67aee Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Fri, 7 Nov 2025 11:17:57 +0530 Subject: [PATCH 49/86] test: add test for partial dn against reserved stock --- .../delivery_note/test_delivery_note.py | 71 ++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index dd9d247902b..450c2243620 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -39,7 +39,7 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation, set_valuation_method, ) -from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse +from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse, get_warehouse from erpnext.stock.stock_ledger import get_previous_sle @@ -2719,6 +2719,75 @@ class TestDeliveryNote(FrappeTestCase): serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no], ) + @change_settings("Stock Settings", {"allow_negative_stock": 0, "enable_stock_reservation": 1}) + def test_partial_delivery_note_against_reserved_stock(self): + from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( + get_stock_reservation_entries_for_voucher, + ) + + # create batch item + batch_item = make_item( + "_Test Batch Item For DN Reserve Check", + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TBDNR.#####", + }, + ) + serial_item = make_item( + "_Test Serial Item For DN Reserve Check", + { + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "TSNDNR.#####", + }, + ) + + company = "_Test Company" + + warehouse = create_warehouse("Test Partial DN Reserved Stock", company=company) + customer = "_Test Customer" + + items = [batch_item.name, serial_item.name] + + for idx, item in enumerate(items): + # make inward entry for batch item + se = make_stock_entry(item_code=item, purpose="Material Receipt", qty=10, to_warehouse=warehouse) + sabb = se.items[0].serial_and_batch_bundle + + batch_no = get_batch_from_bundle(sabb) if not idx else None + serial_nos = get_serial_nos_from_bundle(sabb) if idx else None + + # make sales order and reserve the quantites against the so + so = make_sales_order(item_code=item, qty=10, rate=100, customer=customer, warehouse=warehouse) + so.submit() + so.create_stock_reservation_entries() + so.reload() + + # create a delivery note with partial quantity from resreved quantity + dn = create_dn_against_so(so=so.name, delivered_qty=5, do_not_submit=True) + dn.items[0].use_serial_batch_fields = 1 + if batch_no: + dn.items[0].batch_no = batch_no + else: + dn.items[0].serial_no = "\n".join(serial_nos[:5]) + + dn.save() + dn.submit() + + against_sales_order = dn.items[0].against_sales_order + so_detail = dn.items[0].so_detail + + sre_details = get_stock_reservation_entries_for_voucher( + so.doctype, against_sales_order, so_detail, ["reserved_qty", "delivered_qty", "status"] + ) + + # check partially delivered reserved stock + self.assertEqual(sre_details[0].status, "Partially Delivered") + self.assertEqual(sre_details[0].reserved_qty, so.items[0].qty) + self.assertEqual(sre_details[0].delivered_qty, dn.items[0].qty) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") From ac0375fc2e0e0ac3073efa6fd391b494a592b0b5 Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Wed, 5 Nov 2025 17:34:55 +0000 Subject: [PATCH 50/86] fix: add validation to reject empty readings (cherry picked from commit 405d901514519a043e43dafa9b9b14f350536a44) --- .../stock/doctype/quality_inspection/quality_inspection.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index c7a315d2298..faef473a7fa 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -283,9 +283,11 @@ class QualityInspection(Document): def min_max_criteria_passed(self, reading): """Determine whether all readings fall in the acceptable range.""" + has_reading = False for i in range(1, 11): reading_value = reading.get("reading_" + str(i)) if reading_value is not None and reading_value.strip(): + has_reading = True result = ( flt(reading.get("min_value")) <= parse_float(reading_value) @@ -293,7 +295,7 @@ class QualityInspection(Document): ) if not result: return False - return True + return has_reading def set_status_based_on_acceptance_formula(self, reading): if not reading.acceptance_formula: From dc88f7b30bc38d8027b33bdb7e34b089b5cd93ed Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Thu, 6 Nov 2025 06:32:04 +0000 Subject: [PATCH 51/86] refactor: add default reading value when creating a quality inspection (cherry picked from commit 63fb9f55e7935b247d419f6b2a1efda21831eb48) --- .../stock/doctype/quality_inspection/test_quality_inspection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index f59d7c3216b..b0b67d8c8ea 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -290,7 +290,7 @@ def create_quality_inspection(**args): if not args.readings: create_quality_inspection_parameter("Size") - readings = {"specification": "Size", "min_value": 0, "max_value": 10} + readings = {"specification": "Size", "min_value": 0, "max_value": 10, "reading_1": "5"} if args.status == "Rejected": readings["reading_1"] = "12" # status is auto set in child on save else: From e8e26a91bb6367057cb70e760d659065a8620a7b Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 9 Nov 2025 17:56:24 +0100 Subject: [PATCH 52/86] fix(buying): fetch Cost Center from Project (cherry picked from commit bdabcb081a45f6b0d5a80c654523610237cc334c) --- erpnext/public/js/controllers/buying.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 8c0be8042ac..ba1e5348847 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -2,7 +2,6 @@ // License: GNU General Public License v3. See license.txt frappe.provide("erpnext.buying"); -// cur_frm.add_fetch('project', 'cost_center', 'cost_center'); erpnext.buying = { setup_buying_controller: function() { @@ -11,6 +10,7 @@ erpnext.buying = { super.setup(); this.toggle_enable_for_stock_uom("allow_to_edit_stock_uom_qty_for_purchase"); this.frm.email_field = "contact_email"; + this.frm.add_fetch("project", "cost_center", "cost_center"); } onload(doc, cdt, cdn) { From df6ca3af57d9104a7f84acd386883745625ca6b9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 10 Nov 2025 12:43:05 +0530 Subject: [PATCH 53/86] chore: resolve conflicts --- erpnext/projects/doctype/task/task.py | 4 ---- erpnext/projects/doctype/task/test_task.py | 5 ----- 2 files changed, 9 deletions(-) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index ce324513951..7eb55272eb8 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -87,11 +87,7 @@ class Task(NestedSet): self.update_depends_on() self.validate_dependencies_for_template_task() self.validate_completed_on() -<<<<<<< HEAD -======= - self.set_default_end_date_if_missing() self.validate_parent_is_group() ->>>>>>> ed1a1099cb (fix: validate is_group for parent task) def validate_dates(self): self.validate_from_to_dates("exp_start_date", "exp_end_date") diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index 0cb4348a2cb..bf5852834b2 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -6,12 +6,7 @@ import unittest import frappe from frappe.utils import add_days, getdate, nowdate -<<<<<<< HEAD -from erpnext.projects.doctype.task.task import CircularReferenceError -======= from erpnext.projects.doctype.task.task import CircularReferenceError, ParentIsGroupError -from erpnext.tests.utils import ERPNextTestSuite ->>>>>>> 291f0c7161 (test: add test for parent task is_group validation) class TestTask(unittest.TestCase): From 0e7f9711e12923fda462099d52e441b0546dc23d Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:28:37 +0530 Subject: [PATCH 54/86] fix(stock): ignore current voucher in reserved stock validation --- erpnext/stock/stock_ledger.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 34b442572a0..47cb41852c2 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -2235,6 +2235,10 @@ def get_future_sle_with_negative_batch_qty(sle_args): def validate_reserved_stock(kwargs): + # ignore current voucher when validating the reserved stock + if not kwargs.ignore_voucher_nos and kwargs.voucher_no: + kwargs.ignore_voucher_nos = [kwargs.voucher_no] + if kwargs.serial_no: validate_reserved_serial_nos(kwargs) From b8ec3ae23aab5ad5a15a4ac61352695a3041c537 Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:51:24 +0530 Subject: [PATCH 55/86] fix: Nonetype error if reserved stock is not present --- erpnext/stock/doctype/delivery_note/delivery_note.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 263d45599ed..4a0d4048b78 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -523,7 +523,7 @@ class DeliveryNote(SellingController): reserved_stocks = self.get_reserved_stock_details() for row in self.items: - if reserved_stocks.get((row.item_code, row.warehouse)) > 0: + if flt(reserved_stocks.get((row.item_code, row.warehouse))) > 0: args = frappe._dict( { "item_code": row.item_code, From 73b8a294cfad149da5ebde3c2fb2b7bd058f98de Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Wed, 5 Nov 2025 08:09:56 +0000 Subject: [PATCH 56/86] fix: reset billing and shipping address when company changes (cherry picked from commit 0510f7e13ff91879b5fcd92be597cba0918869c2) --- erpnext/public/js/controllers/buying.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index ba1e5348847..c8a61a0ca79 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -171,15 +171,13 @@ erpnext.buying = { shipping_address: this.frm.doc.shipping_address }, callback: (r) => { - if (!this.frm.doc.billing_address) - this.frm.set_value("billing_address", r.message.primary_address || ""); + if (!r.message) return; - if ( - !frappe.meta.has_field(this.frm.doc.doctype, "shipping_address") || - this.frm.doc.shipping_address - ) - return; - this.frm.set_value("shipping_address", r.message.shipping_address || ""); + this.frm.set_value("billing_address", r.message.primary_address || ""); + + if (frappe.meta.has_field(this.frm.doc.doctype, "shipping_address")) { + this.frm.set_value("shipping_address", r.message.shipping_address || ""); + } }, }); erpnext.utils.set_letter_head(this.frm) From 6ba24912c32420786165f496c98b14684b54c919 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 10 Nov 2025 16:35:36 +0530 Subject: [PATCH 57/86] perf: serial no creation (cherry picked from commit 19a9497273c39b136459c883ab839de7e34f88c4) # Conflicts: # erpnext/stock/serial_batch_bundle.py --- erpnext/stock/serial_batch_bundle.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 890899c7343..c5a8feb3e74 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -2,7 +2,7 @@ from collections import defaultdict import frappe from frappe import _, bold -from frappe.model.naming import make_autoname +from frappe.model.naming import NamingSeries, make_autoname, parse_naming_series from frappe.query_builder import Case from frappe.query_builder.functions import CombineDatetime, Sum, Timestamp from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, now, nowtime, today @@ -1334,8 +1334,29 @@ class SerialBatchCreation: if self.get("voucher_no"): voucher_no = self.get("voucher_no") +<<<<<<< HEAD +======= + voucher_type = "" + if self.get("voucher_type"): + voucher_type = self.get("voucher_type") + + obj = NamingSeries(self.serial_no_series) + current_value = obj.get_current_value() + + def get_series(partial_series, digits): + return f"{current_value:0{digits}d}" + + posting_date = frappe.db.get_value( + voucher_type, + voucher_no, + "posting_date", + ) + +>>>>>>> 19a9497273 (perf: serial no creation) for _i in range(abs(cint(self.actual_qty))): - serial_no = make_autoname(self.serial_no_series, "Serial No") + current_value += 1 + serial_no = parse_naming_series(self.serial_no_series, number_generator=get_series) + sr_nos.append(serial_no) serial_nos_details.append( ( @@ -1376,6 +1397,8 @@ class SerialBatchCreation: frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details)) + obj.update_counter(current_value) + return sr_nos From 3f490f11d52a7cdaa3dfb180cb14bb89ea6c4f4f Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Tue, 4 Nov 2025 18:30:53 +0530 Subject: [PATCH 58/86] fix: include cost_center and project upon accounting dimension fetch (cherry picked from commit 46802953034db8fc2254330680cfbd8ebd78607b) --- .../doctype/accounting_dimension/accounting_dimension.py | 4 ++-- .../doctype/payment_reconciliation/payment_reconciliation.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index cdaaa6dd069..02019943c25 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -309,8 +309,8 @@ def get_dimensions(with_cost_center_and_project=False): if with_cost_center_and_project: dimension_filters.extend( [ - {"fieldname": "cost_center", "document_type": "Cost Center"}, - {"fieldname": "project", "document_type": "Project"}, + frappe._dict({"fieldname": "cost_center", "document_type": "Cost Center"}), + frappe._dict({"fieldname": "project", "document_type": "Project"}), ] ) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 1e1a71ea707..2dec2bf8e94 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -72,7 +72,7 @@ class PaymentReconciliation(Document): self.common_filter_conditions = [] self.accounting_dimension_filter_conditions = [] self.ple_posting_date_filter = [] - self.dimensions = get_dimensions()[0] + self.dimensions = get_dimensions(with_cost_center_and_project=True)[0] def load_from_db(self): # 'modified' attribute is required for `run_doc_method` to work properly. From b1816864deaa1076121906197a32a345f26cf9f1 Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Tue, 4 Nov 2025 18:31:47 +0530 Subject: [PATCH 59/86] fix: apply company,is_group filter for cost center (cherry picked from commit dcdc1c6a89fd52efdf7f116fd767ea1ccab7518f) --- .../payment_reconciliation.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index 75a86318fbb..74a9f709caa 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -61,6 +61,22 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo }, }; }); + this.frm.set_query("cost_center", "payments", () => { + return { + filters: { + company: this.frm.doc.company, + is_group: 0, + }, + }; + }); + this.frm.set_query("cost_center", "allocation", () => { + return { + filters: { + company: this.frm.doc.company, + is_group: 0, + }, + }; + }); } refresh() { From 5b643433e51954d0030b9d2a4ed8fef9d63fcd1c Mon Sep 17 00:00:00 2001 From: Assem Bahnasy Date: Tue, 11 Nov 2025 07:23:25 +0300 Subject: [PATCH 60/86] Fix: Product Bundle Purchase Order Creation Logic (#49831) Co-authored-by: Mihir Kandoi --- erpnext/selling/doctype/sales_order/sales_order.js | 8 +++++--- erpnext/selling/doctype/sales_order/sales_order.py | 6 ++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 4e6f8203f58..3e0425c1201 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -1179,7 +1179,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex make_purchase_order() { let pending_items = this.frm.doc.items.some((item) => { - let pending_qty = flt(item.stock_qty) - flt(item.ordered_qty); + const pending_qty = flt(item.stock_qty) - this.get_ordered_qty(item, this.frm.doc); return pending_qty > 0; }); if (!pending_items) { @@ -1333,8 +1333,10 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex // calculate ordered qty based on packed items in case of product bundle let packed_items = so.packed_items.filter((pi) => pi.parent_detail_docname == item.name); if (packed_items && packed_items.length) { - ordered_qty = packed_items.reduce((sum, pi) => sum + flt(pi.ordered_qty), 0); - ordered_qty = ordered_qty / packed_items.length; + const all_packed_items_ordered = packed_items.every( + (pi) => flt(pi.ordered_qty) >= flt(pi.qty) + ); + ordered_qty = all_packed_items_ordered ? item.stock_qty : 0; } } return ordered_qty; diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index eb6134d4259..a6f1cf4b666 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1465,7 +1465,8 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t "pricing_rules", ], "postprocess": update_item_for_packed_item, - "condition": lambda doc: doc.parent_item in items_to_map, + "condition": lambda doc: doc.parent_item in items_to_map + and flt(doc.ordered_qty) < flt(doc.qty), }, }, target_doc, @@ -1603,7 +1604,8 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): "pricing_rules", ], "postprocess": update_item_for_packed_item, - "condition": lambda doc: doc.parent_item in items_to_map, + "condition": lambda doc: doc.parent_item in items_to_map + and flt(doc.ordered_qty) < flt(doc.qty), }, }, target_doc, From 8f2002d419047eeb9dcb96ecb71cf8b82bf529f1 Mon Sep 17 00:00:00 2001 From: Rehan Ansari <32939507+rehanrehman389@users.noreply.github.com> Date: Tue, 11 Nov 2025 09:58:33 +0530 Subject: [PATCH 61/86] fix: add missing stock entry UOM filtering based on item master (#50135) Co-authored-by: rehansari26 (cherry picked from commit 7baf6ec3d648aa2bbdafd2a49d67c3a5d4a2ff4d) --- erpnext/stock/doctype/stock_entry/stock_entry.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 332f05fafd2..a41a529ff2c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1073,6 +1073,21 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle }; }); + this.frm.set_query("uom", "items", function (doc, cdt, cdn) { + let row = locals[cdt][cdn]; + + if (!row.item_code) { + return; + } + + return { + query: "erpnext.controllers.queries.get_item_uom_query", + filters: { + item_code: row.item_code, + }, + }; + }); + this.frm.fields_dict.items.grid.get_field("expense_account").get_query = function () { if (erpnext.is_perpetual_inventory_enabled(me.frm.doc.company)) { return { From f2fef54b835650a11911d4608991fb1c3ee0c316 Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Thu, 6 Nov 2025 19:59:42 +0000 Subject: [PATCH 62/86] fix: material request item quantity validation against sales order with over-receipt allowance (cherry picked from commit 8d7e31e3f2550023883e485c70e40daac71aebc2) --- .../material_request/material_request.py | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 6cd5a035c99..8b0accee41d 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -75,6 +75,21 @@ class MaterialRequest(BuyingController): work_order: DF.Link | None # end: auto-generated types + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.status_updater = [ + { + "source_dt": "Material Request Item", + "target_dt": "Sales Order Item", + "target_field": "ordered_qty", + "target_parent_dt": "Sales Order", + "target_parent_field": "", + "join_field": "sales_order_item", + "target_ref_field": "stock_qty", + "source_field": "stock_qty", + } + ] + def check_if_already_pulled(self): pass @@ -175,10 +190,10 @@ class MaterialRequest(BuyingController): def on_submit(self): self.update_requested_qty_in_production_plan() self.update_requested_qty() - if self.material_request_type == "Purchase" and frappe.db.exists( - "Budget", {"applicable_on_material_request": 1, "docstatus": 1} - ): - self.validate_budget() + if self.material_request_type == "Purchase": + self.update_prevdoc_status() + if frappe.db.exists("Budget", {"applicable_on_material_request": 1, "docstatus": 1}): + self.validate_budget() def before_save(self): self.set_status(update=True) From 249d14b0721370145d5fd625bf81c1525d16038b Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Thu, 6 Nov 2025 20:06:07 +0000 Subject: [PATCH 63/86] test: add test for validate mr item qty against so with over-receipt allowance (cherry picked from commit 55f531bad620836ae2d5fd79903dbcabaafb737d) --- .../material_request/test_material_request.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 66dd9c0c795..d3d512fd143 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -883,6 +883,18 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(mr.per_ordered, 100) self.assertEqual(mr.status, "Ordered") + def test_material_request_qty_over_sales_order_limit(self): + from erpnext.controllers.status_updater import OverAllowanceError + from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + + so = make_sales_order() + mr = make_material_request(qty=100, do_not_submit=True) + mr.items[0].sales_order = so.name + mr.items[0].sales_order_item = so.items[0].name + mr.save() + + self.assertRaises(OverAllowanceError, mr.submit) + def get_in_transit_warehouse(company): if not frappe.db.exists("Warehouse Type", "Transit"): From 9012a721852e35addcda803da5b01fe3f523052d Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Mon, 10 Nov 2025 13:59:27 +0000 Subject: [PATCH 64/86] fix: change fieldtype from link to data for document_type in production plan summary (cherry picked from commit 462deb3755ab94c47cc94bbbbb1961e15af84eca) --- .../report/production_plan_summary/production_plan_summary.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py index c62cab77d61..052b2536d00 100644 --- a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py +++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py @@ -151,10 +151,9 @@ def get_column(filters): }, { "label": _("Document Type"), - "fieldtype": "Link", + "fieldtype": "Data", "fieldname": "document_type", "width": 150, - "options": "DocType", }, { "label": _("Document Name"), From 2b766bca974db3bccace3976dc52252d449f1bdd Mon Sep 17 00:00:00 2001 From: rethik Date: Tue, 4 Nov 2025 11:10:27 +0530 Subject: [PATCH 65/86] chore: fix typo "show_disables_items" to "show_disabled_items" (cherry picked from commit d26c598daa9a6cefe9df095577951920565ea714) --- .../stock_qty_vs_serial_no_count.js | 2 +- .../stock_qty_vs_serial_no_count.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js index df146358390..c38f0237436 100644 --- a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js @@ -25,7 +25,7 @@ frappe.query_reports["Stock Qty vs Serial No Count"] = { reqd: 1, }, { - fieldname: "show_disables_items", + fieldname: "show_disabled_items", label: __("Show Disabled Items"), fieldtype: "Check", }, diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py index b29674bbd94..6087c747374 100644 --- a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py @@ -9,7 +9,7 @@ from frappe import _ def execute(filters=None): validate_warehouse(filters) columns = get_columns() - data = get_data(filters.warehouse, filters.show_disables_items) + data = get_data(filters.warehouse, filters.show_disabled_items) return columns, data @@ -38,9 +38,9 @@ def get_columns(): return columns -def get_data(warehouse, show_disables_items): +def get_data(warehouse, show_disabled_items): filters = {"has_serial_no": True} - if not show_disables_items: + if not show_disabled_items: filters["disabled"] = False serial_item_list = frappe.get_all( "Item", From 0f00581f839f64a017d1c75408192f58d8990d92 Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Mon, 3 Nov 2025 15:54:00 +0000 Subject: [PATCH 66/86] fix: add is_group filter in task for timesheet (cherry picked from commit 5bac8963296648cbf9a385a90f195bcf9beb5270) --- erpnext/projects/doctype/timesheet/timesheet.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index 4c78d939ebc..9b01d1c429f 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -21,6 +21,7 @@ frappe.ui.form.on("Timesheet", { filters: { project: child.project, status: ["!=", "Cancelled"], + is_group: 0, }, }; }; From d4f6ca35648f97c532b07e8e70091ce347ab4892 Mon Sep 17 00:00:00 2001 From: mahsem <137205921+mahsem@users.noreply.github.com> Date: Tue, 11 Nov 2025 06:07:34 +0100 Subject: [PATCH 67/86] fix: state_to_state_province for translation (#50244) Co-authored-by: Mihir Kandoi (cherry picked from commit e148a38353e70ac1e0d64769f29d0802656bf7ce) --- erpnext/utilities/web_form/addresses/addresses.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/utilities/web_form/addresses/addresses.json b/erpnext/utilities/web_form/addresses/addresses.json index 38b73926985..74700607ff1 100644 --- a/erpnext/utilities/web_form/addresses/addresses.json +++ b/erpnext/utilities/web_form/addresses/addresses.json @@ -21,8 +21,8 @@ "list_title": "", "login_required": 1, "max_attachment_size": 0, - "modified": "2024-01-24 10:28:35.026064", - "modified_by": "rohitw1991@gmail.com", + "modified": "2025-10-27 21:05:21.125639", + "modified_by": "Administrator", "module": "Utilities", "name": "addresses", "owner": "Administrator", @@ -113,7 +113,7 @@ "fieldname": "state", "fieldtype": "Data", "hidden": 0, - "label": "State", + "label": "State/Province", "max_length": 0, "max_value": 0, "read_only": 0, @@ -197,4 +197,4 @@ "show_in_filter": 0 } ] -} \ No newline at end of file +} From 3dbc90a0b4c399638de7519fa524a29515f2fa4e Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Sat, 8 Nov 2025 15:06:58 +0530 Subject: [PATCH 68/86] fix: show only stock items in delivered items to be billed and received items to be billed reports (cherry picked from commit 1b2e5c97065d08efbeaa5cf57211948dfb5be67b) --- erpnext/accounts/report/non_billed_report.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/report/non_billed_report.py b/erpnext/accounts/report/non_billed_report.py index 6a6f58c74bb..c3e3df5fb00 100644 --- a/erpnext/accounts/report/non_billed_report.py +++ b/erpnext/accounts/report/non_billed_report.py @@ -21,6 +21,7 @@ def get_ordered_to_be_billed_data(args, filters=None): doctype = frappe.qb.DocType(doctype) child_doctype = frappe.qb.DocType(child_tab) + item = frappe.qb.DocType("Item") docname = filters.get(args.get("reference_field"), None) project_field = get_project_field(doctype, child_doctype, party) @@ -29,6 +30,8 @@ def get_ordered_to_be_billed_data(args, filters=None): frappe.qb.from_(doctype) .inner_join(child_doctype) .on(doctype.name == child_doctype.parent) + .join(item) + .on(item.name == child_doctype.item_code) .select( doctype.name, doctype[args.get("date")].as_("date"), @@ -54,6 +57,7 @@ def get_ordered_to_be_billed_data(args, filters=None): & (doctype.company == filters.get("company")) & (doctype.posting_date <= filters.get("posting_date")) & (child_doctype.amount > 0) + & (item.is_stock_item == 1) & ( child_doctype.base_amount - Round(child_doctype.billed_amt * IfNull(doctype.conversion_rate, 1), precision) From 4ec25ac82e411ac3eb3fc8148fd80883adfa4010 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 11 Nov 2025 10:47:06 +0530 Subject: [PATCH 69/86] chore: fix conflicts Removed unused voucher_no and posting_date retrieval. --- erpnext/stock/serial_batch_bundle.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index c5a8feb3e74..4a04272a580 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -1334,8 +1334,6 @@ class SerialBatchCreation: if self.get("voucher_no"): voucher_no = self.get("voucher_no") -<<<<<<< HEAD -======= voucher_type = "" if self.get("voucher_type"): voucher_type = self.get("voucher_type") @@ -1346,13 +1344,6 @@ class SerialBatchCreation: def get_series(partial_series, digits): return f"{current_value:0{digits}d}" - posting_date = frappe.db.get_value( - voucher_type, - voucher_no, - "posting_date", - ) - ->>>>>>> 19a9497273 (perf: serial no creation) for _i in range(abs(cint(self.actual_qty))): current_value += 1 serial_no = parse_naming_series(self.serial_no_series, number_generator=get_series) From 32d3fbf1e80459e39f9ac8716a328d6738f7cfce Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Mon, 10 Nov 2025 15:19:55 +0000 Subject: [PATCH 70/86] fix: add company filter for default warehouse for sales return (cherry picked from commit 0b614007bb73c17c1df4661e910c5b432d7b1062) --- erpnext/setup/doctype/company/company.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 72d28a705ad..f736769b915 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -50,6 +50,15 @@ frappe.ui.form.on("Company", { }, }; }); + + frm.set_query("default_warehouse_for_sales_return", function () { + return { + filters: { + company: frm.doc.name, + is_group: 0, + }, + }; + }); }, company_name: function (frm) { From 87f3ba579428f70dbd523cbc17f83a275e232f77 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 11 Nov 2025 10:51:19 +0530 Subject: [PATCH 71/86] chore: fix linters issue --- erpnext/stock/serial_batch_bundle.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 4a04272a580..db9cf422b5f 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -1334,10 +1334,6 @@ class SerialBatchCreation: if self.get("voucher_no"): voucher_no = self.get("voucher_no") - voucher_type = "" - if self.get("voucher_type"): - voucher_type = self.get("voucher_type") - obj = NamingSeries(self.serial_no_series) current_value = obj.get_current_value() From 1747e83cb17e37203ecfecd0d7fa9cd58ac6eeb3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 07:18:41 +0000 Subject: [PATCH 72/86] feat: make material transfer warehouse validation optional (backport #50461) (#50462) Co-authored-by: Mihir Kandoi --- .../stock/doctype/stock_entry/stock_entry.py | 62 ++++++++++--------- .../stock_settings/stock_settings.json | 14 ++++- .../doctype/stock_settings/stock_settings.py | 1 + 3 files changed, 44 insertions(+), 33 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 63d2e80888c..76c57ae873d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -807,43 +807,45 @@ class StockEntry(StockController): and target to ensure a meaningful transfer is occurring. Raises: - frappe.ValidationError: If warehouses are same and no inventory dimensions differ + frappe.ValidationError: If warehouses are same and no inventory dimensions differ """ - from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions - inventory_dimensions = get_inventory_dimensions() - if self.purpose == "Material Transfer": - for item in self.items: - if cstr(item.s_warehouse) == cstr(item.t_warehouse): - if not inventory_dimensions: - frappe.throw( - _( - "Row #{0}: Source and Target Warehouse cannot be the same for Material Transfer" - ).format(item.idx), - title=_("Invalid Source and Target Warehouse"), - ) - else: - difference_found = False - for dimension in inventory_dimensions: - fieldname = ( - dimension.source_fieldname - if dimension.source_fieldname.startswith("to_") - else f"to_{dimension.source_fieldname}" - ) - if ( - item.get(dimension.source_fieldname) - and item.get(fieldname) - and item.get(dimension.source_fieldname) != item.get(fieldname) - ): - difference_found = True - break - if not difference_found: + if frappe.get_single_value("Stock Settings", "validate_material_transfer_warehouses"): + from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions + + inventory_dimensions = get_inventory_dimensions() + if self.purpose == "Material Transfer": + for item in self.items: + if cstr(item.s_warehouse) == cstr(item.t_warehouse): + if not inventory_dimensions: frappe.throw( _( - "Row #{0}: Source, Target Warehouse and Inventory Dimensions cannot be the exact same for Material Transfer" + "Row #{0}: Source and Target Warehouse cannot be the same for Material Transfer" ).format(item.idx), title=_("Invalid Source and Target Warehouse"), ) + else: + difference_found = False + for dimension in inventory_dimensions: + fieldname = ( + dimension.source_fieldname + if dimension.source_fieldname.startswith("to_") + else f"to_{dimension.source_fieldname}" + ) + if ( + item.get(dimension.source_fieldname) + and item.get(fieldname) + and item.get(dimension.source_fieldname) != item.get(fieldname) + ): + difference_found = True + break + if not difference_found: + frappe.throw( + _( + "Row #{0}: Source, Target Warehouse and Inventory Dimensions cannot be the exact same for Material Transfer" + ).format(item.idx), + title=_("Invalid Source and Target Warehouse"), + ) def get_matched_items(self, item_code): for row in self.items: diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 0faa600cbc7..d61c8f575cb 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -36,6 +36,7 @@ "show_barcode_field", "clean_description_html", "allow_internal_transfer_at_arms_length_price", + "validate_material_transfer_warehouses", "serial_and_batch_item_settings_tab", "section_break_7", "allow_existing_serial_no", @@ -530,6 +531,13 @@ "label": "Update Price List Based On", "mandatory_depends_on": "eval: doc.auto_insert_price_list_rate_if_missing", "options": "Rate\nPrice List Rate" + }, + { + "default": "0", + "description": "If enabled, the source and target warehouse in the Material Transfer Stock Entry must be different else an error will be thrown. If inventory dimensions are present, same source and target warehouse can be allowed but atleast any one of the inventory dimension fields must be different.", + "fieldname": "validate_material_transfer_warehouses", + "fieldtype": "Check", + "label": "Validate Material Transfer Warehouses" } ], "icon": "icon-cog", @@ -537,8 +545,8 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-10-17 18:32:35.829395", - "modified_by": "hello@aerele.in", + "modified": "2025-11-11 11:35:39.864923", + "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", "owner": "Administrator", @@ -563,4 +571,4 @@ "sort_order": "ASC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index c05b100b3fc..268d1570b08 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -67,6 +67,7 @@ class StockSettings(Document): update_price_list_based_on: DF.Literal["Rate", "Price List Rate"] use_naming_series: DF.Check use_serial_batch_fields: DF.Check + validate_material_transfer_warehouses: DF.Check valuation_method: DF.Literal["FIFO", "Moving Average", "LIFO"] # end: auto-generated types From f039bfe35a575272049534bac9aa771260691bde Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Wed, 5 Nov 2025 16:39:45 +0530 Subject: [PATCH 73/86] refactor: enqueue exchange rate revaluation per company (cherry picked from commit b10e7bf7b5ca45bfa999ef49954d1d09717358f2) --- erpnext/accounts/utils.py | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 85f2a1d5ee4..06685845b58 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1755,24 +1755,22 @@ def check_and_delete_linked_reports(report): frappe.delete_doc("Desktop Icon", icon) -def create_err_and_its_journals(companies: list | None = None) -> None: - if companies: - for company in companies: - err = frappe.new_doc("Exchange Rate Revaluation") - err.company = company.name - err.posting_date = nowdate() - err.rounding_loss_allowance = 0.0 +def create_err_and_its_journals(company: dict) -> None: + err = frappe.new_doc("Exchange Rate Revaluation") + err.company = company.name + err.posting_date = nowdate() + err.rounding_loss_allowance = 0.0 - err.fetch_and_calculate_accounts_data() - if err.accounts: - err.save().submit() - response = err.make_jv_entries() + err.fetch_and_calculate_accounts_data() + if err.accounts: + err.save().submit() + response = err.make_jv_entries() - if company.submit_err_jv: - jv = response.get("revaluation_jv", None) - jv and frappe.get_doc("Journal Entry", jv).submit() - jv = response.get("zero_balance_jv", None) - jv and frappe.get_doc("Journal Entry", jv).submit() + if company.submit_err_jv: + jv = response.get("revaluation_jv", None) + jv and frappe.get_doc("Journal Entry", jv).submit() + jv = response.get("zero_balance_jv", None) + jv and frappe.get_doc("Journal Entry", jv).submit() def _auto_create_exchange_rate_revaluation_for(frequency: str) -> None: @@ -1785,7 +1783,14 @@ def _auto_create_exchange_rate_revaluation_for(frequency: str) -> None: filters={"auto_exchange_rate_revaluation": 1, "auto_err_frequency": frequency}, fields=["name", "submit_err_jv"], ) - create_err_and_its_journals(companies) + + if companies: + for company in companies: + frappe.enqueue( + "erpnext.accounts.utils.create_err_and_its_journals", + company=company, + queue="long", + ) def auto_create_exchange_rate_revaluation_daily() -> None: From 2a2ae9a20c2269a8e8231603753475e648de75f6 Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Tue, 11 Nov 2025 13:17:21 +0530 Subject: [PATCH 74/86] fix: update uom when item changes --- erpnext/public/js/controllers/transaction.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 66c54ec6639..7b63d78c09b 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -558,6 +558,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe item.weight_per_unit = 0; item.weight_uom = ''; + item.uom = null // make UOM blank to update the existing UOM when item changes item.conversion_factor = 0; if(['Sales Invoice', 'Purchase Invoice'].includes(this.frm.doc.doctype)) { From ce7ab8df9a4d6d63f0f91ef1fbe9228577a893e0 Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Tue, 21 Oct 2025 20:15:52 +0530 Subject: [PATCH 75/86] fix: Update pick list locations quantity (cherry picked from commit bd9e240ca5e72410dfa0eed140826607cdfca50f) --- .../doctype/material_request/material_request.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 8b0accee41d..e9c7a813698 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -831,6 +831,16 @@ def raise_work_orders(material_request): @frappe.whitelist() def create_pick_list(source_name, target_doc=None): + def update_item(obj, target, source_parent): + qty = ( + flt(flt(obj.stock_qty) - flt(obj.ordered_qty)) / target.conversion_factor + if flt(obj.stock_qty) > flt(obj.ordered_qty) + else 0 + ) + target.qty = qty + target.stock_qty = qty * obj.conversion_factor + target.conversion_factor = obj.conversion_factor + doc = get_mapped_doc( "Material Request", source_name, @@ -843,6 +853,11 @@ def create_pick_list(source_name, target_doc=None): "Material Request Item": { "doctype": "Pick List Item", "field_map": {"name": "material_request_item", "stock_qty": "stock_qty"}, + "postprocess": update_item, + "condition": lambda doc: ( + flt(doc.ordered_qty, doc.precision("ordered_qty")) + < flt(doc.stock_qty, doc.precision("ordered_qty")) + ), }, }, target_doc, From 95ea9ca66bb033bf8e8b377fe961d67ce07d37bd Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Tue, 21 Oct 2025 20:16:43 +0530 Subject: [PATCH 76/86] fix: Pass stock_qty and picked_qty in transfer entry (cherry picked from commit 6db605c443205f2d22a98c21ea833835945d6a9d) --- erpnext/stock/doctype/pick_list/pick_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 38cede3df47..330d01edb3d 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -1560,8 +1560,8 @@ def update_stock_entry_items_with_no_reference(pick_list, stock_entry): def update_common_item_properties(item, location): item.item_code = location.item_code item.s_warehouse = location.warehouse - item.qty = location.picked_qty * location.conversion_factor item.transfer_qty = location.picked_qty + item.qty = location.qty item.uom = location.uom item.conversion_factor = location.conversion_factor item.stock_uom = location.stock_uom From a0d94c38c141ec75a32278c5bc4990e4db17eeab Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Sun, 9 Nov 2025 00:48:27 +0530 Subject: [PATCH 77/86] test: add test for pending qty calculation in Pick List (cherry picked from commit 3f7a60d56cd7239efb1ed7d05da82ca4df8722df) --- .../material_request/test_material_request.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index d3d512fd143..7bcab314e1a 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -12,6 +12,7 @@ from frappe.utils import flt, today from erpnext.controllers.accounts_controller import InvalidQtyError from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.material_request.material_request import ( + create_pick_list, make_in_transit_stock_entry, make_purchase_order, make_stock_entry, @@ -895,6 +896,36 @@ class TestMaterialRequest(FrappeTestCase): self.assertRaises(OverAllowanceError, mr.submit) + def test_pending_qty_in_pick_list(self): + """Test for pick list mapped doc qty from partially received Material Request Transfer""" + import json + + from erpnext.stock.doctype.pick_list.pick_list import create_stock_entry + + mr = make_material_request(material_request_type="Material Transfer") + pl = create_pick_list(mr.name) + pl.save() + pl.locations[0].qty = 5 + pl.locations[0].stock_qty = 5 + pl.submit() + + to_warehouse = create_warehouse("Test To Warehouse") + + se_data = create_stock_entry(json.dumps(pl.as_dict())) + se = frappe.get_doc(se_data) + se.items[0].t_warehouse = to_warehouse + se.save() + se.submit() + + pl.load_from_db() + self.assertEqual(pl.locations[0].picked_qty, se.items[0].qty) + + mr.load_from_db() + self.assertEqual(mr.status, "Partially Received") + + pl_for_pending = create_pick_list(mr.name) + self.assertEqual(pl_for_pending.locations[0].qty, 5) + def get_in_transit_warehouse(company): if not frappe.db.exists("Warehouse Type", "Transit"): From 44539f094469003c1663ee202c3a1618a655e6d9 Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Fri, 17 Oct 2025 15:31:21 +0530 Subject: [PATCH 78/86] fix: ensure that additional discount amount is not mapped repeatedly (cherry picked from commit feb62102d95868756108665a75f6130150a8f837) --- erpnext/controllers/accounts_controller.py | 79 ++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 6410c28ed19..3a1f4c718b0 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -39,6 +39,8 @@ from erpnext.accounts.doctype.pricing_rule.utils import ( ) from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center from erpnext.accounts.party import ( + PURCHASE_TRANSACTION_TYPES, + SALES_TRANSACTION_TYPES, get_party_account, get_party_account_currency, get_party_gle_currency, @@ -2918,6 +2920,83 @@ class AccountsController(TransactionBase): x["transaction_currency"] = self.currency x["transaction_exchange_rate"] = self.get("conversion_rate") or 1 + def after_mapping(self, source_doc): + self.set_discount_amount_after_mapping(source_doc) + + def set_discount_amount_after_mapping(self, source_doc): + """ + Ensures that Additional Discount Amount is not copied repeatedly + for multiple mappings of a single source transaction. + """ + + # source and target doctypes should both be buying / selling + for transaction_types in (PURCHASE_TRANSACTION_TYPES, SALES_TRANSACTION_TYPES): + if self.doctype in transaction_types and source_doc.doctype in transaction_types: + break + + else: + return + + # ensure both doctypes have discount_amount field + if not self.meta.get_field("discount_amount") or not source_doc.meta.get_field("discount_amount"): + return + + # ensure discount_amount is set in source doc + if not source_doc.discount_amount: + return + + # ensure additional_discount_percentage is not set in the source doc + if source_doc.get("additional_discount_percentage"): + return + + item_doctype = self.meta.get_field("items").options + item_meta = frappe.get_meta(item_doctype) + + reference_fieldname = next( + ( + row.fieldname + for row in item_meta.fields + if row.fieldtype == "Link" + and row.options == source_doc.doctype + and not row.get("is_custom_field") + ), + None, + ) + + if not reference_fieldname: + return + + doctype_table = frappe.qb.DocType(self.doctype) + item_table = frappe.qb.DocType(item_doctype) + discount_already_applied = ( + frappe.qb.from_(doctype_table) + .where(doctype_table.docstatus == 1) + .where(doctype_table.discount_amount != 0) + .where( + doctype_table.name.isin( + frappe.qb.from_(item_table) + .select(item_table.parent) + .where(item_table[reference_fieldname] == source_doc.name) + .distinct() + ) + ) + .select(Sum(doctype_table.discount_amount)) + ).run() + + if not discount_already_applied: + return + + discount_already_applied = flt(discount_already_applied[0][0], self.precision("discount_amount")) + if (source_doc.discount_amount * (discount_already_applied - source_doc.discount_amount)) >= 0: + # full discount already applied or exceeded + self.discount_amount = 0 + else: + self.discount_amount = flt( + self.discount_amount - discount_already_applied, self.precision("discount_amount") + ) + + self.calculate_taxes_and_totals() + @frappe.whitelist() def get_tax_rate(account_head): From e559fafa83359d5d2e2240ffd4c50f35368af2c4 Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Fri, 17 Oct 2025 16:12:30 +0530 Subject: [PATCH 79/86] fix: validate that discount amount cannot exceed total before discount (cherry picked from commit f4f79d99e45a4e1f785d26713bf9f00045e9571f) # Conflicts: # erpnext/controllers/taxes_and_totals.py --- erpnext/controllers/taxes_and_totals.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 5543129d323..0048ec9acab 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -7,8 +7,12 @@ import json import frappe from frappe import _, scrub from frappe.model.document import Document +<<<<<<< HEAD from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction from frappe.utils.deprecations import deprecated +======= +from frappe.utils import cint, flt, fmt_money, round_based_on_smallest_currency_fraction +>>>>>>> f4f79d99e4 (fix: validate that discount amount cannot exceed total before discount) import erpnext from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate @@ -682,6 +686,22 @@ class calculate_taxes_and_totals: self.doc.precision("discount_amount"), ) + discount_amount = self.doc.discount_amount or 0 + grand_total = self.doc.grand_total + + # validate that discount amount cannot exceed the total before discount + if grand_total * (discount_amount - grand_total) > 0: + frappe.throw( + _( + "Additional Discount Amount ({discount_amount}) cannot exceed " + "the total before such discount ({total_before_discount})" + ).format( + discount_amount=self.doc.get_formatted("discount_amount"), + total_before_discount=self.doc.get_formatted("grand_total"), + ), + title=_("Invalid Discount Amount"), + ) + def apply_discount_amount(self): if self.doc.discount_amount: if not self.doc.apply_discount_on: From 22b6760164e0cbf1121929d69e07281146d1c7f8 Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Fri, 17 Oct 2025 17:32:16 +0530 Subject: [PATCH 80/86] test: some tests to ensure correct discount mapping (cherry picked from commit 0968f435d28dd3f6aab9bd6ccbeb8210670b95bd) --- .../tests/test_accounts_controller.py | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index f2558572dc9..339607e9774 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -2258,3 +2258,141 @@ class TestAccountsController(FrappeTestCase): self.assertRaises(frappe.ValidationError, si.save) si.contact_person = customer_contact.name si.save() + + def test_discount_amount_not_mapped_repeatedly_for_sales_transactions(self): + """ + Test that additional discount amount is not copied repeatedly + when creating multiple delivery notes from a single sales order with discount_amount set + """ + from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note + from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + + # Create a sales order with discount amount + so = make_sales_order(qty=10, rate=100, do_not_submit=True) + so.apply_discount_on = "Net Total" + so.discount_amount = 100 + so.save() + so.submit() + + # Create first delivery note from sales order (partial qty) + dn1 = make_delivery_note(so.name) + dn1.items[0].qty = 5 + dn1.save() + dn1.submit() + + # First delivery note should have full discount amount + self.assertEqual(dn1.discount_amount, 100) + self.assertEqual(dn1.grand_total, 400) + + # Create second delivery note from the same sales order (remaining qty) + dn2 = make_delivery_note(so.name) + dn2.items[0].qty = 5 + dn2.save() + dn2.submit() + + # Second delivery note should have discount_amount set to 0 + # because discount was already fully applied in first delivery note + self.assertEqual(dn2.discount_amount, 0) + self.assertEqual(dn2.grand_total, 500) + + def test_discount_amount_not_mapped_repeatedly_for_purchase_transactions(self): + """ + Test that additional discount amount is not copied repeatedly + when creating multiple purchase receipts from a single purchase order with discount_amount set + """ + from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + + # Create a purchase order with discount amount + po = create_purchase_order(qty=10, rate=100, do_not_submit=True) + po.apply_discount_on = "Net Total" + po.discount_amount = 100 + po.save() + po.submit() + + # Create first purchase receipt from purchase order (partial qty) + pr1 = make_purchase_receipt(po.name) + pr1.items[0].qty = 5 + pr1.save() + pr1.submit() + + # First purchase receipt should have full discount amount + self.assertEqual(pr1.discount_amount, 100) + self.assertEqual(pr1.grand_total, 400) + + # Create second purchase receipt from the same purchase order (remaining qty) + pr2 = make_purchase_receipt(po.name) + pr2.items[0].qty = 5 + pr2.save() + pr2.submit() + + # Second purchase receipt should have discount_amount set to 0 + # because discount was already fully applied in first purchase receipt + self.assertEqual(pr2.discount_amount, 0) + self.assertEqual(pr2.grand_total, 500) + + def test_discount_amount_partial_application_in_mapped_transactions(self): + """ + Test that discount amount is partially applied when some discount + has already been used in previous mapped transactions + """ + from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice + from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + + # Create a sales order with discount amount + so = make_sales_order(qty=10, rate=100, do_not_submit=True) + so.apply_discount_on = "Net Total" + so.discount_amount = 200 + so.save() + so.submit() + + self.assertEqual(so.discount_amount, 200) + self.assertEqual(so.grand_total, 800) + + # Create first invoice with partial discount (manually set lower discount) + si1 = make_sales_invoice(so.name) + si1.items[0].qty = 5 + si1.discount_amount = 50 # Partial discount application + si1.save() + si1.submit() + + self.assertEqual(si1.discount_amount, 50) + self.assertEqual(si1.grand_total, 450) + + # Create second invoice from the same sales order + si2 = make_sales_invoice(so.name) + si2.items[0].qty = 5 + si2.save() + si2.submit() + + # Second invoice should have remaining discount (200 - 50 = 150) + self.assertEqual(si2.discount_amount, 150) + self.assertEqual(si2.grand_total, 350) + + def test_discount_amount_not_mapped_when_percentage_is_set(self): + """ + Test that discount amount is not adjusted when additional_discount_percentage + is set in the source document (as it will be recalculated based on percentage) + """ + from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note + from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + + # Create a sales order with discount percentage instead of amount + so = make_sales_order(qty=10, rate=100, do_not_submit=True) + so.apply_discount_on = "Net Total" + so.additional_discount_percentage = 10 # 10% discount + so.save() + so.submit() + + self.assertEqual(so.discount_amount, 100) # 10% of 1000 + self.assertEqual(so.grand_total, 900) + + # Create delivery note from sales order + dn = make_delivery_note(so.name) + dn.items[0].qty = 5 + dn.save() + + # Delivery note should have discount amount recalculated based on percentage + # and not affected by the repeated mapping logic + self.assertEqual(dn.additional_discount_percentage, 10) + self.assertEqual(dn.discount_amount, 50) # 10% of 500 From 9ed40cc17d1e353da351f1b73a6fd251cc31d0b0 Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Fri, 17 Oct 2025 19:21:24 +0530 Subject: [PATCH 81/86] fix: handle returns as well (cherry picked from commit 0e026b9ccd93e4e502cf55b177ee76f6edbc8440) --- erpnext/controllers/accounts_controller.py | 74 +++++++++++++------ .../tests/test_accounts_controller.py | 32 ++++++++ 2 files changed, 82 insertions(+), 24 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 3a1f4c718b0..01b10e460c1 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2950,29 +2950,46 @@ class AccountsController(TransactionBase): return item_doctype = self.meta.get_field("items").options - item_meta = frappe.get_meta(item_doctype) - - reference_fieldname = next( - ( - row.fieldname - for row in item_meta.fields - if row.fieldtype == "Link" - and row.options == source_doc.doctype - and not row.get("is_custom_field") - ), - None, - ) - - if not reference_fieldname: - return - doctype_table = frappe.qb.DocType(self.doctype) item_table = frappe.qb.DocType(item_doctype) - discount_already_applied = ( + + is_same_doctype = self.doctype == source_doc.doctype + is_return = self.get("is_return") and is_same_doctype + + if is_same_doctype and not is_return: + # should never happen + # you don't map to the same doctype without it being a return + return + + query = ( frappe.qb.from_(doctype_table) .where(doctype_table.docstatus == 1) .where(doctype_table.discount_amount != 0) - .where( + .select(Sum(doctype_table.discount_amount)) + ) + + if is_return: + query = query.where(doctype_table.is_return == 1).where( + doctype_table.return_against == source_doc.name + ) + + else: + item_meta = frappe.get_meta(item_doctype) + reference_fieldname = next( + ( + row.fieldname + for row in item_meta.fields + if row.fieldtype == "Link" + and row.options == source_doc.doctype + and not row.get("is_custom_field") + ), + None, + ) + + if not reference_fieldname: + return + + query = query.where( doctype_table.name.isin( frappe.qb.from_(item_table) .select(item_table.parent) @@ -2980,20 +2997,29 @@ class AccountsController(TransactionBase): .distinct() ) ) - .select(Sum(doctype_table.discount_amount)) - ).run() + result = query.run() + if not result: + return + + discount_already_applied = result[0][0] if not discount_already_applied: return - discount_already_applied = flt(discount_already_applied[0][0], self.precision("discount_amount")) + if is_return: + # returns have negative discount + discount_already_applied *= -1 + if (source_doc.discount_amount * (discount_already_applied - source_doc.discount_amount)) >= 0: # full discount already applied or exceeded self.discount_amount = 0 else: - self.discount_amount = flt( - self.discount_amount - discount_already_applied, self.precision("discount_amount") - ) + discount_amount = source_doc.discount_amount - discount_already_applied + if is_return: + # returns have negative discount + discount_amount *= -1 + + self.discount_amount = flt(discount_amount, self.precision("discount_amount")) self.calculate_taxes_and_totals() diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 339607e9774..899ec45c169 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -2396,3 +2396,35 @@ class TestAccountsController(FrappeTestCase): # and not affected by the repeated mapping logic self.assertEqual(dn.additional_discount_percentage, 10) self.assertEqual(dn.discount_amount, 50) # 10% of 500 + + def test_discount_amount_for_multiple_returns(self): + """ + Test that discount amount is correctly adjusted when multiple return invoices + are created against the same original invoice to prevent over-returning discount + """ + from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return + + # Create original sales invoice with discount + si = create_sales_invoice(qty=10, rate=100, do_not_submit=True) + si.apply_discount_on = "Net Total" + si.discount_amount = 100 + si.save() + si.submit() + + # Create first return - Frappe will copy full discount by default, we need to adjust it + return_si_1 = make_sales_return(si.name) + return_si_1.items[0].qty = -6 # Return 6 out of 10 items + # Manually set discount to match the proportion (60% of discount) + return_si_1.discount_amount = -60 + return_si_1.save() + return_si_1.submit() + + self.assertEqual(return_si_1.discount_amount, -60) + + # Create second return for remaining items + return_si_2 = make_sales_return(si.name) + return_si_2.items[0].qty = -4 # Return remaining 4 out of 10 items + return_si_2.save() + + # Second return should only get remaining discount (100 - 60 = 40) + self.assertEqual(return_si_2.discount_amount, -40) From b4df87e545143f8e21eea41bdc80224bdf60cb8f Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Sat, 18 Oct 2025 00:00:36 +0530 Subject: [PATCH 82/86] refactor: simplify logic (cherry picked from commit 95f604457d42e601d39108a658d36cbe8c214df6) --- erpnext/controllers/accounts_controller.py | 13 ++++--------- erpnext/controllers/taxes_and_totals.py | 5 ++++- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 01b10e460c1..f60fe003b0d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -3010,16 +3010,11 @@ class AccountsController(TransactionBase): # returns have negative discount discount_already_applied *= -1 - if (source_doc.discount_amount * (discount_already_applied - source_doc.discount_amount)) >= 0: - # full discount already applied or exceeded - self.discount_amount = 0 - else: - discount_amount = source_doc.discount_amount - discount_already_applied - if is_return: - # returns have negative discount - discount_amount *= -1 + discount_amount = max(source_doc.discount_amount - discount_already_applied, 0) + if discount_amount and is_return: + discount_amount *= -1 - self.discount_amount = flt(discount_amount, self.precision("discount_amount")) + self.discount_amount = flt(discount_amount, self.precision("discount_amount")) self.calculate_taxes_and_totals() diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 0048ec9acab..3aa2a88a17c 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -690,7 +690,10 @@ class calculate_taxes_and_totals: grand_total = self.doc.grand_total # validate that discount amount cannot exceed the total before discount - if grand_total * (discount_amount - grand_total) > 0: + if ( + (grand_total >= 0 and discount_amount > grand_total) + or (grand_total < 0 and discount_amount < grand_total) # returns + ): frappe.throw( _( "Additional Discount Amount ({discount_amount}) cannot exceed " From 22848eb4da623e8943dad5963e184fe85e4a4bb1 Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Sat, 18 Oct 2025 00:08:38 +0530 Subject: [PATCH 83/86] chore: remove unused import (cherry picked from commit 81ab15351e88352aeae9db0d9adb0db0f13bafc6) # Conflicts: # erpnext/controllers/taxes_and_totals.py --- erpnext/controllers/taxes_and_totals.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 3aa2a88a17c..50bae1378b5 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -8,11 +8,15 @@ import frappe from frappe import _, scrub from frappe.model.document import Document <<<<<<< HEAD +<<<<<<< HEAD from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction from frappe.utils.deprecations import deprecated ======= from frappe.utils import cint, flt, fmt_money, round_based_on_smallest_currency_fraction >>>>>>> f4f79d99e4 (fix: validate that discount amount cannot exceed total before discount) +======= +from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction +>>>>>>> 81ab15351e (chore: remove unused import) import erpnext from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate From 25f5fb7637f178763816a94019477bd6a38a472e Mon Sep 17 00:00:00 2001 From: ljain112 Date: Fri, 26 Sep 2025 18:52:05 +0530 Subject: [PATCH 84/86] fix: automatically append taxes if taxes_and_charges is set in Buying controller (cherry picked from commit d171dc73284592572648562d714f5558a523b268) --- erpnext/controllers/buying_controller.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 643fafb74ab..8c8692d0be3 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -13,6 +13,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.party import get_party_details from erpnext.buying.utils import update_last_purchase_rate, validate_for_items +from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.controllers.sales_and_purchase_return import get_rate_for_return from erpnext.controllers.subcontracting_controller import SubcontractingController from erpnext.stock.get_item_details import get_conversion_factor @@ -180,6 +181,12 @@ class BuyingController(SubcontractingController): self.set_missing_item_details(for_validate) + if self.meta.get_field("taxes"): + if self.get("taxes_and_charges") and not self.get("taxes") and not for_validate: + taxes = get_taxes_and_charges("Purchase Taxes and Charges Template", self.taxes_and_charges) + for tax in taxes: + self.append("taxes", tax) + def set_supplier_from_item_default(self): if self.meta.get_field("supplier") and not self.supplier: for d in self.get("items"): From e2a0d6e5f64714eaba816ef19f79295f2ad8e3d5 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 11 Nov 2025 14:00:55 +0530 Subject: [PATCH 85/86] test: add automatic tax addition for buying controller (cherry picked from commit 3d0a668c506dc1bdbff0bf30ee3aa26a8da0bbb5) --- .../test_request_for_quotation.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py index 1a8b3a8ac47..c2578c49080 100644 --- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py @@ -76,6 +76,46 @@ class TestRequestforQuotation(FrappeTestCase): self.assertEqual(sq1.get("items")[0].item_code, "_Test Item") self.assertEqual(sq1.get("items")[0].qty, 5) + def test_make_supplier_quotation_with_taxes(self): + """Test automatic tax addition when supplier quotation is created from RFQ taxes_and_charges are set""" + + # Create a Purchase Taxes and Charges Template for testing + tax_template = frappe.new_doc("Purchase Taxes and Charges Template") + tax_template.doctype = "Purchase Taxes and Charges Template" + tax_template.title = "_Test Purchase Taxes Template for RFQ" + tax_template.company = "_Test Company" + tax_template.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "description": "VAT", + "rate": 10, + }, + ) + tax_template.save() + + rfq = make_request_for_quotation() + supplier = rfq.get("suppliers")[0].supplier + + tax_rule = frappe.new_doc("Tax Rule") + tax_rule.company = "_Test Company" + tax_rule.tax_type = "Purchase" + tax_rule.supplier = supplier + tax_rule.purchase_tax_template = tax_template.name + tax_rule.save() + + sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=supplier) + + # Verify that taxes_and_charges is set from get_party_details + self.assertEqual(sq.taxes_and_charges, tax_template.name) + + # Verify that taxes are automatically added + self.assertGreaterEqual(len(sq.get("taxes")), 1) + + tax_rule.delete() + tax_template.delete() + def test_make_supplier_quotation_with_special_characters(self): frappe.delete_doc_if_exists("Supplier", "_Test Supplier '1", force=1) supplier = frappe.new_doc("Supplier") From 313e6af52853ac2123a37a018319608ab7ec2ba1 Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Tue, 11 Nov 2025 15:11:52 +0530 Subject: [PATCH 86/86] chore: resolve conflicts --- erpnext/controllers/taxes_and_totals.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 50bae1378b5..eda94351515 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -7,16 +7,8 @@ import json import frappe from frappe import _, scrub from frappe.model.document import Document -<<<<<<< HEAD -<<<<<<< HEAD from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction from frappe.utils.deprecations import deprecated -======= -from frappe.utils import cint, flt, fmt_money, round_based_on_smallest_currency_fraction ->>>>>>> f4f79d99e4 (fix: validate that discount amount cannot exceed total before discount) -======= -from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction ->>>>>>> 81ab15351e (chore: remove unused import) import erpnext from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate