Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: capacity planning issue in the job card #40092

Merged
merged 2 commits into from
Feb 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 18 additions & 11 deletions erpnext/manufacturing/doctype/job_card/job_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,12 +239,12 @@ def validate_time_logs(self):
for row in self.sub_operations:
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.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:
return {}
Expand All @@ -269,7 +269,7 @@ def get_overlap_for(self, args, check_next_available_slot=False):
self.workstation = workstation_time.get("workstation")
return workstation_time

return time_logs[-1]
return time_logs[0]

def has_overlap(self, production_capacity, time_logs):
overlap = False
Expand Down Expand Up @@ -308,7 +308,7 @@ def has_overlap(self, production_capacity, time_logs):
return True
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")
jctl = frappe.qb.DocType(doctype)

Expand All @@ -318,9 +318,6 @@ def get_time_logs(self, args, doctype, check_next_available_slot=False):
((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 = (
frappe.qb.from_(jctl)
.from_(jc)
Expand Down Expand Up @@ -395,18 +392,28 @@ def schedule_time_logs(self, row):

def validate_overlap_for_workstation(self, args, row):
# 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:
workstations = get_workstations(self.workstation_type)
if workstations:
# Get the first workstation
self.workstation = workstations[0]

if not data:
row.planned_start_time = args.from_time
return

if data:
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:
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):
workstation_doc = frappe.get_cached_doc("Workstation", self.workstation)
Expand Down
107 changes: 107 additions & 0 deletions erpnext/manufacturing/doctype/work_order/test_work_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -1822,6 +1822,113 @@ def test_get_rm_cost_from_consumption_entry(self):
valuation_rate = sum([item.valuation_rate * item.transfer_qty for item in mce.items]) / 10
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():
if not frappe.db.exists("BOM", {"item": "Test Final SF Item 1"}):
Expand Down
19 changes: 13 additions & 6 deletions erpnext/manufacturing/doctype/work_order/work_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,12 @@ def validate_warehouse_belongs_to_company(self):
def calculate_operating_cost(self):
self.planned_operating_cost, self.actual_operating_cost = 0.0, 0.0
for d in self.get("operations"):
d.planned_operating_cost = flt(d.hour_rate) * (flt(d.time_in_mins) / 60.0)
d.actual_operating_cost = flt(d.hour_rate) * (flt(d.actual_operation_time) / 60.0)
d.planned_operating_cost = flt(
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.actual_operating_cost += flt(d.actual_operating_cost)
Expand Down Expand Up @@ -588,7 +592,6 @@ def create_job_card(self):
def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_planning):
self.set_operation_start_end_time(index, row)

original_start_time = row.planned_start_time
job_card_doc = create_job_card(
self, row, auto_create=True, enable_capacity_planning=enable_capacity_planning
)
Expand All @@ -597,11 +600,15 @@ def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_plann
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

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.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,
)
Expand Down
Loading