Skip to content
Permalink
Browse files

[IMP] mrp: alternative workcenters

Purpose: link workcenters that can do the same job together.
Planning a workorder will be done by choosing, among the similar
workcenters, the one that will finish the production the soonest.

This commit adds a new many2many field on the mrp.workcenter
model. Planning the workorders will:
 1. Loop on the operation's workcenter and all the alternative ones.
 2. Compute the first available work slot for each of those workcenters.
 3. Choose the soonest finished slot

Task : 1892819
  • Loading branch information...
Whenrow committed May 15, 2019
1 parent e1424d4 commit 4dc77a17eedf5c3079fcde4bac46bee65599d227
@@ -1,15 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from collections import defaultdict
from datetime import datetime
from dateutil.relativedelta import relativedelta
import math

from odoo import api, fields, models, _
from odoo.addons import decimal_precision as dp
from odoo.exceptions import AccessError, UserError
from odoo.tools import float_compare, float_round, DEFAULT_SERVER_DATETIME_FORMAT
from odoo.tools import date_utils, float_round


class MrpProduction(models.Model):
""" Manufacturing Orders """
@@ -610,54 +608,88 @@ def button_plan(self):
quantity = order.product_uom_id._compute_quantity(order.product_qty, order.bom_id.product_uom_id) / order.bom_id.product_qty
boms, lines = order.bom_id.explode(order.product_id, quantity, picking_type=order.bom_id.picking_type_id)
order._generate_workorders(boms)
self.plan_workorders()
order._plan_workorders()
return True

def _get_start_date(self):
return self.date_start_wo or datetime.now()

def plan_workorders(self):
WorkOrder = self.env['mrp.workorder']
ProductUom = self.env['uom.uom']
for order in self.filtered(lambda x: x.state == 'planned'):
order.workorder_ids.write({'date_planned_start': False, 'date_planned_finished': False})
def _plan_workorders(self):
""" Plan all the production's workorders depending on the workcenters
work schedule"""
self.ensure_one()

# Schedule all work orders (new ones and those already created)
for order in self:
start_date = order._get_start_date()
from_date_set = False
for workorder in order.workorder_ids:
workcenter = workorder.workcenter_id
wos = WorkOrder.search([('workcenter_id', '=', workcenter.id), ('date_planned_finished', '<>', False),
('state', 'in', ('ready', 'pending', 'progress')),
('date_planned_finished', '>=', start_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT))], order='date_planned_start')
start_date = self._get_start_date()
for workorder in self.workorder_ids:
workcenters = workorder.workcenter_id | workorder.workcenter_id.alternative_workcenter_ids

# extract already planned workorders
wos = self.env['mrp.workorder'].read_group([
('workcenter_id', 'in', workcenters.ids),
('state', 'in', ('ready', 'pending', 'progress')),
('date_planned_finished', '>=', start_date)],
['workcenter_id', 'date_planned_start:array_agg', 'date_planned_finished:array_agg'],
['workcenter_id'],
orderby='date_planned_finished'
)
wos = {wo['workcenter_id'][0]: zip(wo['date_planned_start'], wo['date_planned_finished']) for wo in wos}
best_finished_date = datetime.max
best_values = {}
for workcenter in workcenters:
# get duration expected for this workorder
operation = workorder.operation_id
cycle_number = float_round(workorder.qty_producing / workcenter.capacity, precision_digits=0, rounding_method='UP')
duration_expected = workcenter.time_start + workcenter.time_stop + cycle_number * operation.time_cycle * 100.0 / workcenter.time_efficiency

from_date = start_date
to_date = workcenter.resource_calendar_id.attendance_ids and workcenter.resource_calendar_id.plan_hours(workorder.duration_expected / 60.0, from_date, compute_leaves=True, resource=workcenter.resource_id)
if to_date:
if not from_date_set:
# planning 0 hours gives the start of the next attendance
from_date = workcenter.resource_calendar_id.plan_hours(0, from_date, compute_leaves=True, resource=workcenter.resource_id)
from_date_set = True
# Check if the workcenter is subject to a schedule
workcenter_calendar = workcenter.resource_calendar_id
if workcenter_calendar.attendance_ids:
# planning 0 hours gives the start of the next attendance
from_date = workcenter_calendar.plan_hours(0, from_date, compute_leaves=True, resource=workcenter.resource_id)
to_date = workcenter_calendar.plan_hours(duration_expected / 60.0, from_date, compute_leaves=True, resource=workcenter.resource_id)
else:
to_date = from_date + relativedelta(minutes=workorder.duration_expected)
# Check interval
for wo in wos:
if from_date < fields.Datetime.from_string(wo.date_planned_finished) and (to_date > fields.Datetime.from_string(wo.date_planned_start)):
from_date = fields.Datetime.from_string(wo.date_planned_finished)
to_date = workcenter.resource_calendar_id.attendance_ids and workcenter.resource_calendar_id.plan_hours(workorder.duration_expected / 60.0, from_date, compute_leaves=True, resource=workcenter.resource_id)
if not to_date:
to_date = from_date + relativedelta(minutes=workorder.duration_expected)
workorder.write({'date_planned_start': from_date, 'date_planned_finished': to_date})

