diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 728e8fc27ec..21f0adbdd74 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", @@ -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", diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index b2a1eba0232..628aa58b187 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -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") diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 0e8729bf4ba..9840ed1c13d 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -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, }); } } diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 488f00aa9ac..548bb8c1fc6 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -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")