mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-25 17:48:30 +00:00
Merge pull request #54350 from rohitwaghchaure/feat-backflush-based-on-in-bom
feat: backflush based on in BOM
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user