diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 7d809285a1c..b13bcf51932 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -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: diff --git a/addons/mrp/models/mrp_workcenter.py b/addons/mrp/models/mrp_workcenter.py index 690b4c41fa1..ee1ac0dd19b 100644 --- a/addons/mrp/models/mrp_workcenter.py +++ b/addons/mrp/models/mrp_workcenter.py @@ -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): diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py index 7a8676ef016..28272ece51f 100644 --- a/addons/mrp/models/mrp_workorder.py +++ b/addons/mrp/models/mrp_workorder.py @@ -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 diff --git a/addons/mrp/tests/test_workorder_operation.py b/addons/mrp/tests/test_workorder_operation.py index 4f921c7e7d3..d3ccb8ba649 100644 --- a/addons/mrp/tests/test_workorder_operation.py +++ b/addons/mrp/tests/test_workorder_operation.py @@ -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() diff --git a/addons/mrp/views/mrp_workcenter_views.xml b/addons/mrp/views/mrp_workcenter_views.xml index 2593877c16a..7d3d99dce6b 100644 --- a/addons/mrp/views/mrp_workcenter_views.xml +++ b/addons/mrp/views/mrp_workcenter_views.xml @@ -329,6 +329,11 @@ + @@ -339,7 +344,7 @@ - +