From ae7e5960f1050a96567e224b7a8f123fc9dc5e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Voet=20=28ryv=29?= Date: Thu, 27 May 2021 09:59:02 +0000 Subject: [PATCH] [IMP] mrp: update workorder readiness behavior - The work order state changes based on the availability of the components - So if all BOM components are available the state of the first WO is set to ready - The color of ready in the WO tree editable view will stay blue as long as done reserves the green color - If one or multiple components are not available, the state of the first WO is set to waiting - The color of waiting in the WO tree editable view is set to orange By default to launch takes workorder_ready_count so it counts only ready WOs - The user can always process the WO without restrictions even if the WO state is waiting Task-2426773 closes odoo/odoo#71250 Related: odoo/enterprise#18523 Signed-off-by: Arnold Moyaux Co-authored-by: nouraellm --- addons/mrp/models/mrp_production.py | 29 ++++++------ addons/mrp/models/mrp_workcenter.py | 10 +++-- addons/mrp/models/mrp_workorder.py | 32 +++++++++---- addons/mrp/tests/test_order.py | 57 ++++++++++++++++++++++-- addons/mrp/views/mrp_workorder_views.xml | 18 +++++--- 5 files changed, 108 insertions(+), 38 deletions(-) diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index e4ecc6590ca..f6722e849b1 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -423,7 +423,7 @@ class MrpProduction(models.Model): @api.depends( 'move_raw_ids.state', 'move_raw_ids.quantity_done', 'move_finished_ids.state', - 'workorder_ids', 'workorder_ids.state', 'product_qty', 'qty_producing') + 'workorder_ids.state', 'product_qty', 'qty_producing') def _compute_state(self): """ Compute the production state. This uses a similar process to stock picking, but has been adapted to support having no moves. This adaption @@ -452,20 +452,19 @@ class MrpProduction(models.Model): @api.depends('state', 'move_raw_ids.state') def _compute_reservation_state(self): + self.reservation_state = False for production in self: - # Compute reservation state - # State where the reservation does not matter. - production.reservation_state = False + if production.state in ('draft', 'done', 'cancel'): + continue + relevant_move_state = production.move_raw_ids._get_relevant_state_among_moves() # Compute reservation state according to its component's moves. - if production.state not in ('draft', 'done', 'cancel'): - relevant_move_state = production.move_raw_ids._get_relevant_state_among_moves() - if relevant_move_state == 'partially_available': - if production.bom_id.operation_ids and production.bom_id.ready_to_produce == 'asap': - production.reservation_state = production._get_ready_to_produce_state() - else: - production.reservation_state = 'confirmed' - elif relevant_move_state != 'draft': - production.reservation_state = relevant_move_state + if relevant_move_state == 'partially_available': + if production.bom_id.operation_ids and production.bom_id.ready_to_produce == 'asap': + production.reservation_state = production._get_ready_to_produce_state() + else: + production.reservation_state = 'confirmed' + elif relevant_move_state != 'draft': + production.reservation_state = relevant_move_state @api.depends('move_raw_ids', 'state', 'move_raw_ids.product_uom_qty') def _compute_unreserve_visible(self): @@ -1110,7 +1109,7 @@ class MrpProduction(models.Model): """ Plan all the production's workorders depending on the workcenters work schedule. - :param replan: If it is a replan, only ready and pending workorder will be take in account + :param replan: If it is a replan, only ready and pending workorder will be taken into account :type replan: bool. """ self.ensure_one() @@ -1122,7 +1121,7 @@ class MrpProduction(models.Model): qty_to_produce = self.product_uom_id._compute_quantity(qty_to_produce, self.product_id.uom_id) start_date = max(self.date_planned_start, datetime.datetime.now()) if replan: - workorder_ids = self.workorder_ids.filtered(lambda wo: wo.state in ['ready', 'pending']) + workorder_ids = self.workorder_ids.filtered(lambda wo: wo.state in ('pending', 'waiting', 'ready')) # 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. workorder_ids.leave_id.unlink() diff --git a/addons/mrp/models/mrp_workcenter.py b/addons/mrp/models/mrp_workcenter.py index 943a5a68a64..5c65f0f45ab 100644 --- a/addons/mrp/models/mrp_workcenter.py +++ b/addons/mrp/models/mrp_workcenter.py @@ -83,17 +83,19 @@ class MrpWorkcenter(models.Model): MrpWorkorder = self.env['mrp.workorder'] result = {wid: {} for wid in self._ids} result_duration_expected = {wid: 0 for wid in self._ids} - #Count Late Workorder - data = MrpWorkorder.read_group([('workcenter_id', 'in', self.ids), ('state', 'in', ('pending', 'ready')), ('date_planned_start', '<', datetime.datetime.now().strftime('%Y-%m-%d'))], ['workcenter_id'], ['workcenter_id']) + # Count Late Workorder + data = MrpWorkorder.read_group( + [('workcenter_id', 'in', self.ids), ('state', 'in', ('pending', 'waiting', 'ready')), ('date_planned_start', '<', datetime.datetime.now().strftime('%Y-%m-%d'))], + ['workcenter_id'], ['workcenter_id']) count_data = dict((item['workcenter_id'][0], item['workcenter_id_count']) for item in data) - #Count All, Pending, Ready, Progress Workorder + # Count All, Pending, Ready, Progress Workorder res = MrpWorkorder.read_group( [('workcenter_id', 'in', self.ids)], ['workcenter_id', 'state', 'duration_expected'], ['workcenter_id', 'state'], lazy=False) for res_group in res: result[res_group['workcenter_id'][0]][res_group['state']] = res_group['__count'] - if res_group['state'] in ('pending', 'ready', 'progress'): + if res_group['state'] in ('pending', 'waiting', 'ready', 'progress'): result_duration_expected[res_group['workcenter_id'][0]] += res_group['duration_expected'] for workcenter in self: workcenter.workorder_count = sum(count for state, count in result[workcenter.id].items() if state not in ('done', 'cancel')) diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py index cdc1d15530a..0193535cca0 100644 --- a/addons/mrp/models/mrp_workorder.py +++ b/addons/mrp/models/mrp_workorder.py @@ -60,10 +60,12 @@ class MrpWorkorder(models.Model): compute='_compute_is_produced') state = fields.Selection([ ('pending', 'Waiting for another WO'), + ('waiting', 'Waiting for components'), ('ready', 'Ready'), ('progress', 'In Progress'), ('done', 'Finished'), ('cancel', 'Cancelled')], string='Status', + compute='_compute_state', store=True, default='pending', copy=False, readonly=True) leave_id = fields.Many2one( 'resource.calendar.leaves', @@ -142,6 +144,21 @@ class MrpWorkorder(models.Model): show_json_popover = fields.Boolean('Show Popover?', compute='_compute_json_popover') consumption = fields.Selection(related='production_id.consumption') + @api.depends('production_availability') + def _compute_state(self): + # Force the flush of the production_availability, the wo state is modify in the _compute_reservation_state + # It is a trick to force that the state of workorder is computed as the end of the + # cyclic depends with the mo.state, mo.reservation_state and wo.state + for workorder in self: + if workorder.state not in ('waiting', 'ready'): + continue + if workorder.production_id.reservation_state not in ('waiting', 'confirmed', 'assigned'): + continue + if workorder.production_id.reservation_state == 'assigned' and workorder.state == 'waiting': + workorder.state = 'ready' + elif workorder.production_id.reservation_state != 'assigned' and workorder.state == 'ready': + workorder.state = 'waiting' + @api.depends('production_state', 'date_planned_start', 'date_planned_finished') def _compute_json_popover(self): previous_wo_data = self.env['mrp.workorder'].read_group( @@ -161,7 +178,7 @@ class MrpWorkorder(models.Model): wo.show_json_popover = False wo.json_popover = False continue - if wo.state in ['pending', 'ready']: + if wo.state in ('pending', 'waiting', 'ready'): 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 @@ -420,10 +437,7 @@ class MrpWorkorder(models.Model): moves = production.move_raw_ids | production.move_finished_ids for workorder in self: - if workorder.operation_id.bom_id: - bom = workorder.operation_id.bom_id - if not bom: - bom = workorder.production_id.bom_id + bom = workorder.operation_id.bom_id or workorder.production_id.bom_id previous_workorder = workorders_by_bom[bom][-1:] previous_workorder.next_work_order_id = workorder.id workorders_by_bom[bom] |= workorder @@ -454,7 +468,7 @@ class MrpWorkorder(models.Model): if not workorders: continue if workorders[0].state == 'pending': - workorders[0].state = 'ready' + workorders[0].state = 'ready' if workorders[0].production_availability == 'assigned' else 'waiting' for workorder in workorders: workorder._start_nextworkorder() @@ -463,7 +477,7 @@ class MrpWorkorder(models.Model): def _start_nextworkorder(self): if self.state == 'done' and self.next_work_order_id.state == 'pending': - self.next_work_order_id.state = 'ready' + self.next_work_order_id.state = 'ready' if self.next_work_order_id.production_availability == 'assigned' else 'waiting' @api.model def gantt_unavailability(self, start_date, end_date, scale, group_bys=None, rows=None): @@ -701,8 +715,8 @@ class MrpWorkorder(models.Model): FROM mrp_workorder wo1, mrp_workorder wo2 WHERE wo1.id IN %s - AND wo1.state IN ('pending','ready') - AND wo2.state IN ('pending','ready') + AND wo1.state IN ('pending', 'waiting', 'ready') + AND wo2.state IN ('pending', 'waiting', 'ready') AND wo1.id != wo2.id AND wo1.workcenter_id = wo2.workcenter_id AND (DATE_TRUNC('second', wo2.date_planned_start), DATE_TRUNC('second', wo2.date_planned_finished)) diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index b6e9cd9b231..b57f2a79a6e 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -1741,7 +1741,7 @@ class TestMrpOrder(TestMrpCommon): self.assertEqual(list(warning.keys())[0], 'warning', 'Warning message was not returned') self.assertEqual(scrap.location_id, mo.location_dest_id, 'Location was not auto-corrected') - def test_multi_button_plan(self): + def test_a_multi_button_plan(self): """ Test batch methods (confirm/validate) of the MO with the same bom """ self.bom_2.type = "normal" # avoid to get the operation of the kit bom @@ -1753,7 +1753,7 @@ class TestMrpOrder(TestMrpCommon): mo_3.button_plan() self.assertEqual(mo_3.state, 'confirmed') - self.assertEqual(mo_3.workorder_ids[0].state, 'ready') + self.assertEqual(mo_3.workorder_ids[0].state, 'waiting') mo_1 = Form(self.env['mrp.production']) mo_1.bom_id = self.bom_3 @@ -1772,8 +1772,8 @@ class TestMrpOrder(TestMrpCommon): (mo_1 | mo_2).button_plan() # Confirm and plan in the same "request" self.assertEqual(mo_1.state, 'confirmed') self.assertEqual(mo_2.state, 'confirmed') - self.assertEqual(mo_1.workorder_ids[0].state, 'ready') - self.assertEqual(mo_2.workorder_ids[0].state, 'ready') + self.assertEqual(mo_1.workorder_ids[0].state, 'waiting') + self.assertEqual(mo_2.workorder_ids[0].state, 'waiting') # produce res_dict = (mo_1 | mo_2).button_mark_done() @@ -1863,3 +1863,52 @@ class TestMrpOrder(TestMrpCommon): ('raw_material_production_id', '=', mo_backorder.id)]) self.assertEqual(sum(move_prod_1_bo.mapped('product_uom_qty')), 60.0) self.assertEqual(sum(move_prod_2_bo.mapped('product_uom_qty')), 40.0) + + def test_state_workorders(self): + bom = self.env['mrp.bom'].create({ + 'product_id': self.product_4.id, + 'product_tmpl_id': self.product_4.product_tmpl_id.id, + 'product_uom_id': self.uom_unit.id, + 'product_qty': 1.0, + 'consumption': 'flexible', + 'type': 'normal', + 'bom_line_ids': [ + (0, 0, {'product_id': self.product_2.id, 'product_qty': 1}) + ], + 'operation_ids': [ + (0, 0, {'name': 'amUgbidhaW1lIHBhcyBsZSBKUw==', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}), + (0, 0, {'name': '137 Python', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 1, 'sequence': 2}), + ], + }) + + self.env['stock.quant'].create({ + 'location_id': self.stock_location_components.id, + 'product_id': self.product_2.id, + 'inventory_quantity': 10 + }).action_apply_inventory() + + mo = Form(self.env['mrp.production']) + mo.bom_id = bom + mo = mo.save() + + self.assertEqual(list(mo.workorder_ids.mapped("state")), ["pending", "pending"]) + + mo.action_confirm() + mo.action_assign() + self.assertEqual(mo.move_raw_ids.state, "assigned") + self.assertEqual(list(mo.workorder_ids.mapped("state")), ["ready", "pending"]) + mo.do_unreserve() + + self.assertEqual(list(mo.workorder_ids.mapped("state")), ["waiting", "pending"]) + + mo.workorder_ids[0].unlink() + + self.assertEqual(list(mo.workorder_ids.mapped("state")), ["waiting"]) + mo.action_assign() + self.assertEqual(list(mo.workorder_ids.mapped("state")), ["ready"]) + + res_dict = mo.button_mark_done() + self.assertEqual(res_dict.get('res_model'), 'mrp.immediate.production') + wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save() + wizard.process() + self.assertEqual(list(mo.workorder_ids.mapped("state")), ["done"]) diff --git a/addons/mrp/views/mrp_workorder_views.xml b/addons/mrp/views/mrp_workorder_views.xml index 731df37e72b..3725f8c137a 100644 --- a/addons/mrp/views/mrp_workorder_views.xml +++ b/addons/mrp/views/mrp_workorder_views.xml @@ -8,6 +8,7 @@ + @@ -82,7 +83,8 @@ - +