From 795108c1ddaa607d8b47bde8cdc403a823c5e30c 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 (cherry picked from commit 384f4e120a0df9eb0e88f580a077a4ed1ccf55df) # Conflicts: # erpnext/manufacturing/doctype/production_plan/production_plan.js # erpnext/manufacturing/doctype/production_plan/test_production_plan.py # erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json --- .../production_plan/production_plan.js | 91 +++- .../production_plan/production_plan.py | 9 + .../production_plan/test_production_plan.py | 471 ++++++++++++++++++ .../production_plan_sub_assembly_item.json | 35 ++ .../production_plan_sub_assembly_item.py | 1 + .../doctype/work_order/work_order.py | 28 +- 6 files changed, 626 insertions(+), 9 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index f381c9d39e2..5072b130b2c 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -116,7 +116,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"), () => { @@ -193,6 +195,93 @@ frappe.ui.form.on("Production Plan", { set_field_options("projected_qty_formula", projected_qty_formula); }, +<<<<<<< HEAD +======= + 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) + ); + + return has_unreserved_stock; + }, + + has_reserved_stock(frm, table) { + let has_reserved_stock = frm.doc[table].some((item) => flt(item.stock_reserved_qty) > 0); + + return has_reserved_stock; + }, + + setup_stock_reservation_for_sub_assembly(frm) { + if (frm.doc.docstatus === 1 && frm.doc.reserve_stock) { + if (frm.events.has_unreserved_stock(frm, "sub_assembly_items")) { + frm.add_custom_button( + __("Reserve for Sub-assembly"), + () => erpnext.stock_reservation.make_entries(frm, "sub_assembly_items"), + __("Stock Reservation") + ); + } + + if (frm.events.has_reserved_stock(frm, "sub_assembly_items")) { + frm.add_custom_button( + __("Unreserve for Sub-assembly"), + () => erpnext.stock_reservation.unreserve_stock(frm), + __("Stock Reservation") + ); + + frm.add_custom_button( + __("Reserved Stock for Sub-assembly"), + () => erpnext.stock_reservation.show_reserved_stock(frm, "sub_assembly_items"), + __("Stock Reservation") + ); + } + } + }, + + setup_stock_reservation_for_raw_materials(frm) { + if (frm.doc.docstatus === 1 && frm.doc.reserve_stock) { + if (frm.events.has_unreserved_stock(frm, "mr_items", "required_bom_qty")) { + frm.add_custom_button( + __("Reserve for Raw Materials"), + () => erpnext.stock_reservation.make_entries(frm, "mr_items"), + __("Stock Reservation") + ); + } + + if (frm.events.has_reserved_stock(frm, "mr_items")) { + frm.add_custom_button( + __("Unreserve for Raw Materials"), + () => erpnext.stock_reservation.unreserve_stock(frm), + __("Stock Reservation") + ); + + frm.add_custom_button( + __("Reserved Stock for Raw Materials"), + () => erpnext.stock_reservation.show_reserved_stock(frm, "mr_items"), + __("Stock Reservation") + ); + } + } + }, + +>>>>>>> 384f4e120a (fix: do not create repeat work orders) close_open_production_plan(frm, close = false) { frappe.call({ method: "set_status", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index dbcf07bbccf..c6b6b7e1d9d 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -751,7 +751,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) @@ -771,6 +778,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 e5b60c7a4e6..0bac38dd47d 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1693,6 +1693,477 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(mr_items[0].get("quantity"), 80) self.assertEqual(mr_items[1].get("quantity"), 70) +<<<<<<< HEAD +======= + def test_stock_reservation_against_production_plan(self): + from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + from erpnext.stock.doctype.material_request.material_request import make_purchase_order + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1) + + bom_tree = { + "Finished Good For SR": { + "Sub Assembly For SR 1": {"Raw Material For SR 1": {}}, + "Sub Assembly For SR 2": {"Raw Material For SR 2": {}}, + "Sub Assembly For SR 3": {"Raw Material For SR 3": {}}, + } + } + parent_bom = create_nested_bom(bom_tree, prefix="") + + warehouse = "_Test Warehouse - _TC" + + for item_code in [ + "Sub Assembly For SR 1", + "Sub Assembly For SR 2", + "Sub Assembly For SR 3", + "Raw Material For SR 1", + "Raw Material For SR 2", + "Raw Material For SR 3", + ]: + make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100) + + plan = create_production_plan( + item_code=parent_bom.item, + planned_qty=15, + skip_available_sub_assembly_item=1, + ignore_existing_ordered_qty=1, + do_not_submit=1, + warehouse=warehouse, + sub_assembly_warehouse=warehouse, + for_warehouse=warehouse, + reserve_stock=1, + ) + + plan.get_sub_assembly_items() + plan.set("mr_items", []) + mr_items = get_items_for_material_requests(plan.as_dict()) + for d in mr_items: + plan.append("mr_items", d) + + plan.save() + + self.assertTrue(len(plan.sub_assembly_items) == 3) + for row in plan.sub_assembly_items: + self.assertEqual(row.required_qty, 15.0) + self.assertEqual(row.qty, 10.0) + + self.assertTrue(len(plan.mr_items) == 3) + for row in plan.mr_items: + self.assertEqual(row.required_bom_qty, 10.0) + self.assertEqual(row.quantity, 5.0) + + plan.submit() + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 6) + + for row in reserved_entries: + self.assertEqual(row.reserved_qty, 5.0) + + plan.submit_material_request = 1 + plan.make_material_request() + plan.make_work_order() + + material_requests = frappe.get_all( + "Material Request", filters={"production_plan": plan.name}, pluck="name" + ) + + self.assertTrue(len(material_requests) > 0) + for mr_name in list(set(material_requests)): + po = make_purchase_order(mr_name) + po.supplier = "_Test Supplier" + po.submit() + + pr = make_purchase_receipt(po.name) + pr.submit() + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 9) + + work_orders = frappe.get_all("Work Order", filters={"production_plan": plan.name}, pluck="name") + for wo_name in list(set(work_orders)): + wo_doc = frappe.get_doc("Work Order", wo_name) + self.assertEqual(wo_doc.reserve_stock, 1) + + wo_doc.source_warehouse = warehouse + wo_doc.wip_warehouse = warehouse + wo_doc.fg_warehouse = warehouse + wo_doc.submit() + + sre = StockReservation(wo_doc) + reserved_entries = sre.get_reserved_entries("Work Order", wo_doc.name) + if wo_doc.production_item == "Finished Good For SR": + self.assertEqual(len(reserved_entries), 3) + else: + # For raw materials 2 stock reservation entries + # 5 qty was present already in stock and 5 added from new PO + self.assertEqual(len(reserved_entries), 2) + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 0) + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0) + + def test_stock_reservation_of_serial_nos_against_production_plan(self): + from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + from erpnext.stock.doctype.material_request.material_request import make_purchase_order + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1) + + bom_tree = { + "Finished Good For SR": { + "SN Sub Assembly For SR 1": {"SN Raw Material For SR 1": {}}, + "SN Sub Assembly For SR 2": {"SN Raw Material For SR 2": {}}, + "SN Sub Assembly For SR 3": {"SN Raw Material For SR 3": {}}, + } + } + parent_bom = create_nested_bom(bom_tree, prefix="") + + warehouse = "_Test Warehouse - _TC" + + for item_code in [ + "SN Sub Assembly For SR 1", + "SN Sub Assembly For SR 2", + "SN Sub Assembly For SR 3", + "SN Raw Material For SR 1", + "SN Raw Material For SR 2", + "SN Raw Material For SR 3", + ]: + doc = frappe.get_doc("Item", item_code) + doc.has_serial_no = 1 + doc.serial_no_series = f"SNN-{item_code}.-.#####" + doc.save() + + make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100) + + plan = create_production_plan( + item_code=parent_bom.item, + planned_qty=15, + skip_available_sub_assembly_item=1, + ignore_existing_ordered_qty=1, + do_not_submit=1, + warehouse=warehouse, + sub_assembly_warehouse=warehouse, + for_warehouse=warehouse, + reserve_stock=1, + ) + + plan.get_sub_assembly_items() + plan.set("mr_items", []) + mr_items = get_items_for_material_requests(plan.as_dict()) + for d in mr_items: + plan.append("mr_items", d) + + plan.save() + + self.assertTrue(len(plan.sub_assembly_items) == 3) + for row in plan.sub_assembly_items: + self.assertEqual(row.required_qty, 15.0) + self.assertEqual(row.qty, 10.0) + + self.assertTrue(len(plan.mr_items) == 3) + for row in plan.mr_items: + self.assertEqual(row.required_bom_qty, 10.0) + self.assertEqual(row.quantity, 5.0) + + plan.submit() + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 6) + + for row in reserved_entries: + self.assertEqual(row.reserved_qty, 5.0) + + plan.submit_material_request = 1 + plan.make_material_request() + plan.make_work_order() + + material_requests = frappe.get_all( + "Material Request", filters={"production_plan": plan.name}, pluck="name" + ) + + additional_serial_nos = [] + + for item_code in [ + "SN Sub Assembly For SR 1", + "SN Sub Assembly For SR 2", + "SN Sub Assembly For SR 3", + "SN Raw Material For SR 1", + "SN Raw Material For SR 2", + "SN Raw Material For SR 3", + ]: + se = make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100) + additional_serial_nos.extend(get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)) + + self.assertTrue(additional_serial_nos) + + self.assertTrue(len(material_requests) > 0) + for mr_name in list(set(material_requests)): + po = make_purchase_order(mr_name) + po.supplier = "_Test Supplier" + po.submit() + + pr = make_purchase_receipt(po.name) + pr.submit() + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 9) + serial_nos_res_for_pp = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": ("in", [x.name for x in reserved_entries]), "docstatus": 1}, + pluck="serial_no", + ) + + work_orders = frappe.get_all("Work Order", filters={"production_plan": plan.name}, pluck="name") + for wo_name in list(set(work_orders)): + wo_doc = frappe.get_doc("Work Order", wo_name) + self.assertEqual(wo_doc.reserve_stock, 1) + + wo_doc.source_warehouse = warehouse + wo_doc.wip_warehouse = warehouse + wo_doc.fg_warehouse = warehouse + wo_doc.submit() + + sre = StockReservation(wo_doc) + reserved_entries = sre.get_reserved_entries("Work Order", wo_doc.name) + serial_nos_res_for_wo = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": ("in", [x.name for x in reserved_entries]), "docstatus": 1}, + pluck="serial_no", + ) + + for serial_no in serial_nos_res_for_wo: + self.assertTrue(serial_no in serial_nos_res_for_pp) + self.assertFalse(serial_no in additional_serial_nos) + + if wo_doc.production_item == "Finished Good For SR": + self.assertEqual(len(reserved_entries), 3) + else: + # For raw materials 2 stock reservation entries + # 5 qty was present already in stock and 5 added from new PO + self.assertEqual(len(reserved_entries), 2) + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 0) + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0) + + def test_stock_reservation_of_batch_nos_against_production_plan(self): + from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + from erpnext.stock.doctype.material_request.material_request import make_purchase_order + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1) + + bom_tree = { + "Finished Good For SR": { + "Batch Sub Assembly For SR 1": {"Batch Raw Material For SR 1": {}}, + "Batch Sub Assembly For SR 2": {"Batch Raw Material For SR 2": {}}, + "Batch Sub Assembly For SR 3": {"Batch Raw Material For SR 3": {}}, + } + } + parent_bom = create_nested_bom(bom_tree, prefix="") + + warehouse = "_Test Warehouse - _TC" + + for item_code in [ + "Batch Sub Assembly For SR 1", + "Batch Sub Assembly For SR 2", + "Batch Sub Assembly For SR 3", + "Batch Raw Material For SR 1", + "Batch Raw Material For SR 2", + "Batch Raw Material For SR 3", + ]: + doc = frappe.get_doc("Item", item_code) + doc.has_batch_no = 1 + doc.create_new_batch = 1 + doc.batch_number_series = f"BCH-{item_code}.-.#####" + doc.save() + + make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100) + + plan = create_production_plan( + item_code=parent_bom.item, + planned_qty=15, + skip_available_sub_assembly_item=1, + ignore_existing_ordered_qty=1, + do_not_submit=1, + warehouse=warehouse, + sub_assembly_warehouse=warehouse, + for_warehouse=warehouse, + reserve_stock=1, + ) + + plan.get_sub_assembly_items() + plan.set("mr_items", []) + mr_items = get_items_for_material_requests(plan.as_dict()) + for d in mr_items: + plan.append("mr_items", d) + + plan.save() + + self.assertTrue(len(plan.sub_assembly_items) == 3) + for row in plan.sub_assembly_items: + self.assertEqual(row.required_qty, 15.0) + self.assertEqual(row.qty, 10.0) + + self.assertTrue(len(plan.mr_items) == 3) + for row in plan.mr_items: + self.assertEqual(row.required_bom_qty, 10.0) + self.assertEqual(row.quantity, 5.0) + + plan.submit() + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 6) + + for row in reserved_entries: + self.assertEqual(row.reserved_qty, 5.0) + + plan.submit_material_request = 1 + plan.make_material_request() + plan.make_work_order() + + material_requests = frappe.get_all( + "Material Request", filters={"production_plan": plan.name}, pluck="name" + ) + + additional_batches = [] + + for item_code in [ + "Batch Sub Assembly For SR 1", + "Batch Sub Assembly For SR 2", + "Batch Sub Assembly For SR 3", + "Batch Raw Material For SR 1", + "Batch Raw Material For SR 2", + "Batch Raw Material For SR 3", + ]: + se = make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100) + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + additional_batches.append(batch_no) + + self.assertTrue(additional_batches) + + self.assertTrue(len(material_requests) > 0) + for mr_name in list(set(material_requests)): + po = make_purchase_order(mr_name) + po.supplier = "_Test Supplier" + po.submit() + + pr = make_purchase_receipt(po.name) + pr.submit() + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 9) + batches_reserved_for_pp = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": ("in", [x.name for x in reserved_entries]), "docstatus": 1}, + pluck="batch_no", + ) + + work_orders = frappe.get_all("Work Order", filters={"production_plan": plan.name}, pluck="name") + for wo_name in list(set(work_orders)): + wo_doc = frappe.get_doc("Work Order", wo_name) + self.assertEqual(wo_doc.reserve_stock, 1) + + wo_doc.source_warehouse = warehouse + wo_doc.wip_warehouse = warehouse + wo_doc.fg_warehouse = warehouse + wo_doc.submit() + + sre = StockReservation(wo_doc) + reserved_entries = sre.get_reserved_entries("Work Order", wo_doc.name) + batches_reserved_for_wo = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": ("in", [x.name for x in reserved_entries]), "docstatus": 1}, + pluck="batch_no", + ) + + for batch_no in batches_reserved_for_wo: + self.assertTrue(batch_no in batches_reserved_for_pp) + self.assertFalse(batch_no in additional_batches) + + if wo_doc.production_item == "Finished Good For SR": + self.assertEqual(len(reserved_entries), 3) + else: + # For raw materials 2 stock reservation entries + # 5 qty was present already in stock and 5 added from new PO + self.assertEqual(len(reserved_entries), 2) + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + 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) + +>>>>>>> 384f4e120a (fix: do not create repeat work orders) 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 7965965d2b6..76d52f12bcd 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 @@ -21,6 +21,7 @@ "purchase_order", "production_plan_item", "column_break_7", + "ordered_qty", "received_qty", "indent", "section_break_19", @@ -204,12 +205,46 @@ "fieldtype": "Float", "label": "Produced Qty", "read_only": 1 +<<<<<<< HEAD +======= + }, + { + "columns": 2, + "fieldname": "required_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Required Qty" + }, + { + "fieldname": "subcontracting_section", + "fieldtype": "Section Break", + "label": "Subcontracting" + }, + { + "fieldname": "stock_reserved_qty", + "fieldtype": "Float", + "label": "Stock Reserved Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "ordered_qty", + "fieldtype": "Float", + "label": "Ordered Qty", + "no_copy": 1, + "read_only": 1 +>>>>>>> 384f4e120a (fix: do not create repeat work orders) } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2024-02-27 13:45:17.422435", +======= + "modified": "2025-06-10 13:36:24.759101", +>>>>>>> 384f4e120a (fix: do not create repeat work orders) "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 ad1d655de8b..7e29675136c 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 270c23e913d..709d64cf99f 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -762,22 +762,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()