[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:
committed by
Arnold Moyaux
co-authored by
nouraellm
parent
37e49780e2
commit
ae7e5960f1
@@ -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()
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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!
|
||||
|
||||
Reference in New Issue
Block a user