diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index abb9b96020a..b6032bba1fe 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,12 @@ "fieldname": "fetch_valuation_rate_for_internal_transaction", "fieldtype": "Check", "label": "Fetch Valuation Rate for Internal Transaction" + }, + { + "default": "1", + "fieldname": "use_legacy_controller_for_pcv", + "fieldtype": "Check", + "label": "Use Legacy Controller For Period Closing Voucher" } ], "icon": "icon-cog", @@ -636,7 +643,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-07-18 13:56:47.192437", + "modified": "2025-10-20 14:06:08.870427", "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..5144e87a3a2 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -73,6 +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 + use_legacy_controller_for_pcv: DF.Check # end: auto-generated types def validate(self): 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 fba261a1484..210dbc5bbf5 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -132,7 +132,11 @@ class PeriodClosingVoucher(AccountsController): 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.make_gl_entries() + else: + ppcv = frappe.get_doc({"doctype": "Process Period Closing Voucher", "parent_pcv": self.name}) + ppcv.save().submit() def on_cancel(self): self.ignore_linked_doctypes = ( @@ -140,11 +144,29 @@ class PeriodClosingVoucher(AccountsController): "Stock Ledger Entry", "Payment Ledger Entry", "Account Closing Balance", + "Process Period Closing Voucher", ) 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, force=True, ignore_permissions=True) + def make_gl_entries(self): if frappe.db.estimate_count("GL Entry") > 100_000: frappe.enqueue( 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..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 @@ -14,6 +14,10 @@ from erpnext.accounts.utils import get_fiscal_year class TestPeriodClosingVoucher(unittest.TestCase): + def setUp(self): + super().setUp() + frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1) + 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/__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..a5b186826e6 --- /dev/null +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js @@ -0,0 +1,71 @@ +// 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) { + 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(); + } + }); + }); + } + + 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(__("PCV Paused")); + frm.reload_doc(); + } + }); + }); + } + + 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.resume_pcv_processing", + args: { + docname: frm.doc.name, + }, + }).then((r) => { + if (!r.exc) { + frappe.show_alert(__("PCV Resumed")); + frm.reload_doc(); + } + }); + }); + } + // 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, ""); + }, +}); 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..a06a16f156c --- /dev/null +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.json @@ -0,0 +1,113 @@ +{ + "actions": [], + "autoname": "format:Process-PCV-{###}", + "creation": "2025-09-25 15:44:03.534699", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "parent_pcv", + "status", + "p_l_closing_balance", + "normal_balances", + "bs_closing_balance", + "z_opening_balances", + "amended_from" + ], + "fields": [ + { + "fieldname": "parent_pcv", + "fieldtype": "Link", + "in_list_view": 1, + "label": "PCV", + "options": "Period Closing Voucher", + "reqd": 1 + }, + { + "default": "Queued", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "no_copy": 1, + "options": "Queued\nRunning\nPaused\nCompleted\nCancelled" + }, + { + "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 + }, + { + "fieldname": "p_l_closing_balance", + "fieldtype": "JSON", + "label": "P&L Closing Balance", + "no_copy": 1 + }, + { + "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, + "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-11-05 11:40:24.996403", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Process Period Closing Voucher", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "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 + } + ], + "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..c63c5652cb3 --- /dev/null +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py @@ -0,0 +1,559 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import copy +from datetime import timedelta + +import frappe +from frappe import qb +from frappe.model.document import Document +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, +) + + +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 + + from erpnext.accounts.doctype.process_period_closing_voucher_detail.process_period_closing_voucher_detail import ( + ProcessPeriodClosingVoucherDetail, + ) + + 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 + status: DF.Literal["Queued", "Running", "Paused", "Completed", "Cancelled"] + z_opening_balances: DF.Table[ProcessPeriodClosingVoucherDetail] + # end: auto-generated types + + def validate(self): + self.status = "Queued" + self.populate_processing_tables() + + 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.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( + "normal_balances", + {"processing_date": x, "status": "Queued", "report_type": "Profit and Loss"}, + ) + self.append( + "normal_balances", {"processing_date": x, "status": "Queued", "report_type": "Balance Sheet"} + ) + + def generate_opening_balances_dates(self): + self.z_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( + "z_opening_balances", + {"processing_date": x, "status": "Queued", "report_type": "Balance Sheet"}, + ) + + 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): + if frappe.db.get_value("Process Period Closing Voucher", docname, "status") in ["Queued", "Running"]: + frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running") + if normal_balances := frappe.db.get_all( + "Process Period Closing Voucher Detail", + filters={"parent": docname, "status": "Queued"}, + fields=["processing_date", "report_type", "parentfield"], + order_by="parentfield, idx, processing_date", + limit=4, + ): + if not is_scheduler_inactive(): + 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", + ) + 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, + date=x.processing_date, + report_type=x.report_type, + parentfield=x.parentfield, + ) + else: + frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed") + + +@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() + + 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() + + +@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") + 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 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 + ) + 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 + update_default_dimensions(get_dimensions(), gl_entry, dimensions) + 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 + update_default_dimensions(get_dimensions(), gl_entry, dimensions) + return gl_entry + + +@frappe.whitelist() +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", "parentfield"], + order_by="parentfield, idx, processing_date", + limit=1, + ): + if not is_scheduler_inactive(): + frappe.db.set_value( + "Process Period Closing Voucher Detail", + { + "processing_date": to_process[0].processing_date, + "parent": docname, + "report_type": to_process[0].report_type, + "parentfield": to_process[0].parentfield, + }, + "status", + "Running", + ) + 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, + 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 = ( + 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] + ) + # Ensure both normal and opening balances are processed for all dates + if total_no_of_dates == completed: + 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 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 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) + + # calculate balance + gl_entries = get_consolidated_gles(ppcv.normal_balances, "Profit and Loss") + pl_dimension_wise_acc_balance = build_dimension_wise_balance_dict(gl_entries) + + # 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) + ) + + # 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)) + + return pl_accounts_reverse_gle, closing_account_gle + + +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 + bs_dimension_wise_acc_balance = build_dimension_wise_balance_dict(gl_entries) + + # 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) + ) + return bs_dimension_wise_acc_balance + + +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 + 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) + + 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: + 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) + + frappe.db.set_value("Period Closing Voucher", pcv.name, "gle_processing_status", "Completed") + 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 + update_default_dimensions(get_dimensions(), closing_entry, dimensions) + return closing_entry + + +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_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, report_type, parentfield): + current_date_status = frappe.db.get_value( + "Process Period Closing Voucher Detail", + {"processing_date": date, "report_type": report_type, "parentfield": parentfield}, + "status", + ) + if current_date_status != "Running": + return + + pcv_name = frappe.db.get_value("Process Period Closing Voucher", docname, "parent_pcv") + company = frappe.db.get_value("Period Closing Voucher", pcv_name, "company") + + dimensions = get_dimensions() + + accounts = frappe.db.get_all( + "Account", filters={"company": company, "report_type": report_type}, pluck="name" + ) + + # 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(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]) + res = query.run(as_dict=True) + + # save results + frappe.db.set_value( + "Process Period Closing Voucher Detail", + {"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, "parentfield": parentfield}, + "status", + "Completed", + ) + + # chain call + schedule_next_date(docname) 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..e695b11bcb5 --- /dev/null +++ b/erpnext/accounts/doctype/process_period_closing_voucher/test_process_period_closing_voucher.py @@ -0,0 +1,4 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe 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..8d881ade663 --- /dev/null +++ b/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.json @@ -0,0 +1,58 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-10-01 15:58:17.544153", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "processing_date", + "report_type", + "status", + "closing_balance" + ], + "fields": [ + { + "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\nPaused\nCompleted\nCancelled" + }, + { + "fieldname": "closing_balance", + "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-20 12:03:59.106931", + "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..f3a8302ac5b --- /dev/null +++ b/erpnext/accounts/doctype/process_period_closing_voucher_detail/process_period_closing_voucher_detail.py @@ -0,0 +1,26 @@ +# 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 + + closing_balance: DF.JSON | None + parent: DF.Data + 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", "Cancelled"] + # end: auto-generated types + + pass 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)