mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-17 18:45:20 +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
|
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.buying.utils import check_on_hold_or_closed_status
|
||||||
from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
|
from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
|
||||||
from erpnext.manufacturing.doctype.bom.bom import (
|
from erpnext.manufacturing.doctype.bom.bom import (
|
||||||
@@ -1050,137 +1048,9 @@ class StockEntry(StockController, SubcontractingInwardController):
|
|||||||
sl_entries.append(sle)
|
sl_entries.append(sle)
|
||||||
|
|
||||||
def get_gl_entries(self, inventory_account_map):
|
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"):
|
return StockEntryGLComposer(self).compose(inventory_account_map)
|
||||||
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,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pro_doc(self):
|
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
|
return new_sl_entries
|
||||||
|
|
||||||
def get_gl_entries(self, inventory_account_map=None):
|
def get_gl_entries(self, inventory_account_map=None):
|
||||||
if not self.cost_center:
|
from erpnext.stock.doctype.stock_reconciliation.services.gl_composer import (
|
||||||
msgprint(_("Please enter Cost Center"), raise_exception=1)
|
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):
|
def validate_expense_account(self):
|
||||||
if not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
|
if not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
|
||||||
|
|||||||
Reference in New Issue
Block a user