diff --git a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.json b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.json index a06a16f156c..0a30b6564b5 100644 --- a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.json +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_bulk_edit": 1, "autoname": "format:Process-PCV-{###}", "creation": "2025-09-25 15:44:03.534699", "doctype": "DocType", @@ -7,11 +8,13 @@ "field_order": [ "parent_pcv", "status", + "amended_from", + "section_normal_balances", "p_l_closing_balance", - "normal_balances", "bs_closing_balance", - "z_opening_balances", - "amended_from" + "normal_balances", + "section_opening_balances", + "z_opening_balances" ], "fields": [ { @@ -64,17 +67,27 @@ "fieldname": "bs_closing_balance", "fieldtype": "JSON", "label": "Balance Sheet Closing Balance" + }, + { + "fieldname": "section_normal_balances", + "fieldtype": "Tab Break", + "label": "Normal Balances" + }, + { + "fieldname": "section_opening_balances", + "fieldtype": "Tab Break", + "label": "Opening Balances" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-11-05 11:40:24.996403", + "modified": "2026-06-01 12:16:37.374412", "modified_by": "Administrator", "module": "Accounts", "name": "Process Period Closing Voucher", - "naming_rule": "Expression", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { 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 24b9fabd998..2d8ad237c64 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,8 +36,8 @@ class ProcessPeriodClosingVoucher(Document): parent_pcv: DF.Link status: DF.Literal["Queued", "Running", "Paused", "Completed", "Cancelled"] z_opening_balances: DF.Table[ProcessPeriodClosingVoucherDetail] - # end: auto-generated types + def on_discard(self): self.db_set("status", "Cancelled") @@ -562,6 +562,9 @@ def process_individual_date(docname: str, date, report_type, parentfield): if parentfield == "z_opening_balances": query = query.where(gle.is_opening.eq("Yes")) + else: + # Keep balances aligned with legacy PCV logic (non-opening transactions only) + query = query.where(gle.is_opening.eq("No")) query = query.groupby(gle.account) for dim in dimensions: diff --git a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher_list.js b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher_list.js new file mode 100644 index 00000000000..4b117b8fbcf --- /dev/null +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher_list.js @@ -0,0 +1,17 @@ +// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// License: GNU General Public License v3. See license.txt + +// render +frappe.listview_settings["Process Period Closing Voucher"] = { + add_fields: ["status"], + get_indicator: function (doc) { + const status_colors = { + Queued: "blue", + Running: "orange", + Paused: "gray", + Completed: "green", + Cancelled: "red", + }; + return [__(doc.status), status_colors[doc.status], "status,=," + doc.status]; + }, +}; 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 e695b11bcb5..f34c1dbedfe 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 @@ -1,4 +1,173 @@ # Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe +import frappe +from frappe.utils import today + +from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry +from erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher import ( + process_individual_date, +) +from erpnext.accounts.utils import get_fiscal_year +from erpnext.tests.utils import ERPNextTestSuite + + +class TestProcessPeriodClosingVoucher(ERPNextTestSuite): + def setUp(self): + frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 0) + self.company = "_Test Company" + + def make_period_closing_voucher(self, posting_date, submit=True): + fy = get_fiscal_year(posting_date, company="_Test Company") + pcv = frappe.get_doc( + { + "doctype": "Period Closing Voucher", + "transaction_date": posting_date or today(), + "period_start_date": fy[1], + "period_end_date": fy[2], + "company": self.company, + "fiscal_year": fy[0], + "closing_account_head": "Retained Earnings - _TC", + "remarks": "closing", + } + ) + pcv.insert() + if submit: + pcv.submit() + + return pcv + + def make_process_pcv(self): + self.pcv = self.make_period_closing_voucher(posting_date=today(), submit=False) + ppcv = frappe.get_doc( + { + "doctype": "Process Period Closing Voucher", + "parent_pcv": self.pcv.name, + } + ) + ppcv.save() + return ppcv + + def set_processing_date_status(self, date, ppcv, rpt_type, parentfield, status): + frappe.db.set_value( + "Process Period Closing Voucher Detail", + {"processing_date": date, "parent": ppcv, "report_type": rpt_type, "parentfield": parentfield}, + "status", + status, + ) + + def get_processing_date_closing_balance(self, date, ppcv, rpt_type, parentfield): + return frappe.db.get_value( + "Process Period Closing Voucher Detail", + {"processing_date": date, "parent": ppcv, "report_type": rpt_type, "parentfield": parentfield}, + "closing_balance", + ) + + def test_opening_balance_double_counting(self): + ppcv = self.make_process_pcv() + self.assertEqual(self.pcv.is_first_period_closing_voucher(), True) + opening_jv = make_journal_entry( + posting_date=today(), + amount=10, + account1="Cash - _TC", + account2="Debtors - _TC", + company=self.company, + save=False, + ) + opening_jv.accounts[1].party_type = "Customer" + opening_jv.accounts[1].party = "_Test Customer" + opening_jv.is_opening = "Yes" + opening_jv.save() + opening_jv.submit() + + jv = make_journal_entry( + posting_date=today(), + amount=120, + account1="Debtors - _TC", + account2="Sales - _TC", + company=self.company, + save=False, + ) + jv.accounts[0].party_type = "Customer" + jv.accounts[0].party = "_Test Customer" + jv.save() + jv.submit() + + # P&L balance + parentfield = "normal_balances" + rpt_type = "Profit and Loss" + # status has to be set to 'Running' for logic to run + self.set_processing_date_status(today(), ppcv.name, rpt_type, parentfield, "Running") + process_individual_date(ppcv.name, today(), rpt_type, parentfield) + bal = frappe.parse_json( + self.get_processing_date_closing_balance(today(), ppcv.name, rpt_type, parentfield) + ) + self.assertEqual(len(bal), 1) + expected_pl = { + "account": "Sales - _TC", + "cost_center": "_Test Cost Center - _TC", + "debit": 0.0, + "credit": 120.0, + "debit_in_account_currency": 0.0, + "credit_in_account_currency": 120.0, + } + for k in expected_pl.keys(): + with self.subTest(k): + self.assertEqual(expected_pl[k], bal[0][k]) + + # Balance sheet balance + rpt_type = "Balance Sheet" + self.set_processing_date_status(today(), ppcv.name, rpt_type, parentfield, "Running") + process_individual_date(ppcv.name, today(), rpt_type, parentfield) + bal = frappe.parse_json( + self.get_processing_date_closing_balance(today(), ppcv.name, rpt_type, parentfield) + ) + self.assertEqual(len(bal), 1) + expected_bs = { + "account": "Debtors - _TC", + "cost_center": "_Test Cost Center - _TC", + "debit": 120.0, + "credit": 0.0, + "debit_in_account_currency": 120.0, + "credit_in_account_currency": 0.0, + } + for k in expected_bs.keys(): + with self.subTest(k): + self.assertEqual(expected_bs[k], bal[0][k]) + + # Opening balance + parentfield = "z_opening_balances" + rpt_type = "Balance Sheet" + self.set_processing_date_status(today(), ppcv.name, rpt_type, parentfield, "Running") + process_individual_date(ppcv.name, today(), rpt_type, parentfield) + bal = frappe.parse_json( + self.get_processing_date_closing_balance(today(), ppcv.name, rpt_type, parentfield) + ) + self.assertEqual(len(bal), 2) + opening_cash = next(x for x in bal if x["account"] == "Cash - _TC") + expected_opening_cash = { + "account": "Cash - _TC", + "cost_center": "_Test Cost Center - _TC", + "debit": 10.0, + "credit": 0.0, + "debit_in_account_currency": 10.0, + "credit_in_account_currency": 0.0, + "account_currency": "INR", + } + for k in expected_opening_cash.keys(): + with self.subTest(k): + self.assertEqual(expected_opening_cash[k], opening_cash[k]) + + opening_debtors = next(x for x in bal if x["account"] == "Debtors - _TC") + expected_opening_debtors = { + "account": "Debtors - _TC", + "cost_center": "_Test Cost Center - _TC", + "debit": 0.0, + "credit": 10.0, + "debit_in_account_currency": 0.0, + "credit_in_account_currency": 10.0, + "account_currency": "INR", + } + for k in expected_opening_debtors.keys(): + with self.subTest(k): + self.assertEqual(expected_opening_debtors[k], opening_debtors[k])