mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-22 22:49:19 +00:00
fix: wrong operation time calculation (#53796)
(cherry picked from commit f37bf62824)
# Conflicts:
# erpnext/manufacturing/doctype/job_card/job_card.json
# erpnext/manufacturing/doctype/work_order/work_order.js
# erpnext/manufacturing/doctype/work_order/work_order.py
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"autoname": "naming_series:",
|
"autoname": "naming_series:",
|
||||||
"creation": "2018-07-09 17:23:29.518745",
|
"creation": "2026-03-31 21:06:16.282931",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
@@ -511,7 +511,11 @@
|
|||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
|
<<<<<<< HEAD
|
||||||
"modified": "2025-08-04 15:47:54.514290",
|
"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",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Job Card",
|
"name": "Job Card",
|
||||||
|
|||||||
@@ -699,10 +699,14 @@ class TestWorkOrder(FrappeTestCase):
|
|||||||
if not bom_name:
|
if not bom_name:
|
||||||
bom = make_bom(item=fg_item, rate=1000, raw_materials=[rm1], do_not_save=True)
|
bom = make_bom(item=fg_item, rate=1000, raw_materials=[rm1], do_not_save=True)
|
||||||
bom.with_operations = 1
|
bom.with_operations = 1
|
||||||
|
operation = make_operation(operation="Batch Size Operation")
|
||||||
|
operation.create_job_card_based_on_batch_size = 1
|
||||||
|
operation.save()
|
||||||
|
|
||||||
bom.append(
|
bom.append(
|
||||||
"operations",
|
"operations",
|
||||||
{
|
{
|
||||||
"operation": "_Test Operation 1",
|
"operation": "Batch Size Operation",
|
||||||
"workstation": "_Test Workstation 1",
|
"workstation": "_Test Workstation 1",
|
||||||
"description": "Test Data",
|
"description": "Test Data",
|
||||||
"operating_cost": 100,
|
"operating_cost": 100,
|
||||||
@@ -3631,6 +3635,64 @@ class TestWorkOrder(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertEqual(bin1_at_completion.reserved_qty_for_production, 0)
|
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):
|
def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse):
|
||||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||||
|
|||||||
@@ -392,7 +392,13 @@ frappe.ui.form.on("Work Order", {
|
|||||||
qty: pending_qty,
|
qty: pending_qty,
|
||||||
pending_qty: pending_qty,
|
pending_qty: pending_qty,
|
||||||
sequence_id: data.sequence_id,
|
sequence_id: data.sequence_id,
|
||||||
|
<<<<<<< HEAD
|
||||||
bom: data.bom,
|
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))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -905,7 +905,7 @@ class WorkOrder(Document):
|
|||||||
def set_work_order_operations(self):
|
def set_work_order_operations(self):
|
||||||
"""Fetch operations from BOM and set in 'Work Order'"""
|
"""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(
|
data = frappe.get_all(
|
||||||
"BOM Operation",
|
"BOM Operation",
|
||||||
filters={"parent": bom_no},
|
filters={"parent": bom_no},
|
||||||
@@ -927,7 +927,18 @@ class WorkOrder(Document):
|
|||||||
|
|
||||||
for d in data:
|
for d in data:
|
||||||
if not d.fixed_time:
|
if not d.fixed_time:
|
||||||
|
<<<<<<< HEAD
|
||||||
d.time_in_mins = flt(d.time_in_mins) * flt(qty)
|
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"
|
d.status = "Pending"
|
||||||
|
|
||||||
return data
|
return data
|
||||||
@@ -944,7 +955,9 @@ class WorkOrder(Document):
|
|||||||
|
|
||||||
for node in bom_traversal:
|
for node in bom_traversal:
|
||||||
if node.is_bom:
|
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")
|
bom_qty = frappe.get_cached_value("BOM", self.bom_no, "quantity")
|
||||||
operations.extend(_get_operations(self.bom_no, qty=1.0 / bom_qty))
|
operations.extend(_get_operations(self.bom_no, qty=1.0 / bom_qty))
|
||||||
@@ -958,7 +971,7 @@ class WorkOrder(Document):
|
|||||||
def calculate_time(self):
|
def calculate_time(self):
|
||||||
for d in self.get("operations"):
|
for d in self.get("operations"):
|
||||||
if not d.fixed_time:
|
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()
|
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):
|
def create_job_card(work_order, row, enable_capacity_planning=False, auto_create=False):
|
||||||
doc = frappe.new_doc("Job Card")
|
doc = frappe.new_doc("Job Card")
|
||||||
|
qty = row.job_card_qty or work_order.get("qty", 0)
|
||||||
doc.update(
|
doc.update(
|
||||||
{
|
{
|
||||||
"work_order": work_order.name,
|
"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"),
|
"operation": row.get("operation"),
|
||||||
"workstation": row.get("workstation"),
|
"workstation": row.get("workstation"),
|
||||||
"posting_date": nowdate(),
|
"posting_date": nowdate(),
|
||||||
"for_quantity": row.job_card_qty or work_order.get("qty", 0),
|
"for_quantity": qty,
|
||||||
"operation_id": row.get("name"),
|
"operation_id": row.get("name"),
|
||||||
"bom_no": row.get("bom") or work_order.bom_no,
|
"bom_no": row.get("bom") or work_order.bom_no,
|
||||||
"project": work_order.project,
|
"project": work_order.project,
|
||||||
"company": work_order.company,
|
"company": work_order.company,
|
||||||
"sequence_id": row.get("sequence_id"),
|
"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")
|
"wip_warehouse": work_order.wip_warehouse or row.get("wip_warehouse")
|
||||||
if not work_order.skip_transfer or work_order.from_wip_warehouse
|
if not work_order.skip_transfer or work_order.from_wip_warehouse
|
||||||
else work_order.source_warehouse or row.get("source_warehouse"),
|
else work_order.source_warehouse or row.get("source_warehouse"),
|
||||||
|
|||||||
Reference in New Issue
Block a user