if (workorder.operation_id.batch == 'no') or (workorder.operation_id.batch_size >= workorder.qty_production):
start_date = to_date
to_date = date_utils.add(from_date, minutes=duration_expected)
# Check intervals
for date_start, date_finished in wos.get(workcenter.id, []):
if best_finished_date < date_finished or to_date <= date_start:
# 1.workorders are ordered by date. There is no need to check the other ones
# or 2.The current time slot (from_date, to_date) intersect an existing workorder slot
break
else:
from_date = date_finished
if workcenter_calendar.attendance_ids:
to_date = workcenter_calendar.plan_hours(duration_expected / 60.0, from_date, compute_leaves=True, resource=workcenter.resource_id)
else:
to_date = date_utils.add(from_date, minutes=duration_expected)

if to_date < best_finished_date:
best_start_date = from_date
best_finished_date = to_date
best_values = {
'date_planned_start': best_start_date,
'date_planned_finished': best_finished_date,
'workcenter_id': workcenter.id,
'capacity': workcenter.capacity,
'duration_expected': duration_expected,
}

workorder.write(best_values)

# Compute start_date for next workorder
if workorder.next_work_order_id:
if (workorder.operation_id.batch == 'no') or (workorder.operation_id.batch_size >= workorder.qty_producing):
start_date = best_finished_date
else:
qty = min(workorder.operation_id.batch_size, workorder.qty_production)
cycle_number = math.ceil(qty / workorder.production_id.product_qty / workcenter.capacity)
workcenter = workorder.workcenter_id
cycle_number = float_round(workorder.operation_id.batch_size / workcenter.capacity, precision_digits=0, rounding_method='UP')
duration = workcenter.time_start + cycle_number * workorder.operation_id.time_cycle * 100.0 / workcenter.time_efficiency
to_date = workcenter.resource_calendar_id.attendance_ids and workcenter.resource_calendar_id.plan_hours(duration / 60.0, from_date, compute_leaves=True, resource=workcenter.resource_id)
if not to_date:
start_date = from_date + relativedelta(minutes=duration)
workcenter_calendar = workcenter.resource_calendar_id
if workcenter_calendar.attendance_ids:
start_date = workcenter_calendar.plan_hours(duration / 60.0, best_start_date, compute_leaves=True, resource=workcenter.resource_id)
else:
start_date = date_utils.add(best_start_date, minutes=duration)

def button_unplan(self):
if any(wo.state == 'done' for wo in self.workorder_ids):
@@ -687,35 +719,23 @@ def _workorders_create(self, bom, bom_data):
BoMs
"""
workorders = self.env['mrp.workorder']
bom_qty = bom_data['qty']

# Initial qty producing
quantity = max(self.product_qty - sum(self.move_finished_ids.filtered(lambda move: move.product_id == self.product_id).mapped('quantity_done')), 0)
quantity = self.product_id.uom_id._compute_quantity(quantity, self.product_uom_id)
if self.product_id.tracking == 'serial':
quantity = 1.0
else:
quantity = self.product_qty - sum(self.move_finished_ids.mapped('quantity_done'))
quantity = quantity if (quantity > 0) else 0

for operation in bom.routing_id.operation_ids:
# create workorder
cycle_number = float_round(bom_qty / operation.workcenter_id.capacity, precision_digits=0, rounding_method='UP')
duration_expected = (operation.workcenter_id.time_start +
operation.workcenter_id.time_stop +
cycle_number * operation.time_cycle * 100.0 / operation.workcenter_id.time_efficiency)
if self.product_uom_id.uom_type != 'reference':
todo_uom = self.env['uom.uom'].search([('category_id', '=', self.product_uom_id.category_id.id), ('uom_type', '=', 'reference')]).id
else:
todo_uom = self.product_uom_id.id
workorder = workorders.create({
'name': operation.name,
'production_id': self.id,
'workcenter_id': operation.workcenter_id.id,
'product_uom_id': todo_uom,
'product_uom_id': self.product_id.uom_id.id,
'operation_id': operation.id,
'duration_expected': duration_expected,
'state': len(workorders) == 0 and 'ready' or 'pending',
'qty_producing': quantity,
'capacity': operation.workcenter_id.capacity,
'consumption': self.bom_id.consumption,
})
if workorders:
@@ -5,6 +5,7 @@
import datetime

from odoo import api, exceptions, fields, models, _
from odoo.exceptions import ValidationError


class MrpWorkcenter(models.Model):
@@ -55,6 +56,19 @@ class MrpWorkcenter(models.Model):
oee_target = fields.Float(string='OEE Target', help="OEE Target in percentage", default=90)
performance = fields.Integer('Performance', compute='_compute_performance', help='Performance over the last month')
workcenter_load = fields.Float('Work Center Load', compute='_compute_workorder_count')
alternative_workcenter_ids = fields.Many2many(
'mrp.workcenter',
'mrp_workcenter_alternative_rel',
'workcenter_id',
'alternativ_workcenter_id',
string="Alternative Workcenters",
help="Alternative workcenters that can be substituted to this one in order to dispatch production"
)

@api.constrains('alternative_workcenter_ids')
def _check_alternative_workcenter(self):
if self in self.alternative_workcenter_ids:
raise ValidationError(_("A workcenter cannot be an alternative of itself"))

@api.depends('order_ids.duration_expected', 'order_ids.workcenter_id', 'order_ids.state', 'order_ids.date_planned_start')
def _compute_workorder_count(self):
Oops, something went wrong.

0 comments on commit 4dc77a1

Please sign in to comment.
You can’t perform that action at this time.