Merge pull request #54350 from rohitwaghchaure/feat-backflush-based-on-in-bom

feat: backflush based on in BOM
This commit is contained in:
rohitwaghchaure
2026-04-20 16:36:36 +05:30
committed by GitHub
9 changed files with 201 additions and 77 deletions

View File

@@ -7,31 +7,18 @@
"engine": "InnoDB",
"field_order": [
"production_item_tab",
"final_product_section",
"company",
"item",
"column_break_ztxc",
"quantity",
"cb0",
"is_active",
"is_default",
"allow_alternative_item",
"set_rate_of_sub_assembly_item_based_on_bom",
"is_phantom_bom",
"cost_allocation_section",
"uom",
"cost_allocation__process_loss_section",
"cost_allocation_per",
"column_break_srby",
"cost_allocation",
"process_loss_section",
"column_break_tgkb",
"process_loss_percentage",
"column_break_ssj2",
"process_loss_qty",
"currency_detail",
"rm_cost_as_per",
"buying_price_list",
"price_list_currency",
"plc_conversion_rate",
"column_break_ivyw",
"currency",
"conversion_rate",
"operations_section_section",
"with_operations",
"track_semi_finished_goods",
@@ -46,8 +33,27 @@
"operations",
"materials_section",
"items",
"secondary_items_tab",
"section_break_hygk",
"secondary_items",
"bom_conf_tab",
"bom_configuration_section",
"column_break_zbzp",
"is_active",
"is_default",
"set_rate_of_sub_assembly_item_based_on_bom",
"cb0",
"is_phantom_bom",
"allow_alternative_item",
"quality_inspection_section_break",
"inspection_required",
"column_break_dxp7",
"quality_inspection_template",
"default_warehouse_section",
"default_source_warehouse",
"column_break_inep",
"default_target_warehouse",
"consume_components_section",
"backflush_based_on",
"costing",
"operating_cost",
"raw_material_cost",
@@ -59,23 +65,21 @@
"column_break_26",
"total_cost",
"base_total_cost",
"quality_inspection_tab",
"quality_inspection_section_break",
"inspection_required",
"column_break_dxp7",
"quality_inspection_template",
"more_info_tab",
"currency_detail",
"rm_cost_as_per",
"buying_price_list",
"price_list_currency",
"plc_conversion_rate",
"column_break_ivyw",
"currency",
"conversion_rate",
"production_item_info_section",
"item_name",
"uom",
"image",
"column_break_27",
"description",
"has_variants",
"default_warehouse_section",
"default_source_warehouse",
"column_break_inep",
"default_target_warehouse",
"section_break_ouuf",
"project",
"section_break0",
@@ -99,17 +103,18 @@
],
"fields": [
{
"description": "Item to be manufactured or repacked",
"description": "The final item that will be produced using this BOM.",
"fieldname": "item",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Item",
"label": "Item to Manufacture",
"oldfieldname": "item",
"oldfieldtype": "Link",
"options": "Item",
"reqd": 1,
"search_index": 1
"search_index": 1,
"show_description_on_click": 1
},
{
"fetch_from": "item.item_name",
@@ -130,23 +135,26 @@
"read_only": 1
},
{
"depends_on": "item",
"fetch_from": "item.stock_uom",
"fieldname": "uom",
"fieldtype": "Link",
"label": "Item UOM",
"label": "Unit Of Measure",
"options": "UOM",
"read_only": 1
},
{
"default": "1",
"description": "Quantity of item obtained after manufacturing / repacking from given quantities of raw materials",
"depends_on": "item",
"description": "How many units of the final product this BOM makes.",
"fieldname": "quantity",
"fieldtype": "Float",
"label": "Quantity",
"label": "Quantity (Output Qty)",
"non_negative": 1,
"oldfieldname": "quantity",
"oldfieldtype": "Currency",
"reqd": 1
"reqd": 1,
"show_description_on_click": 1
},
{
"fieldname": "cb0",
@@ -288,14 +296,13 @@
{
"fieldname": "materials_section",
"fieldtype": "Section Break",
"label": "Raw Materials",
"oldfieldtype": "Section Break"
},
{
"allow_bulk_edit": 1,
"fieldname": "items",
"fieldtype": "Table",
"label": "Items",
"label": "Components",
"oldfieldname": "bom_materials",
"oldfieldtype": "Table",
"options": "BOM Item",
@@ -415,6 +422,7 @@
"depends_on": "eval:!doc.is_phantom_bom",
"fieldname": "website_section",
"fieldtype": "Tab Break",
"hidden": 1,
"label": "Website"
},
{
@@ -528,11 +536,6 @@
"fieldtype": "Section Break",
"label": "Operations"
},
{
"fieldname": "process_loss_section",
"fieldtype": "Section Break",
"label": "Process Loss"
},
{
"fieldname": "process_loss_percentage",
"fieldtype": "Percent",
@@ -546,10 +549,6 @@
"non_negative": 1,
"read_only": 1
},
{
"fieldname": "column_break_ssj2",
"fieldtype": "Column Break"
},
{
"fieldname": "more_info_tab",
"fieldtype": "Tab Break",
@@ -668,11 +667,6 @@
"fieldname": "section_break_ouuf",
"fieldtype": "Section Break"
},
{
"fieldname": "quality_inspection_tab",
"fieldtype": "Tab Break",
"label": "Quality Inspection"
},
{
"fieldname": "secondary_items",
"fieldtype": "Table",
@@ -697,20 +691,6 @@
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "secondary_items_tab",
"fieldtype": "Tab Break",
"label": "Secondary Items"
},
{
"fieldname": "cost_allocation_section",
"fieldtype": "Section Break",
"label": "Cost Allocation"
},
{
"fieldname": "column_break_srby",
"fieldtype": "Column Break"
},
{
"fieldname": "cost_allocation",
"fieldtype": "Currency",
@@ -725,6 +705,55 @@
"fieldtype": "Percent",
"label": "% Cost Allocation",
"non_negative": 1
},
{
"collapsible": 1,
"fieldname": "bom_configuration_section",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_zbzp",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_ztxc",
"fieldtype": "Column Break"
},
{
"collapsible": 1,
"fieldname": "cost_allocation__process_loss_section",
"fieldtype": "Section Break",
"label": "Cost Allocation / Process Loss"
},
{
"fieldname": "column_break_tgkb",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_hygk",
"fieldtype": "Section Break"
},
{
"fieldname": "final_product_section",
"fieldtype": "Section Break"
},
{
"fieldname": "bom_conf_tab",
"fieldtype": "Tab Break",
"label": "BOM Configuration"
},
{
"fieldname": "consume_components_section",
"fieldtype": "Section Break",
"label": "Consume Components"
},
{
"description": "Controls how raw materials are consumed during the \u2018Manufacture\u2019 stock entry.",
"fieldname": "backflush_based_on",
"fieldtype": "Select",
"label": "Based On",
"options": "\nBOM\nMaterial Transferred for Manufacture",
"show_description_on_click": 1
}
],
"icon": "fa fa-sitemap",
@@ -732,7 +761,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2026-02-26 14:13:34.040181",
"modified": "2026-04-17 15:22:33.598938",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",

View File

@@ -118,6 +118,7 @@ class BOM(WebsiteGenerator):
allow_alternative_item: DF.Check
amended_from: DF.Link | None
backflush_based_on: DF.Literal["", "BOM", "Material Transferred for Manufacture"]
base_operating_cost: DF.Currency
base_raw_material_cost: DF.Currency
base_secondary_items_cost: DF.Currency
@@ -1999,3 +2000,16 @@ def get_secondary_items_from_sub_assemblies(bom_no, company, qty, secondary_item
get_secondary_items_from_sub_assemblies(row.bom_no, company, qty, secondary_items)
return secondary_items
def get_backflush_based_on(bom_no):
backflush_based_on = None
if bom_no:
backflush_based_on = frappe.get_cached_value("BOM", bom_no, "backflush_based_on")
if not backflush_based_on:
backflush_based_on = frappe.db.get_single_value(
"Manufacturing Settings", "backflush_raw_materials_based_on"
)
return backflush_based_on

View File

@@ -2876,6 +2876,9 @@ def make_bom(**args):
}
)
if args.backflush_based_on:
bom.backflush_based_on = args.backflush_based_on
if args.operating_cost_per_bom_quantity:
bom.fg_based_operating_cost = 1
bom.operating_cost_per_bom_quantity = args.operating_cost_per_bom_quantity

