Merge pull request #55553 from aerele/fix/support-#69882

fix(manufacturing): fetch raw materials from work order
This commit is contained in:
Mihir Kandoi
2026-06-16 20:35:15 +05:30
committed by GitHub
2 changed files with 118 additions and 1 deletions

View File

@@ -3631,6 +3631,58 @@ class TestWorkOrder(FrappeTestCase):
self.assertEqual(bin1_at_completion.reserved_qty_for_production, 0)
@change_settings(
"Manufacturing Settings",
{"allow_editing_of_items_and_quantities_in_work_order": 1},
)
def test_manufacture_se_fetches_edited_qty_from_work_order(self):
"""When a raw material qty is edited on the Work Order, the Manufacture Stock Entry
must consume the edited quantity (scaled to fg_completed_qty) from the Work Order,
not the original BOM quantity."""
warehouse = "_Test Warehouse - _TC"
wo_order = make_wo_order_test_record(
item="_Test FG Item", qty=10, skip_transfer=1, source_warehouse=warehouse
)
# edit a required item's qty
wo_order.required_items[0].db_set("required_qty", flt(wo_order.required_items[0].required_qty) + 7)
wo_order.reload()
edited_row = wo_order.required_items[0]
fg_qty = 5
se = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", fg_qty))
se_qty = {row.item_code: row.qty for row in se.items if row.s_warehouse}
precision = frappe.get_precision("Stock Entry Detail", "qty")
expected = flt(edited_row.required_qty / wo_order.qty * fg_qty, precision)
self.assertEqual(flt(se_qty.get(edited_row.item_code)), expected)
@change_settings(
"Manufacturing Settings",
{"allow_editing_of_items_and_quantities_in_work_order": 1},
)
def test_manufacture_se_fetches_item_not_in_bom_from_work_order(self):
"""A raw material that is present on the Work Order but not on the BOM must still be
fetched into the Manufacture Stock Entry, proving items are sourced from the Work
Order's required_items rather than re-derived from the BOM."""
extra_item = make_item(
"_Test WO Extra Raw Material", {"is_stock_item": 1, "valuation_rate": 100}
).name
warehouse = "_Test Warehouse - _TC"
wo_order = make_wo_order_test_record(
item="_Test FG Item", qty=10, skip_transfer=1, source_warehouse=warehouse
)
original_item = wo_order.required_items[0].item_code
wo_order.required_items[0].db_set("item_code", extra_item)
wo_order.reload()
se = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 5))
se_items = [row.item_code for row in se.items if row.s_warehouse]
self.assertIn(extra_item, se_items)
self.assertNotIn(original_item, se_items)
def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse):
from erpnext.stock.doctype.stock_entry.test_stock_entry import (

View File

@@ -2467,9 +2467,24 @@ class StockEntry(StockController):
):
self.get_unconsumed_raw_materials()
elif self.pro_doc and (
self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture"
):
if not self.fg_completed_qty:
frappe.throw(_("{0} is mandatory").format(_(self.meta.get_label("fg_completed_qty"))))
item_dict = self.get_work_order_raw_materials(self.fg_completed_qty)
for item in item_dict.values():
if self.pro_doc.from_wip_warehouse:
item["from_warehouse"] = self.pro_doc.wip_warehouse
item["to_warehouse"] = ""
self.add_to_stock_entry_detail(item_dict)
else:
if not self.fg_completed_qty:
frappe.throw(_("Manufacturing Quantity is mandatory"))
frappe.throw(_("{0} is mandatory").format(_(self.meta.get_label("fg_completed_qty"))))
item_dict = self.get_bom_raw_materials(self.fg_completed_qty)
@@ -2724,6 +2739,56 @@ class StockEntry(StockController):
return item_dict
def get_work_order_raw_materials(self, qty):
item_dict = frappe._dict()
used_alternative_items = get_used_alternative_items(
subcontract_order_field=self.subcontract_data.order_field, work_order=self.work_order
)
for d in self.pro_doc.get("required_items"):
item_qty = flt(
(d.required_qty / self.pro_doc.qty) * qty, frappe.get_precision("Stock Entry Detail", "qty")
)
from_warehouse = (
d.source_warehouse
if self.pro_doc.skip_transfer and not self.pro_doc.from_wip_warehouse
else self.from_warehouse or d.source_warehouse
)
item_row = frappe._dict(
{
"item_code": d.item_code,
"item_name": d.item_name,
"description": d.description,
"qty": item_qty,
"stock_uom": d.stock_uom,
"uom": d.stock_uom,
"conversion_factor": 1,
"from_warehouse": from_warehouse,
"allow_alternative_item": d.allow_alternative_item
and self.pro_doc.allow_alternative_item,
}
)
if d.item_code in used_alternative_items:
alt = used_alternative_items.get(d.item_code)
item_row.update(
{
"item_code": alt.item_code,
"item_name": alt.item_name,
"stock_uom": alt.stock_uom,
"uom": alt.uom,
"conversion_factor": alt.conversion_factor,
"description": alt.description,
"original_item": d.item_code,
}
)
item_dict[d.item_code] = item_row
return item_dict
def get_bom_scrap_material(self, qty):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict