fix: do not create repeat work orders

(cherry picked from commit 384f4e120a)

# 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
This commit is contained in:
Rohit Waghchaure
2025-06-09 17:44:39 +05:30
committed by Mergify
parent 7348778220
commit 795108c1dd
6 changed files with 626 additions and 9 deletions

View File

@@ -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",

View File

@@ -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,

View File

@@ -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):
"""

View File

@@ -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",

View File

@@ -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

View File

@@ -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()