Skip to content

Commit

Permalink
fix: validate component quantity according to BOM (backport #43011) (#…
Browse files Browse the repository at this point in the history
…43014)

* fix: validate component quantity according to BOM (#43011)

(cherry picked from commit f3b91d4)

# Conflicts:
#	erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json

* chore: fix conflicts

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
  • Loading branch information
mergify[bot] and rohitwaghchaure authored Sep 2, 2024
1 parent 23d9114 commit fee2255
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,17 @@
"document_type": "Document",
"engine": "InnoDB",
"field_order": [
"bom_and_work_order_tab",
"raw_materials_consumption_section",
"material_consumption",
"get_rm_cost_from_consumption_entry",
"column_break_3",
"backflush_raw_materials_based_on",
"capacity_planning",
"disable_capacity_planning",
"allow_overtime",
"allow_production_on_holidays",
"column_break_5",
"capacity_planning_for_days",
"mins_between_operations",
"validate_components_quantities_per_bom",
"bom_section",
"update_bom_costs_automatically",
"column_break_lhyt",
"manufacture_sub_assembly_in_operation",
"section_break_6",
"default_wip_warehouse",
"default_fg_warehouse",
Expand All @@ -30,8 +29,14 @@
"add_corrective_operation_cost_in_finished_good_valuation",
"column_break_24",
"job_card_excess_transfer",
"capacity_planning",
"disable_capacity_planning",
"allow_overtime",
"allow_production_on_holidays",
"column_break_5",
"capacity_planning_for_days",
"mins_between_operations",
"other_settings_section",
"update_bom_costs_automatically",
"set_op_cost_and_scrape_from_sub_assemblies",
"column_break_23",
"make_serial_no_batch_from_work_order"
Expand Down Expand Up @@ -149,7 +154,7 @@
{
"fieldname": "raw_materials_consumption_section",
"fieldtype": "Section Break",
"label": "Raw Materials Consumption"
"label": "Raw Materials Consumption "
},
{
"fieldname": "column_break_16",
Expand Down Expand Up @@ -183,8 +188,8 @@
},
{
"fieldname": "job_card_section",
"fieldtype": "Section Break",
"label": "Job Card"
"fieldtype": "Tab Break",
"label": "Job Card and Capacity Planning"
},
{
"fieldname": "column_break_24",
Expand All @@ -210,13 +215,41 @@
"fieldname": "get_rm_cost_from_consumption_entry",
"fieldtype": "Check",
"label": "Get Raw Materials Cost from Consumption Entry"
},
{
"fieldname": "bom_and_work_order_tab",
"fieldtype": "Tab Break",
"label": "BOM and Production"
},
{
"fieldname": "bom_section",
"fieldtype": "Section Break",
"label": "BOM"
},
{
"fieldname": "column_break_lhyt",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "If enabled then system will manufacture Sub-assembly against the Job Card (operation).",
"fieldname": "manufacture_sub_assembly_in_operation",
"fieldtype": "Check",
"label": "Manufacture Sub-assembly in Operation"
},
{
"default": "0",
"depends_on": "eval:doc.backflush_raw_materials_based_on == \"BOM\"",
"fieldname": "validate_components_quantities_per_bom",
"fieldtype": "Check",
"label": "Validate Components Quantities Per BOM"
}
],
"icon": "icon-wrench",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2024-02-08 19:00:37.561244",
"modified": "2024-09-02 12:12:03.132567",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing Settings",
Expand All @@ -234,4 +267,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,22 @@ class ManufacturingSettings(Document):
get_rm_cost_from_consumption_entry: DF.Check
job_card_excess_transfer: DF.Check
make_serial_no_batch_from_work_order: DF.Check
manufacture_sub_assembly_in_operation: DF.Check
material_consumption: DF.Check
mins_between_operations: DF.Int
overproduction_percentage_for_sales_order: DF.Percent
overproduction_percentage_for_work_order: DF.Percent
set_op_cost_and_scrape_from_sub_assemblies: DF.Check
update_bom_costs_automatically: DF.Check
validate_components_quantities_per_bom: DF.Check
# end: auto-generated types

pass
def before_save(self):
self.reset_values()

def reset_values(self):
if self.backflush_raw_materials_based_on != "BOM" and self.validate_components_quantities_per_bom:
self.validate_components_quantities_per_bom = 0


def get_mins_between_operations():
Expand Down
53 changes: 53 additions & 0 deletions erpnext/manufacturing/doctype/work_order/test_work_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -2102,6 +2102,59 @@ def test_disassemby_order(self):

stock_entry.submit()

def test_components_qty_for_bom_based_manufacture_entry(self):
frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")
frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 1)

