diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 7f4fbdaef06..c9bac3dd9b8 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -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", @@ -511,7 +511,11 @@ ], "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2025-08-04 15:47:54.514290", +======= + "modified": "2026-03-31 21:06:48.987740", +>>>>>>> f37bf62824 (fix: wrong operation time calculation (#53796)) "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index f4a0d6f6145..6dd2d370336 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -699,10 +699,14 @@ class TestWorkOrder(FrappeTestCase): 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, @@ -3631,6 +3635,64 @@ class TestWorkOrder(FrappeTestCase): 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 make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import ( diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 6e20e789899..8b761faf3f2 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -392,7 +392,13 @@ frappe.ui.form.on("Work Order", { qty: pending_qty, pending_qty: pending_qty, sequence_id: data.sequence_id, +<<<<<<< HEAD bom: data.bom, +======= + skip_material_transfer: data.skip_material_transfer, + backflush_from_wip_warehouse: data.backflush_from_wip_warehouse, + time_in_mins: data.time_in_mins, +>>>>>>> f37bf62824 (fix: wrong operation time calculation (#53796)) }); } } diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 8bc2e7c1953..f5d48ee571e 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -905,7 +905,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}, @@ -927,7 +927,18 @@ class WorkOrder(Document): for d in data: if not d.fixed_time: +<<<<<<< HEAD d.time_in_mins = flt(d.time_in_mins) * flt(qty) +======= + 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(qty) + +>>>>>>> f37bf62824 (fix: wrong operation time calculation (#53796)) d.status = "Pending" return data @@ -944,7 +955,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=1.0 / bom_qty)) @@ -958,7 +971,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() @@ -1723,6 +1736,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, @@ -1730,12 +1744,20 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create "operation": row.get("operation"), "workstation": row.get("workstation"), "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": row.get("bom") or work_order.bom_no, "project": work_order.project, "company": work_order.company, "sequence_id": row.get("sequence_id"), +<<<<<<< HEAD +======= + "hour_rate": row.get("hour_rate"), + "serial_no": row.get("serial_no"), + "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"), +>>>>>>> f37bf62824 (fix: wrong operation time calculation (#53796)) "wip_warehouse": work_order.wip_warehouse or row.get("wip_warehouse") if not work_order.skip_transfer or work_order.from_wip_warehouse else work_order.source_warehouse or row.get("source_warehouse"),