[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 <amoyaux@users.noreply.github.com>
Co-authored-by: nouraellm <nea@odoo.com>
This commit is contained in:
Rémy Voet (ryv)
2021-07-13 11:53:12 +00:00
committed by Arnold Moyaux
co-authored by nouraellm
parent 37e49780e2
commit ae7e5960f1
5 changed files with 108 additions and 38 deletions
+14 -15
View File
@@ -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()
+6 -4
View File
@@ -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'))
+23 -9
View File
@@ -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))
+53 -4
View File
@@ -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"])
+12 -6
View File
@@ -8,6 +8,7 @@
<field name="production_id"/>
<field name="workcenter_id"/>
<filter string="Ready" name="ready" domain="[('state','=','ready')]"/>
<filter string="Waiting" name="waiting" domain="[('state','=','waiting')]"/>
<filter string="Pending" name="pending" domain="[('state','=','pending')]"/>
<filter string="In Progress" name="progress" domain="[('state','=','progress')]"/>
<filter string="Done" name="done" domain="[('state','=', 'done')]"/>
@@ -82,7 +83,8 @@
<field name="duration_expected" widget="float_time"/>
<field name="duration" widget="mrp_time_counter"
attrs="{'invisible': [('production_state','=', 'draft')], 'readonly': [('is_user_working', '=', True)]}"/>
<field name="state" widget="badge" decoration-success="state == 'done'" decoration-info="state not in ('done', 'cancel')" attrs="{'invisible': [('production_state', 'in', ('draft', 'done'))]}"/>
<field name="state" widget="badge" decoration-warning="state == 'waiting'" decoration-success="state == 'done'" decoration-info="state not in ('waiting', 'done', 'cancel')"
attrs="{'invisible': [('production_state', '=', 'draft')], 'column_invisible': [('parent.state', '=', 'draft')]}"/>
<button name="button_start" type="object" string="Start" class="btn-success"
attrs="{'invisible': ['|', '|', '|', ('production_state','in', ('draft', 'done', 'cancel')), ('working_state', '=', 'blocked'), ('state', '=', 'done'), ('is_user_working', '!=', False)]}"/>
<button name="button_pending" type="object" string="Pause" class="btn-warning"
@@ -115,6 +117,9 @@
<field name="workcenter_id" position="after">
<field name="production_id"/>
</field>
<field name="state" position="attributes">
<attribute name="attrs">{'invisible': [('production_state', '=', 'draft')]}</attribute>
</field>
</field>
</record>
@@ -127,7 +132,7 @@
<field name="working_state" invisible="1"/>
<field name="production_state" invisible="1"/>
<header>
<field name="state" widget="statusbar" statusbar_visible="pending,ready,progress,done"/>
<field name="state" widget="statusbar" statusbar_visible="pending,waiting,ready,progress,done"/>
</header>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" name="action_see_move_scrap" type="object" icon="fa-arrows-v" attrs="{'invisible': [('scrap_count', '=', 0)]}">
@@ -229,6 +234,7 @@
<field name="production_id"/>
<filter string="In Progress" name="progress" domain="[('state', '=', 'progress')]"/>
<filter string="Ready" name="ready" domain="[('state', '=', 'ready')]"/>
<filter string="Waiting" name="waiting" domain="[('state', '=', 'waiting')]"/>
<filter string="Pending" name="pending" domain="[('state', '=', 'pending')]"/>
<filter string="Finished" name="finish" domain="[('state', '=', 'done')]"/>
<filter string="Available" name="available" domain="[('production_availability', '=', 'assigned')]"/>
@@ -383,7 +389,7 @@
</div>
<div class="o_kanban_manage_button_section">
<h2>
<span t-attf-class="badge #{['pending'].indexOf(record.state.raw_value) > -1 ? 'badge-warning' :['progress'].indexOf(record.state.raw_value) > -1 ? 'badge-secondary' : ['ready'].indexOf(record.state.raw_value) > -1 ? 'badge-primary' : ['done'].indexOf(record.state.raw_value) > -1 ? 'badge-success' : 'badge-danger'}">
<span t-attf-class="badge #{['pending', 'waiting'].indexOf(record.state.raw_value) > -1 ? 'badge-warning' :['progress'].indexOf(record.state.raw_value) > -1 ? 'badge-secondary' : ['ready'].indexOf(record.state.raw_value) > -1 ? 'badge-primary' : ['done'].indexOf(record.state.raw_value) > -1 ? 'badge-success' : ['cancel'].indexOf(record.state.raw_value) > -1 ? 'badge-muted' : 'badge-danger'}">
<t t-esc="record.state.value"/>
</span>
</h2>
@@ -416,7 +422,7 @@
<field name="view_mode">gantt,tree,form,calendar,pivot,graph</field>
<field name="search_view_id" ref="view_mrp_production_workorder_form_view_filter"/>
<field name="view_id" ref="mrp_workorder_view_gantt"/>
<field name="context">{'search_default_work_center': True, 'search_default_ready': True, 'search_default_progress': True, 'search_default_pending': True}</field>
<field name="context">{'search_default_work_center': True, 'search_default_ready': True, 'search_default_waiting': True, 'search_default_progress': True, 'search_default_pending': True}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No work orders to do!
@@ -435,7 +441,7 @@
<field name="view_mode">gantt,tree,form,calendar,pivot,graph</field>
<field name="search_view_id" ref="view_mrp_production_workorder_form_view_filter"/>
<field name="view_id" ref="workcenter_line_gantt_production"/>
<field name="context">{'search_default_production': True, 'search_default_ready': True, 'search_default_progress': True, 'search_default_pending': True}</field>
<field name="context">{'search_default_production': True, 'search_default_ready': True, 'search_default_waiting': True, 'search_default_progress': True, 'search_default_pending': True}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No work orders to do!
@@ -461,7 +467,7 @@
<field name="res_model">mrp.workorder</field>
<field name="view_mode">tree,kanban,form,calendar,pivot,graph,gantt</field>
<field name="search_view_id" ref="view_mrp_production_workorder_form_view_filter"/>
<field name="context">{'search_default_ready': True, 'search_default_progress': True, 'search_default_pending': True}</field>
<field name="context">{'search_default_ready': True, 'search_default_waiting': True, 'search_default_progress': True, 'search_default_pending': True}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No work orders to do!