From 983d80f7c5e33a897f039be57fccbf848eef13ad Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 27 May 2026 22:32:06 +0530 Subject: [PATCH] refactor(accounts): merge gl_entry_builder.py into base_gl_composer.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The free functions (get_gl_dict, add_gl_entry, get_voucher_subtype, etc.) live in the same module as BaseGLComposer — they are all about building GL entries, so there is no reason to split them across two files. Removes gl_entry_builder.py and updates all import references to base_gl_composer. --- erpnext/accounts/services/base_gl_composer.py | 224 +++++++++++++++++- erpnext/accounts/services/gl_entry_builder.py | 223 ----------------- erpnext/controllers/accounts_controller.py | 12 +- erpnext/controllers/stock_controller.py | 2 +- 4 files changed, 225 insertions(+), 236 deletions(-) delete mode 100644 erpnext/accounts/services/gl_entry_builder.py diff --git a/erpnext/accounts/services/base_gl_composer.py b/erpnext/accounts/services/base_gl_composer.py index 2a39c4a5243..270658aca13 100644 --- a/erpnext/accounts/services/base_gl_composer.py +++ b/erpnext/accounts/services/base_gl_composer.py @@ -1,16 +1,226 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -"""Base class for per-document GL entry composers. +"""Base class and free functions for per-document GL entry composition. -A composer assembles the list of GL entry dicts for a single voucher. Unlike -the posting sink (``general_ledger.make_gl_entries``) and the stateless -validators (``gl_validator``), composing is stateful and per-document, so it is -modelled as a class holding the document being composed. Subclasses implement -``compose`` to return the voucher-specific list of GL entries. +``BaseGLComposer`` holds the document being composed and exposes +``get_gl_dict`` / ``add_gl_entry`` as instance methods. The underlying logic +lives in the module-level free functions below (``doc`` as first argument), so +``AccountsController`` and ``StockController`` can delegate to them via thin +shims without forcing every GL-building doctype to inherit from those classes. + +Subclasses implement ``compose`` to return the voucher-specific list of GL +entries. """ -from erpnext.accounts.services.gl_entry_builder import add_gl_entry, get_gl_dict +import frappe +from frappe import _ +from frappe.utils import flt, formatdate + +import erpnext +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions +from erpnext.accounts.services.taxes import set_balance_in_account_currency +from erpnext.accounts.utils import get_account_currency, get_fiscal_years +from erpnext.utilities.regional import temporary_flag + + +def get_gl_dict(doc, args: dict, account_currency: str | None = None, item=None) -> dict: + """Build a GL entry dict populated with doc-level fields.""" + posting_date = args.get("posting_date") or doc.get("posting_date") + fiscal_years = get_fiscal_years(posting_date, company=doc.company) + if len(fiscal_years) > 1: + frappe.throw( + _("Multiple fiscal years exist for the date {0}. Please set company in Fiscal Year").format( + formatdate(posting_date) + ) + ) + else: + fiscal_year = fiscal_years[0][0] + + gl_dict = frappe._dict( + { + "company": doc.company, + "posting_date": posting_date, + "fiscal_year": fiscal_year, + "voucher_type": doc.doctype, + "voucher_no": doc.name, + "remarks": doc.get("remarks") or doc.get("remark"), + "debit": 0, + "credit": 0, + "debit_in_account_currency": 0, + "credit_in_account_currency": 0, + "is_opening": doc.get("is_opening") or "No", + "party_type": None, + "party": None, + "project": doc.get("project"), + "post_net_value": args.get("post_net_value"), + "voucher_detail_no": args.get("voucher_detail_no"), + "voucher_subtype": get_voucher_subtype(doc), + } + ) + + with temporary_flag("company", doc.company): + update_gl_dict_with_regional_fields(doc, gl_dict) + + update_gl_dict_with_app_based_fields(doc, gl_dict) + + accounting_dimensions = get_accounting_dimensions() + dimension_dict = frappe._dict() + for dimension in accounting_dimensions: + dimension_dict[dimension] = doc.get(dimension) + if item and item.get(dimension): + dimension_dict[dimension] = item.get(dimension) + + gl_dict.update(dimension_dict) + gl_dict.update(args) + + if not account_currency: + account_currency = get_account_currency(gl_dict.account) + + if gl_dict.account and doc.doctype not in [ + "Journal Entry", + "Period Closing Voucher", + "Payment Entry", + "Purchase Receipt", + "Purchase Invoice", + "Stock Entry", + ]: + validate_account_currency(doc, gl_dict.account, account_currency) + + if gl_dict.account and doc.doctype not in [ + "Journal Entry", + "Period Closing Voucher", + "Payment Entry", + ]: + set_balance_in_account_currency( + gl_dict, + account_currency, + args.get("transaction_exchange_rate") or doc.get("conversion_rate"), + doc.company_currency, + ) + + if doc.doctype not in ["Purchase Invoice", "Sales Invoice", "Journal Entry", "Payment Entry"]: + gl_dict.update( + { + "transaction_currency": doc.get("currency") or doc.company_currency, + "transaction_exchange_rate": args.get("transaction_exchange_rate") + or doc.get("conversion_rate", 1), + "debit_in_transaction_currency": get_value_in_transaction_currency( + doc, account_currency, gl_dict, "debit" + ), + "credit_in_transaction_currency": get_value_in_transaction_currency( + doc, account_currency, gl_dict, "credit" + ), + } + ) + + if not args.get("against_voucher_type") and doc.get("against_voucher_type"): + gl_dict.update({"against_voucher_type": doc.get("against_voucher_type")}) + + if not args.get("against_voucher") and doc.get("against_voucher"): + gl_dict.update({"against_voucher": doc.get("against_voucher")}) + + return gl_dict + + +def add_gl_entry( + doc, + gl_entries: list, + account: str, + cost_center: str, + debit: float, + credit: float, + remarks: str, + against_account: str, + debit_in_account_currency: float | None = None, + credit_in_account_currency: float | None = None, + account_currency: str | None = None, + project: str | None = None, + voucher_detail_no: str | None = None, + item=None, + posting_date=None, +) -> None: + """Build a GL entry via get_gl_dict and append it to gl_entries.""" + gl_entry = { + "account": account, + "cost_center": cost_center, + "debit": debit, + "credit": credit, + "against": against_account, + "remarks": remarks, + } + + if voucher_detail_no: + gl_entry["voucher_detail_no"] = voucher_detail_no + + if debit_in_account_currency: + gl_entry["debit_in_account_currency"] = debit_in_account_currency + + if credit_in_account_currency: + gl_entry["credit_in_account_currency"] = credit_in_account_currency + + if posting_date: + gl_entry["posting_date"] = posting_date + + gl_entries.append(get_gl_dict(doc, gl_entry, account_currency, item=item)) + + +def get_voucher_subtype(doc) -> str: + voucher_subtypes = { + "Journal Entry": "voucher_type", + "Payment Entry": "payment_type", + "Stock Entry": "stock_entry_type", + "Asset Capitalization": "entry_type", + } + + for method_name in frappe.get_hooks("voucher_subtypes"): + voucher_subtype = frappe.get_attr(method_name)(doc) + if voucher_subtype: + return voucher_subtype + + if doc.doctype in voucher_subtypes: + return doc.get(voucher_subtypes[doc.doctype]) + elif doc.doctype == "Purchase Receipt" and doc.is_return: + return "Purchase Return" + elif doc.doctype == "Delivery Note" and doc.is_return: + return "Sales Return" + elif doc.doctype == "Sales Invoice" and doc.is_return: + return "Credit Note" + elif doc.doctype == "Sales Invoice" and doc.is_debit_note: + return "Debit Note" + elif doc.doctype == "Purchase Invoice" and doc.is_return: + return "Debit Note" + + return doc.doctype + + +def get_value_in_transaction_currency(doc, account_currency: str, gl_dict: dict, field: str) -> float: + if account_currency == doc.get("currency"): + return gl_dict.get(field + "_in_account_currency") + return flt(gl_dict.get(field, 0) / doc.get("conversion_rate", 1)) + + +def validate_account_currency(doc, account: str, account_currency: str | None = None) -> None: + valid_currency = [doc.company_currency] + if doc.get("currency") and doc.currency != doc.company_currency: + valid_currency.append(doc.currency) + + if account_currency not in valid_currency: + frappe.throw( + _("Account {0} is invalid. Account Currency must be {1}").format( + account, (" " + _("or") + " ").join(valid_currency) + ) + ) + + +@erpnext.allow_regional +def update_gl_dict_with_regional_fields(doc, gl_dict): + pass + + +def update_gl_dict_with_app_based_fields(doc, gl_dict): + for method in frappe.get_hooks("update_gl_dict_with_app_based_fields", default=[]): + frappe.get_attr(method)(doc, gl_dict) class BaseGLComposer: diff --git a/erpnext/accounts/services/gl_entry_builder.py b/erpnext/accounts/services/gl_entry_builder.py deleted file mode 100644 index df304c020a6..00000000000 --- a/erpnext/accounts/services/gl_entry_builder.py +++ /dev/null @@ -1,223 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -"""Free functions for building GL entry dicts. - -These are the implementations behind ``AccountsController.get_gl_dict`` and -``StockController.add_gl_entry``. Extracting them as free functions (with -``doc`` as the first argument) allows ``BaseGLComposer`` to delegate to them -directly — without requiring every composing doctype to inherit from -``AccountsController``. - -``AccountsController`` and ``StockController`` keep thin shims that call these -functions so that existing code continues to work unchanged. -""" - -import frappe -from frappe import _ -from frappe.utils import flt, formatdate - -import erpnext -from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions -from erpnext.accounts.services.taxes import set_balance_in_account_currency -from erpnext.accounts.utils import get_account_currency, get_fiscal_years -from erpnext.utilities.regional import temporary_flag - - -def get_gl_dict(doc, args: dict, account_currency: str | None = None, item=None) -> dict: - """Build a GL entry dict populated with doc-level fields.""" - posting_date = args.get("posting_date") or doc.get("posting_date") - fiscal_years = get_fiscal_years(posting_date, company=doc.company) - if len(fiscal_years) > 1: - frappe.throw( - _("Multiple fiscal years exist for the date {0}. Please set company in Fiscal Year").format( - formatdate(posting_date) - ) - ) - else: - fiscal_year = fiscal_years[0][0] - - gl_dict = frappe._dict( - { - "company": doc.company, - "posting_date": posting_date, - "fiscal_year": fiscal_year, - "voucher_type": doc.doctype, - "voucher_no": doc.name, - "remarks": doc.get("remarks") or doc.get("remark"), - "debit": 0, - "credit": 0, - "debit_in_account_currency": 0, - "credit_in_account_currency": 0, - "is_opening": doc.get("is_opening") or "No", - "party_type": None, - "party": None, - "project": doc.get("project"), - "post_net_value": args.get("post_net_value"), - "voucher_detail_no": args.get("voucher_detail_no"), - "voucher_subtype": get_voucher_subtype(doc), - } - ) - - with temporary_flag("company", doc.company): - update_gl_dict_with_regional_fields(doc, gl_dict) - - update_gl_dict_with_app_based_fields(doc, gl_dict) - - accounting_dimensions = get_accounting_dimensions() - dimension_dict = frappe._dict() - for dimension in accounting_dimensions: - dimension_dict[dimension] = doc.get(dimension) - if item and item.get(dimension): - dimension_dict[dimension] = item.get(dimension) - - gl_dict.update(dimension_dict) - gl_dict.update(args) - - if not account_currency: - account_currency = get_account_currency(gl_dict.account) - - if gl_dict.account and doc.doctype not in [ - "Journal Entry", - "Period Closing Voucher", - "Payment Entry", - "Purchase Receipt", - "Purchase Invoice", - "Stock Entry", - ]: - validate_account_currency(doc, gl_dict.account, account_currency) - - if gl_dict.account and doc.doctype not in [ - "Journal Entry", - "Period Closing Voucher", - "Payment Entry", - ]: - set_balance_in_account_currency( - gl_dict, - account_currency, - args.get("transaction_exchange_rate") or doc.get("conversion_rate"), - doc.company_currency, - ) - - if doc.doctype not in ["Purchase Invoice", "Sales Invoice", "Journal Entry", "Payment Entry"]: - gl_dict.update( - { - "transaction_currency": doc.get("currency") or doc.company_currency, - "transaction_exchange_rate": args.get("transaction_exchange_rate") - or doc.get("conversion_rate", 1), - "debit_in_transaction_currency": get_value_in_transaction_currency( - doc, account_currency, gl_dict, "debit" - ), - "credit_in_transaction_currency": get_value_in_transaction_currency( - doc, account_currency, gl_dict, "credit" - ), - } - ) - - if not args.get("against_voucher_type") and doc.get("against_voucher_type"): - gl_dict.update({"against_voucher_type": doc.get("against_voucher_type")}) - - if not args.get("against_voucher") and doc.get("against_voucher"): - gl_dict.update({"against_voucher": doc.get("against_voucher")}) - - return gl_dict - - -def add_gl_entry( - doc, - gl_entries: list, - account: str, - cost_center: str, - debit: float, - credit: float, - remarks: str, - against_account: str, - debit_in_account_currency: float | None = None, - credit_in_account_currency: float | None = None, - account_currency: str | None = None, - project: str | None = None, - voucher_detail_no: str | None = None, - item=None, - posting_date=None, -) -> None: - """Build a GL entry via get_gl_dict and append it to gl_entries.""" - gl_entry = { - "account": account, - "cost_center": cost_center, - "debit": debit, - "credit": credit, - "against": against_account, - "remarks": remarks, - } - - if voucher_detail_no: - gl_entry["voucher_detail_no"] = voucher_detail_no - - if debit_in_account_currency: - gl_entry["debit_in_account_currency"] = debit_in_account_currency - - if credit_in_account_currency: - gl_entry["credit_in_account_currency"] = credit_in_account_currency - - if posting_date: - gl_entry["posting_date"] = posting_date - - gl_entries.append(get_gl_dict(doc, gl_entry, account_currency, item=item)) - - -def get_voucher_subtype(doc) -> str: - voucher_subtypes = { - "Journal Entry": "voucher_type", - "Payment Entry": "payment_type", - "Stock Entry": "stock_entry_type", - "Asset Capitalization": "entry_type", - } - - for method_name in frappe.get_hooks("voucher_subtypes"): - voucher_subtype = frappe.get_attr(method_name)(doc) - if voucher_subtype: - return voucher_subtype - - if doc.doctype in voucher_subtypes: - return doc.get(voucher_subtypes[doc.doctype]) - elif doc.doctype == "Purchase Receipt" and doc.is_return: - return "Purchase Return" - elif doc.doctype == "Delivery Note" and doc.is_return: - return "Sales Return" - elif doc.doctype == "Sales Invoice" and doc.is_return: - return "Credit Note" - elif doc.doctype == "Sales Invoice" and doc.is_debit_note: - return "Debit Note" - elif doc.doctype == "Purchase Invoice" and doc.is_return: - return "Debit Note" - - return doc.doctype - - -def get_value_in_transaction_currency(doc, account_currency: str, gl_dict: dict, field: str) -> float: - if account_currency == doc.get("currency"): - return gl_dict.get(field + "_in_account_currency") - return flt(gl_dict.get(field, 0) / doc.get("conversion_rate", 1)) - - -def validate_account_currency(doc, account: str, account_currency: str | None = None) -> None: - valid_currency = [doc.company_currency] - if doc.get("currency") and doc.currency != doc.company_currency: - valid_currency.append(doc.currency) - - if account_currency not in valid_currency: - frappe.throw( - _("Account {0} is invalid. Account Currency must be {1}").format( - account, (" " + _("or") + " ").join(valid_currency) - ) - ) - - -@erpnext.allow_regional -def update_gl_dict_with_regional_fields(doc, gl_dict): - pass - - -def update_gl_dict_with_app_based_fields(doc, gl_dict): - for method in frappe.get_hooks("update_gl_dict_with_app_based_fields", default=[]): - frappe.get_attr(method)(doc, gl_dict) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index abd708bad1a..7c200698a89 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -19,6 +19,7 @@ from frappe.utils import ( comma_and, flt, fmt_money, + formatdate, get_last_day, get_link_to_form, getdate, @@ -50,6 +51,7 @@ from erpnext.accounts.utils import ( create_gain_loss_journal, get_account_currency, get_currency_precision, + get_fiscal_years, validate_fiscal_year, ) from erpnext.accounts.utils import ( @@ -1291,17 +1293,17 @@ class AccountsController(TransactionBase): ) def get_gl_dict(self, args, account_currency=None, item=None): - from erpnext.accounts.services.gl_entry_builder import get_gl_dict + from erpnext.accounts.services.base_gl_composer import get_gl_dict return get_gl_dict(self, args, account_currency, item) def get_voucher_subtype(self): - from erpnext.accounts.services.gl_entry_builder import get_voucher_subtype + from erpnext.accounts.services.base_gl_composer import get_voucher_subtype return get_voucher_subtype(self) def get_value_in_transaction_currency(self, account_currency, gl_dict, field): - from erpnext.accounts.services.gl_entry_builder import get_value_in_transaction_currency + from erpnext.accounts.services.base_gl_composer import get_value_in_transaction_currency return get_value_in_transaction_currency(self, account_currency, gl_dict, field) @@ -1335,7 +1337,7 @@ class AccountsController(TransactionBase): ) def validate_account_currency(self, account, account_currency=None): - from erpnext.accounts.services.gl_entry_builder import validate_account_currency + from erpnext.accounts.services.base_gl_composer import validate_account_currency return validate_account_currency(self, account, account_currency) @@ -3492,7 +3494,7 @@ def validate_einvoice_fields(doc): pass -from erpnext.accounts.services.gl_entry_builder import ( +from erpnext.accounts.services.base_gl_composer import ( update_gl_dict_with_app_based_fields, update_gl_dict_with_regional_fields, ) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index b8cbce7045f..aebb30bf57c 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -1661,7 +1661,7 @@ class StockController(AccountsController): item=None, posting_date=None, ): - from erpnext.accounts.services.gl_entry_builder import add_gl_entry + from erpnext.accounts.services.base_gl_composer import add_gl_entry add_gl_entry( self,