mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-29 03:28:32 +00:00
fix: capacity planning issue in the job card (#40092)
* fix: capacity planning issue in the job card
* test: test case to test capacity planning for workstation
(cherry picked from commit 75f8464724)
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
This commit is contained in:
@@ -239,12 +239,12 @@ class JobCard(Document):
|
|||||||
for row in self.sub_operations:
|
for row in self.sub_operations:
|
||||||
self.total_completed_qty += row.completed_qty
|
self.total_completed_qty += row.completed_qty
|
||||||
|
|
||||||
def get_overlap_for(self, args, check_next_available_slot=False):
|
def get_overlap_for(self, args):
|
||||||
time_logs = []
|
time_logs = []
|
||||||
|
|
||||||
time_logs.extend(self.get_time_logs(args, "Job Card Time Log", check_next_available_slot))
|
time_logs.extend(self.get_time_logs(args, "Job Card Time Log"))
|
||||||
|
|
||||||
time_logs.extend(self.get_time_logs(args, "Job Card Scheduled Time", check_next_available_slot))
|
time_logs.extend(self.get_time_logs(args, "Job Card Scheduled Time"))
|
||||||
|
|
||||||
if not time_logs:
|
if not time_logs:
|
||||||
return {}
|
return {}
|
||||||
@@ -269,7 +269,7 @@ class JobCard(Document):
|
|||||||
self.workstation = workstation_time.get("workstation")
|
self.workstation = workstation_time.get("workstation")
|
||||||
return workstation_time
|
return workstation_time
|
||||||
|
|
||||||
return time_logs[-1]
|
return time_logs[0]
|
||||||
|
|
||||||
def has_overlap(self, production_capacity, time_logs):
|
def has_overlap(self, production_capacity, time_logs):
|
||||||
overlap = False
|
overlap = False
|
||||||
@@ -308,7 +308,7 @@ class JobCard(Document):
|
|||||||
return True
|
return True
|
||||||
return overlap
|
return overlap
|
||||||
|
|
||||||
def get_time_logs(self, args, doctype, check_next_available_slot=False):
|
def get_time_logs(self, args, doctype):
|
||||||
jc = frappe.qb.DocType("Job Card")
|
jc = frappe.qb.DocType("Job Card")
|
||||||
jctl = frappe.qb.DocType(doctype)
|
jctl = frappe.qb.DocType(doctype)
|
||||||
|
|
||||||
@@ -318,9 +318,6 @@ class JobCard(Document):
|
|||||||
((jctl.from_time >= args.from_time) & (jctl.to_time <= args.to_time)),
|
((jctl.from_time >= args.from_time) & (jctl.to_time <= args.to_time)),
|
||||||
]
|
]
|
||||||
|
|
||||||
if check_next_available_slot:
|
|
||||||
time_conditions.append(((jctl.from_time >= args.from_time) & (jctl.to_time >= args.to_time)))
|
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(jctl)
|
frappe.qb.from_(jctl)
|
||||||
.from_(jc)
|
.from_(jc)
|
||||||
@@ -395,18 +392,28 @@ class JobCard(Document):
|
|||||||
|
|
||||||
def validate_overlap_for_workstation(self, args, row):
|
def validate_overlap_for_workstation(self, args, row):
|
||||||
# get the last record based on the to time from the job card
|
# get the last record based on the to time from the job card
|
||||||
data = self.get_overlap_for(args, check_next_available_slot=True)
|
data = self.get_overlap_for(args)
|
||||||
|
|
||||||
if not self.workstation:
|
if not self.workstation:
|
||||||
workstations = get_workstations(self.workstation_type)
|
workstations = get_workstations(self.workstation_type)
|
||||||
if workstations:
|
if workstations:
|
||||||
# Get the first workstation
|
# Get the first workstation
|
||||||
self.workstation = workstations[0]
|
self.workstation = workstations[0]
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
row.planned_start_time = args.from_time
|
||||||
|
return
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
if data.get("planned_start_time"):
|
if data.get("planned_start_time"):
|
||||||
row.planned_start_time = get_datetime(data.planned_start_time)
|
args.planned_start_time = get_datetime(data.planned_start_time)
|
||||||
else:
|
else:
|
||||||
row.planned_start_time = get_datetime(data.to_time + get_mins_between_operations())
|
args.planned_start_time = get_datetime(data.to_time + get_mins_between_operations())
|
||||||
|
|
||||||
|
args.from_time = args.planned_start_time
|
||||||
|
args.to_time = add_to_date(args.planned_start_time, minutes=row.remaining_time_in_mins)
|
||||||
|
|
||||||
|
self.validate_overlap_for_workstation(args, row)
|
||||||
|
|
||||||
def check_workstation_time(self, row):
|
def check_workstation_time(self, row):
|
||||||
workstation_doc = frappe.get_cached_doc("Workstation", self.workstation)
|
workstation_doc = frappe.get_cached_doc("Workstation", self.workstation)
|
||||||
|
|||||||
@@ -1821,6 +1821,113 @@ class TestWorkOrder(FrappeTestCase):
|
|||||||
valuation_rate = sum([item.valuation_rate * item.transfer_qty for item in mce.items]) / 10
|
valuation_rate = sum([item.valuation_rate * item.transfer_qty for item in mce.items]) / 10
|
||||||
self.assertEqual(me.items[0].valuation_rate, valuation_rate)
|
self.assertEqual(me.items[0].valuation_rate, valuation_rate)
|
||||||
|
|
||||||
|
def test_capcity_planning_for_workstation(self):
|
||||||
|
frappe.db.set_single_value(
|
||||||
|
"Manufacturing Settings",
|
||||||
|
{
|
||||||
|
"disable_capacity_planning": 0,
|
||||||
|
"capacity_planning_for_days": 1,
|
||||||
|
"mins_between_operations": 10,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
properties = {"is_stock_item": 1, "valuation_rate": 100}
|
||||||
|
fg_item = make_item("Test FG Item For Capacity Planning", properties).name
|
||||||
|
|
||||||
|
rm_item = make_item("Test RM Item For Capacity Planning", properties).name
|
||||||
|
|
||||||
|
workstation = "Test Workstation For Capacity Planning"
|
||||||
|
if not frappe.db.exists("Workstation", workstation):
|
||||||
|
make_workstation(workstation=workstation, production_capacity=1)
|
||||||
|
|
||||||
|
operation = "Test Operation For Capacity Planning"
|
||||||
|
if not frappe.db.exists("Operation", operation):
|
||||||
|
make_operation(operation=operation, workstation=workstation)
|
||||||
|
|
||||||
|
bom_doc = make_bom(
|
||||||
|
item=fg_item,
|
||||||
|
source_warehouse="Stores - _TC",
|
||||||
|
raw_materials=[rm_item],
|
||||||
|
with_operations=1,
|
||||||
|
do_not_submit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
bom_doc.append(
|
||||||
|
"operations",
|
||||||
|
{"operation": operation, "time_in_mins": 1420, "hour_rate": 100, "workstation": workstation},
|
||||||
|
)
|
||||||
|
bom_doc.submit()
|
||||||
|
|
||||||
|
# 1st Work Order,
|
||||||
|
# Capacity to run parallel the operation 'Test Operation For Capacity Planning' is 2
|
||||||
|
wo_doc = make_wo_order_test_record(
|
||||||
|
production_item=fg_item, qty=1, planned_start_date="2024-02-25 00:00:00", do_not_submit=1
|
||||||
|
)
|
||||||
|
|
||||||
|
wo_doc.submit()
|
||||||
|
job_cards = frappe.get_all(
|
||||||
|
"Job Card",
|
||||||
|
filters={"work_order": wo_doc.name},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(job_cards), 1)
|
||||||
|
|
||||||
|
# 2nd Work Order,
|
||||||
|
wo_doc = make_wo_order_test_record(
|
||||||
|
production_item=fg_item, qty=1, planned_start_date="2024-02-25 00:00:00", do_not_submit=1
|
||||||
|
)
|
||||||
|
|
||||||
|
wo_doc.submit()
|
||||||
|
job_cards = frappe.get_all(
|
||||||
|
"Job Card",
|
||||||
|
filters={"work_order": wo_doc.name},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(job_cards), 1)
|
||||||
|
|
||||||
|
# 3rd Work Order, capacity is full
|
||||||
|
wo_doc = make_wo_order_test_record(
|
||||||
|
production_item=fg_item, qty=1, planned_start_date="2024-02-25 00:00:00", do_not_submit=1
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertRaises(CapacityError, wo_doc.submit)
|
||||||
|
|
||||||
|
frappe.db.set_single_value(
|
||||||
|
"Manufacturing Settings", {"disable_capacity_planning": 1, "mins_between_operations": 0}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_operation(**kwargs):
|
||||||
|
kwargs = frappe._dict(kwargs)
|
||||||
|
|
||||||
|
operation_doc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Operation",
|
||||||
|
"name": kwargs.operation,
|
||||||
|
"workstation": kwargs.workstation,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
operation_doc.insert()
|
||||||
|
|
||||||
|
return operation_doc
|
||||||
|
|
||||||
|
|
||||||
|
def make_workstation(**kwargs):
|
||||||
|
kwargs = frappe._dict(kwargs)
|
||||||
|
|
||||||
|
workstation_doc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Workstation",
|
||||||
|
"workstation_name": kwargs.workstation,
|
||||||
|
"workstation_type": kwargs.workstation_type,
|
||||||
|
"production_capacity": kwargs.production_capacity or 0,
|
||||||
|
"hour_rate": kwargs.hour_rate or 100,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
workstation_doc.insert()
|
||||||
|
|
||||||
|
return workstation_doc
|
||||||
|
|
||||||
|
|
||||||
def prepare_boms_for_sub_assembly_test():
|
def prepare_boms_for_sub_assembly_test():
|
||||||
if not frappe.db.exists("BOM", {"item": "Test Final SF Item 1"}):
|
if not frappe.db.exists("BOM", {"item": "Test Final SF Item 1"}):
|
||||||
|
|||||||
@@ -242,8 +242,12 @@ class WorkOrder(Document):
|
|||||||
def calculate_operating_cost(self):
|
def calculate_operating_cost(self):
|
||||||
self.planned_operating_cost, self.actual_operating_cost = 0.0, 0.0
|
self.planned_operating_cost, self.actual_operating_cost = 0.0, 0.0
|
||||||
for d in self.get("operations"):
|
for d in self.get("operations"):
|
||||||
d.planned_operating_cost = flt(d.hour_rate) * (flt(d.time_in_mins) / 60.0)
|
d.planned_operating_cost = flt(
|
||||||
d.actual_operating_cost = flt(d.hour_rate) * (flt(d.actual_operation_time) / 60.0)
|
flt(d.hour_rate) * (flt(d.time_in_mins) / 60.0), d.precision("planned_operating_cost")
|
||||||
|
)
|
||||||
|
d.actual_operating_cost = flt(
|
||||||
|
flt(d.hour_rate) * (flt(d.actual_operation_time) / 60.0), d.precision("actual_operating_cost")
|
||||||
|
)
|
||||||
|
|
||||||
self.planned_operating_cost += flt(d.planned_operating_cost)
|
self.planned_operating_cost += flt(d.planned_operating_cost)
|
||||||
self.actual_operating_cost += flt(d.actual_operating_cost)
|
self.actual_operating_cost += flt(d.actual_operating_cost)
|
||||||
@@ -588,7 +592,6 @@ class WorkOrder(Document):
|
|||||||
def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_planning):
|
def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_planning):
|
||||||
self.set_operation_start_end_time(index, row)
|
self.set_operation_start_end_time(index, row)
|
||||||
|
|
||||||
original_start_time = row.planned_start_time
|
|
||||||
job_card_doc = create_job_card(
|
job_card_doc = create_job_card(
|
||||||
self, row, auto_create=True, enable_capacity_planning=enable_capacity_planning
|
self, row, auto_create=True, enable_capacity_planning=enable_capacity_planning
|
||||||
)
|
)
|
||||||
@@ -597,11 +600,15 @@ class WorkOrder(Document):
|
|||||||
row.planned_start_time = job_card_doc.scheduled_time_logs[-1].from_time
|
row.planned_start_time = job_card_doc.scheduled_time_logs[-1].from_time
|
||||||
row.planned_end_time = job_card_doc.scheduled_time_logs[-1].to_time
|
row.planned_end_time = job_card_doc.scheduled_time_logs[-1].to_time
|
||||||
|
|
||||||
if date_diff(row.planned_start_time, original_start_time) > plan_days:
|
if date_diff(row.planned_end_time, self.planned_start_date) > plan_days:
|
||||||
frappe.message_log.pop()
|
frappe.message_log.pop()
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Unable to find the time slot in the next {0} days for the operation {1}.").format(
|
_(
|
||||||
plan_days, row.operation
|
"Unable to find the time slot in the next {0} days for the operation {1}. Please increase the 'Capacity Planning For (Days)' in the {2}."
|
||||||
|
).format(
|
||||||
|
plan_days,
|
||||||
|
row.operation,
|
||||||
|
get_link_to_form("Manufacturing Settings", "Manufacturing Settings"),
|
||||||
),
|
),
|
||||||
CapacityError,
|
CapacityError,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user