From b68daea365d01e158c37ee917e8dcbc67b7ba472 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 27 May 2026 14:47:19 +0530 Subject: [PATCH] refactor: introduce BaseStockGLComposer, slim StockController.get_gl_entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the StockController.get_gl_entries body into erpnext/stock/services/base_stock_gl_composer.py → BaseStockGLComposer(BaseGLComposer). compose(inventory_account_map, default_expense_account, default_cost_center) contains all warehouse↔expense-account GL pair building and the internal-transfer rounding-diff block; all helpers (get_inventory_account_dict, get_stock_ledger_details, etc.) remain on self.doc and are called via doc.. StockController.get_gl_entries becomes a 3-line shim. Delivery Note, Stock Entry, and Stock Reconciliation continue to work unchanged — DN inherits the shim directly; SE and SR override and call super(), which now delegates to the composer. Verified: 22 GL snapshots byte-identical on test-erpnext-v17. --- erpnext/controllers/stock_controller.py | 137 +--------------- erpnext/stock/services/__init__.py | 0 .../stock/services/base_stock_gl_composer.py | 154 ++++++++++++++++++ 3 files changed, 157 insertions(+), 134 deletions(-) create mode 100644 erpnext/stock/services/__init__.py create mode 100644 erpnext/stock/services/base_stock_gl_composer.py diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 9fb9dfe58ab..cf8f27560a5 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -13,7 +13,6 @@ import erpnext from erpnext.accounts.general_ledger import ( make_gl_entries, make_reverse_gl_entries, - process_gl_map, ) from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_fiscal_year from erpnext.controllers.accounts_controller import AccountsController @@ -691,140 +690,10 @@ class StockController(AccountsController): def get_gl_entries( self, inventory_account_map=None, default_expense_account=None, default_cost_center=None ): - if not inventory_account_map: - inventory_account_map = self.get_inventory_account_map() + from erpnext.stock.services.base_stock_gl_composer import BaseStockGLComposer - sle_map = self.get_stock_ledger_details() - voucher_details = self.get_voucher_details(default_expense_account, default_cost_center, sle_map) - - gl_list = [] - warehouse_with_no_account = [] - precision = self.get_debit_field_precision() - for item_row in voucher_details: - sle_list = sle_map.get(item_row.name) - sle_rounding_diff = 0.0 - if sle_list: - for sle in sle_list: - _inv_dict = self.get_inventory_account_dict(sle, inventory_account_map) - - if _inv_dict.get("account"): - # from warehouse account - - sle_rounding_diff += flt(sle.stock_value_difference) - - self.check_expense_account(item_row) - - # expense account/ target_warehouse / source_warehouse - if item_row.get("target_warehouse"): - _target_wh_inv_dict = self.get_inventory_account_dict( - item_row, inventory_account_map, warehouse_field="target_warehouse" - ) - expense_account = _target_wh_inv_dict["account"] - else: - expense_account = item_row.expense_account - - gl_list.append( - self.get_gl_dict( - { - "account": _inv_dict["account"], - "against": expense_account, - "cost_center": item_row.cost_center, - "project": sle.get("project") or item_row.project or self.get("project"), - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "debit": flt(sle.stock_value_difference, precision), - "is_opening": item_row.get("is_opening") - or self.get("is_opening") - or "No", - }, - _inv_dict["account_currency"], - item=item_row, - ) - ) - - gl_list.append( - self.get_gl_dict( - { - "account": expense_account, - "against": _inv_dict["account"], - "cost_center": item_row.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "debit": -1 * flt(sle.stock_value_difference, precision), - "project": sle.get("project") - or item_row.get("project") - or self.get("project"), - "is_opening": item_row.get("is_opening") - or self.get("is_opening") - or "No", - }, - item=item_row, - ) - ) - elif sle.warehouse not in warehouse_with_no_account: - warehouse_with_no_account.append(sle.warehouse) - - if abs(sle_rounding_diff) > (1.0 / (10**precision)) and self.is_internal_transfer(): - warehouse_asset_account = "" - if self.get("is_internal_customer"): - _inv_dict = self.get_inventory_account_dict( - item_row, inventory_account_map, warehouse_field="target_warehouse" - ) - - warehouse_asset_account = _inv_dict.get("account") if _inv_dict else None - elif self.get("is_internal_supplier"): - _inv_dict = self.get_inventory_account_dict(item_row, inventory_account_map) - - warehouse_asset_account = _inv_dict.get("account") if _inv_dict else None - - expense_account = frappe.get_cached_value("Company", self.company, "default_expense_account") - if not expense_account: - frappe.throw( - _( - "Please set default cost of goods sold account in company {0} for booking rounding gain and loss during stock transfer" - ).format(frappe.bold(self.company)) - ) - - gl_list.append( - self.get_gl_dict( - { - "account": expense_account, - "against": warehouse_asset_account, - "cost_center": item_row.cost_center, - "project": item_row.project or self.get("project"), - "remarks": _("Rounding gain/loss Entry for Stock Transfer"), - "debit": sle_rounding_diff, - "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No", - }, - _inv_dict["account_currency"], - item=item_row, - ) - ) - - gl_list.append( - self.get_gl_dict( - { - "account": warehouse_asset_account, - "against": expense_account, - "cost_center": item_row.cost_center, - "remarks": _("Rounding gain/loss Entry for Stock Transfer"), - "credit": sle_rounding_diff, - "project": item_row.get("project") or self.get("project"), - "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No", - }, - item=item_row, - ) - ) - - if warehouse_with_no_account: - for wh in warehouse_with_no_account: - if frappe.get_cached_value("Warehouse", wh, "company"): - frappe.throw( - _( - "Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}." - ).format(wh, self.company) - ) - - return process_gl_map( - gl_list, precision=precision, from_repost=frappe.flags.through_repost_item_valuation + return BaseStockGLComposer(self).compose( + inventory_account_map, default_expense_account, default_cost_center ) def get_debit_field_precision(self): diff --git a/erpnext/stock/services/__init__.py b/erpnext/stock/services/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/services/base_stock_gl_composer.py b/erpnext/stock/services/base_stock_gl_composer.py new file mode 100644 index 00000000000..27731c0eb9e --- /dev/null +++ b/erpnext/stock/services/base_stock_gl_composer.py @@ -0,0 +1,154 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +from frappe import _ +from frappe.utils import flt + +from erpnext.accounts.general_ledger import process_gl_map +from erpnext.accounts.services.base_gl_composer import BaseGLComposer + + +class BaseStockGLComposer(BaseGLComposer): + """Shared GL composition logic for stock vouchers. + + Subclasses override ``compose()`` and call ``super().compose()`` to get the + warehouse ↔ expense-account GL pairs, then append any doctype-specific + entries on top. + """ + + def compose( + self, + inventory_account_map: dict | None = None, + default_expense_account: str | None = None, + default_cost_center: str | None = None, + ) -> list: + doc = self.doc + + if not inventory_account_map: + inventory_account_map = doc.get_inventory_account_map() + + sle_map = doc.get_stock_ledger_details() + voucher_details = doc.get_voucher_details(default_expense_account, default_cost_center, sle_map) + + gl_list = [] + warehouse_with_no_account = [] + precision = doc.get_debit_field_precision() + + for item_row in voucher_details: + sle_list = sle_map.get(item_row.name) + sle_rounding_diff = 0.0 + if sle_list: + for sle in sle_list: + _inv_dict = doc.get_inventory_account_dict(sle, inventory_account_map) + + if _inv_dict.get("account"): + sle_rounding_diff += flt(sle.stock_value_difference) + + doc.check_expense_account(item_row) + + if item_row.get("target_warehouse"): + _target_wh_inv_dict = doc.get_inventory_account_dict( + item_row, inventory_account_map, warehouse_field="target_warehouse" + ) + expense_account = _target_wh_inv_dict["account"] + else: + expense_account = item_row.expense_account + + gl_list.append( + doc.get_gl_dict( + { + "account": _inv_dict["account"], + "against": expense_account, + "cost_center": item_row.cost_center, + "project": sle.get("project") or item_row.project or doc.get("project"), + "remarks": doc.get("remarks") or _("Accounting Entry for Stock"), + "debit": flt(sle.stock_value_difference, precision), + "is_opening": item_row.get("is_opening") or doc.get("is_opening") or "No", + }, + _inv_dict["account_currency"], + item=item_row, + ) + ) + + gl_list.append( + doc.get_gl_dict( + { + "account": expense_account, + "against": _inv_dict["account"], + "cost_center": item_row.cost_center, + "remarks": doc.get("remarks") or _("Accounting Entry for Stock"), + "debit": -1 * flt(sle.stock_value_difference, precision), + "project": sle.get("project") + or item_row.get("project") + or doc.get("project"), + "is_opening": item_row.get("is_opening") or doc.get("is_opening") or "No", + }, + item=item_row, + ) + ) + elif sle.warehouse not in warehouse_with_no_account: + warehouse_with_no_account.append(sle.warehouse) + + if abs(sle_rounding_diff) > (1.0 / (10**precision)) and doc.is_internal_transfer(): + warehouse_asset_account = "" + if doc.get("is_internal_customer"): + _inv_dict = doc.get_inventory_account_dict( + item_row, inventory_account_map, warehouse_field="target_warehouse" + ) + warehouse_asset_account = _inv_dict.get("account") if _inv_dict else None + elif doc.get("is_internal_supplier"): + _inv_dict = doc.get_inventory_account_dict(item_row, inventory_account_map) + warehouse_asset_account = _inv_dict.get("account") if _inv_dict else None + + expense_account = frappe.get_cached_value("Company", doc.company, "default_expense_account") + if not expense_account: + frappe.throw( + _( + "Please set default cost of goods sold account in company {0} for booking rounding gain and loss during stock transfer" + ).format(frappe.bold(doc.company)) + ) + + gl_list.append( + doc.get_gl_dict( + { + "account": expense_account, + "against": warehouse_asset_account, + "cost_center": item_row.cost_center, + "project": item_row.project or doc.get("project"), + "remarks": _("Rounding gain/loss Entry for Stock Transfer"), + "debit": sle_rounding_diff, + "is_opening": item_row.get("is_opening") or doc.get("is_opening") or "No", + }, + _inv_dict["account_currency"], + item=item_row, + ) + ) + + gl_list.append( + doc.get_gl_dict( + { + "account": warehouse_asset_account, + "against": expense_account, + "cost_center": item_row.cost_center, + "remarks": _("Rounding gain/loss Entry for Stock Transfer"), + "credit": sle_rounding_diff, + "project": item_row.get("project") or doc.get("project"), + "is_opening": item_row.get("is_opening") or doc.get("is_opening") or "No", + }, + item=item_row, + ) + ) + + if warehouse_with_no_account: + for wh in warehouse_with_no_account: + if frappe.get_cached_value("Warehouse", wh, "company"): + frappe.throw( + _( + "Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}." + ).format(wh, doc.company) + ) + + return process_gl_map( + gl_list, precision=precision, from_repost=frappe.flags.through_repost_item_valuation + )