fix(manufacturing): allow to edit batch size while creating a work order (#55058)

(cherry picked from commit 3ad67021d6)

# Conflicts:
#	erpnext/manufacturing/doctype/bom_operation/bom_operation.json
#	erpnext/manufacturing/doctype/bom_operation/bom_operation.py
#	erpnext/manufacturing/doctype/work_order/test_work_order.py
#	erpnext/manufacturing/doctype/work_order/work_order.py
#	erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
This commit is contained in:
Sudharsanan Ashok
2026-05-27 11:27:17 +05:30
committed by Mergify
parent 60fdc6bc1a
commit 96ba71c660
5 changed files with 201 additions and 5 deletions

View File

@@ -1,5 +1,6 @@
{
"actions": [],
"allow_bulk_edit": 1,
"creation": "2013-02-22 01:27:49",
"doctype": "DocType",
"document_type": "Setup",
@@ -126,11 +127,13 @@
"label": "Image"
},
{
"default": "1",
"fetch_from": "operation.batch_size",
"fetch_if_empty": 1,
"fieldname": "batch_size",
"fieldtype": "Int",
"label": "Batch Size"
"fieldtype": "Float",
"label": "Batch Size",
"non_negative": 1
},
{
"depends_on": "eval:doc.parenttype == \"Routing\" || !parent.routing",
@@ -196,7 +199,11 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
<<<<<<< HEAD
"modified": "2026-02-17 15:33:28.495850",
=======
"modified": "2026-05-25 17:15:42.044630",
>>>>>>> 3ad67021d6 (fix(manufacturing): allow to edit batch size while creating a work order (#55058))
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Operation",

View File

@@ -17,7 +17,12 @@ class BOMOperation(Document):
base_cost_per_unit: DF.Float
base_hour_rate: DF.Currency
base_operating_cost: DF.Currency
<<<<<<< HEAD
batch_size: DF.Int
=======
batch_size: DF.Float
bom_no: DF.Link | None
>>>>>>> 3ad67021d6 (fix(manufacturing): allow to edit batch size while creating a work order (#55058))
cost_per_unit: DF.Float
description: DF.TextEditor | None
fixed_time: DF.Check

View File

@@ -3631,6 +3631,178 @@ class TestWorkOrder(FrappeTestCase):
self.assertEqual(bin1_at_completion.reserved_qty_for_production, 0)
<<<<<<< HEAD
=======
def test_operating_time(self):
workstation = make_workstation(workstation="Test Workstation for Operating Time")
raw_material = make_item(item_code="Raw Material 1", properties={"is_stock_item": 1})
subassembly_item = make_item(item_code="Subassembly Item", properties={"is_stock_item": 1})
subassembly_bom = make_bom(
item=subassembly_item.name,
quantity=5,
raw_materials=[raw_material.name],
rm_qty=25,
with_operations=1,
do_not_submit=True,
)
subassembly_operation = make_operation(operation="Subassembly Operation")
subassembly_bom.append(
"operations",
{
"operation": subassembly_operation.name,
"time_in_mins": 60,
"workstation": workstation.name,
},
)
subassembly_bom.save()
subassembly_bom.submit()
fg_item = make_item(item_code="FG Item", properties={"is_stock_item": 1})
fg_bom = make_bom(
item=fg_item.name,
quantity=50,
raw_materials=[subassembly_item.name],
rm_qty=3,
with_operations=1,
do_not_submit=True,
)
fg_operation = make_operation(operation="FG Operation")
fg_operation.create_job_card_based_on_batch_size = 1
fg_operation.batch_size = 25
fg_operation.save()
fg_bom.append(
"operations",
{
"operation": fg_operation.name,
"batch_size": fg_operation.batch_size,
"time_in_mins": 60,
"workstation": workstation.name,
},
)
fg_bom.items[0].do_not_explode = 0
fg_bom.items[0].bom_no = subassembly_bom.name
fg_bom.save()
fg_bom.submit()
self.assertEqual(fg_bom.operations[0].batch_size, 25)
wo_order = make_wo_order_test_record(
item=fg_item.name,
qty=100,
use_multi_level_bom=1,
)
self.assertEqual(wo_order.operations[0].time_in_mins, 72)
self.assertEqual(wo_order.operations[1].time_in_mins, 240)
def test_backflush_based_on_in_bom(self):
raw_material_1 = make_item(item_code="BOM RM 1", properties={"is_stock_item": 1}).name
raw_material_2 = make_item(item_code="BOM RM 2", properties={"is_stock_item": 1}).name
fg_item = make_item(item_code="BOM FG 1", properties={"is_stock_item": 1}).name
frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")
backflush_based_on = frappe.db.get_single_value(
"Manufacturing Settings", "backflush_raw_materials_based_on"
)
self.assertEqual(backflush_based_on, "BOM")
for item_code in [raw_material_1, raw_material_2]:
test_stock_entry.make_stock_entry(
item_code=item_code, target="Stores - _TC", qty=1, basic_rate=100
)
bom = make_bom(
item=fg_item,
quantity=1,
raw_materials=[raw_material_1],
backflush_based_on="Material Transferred for Manufacture",
)
wo_order = make_wo_order_test_record(item=fg_item, qty=1, source_warehouse="Stores - _TC")
self.assertEqual(bom.name, wo_order.bom_no)
backflush_based_on = frappe.db.get_value("BOM", wo_order.bom_no, "backflush_based_on")
self.assertEqual(backflush_based_on, "Material Transferred for Manufacture")
material_transfer_entry = frappe.get_doc(
make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 1)
)
material_transfer_entry.save()
# Add second raw material in the material transfer entry which is not in the BOM to simulate backflush based on material transfer scenario
material_transfer_entry.append(
"items",
{
"item_code": raw_material_2,
"item_name": raw_material_2,
"item_group": frappe.get_value("Item", raw_material_2, "item_group"),
"uom": frappe.get_value("Item", raw_material_2, "stock_uom"),
"conversion_factor": 1,
"s_warehouse": "Stores - _TC",
"t_warehouse": material_transfer_entry.items[0].t_warehouse,
"qty": 1,
},
)
material_transfer_entry.submit()
manufacture_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 1))
manufacture_entry.save()
self.assertEqual(len(manufacture_entry.items), 3)
for row in manufacture_entry.items:
if row.s_warehouse:
self.assertIn(row.item_code, [raw_material_1, raw_material_2])
def get_reserved_entries(voucher_no, warehouse=None):
doctype = frappe.qb.DocType("Stock Reservation Entry")
sabb = frappe.qb.DocType("Serial and Batch Entry")
query = (
frappe.qb.from_(doctype)
.left_join(sabb)
.on(doctype.name == sabb.parent)
.select(
doctype.name,
doctype.item_code,
doctype.warehouse,
doctype.reserved_qty,
sabb.serial_no,
sabb.batch_no,
sabb.qty,
sabb.delivered_qty,
)
.where((doctype.voucher_no == voucher_no) & (doctype.docstatus == 1))
)
if warehouse:
query = query.where(doctype.warehouse == warehouse)
reservation_entries = query.run(as_dict=True)
_reserved_item = frappe._dict({})
for entry in reservation_entries:
key = (entry.item_code, entry.warehouse)
if key not in _reserved_item:
_reserved_item[key] = frappe._dict(
{
"reserved_qty": 0,
"serial_nos": [],
"batch_nos": defaultdict(int),
"serial_batches": defaultdict(list),
}
)
_reserved_item[key].reserved_qty += entry.qty
if entry.batch_no:
_reserved_item[key].batch_nos[entry.batch_no] += entry.qty
if entry.serial_no:
_reserved_item[key].serial_batches[entry.batch_no].append(entry.serial_no)
if entry.serial_no:
_reserved_item[key].serial_nos.append(entry.serial_no)
return _reserved_item
>>>>>>> 3ad67021d6 (fix(manufacturing): allow to edit batch size while creating a work order (#55058))
def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse):
from erpnext.stock.doctype.stock_entry.test_stock_entry import (

View File

@@ -158,7 +158,7 @@ class WorkOrder(Document):
self.calculate_operating_cost()
self.validate_qty()
self.validate_transfer_against()
self.validate_operation_time()
self.validate_operations()
self.status = self.get_status()
self.validate_workstation_type()
self.reset_use_multi_level_bom()
@@ -1120,9 +1120,16 @@ class WorkOrder(Document):
title=_("Missing value"),
)
def validate_operation_time(self):
def validate_operations(self):
for d in self.operations:
<<<<<<< HEAD
if not d.time_in_mins > 0:
=======
if not d.batch_size or d.batch_size <= 0:
d.batch_size = 1
if d.time_in_mins <= 0:
>>>>>>> 3ad67021d6 (fix(manufacturing): allow to edit batch size while creating a work order (#55058))
frappe.throw(_("Operation Time must be greater than 0 for Operation {0}").format(d.operation))
def update_required_items(self):

View File

@@ -185,10 +185,11 @@
"read_only": 1
},
{
"default": "1",
"fieldname": "batch_size",
"fieldtype": "Float",
"label": "Batch Size",
"read_only": 1
"non_negative": 1
},
{
"fieldname": "sequence_id",
@@ -225,7 +226,11 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
<<<<<<< HEAD
"modified": "2025-05-15 15:10:06.885440",
=======
"modified": "2026-05-25 17:15:12.038470",
>>>>>>> 3ad67021d6 (fix(manufacturing): allow to edit batch size while creating a work order (#55058))
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order Operation",