refactor(accounts): merge gl_entry_builder.py into base_gl_composer.py

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.
This commit is contained in:
Nabin Hait
2026-05-27 22:32:06 +05:30
parent cba6a31497
commit 983d80f7c5
4 changed files with 225 additions and 236 deletions

View File

@@ -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:

View File

@@ -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)

View File

@@ -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,
)

View File

@@ -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,