View File

@@ -4240,6 +4240,66 @@ class TestWorkOrder(ERPNextTestSuite):
self.assertEqual(wo_order.operations[0].time_in_mins, 72)
self.assertEqual(wo_order.operations[1].time_in_mins, 240)
def test_backflush_based_on_in_bom(self):
raw_material_1 = make_item(item_code="BOM RM 1", properties={"is_stock_item": 1}).name
raw_material_2 = make_item(item_code="BOM RM 2", properties={"is_stock_item": 1}).name
fg_item = make_item(item_code="BOM FG 1", properties={"is_stock_item": 1}).name
frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")
backflush_based_on = frappe.db.get_single_value(
"Manufacturing Settings", "backflush_raw_materials_based_on"
)
self.assertEqual(backflush_based_on, "BOM")
for item_code in [raw_material_1, raw_material_2]:
test_stock_entry.make_stock_entry(
item_code=item_code, target="Stores - _TC", qty=1, basic_rate=100
)
bom = make_bom(
item=fg_item,
quantity=1,
raw_materials=[raw_material_1],
backflush_based_on="Material Transferred for Manufacture",
)
wo_order = make_wo_order_test_record(item=fg_item, qty=1, source_warehouse="Stores - _TC")
self.assertEqual(bom.name, wo_order.bom_no)
backflush_based_on = frappe.db.get_value("BOM", wo_order.bom_no, "backflush_based_on")
self.assertEqual(backflush_based_on, "Material Transferred for Manufacture")
material_transfer_entry = frappe.get_doc(
make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 1)
)
material_transfer_entry.save()
# Add second raw material in the material transfer entry which is not in the BOM to simulate backflush based on material transfer scenario
material_transfer_entry.append(
"items",
{
"item_code": raw_material_2,
"item_name": raw_material_2,
"item_group": frappe.get_value("Item", raw_material_2, "item_group"),
"uom": frappe.get_value("Item", raw_material_2, "stock_uom"),
"conversion_factor": 1,
"s_warehouse": "Stores - _TC",
"t_warehouse": material_transfer_entry.items[0].t_warehouse,
"qty": 1,
},
)
material_transfer_entry.submit()
manufacture_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 1))
manufacture_entry.save()
self.assertEqual(len(manufacture_entry.items), 3)
for row in manufacture_entry.items:
if row.s_warehouse:
self.assertIn(row.item_code, [raw_material_1, raw_material_2])
def get_reserved_entries(voucher_no, warehouse=None):
doctype = frappe.qb.DocType("Stock Reservation Entry")

