refactor: introduce BaseStockGLComposer, slim StockController.get_gl_entries

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.<method>.

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.
This commit is contained in:
Nabin Hait
2026-05-27 14:47:19 +05:30
parent e8f9cf6e3f
commit b68daea365
3 changed files with 157 additions and 134 deletions

View File

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

View File

View File

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