[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:
Rémy Voet (ryv)
2020-01-22 12:31:40 +00:00
parent cbab786eb2
commit 0f679c66ba
4 changed files with 100 additions and 13 deletions
+6 -10
View File
@@ -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
+45
View File
@@ -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"
+2 -2
View File
@@ -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") % (
+47 -1
View File
@@ -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