View File

@@ -242,6 +242,11 @@ frappe.ui.form.on("Work Order", {
frm.trigger("hide_reserve_stock_button");
frm.trigger("toggle_items_editable");
frm.trigger("set_fg_warehouse_mandatory");
frm.trigger("toggle_hide_fields");
},
toggle_hide_fields(frm) {
frm.toggle_display("operations", frm.doc?.operations && frm.doc.operations.length > 0);
},
skip_transfer(frm) {
@@ -638,6 +643,8 @@ frappe.ui.form.on("Work Order", {
if (r.message["set_scrap_wh_mandatory"]) {
frm.toggle_reqd("scrap_warehouse", true);
}
frm.trigger("toggle_hide_fields");
},
});
},

View File

@@ -15,7 +15,6 @@
"column_break1",
"qty",
"sales_order",
"track_semi_finished_goods",
"reserve_stock",
"section_break_vrpa",
"max_producible_qty",
@@ -86,6 +85,7 @@
"product_bundle_item",
"section_break_ynih",
"status",
"track_semi_finished_goods",
"column_break_cvuw",
"amended_from",
"connections_tab"
@@ -608,6 +608,7 @@
"fetch_from": "bom_no.track_semi_finished_goods",
"fieldname": "track_semi_finished_goods",
"fieldtype": "Check",
"hidden": 1,
"label": "Track Semi Finished Goods",
"read_only": 1
},
@@ -705,7 +706,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2026-03-16 10:15:28.708688",
"modified": "2026-04-17 13:42:12.374055",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",

