fix: wrong operation time calculation (#53796)

This commit is contained in:
Mihir Kandoi
2026-04-14 14:42:02 +05:30
committed by GitHub
parent 8a72d7fafe
commit f37bf62824
4 changed files with 80 additions and 12 deletions

View File

@@ -1,7 +1,7 @@
{
"actions": [],
"autoname": "naming_series:",
"creation": "2018-07-09 17:23:29.518745",
"creation": "2026-03-31 21:06:16.282931",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
@@ -452,7 +452,6 @@
"show_dashboard": 1
},
{
"depends_on": "expected_start_date",
"fieldname": "scheduled_time_section",
"fieldtype": "Section Break",
"label": "Scheduled Time"
@@ -628,7 +627,7 @@
"grid_page_length": 50,
"is_submittable": 1,
"links": [],
"modified": "2026-02-26 15:13:56.767070",
"modified": "2026-03-31 21:06:48.987740",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card",

View File

@@ -698,10 +698,14 @@ class TestWorkOrder(ERPNextTestSuite):
if not bom_name:
bom = make_bom(item=fg_item, rate=1000, raw_materials=[rm1], do_not_save=True)
bom.with_operations = 1
operation = make_operation(operation="Batch Size Operation")
operation.create_job_card_based_on_batch_size = 1
operation.save()
bom.append(
"operations",
{
"operation": "_Test Operation 1",
"operation": "Batch Size Operation",
"workstation": "_Test Workstation 1",
"description": "Test Data",
"operating_cost": 100,
@@ -4178,6 +4182,64 @@ class TestWorkOrder(ERPNextTestSuite):
self.assertEqual(bin1_at_completion.reserved_qty_for_production, 0)
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,
"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()
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 get_reserved_entries(voucher_no, warehouse=None):
doctype = frappe.qb.DocType("Stock Reservation Entry")

View File

@@ -419,6 +419,7 @@ frappe.ui.form.on("Work Order", {
sequence_id: data.sequence_id,
skip_material_transfer: data.skip_material_transfer,
backflush_from_wip_warehouse: data.backflush_from_wip_warehouse,
time_in_mins: data.time_in_mins,
});
}
}

View File

@@ -1252,7 +1252,7 @@ class WorkOrder(Document):
def set_work_order_operations(self):
"""Fetch operations from BOM and set in 'Work Order'"""
def _get_operations(bom_no, qty=1):
def _get_operations(bom_no, qty=1, exploded=False):
data = frappe.get_all(
"BOM Operation",
filters={"parent": bom_no},
@@ -1284,10 +1284,13 @@ class WorkOrder(Document):
for d in data:
if not d.fixed_time:
if d.set_cost_based_on_bom_qty:
d.time_in_mins = flt(d.time_in_mins) * flt(flt(qty) / flt(d.batch_size or 1))
if frappe.get_value("Operation", d.operation, "create_job_card_based_on_batch_size"):
qty = d.batch_size
if exploded:
d.time_in_mins *= flt(qty)
else:
d.time_in_mins = flt(d.time_in_mins) * flt(qty)
d.time_in_mins /= flt(qty)
d.status = "Pending"
@@ -1308,7 +1311,9 @@ class WorkOrder(Document):
for node in bom_traversal:
if node.is_bom:
operations.extend(_get_operations(node.name, qty=node.exploded_qty / node.bom_qty))
operations.extend(
_get_operations(node.name, qty=node.exploded_qty / node.bom_qty, exploded=True)
)
bom_qty = frappe.get_cached_value("BOM", self.bom_no, "quantity")
operations.extend(_get_operations(self.bom_no, qty=bom_qty))
@@ -1322,7 +1327,7 @@ class WorkOrder(Document):
def calculate_time(self):
for d in self.get("operations"):
if not d.fixed_time:
d.time_in_mins = flt(d.time_in_mins) * (flt(self.qty) / flt(d.batch_size))
d.time_in_mins = flt(d.time_in_mins) * flt(self.qty)
self.calculate_operating_cost()
@@ -2639,6 +2644,7 @@ def validate_operation_data(row):
def create_job_card(work_order, row, enable_capacity_planning=False, auto_create=False):
doc = frappe.new_doc("Job Card")
qty = row.job_card_qty or work_order.get("qty", 0)
doc.update(
{
"work_order": work_order.name,
@@ -2647,7 +2653,7 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create
"workstation": row.get("workstation"),
"operation_row_id": cint(row.idx),
"posting_date": nowdate(),
"for_quantity": row.job_card_qty or work_order.get("qty", 0),
"for_quantity": qty,
"operation_id": row.get("name"),
"bom_no": work_order.bom_no,
"project": work_order.project,
@@ -2655,7 +2661,7 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create
"sequence_id": row.get("sequence_id"),
"hour_rate": row.get("hour_rate"),
"serial_no": row.get("serial_no"),
"time_required": row.get("time_in_mins"),
"time_required": (row.get("time_in_mins", 0) / work_order.qty) * qty,
"source_warehouse": row.get("source_warehouse") or work_order.get("source_warehouse"),
"target_warehouse": row.get("fg_warehouse") or work_order.get("fg_warehouse"),
"wip_warehouse": work_order.wip_warehouse or row.get("wip_warehouse")