From b8f0dbab455a6a1f9f33d607f7b971254858d379 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 8 Sep 2025 20:54:43 +0530 Subject: [PATCH 1/2] feat: allow to transfer additional materials --- .../manufacturing_settings.json | 20 ++++++- .../manufacturing_settings.py | 1 + .../doctype/work_order/work_order.js | 57 +++++++++++++++---- .../doctype/work_order/work_order.py | 8 +++ .../stock/doctype/stock_entry/stock_entry.py | 17 ++++++ 5 files changed, 92 insertions(+), 11 deletions(-) diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json index 1d1a2286e57..2f8ed4a04f5 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -24,6 +24,9 @@ "overproduction_percentage_for_sales_order", "column_break_16", "overproduction_percentage_for_work_order", + "section_break_xhtl", + "transfer_extra_materials_percentage", + "column_break_kemp", "job_card_section", "add_corrective_operation_cost_in_finished_good_valuation", "enforce_time_logs", @@ -243,13 +246,28 @@ "fieldname": "enforce_time_logs", "fieldtype": "Check", "label": "Enforce Time Logs" + }, + { + "fieldname": "section_break_xhtl", + "fieldtype": "Section Break", + "label": "Extra Material Transfer" + }, + { + "fieldname": "column_break_kemp", + "fieldtype": "Column Break" + }, + { + "description": "The user will be able to transfer additional materials from the store to the Work in Progress (WIP) warehouse.", + "fieldname": "transfer_extra_materials_percentage", + "fieldtype": "Percent", + "label": "Transfer Extra Raw Materials to WIP (%)" } ], "icon": "icon-wrench", "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-05-16 11:23:16.916512", + "modified": "2025-09-08 19:48:31.726126", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing Settings", diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py index e9f011f78ba..85669c8372a 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py @@ -35,6 +35,7 @@ class ManufacturingSettings(Document): overproduction_percentage_for_sales_order: DF.Percent overproduction_percentage_for_work_order: DF.Percent set_op_cost_and_scrap_from_sub_assemblies: DF.Check + transfer_extra_materials_percentage: DF.Percent update_bom_costs_automatically: DF.Check validate_components_quantities_per_bom: DF.Check # end: auto-generated types diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 8e4f56867ab..2add6488d33 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -732,6 +732,19 @@ erpnext.work_order = { let pending_to_transfer = frm.doc.required_items.some( (item) => flt(item.transferred_qty) < flt(item.required_qty) ); + + let transfer_extra_materials_percentage = + frm.doc.__onload?.transfer_extra_materials_percentage; + let allowed_qty = 0; + let transfer_extra_materials = false; + if (!pending_to_transfer && transfer_extra_materials_percentage) { + allowed_qty = frm.doc.qty + (transfer_extra_materials_percentage / 100) * frm.doc.qty; + + if (allowed_qty > frm.doc.material_transferred_for_manufacturing) { + transfer_extra_materials = true; + } + } + if (pending_to_transfer && frm.doc.status != "Stopped") { frm.has_start_btn = true; frm.add_custom_button(__("Create Pick List"), function () { @@ -742,6 +755,14 @@ erpnext.work_order = { erpnext.work_order.make_se(frm, "Material Transfer for Manufacture"); }); start_btn.addClass("btn-primary"); + } else if (transfer_extra_materials && allowed_qty) { + let qty = allowed_qty - flt(frm.doc.material_transferred_for_manufacturing); + + if (qty > 0) { + frm.add_custom_button(__("Transfer Extra Material"), function () { + erpnext.work_order.make_se(frm, "Material Transfer for Manufacture", qty); + }); + } } } } @@ -967,19 +988,35 @@ erpnext.work_order = { }); }, - make_se: function (frm, purpose) { - this.show_prompt_for_qty_input(frm, purpose) - .then((data) => { - return frappe.xcall("erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", { + make_se: function (frm, purpose, qty) { + if (qty) { + frappe + .xcall("erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", { work_order_id: frm.doc.name, purpose: purpose, - qty: data.qty, + qty: qty, + }) + .then((stock_entry) => { + frappe.model.sync(stock_entry); + frappe.set_route("Form", stock_entry.doctype, stock_entry.name); }); - }) - .then((stock_entry) => { - frappe.model.sync(stock_entry); - frappe.set_route("Form", stock_entry.doctype, stock_entry.name); - }); + } else { + this.show_prompt_for_qty_input(frm, purpose) + .then((data) => { + return frappe.xcall( + "erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", + { + work_order_id: frm.doc.name, + purpose: purpose, + qty: data.qty, + } + ); + }) + .then((stock_entry) => { + frappe.model.sync(stock_entry); + frappe.set_route("Form", stock_entry.doctype, stock_entry.name); + }); + } }, create_pick_list: function (frm, purpose = "Material Transfer for Manufacture") { diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 6c3f6725273..8f9991e21a2 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -149,6 +149,7 @@ class WorkOrder(Document): self.set_onload("material_consumption", ms.material_consumption) self.set_onload("backflush_raw_materials_based_on", ms.backflush_raw_materials_based_on) self.set_onload("overproduction_percentage", ms.overproduction_percentage_for_work_order) + self.set_onload("transfer_extra_materials_percentage", ms.transfer_extra_materials_percentage) self.set_onload("show_create_job_card_button", self.show_create_job_card_button()) self.set_onload( "enable_stock_reservation", @@ -485,6 +486,13 @@ class WorkOrder(Document): qty = self.get_transferred_or_manufactured_qty(purpose) + if not allowance_percentage and purpose == "Material Transfer for Manufacture": + allowance_percentage = flt( + frappe.db.get_single_value( + "Manufacturing Settings", "transfer_extra_materials_percentage" + ) + ) + completed_qty = self.qty + (allowance_percentage / 100 * self.qty) if qty > completed_qty: frappe.throw( diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index efe4468c68b..442a2bb5f7c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2640,10 +2640,16 @@ class StockEntry(StockController): frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order") ) + transfer_extra_materials_percentage = flt( + frappe.db.get_single_value("Manufacturing Settings", "transfer_extra_materials_percentage") + ) + to_transfer_qty = flt(self.pro_doc.material_transferred_for_manufacturing) + flt( self.fg_completed_qty ) transfer_limit_qty = max_qty + ((max_qty * overproduction_percentage) / 100) + if transfer_extra_materials_percentage: + transfer_limit_qty = max_qty + ((max_qty * transfer_extra_materials_percentage) / 100) if transfer_limit_qty >= to_transfer_qty: allow_overproduction = True @@ -2693,11 +2699,22 @@ class StockEntry(StockController): else: wip_warehouse = None + transfer_extra_materials_percentage = flt( + frappe.db.get_single_value("Manufacturing Settings", "transfer_extra_materials_percentage") + ) + for d in work_order.get("required_items"): if consider_job_card and (d.item_code not in job_card_items): continue + additional_qty = 0.0 + if transfer_extra_materials_percentage: + additional_qty = transfer_extra_materials_percentage * flt(d.required_qty) / 100 + transfer_pending = flt(d.required_qty) > flt(d.transferred_qty) + if additional_qty: + transfer_pending = (flt(d.required_qty) + additional_qty) > flt(d.transferred_qty) + can_transfer = transfer_pending or (backflush_based_on == "Material Transferred for Manufacture") if not can_transfer: -- GitLab From 97ae282b0a03c067026c7a18360141d01826c0b8 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 9 Sep 2025 11:44:17 +0530 Subject: [PATCH 2/2] test: test case for additional material transfer --- .../doctype/work_order/test_work_order.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 54aaae61615..a6500fd9a36 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -3005,6 +3005,36 @@ class TestWorkOrder(IntegrationTestCase): wo.operations[3].planned_start_time, add_to_date(wo.operations[1].planned_end_time, minutes=10) ) + def test_allow_additional_material_transfer(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import ( + make_stock_entry as make_stock_entry_test_record, + ) + + frappe.db.set_single_value("Manufacturing Settings", "transfer_extra_materials_percentage", 50) + wo_order = make_wo_order_test_record(planned_start_date=now(), qty=2) + for row in wo_order.required_items: + make_stock_entry_test_record( + item_code=row.item_code, + target=row.source_warehouse, + qty=row.required_qty * 2, + basic_rate=100, + ) + + stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 2)) + stock_entry.insert() + stock_entry.submit() + + wo_order.reload() + self.assertEqual(wo_order.material_transferred_for_manufacturing, 2) + + stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 1)) + stock_entry.insert() + stock_entry.submit() + + wo_order.reload() + self.assertEqual(wo_order.material_transferred_for_manufacturing, 3) + frappe.db.set_single_value("Manufacturing Settings", "transfer_extra_materials_percentage", 0) + def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import ( -- GitLab