From 779074e6a68b0fb380257e2dec6bc3e7ccc38386 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 14 Oct 2025 14:03:31 +0000 Subject: [PATCH 01/60] chore(release): Bumped to Version 15.83.0 # [15.83.0](https://github.com/frappe/erpnext/compare/v15.82.2...v15.83.0) (2025-10-14) ### Bug Fixes * add GROUP BY for dn_detail and convert SQL query to QB ([9aa9b18](https://github.com/frappe/erpnext/commit/9aa9b181e5b44eed49ccb0b1fa3428246febcea8)) * **asset movement:** clear custodian if not present ([4ec5b28](https://github.com/frappe/erpnext/commit/4ec5b28fd27a56a238d27273642581a8f0742b97)) * Batch ordering based on the method mentioned in settings ([50266d3](https://github.com/frappe/erpnext/commit/50266d3b6bb899ec9536e6909972a42e164563f4)) * batch qty for expired batches ([f4816e4](https://github.com/frappe/erpnext/commit/f4816e4960c1fd0cafb34cdac6a8c0361cf62a2b)) * consider negative qty in batch qty calculation ([4370a59](https://github.com/frappe/erpnext/commit/4370a591830fc2604e27f7118e90687a3240dee9)) * **deferred revenue:** validate service stop date ([557d53a](https://github.com/frappe/erpnext/commit/557d53a9535397abc76ed735ba0c0d4166012921)) * do reposting of first transfer entry based on item-wh combination ([e9d71e0](https://github.com/frappe/erpnext/commit/e9d71e013ae110ce49f38fdbf0edffb8d94a2a67)) * duplicate serial nos ([9854ded](https://github.com/frappe/erpnext/commit/9854dedc06e96085866e74f728a4fd733c6c253d)) * enhance sub-assembly item handling in raw material request calculations ([467fcea](https://github.com/frappe/erpnext/commit/467fcea728f0c70ca001d83acdbb45bdedc8c605)) * filter sales team to show only active individual salespersons ([38efd5c](https://github.com/frappe/erpnext/commit/38efd5cb0b2e0a689ea1ee9f63c87705cb255845)) * fixed asset register showing opening entries ([1ea6e1d](https://github.com/frappe/erpnext/commit/1ea6e1db1293726e15ce42bff185059f390236e8)) * hide sales invoice creation for fully returned delivery notes ([b426b8c](https://github.com/frappe/erpnext/commit/b426b8c07f92a06a1c2f62a0fc4776755b2cb205)) * incorrect field valuation_rate ([93df11a](https://github.com/frappe/erpnext/commit/93df11a0cf3865c94f6342dc79d88027f7e8a629)) * negative error not throw for backdated entry ([1fc21d6](https://github.com/frappe/erpnext/commit/1fc21d60c60a62a3431da82d16624c120740cd2c)) * performance issue by adding index ([e4fd49e](https://github.com/frappe/erpnext/commit/e4fd49e9910a5ee51242e79638c130f6dd226f99)) * preserve address if present ([aaf470c](https://github.com/frappe/erpnext/commit/aaf470cf5c223fd780d0e065a6d3cf478e42510f)) * prevent empty Create dropdown when In Process (backport [#49891](https://github.com/frappe/erpnext/issues/49891)) ([#50063](https://github.com/frappe/erpnext/issues/50063)) ([b67b292](https://github.com/frappe/erpnext/commit/b67b29200c1609557a0f88c273422b16ef6f23b3)) * **production plan:** filter sales orders by item ([20c2809](https://github.com/frappe/erpnext/commit/20c2809437494a72111c01e5d262ca7d50dd723a)) * reset raw materials considering not available batches ([2184a28](https://github.com/frappe/erpnext/commit/2184a28e9143ca2da17c8120427ceaafdd216233)) * Reset Raw Materials Table button not working ([81ed32f](https://github.com/frappe/erpnext/commit/81ed32ff51eff145293eca55203a2f131b8a41b5)) * resolve conflict ([dccc561](https://github.com/frappe/erpnext/commit/dccc561eec5f952b85ba24331f12630f4de25b8a)) * resolve conflict ([38e1ca1](https://github.com/frappe/erpnext/commit/38e1ca13627a5a922d9df96335df1eedbee628a0)) * revert unrelated manual modified timestamp change ([c4cba78](https://github.com/frappe/erpnext/commit/c4cba781246d40b54f07d7b3b61317b6aee656ea)) * sales return for product bundle items ([ac46b3d](https://github.com/frappe/erpnext/commit/ac46b3d1cab3a0bdf08c7e772bbbc28b7c859d73)) * sanitize projects field in tasks webform ([#50089](https://github.com/frappe/erpnext/issues/50089)) ([432201f](https://github.com/frappe/erpnext/commit/432201f634517d8afed1fa234a5c1938b1bdde4f)) * set default roles on role_profile during reinstallation ([c93fbf3](https://github.com/frappe/erpnext/commit/c93fbf3982d7ba035ab2f42db609a087edd917ac)) * skip auto-cancel of depreciation for components during asset capitalization ([6d5f2b5](https://github.com/frappe/erpnext/commit/6d5f2b50241d2f4e5af8e04821fc26436b788ab6)) * skip party validation for payroll & it's journal & GL entry submission (backport [#49638](https://github.com/frappe/erpnext/issues/49638)) ([#49826](https://github.com/frappe/erpnext/issues/49826)) ([957b47f](https://github.com/frappe/erpnext/commit/957b47f351abd792ec82132f500034ad7937ee2d)) * stock ledger adjustment entry ([8020159](https://github.com/frappe/erpnext/commit/8020159c14c0cce1d86b6773bcefbfd1dae6d5cc)) * **stock-entry:** fetch empty batch for finished item ([af3d7ef](https://github.com/frappe/erpnext/commit/af3d7ef3001bacadafa888b7851d65c803e31bf9)) * **stock-reconciliation:** include inventory dimensions in duplicate validation ([21a972a](https://github.com/frappe/erpnext/commit/21a972ad95228fa79925e42213381df7d6bc4364)) * **Supplier Quotation Comparison:** add a missing translate function (backport [#49497](https://github.com/frappe/erpnext/issues/49497)) ([#50055](https://github.com/frappe/erpnext/issues/50055)) ([b7c2405](https://github.com/frappe/erpnext/commit/b7c2405113fbc19dd560f3c26424576a651bf4bc)) * swap warehouse labels for return entry ([c5dc810](https://github.com/frappe/erpnext/commit/c5dc81064283c9e85ff279d333da4f36499baac1)) * warehouse source reference in production report ([db93e50](https://github.com/frappe/erpnext/commit/db93e50f16da1624ff8108d5db8af3626a5438b0)) ### Features * add asset name to Asset Depreciations and Balances report ([0776b30](https://github.com/frappe/erpnext/commit/0776b300e82edd4b8c05bdf4dcb41bca11bb4330)) ### Performance Improvements * add composite indexes to Advance Payment Ledger Entry table ([5652e92](https://github.com/frappe/erpnext/commit/5652e926d7fcdc9bb5e759a132d420add9f704ae)) * optimize sql query ([79a8e26](https://github.com/frappe/erpnext/commit/79a8e2656bf07e5d34b1469224a93abe8095b79f)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index cbcfb489235..c9a5e1d70fc 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from frappe.utils.user import is_website_user -__version__ = "15.82.2" +__version__ = "15.83.0" def get_default_company(user=None): From 555d5da6112d2c262caacbb5e33b55bfd55603a1 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 16 Oct 2025 02:23:29 +0530 Subject: [PATCH 02/60] fix: validation for negative batch (cherry picked from commit f9c8f27586fedeca90ec72c901bd8ba38fb2b643) (cherry picked from commit b9dd05f29264a7e9c295ed94f263353bda0b803d) --- erpnext/stock/deprecated_serial_batch.py | 3 +++ .../serial_and_batch_bundle.py | 13 ++++++++----- erpnext/stock/serial_batch_bundle.py | 16 ++++++++++------ 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index 69443e3a608..f6984c9edab 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -78,6 +78,7 @@ class DeprecatedBatchNoValuation: for ledger in entries: self.stock_value_differece[ledger.batch_no] += flt(ledger.batch_value) self.available_qty[ledger.batch_no] += flt(ledger.batch_qty) + self.total_qty[ledger.batch_no] += flt(ledger.batch_qty) @deprecated def get_sle_for_batches(self): @@ -230,6 +231,7 @@ class DeprecatedBatchNoValuation: batch_data = query.run(as_dict=True) for d in batch_data: self.available_qty[d.batch_no] += flt(d.batch_qty) + self.total_qty[d.batch_no] += flt(d.batch_qty) for d in batch_data: if self.available_qty.get(d.batch_no): @@ -330,6 +332,7 @@ class DeprecatedBatchNoValuation: batch_data = query.run(as_dict=True) for d in batch_data: self.available_qty[d.batch_no] += flt(d.batch_qty) + self.total_qty[d.batch_no] += flt(d.batch_qty) if not self.last_sle: return 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 977a41f0369..0655cd2cfd7 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 @@ -518,12 +518,15 @@ class SerialandBatchBundle(Document): else: d.incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(d.batch_no))) - available_qty = flt(sn_obj.available_qty.get(d.batch_no), d.precision("qty")) - if self.docstatus == 1: - available_qty += flt(d.qty, d.precision("qty")) + precision = d.precision("qty") + for field in ["available_qty", "total_qty"]: + value = getattr(sn_obj, field) + available_qty = flt(value.get(d.batch_no), precision) + if self.docstatus == 1: + available_qty += flt(d.qty, precision) - if not allow_negative_stock: - self.validate_negative_batch(d.batch_no, available_qty) + if not allow_negative_stock: + self.validate_negative_batch(d.batch_no, available_qty) d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index bea1cb7386e..fbd30075be3 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -3,6 +3,7 @@ from collections import defaultdict import frappe from frappe import _, bold from frappe.model.naming import make_autoname +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 from pypika import Order @@ -708,6 +709,7 @@ class BatchNoValuation(DeprecatedBatchNoValuation): for key, value in kwargs.items(): setattr(self, key, value) + self.total_qty = defaultdict(float) self.stock_queue = [] self.batch_nos = self.get_batch_nos() self.prepare_batches() @@ -729,6 +731,7 @@ class BatchNoValuation(DeprecatedBatchNoValuation): for ledger in entries: self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate) self.available_qty[ledger.batch_no] += flt(ledger.qty) + self.total_qty[ledger.batch_no] += flt(ledger.total_qty) self.calculate_avg_rate_from_deprecarated_ledgers() self.calculate_avg_rate_for_non_batchwise_valuation() @@ -762,13 +765,16 @@ class BatchNoValuation(DeprecatedBatchNoValuation): .on(parent.name == child.parent) .select( child.batch_no, - Sum(child.stock_value_difference).as_("incoming_rate"), - Sum(child.qty).as_("qty"), + Sum(Case().when(timestamp_condition, child.stock_value_difference).else_(0)).as_( + "incoming_rate" + ), + Sum(Case().when(timestamp_condition, child.qty).else_(0)).as_("qty"), + Sum(child.qty).as_("total_qty"), ) .where( - (child.batch_no.isin(self.batchwise_valuation_batches)) - & (parent.warehouse == self.sle.warehouse) + (parent.warehouse == self.sle.warehouse) & (parent.item_code == self.sle.item_code) + & (child.batch_no.isin(self.batchwise_valuation_batches)) & (parent.docstatus == 1) & (parent.is_cancelled == 0) & (parent.type_of_transaction.isin(["Inward", "Outward"])) @@ -784,8 +790,6 @@ class BatchNoValuation(DeprecatedBatchNoValuation): query = query.where(parent.voucher_no != self.sle.voucher_no) query = query.where(parent.voucher_type != "Pick List") - if timestamp_condition: - query = query.where(timestamp_condition) return query.run(as_dict=True) From 5e21c9c5c98cc4bf4a6ac107dffc0dc04ade9660 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Fri, 17 Oct 2025 09:19:51 +0000 Subject: [PATCH 03/60] chore(release): Bumped to Version 15.83.1 ## [15.83.1](https://github.com/frappe/erpnext/compare/v15.83.0...v15.83.1) (2025-10-17) ### Bug Fixes * validation for negative batch ([555d5da](https://github.com/frappe/erpnext/commit/555d5da6112d2c262caacbb5e33b55bfd55603a1)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index c9a5e1d70fc..fb73273280b 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from frappe.utils.user import is_website_user -__version__ = "15.83.0" +__version__ = "15.83.1" def get_default_company(user=None): From 97cdac10d74963c3ae32c7e146b9b7424e99d89a Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 17 Oct 2025 18:28:19 +0530 Subject: [PATCH 04/60] fix: internal transfer entry with serial/batch (cherry picked from commit 9b4e62a75875993d28991f21b32d796e5e854e38) (cherry picked from commit d67a439051b87feefbc64caa68d4059751579483) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 3 +++ erpnext/controllers/stock_controller.py | 8 ++++++++ erpnext/stock/doctype/delivery_note/delivery_note.py | 3 +++ 3 files changed, 14 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 2de3feb9a35..3dcbcbfc2ab 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2411,6 +2411,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): target.purchase_order = source.purchase_order target.po_detail = source.purchase_order_item + if (source.get("serial_no") or source.get("batch_no")) and not source.get("serial_and_batch_bundle"): + target.use_serial_batch_fields = 1 + item_field_map = { "doctype": target_doctype + " Item", "field_no_map": ["income_account", "expense_account", "cost_center", "warehouse"], diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index d7c9bb61a20..1726756cc26 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -253,6 +253,14 @@ class StockController(AccountsController): "do_not_submit": True if not via_landed_cost_voucher else False, } + if self.is_internal_transfer() and row.get("from_warehouse") and not self.is_return: + self.update_bundle_details(bundle_details, table_name, row) + bundle_details["type_of_transaction"] = "Outward" + bundle_details["warehouse"] = row.get("from_warehouse") + bundle_details["qty"] = row.get("stock_qty") or row.get("qty") + self.create_serial_batch_bundle(bundle_details, row) + continue + if row.get("qty") or row.get("consumed_qty") or row.get("stock_qty"): self.update_bundle_details(bundle_details, table_name, row, parent_details=parent_details) self.create_serial_batch_bundle(bundle_details, row) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index d2a15c2bd66..792bbf902fc 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -1310,6 +1310,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): if source.get("use_serial_batch_fields"): target.set("use_serial_batch_fields", 1) + if (source.get("serial_no") or source.get("batch_no")) and not source.get("serial_and_batch_bundle"): + target.set("use_serial_batch_fields", 1) + doclist = get_mapped_doc( doctype, source_name, From 5e8e6ef2f39f8efe3a2cabf67cc902e4b04fa40b Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Fri, 17 Oct 2025 18:35:53 +0530 Subject: [PATCH 05/60] fix(stock): remove duplicate fields (cherry picked from commit 58a1383380c1e5a1dcee18207bbbc05bc9daaed9) --- .../doctype/stock_settings/stock_settings.json | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index ec154c95136..0faa600cbc7 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -36,17 +36,6 @@ "show_barcode_field", "clean_description_html", "allow_internal_transfer_at_arms_length_price", - "quality_inspection_settings_section", - "action_if_quality_inspection_is_not_submitted", - "column_break_23", - "action_if_quality_inspection_is_rejected", - "stock_reservation_tab", - "enable_stock_reservation", - "column_break_rx3e", - "allow_partial_reservation", - "auto_reserve_stock_for_sales_order_on_purchase", - "serial_and_batch_reservation_section", - "auto_reserve_serial_and_batch", "serial_and_batch_item_settings_tab", "section_break_7", "allow_existing_serial_no", @@ -548,8 +537,8 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-05-06 02:39:24.284587", - "modified_by": "Administrator", + "modified": "2025-10-17 18:32:35.829395", + "modified_by": "hello@aerele.in", "module": "Stock", "name": "Stock Settings", "owner": "Administrator", @@ -569,8 +558,9 @@ } ], "quick_entry": 1, + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "ASC", "states": [], "track_changes": 1 -} +} \ No newline at end of file From 8b9788ca7463d1254269258ba74ec7d087b875f3 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Sat, 18 Oct 2025 04:42:12 +0000 Subject: [PATCH 06/60] chore(release): Bumped to Version 15.83.2 ## [15.83.2](https://github.com/frappe/erpnext/compare/v15.83.1...v15.83.2) (2025-10-18) ### Bug Fixes * internal transfer entry with serial/batch ([97cdac1](https://github.com/frappe/erpnext/commit/97cdac10d74963c3ae32c7e146b9b7424e99d89a)) * **stock:** remove duplicate fields ([5e8e6ef](https://github.com/frappe/erpnext/commit/5e8e6ef2f39f8efe3a2cabf67cc902e4b04fa40b)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index fb73273280b..11e1b63ba50 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from frappe.utils.user import is_website_user -__version__ = "15.83.1" +__version__ = "15.83.2" def get_default_company(user=None): From e714b82802738c66773e27bf8ec824e95fc0c9e7 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 21 Oct 2025 12:44:58 +0000 Subject: [PATCH 07/60] chore(release): Bumped to Version 15.84.0 # [15.84.0](https://github.com/frappe/erpnext/compare/v15.83.2...v15.84.0) (2025-10-21) ### Bug Fixes * added exception handling on service level agreement apply hook ([#50096](https://github.com/frappe/erpnext/issues/50096)) ([a3a6d39](https://github.com/frappe/erpnext/commit/a3a6d394369182ee94150464077176bf91ad6cd3)) * adjustment entry ([76cfa28](https://github.com/frappe/erpnext/commit/76cfa28a424059c459c0a3d9853adb392432f225)) * correct monthly sales history (backport [#50056](https://github.com/frappe/erpnext/issues/50056)) ([#50179](https://github.com/frappe/erpnext/issues/50179)) ([afc2d95](https://github.com/frappe/erpnext/commit/afc2d957363b33ee552b5730a04044da6c2f7c30)) * handle flt conversion for prev_ordered_qty ([3d3e116](https://github.com/frappe/erpnext/commit/3d3e1167974393669ec2836a09bff886cb4226fc)) * internal transfer entry with serial/batch ([d67a439](https://github.com/frappe/erpnext/commit/d67a439051b87feefbc64caa68d4059751579483)) * **point-of-sale:** render payment methods only payment component is visible ([2b0281c](https://github.com/frappe/erpnext/commit/2b0281c510ab8ea4bab29184a33e1332c3290562)) * preview stock ledger for manual serial and batch values ([c64dcf3](https://github.com/frappe/erpnext/commit/c64dcf34239cdded72b6eca3b285e569b6da4d89)) * **stock:** remove duplicate fields ([58a1383](https://github.com/frappe/erpnext/commit/58a1383380c1e5a1dcee18207bbbc05bc9daaed9)) * validation for negative batch ([b9dd05f](https://github.com/frappe/erpnext/commit/b9dd05f29264a7e9c295ed94f263353bda0b803d)) ### Features * set options for IBAN fields (backport [#49377](https://github.com/frappe/erpnext/issues/49377)) ([#49413](https://github.com/frappe/erpnext/issues/49413)) ([bd3a132](https://github.com/frappe/erpnext/commit/bd3a1328687e6cd18580caf898dffac5baa5a4f4)) ### Performance Improvements * Add index for faster queries ([#50175](https://github.com/frappe/erpnext/issues/50175)) ([0571ed0](https://github.com/frappe/erpnext/commit/0571ed07357951b58f21e1191ab310ed9193a91e)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 11e1b63ba50..6984151d62d 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from frappe.utils.user import is_website_user -__version__ = "15.83.2" +__version__ = "15.84.0" def get_default_company(user=None): From 49f66a8a51f0f3203927b3b72aee138501bee6bf Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 28 Oct 2025 13:48:43 +0000 Subject: [PATCH 08/60] chore(release): Bumped to Version 15.85.0 # [15.85.0](https://github.com/frappe/erpnext/compare/v15.84.0...v15.85.0) (2025-10-28) ### Bug Fixes * **accounts:** update payment mode account retrieval to use namespaced function ([2300660](https://github.com/frappe/erpnext/commit/23006601b282513675dfc090894f038062a47b11)) * add message for missing purchase orders in production plan ([5798409](https://github.com/frappe/erpnext/commit/5798409f695d8adadd5a5bbba971708015658250)) * **asset depreciations and balances:** showing opening entries ([81f19b9](https://github.com/frappe/erpnext/commit/81f19b950d0384abc250573540b5fa8189984a36)) * avoid group columns mutation ([158e1b2](https://github.com/frappe/erpnext/commit/158e1b28a9682a6f5f73d1783f99d6a746b7efa0)) * **Bank Transaction:** make transaction ID non-unique ([b52b04a](https://github.com/frappe/erpnext/commit/b52b04a10c25b32b0c5b5779c25213cf3338e6fe)) * fiscal year overlap validation for company-specific years ([482a796](https://github.com/frappe/erpnext/commit/482a7962129adc915fd9261ada5f6dc98c76153c)) * get valuation rate based of previous SLEs for material receipt ([e9f4a34](https://github.com/frappe/erpnext/commit/e9f4a34d8dcd775c747bb1e5df3d5bfe154e25f3)) * **gross profit:** remove customer name from columns ([a767253](https://github.com/frappe/erpnext/commit/a7672530f43521ba94609af5aaf35d33ec93e271)) * **journal-entry:** allow copy account currency when duplicating JV ([1b227b8](https://github.com/frappe/erpnext/commit/1b227b8b4fbec3918f827b22214ec3317bca7faf)) * optimized the slow query to get the batchwise available qty ([85bf936](https://github.com/frappe/erpnext/commit/85bf9366b0c819e04b47a16d067f93c1d8d6e49b)) * Pass uom field name to update existing item qty ([93b2786](https://github.com/frappe/erpnext/commit/93b27868655e5eb5f07fc7acbc6ebcb2d93ea1ed)) * provision to find and fix incorrect serial and batch bundles ([2276741](https://github.com/frappe/erpnext/commit/22767410d56602dc14077844538fbd2ca904b26d)) * recalculate amount based on allocated amount ([2a90bff](https://github.com/frappe/erpnext/commit/2a90bffb5f774bd82dd7e14f081ba0585c160bf4)) * resolve conflicts ([97147a4](https://github.com/frappe/erpnext/commit/97147a484df66894e8e29b6e319be8c083eb585e)) * sabb missed in the incorrect serial no valuation report ([8a995f2](https://github.com/frappe/erpnext/commit/8a995f28c9f943ad3ec523c43b9d2521e4fa6d44)) * set default value for as zero for additional asset cost ([ee5e4ec](https://github.com/frappe/erpnext/commit/ee5e4eccecef33660c037106842fa952d17217d6)) * set status to Draft for auto-created assets from Purchase Receipt ([d8eddbf](https://github.com/frappe/erpnext/commit/d8eddbfd03650592d7c54db99df3a501465227ad)) * stock difference value for adjustment entry ([6c0694f](https://github.com/frappe/erpnext/commit/6c0694ff1707b2115fe22c7cd19aeddb44980c75)) * **Task:** make Timesheet-dependent fields no_copy (backport [#50130](https://github.com/frappe/erpnext/issues/50130)) ([#50196](https://github.com/frappe/erpnext/issues/50196)) ([4988ff8](https://github.com/frappe/erpnext/commit/4988ff84dfce2566d8f7a3af8f3be238c71208b4)) * use correct field name ([a82fa8c](https://github.com/frappe/erpnext/commit/a82fa8c26b59ac49f6bf9c431679c70dde288501)) ### Features * Add posting date param for reverse GL entries ([1a7092d](https://github.com/frappe/erpnext/commit/1a7092d7b665aacf3ee49a6ebe30063ce0968a26)) * add project filter to Delayed Tasks Summary report ([82ca729](https://github.com/frappe/erpnext/commit/82ca729e2b5e9b2b5d0dceb9338c3b30700ea3e4)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 6984151d62d..bf09519b470 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from frappe.utils.user import is_website_user -__version__ = "15.84.0" +__version__ = "15.85.0" def get_default_company(user=None): From 8bd1e8ff74dc6f50c1161c4df865ad59c0518a3d Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Thu, 30 Oct 2025 18:16:11 +0530 Subject: [PATCH 09/60] fix(pos): order pos invoices by timestamp (cherry picked from commit 12903b11ed8fc834f301e262dc4b686f5df548c3) --- erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py index 4fa8317ff76..2a6dc4d291a 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py @@ -161,6 +161,8 @@ def get_pos_invoices(start, end, pos_profile, user): `tabPOS Invoice` where owner = %s and docstatus = 1 and pos_profile = %s and ifnull(consolidated_invoice,'') = '' + order by + timestamp """, (user, pos_profile), as_dict=1, From 4ef07f97c9696debbc8a3e6e12520299f7666530 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Thu, 30 Oct 2025 15:42:14 +0000 Subject: [PATCH 10/60] chore(release): Bumped to Version 15.85.1 ## [15.85.1](https://github.com/frappe/erpnext/compare/v15.85.0...v15.85.1) (2025-10-30) ### Bug Fixes * **pos:** order pos invoices by timestamp ([8bd1e8f](https://github.com/frappe/erpnext/commit/8bd1e8ff74dc6f50c1161c4df865ad59c0518a3d)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index bf09519b470..32d2669edfc 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from frappe.utils.user import is_website_user -__version__ = "15.85.0" +__version__ = "15.85.1" def get_default_company(user=None): From 09d12f2f9fbb54fd6435fa8f4cb7559476e73556 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 4 Nov 2025 12:32:53 +0000 Subject: [PATCH 11/60] chore(release): Bumped to Version 15.86.0 # [15.86.0](https://github.com/frappe/erpnext/compare/v15.85.1...v15.86.0) (2025-11-04) ### Bug Fixes * **accounts-receivable:** ensure report data with party account currency ([164333a](https://github.com/frappe/erpnext/commit/164333a7304ee957bf4a05d560c7498290606c56)) * **accounts-receivable:** ensure report data with party account currency (backport [#50035](https://github.com/frappe/erpnext/issues/50035)) ([#50311](https://github.com/frappe/erpnext/issues/50311)) ([ae23cdd](https://github.com/frappe/erpnext/commit/ae23cdd8e9df37329f35c3176e45fd176f836358)) * **accounts:** populate correct fields on GL Entry during discount accounting ([4076798](https://github.com/frappe/erpnext/commit/4076798707c1f1fd569fd063e7175f58ee00429f)) * added validation for default accounts on company ([d9d9230](https://github.com/frappe/erpnext/commit/d9d9230d4a79249f8d2b92f78883a0ba1af274b4)) * allow bulk edit for bill of material items ([afe42ee](https://github.com/frappe/erpnext/commit/afe42ee3e9f052b586d96ee2eeeb979a43a99871)) * create GL entries via hooks only for submitted assets ([3d78277](https://github.com/frappe/erpnext/commit/3d7827731b2c623e0af8fa9d44ec228a2e8d8dd7)) * disallow material transfer if source and target warehouse are same ([#50331](https://github.com/frappe/erpnext/issues/50331)) ([d920520](https://github.com/frappe/erpnext/commit/d9205208430efcad5fd742bb4ecc92332f78652c)), closes [#48697](https://github.com/frappe/erpnext/issues/48697) * handle None in last_valuation_rate check ([e3110b3](https://github.com/frappe/erpnext/commit/e3110b39932cf41f7569a9170f9ef6169045b0b7)) * job card timer ([053765a](https://github.com/frappe/erpnext/commit/053765a466d444ec814e40357caf7fc0705f5cc6)) * **pos:** order pos invoices by timestamp ([12903b1](https://github.com/frappe/erpnext/commit/12903b11ed8fc834f301e262dc4b686f5df548c3)) * Respect allowed zero qty in SO and PO based on Buying/Selling settings when update items ([#49673](https://github.com/frappe/erpnext/issues/49673)) ([5d4da9c](https://github.com/frappe/erpnext/commit/5d4da9c68f8c3ebd528af6099127c2c78397523b)) * set valuation rate for rejected serial/batch item ([f72d14b](https://github.com/frappe/erpnext/commit/f72d14b1ca9824100b84ab635561fdeb00cead08)) ### Features * option to exclude stand-alone returned sales invoices from the Gross Profit report ([017dc79](https://github.com/frappe/erpnext/commit/017dc792e6fc5ff109e3b64943c3c11d58b0f045)) ### Reverts * Revert "refactor: add supplier filter in buying (backport [#50013](https://github.com/frappe/erpnext/issues/50013)) ([#50107](https://github.com/frappe/erpnext/issues/50107))" ([288570a](https://github.com/frappe/erpnext/commit/288570acdea0bfb5cea54cff2e4f6d998e005b10)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 32d2669edfc..0017db645e5 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from frappe.utils.user import is_website_user -__version__ = "15.85.1" +__version__ = "15.86.0" def get_default_company(user=None): From 8cdbaacfffd96d5f6a1ca393d631d7c0fc92a486 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 25 Sep 2025 16:35:54 +0530 Subject: [PATCH 12/60] 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 797b418af4fc14f0d1c93a9484bbe447dbd7e52e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 1 Oct 2025 15:20:07 +0530 Subject: [PATCH 13/60] 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 dc1147d504038d39270789d644b09fd260f6f55e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 3 Oct 2025 11:17:29 +0530 Subject: [PATCH 14/60] 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 cd830a6b6ca7a6b3ab48010248122d4661b784c2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 3 Oct 2025 14:24:35 +0530 Subject: [PATCH 15/60] 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 86a36b3ef4acfa5dbb50f54f5d23ed6725c3c46c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 8 Oct 2025 10:23:24 +0530 Subject: [PATCH 16/60] 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 1c044759ac466b513177cd95b58f6c291e3457db Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 8 Oct 2025 11:02:52 +0530 Subject: [PATCH 17/60] 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 5992ce533e485739b162f59a57e3c58c0fdff07a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 10 Oct 2025 10:50:03 +0530 Subject: [PATCH 18/60] 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 efd90f554f8abc80505c595e7c6e7d6c98e579df Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 13 Oct 2025 13:40:40 +0530 Subject: [PATCH 19/60] 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 3b0a1bce3d348b956a2b46d60dc124621eb89649 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 14 Oct 2025 16:31:46 +0530 Subject: [PATCH 20/60] 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 0d9c711b6d670a514b293bb4b5ca150d8a764e92 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 15 Oct 2025 13:09:29 +0530 Subject: [PATCH 21/60] 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 37c1d665e3d8cf400396a8ee574d46afe9939ade Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 16 Oct 2025 11:54:17 +0530 Subject: [PATCH 22/60] 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 18c7121230e5805d3c0d273c271928f0964cb895 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 16 Oct 2025 13:13:33 +0530 Subject: [PATCH 23/60] 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 62e8d4753e6800d26437ddcad00b4851e2bcd5af Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 16 Oct 2025 15:34:08 +0530 Subject: [PATCH 24/60] 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 3a13c04a71bc49454ab2acf5c060eddc5699f756 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 16 Oct 2025 17:05:43 +0530 Subject: [PATCH 25/60] 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 f49f60b5f49ed6b963a8fef1253f84c75ce8e387 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Oct 2025 11:27:03 +0530 Subject: [PATCH 26/60] 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 8be8a73a36035dea75b21bfd235d90ca55826302 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Oct 2025 11:49:15 +0530 Subject: [PATCH 27/60] 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 ac0a837bf324236fde24e4c1428ad8d59f056d91 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Oct 2025 13:05:03 +0530 Subject: [PATCH 28/60] 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 6216547027dbcc5bd0cbc55c50569197b886b6d2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Oct 2025 13:16:56 +0530 Subject: [PATCH 29/60] 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 7c85d7f163a6705ef08cf43c9b0c19aa36414164 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Oct 2025 14:42:29 +0530 Subject: [PATCH 30/60] 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 7edbf25e6043e7c3e45bc078ecba0e65b110dfa7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Oct 2025 15:17:26 +0530 Subject: [PATCH 31/60] 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 df4347a6cb6dc99a15280943870ce9a6df4337d0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Oct 2025 15:35:32 +0530 Subject: [PATCH 32/60] 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 347a7e3b1fa0384b07adbaffb9772360b3d5e8b8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Oct 2025 15:40:53 +0530 Subject: [PATCH 33/60] 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 a624ae07f0ff02b7ce99fb79bb342f007c3169c6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Oct 2025 19:48:35 +0530 Subject: [PATCH 34/60] 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 2712310c0aa0745be196c66216169e810e95e70d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Oct 2025 19:55:09 +0530 Subject: [PATCH 35/60] 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 db5013fca13904065345f26b60ccea7553df60ef Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Oct 2025 20:27:22 +0530 Subject: [PATCH 36/60] 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 bdeee94c180c4a719f5cf4e653be51de4345a30d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 20 Oct 2025 10:59:59 +0530 Subject: [PATCH 37/60] 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 a3068c8bf666bbac0b445e1519995e17b5a7f442 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 20 Oct 2025 12:20:02 +0530 Subject: [PATCH 38/60] 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 6f58614ef247d0e15d5b27142af74bd1f8bee375 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 20 Oct 2025 12:22:45 +0530 Subject: [PATCH 39/60] 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 83e0ce4020a8edbfde1c3ad22ce9a1198e10faf4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 20 Oct 2025 12:41:15 +0530 Subject: [PATCH 40/60] 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 5db7888a8d8688d40dc796d8b4b3f4f814e99e61 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 20 Oct 2025 13:16:18 +0530 Subject: [PATCH 41/60] 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 178eded25909f51cc43151e4a8b8f342cef4aaec Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 3 Nov 2025 16:44:26 +0530 Subject: [PATCH 42/60] 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 e6d38d009f3dbf85fbe9128e8aa458667ef6478b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 5 Nov 2025 11:42:31 +0530 Subject: [PATCH 43/60] 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 a333ec28e9effa0fc8ef9f5c1148192eaf5ed7a4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 5 Nov 2025 17:46:59 +0530 Subject: [PATCH 44/60] 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 ad4fe2db9eea85fbcd66f23197b3424e29db2629 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 5 Nov 2025 12:37:51 +0000 Subject: [PATCH 45/60] chore(release): Bumped to Version 15.87.0 # [15.87.0](https://github.com/frappe/erpnext/compare/v15.86.0...v15.87.0) (2025-11-05) ### Features * process period closing voucher ([8cdbaac](https://github.com/frappe/erpnext/commit/8cdbaacfffd96d5f6a1ca393d631d7c0fc92a486)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 0017db645e5..a4e4f66a8f8 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from frappe.utils.user import is_website_user -__version__ = "15.86.0" +__version__ = "15.87.0" def get_default_company(user=None): From 7466d91e12cb8e23db9ff60ca022dfb4282dfd1f 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 46/60] fix: handle partial dn against reserved stock (cherry picked from commit 9d979e34abbc99045b0d11464db8150d11ce5172) --- .../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 88fdab99a9e8ab30160a4e9ab1d7c0b25771abe6 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 47/60] test: add test for partial dn against reserved stock (cherry picked from commit ef719fe7296d4621acae73aaa014b40109e67aee) --- .../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 2863b0fe526ffaf19011fc710a7a6adb9ec251fc Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Fri, 7 Nov 2025 16:36:23 +0000 Subject: [PATCH 48/60] chore(release): Bumped to Version 15.87.1 ## [15.87.1](https://github.com/frappe/erpnext/compare/v15.87.0...v15.87.1) (2025-11-07) ### Bug Fixes * handle partial dn against reserved stock ([7466d91](https://github.com/frappe/erpnext/commit/7466d91e12cb8e23db9ff60ca022dfb4282dfd1f)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index a4e4f66a8f8..6f7cb0dfed9 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from frappe.utils.user import is_website_user -__version__ = "15.87.0" +__version__ = "15.87.1" def get_default_company(user=None): From 9d2c456668ebe269629f6b3598628f5407235001 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 49/60] fix(stock): ignore current voucher in reserved stock validation (cherry picked from commit 0e7f9711e12923fda462099d52e441b0546dc23d) --- 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 cf66b5aa3403b0732b4ae55df8bcc7c37c093600 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 50/60] fix: Nonetype error if reserved stock is not present (cherry picked from commit b8ec3ae23aab5ad5a15a4ac61352695a3041c537) --- 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 292f71bcefb1430c263fdc9855820d7de533baa8 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Mon, 10 Nov 2025 10:47:57 +0000 Subject: [PATCH 51/60] chore(release): Bumped to Version 15.87.2 ## [15.87.2](https://github.com/frappe/erpnext/compare/v15.87.1...v15.87.2) (2025-11-10) ### Bug Fixes * Nonetype error if reserved stock is not present ([cf66b5a](https://github.com/frappe/erpnext/commit/cf66b5aa3403b0732b4ae55df8bcc7c37c093600)) * **stock:** ignore current voucher in reserved stock validation ([9d2c456](https://github.com/frappe/erpnext/commit/9d2c456668ebe269629f6b3598628f5407235001)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 6f7cb0dfed9..41f3e7b5b3e 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from frappe.utils.user import is_website_user -__version__ = "15.87.1" +__version__ = "15.87.2" def get_default_company(user=None): From e112290728b608ba6fcd66f48724ff80c2db71dd Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 11 Nov 2025 14:49:50 +0000 Subject: [PATCH 52/60] chore(release): Bumped to Version 15.88.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [15.88.0](https://github.com/frappe/erpnext/compare/v15.87.2...v15.88.0) (2025-11-11) ### Bug Fixes * add company filter for default warehouse for sales return ([32d3fbf](https://github.com/frappe/erpnext/commit/32d3fbf1e80459e39f9ac8716a328d6738f7cfce)) * add is_group filter in task for timesheet ([0f00581](https://github.com/frappe/erpnext/commit/0f00581f839f64a017d1c75408192f58d8990d92)) * add missing stock entry UOM filtering based on item master ([#50135](https://github.com/frappe/erpnext/issues/50135)) ([8f2002d](https://github.com/frappe/erpnext/commit/8f2002d419047eeb9dcb96ecb71cf8b82bf529f1)) * add validation to reject empty readings ([ac0375f](https://github.com/frappe/erpnext/commit/ac0375fc2e0e0ac3073efa6fd391b494a592b0b5)) * apply company,is_group filter for cost center ([b181686](https://github.com/frappe/erpnext/commit/b1816864deaa1076121906197a32a345f26cf9f1)) * automatically append taxes if taxes_and_charges is set in Buying controller ([25f5fb7](https://github.com/frappe/erpnext/commit/25f5fb7637f178763816a94019477bd6a38a472e)) * **buying:** fetch Cost Center from Project ([e8e26a9](https://github.com/frappe/erpnext/commit/e8e26a91bb6367057cb70e760d659065a8620a7b)) * change fieldtype from link to data for document_type in producti… (backport [#50443](https://github.com/frappe/erpnext/issues/50443)) ([#50455](https://github.com/frappe/erpnext/issues/50455)) ([a3ddc95](https://github.com/frappe/erpnext/commit/a3ddc9533a6351da6791c9504ffc14b148e6c221)) * change fieldtype from link to data for document_type in production plan summary ([9012a72](https://github.com/frappe/erpnext/commit/9012a721852e35addcda803da5b01fe3f523052d)) * check warehouse account before accessing ([79b3af6](https://github.com/frappe/erpnext/commit/79b3af6d3efcf4e21ff181f08785bbed25915116)) * ensure that additional discount amount is not mapped repeatedly ([44539f0](https://github.com/frappe/erpnext/commit/44539f094469003c1663ee202c3a1618a655e6d9)) * handle partial dn against reserved stock ([9d979e3](https://github.com/frappe/erpnext/commit/9d979e34abbc99045b0d11464db8150d11ce5172)) * handle returns as well ([9ed40cc](https://github.com/frappe/erpnext/commit/9ed40cc17d1e353da351f1b73a6fd251cc31d0b0)) * hide total row in general ledger report ([56bb88d](https://github.com/frappe/erpnext/commit/56bb88d281913efac37cd5244776c4357680cd26)) * ignore Department doctype ([32182d7](https://github.com/frappe/erpnext/commit/32182d7cc7b30c62f1c7697c5c687fc24a5b406d)) * include cost_center and project upon accounting dimension fetch ([3f490f1](https://github.com/frappe/erpnext/commit/3f490f11d52a7cdaa3dfb180cb14bb89ea6c4f4f)) * material request item quantity validation against sales order with over-receipt allowance ([f2fef54](https://github.com/frappe/erpnext/commit/f2fef54b835650a11911d4608991fb1c3ee0c316)) * **material request:** set default buying price list if not exists ([670c6dc](https://github.com/frappe/erpnext/commit/670c6dcdd7cdf017f52bd4ba595aa570999b567f)) * Nonetype error if reserved stock is not present ([b8ec3ae](https://github.com/frappe/erpnext/commit/b8ec3ae23aab5ad5a15a4ac61352695a3041c537)) * Pass stock_qty and picked_qty in transfer entry ([95ea9ca](https://github.com/frappe/erpnext/commit/95ea9ca66bb033bf8e8b377fe961d67ce07d37bd)) * removed the validation ([080e9a3](https://github.com/frappe/erpnext/commit/080e9a3d73d4a9be2e0b4777918058548ccb79ff)) * reset billing and shipping address when company changes ([73b8a29](https://github.com/frappe/erpnext/commit/73b8a294cfad149da5ebde3c2fb2b7bd058f98de)) * resolve conflict ([7d593dd](https://github.com/frappe/erpnext/commit/7d593dd3db06eb66ed9eea083092400d2f32d07b)) * set company before creating asset movement to avoid permission error ([3fad90e](https://github.com/frappe/erpnext/commit/3fad90ebb9c4872fdbdfbc46edf6ce85a9f72218)) * show only stock items in delivered items to be billed and received items to be billed reports ([3dbc90a](https://github.com/frappe/erpnext/commit/3dbc90a0b4c399638de7519fa524a29515f2fa4e)) * state_to_state_province for translation ([#50244](https://github.com/frappe/erpnext/issues/50244)) ([d4f6ca3](https://github.com/frappe/erpnext/commit/d4f6ca35648f97c532b07e8e70091ce347ab4892)) * **stock:** ignore current voucher in reserved stock validation ([0e7f971](https://github.com/frappe/erpnext/commit/0e7f9711e12923fda462099d52e441b0546dc23d)) * **Timesheet:** don't use billing_hours for costing amount ([#50394](https://github.com/frappe/erpnext/issues/50394)) ([29c976e](https://github.com/frappe/erpnext/commit/29c976e9ae9d3a7b3e455de4026df1b618cc843b)) * trends report total mismatch with group filters ([7e3f30b](https://github.com/frappe/erpnext/commit/7e3f30baad922a565ff21c6172fb5376e7ef59d3)) * Update pick list locations quantity ([ce7ab8d](https://github.com/frappe/erpnext/commit/ce7ab8df9a4d6d63f0f91ef1fbe9228577a893e0)) * update uom when item changes ([2a2ae9a](https://github.com/frappe/erpnext/commit/2a2ae9a20c2269a8e8231603753475e648de75f6)) * validate is_group for parent task ([3380dea](https://github.com/frappe/erpnext/commit/3380deab02382fb043daa19c90d84204ad560e40)) * validate that discount amount cannot exceed total before discount ([e559faf](https://github.com/frappe/erpnext/commit/e559fafa83359d5d2e2240ffd4c50f35368af2c4)) ### Features * **account settings:** add checkbox to show balances in payment entry ([90500f0](https://github.com/frappe/erpnext/commit/90500f0ffc2e3965fac96fbfdf1050dde8db1564)) * add asset name column ([c486471](https://github.com/frappe/erpnext/commit/c48647100fea5f1584b045d27d6bfe907e3894c4)) * make material transfer warehouse validation optional (backport [#50461](https://github.com/frappe/erpnext/issues/50461)) ([#50462](https://github.com/frappe/erpnext/issues/50462)) ([1747e83](https://github.com/frappe/erpnext/commit/1747e83cb17e37203ecfecd0d7fa9cd58ac6eeb3)) * process period closing voucher ([c8e3da0](https://github.com/frappe/erpnext/commit/c8e3da0a713efda19a2a3b457fb487d08cdd06db)) ### Performance Improvements * serial no creation ([6ba2491](https://github.com/frappe/erpnext/commit/6ba24912c32420786165f496c98b14684b54c919)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 41f3e7b5b3e..efaa7009e60 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from frappe.utils.user import is_website_user -__version__ = "15.87.2" +__version__ = "15.88.0" def get_default_company(user=None): From eab6d69ec9769175d459f3304ae17afe417db794 Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Wed, 12 Nov 2025 16:37:52 +0530 Subject: [PATCH 53/60] fix: handle NoneType object error for product bundle (cherry picked from commit 2b7abfb34b6a3446d9efb990bc88fc59b59e323f) --- .../serial_and_batch_bundle/serial_and_batch_bundle.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 8d216171641..d9071c406a2 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 @@ -1390,13 +1390,15 @@ class SerialandBatchBundle(Document): so_name, so_detail_no = frappe.db.get_value( "Delivery Note Item", self.voucher_detail_no, ["against_sales_order", "so_detail"] - ) + ) or [None, None] if so_name and so_detail_no: sre_names = get_sre_against_so_for_dn(so_name, so_detail_no) return sre_names + return None + @frappe.whitelist() def download_blank_csv_template(content): From 70b8b3bb9e6f744634ab1bd96de4823af11e40be Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 12 Nov 2025 18:26:04 +0000 Subject: [PATCH 54/60] chore(release): Bumped to Version 15.88.1 ## [15.88.1](https://github.com/frappe/erpnext/compare/v15.88.0...v15.88.1) (2025-11-12) ### Bug Fixes * handle NoneType object error for product bundle ([eab6d69](https://github.com/frappe/erpnext/commit/eab6d69ec9769175d459f3304ae17afe417db794)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index efaa7009e60..a5783691ec3 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from frappe.utils.user import is_website_user -__version__ = "15.88.0" +__version__ = "15.88.1" def get_default_company(user=None): From d8dab986fa7bdda88df74906df61df4c81e2c8b0 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 19 Nov 2025 02:58:17 +0000 Subject: [PATCH 55/60] chore(release): Bumped to Version 15.89.0 # [15.89.0](https://github.com/frappe/erpnext/compare/v15.88.1...v15.89.0) (2025-11-19) ### Bug Fixes * add cancelled option in status field ([623a0a9](https://github.com/frappe/erpnext/commit/623a0a932ec88d613256024b5fdade1a261df86d)) * add condition for allow negative stock in pos (backport [#50369](https://github.com/frappe/erpnext/issues/50369)) ([#50600](https://github.com/frappe/erpnext/issues/50600)) ([2d6640a](https://github.com/frappe/erpnext/commit/2d6640ac61fc8fdf51649b54f9b55dbce879b2eb)) * add doctype parameter to lead details for correct company details ([f0eac47](https://github.com/frappe/erpnext/commit/f0eac4703726e00f151231b9694f40a4b8048520)) * **asset repair:** validate pi status ([2db91ee](https://github.com/frappe/erpnext/commit/2db91ee67efe95f97528277636245d124eb275cc)) * back calcalute total amount from rate and tax_amount in tax withholding details report ([5728299](https://github.com/frappe/erpnext/commit/57282999ad70b543aefb0e372b85675eb4b91bfb)) * construct batch_nos and serial_nos to avoid NoneType error ([0a0177c](https://github.com/frappe/erpnext/commit/0a0177cb9e1af611c0d596eca97de38afa8d595c)) * correct profit after tax calculation by reducing expenses from income ([627b34a](https://github.com/frappe/erpnext/commit/627b34a120f8086d33967b5062bfa1b9dd85dbe5)) * current qty in stock reco ([b4b8459](https://github.com/frappe/erpnext/commit/b4b8459f2c8942ae3a1cfdaccfeb46c3fbbb3c6e)) * enable allow_negative_stock settings ([2a5c9b4](https://github.com/frappe/erpnext/commit/2a5c9b469c61995a6850dee32f5572541dc87223)) * **financial reports:** set fiscal year associated with the default company ([ac40b59](https://github.com/frappe/erpnext/commit/ac40b596651fa1e206a7f2a481d7af7dbdc1add8)) * first and last name in supplier quick entry (backport [#50510](https://github.com/frappe/erpnext/issues/50510)) ([#50514](https://github.com/frappe/erpnext/issues/50514)) ([3b636d5](https://github.com/frappe/erpnext/commit/3b636d5db78feada2af765cbb97065dc16824e86)) * **general_ledger:** add translation for accounting dimension ([799119a](https://github.com/frappe/erpnext/commit/799119ad3ee843e911cd1aa0366b252855984f2a)) * handle NoneType object error for product bundle ([2b7abfb](https://github.com/frappe/erpnext/commit/2b7abfb34b6a3446d9efb990bc88fc59b59e323f)) * improve precision in tax amount calculations in tax withholding details report ([c150e57](https://github.com/frappe/erpnext/commit/c150e5795e9fbcf4132ac1c64f9d11c9c7a0a0c2)) * on changes of paid from/to account fetch company bank account ([3d8a344](https://github.com/frappe/erpnext/commit/3d8a344173131e5265b482a7df51115d4cab377e)) * **period closing voucher:** add title to error log ([#50498](https://github.com/frappe/erpnext/issues/50498)) ([33962ac](https://github.com/frappe/erpnext/commit/33962ac995381cf9da2b3b62946133489820d0b5)) * prevent pos opening entry creation for disabled pos profile ([68747b5](https://github.com/frappe/erpnext/commit/68747b5818e10cbe2a92df2b1d6f0d9e236e76e7)) * **stock-entry:** prevent default warehouse from overriding parent warehouse ([a5ec0e4](https://github.com/frappe/erpnext/commit/a5ec0e4f5079a2a9919ee245124f1f53ec20bcaf)) * unintended backported depends_on expression ([#50529](https://github.com/frappe/erpnext/issues/50529)) ([81a1628](https://github.com/frappe/erpnext/commit/81a16286a144bd45d2f1c1fcb78d57c4c9c07869)) * use dynamic account type to get average ratio balance ([a2c82b4](https://github.com/frappe/erpnext/commit/a2c82b4dc3c09da9923e92c7d62e462b262b24d7)) ### Features * Add first and last name fields to quick entry customer creation (backport [#46281](https://github.com/frappe/erpnext/issues/46281)) ([#50522](https://github.com/frappe/erpnext/issues/50522)) ([8c98f16](https://github.com/frappe/erpnext/commit/8c98f1692a4c36a99df90cc375ed96736716e7f4)) * **Company:** allow setting default sales contact, fetch into sales transaction (backport [#50159](https://github.com/frappe/erpnext/issues/50159)) ([#50599](https://github.com/frappe/erpnext/issues/50599)) ([f8294f1](https://github.com/frappe/erpnext/commit/f8294f17543283faf248ba878732d607a60f3f34)) * **Item Price:** validate UOM ([376da8d](https://github.com/frappe/erpnext/commit/376da8df0a20971caf1800d5d6bbdeece88af2cd)) * **pos:** prevent disabling POS Profile when open POS sessions exist ([87e8305](https://github.com/frappe/erpnext/commit/87e8305753d5c26371dfa6b0ad73891c3973e22d)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index a5783691ec3..d8f5767733f 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from frappe.utils.user import is_website_user -__version__ = "15.88.1" +__version__ = "15.89.0" def get_default_company(user=None): From 64950d39b528a6f5ce756d290e21bcd3a512e8ea Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Wed, 19 Nov 2025 01:23:26 +0530 Subject: [PATCH 56/60] fix: ignore reserved batches from total available batches (cherry picked from commit 673b893942ca789d7ea554120b75c2f444b28780) --- .../serial_and_batch_bundle/serial_and_batch_bundle.py | 6 +++++- erpnext/stock/stock_ledger.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index d9071c406a2..690e3c81c92 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 @@ -2297,7 +2297,11 @@ def get_auto_batch_nos(kwargs): stock_ledgers_batches = get_stock_ledgers_batches(kwargs) pos_invoice_batches = get_reserved_batches_for_pos(kwargs) - sre_reserved_batches = get_reserved_batches_for_sre(kwargs) + + sre_reserved_batches = frappe._dict() + if not kwargs.ignore_reserved_stock: + sre_reserved_batches = get_reserved_batches_for_sre(kwargs) + picked_batches = frappe._dict() if kwargs.get("is_pick_list"): picked_batches = get_picked_batches(kwargs) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index f4956e1b600..8b61ca56983 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -2313,6 +2313,7 @@ def validate_reserved_batch_nos(kwargs): "posting_date": kwargs.posting_date, "posting_time": kwargs.posting_time, "ignore_voucher_nos": kwargs.ignore_voucher_nos, + "ignore_reserved_stock": True, } ) ) From 15c41178d093b16c3369400356e32d6e493ee2ab Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Wed, 19 Nov 2025 01:24:07 +0530 Subject: [PATCH 57/60] test: add unit test for reserved stock validation (cherry picked from commit 55f2f1c515133a1fdf2152bd44e88f26c965267f) --- .../test_stock_reservation_entry.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py index ddbf0b2dc25..942c7f482ae 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py @@ -678,6 +678,36 @@ class TestStockReservationEntry(FrappeTestCase): # Test - 1: ValidationError should be thrown as the inwarded stock is reserved. self.assertRaises(frappe.ValidationError, se.cancel) + @change_settings("Stock Settings", {"allow_negative_stock": 0, "enable_stock_reservation": 1}) + def test_reserved_stock_validation_for_batch_item(self): + item_properties = { + "is_stock_item": 1, + "valuation_rate": 100, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "SRBV-.#####.", + } + sr_item = make_item(item_code="Test Reserve Item", properties=item_properties) + # inward 100 qty of stock + create_material_receipt(items={sr_item.name: sr_item}, warehouse=self.warehouse, qty=100) + + # reserve 80 qty from sales order + so = make_sales_order(item_code=sr_item.name, warehouse=self.warehouse, qty=80) + so.create_stock_reservation_entries() + + # create a material issue entry including the reserved qty 10 + se = make_stock_entry( + item_code=sr_item.name, + qty=30, + from_warehouse=self.warehouse, + rate=100, + purpose="Material Issue", + do_not_submit=True, + ) + + # validation for reserved stock should be thrown + self.assertRaises(frappe.ValidationError, se.submit) + def tearDown(self) -> None: cancel_all_stock_reservation_entries() return super().tearDown() From b7e4fb9d83786eb2dfb798e05a7b73af7cbfdf8c Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Fri, 21 Nov 2025 10:36:37 +0000 Subject: [PATCH 58/60] chore(release): Bumped to Version 15.89.1 ## [15.89.1](https://github.com/frappe/erpnext/compare/v15.89.0...v15.89.1) (2025-11-21) ### Bug Fixes * ignore reserved batches from total available batches ([64950d3](https://github.com/frappe/erpnext/commit/64950d39b528a6f5ce756d290e21bcd3a512e8ea)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index d8f5767733f..588340710b2 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from frappe.utils.user import is_website_user -__version__ = "15.89.0" +__version__ = "15.89.1" def get_default_company(user=None): From c082edabf4c6a41557df7673ef5aa3e69433895a Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Tue, 11 Nov 2025 10:26:31 +0000 Subject: [PATCH 59/60] fix: use current_tax_amount value for base_total_taxes_and_charges (cherry picked from commit 5a3fcbedb55ca46376657f8b62e3036e2390c1a4) (cherry picked from commit 7ed3c6d18a51e8151d571f0168447b5e594f41a2) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 4196b81c9ba..bfa28630d6f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1800,7 +1800,7 @@ class PaymentEntry(AccountsController): else: self.total_taxes_and_charges += current_tax_amount - self.base_total_taxes_and_charges += tax.base_tax_amount + self.base_total_taxes_and_charges += current_tax_amount if self.get("taxes"): self.paid_amount_after_tax = self.get("taxes")[-1].base_total From 38c1867ade714d8249eea8658897fb4e40c96834 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Fri, 21 Nov 2025 17:34:52 +0000 Subject: [PATCH 60/60] chore(release): Bumped to Version 15.89.2 ## [15.89.2](https://github.com/frappe/erpnext/compare/v15.89.1...v15.89.2) (2025-11-21) ### Bug Fixes * use current_tax_amount value for base_total_taxes_and_charges ([c082eda](https://github.com/frappe/erpnext/commit/c082edabf4c6a41557df7673ef5aa3e69433895a)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 588340710b2..ec16de1a135 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import inspect import frappe from frappe.utils.user import is_website_user -__version__ = "15.89.1" +__version__ = "15.89.2" def get_default_company(user=None):