View File

@@ -162,6 +162,10 @@ class WorkOrder(Document):
frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"),
)
if self.bom_no:
if based_on := frappe.get_cached_value("BOM", self.bom_no, "backflush_based_on"):
self.set_onload("backflush_raw_materials_based_on", based_on)
def show_create_job_card_button(self):
operation_details = frappe._dict(
frappe.get_all(

View File

@@ -2541,14 +2541,12 @@ class StockEntry(StockController, SubcontractingInwardController):
frappe.throw(_("Posting date and posting time is mandatory"))
self.set_work_order_details()
self.flags.backflush_based_on = frappe.db.get_single_value(
backflush_based_on = frappe.db.get_single_value(
"Manufacturing Settings", "backflush_raw_materials_based_on"
)
if self.bom_no:
backflush_based_on = frappe.db.get_single_value(
"Manufacturing Settings", "backflush_raw_materials_based_on"
)
backflush_based_on = self.get_backflush_based_on()
if self.purpose in [
"Material Issue",
@@ -2573,7 +2571,7 @@ class StockEntry(StockController, SubcontractingInwardController):
or self.purpose == "Material Consumption for Manufacture"
)
and not self.pro_doc.skip_transfer
and self.flags.backflush_based_on == "Material Transferred for Manufacture"
and backflush_based_on == "Material Transferred for Manufacture"
):
self.add_transfered_raw_materials_in_items()
@@ -2583,7 +2581,7 @@ class StockEntry(StockController, SubcontractingInwardController):
self.purpose == "Manufacture"
or self.purpose == "Material Consumption for Manufacture"
)
and self.flags.backflush_based_on == "BOM"
and backflush_based_on == "BOM"
and frappe.db.get_single_value("Manufacturing Settings", "material_consumption") == 1
):
self.get_unconsumed_raw_materials()
@@ -2653,8 +2651,7 @@ class StockEntry(StockController, SubcontractingInwardController):
if (
self.purpose not in ["Material Transfer for Manufacture"]
and frappe.db.get_single_value("Manufacturing Settings", "backflush_raw_materials_based_on")
!= "BOM"
and self.get_backflush_based_on() != "BOM"
and not skip_transfer
):
return
@@ -2731,6 +2728,11 @@ class StockEntry(StockController, SubcontractingInwardController):
row.idx = idx
self.set("items", sorted_items)
def get_backflush_based_on(self):
from erpnext.manufacturing.doctype.bom.bom import get_backflush_based_on
return get_backflush_based_on(self.bom_no)
def get_available_reserved_materials(self):
reserved_entries = self.get_reserved_materials()
if not reserved_entries:

View File

@@ -114,6 +114,10 @@ class ManufactureEntry:
"Manufacturing Settings", "backflush_raw_materials_based_on"
)
if self.bom_no:
if based_on := frappe.get_cached_value("BOM", self.bom_no, "backflush_based_on"):
backflush_based_on = based_on
available_serial_batches = frappe._dict({})
if backflush_based_on != "BOM":
available_serial_batches = self.get_transferred_serial_batches()