refactor: introduce StockEntryGLComposer and StockReconciliationGLComposer

Stock Entry
  stock_entry/services/gl_composer.py → StockEntryGLComposer(BaseStockGLComposer)
  compose() calls super().compose() for the base warehouse↔expense GL pairs,
  then adds additional-cost entries (_build_additional_cost_per_item_account +
  _append_additional_cost_gl_entries) and LCV adjustments (_append_lcv_gl_entries).
  get_item_account_wise_lcv_entries stays on StockController (called via self.doc).
  StockEntry.get_gl_entries is now a 3-line shim.
  Removed private helpers from StockEntry; dropped unused process_gl_map and
  get_account_currency imports.

Stock Reconciliation
  stock_reconciliation/services/gl_composer.py → StockReconciliationGLComposer(BaseStockGLComposer)
  compose() guards cost_center and delegates to
  super().compose(inventory_account_map, doc.expense_account, doc.cost_center).
  StockReconciliation.get_gl_entries is now a 3-line shim.

Verified: 26 GL snapshots byte-identical on test-erpnext-v17;
89 SE tests and 33/34 SR tests green on test-site-ai
(1 pre-existing SR failure in test_serial_no_status_with_backdated_stock_reco,
unrelated to GL — IndexError in serial bundle setup).
This commit is contained in:
Nabin Hait
2026-05-27 15:01:27 +05:30
parent 001c70831c
commit 18188cb1b2
6 changed files with 183 additions and 135 deletions

View File

@@ -0,0 +1,157 @@
# 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.utils import get_account_currency
from erpnext.stock.services.base_stock_gl_composer import BaseStockGLComposer
class StockEntryGLComposer(BaseStockGLComposer):
"""GL composer for Stock Entry.
Extends the base stock GL loop with additional-cost entries (from the
``additional_costs`` child table) and landed-cost voucher adjustments.
"""
def compose(self, inventory_account_map: dict | None = None) -> list:
doc = self.doc
gl_entries = super().compose(inventory_account_map)
if doc.purpose in ("Repack", "Manufacture"):
total_basic_amount = sum(flt(t.basic_amount) for t in doc.get("items") if t.is_finished_item)
else:
total_basic_amount = sum(flt(t.basic_amount) for t in doc.get("items") if t.t_warehouse)
divide_based_on = total_basic_amount
if doc.get("additional_costs") and not total_basic_amount:
divide_based_on = sum(item.qty for item in doc.get("items"))
item_account_wise_additional_cost = self._build_additional_cost_per_item_account(
total_basic_amount, divide_based_on
)
if item_account_wise_additional_cost:
self._append_additional_cost_gl_entries(gl_entries, item_account_wise_additional_cost)
self._append_lcv_gl_entries(gl_entries, inventory_account_map)
return process_gl_map(gl_entries, from_repost=frappe.flags.through_repost_item_valuation)
def _build_additional_cost_per_item_account(
self, total_basic_amount: float, divide_based_on: float
) -> dict:
doc = self.doc
item_account_wise_additional_cost = {}
for t in doc.get("additional_costs"):
for d in doc.get("items"):
if doc.purpose in ("Repack", "Manufacture") and not d.is_finished_item:
continue
elif not d.t_warehouse:
continue
item_account_wise_additional_cost.setdefault((d.item_code, d.name), {})
item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(
t.expense_account, {"amount": 0.0, "base_amount": 0.0}
)
multiply_based_on = d.basic_amount if total_basic_amount else d.qty
entry = item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]
entry["amount"] += flt(t.amount * multiply_based_on) / divide_based_on
entry["base_amount"] += flt(t.base_amount * multiply_based_on) / divide_based_on
return item_account_wise_additional_cost
def _append_additional_cost_gl_entries(
self, gl_entries: list, item_account_wise_additional_cost: dict
) -> None:
doc = self.doc
for d in doc.get("items"):
for account, amount in item_account_wise_additional_cost.get((d.item_code, d.name), {}).items():
if not amount:
continue
gl_entries.append(
doc.get_gl_dict(
{
"account": account,
"against": d.expense_account,
"cost_center": d.cost_center,
"remarks": doc.get("remarks") or _("Accounting Entry for Stock"),
"credit_in_account_currency": flt(amount["amount"]),
"credit": flt(amount["base_amount"]),
},
item=d,
)
)
gl_entries.append(
doc.get_gl_dict(
{
"account": d.expense_account,
"against": account,
"cost_center": d.cost_center,
"remarks": doc.get("remarks") or _("Accounting Entry for Stock"),
"credit": -1 * amount["base_amount"],
},
item=d,
)
)
def _append_lcv_gl_entries(self, gl_entries: list, inventory_account_map: dict) -> None:
doc = self.doc
landed_cost_entries = doc.get_item_account_wise_lcv_entries()
if not landed_cost_entries:
return
for item in doc.get("items"):
if item.s_warehouse:
continue
if (item.item_code, item.name) in landed_cost_entries:
for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
account_currency = get_account_currency(account)
credit_amount = (
flt(amount["base_amount"])
if (amount["base_amount"] or account_currency != doc.company_currency)
else flt(amount["amount"])
)
_inv_dict = doc.get_inventory_account_dict(item, inventory_account_map, "t_warehouse")
gl_entries.append(
doc.get_gl_dict(
{
"account": account,
"against": _inv_dict["account"],
"cost_center": item.cost_center,
"debit": 0.0,
"credit": credit_amount,
"remarks": _("Accounting Entry for LCV in Stock Entry {0}").format(doc.name),
"credit_in_account_currency": flt(amount["amount"]),
"account_currency": account_currency,
"project": item.project,
},
item=item,
)
)
account_currency = get_account_currency(item.expense_account)
gl_entries.append(
doc.get_gl_dict(
{
"account": item.expense_account,
"against": _inv_dict["account"],
"cost_center": item.cost_center,
"debit": 0.0,
"credit": credit_amount * -1,
"remarks": _("Accounting Entry for LCV in Stock Entry {0}").format(doc.name),
"debit_in_account_currency": flt(amount["amount"]),
"account_currency": account_currency,
"project": item.project,
},
item=item,
)
)

