fix: derive operation FG items before material expansion, keep the final one the BOM's item

The finished_good derivation ran in validate_semi_finished_goods,
after set_materials_based_on_operation_bom had already expanded
operation BOM materials. A single-pass insert-and-submit (API or
import) with bom_no set but finished_good empty skipped the expansion,
persisting a submitted BOM without the referenced components. The
derivation also let a final operation inherit another item from its
bom_no, so downstream job cards would produce the wrong item.

Move the derivation into set_operation_finished_goods, called before
the expansion, prefer the BOM's own item for the final operation, and
reject a final operation whose FG item is not the BOM's item.

(cherry picked from commit 1e2e87daac)

# Conflicts:
#	erpnext/manufacturing/doctype/bom/bom.py
This commit is contained in:
Mihir Kandoi
2026-08-08 15:58:07 +05:30
committed by Mergify
parent b4eceeda2d
commit 3e0d0b2d68

View File

@@ -282,6 +282,7 @@ class BOM(WebsiteGenerator):
self.clear_inspection()
self.validate_main_item()
self.validate_currency()
self.set_operation_finished_goods()
self.set_materials_based_on_operation_bom()
self.set_conversion_rate()
self.set_plc_conversion_rate()
@@ -304,8 +305,23 @@ class BOM(WebsiteGenerator):
self.set_fg_cost_allocation()
self.validate_total_cost_allocation()
<<<<<<< HEAD
if self.docstatus == 1:
self.validate_raw_materials_of_operation()
=======
def set_operation_finished_goods(self):
"""Fill each operation's FG item where it is unambiguous: the final operation produces
this BOM's item, an operation with a BOM produces that BOM's item. Runs before
set_materials_based_on_operation_bom so derived rows get their materials expanded."""
if not self.track_semi_finished_goods:
return
for row in self.operations:
if row.is_final_finished_good and not row.finished_good:
row.finished_good = self.item
elif row.bom_no and not row.finished_good:
row.finished_good = frappe.get_cached_value("BOM", row.bom_no, "item")
>>>>>>> 1e2e87daac (fix: derive operation FG items before material expansion, keep the final one the BOM's item)
def validate_semi_finished_goods(self):
if not self.track_semi_finished_goods or not self.operations:
@@ -313,12 +329,6 @@ class BOM(WebsiteGenerator):
fg_items = []
for row in self.operations:
if row.bom_no and not row.finished_good:
row.finished_good = frappe.get_cached_value("BOM", row.bom_no, "item")
if row.is_final_finished_good and not row.finished_good:
row.finished_good = self.item
if not row.finished_good:
frappe.throw(
_(
@@ -329,6 +339,13 @@ class BOM(WebsiteGenerator):
if not row.is_final_finished_good:
continue
if row.finished_good != self.item:
frappe.throw(
_(
"Row #{0}: The operation {1} has 'Is Final Finished Good' checked, so its FG / Semi FG Item must be {2}."
).format(row.idx, bold(row.operation), bold(self.item)),
)
fg_items.append(row.finished_good)
if not fg_items: