mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-19 19:37:56 +00:00
Merge pull request #57732 from mihir-kandoi/fix/secondary-item-without-bom-balances-manufacture
fix(stock): cost a BOM-less secondary item out of the finished good
This commit is contained in:
@@ -70,6 +70,15 @@ from erpnext.controllers.subcontracting_inward_controller import SubcontractingI
|
|||||||
form_grid_templates = {"items": "templates/form_grid/stock_entry_grid.html"}
|
form_grid_templates = {"items": "templates/form_grid/stock_entry_grid.html"}
|
||||||
|
|
||||||
|
|
||||||
|
def is_costed_out_of_finished_item(row) -> bool:
|
||||||
|
"""Whether the row takes its value out of the finished good instead of adding to it.
|
||||||
|
|
||||||
|
A secondary item that is not linked to a BOM has no cost allocation of its own, so it is
|
||||||
|
valued the way the legacy scrap item was: its cost is deducted from the finished good.
|
||||||
|
"""
|
||||||
|
return bool(row.is_legacy_scrap_item or (row.secondary_item_type and not row.bom_secondary_item))
|
||||||
|
|
||||||
|
|
||||||
class StockEntry(StockController, SubcontractingInwardController):
|
class StockEntry(StockController, SubcontractingInwardController):
|
||||||
# begin: auto-generated types
|
# begin: auto-generated types
|
||||||
# This code is auto-generated. Do not modify anything in this block.
|
# This code is auto-generated. Do not modify anything in this block.
|
||||||
@@ -564,7 +573,8 @@ class StockEntry(StockController, SubcontractingInwardController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
zero_valuation_items = []
|
zero_valuation_items = []
|
||||||
for d in self.get("items"):
|
finished_items_last = sorted(self.get("items"), key=lambda row: cint(row.is_finished_item))
|
||||||
|
for d in finished_items_last:
|
||||||
if d.s_warehouse or d.set_basic_rate_manually:
|
if d.s_warehouse or d.set_basic_rate_manually:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -736,7 +746,9 @@ class StockEntry(StockController, SubcontractingInwardController):
|
|||||||
self, finished_item_qty, outgoing_items_cost=0, has_consumption_basis=False
|
self, finished_item_qty, outgoing_items_cost=0, has_consumption_basis=False
|
||||||
) -> float:
|
) -> float:
|
||||||
settings = frappe.get_single("Manufacturing Settings")
|
settings = frappe.get_single("Manufacturing Settings")
|
||||||
scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_legacy_scrap_item])
|
scrap_items_cost = sum(
|
||||||
|
[flt(d.basic_amount) for d in self.get("items") if is_costed_out_of_finished_item(d)]
|
||||||
|
)
|
||||||
|
|
||||||
if settings.material_consumption:
|
if settings.material_consumption:
|
||||||
outgoing_items_cost = self._get_rm_cost_for_manufacture(
|
outgoing_items_cost = self._get_rm_cost_for_manufacture(
|
||||||
|
|||||||
@@ -2759,6 +2759,55 @@ class TestStockEntry(ERPNextTestSuite):
|
|||||||
self.assertRaises(QualityInspectionRequiredError, receipt("").submit)
|
self.assertRaises(QualityInspectionRequiredError, receipt("").submit)
|
||||||
self.assertRaises(QualityInspectionRequiredError, receipt("Scrap").submit)
|
self.assertRaises(QualityInspectionRequiredError, receipt("Scrap").submit)
|
||||||
|
|
||||||
|
def test_manufacture_balances_secondary_item_added_without_a_bom(self):
|
||||||
|
"""A secondary item with no BOM link is costed out of the finished good, as legacy scrap was."""
|
||||||
|
rm_item = make_item(properties={"is_stock_item": 1}).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"
|
||||||
|
|
||||||
|
make_stock_entry(item_code=rm_item, target=warehouse, qty=10, basic_rate=100)
|
||||||
|
|
||||||
|
se = frappe.new_doc("Stock Entry")
|
||||||
|
se.purpose = se.stock_entry_type = "Manufacture"
|
||||||
|
se.company = "_Test Company"
|
||||||
|
se.append(
|
||||||
|
"items", {"item_code": rm_item, "s_warehouse": warehouse, "qty": 10, "conversion_factor": 1}
|
||||||
|
)
|
||||||
|
se.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": fg_item,
|
||||||
|
"t_warehouse": warehouse,
|
||||||
|
"qty": 10,
|
||||||
|
"is_finished_item": 1,
|
||||||
|
"conversion_factor": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
se.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": scrap_item,
|
||||||
|
"t_warehouse": warehouse,
|
||||||
|
"qty": 5,
|
||||||
|
"secondary_item_type": "Scrap",
|
||||||
|
"conversion_factor": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
se.save()
|
||||||
|
|
||||||
|
scrap_row = se.items[2]
|
||||||
|
self.assertEqual(flt(scrap_row.basic_rate), 20.0)
|
||||||
|
self.assertEqual(flt(scrap_row.basic_amount), 100.0)
|
||||||
|
|
||||||
|
fg_row = se.items[1]
|
||||||
|
self.assertEqual(flt(fg_row.basic_rate), 90.0)
|
||||||
|
self.assertEqual(flt(fg_row.basic_amount), 900.0)
|
||||||
|
|
||||||
|
self.assertEqual(flt(se.total_incoming_value), 1000.0)
|
||||||
|
self.assertEqual(flt(se.total_outgoing_value), 1000.0)
|
||||||
|
self.assertEqual(flt(se.value_difference), 0.0)
|
||||||
|
|
||||||
def _make_wo_for_free_raw_material(self, rm_item, fg_item, bom_no):
|
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.test_work_order import make_wo_order_test_record
|
||||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||||
|
|||||||
Reference in New Issue
Block a user