From 384f4e120a0df9eb0e88f580a077a4ed1ccf55df Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 9 Jun 2025 17:44:39 +0530 Subject: [PATCH] fix: do not create repeat work orders --- .../production_plan/production_plan.js | 22 ++++++- .../production_plan/production_plan.py | 9 +++ .../production_plan/test_production_plan.py | 57 +++++++++++++++++++ .../production_plan_sub_assembly_item.json | 10 +++- .../production_plan_sub_assembly_item.py | 1 + .../doctype/work_order/work_order.py | 28 ++++++--- 6 files changed, 117 insertions(+), 10 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 63ead3d4e26..918be507d3a 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -126,7 +126,9 @@ frappe.ui.form.on("Production Plan", { ); } - if (frm.doc.po_items && frm.doc.status !== "Closed") { + let items = frm.events.get_items_for_work_order(frm); + + if (items?.length && frm.doc.status !== "Closed") { frm.add_custom_button( __("Work Order / Subcontract PO"), () => { @@ -207,6 +209,24 @@ frappe.ui.form.on("Production Plan", { set_field_options("projected_qty_formula", projected_qty_formula); }, + get_items_for_work_order(frm) { + let items = frm.doc.po_items; + if (frm.doc.sub_assembly_items?.length) { + items = [...items, ...frm.doc.sub_assembly_items]; + } + + let has_items = + items.filter((item) => { + if (item.pending_qty) { + return item.pending_qty > item.ordered_qty; + } else { + return item.qty > (item.received_qty || item.ordered_qty); + } + }) || []; + + return has_items; + }, + has_unreserved_stock(frm, table, qty_field = "required_qty") { let has_unreserved_stock = frm.doc[table].some( (item) => flt(item[qty_field]) > flt(item.stock_reserved_qty) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 8e40804e761..bd84148b600 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -778,7 +778,14 @@ class ProductionPlan(Document): "company": self.get("company"), } + if flt(row.qty) <= flt(row.ordered_qty): + continue + self.prepare_data_for_sub_assembly_items(row, work_order_data) + + if work_order_data.get("qty") <= 0: + continue + work_order = self.create_work_order(work_order_data) if work_order: wo_list.append(work_order) @@ -798,6 +805,8 @@ class ProductionPlan(Document): if row.get(field): wo_data[field] = row.get(field) + wo_data["qty"] = flt(row.get("qty")) - flt(row.get("ordered_qty")) + wo_data.update( { "use_multi_level_bom": 0, diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 2010161170b..b8e30a43015 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -2336,6 +2336,63 @@ class TestProductionPlan(IntegrationTestCase): self.assertTrue(len(reserved_entries) == 0) frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0) + def test_production_plan_for_partial_sub_assembly_items(self): + from erpnext.controllers.status_updater import OverAllowanceError + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + from erpnext.subcontracting.doctype.subcontracting_bom.test_subcontracting_bom import ( + create_subcontracting_bom, + ) + + frappe.flags.test_print = False + + fg_wo_item = "Test Motherboard 11" + bom_tree_1 = {"Test Laptop 11": {fg_wo_item: {"Test Motherboard Wires 11": {}}}} + create_nested_bom(bom_tree_1, prefix="") + + plan = create_production_plan( + item_code="Test Laptop 11", + planned_qty=10, + use_multi_level_bom=1, + do_not_submit=True, + company="_Test Company", + skip_getting_mr_items=True, + ) + plan.get_sub_assembly_items() + plan.submit() + plan.make_work_order() + + work_order = frappe.db.get_value("Work Order", {"production_plan": plan.name, "docstatus": 0}, "name") + wo_doc = frappe.get_doc("Work Order", work_order) + + wo_doc.qty = 5.0 + wo_doc.skip_transfer = 1 + wo_doc.from_wip_warehouse = 1 + wo_doc.wip_warehouse = "_Test Warehouse - _TC" + wo_doc.fg_warehouse = "_Test Warehouse - _TC" + wo_doc.submit() + + plan.reload() + + for row in plan.sub_assembly_items: + self.assertEqual(row.ordered_qty, 5.0) + + plan.make_work_order() + + work_order = frappe.db.get_value("Work Order", {"production_plan": plan.name, "docstatus": 0}, "name") + wo_doc = frappe.get_doc("Work Order", work_order) + self.assertEqual(wo_doc.qty, 5.0) + + wo_doc.skip_transfer = 1 + wo_doc.from_wip_warehouse = 1 + wo_doc.wip_warehouse = "_Test Warehouse - _TC" + wo_doc.fg_warehouse = "_Test Warehouse - _TC" + wo_doc.submit() + + plan.reload() + + for row in plan.sub_assembly_items: + self.assertEqual(row.ordered_qty, 10.0) + def create_production_plan(**args): """ diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json index 6a5d7dcb3d2..0dfa29b8ddd 100644 --- a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json +++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json @@ -26,6 +26,7 @@ "wo_produced_qty", "stock_reserved_qty", "column_break_7", + "ordered_qty", "received_qty", "indent", "section_break_19", @@ -231,13 +232,20 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "ordered_qty", + "fieldtype": "Float", + "label": "Ordered Qty", + "no_copy": 1, + "read_only": 1 } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-05-01 14:28:35.979941", + "modified": "2025-06-10 13:36:24.759101", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan Sub Assembly Item", diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py index 41e5bf28b56..5cdcc6cf118 100644 --- a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py +++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py @@ -22,6 +22,7 @@ class ProductionPlanSubAssemblyItem(Document): fg_warehouse: DF.Link | None indent: DF.Int item_name: DF.Data | None + ordered_qty: DF.Float parent: DF.Data parent_item_code: DF.Link | None parentfield: DF.Data diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 969375c36bf..01387be446c 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -845,22 +845,34 @@ class WorkOrder(Document): ) def update_ordered_qty(self): - if self.production_plan and self.production_plan_item and not self.production_plan_sub_assembly_item: + if self.production_plan and (self.production_plan_item or self.production_plan_sub_assembly_item): table = frappe.qb.DocType("Work Order") query = ( frappe.qb.from_(table) .select(Sum(table.qty)) - .where( - (table.production_plan == self.production_plan) - & (table.production_plan_item == self.production_plan_item) - & (table.docstatus == 1) - ) - ).run() + .where((table.production_plan == self.production_plan) & (table.docstatus == 1)) + ) + if self.production_plan_item: + query = query.where(table.production_plan_item == self.production_plan_item) + elif self.production_plan_sub_assembly_item: + query = query.where( + table.production_plan_sub_assembly_item == self.production_plan_sub_assembly_item + ) + + query = query.run() qty = flt(query[0][0]) if query else 0 - frappe.db.set_value("Production Plan Item", self.production_plan_item, "ordered_qty", qty) + if self.production_plan_item: + frappe.db.set_value("Production Plan Item", self.production_plan_item, "ordered_qty", qty) + elif self.production_plan_sub_assembly_item: + frappe.db.set_value( + "Production Plan Sub Assembly Item", + self.production_plan_sub_assembly_item, + "ordered_qty", + qty, + ) doc = frappe.get_doc("Production Plan", self.production_plan) doc.set_status()