mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-20 17:59:57 +00:00
fix(stock): allocate secondary item cost from the consumption entry (#57738)
* fix(stock): allocate secondary item cost from the consumption entry A secondary item's rate is its BOM share of the cost of the consumed rows. With Get RM Cost From Consumption Entry enabled the consumption happens in a separate document, so the Manufacture entry carries no consumed rows and that cost is zero. The share evaluated to zero, and the row fell through to the item's own valuation rate. Only the finished good substituted the consumption entry's cost. Against a consumption entry of 1000 and a BOM allocating 75% to the finished good and 25% to scrap, the finished good took its 750 while the scrap took an unrelated valuation of 100, booking 850 for 1000 consumed. Derive the allocation base once and use it for both sides. * test(stock): cover secondary allocation against a consumption entry A consumption entry of 1000 splits into 750 and 250 by the BOM's shares.
This commit is contained in:
@@ -572,6 +572,8 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
frappe.get_cached_value("BOM", self.bom_no, "cost_allocation_per") if self.bom_no else None
|
||||
)
|
||||
|
||||
secondary_items_cost_basis = self.get_secondary_items_cost_basis(outgoing_items_cost)
|
||||
|
||||
zero_valuation_items = []
|
||||
finished_items_last = sorted(self.get("items"), key=lambda row: cint(row.is_finished_item))
|
||||
for d in finished_items_last:
|
||||
@@ -591,11 +593,26 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
zero_valuation_items,
|
||||
bom_cost_allocation_per,
|
||||
has_consumption_basis,
|
||||
secondary_items_cost_basis,
|
||||
)
|
||||
|
||||
if zero_valuation_items:
|
||||
self._notify_zero_valuation_rate(zero_valuation_items)
|
||||
|
||||
def get_secondary_items_cost_basis(self, outgoing_items_cost) -> float:
|
||||
"""The cost a BOM allocation splits: the consumed rows, or the entry that replaced them."""
|
||||
if outgoing_items_cost or self.purpose != "Manufacture" or not self.work_order:
|
||||
return outgoing_items_cost
|
||||
|
||||
settings = frappe.get_single("Manufacturing Settings")
|
||||
if not (settings.material_consumption and settings.get_rm_cost_from_consumption_entry):
|
||||
return outgoing_items_cost
|
||||
|
||||
if not self.get_consumption_entries():
|
||||
return outgoing_items_cost
|
||||
|
||||
return self._fetch_consumption_entry_cost()
|
||||
|
||||
def has_consumption_basis(self) -> bool:
|
||||
"""Whether the cost of the consumed items is known, even when that cost is zero."""
|
||||
if any(d.s_warehouse for d in self.get("items")):
|
||||
@@ -629,6 +646,7 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
zero_valuation_items,
|
||||
bom_cost_allocation_per=None,
|
||||
has_consumption_basis=False,
|
||||
secondary_items_cost_basis=0,
|
||||
):
|
||||
has_derived_rate = False
|
||||
|
||||
@@ -653,7 +671,7 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
frappe.get_value("BOM Secondary Item", d.bom_secondary_item, "cost_allocation_per")
|
||||
)
|
||||
if flt(d.transfer_qty):
|
||||
d.basic_rate = (outgoing_items_cost * (cost_allocation_per / 100)) / d.transfer_qty
|
||||
d.basic_rate = (secondary_items_cost_basis * (cost_allocation_per / 100)) / d.transfer_qty
|
||||
has_derived_rate = True
|
||||
|
||||
# A rate of zero that was derived rather than left unset is a real cost. Falling back to
|
||||
|
||||
@@ -2917,6 +2917,66 @@ class TestStockEntry(ERPNextTestSuite):
|
||||
self.assertEqual(flt(fg_row.basic_amount), 1000.0)
|
||||
self.assertEqual(flt(se.value_difference), 0.0)
|
||||
|
||||
@ERPNextTestSuite.change_settings(
|
||||
"Manufacturing Settings", {"material_consumption": 1, "get_rm_cost_from_consumption_entry": 1}
|
||||
)
|
||||
def test_secondary_item_allocation_uses_consumption_entry_cost(self):
|
||||
"""A BOM allocation splits the consumption entry's cost, not an empty set of consumed rows."""
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
make_stock_entry as make_stock_entry_from_wo,
|
||||
)
|
||||
|
||||
rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100}).name
|
||||
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||
scrap_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 20}).name
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
bom = frappe.get_doc(
|
||||
{
|
||||
"doctype": "BOM",
|
||||
"item": fg_item,
|
||||
"currency": "INR",
|
||||
"quantity": 10,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
)
|
||||
bom.append("items", {"item_code": rm_item, "qty": 10})
|
||||
bom.append(
|
||||
"secondary_items",
|
||||
{
|
||||
"secondary_item_type": "Scrap",
|
||||
"item_code": scrap_item,
|
||||
"item_name": scrap_item,
|
||||
"qty": 5,
|
||||
"cost_allocation_per": 25,
|
||||
"process_loss_per": 0,
|
||||
},
|
||||
)
|
||||
bom.insert()
|
||||
bom.submit()
|
||||
|
||||
make_stock_entry(item_code=rm_item, target=warehouse, qty=100, basic_rate=100)
|
||||
wo = make_wo_order_test_record(
|
||||
production_item=fg_item, bom_no=bom.name, qty=10, skip_transfer=1, source_warehouse=warehouse
|
||||
)
|
||||
|
||||
consumption = frappe.get_doc(
|
||||
make_stock_entry_from_wo(wo.name, "Material Consumption for Manufacture", 10)
|
||||
)
|
||||
consumption.submit()
|
||||
self.assertEqual(flt(consumption.total_outgoing_value), 1000.0)
|
||||
|
||||
se = frappe.get_doc(make_stock_entry_from_wo(wo.name, "Manufacture", 10))
|
||||
se.save()
|
||||
|
||||
scrap_row = next(d for d in se.items if d.secondary_item_type)
|
||||
fg_row = next(d for d in se.items if d.is_finished_item)
|
||||
|
||||
self.assertEqual(flt(fg_row.basic_amount), 750.0)
|
||||
self.assertEqual(flt(scrap_row.basic_amount), 250.0)
|
||||
self.assertEqual(flt(se.total_incoming_value), 1000.0)
|
||||
|
||||
def _make_wo_for_free_raw_material(self, rm_item, fg_item, bom_no):
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
|
||||
Reference in New Issue
Block a user