mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-12 06:01:46 +00:00
refactor(stock): de-conditionalize BaseStockGLComposer via subclass hooks
Remove per-doctype branching from the shared stock GL composer so each voucher owns its own behavior: - Move the Stock Reconciliation voucher-detail synthesis out of BaseStockGLComposer.get_voucher_details into a StockReconciliationGLComposer.get_voucher_details override. - Replace the hardcoded doctype allow-list in check_expense_account with an overridable class attribute enforce_pl_expense_account (default True). Vouchers that post the difference to a balance-sheet account (Stock Entry, Stock Reconciliation, Delivery Note) set it False. - Add DeliveryNoteGLComposer to own the P&L-exempt rule and wire DeliveryNote.get_gl_entries to it. No change to GL output; behavior is relocated, not altered.
This commit is contained in:
@@ -418,6 +418,11 @@ class DeliveryNote(SellingController):
|
||||
d.actual_qty = flt(bin_qty.actual_qty)
|
||||
d.projected_qty = flt(bin_qty.projected_qty)
|
||||
|
||||
def get_gl_entries(self, inventory_account_map=None):
|
||||
from erpnext.stock.doctype.delivery_note.services.gl_composer import DeliveryNoteGLComposer
|
||||
|
||||
return DeliveryNoteGLComposer(self).compose(inventory_account_map)
|
||||
|
||||
def validate_expense_account(self):
|
||||
company_values = frappe.get_cached_value(
|
||||
"Company",
|
||||
|
||||
17
erpnext/stock/doctype/delivery_note/services/gl_composer.py
Normal file
17
erpnext/stock/doctype/delivery_note/services/gl_composer.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from erpnext.stock.services.base_stock_gl_composer import BaseStockGLComposer
|
||||
|
||||
|
||||
class DeliveryNoteGLComposer(BaseStockGLComposer):
|
||||
"""GL composer for Delivery Note.
|
||||
|
||||
Delivery Note posts the standard stock ↔ expense (COGS) entries produced by
|
||||
the base stock GL loop and adds no voucher-specific rows. It only relaxes the
|
||||
expense-account rule: the delivery difference may land on a balance-sheet
|
||||
account (e.g. the target warehouse account on an internal customer transfer),
|
||||
so P&L enforcement is off.
|
||||
"""
|
||||
|
||||
enforce_pl_expense_account = False
|
||||
@@ -15,8 +15,12 @@ class StockEntryGLComposer(BaseStockGLComposer):
|
||||
|
||||
Extends the base stock GL loop with additional-cost entries (from the
|
||||
``additional_costs`` child table) and landed-cost voucher adjustments.
|
||||
The difference is posted to warehouse/balance-sheet accounts, so P&L
|
||||
enforcement on the expense account is off.
|
||||
"""
|
||||
|
||||
enforce_pl_expense_account = False
|
||||
|
||||
def compose(self, inventory_account_map: dict | None = None) -> list:
|
||||
doc = self.doc
|
||||
gl_entries = super().compose(inventory_account_map)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
|
||||
from erpnext.stock.services.base_stock_gl_composer import BaseStockGLComposer
|
||||
@@ -10,11 +11,30 @@ 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.
|
||||
defaults into the base stock GL composition loop. It synthesises one voucher
|
||||
detail per stock ledger entry (SR has no ``items`` table with expense rows)
|
||||
and posts the difference to a balance-sheet account, so P&L enforcement is
|
||||
off.
|
||||
"""
|
||||
|
||||
enforce_pl_expense_account = False
|
||||
|
||||
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)
|
||||
|
||||
def get_voucher_details(self, default_expense_account, default_cost_center, sle_map):
|
||||
is_opening = "Yes" if self.doc.purpose == "Opening Stock" else "No"
|
||||
return [
|
||||
frappe._dict(
|
||||
{
|
||||
"name": voucher_detail_no,
|
||||
"expense_account": default_expense_account,
|
||||
"cost_center": default_cost_center,
|
||||
"is_opening": is_opening,
|
||||
}
|
||||
)
|
||||
for voucher_detail_no in sle_map
|
||||
]
|
||||
|
||||
@@ -17,6 +17,11 @@ class BaseStockGLComposer(BaseGLComposer):
|
||||
entries on top.
|
||||
"""
|
||||
|
||||
#: Whether the item's expense/difference account must be a 'Profit and Loss'
|
||||
#: account. Vouchers that legitimately post the difference to a balance-sheet
|
||||
#: account (stock transfers, deliveries, reconciliations) set this to False.
|
||||
enforce_pl_expense_account = True
|
||||
|
||||
def compose(
|
||||
self,
|
||||
inventory_account_map: dict | None = None,
|
||||
@@ -160,34 +165,16 @@ class BaseStockGLComposer(BaseGLComposer):
|
||||
return frappe.flags.debit_field_precision
|
||||
|
||||
def get_voucher_details(self, default_expense_account, default_cost_center, sle_map):
|
||||
doc = self.doc
|
||||
if doc.doctype == "Stock Reconciliation":
|
||||
reconciliation_purpose = frappe.db.get_value(doc.doctype, doc.name, "purpose")
|
||||
is_opening = "Yes" if reconciliation_purpose == "Opening Stock" else "No"
|
||||
details = []
|
||||
for voucher_detail_no in sle_map:
|
||||
details.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"name": voucher_detail_no,
|
||||
"expense_account": default_expense_account,
|
||||
"cost_center": default_cost_center,
|
||||
"is_opening": is_opening,
|
||||
}
|
||||
)
|
||||
)
|
||||
return details
|
||||
else:
|
||||
details = doc.get("items")
|
||||
details = self.doc.get("items")
|
||||
|
||||
if default_expense_account or default_cost_center:
|
||||
for d in details:
|
||||
if default_expense_account and not d.get("expense_account"):
|
||||
d.expense_account = default_expense_account
|
||||
if default_cost_center and not d.get("cost_center"):
|
||||
d.cost_center = default_cost_center
|
||||
if default_expense_account or default_cost_center:
|
||||
for d in details:
|
||||
if default_expense_account and not d.get("expense_account"):
|
||||
d.expense_account = default_expense_account
|
||||
if default_cost_center and not d.get("cost_center"):
|
||||
d.cost_center = default_cost_center
|
||||
|
||||
return details
|
||||
return details
|
||||
|
||||
def check_expense_account(self, item):
|
||||
if not item.get("expense_account"):
|
||||
@@ -204,18 +191,7 @@ class BaseStockGLComposer(BaseGLComposer):
|
||||
frappe.get_cached_value("Account", item.get("expense_account"), "report_type")
|
||||
== "Profit and Loss"
|
||||
)
|
||||
if (
|
||||
self.doc.doctype
|
||||
not in (
|
||||
"Purchase Receipt",
|
||||
"Purchase Invoice",
|
||||
"Stock Reconciliation",
|
||||
"Stock Entry",
|
||||
"Subcontracting Receipt",
|
||||
"Delivery Note",
|
||||
)
|
||||
and not is_expense_account
|
||||
):
|
||||
if self.enforce_pl_expense_account and not is_expense_account:
|
||||
frappe.throw(
|
||||
_("Expense / Difference account ({0}) must be a 'Profit or Loss' account").format(
|
||||
item.get("expense_account")
|
||||
|
||||
Reference in New Issue
Block a user