fg_item = "Test FG Item For Component Validation"
source_warehouse = "Stores - _TC"
raw_materials = ["Test Component Validation RM Item 1", "Test Component Validation RM Item 2"]

make_item(fg_item, {"is_stock_item": 1})
for item in raw_materials:
make_item(item, {"is_stock_item": 1})
test_stock_entry.make_stock_entry(
item_code=item,
target=source_warehouse,
qty=10,
basic_rate=100,
)

make_bom(item=fg_item, source_warehouse=source_warehouse, raw_materials=raw_materials)

wo = make_wo_order_test_record(
item=fg_item,
qty=10,
source_warehouse=source_warehouse,
)

transfer_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 10))
transfer_entry.save()
for row in transfer_entry.items:
row.qty = 5

self.assertRaises(frappe.ValidationError, transfer_entry.save)

transfer_entry.reload()
for row in transfer_entry.items:
self.assertEqual(row.qty, 10)

transfer_entry.submit()

manufacture_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 10))
manufacture_entry.save()
for row in manufacture_entry.items:
if not row.s_warehouse:
continue

row.qty = 5

self.assertRaises(frappe.ValidationError, manufacture_entry.save)
manufacture_entry.reload()
manufacture_entry.submit()

frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 0)


def make_operation(**kwargs):
kwargs = frappe._dict(kwargs)
Expand Down
29 changes: 29 additions & 0 deletions erpnext/public/js/utils/serial_no_batch_selector.js
Original file line number Diff line number Diff line change
Expand Up @@ -368,8 +368,28 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
];
}

get_batch_qty(batch_no, callback) {
let warehouse = this.item.s_warehouse || this.item.t_warehouse || this.item.warehouse;
frappe.call({
method: "erpnext.stock.doctype.batch.batch.get_batch_qty",
args: {
batch_no: batch_no,
warehouse: warehouse,
item_code: this.item.item_code,
posting_date: this.frm.doc.posting_date,
posting_time: this.frm.doc.posting_time,
},
callback: (r) => {
if (r.message) {
callback(flt(r.message));
}
},
});
}

get_dialog_table_fields() {
let fields = [];
let me = this;

if (this.item.has_serial_no) {
fields.push({
Expand All @@ -395,6 +415,15 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
fieldname: "batch_no",
label: __("Batch No"),
in_list_view: 1,
change() {
let doc = this.doc;
if (!doc.qty && me.item.type_of_transaction === "Outward") {
me.get_batch_qty(doc.batch_no, (qty) => {
doc.qty = qty;
this.grid.set_value("qty", qty, doc);
});
}
},
get_query: () => {
let is_inward = false;
if (
Expand Down
29 changes: 29 additions & 0 deletions erpnext/stock/doctype/stock_entry/stock_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ def validate(self):
self.validate_serialized_batch()
self.calculate_rate_and_amount()
self.validate_putaway_capacity()
self.validate_component_quantities()

if not self.get("purpose") == "Manufacture":
# ignore scrap item wh difference and empty source/target wh
Expand Down Expand Up @@ -747,6 +748,34 @@ def set_actual_qty(self):
title=_("Insufficient Stock"),
)

def validate_component_quantities(self):
if self.purpose not in ["Manufacture", "Material Transfer for Manufacture"]:
return

if not frappe.db.get_single_value("Manufacturing Settings", "validate_components_quantities_per_bom"):
return

if not self.fg_completed_qty:
return

raw_materials = self.get_bom_raw_materials(self.fg_completed_qty)

precision = frappe.get_precision("Stock Entry Detail", "qty")
for row in self.items:
if not row.s_warehouse:
continue

if details := raw_materials.get(row.item_code):
if flt(details.get("qty"), precision) != flt(row.qty, precision):
frappe.throw(
_("For the item {0}, the quantity should be {1} according to the BOM {2}.").format(
frappe.bold(row.item_code),
flt(details.get("qty"), precision),
get_link_to_form("BOM", self.bom_no),
),
title=_("Incorrect Component Quantity"),
)

@frappe.whitelist()
def get_stock_and_rate(self):
"""
Expand Down

0 comments on commit fee2255

Please sign in to comment.