mirror of
https://github.com/frappe/erpnext.git
synced 2026-03-18 22:42:12 +00:00
* fix: validate component quantity according to BOM (#43011)
(cherry picked from commit f3b91d4d62)
# Conflicts:
# erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
* chore: fix conflicts
---------
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
This commit is contained in:
@@ -5,18 +5,17 @@
|
|||||||
"document_type": "Document",
|
"document_type": "Document",
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
|
"bom_and_work_order_tab",
|
||||||
"raw_materials_consumption_section",
|
"raw_materials_consumption_section",
|
||||||
"material_consumption",
|
"material_consumption",
|
||||||
"get_rm_cost_from_consumption_entry",
|
"get_rm_cost_from_consumption_entry",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"backflush_raw_materials_based_on",
|
"backflush_raw_materials_based_on",
|
||||||
"capacity_planning",
|
"validate_components_quantities_per_bom",
|
||||||
"disable_capacity_planning",
|
"bom_section",
|
||||||
"allow_overtime",
|
"update_bom_costs_automatically",
|
||||||
"allow_production_on_holidays",
|
"column_break_lhyt",
|
||||||
"column_break_5",
|
"manufacture_sub_assembly_in_operation",
|
||||||
"capacity_planning_for_days",
|
|
||||||
"mins_between_operations",
|
|
||||||
"section_break_6",
|
"section_break_6",
|
||||||
"default_wip_warehouse",
|
"default_wip_warehouse",
|
||||||
"default_fg_warehouse",
|
"default_fg_warehouse",
|
||||||
@@ -30,8 +29,14 @@
|
|||||||
"add_corrective_operation_cost_in_finished_good_valuation",
|
"add_corrective_operation_cost_in_finished_good_valuation",
|
||||||
"column_break_24",
|
"column_break_24",
|
||||||
"job_card_excess_transfer",
|
"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",
|
"other_settings_section",
|
||||||
"update_bom_costs_automatically",
|
|
||||||
"set_op_cost_and_scrape_from_sub_assemblies",
|
"set_op_cost_and_scrape_from_sub_assemblies",
|
||||||
"column_break_23",
|
"column_break_23",
|
||||||
"make_serial_no_batch_from_work_order"
|
"make_serial_no_batch_from_work_order"
|
||||||
@@ -149,7 +154,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "raw_materials_consumption_section",
|
"fieldname": "raw_materials_consumption_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Raw Materials Consumption"
|
"label": "Raw Materials Consumption "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_16",
|
"fieldname": "column_break_16",
|
||||||
@@ -183,8 +188,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "job_card_section",
|
"fieldname": "job_card_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Job Card"
|
"label": "Job Card and Capacity Planning"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_24",
|
"fieldname": "column_break_24",
|
||||||
@@ -210,13 +215,41 @@
|
|||||||
"fieldname": "get_rm_cost_from_consumption_entry",
|
"fieldname": "get_rm_cost_from_consumption_entry",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Get Raw Materials Cost from Consumption Entry"
|
"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",
|
"icon": "icon-wrench",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-02-08 19:00:37.561244",
|
"modified": "2024-09-02 12:12:03.132567",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Manufacturing Settings",
|
"name": "Manufacturing Settings",
|
||||||
@@ -234,4 +267,4 @@
|
|||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,15 +29,22 @@ class ManufacturingSettings(Document):
|
|||||||
get_rm_cost_from_consumption_entry: DF.Check
|
get_rm_cost_from_consumption_entry: DF.Check
|
||||||
job_card_excess_transfer: DF.Check
|
job_card_excess_transfer: DF.Check
|
||||||
make_serial_no_batch_from_work_order: DF.Check
|
make_serial_no_batch_from_work_order: DF.Check
|
||||||
|
manufacture_sub_assembly_in_operation: DF.Check
|
||||||
material_consumption: DF.Check
|
material_consumption: DF.Check
|
||||||
mins_between_operations: DF.Int
|
mins_between_operations: DF.Int
|
||||||
overproduction_percentage_for_sales_order: DF.Percent
|
overproduction_percentage_for_sales_order: DF.Percent
|
||||||
overproduction_percentage_for_work_order: DF.Percent
|
overproduction_percentage_for_work_order: DF.Percent
|
||||||
set_op_cost_and_scrape_from_sub_assemblies: DF.Check
|
set_op_cost_and_scrape_from_sub_assemblies: DF.Check
|
||||||
update_bom_costs_automatically: DF.Check
|
update_bom_costs_automatically: DF.Check
|
||||||
|
validate_components_quantities_per_bom: DF.Check
|
||||||
# end: auto-generated types
|
# 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():
|
def get_mins_between_operations():
|
||||||
|
|||||||
@@ -2102,6 +2102,59 @@ class TestWorkOrder(FrappeTestCase):
|
|||||||
|
|
||||||
stock_entry.submit()
|
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):
|
def make_operation(**kwargs):
|
||||||
kwargs = frappe._dict(kwargs)
|
kwargs = frappe._dict(kwargs)
|
||||||
|
|||||||
@@ -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() {
|
get_dialog_table_fields() {
|
||||||
let fields = [];
|
let fields = [];
|
||||||
|
let me = this;
|
||||||
|
|
||||||
if (this.item.has_serial_no) {
|
if (this.item.has_serial_no) {
|
||||||
fields.push({
|
fields.push({
|
||||||
@@ -395,6 +415,15 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
fieldname: "batch_no",
|
fieldname: "batch_no",
|
||||||
label: __("Batch No"),
|
label: __("Batch No"),
|
||||||
in_list_view: 1,
|
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: () => {
|
get_query: () => {
|
||||||
let is_inward = false;
|
let is_inward = false;
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -232,6 +232,7 @@ class StockEntry(StockController):
|
|||||||
self.validate_serialized_batch()
|
self.validate_serialized_batch()
|
||||||
self.calculate_rate_and_amount()
|
self.calculate_rate_and_amount()
|
||||||
self.validate_putaway_capacity()
|
self.validate_putaway_capacity()
|
||||||
|
self.validate_component_quantities()
|
||||||
|
|
||||||
if not self.get("purpose") == "Manufacture":
|
if not self.get("purpose") == "Manufacture":
|
||||||
# ignore scrap item wh difference and empty source/target wh
|
# ignore scrap item wh difference and empty source/target wh
|
||||||
@@ -747,6 +748,34 @@ class StockEntry(StockController):
|
|||||||
title=_("Insufficient Stock"),
|
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()
|
@frappe.whitelist()
|
||||||
def get_stock_and_rate(self):
|
def get_stock_and_rate(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user