View File

@@ -24,8 +24,6 @@ from frappe.utils import (
)
import erpnext
from erpnext.accounts.general_ledger import process_gl_map
from erpnext.accounts.utils import get_account_currency
from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
from erpnext.manufacturing.doctype.bom.bom import (
@@ -1050,137 +1048,9 @@ class StockEntry(StockController, SubcontractingInwardController):
sl_entries.append(sle)
def get_gl_entries(self, inventory_account_map):
gl_entries = super().get_gl_entries(inventory_account_map)
from erpnext.stock.doctype.stock_entry.services.gl_composer import StockEntryGLComposer
if self.purpose in ("Repack", "Manufacture"):
total_basic_amount = sum(flt(t.basic_amount) for t in self.get("items") if t.is_finished_item)
else:
total_basic_amount = sum(flt(t.basic_amount) for t in self.get("items") if t.t_warehouse)
divide_based_on = total_basic_amount
if self.get("additional_costs") and not total_basic_amount:
divide_based_on = sum(item.qty for item in self.get("items"))
item_account_wise_additional_cost = self._build_additional_cost_per_item_account(
total_basic_amount, divide_based_on
)
if item_account_wise_additional_cost:
self._append_additional_cost_gl_entries(gl_entries, item_account_wise_additional_cost)
self.set_gl_entries_for_landed_cost_voucher(gl_entries, inventory_account_map)
return process_gl_map(gl_entries, from_repost=frappe.flags.through_repost_item_valuation)
def _build_additional_cost_per_item_account(self, total_basic_amount, divide_based_on):
item_account_wise_additional_cost = {}
for t in self.get("additional_costs"):
for d in self.get("items"):
if self.purpose in ("Repack", "Manufacture") and not d.is_finished_item:
continue
elif not d.t_warehouse:
continue
item_account_wise_additional_cost.setdefault((d.item_code, d.name), {})
item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(
t.expense_account, {"amount": 0.0, "base_amount": 0.0}
)
multiply_based_on = d.basic_amount if total_basic_amount else d.qty
entry = item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]
entry["amount"] += flt(t.amount * multiply_based_on) / divide_based_on
entry["base_amount"] += flt(t.base_amount * multiply_based_on) / divide_based_on
return item_account_wise_additional_cost
def _append_additional_cost_gl_entries(self, gl_entries, item_account_wise_additional_cost):
for d in self.get("items"):
for account, amount in item_account_wise_additional_cost.get((d.item_code, d.name), {}).items():
if not amount:
continue
gl_entries.append(
self.get_gl_dict(
{
"account": account,
"against": d.expense_account,
"cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit_in_account_currency": flt(amount["amount"]),
"credit": flt(amount["base_amount"]),
},
item=d,
)
)
gl_entries.append(
self.get_gl_dict(
{
"account": d.expense_account,
"against": account,
"cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": -1 * amount["base_amount"], # negative credit instead of debit
},
item=d,
)
)
def set_gl_entries_for_landed_cost_voucher(self, gl_entries, inventory_account_map):
landed_cost_entries = self.get_item_account_wise_lcv_entries()
if not landed_cost_entries:
return
for item in self.get("items"):
if item.s_warehouse:
continue
if (item.item_code, item.name) in landed_cost_entries:
for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
account_currency = get_account_currency(account)
credit_amount = (
flt(amount["base_amount"])
if (amount["base_amount"] or account_currency != self.company_currency)
else flt(amount["amount"])
)
_inv_dict = self.get_inventory_account_dict(item, inventory_account_map, "t_warehouse")
gl_entries.append(
self.get_gl_dict(
{
"account": account,
"against": _inv_dict["account"],
"cost_center": item.cost_center,
"debit": 0.0,
"credit": credit_amount,
"remarks": _("Accounting Entry for LCV in Stock Entry {0}").format(self.name),
"credit_in_account_currency": flt(amount["amount"]),
"account_currency": account_currency,
"project": item.project,
},
item=item,
)
)
account_currency = get_account_currency(item.expense_account)
# credit amount in negative to knock off the debit entry
gl_entries.append(
self.get_gl_dict(
{
"account": item.expense_account,
"against": _inv_dict["account"],
"cost_center": item.cost_center,
"debit": 0.0,
"credit": credit_amount * -1,
"remarks": _("Accounting Entry for LCV in Stock Entry {0}").format(self.name),
"debit_in_account_currency": flt(amount["amount"]),
"account_currency": account_currency,
"project": item.project,
},
item=item,
)
)
return StockEntryGLComposer(self).compose(inventory_account_map)
@property
def pro_doc(self):

View File

@@ -0,0 +1,20 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from frappe import _, msgprint
from erpnext.stock.services.base_stock_gl_composer import BaseStockGLComposer
class StockReconciliationGLComposer(BaseStockGLComposer):
"""GL composer for Stock Reconciliation.
SR carries its own expense_account and cost_center which are passed as
defaults into the base stock GL composition loop.
"""
def compose(self, inventory_account_map: dict | None = None) -> list:
doc = self.doc
if not doc.cost_center:
msgprint(_("Please enter Cost Center"), raise_exception=1)
return super().compose(inventory_account_map, doc.expense_account, doc.cost_center)

View File

@@ -975,10 +975,11 @@ class StockReconciliation(StockController):
return new_sl_entries
def get_gl_entries(self, inventory_account_map=None):
if not self.cost_center:
msgprint(_("Please enter Cost Center"), raise_exception=1)
from erpnext.stock.doctype.stock_reconciliation.services.gl_composer import (
StockReconciliationGLComposer,
)
return super().get_gl_entries(inventory_account_map, self.expense_account, self.cost_center)
return StockReconciliationGLComposer(self).compose(inventory_account_map)
def validate_expense_account(self):
if not cint(erpnext.is_perpetual_inventory_enabled(self.company)):