[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:
William Henrotin
2019-05-28 06:50:58 +00:00
parent 6fad651883
commit 0679d0d542
5 changed files with 318 additions and 75 deletions
+66 -60
View File
@@ -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:
+14
View File
@@ -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):
+36
View File
@@ -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
+196 -14
View File
@@ -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()
+6 -1
View File
@@ -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"/> %