[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 Furthermore, planning workorder is done thanks to the resource module. Each workorder is saved on its own workcenter calendar by a resource.calendar.leaves The two date fields are thus computed from the leave date. Task : 1892819 closes odoo/odoo#33018 Signed-off-by: Simon Lejeune (sle) <sle@openerp.com>
This commit is contained in:
@@ -1,15 +1,14 @@
|
||||
# -*- 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
|
||||
import datetime
|
||||
from itertools import groupby
|
||||
|
||||
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 """
|
||||
@@ -624,54 +623,73 @@ class MrpProduction(models.Model):
|
||||
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()
|
||||
return self.date_start_wo or datetime.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')
|
||||
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
|
||||
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})
|
||||
start_date = self._get_start_date()
|
||||
for workorder in self.workorder_ids:
|
||||
workcenters = workorder.workcenter_id | workorder.workcenter_id.alternative_workcenter_ids
|
||||
|
||||
if (workorder.operation_id.batch == 'no') or (workorder.operation_id.batch_size >= workorder.qty_production):
|
||||
start_date = to_date
|
||||
best_finished_date = datetime.datetime.max
|
||||
vals = {}
|
||||
for workcenter in workcenters:
|
||||
# compute theoretical duration
|
||||
time_cycle = workorder.operation_id.time_cycle
|
||||
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 * 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'])])
|
||||
# If the workcenter is unavailable, try planning on the next one
|
||||
if from_date is False:
|
||||
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 < best_finished_date:
|
||||
best_start_date = from_date
|
||||
best_finished_date = to_date
|
||||
best_workcenter = workcenter
|
||||
vals = {
|
||||
'workcenter_id': workcenter.id,
|
||||
'capacity': workcenter.capacity,
|
||||
'duration_expected': duration_expected,
|
||||
}
|
||||
|
||||
# If none of the workcenter are available, raise
|
||||
if best_finished_date == datetime.datetime.max:
|
||||
raise UserError(_('Impossible to plan the workorder. Please check the workcenter availabilities.'))
|
||||
|
||||
# Instantiate start_date for the next workorder planning
|
||||
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)
|
||||
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)
|
||||
cycle_number = float_round(workorder.operation_id.batch_size / best_workcenter.capacity, precision_digits=0, rounding_method='UP')
|
||||
duration = best_workcenter.time_start + cycle_number * workorder.operation_id.time_cycle * 100.0 / best_workcenter.time_efficiency
|
||||
start_date = best_workcenter.resource_calendar_id.plan_hours(duration / 60.0, best_start_date, compute_leaves=True, resource=best_workcenter.resource_id, domain=[('time_type', 'in', ['leave', 'other'])])
|
||||
|
||||
# Create leave on choosen workcenter calendar
|
||||
leave = self.env['resource.calendar.leaves'].create({
|
||||
'name': self.name + ' - ' + workorder.name,
|
||||
'calendar_id': best_workcenter.resource_calendar_id.id,
|
||||
'date_from': best_start_date,
|
||||
'date_to': best_finished_date,
|
||||
'resource_id': best_workcenter.resource_id.id,
|
||||
'time_type': 'other'
|
||||
})
|
||||
vals['leave_id'] = leave.id
|
||||
workorder.write(vals)
|
||||
|
||||
def button_unplan(self):
|
||||
if any(wo.state == 'done' for wo in self.workorder_ids):
|
||||
@@ -701,35 +719,23 @@ class MrpProduction(models.Model):
|
||||
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 @@ from dateutil import relativedelta
|
||||
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',
|
||||
'alternative_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):
|
||||
|
||||
@@ -49,11 +49,20 @@ class MrpWorkorder(models.Model):
|
||||
('done', 'Finished'),
|
||||
('cancel', 'Cancelled')], string='Status',
|
||||
default='pending')
|
||||
leave_id = fields.Many2one(
|
||||
'resource.calendar.leaves',
|
||||
help='Slot into workcenter calendar once planned')
|
||||
date_planned_start = fields.Datetime(
|
||||
'Scheduled Date Start',
|
||||
compute='_compute_dates_planned',
|
||||
inverse='_set_dates_planned',
|
||||
search='_search_date_planned_start',
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
|
||||
date_planned_finished = fields.Datetime(
|
||||
'Scheduled Date Finished',
|
||||
compute='_compute_dates_planned',
|
||||
inverse='_set_dates_planned',
|
||||
search='_search_date_planned_finished',
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
|
||||
date_start = fields.Datetime(
|
||||
'Effective Start Date',
|
||||
@@ -114,6 +123,33 @@ class MrpWorkorder(models.Model):
|
||||
'finished_workorder_id', string='By-products')
|
||||
allowed_lots_domain = fields.One2many(comodel_name='stock.production.lot', compute="_compute_allowed_lots_domain")
|
||||
|
||||
# Both `date_planned_start` and `date_planned_finished` are related fields on `leave_id`. Let's say
|
||||
# we slide a workorder on a gantt view, a single call to write is made with both
|
||||
# fields Changes. As the ORM doesn't batch the write on related fields and instead
|
||||
# makes multiple call, the constraint check_dates() is raised.
|
||||
# That's why the compute and set methods are needed. to ensure the dates are updated
|
||||
# in the same time. The two next search method are needed as the field are non stored and
|
||||
# not direct related fields.
|
||||
@api.depends('leave_id')
|
||||
def _compute_dates_planned(self):
|
||||
for workorder in self:
|
||||
workorder.date_planned_start = workorder.leave_id.date_from
|
||||
workorder.date_planned_finished = workorder.leave_id.date_to
|
||||
|
||||
def _set_dates_planned(self):
|
||||
date_from = self.date_planned_start[0]
|
||||
date_to = self.date_planned_finished[0]
|
||||
self.mapped('leave_id').write({
|
||||
'date_from': date_from,
|
||||
'date_to': date_to,
|
||||
})
|
||||
|
||||
def _search_date_planned_start(self, operator, value):
|
||||
return [('leave_id.date_from', operator, value)]
|
||||
|
||||
def _search_date_planned_finished(self, operator, value):
|
||||
return [('leave_id.date_to', operator, value)]
|
||||
|
||||
@api.onchange('finished_lot_id')
|
||||
def _onchange_finished_lot_id(self):
|
||||
"""When the user changes the lot being currently produced, suggest
|
||||
|
||||
@@ -2,18 +2,56 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import Form
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
|
||||
|
||||
class TestWorkOrderProcess(TestMrpCommon):
|
||||
def full_availability(self):
|
||||
"""set full availability for all calendars"""
|
||||
self.env['resource.calendar'].search([]).write({'attendance_ids': [
|
||||
(0, 0, {'name': 'Monday', 'dayofweek': '0', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Tuesday', 'dayofweek': '1', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Wednesday', 'dayofweek': '2', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Thursday', 'dayofweek': '3', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Friday', 'dayofweek': '4', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Saturday', 'dayofweek': '5', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Sunday', 'dayofweek': '6', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
|
||||
]})
|
||||
|
||||
def setUp(self):
|
||||
super(TestWorkOrderProcess, self).setUp()
|
||||
self.source_location_id = self.ref('stock.stock_location_14')
|
||||
self.warehouse = self.env.ref('stock.warehouse0')
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestWorkOrderProcess, cls).setUpClass()
|
||||
cls.source_location_id = cls.env.ref('stock.stock_location_14').id
|
||||
cls.warehouse = cls.env.ref('stock.warehouse0')
|
||||
# setting up alternative workcenters
|
||||
cls.wc_alt_1 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Nuclear Workcenter bis',
|
||||
'capacity': 3,
|
||||
'time_start': 9,
|
||||
'time_stop': 5,
|
||||
'time_efficiency': 80,
|
||||
})
|
||||
cls.wc_alt_2 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Nuclear Workcenter ter',
|
||||
'capacity': 1,
|
||||
'time_start': 10,
|
||||
'time_stop': 5,
|
||||
'time_efficiency': 85,
|
||||
})
|
||||
cls.product_4.uom_id = cls.uom_unit
|
||||
cls.planning_bom = cls.env['mrp.bom'].create({
|
||||
'product_id': cls.product_4.id,
|
||||
'product_tmpl_id': cls.product_4.product_tmpl_id.id,
|
||||
'product_uom_id': cls.uom_unit.id,
|
||||
'product_qty': 4.0,
|
||||
'routing_id': cls.routing_1.id,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.product_2.id, 'product_qty': 2}),
|
||||
(0, 0, {'product_id': cls.product_1.id, 'product_qty': 4})
|
||||
]})
|
||||
|
||||
def test_00_workorder_process(self):
|
||||
""" Testing consume quants and produced quants with workorder """
|
||||
@@ -352,9 +390,9 @@ class TestWorkOrderProcess(TestMrpCommon):
|
||||
self.assertEqual(kit_wo.state, 'ready', "Workorder should be in ready state.")
|
||||
self.assertEqual(door_wo_1.state, 'ready', "Workorder should be in ready state.")
|
||||
self.assertEqual(door_wo_2.state, 'pending', "Workorder should be in pending state.")
|
||||
self.assertEqual(kit_wo.duration_expected, 80, "Workorder duration should be 80 instead of %s." % str(kit_wo.duration_expected))
|
||||
self.assertEqual(door_wo_1.duration_expected, 20, "Workorder duration should be 20 instead of %s." % str(door_wo_1.duration_expected))
|
||||
self.assertEqual(door_wo_2.duration_expected, 20, "Workorder duration should be 20 instead of %s." % str(door_wo_2.duration_expected))
|
||||
self.assertEqual(kit_wo.duration_expected, 960, "Workorder duration should be 960 instead of %s." % str(kit_wo.duration_expected))
|
||||
self.assertEqual(door_wo_1.duration_expected, 480, "Workorder duration should be 480 instead of %s." % str(door_wo_1.duration_expected))
|
||||
self.assertEqual(door_wo_2.duration_expected, 480, "Workorder duration should be 480 instead of %s." % str(door_wo_2.duration_expected))
|
||||
|
||||
# subbom: kit for stone tools
|
||||
kit_wo.button_start()
|
||||
@@ -781,8 +819,8 @@ class TestWorkOrderProcess(TestMrpCommon):
|
||||
|
||||
def test_04_test_planning_date(self):
|
||||
""" Test that workorder are planned at the correct time. """
|
||||
# Remove attendances linked to the calendar, this means that the workcenter is working 24/7
|
||||
self.env['resource.calendar'].search([]).write({'attendance_ids': [(5, False, False)]})
|
||||
# The workcenter is working 24/7
|
||||
self.full_availability()
|
||||
|
||||
dining_table = self.env.ref("mrp.product_product_computer_desk")
|
||||
|
||||
@@ -804,8 +842,8 @@ class TestWorkOrderProcess(TestMrpCommon):
|
||||
|
||||
def test_04b_test_planning_date(self):
|
||||
""" Test that workorder are planned at the correct time when setting a start date """
|
||||
# Remove attendances linked to the calendar, this means that the workcenter is working 24/7
|
||||
self.env['resource.calendar'].search([]).write({'attendance_ids': [(5, False, False)]})
|
||||
# The workcenter is working 24/7
|
||||
self.full_availability()
|
||||
|
||||
dining_table = self.env.ref("mrp.product_product_computer_desk")
|
||||
|
||||
@@ -827,3 +865,147 @@ class TestWorkOrderProcess(TestMrpCommon):
|
||||
# Check that the workorder is planned now and that it lasts one hour
|
||||
self.assertEqual(workorder.date_planned_start, date_start, msg="Workorder should be planned tomorrow.")
|
||||
self.assertEqual(workorder.date_planned_finished, date_start + timedelta(hours=1), msg="Workorder should be done one hour later.")
|
||||
|
||||
def test_planning_0(self):
|
||||
""" Test alternative conditions
|
||||
1. alternative relation is directionnal
|
||||
2. a workcenter cannot be it's own alternative """
|
||||
self.workcenter_1.alternative_workcenter_ids = self.wc_alt_1 | self.wc_alt_2
|
||||
self.assertEqual(self.wc_alt_1.alternative_workcenter_ids, self.env['mrp.workcenter'], "Alternative workcenter is not reciprocal")
|
||||
self.assertEqual(self.wc_alt_2.alternative_workcenter_ids, self.env['mrp.workcenter'], "Alternative workcenter is not reciprocal")
|
||||
with self.assertRaises(ValidationError):
|
||||
self.workcenter_1.alternative_workcenter_ids |= self.workcenter_1
|
||||
|
||||
def test_planning_1(self):
|
||||
""" Testing planning workorder with alternative workcenters
|
||||
Plan 6 times the same MO, the workorders should be split accross workcenters
|
||||
The 3 workcenters are free, this test plans 3 workorder in a row then three next.
|
||||
The workcenters have not exactly the same parameters (efficiency, start time) so the
|
||||
the last 3 workorder are not dispatched like the 3 first.
|
||||
At the end of the test, the calendars will look like:
|
||||
- calendar wc1 :[mo1][mo4]
|
||||
- calendar wc2 :[mo2 ][mo5 ]
|
||||
- calendar wc3 :[mo3 ][mo6 ]"""
|
||||
self.workcenter_1.alternative_workcenter_ids = self.wc_alt_1 | self.wc_alt_2
|
||||
workcenters = [self.wc_alt_2, self.wc_alt_1, self.workcenter_1]
|
||||
for i in range(3):
|
||||
# Create an MO for product4
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.product_4
|
||||
mo_form.bom_id = self.planning_bom
|
||||
mo_form.product_qty = 1
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.button_plan()
|
||||
# Check that workcenters change
|
||||
self.assertEqual(mo.workorder_ids.workcenter_id, workcenters[i], "wrong workcenter %d" % i)
|
||||
|
||||
for i in range(3):
|
||||
# Planning 3 more should choose workcenters in opposite order as
|
||||
# - wc_alt_2 as the best efficiency
|
||||
# - wc_alt_1 take a little less start time
|
||||
# - workcenter_1 is the worst
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.product_4
|
||||
mo_form.bom_id = self.planning_bom
|
||||
mo_form.product_qty = 1
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.button_plan()
|
||||
# Check that workcenters change
|
||||
self.assertEqual(mo.workorder_ids.workcenter_id, workcenters[i], "wrong workcenter %d" % i)
|
||||
|
||||
def test_planning_2(self):
|
||||
""" Plan some manufacturing orders with 2 workorders each
|
||||
Batch size of the operation will influence start dates of workorders
|
||||
The first unit to be produced can go the second workorder before finishing
|
||||
to produce the second unit.
|
||||
calendar wc1 : [q1][q2]
|
||||
calendar wc2 : [q1][q2]"""
|
||||
self.workcenter_1.alternative_workcenter_ids = self.wc_alt_1 | self.wc_alt_2
|
||||
self.planning_bom.routing_id = self.routing_2
|
||||
# Allow second workorder to start once the first one is not ended yet
|
||||
self.operation_2.batch = 'yes'
|
||||
self.operation_2.batch_size = 1
|
||||
self.env['mrp.workcenter'].search([]).write({'capacity': 1})
|
||||
# workcenters work 24/7
|
||||
self.full_availability()
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.product_4
|
||||
mo_form.bom_id = self.planning_bom
|
||||
mo_form.product_qty = 2
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
plan = datetime.now()
|
||||
mo.button_plan()
|
||||
self.assertEqual(mo.workorder_ids[0].workcenter_id, self.wc_alt_2, "wrong workcenter")
|
||||
self.assertEqual(mo.workorder_ids[1].workcenter_id, self.wc_alt_1, "wrong workcenter")
|
||||
|
||||
duration1 = self.operation_2.time_cycle * 100.0 / self.wc_alt_2.time_efficiency + self.wc_alt_2.time_start
|
||||
duration2 = 2.0 * self.operation_2.time_cycle * 100.0 / self.wc_alt_1.time_efficiency + self.wc_alt_1.time_start + self.wc_alt_1.time_stop
|
||||
wo2_start = mo.workorder_ids[1].date_planned_start
|
||||
wo2_stop = mo.workorder_ids[1].date_planned_finished
|
||||
|
||||
wo2_start_theo = self.wc_alt_2.resource_calendar_id.plan_hours(duration1 / 60.0, plan, compute_leaves=False, resource=self.wc_alt_2.resource_id)
|
||||
wo2_stop_theo = self.wc_alt_1.resource_calendar_id.plan_hours(duration2 / 60.0, wo2_start, compute_leaves=False, resource=self.wc_alt_2.resource_id)
|
||||
|
||||
self.assertAlmostEqual(wo2_start, wo2_start_theo, delta=timedelta(seconds=10), msg="Wrong plannification")
|
||||
self.assertAlmostEqual(wo2_stop, wo2_stop_theo, delta=timedelta(seconds=10), msg="Wrong plannification")
|
||||
|
||||
def test_planning_3(self):
|
||||
""" Plan some manufacturing orders with 1 workorder on 1 workcenter
|
||||
the first workorder will be hard set in the future to see if the second
|
||||
one take the free slot before on the calendar
|
||||
calendar after first mo : [ ][mo1]
|
||||
calendar after second mo: [mo2][mo1] """
|
||||
|
||||
self.workcenter_1.alternative_workcenter_ids = self.wc_alt_1 | self.wc_alt_2
|
||||
self.env['mrp.workcenter'].search([]).write({'tz': 'UTC'}) # compute all date in UTC
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.product_4
|
||||
mo_form.bom_id = self.planning_bom
|
||||
mo_form.product_qty = 1
|
||||
mo_form.date_start_wo = datetime(2019, 5, 13, 14, 0, 0, 0)
|
||||
mo = mo_form.save()
|
||||
start = mo.date_start_wo
|
||||
mo.action_confirm()
|
||||
mo.button_plan()
|
||||
self.assertEqual(mo.workorder_ids[0].workcenter_id, self.wc_alt_2, "wrong workcenter")
|
||||
wo1_start = mo.workorder_ids[0].date_planned_start
|
||||
wo1_stop = mo.workorder_ids[0].date_planned_finished
|
||||
self.assertAlmostEqual(wo1_start, start, delta=timedelta(seconds=10), msg="Wrong plannification")
|
||||
self.assertAlmostEqual(wo1_stop, start + timedelta(minutes=85.58), delta=timedelta(seconds=10), msg="Wrong plannification")
|
||||
|
||||
# second MO should be plan before as there is a free slot before
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.product_4
|
||||
mo_form.bom_id = self.planning_bom
|
||||
mo_form.product_qty = 1
|
||||
mo_form.date_start_wo = datetime(2019, 5, 13, 9, 0, 0, 0)
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.button_plan()
|
||||
self.assertEqual(mo.workorder_ids[0].workcenter_id, self.wc_alt_2, "wrong workcenter")
|
||||
wo1_start = mo.workorder_ids[0].date_planned_start
|
||||
wo1_stop = mo.workorder_ids[0].date_planned_finished
|
||||
self.assertAlmostEqual(wo1_start, datetime(2019, 5, 13, 9, 0, 0, 0), delta=timedelta(seconds=10), msg="Wrong plannification")
|
||||
self.assertAlmostEqual(wo1_stop, datetime(2019, 5, 13, 9, 0, 0, 0) + timedelta(minutes=85.59), delta=timedelta(seconds=10), msg="Wrong plannification")
|
||||
|
||||
def test_planning_4(self):
|
||||
""" Plan a manufacturing orders with 1 workorder on 1 workcenter
|
||||
the workcenter calendar is empty. which means the workcenter is never
|
||||
available. Planning a workorder on it should raise an error"""
|
||||
|
||||
self.workcenter_1.alternative_workcenter_ids = self.wc_alt_1 | self.wc_alt_2
|
||||
self.env['resource.calendar'].search([]).write({'attendance_ids': [(5, False, False)]})
|
||||
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.product_4
|
||||
mo_form.bom_id = self.planning_bom
|
||||
mo_form.product_qty = 1
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
with self.assertRaises(UserError):
|
||||
mo.button_plan()
|
||||
|
||||
@@ -329,6 +329,11 @@
|
||||
<group>
|
||||
<group>
|
||||
<field name="name" string="Work Center Name" required="True"/>
|
||||
<field
|
||||
name="alternative_workcenter_ids"
|
||||
widget="many2many_tags"
|
||||
domain="[('id','!=', id)]"
|
||||
/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="code"/>
|
||||
@@ -339,7 +344,7 @@
|
||||
<notebook>
|
||||
<page string="General Information">
|
||||
<group>
|
||||
<group name="capacity">
|
||||
<group string="Production Information" name="capacity">
|
||||
<label for="time_efficiency"/>
|
||||
<div class="o_row">
|
||||
<field name="time_efficiency"/> %
|
||||
|
||||
Reference in New Issue
Block a user