mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-13 17:20:36 +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.actual_qty = flt(bin_qty.actual_qty)
|
||||||
d.projected_qty = flt(bin_qty.projected_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):
|
def validate_expense_account(self):
|
||||||
company_values = frappe.get_cached_value(
|
company_values = frappe.get_cached_value(
|
||||||
"Company",
|
"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
|
Extends the base stock GL loop with additional-cost entries (from the
|
||||||
``additional_costs`` child table) and landed-cost voucher adjustments.
|
``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:
|
def compose(self, inventory_account_map: dict | None = None) -> list:
|
||||||
doc = self.doc
|
doc = self.doc
|
||||||
gl_entries = super().compose(inventory_account_map)
|
gl_entries = super().compose(inventory_account_map)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
from frappe import _, msgprint
|
from frappe import _, msgprint
|
||||||
|
|
||||||
from erpnext.stock.services.base_stock_gl_composer import BaseStockGLComposer
|
from erpnext.stock.services.base_stock_gl_composer import BaseStockGLComposer
|
||||||
@@ -10,11 +11,30 @@ class StockReconciliationGLComposer(BaseStockGLComposer):
|
|||||||
"""GL composer for Stock Reconciliation.
|
"""GL composer for Stock Reconciliation.
|
||||||
|
|
||||||
SR carries its own expense_account and cost_center which are passed as
|
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:
|
def compose(self, inventory_account_map: dict | None = None) -> list:
|
||||||
doc = self.doc
|
doc = self.doc
|
||||||
if not doc.cost_center:
|
if not doc.cost_center:
|
||||||
msgprint(_("Please enter Cost Center"), raise_exception=1)
|
msgprint(_("Please enter Cost Center"), raise_exception=1)
|
||||||
return super().compose(inventory_account_map, doc.expense_account, doc.cost_center)
|
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.
|
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(
|
def compose(
|
||||||
self,
|
self,
|
||||||
inventory_account_map: dict | None = None,
|
inventory_account_map: dict | None = None,
|
||||||
@@ -160,34 +165,16 @@ class BaseStockGLComposer(BaseGLComposer):
|
|||||||
return frappe.flags.debit_field_precision
|
return frappe.flags.debit_field_precision
|
||||||
|
|
||||||
def get_voucher_details(self, default_expense_account, default_cost_center, sle_map):
|
def get_voucher_details(self, default_expense_account, default_cost_center, sle_map):
|
||||||
doc = self.doc
|
details = self.doc.get("items")
|
||||||
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")
|
|
||||||
|
|
||||||
if default_expense_account or default_cost_center:
|
if default_expense_account or default_cost_center:
|
||||||
for d in details:
|
for d in details:
|
||||||
if default_expense_account and not d.get("expense_account"):
|
if default_expense_account and not d.get("expense_account"):
|
||||||
d.expense_account = default_expense_account
|
d.expense_account = default_expense_account
|
||||||
if default_cost_center and not d.get("cost_center"):
|
if default_cost_center and not d.get("cost_center"):
|
||||||
d.cost_center = default_cost_center
|
d.cost_center = default_cost_center
|
||||||
|
|
||||||
return details
|
return details
|
||||||
|
|
||||||
def check_expense_account(self, item):
|
def check_expense_account(self, item):
|
||||||
if not item.get("expense_account"):
|
if not item.get("expense_account"):
|
||||||
@@ -204,18 +191,7 @@ class BaseStockGLComposer(BaseGLComposer):
|
|||||||
frappe.get_cached_value("Account", item.get("expense_account"), "report_type")
|
frappe.get_cached_value("Account", item.get("expense_account"), "report_type")
|
||||||
== "Profit and Loss"
|
== "Profit and Loss"
|
||||||
)
|
)
|
||||||
if (
|
if self.enforce_pl_expense_account and not is_expense_account:
|
||||||
self.doc.doctype
|
|
||||||
not in (
|
|
||||||
"Purchase Receipt",
|
|
||||||
"Purchase Invoice",
|
|
||||||
"Stock Reconciliation",
|
|
||||||
"Stock Entry",
|
|
||||||
"Subcontracting Receipt",
|
|
||||||
"Delivery Note",
|
|
||||||
)
|
|
||||||
and not is_expense_account
|
|
||||||
):
|
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Expense / Difference account ({0}) must be a 'Profit or Loss' account").format(
|
_("Expense / Difference account ({0}) must be a 'Profit or Loss' account").format(
|
||||||
item.get("expense_account")
|
item.get("expense_account")
|
||||||
|
|||||||
Reference in New Issue
Block a user