[IMP] mrp: change workorder plan behavior
- Before, the auto-planning of workorder autorize overlaps between then. E.g. if a workorder (WO1) begin at Now + 30min with a duration of 60 minutes and we try to plan a new workorder (WO2) Now, this one will be plan now until Now + 120 min (30 min of WO2 + 60 min of WO1 + 30 min of WO2). In our case we don't want this behavior by default, because in real case, it is not efficient to work on job A during 30 min, after change to pass to a other job B, and rework on A after the job B. But in a other hand, we need to accept overlaps with no-working hours intervals. Then, rewritte the _plan_workorders method to take these features in account. Also small fix about the planning has been done: - Due to the strong link of date_planned(_...) between the MO and WO, the replan of the late first WO was wrong. If the MO is planned before now, then don't take in account date_planned_start and try to plan WO as soon as possible. - Also, fix error handling when the WO is planned before the previous one. task-2169447
This commit is contained in:
@@ -743,9 +743,6 @@ class MrpProduction(models.Model):
|
||||
order._plan_workorders()
|
||||
return True
|
||||
|
||||
def _get_start_date(self):
|
||||
return self.date_planned_start or datetime.datetime.now()
|
||||
|
||||
def _plan_workorders(self, replan=False):
|
||||
""" Plan all the production's workorders depending on the workcenters
|
||||
work schedule.
|
||||
@@ -758,9 +755,12 @@ class MrpProduction(models.Model):
|
||||
# Schedule all work orders (new ones and those already created)
|
||||
qty_to_produce = max(self.product_qty - self.qty_produced, 0)
|
||||
qty_to_produce = self.product_uom_id._compute_quantity(qty_to_produce, self.product_id.uom_id)
|
||||
start_date = self._get_start_date()
|
||||
start_date = self.date_planned_start
|
||||
if replan:
|
||||
workorder_ids = self.workorder_ids.filtered(lambda wo: wo.state in ['ready', 'pending'])
|
||||
# We plan the manufacturing order according to its `date_planned_start`, but if
|
||||
# `date_planned_start` is in the past, we plan it as soon as possible.
|
||||
start_date = max(start_date, datetime.datetime.now())
|
||||
workorder_ids.leave_id.unlink()
|
||||
else:
|
||||
workorder_ids = self.workorder_ids
|
||||
@@ -775,14 +775,10 @@ class MrpProduction(models.Model):
|
||||
cycle_number = float_round(qty_to_produce / workcenter.capacity, precision_digits=0, rounding_method='UP')
|
||||
duration_expected = workcenter.time_start + workcenter.time_stop + cycle_number * time_cycle * 100.0 / workcenter.time_efficiency
|
||||
|
||||
# get first free slot
|
||||
# planning 0 hours gives the start of the next attendance
|
||||
from_date = workcenter.resource_calendar_id.plan_hours(0, start_date, compute_leaves=True, resource=workcenter.resource_id, domain=[('time_type', 'in', ['leave', 'other'])])
|
||||
from_date, to_date = workcenter._get_first_available_slot(start_date, duration_expected)
|
||||
# If the workcenter is unavailable, try planning on the next one
|
||||
if from_date is False:
|
||||
if not from_date:
|
||||
continue
|
||||
to_date = workcenter.resource_calendar_id.plan_hours(duration_expected / 60.0, from_date, compute_leaves=True, resource=workcenter.resource_id, domain=[('time_type', 'in', ['leave', 'other'])])
|
||||
|
||||
# Check if this workcenter is better than the previous ones
|
||||
if to_date and to_date < best_finished_date:
|
||||
best_start_date = from_date
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from dateutil import relativedelta
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
import datetime
|
||||
|
||||
from odoo import api, exceptions, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.addons.resource.models.resource import make_aware, Intervals
|
||||
from odoo.tools.float_utils import float_compare
|
||||
|
||||
|
||||
class MrpWorkcenter(models.Model):
|
||||
@@ -204,6 +208,47 @@ class MrpWorkcenter(models.Model):
|
||||
unavailability_ressources = self.resource_id._get_unavailable_intervals(start_datetime, end_datetime)
|
||||
return {wc.id: unavailability_ressources.get(wc.resource_id.id, []) for wc in self}
|
||||
|
||||
def _get_first_available_slot(self, start_datetime, duration):
|
||||
"""Get the first available interval for the workcenter in `self`.
|
||||
|
||||
The available interval is disjoinct with all other workorders planned on this workcenter, but
|
||||
can overlap the time-off of the related calendar (inverse of the working hours).
|
||||
Return the first available interval (start datetime, end datetime) or,
|
||||
if there is none before 700 days, a tuple error (False, 'error message').
|
||||
|
||||
:param start_datetime: begin the search at this datetime
|
||||
:param duration: minutes needed to make the workorder (float)
|
||||
:rtype: tuple
|
||||
"""
|
||||
self.ensure_one()
|
||||
start_datetime, revert = make_aware(start_datetime)
|
||||
|
||||
get_available_intervals = partial(self.resource_calendar_id._work_intervals, domain=[('time_type', 'in', ['other', 'leave'])], resource=self.resource_id)
|
||||
get_workorder_intervals = partial(self.resource_calendar_id._leave_intervals, domain=[('time_type', '=', 'other')], resource=self.resource_id)
|
||||
|
||||
remaining = duration
|
||||
start_interval = start_datetime
|
||||
delta = timedelta(days=14)
|
||||
|
||||
for n in range(50): # 50 * 14 = 700 days in advance (hardcoded)
|
||||
dt = start_datetime + delta * n
|
||||
available_intervals = get_available_intervals(dt, dt + delta)
|
||||
workorder_intervals = get_workorder_intervals(dt, dt + delta)
|
||||
for start, stop, dummy in available_intervals:
|
||||
interval_minutes = (stop - start).total_seconds() / 60
|
||||
# If the remaining minutes has never decrease update start_interval
|
||||
if remaining == duration:
|
||||
start_interval = start
|
||||
# If there is a overlap between the possible available interval and a others WO
|
||||
if Intervals([(start_interval, start + timedelta(minutes=min(remaining, interval_minutes)), dummy)]) & workorder_intervals:
|
||||
remaining = duration
|
||||
start_interval = start
|
||||
elif float_compare(interval_minutes, remaining, precision_digits=3) >= 0:
|
||||
return revert(start_interval), revert(start + timedelta(minutes=remaining))
|
||||
# Decrease a part of the remaining duration
|
||||
remaining -= interval_minutes
|
||||
return False, 'Not available slot 700 days after the planned start'
|
||||
|
||||
|
||||
class MrpWorkcenterProductivityLossType(models.Model):
|
||||
_name = "mrp.workcenter.productivity.loss.type"
|
||||
|
||||
@@ -159,7 +159,7 @@ class MrpWorkorder(models.Model):
|
||||
previous_wo = previous_wo_dict.get(wo.id)
|
||||
prev_start = previous_wo and previous_wo['date_planned_start'] or False
|
||||
prev_finished = previous_wo and previous_wo['date_planned_finished'] or False
|
||||
if wo.state == 'pending' and prev_start and not (prev_start > wo.date_planned_finished):
|
||||
if wo.state == 'pending' and prev_start and not (prev_start > wo.date_planned_start):
|
||||
infos.append({
|
||||
'color': 'text-primary',
|
||||
'msg': _("Waiting the previous work order, planned from %s to %s") % (
|
||||
@@ -171,7 +171,7 @@ class MrpWorkorder(models.Model):
|
||||
'color': 'text-warning',
|
||||
'msg': _("The work order should have already been processed.")
|
||||
})
|
||||
if prev_start and prev_start > wo.date_planned_finished:
|
||||
if prev_start and prev_start > wo.date_planned_start:
|
||||
infos.append({
|
||||
'color': 'text-danger',
|
||||
'msg': _("Scheduled before the previous work order, planned from %s to %s") % (
|
||||
|
||||
@@ -1023,6 +1023,52 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
|
||||
self.assertAlmostEqual(workorder.date_planned_start, date_start, delta=timedelta(seconds=1), msg="Workorder should be planned tomorrow.")
|
||||
self.assertAlmostEqual(workorder.date_planned_finished, date_start + timedelta(hours=1), delta=timedelta(seconds=1), msg="Workorder should be done one hour later.")
|
||||
|
||||
def test_planning_overlaps_wo(self):
|
||||
""" Test that workorder doesn't overlaps between then when plan the MO """
|
||||
self.full_availability()
|
||||
|
||||
dining_table = self.dining_table
|
||||
|
||||
# Take between +30min -> +90min
|
||||
date_start = datetime.now() + timedelta(minutes=30)
|
||||
|
||||
production_table_form = Form(self.env['mrp.production'])
|
||||
production_table_form.product_id = dining_table
|
||||
production_table_form.bom_id = self.mrp_bom_desk
|
||||
production_table_form.product_qty = 1.0
|
||||
production_table_form.product_uom_id = dining_table.uom_id
|
||||
production_table_form.date_planned_start = date_start
|
||||
production_table = production_table_form.save()
|
||||
production_table.action_confirm()
|
||||
|
||||
# Create work order
|
||||
production_table.button_plan()
|
||||
workorder_prev = production_table.workorder_ids[0]
|
||||
|
||||
# Check that the workorder is planned now and that it lasts one hour
|
||||
self.assertAlmostEqual(workorder_prev.date_planned_start, date_start, delta=timedelta(seconds=10), msg="Workorder should be planned in +30min")
|
||||
self.assertAlmostEqual(workorder_prev.date_planned_finished, date_start + timedelta(hours=1), delta=timedelta(seconds=10), msg="Workorder should be done in +90min")
|
||||
|
||||
# As soon as possible, but because of the first one, it will planned only after +90 min
|
||||
date_start = datetime.now()
|
||||
|
||||
production_table_form = Form(self.env['mrp.production'])
|
||||
production_table_form.product_id = dining_table
|
||||
production_table_form.bom_id = self.mrp_bom_desk
|
||||
production_table_form.product_qty = 1.0
|
||||
production_table_form.product_uom_id = dining_table.uom_id
|
||||
production_table_form.date_planned_start = date_start
|
||||
production_table = production_table_form.save()
|
||||
production_table.action_confirm()
|
||||
|
||||
# Create work order
|
||||
production_table.button_plan()
|
||||
workorder = production_table.workorder_ids[0]
|
||||
|
||||
# Check that the workorder is planned now and that it lasts one hour
|
||||
self.assertAlmostEqual(workorder.date_planned_start, workorder_prev.date_planned_finished, delta=timedelta(seconds=10), msg="Workorder should be planned after the first one")
|
||||
self.assertAlmostEqual(workorder.date_planned_finished, workorder_prev.date_planned_finished + timedelta(hours=1), delta=timedelta(seconds=10), msg="Workorder should be done one hour later.")
|
||||
|
||||
def test_change_production_1(self):
|
||||
"""Change the quantity to produce on the MO while workorders are already planned."""
|
||||
dining_table = self.dining_table
|
||||
@@ -1369,7 +1415,7 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
|
||||
self.assertEqual(line3.product_id, self.product_1)
|
||||
self.assertEqual(line3.qty_done, 4)
|
||||
|
||||
def test_replan(self):
|
||||
def test_conflict_and_replan(self):
|
||||
""" TEST Json data conflicted and the replan button of a workorder """
|
||||
self.routing_1.operation_ids[0].write({'workcenter_id': self.mrp_workcenter_3.id})
|
||||
dining_table = self.dining_table
|
||||
|
||||
Reference in New Issue
Block a user