mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-13 14:41:53 +00:00
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:
157
erpnext/stock/doctype/stock_entry/services/gl_composer.py
Normal file
157
erpnext/stock/doctype/stock_entry/services/gl_composer.py
Normal 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,
|
||||
)
|
||||
)
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
@@ -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)):
|
||||
|
||||
Reference in New Issue
Block a user