[FIX] mrp: allow confirming of MOs without components

Prior to this commit, MOs would throw a usererror if there were no
components in it. This restriction made it so MOs created by a
replenishment (i.e. `warehouse.orderpoint`) for a BoM with no components
were never confirmed and the `qty_to_order` would accumulate for each
replenishment's new draft MO.

Steps to reproduce:
1. create a bom for a new product (w/ manufacturing route) with no
   components
2. create and confirm a sales order for the new product
3. go to Replenishment and automate the product's replenishment
4. create a new sales order for the product

Expected result: 2 confirmed MOs where the quantity of the first MO
matches the first sale order's quantity and the 2nd MO matches the
second sale order's quantity

Actual result: 2 draft MOs where the first MO matches the quantity of
the first sale order and the second MO matches the quantity of the first
sale order + the quantity of the second sale order

This commit makes it so MOs can now be confirmed even when it has no
components. Note this requires changing the logic in a few locations to
ensure expected behavior still occurs including:
- backorders are automatically confirmed.
- `reservation_state` is recalculated when expected (will be blank when
  no components).

Task: 2422698
This commit is contained in:
Tiffany Chang (tic)
2021-05-19 06:47:22 +00:00
parent 0454340853
commit bf5e1debf9
5 changed files with 44 additions and 23 deletions
+25 -18
View File
@@ -181,7 +181,7 @@ class MrpProduction(models.Model):
('assigned', 'Ready'),
('waiting', 'Waiting Another Operation')],
string='Material Availability',
compute='_compute_state', copy=False, index=True, readonly=True,
compute='_compute_reservation_state', copy=False, index=True, readonly=True,
store=True, tracking=True,
help=" * Ready: The material is available to start the production.\n\
* Waiting: The material is not available to start the production.\n\
@@ -211,6 +211,9 @@ class MrpProduction(models.Model):
reserve_visible = fields.Boolean(
'Allowed to Reserve Production', compute='_compute_unreserve_visible',
help='Technical field to check when we can reserve quantities')
confirm_no_consumption = fields.Boolean(
compute='_compute_components_availability',
help='Technical field used to see if we have to display a warning or not when confirming an order without any components.')
user_id = fields.Many2one(
'res.users', 'Responsible', default=lambda self: self.env.user,
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
@@ -281,9 +284,13 @@ class MrpProduction(models.Model):
def _compute_components_availability(self):
self.components_availability = False
self.components_availability_state = 'available'
self.confirm_no_consumption = False
productions = self.filtered(lambda mo: mo.state not in ['cancel', 'draft', 'done'])
productions.components_availability = _('Available')
for production in productions:
if not production.move_raw_ids:
production.confirm_no_consumption = True
continue
forecast_date = max(production.move_raw_ids.filtered('forecast_expected_date').mapped('forecast_expected_date'), default=False)
if any(float_compare(move.forecast_availability, move.product_qty, move.product_id.uom_id.rounding) == -1 for move in production.move_raw_ids):
production.components_availability = _('Not Available')
@@ -418,21 +425,21 @@ class MrpProduction(models.Model):
'move_raw_ids.state', 'move_raw_ids.quantity_done', 'move_finished_ids.state',
'workorder_ids', 'workorder_ids.state', 'product_qty', 'qty_producing')
def _compute_state(self):
""" Compute the production state. It use the same process than stock
picking. It exists 3 extra steps for production:
""" Compute the production state. This uses a similar process to stock
picking, but has been adapted to support having no moves. This adaption
includes some state changes outside of this compute.
There exist 3 extra steps for production:
- progress: At least one item is produced or consumed.
- to_close: The quantity produced is greater than the quantity to
produce and all work orders has been finished.
"""
# TODO: duplicated code with stock_picking.py
for production in self:
if not production.move_raw_ids:
if not production.state:
production.state = 'draft'
elif all(move.state == 'draft' for move in production.move_raw_ids):
production.state = 'draft'
elif all(move.state == 'cancel' for move in production.move_raw_ids):
elif production.move_raw_ids and all(move.state == 'cancel' for move in production.move_raw_ids):
production.state = 'cancel'
elif all(move.state in ('cancel', 'done') for move in production.move_raw_ids):
elif production.state == 'done' or (production.move_raw_ids and all(move.state in ('cancel', 'done') for move in production.move_raw_ids)):
production.state = 'done'
elif production.qty_producing >= production.product_qty:
production.state = 'to_close'
@@ -442,9 +449,10 @@ class MrpProduction(models.Model):
production.state = 'progress'
elif any(not float_is_zero(move.quantity_done, precision_rounding=move.product_uom.rounding or move.product_id.uom_id.rounding) for move in production.move_raw_ids):
production.state = 'progress'
else:
production.state = 'confirmed'
@api.depends('state', 'move_raw_ids.state')
def _compute_reservation_state(self):
for production in self:
# Compute reservation state
# State where the reservation does not matter.
production.reservation_state = False
@@ -1063,8 +1071,6 @@ class MrpProduction(models.Model):
for production in self:
if production.bom_id:
production.consumption = production.bom_id.consumption
if not production.move_raw_ids:
raise UserError(_("Add some materials to consume before marking this MO as to do."))
# In case of Serial number tracking, force the UoM to the UoM of product
if production.product_tracking == 'serial' and production.product_uom_id != production.product_id.uom_id:
production.write({
@@ -1079,8 +1085,9 @@ class MrpProduction(models.Model):
production.move_raw_ids._adjust_procure_method()
(production.move_raw_ids | production.move_finished_ids)._action_confirm()
production.workorder_ids._action_confirm()
# run scheduler for moves forecasted to not have enough in stock
production.move_raw_ids._trigger_scheduler()
# run scheduler for moves forecasted to not have enough in stock
self.move_raw_ids._trigger_scheduler()
self.state = 'confirmed'
return True
def action_assign(self):
@@ -1420,9 +1427,8 @@ class MrpProduction(models.Model):
if not close_mo:
self.move_raw_ids.filtered(lambda m: not m.additional)._do_unreserve()
self.move_raw_ids.filtered(lambda m: not m.additional)._action_assign()
# Confirm only productions with remaining components
backorders.filtered(lambda mo: mo.move_raw_ids).action_confirm()
backorders.filtered(lambda mo: mo.move_raw_ids).action_assign()
backorders.action_confirm()
backorders.action_assign()
# Remove the serial move line without reserved quantity. Post inventory will assigned all the non done moves
# So those move lines are duplicated.
@@ -1471,6 +1477,7 @@ class MrpProduction(models.Model):
'product_qty': production.qty_produced,
'priority': '0',
'is_locked': True,
'state': 'done',
})
for workorder in self.workorder_ids.filtered(lambda w: w.state not in ('done', 'cancel')):
+1 -1
View File
@@ -58,7 +58,7 @@ class StockRule(models.Model):
self.env['stock.move'].sudo().create(productions._get_moves_raw_values())
self.env['stock.move'].sudo().create(productions._get_moves_finished_values())
productions._create_workorder()
productions.filtered(lambda p: p.move_raw_ids).action_confirm()
productions.action_confirm()
for production in productions:
origin_production = production.move_dest_ids and production.move_dest_ids[0].raw_material_production_id or False
+1 -1
View File
@@ -41,9 +41,9 @@ class TestMrpCommon(common2.TestStockCommon):
(0, 0, {'product_id': product_to_use_1.id, 'product_qty': qty_base_1})
]})
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = product_to_build
if picking_type_id:
mo_form.picking_type_id = picking_type_id
mo_form.product_id = product_to_build
mo_form.bom_id = bom_1
mo_form.product_qty = qty_final
mo = mo_form.save()
+2 -2
View File
@@ -300,7 +300,7 @@ class TestProcurement(TestMrpCommon):
def test_procurement_with_empty_bom(self):
"""Ensure that a procurement request using a product with an empty BoM
will create a MO in draft state that could be completed afterwards.
will create an empty MO in confirmed state that can be completed afterwards.
"""
self.warehouse = self.env.ref('stock.warehouse0')
route_manufacture = self.warehouse.manufacture_pull_id.route_id.id
@@ -330,7 +330,7 @@ class TestProcurement(TestMrpCommon):
production = self.env['mrp.production'].search([('product_id', '=', product.id)])
self.assertTrue(production)
self.assertFalse(production.move_raw_ids)
self.assertEqual(production.state, 'draft')
self.assertEqual(production.state, 'confirmed')
comp1 = self.env['product.product'].create({
'name': 'egg',
+15 -1
View File
@@ -64,14 +64,28 @@
<form string="Manufacturing Orders">
<header>
<field name="confirm_cancel" invisible="1"/>
<field name="confirm_no_consumption" invisible="1"/>
<field name="show_lock" invisible="1"/>
<button name="button_mark_done" attrs="{'invisible': ['|', ('state', 'in', ('draft', 'cancel', 'done', 'to_close')), ('qty_producing', '=', 0)]}" string="Validate" type="object" class="oe_highlight"/>
<button name="button_mark_done" attrs="{'invisible': ['|', '|', ('state', 'in', ('draft', 'cancel', 'done', 'to_close')), ('qty_producing', '=', 0), ('confirm_no_consumption', '=', False)]}" string="Validate" type="object" class="oe_highlight"
confirm="There are no components to consume. Are you still sure you want to continue?"/>
<button name="button_mark_done" attrs="{'invisible': ['|', '|', ('state', 'in', ('draft', 'cancel', 'done', 'to_close')), ('qty_producing', '=', 0), ('confirm_no_consumption', '=', True)]}" string="Validate" type="object" class="oe_highlight"/>
<button name="button_mark_done" attrs="{'invisible': [
'|',
('confirm_no_consumption', '=', True),
'&amp;',
'|',
('state', 'not in', ('confirmed', 'progress')),
('qty_producing', '!=', 0),
('state', '!=', 'to_close')]}" string="Mark as Done" type="object" class="oe_highlight"/>
<button name="button_mark_done" attrs="{'invisible': [
'|',
('confirm_no_consumption', '=', False),
'&amp;',
'|',
('state', 'not in', ('confirmed', 'progress')),
('qty_producing', '!=', 0),
('state', '!=', 'to_close')]}" string="Mark as Done" type="object" class="oe_highlight"
confirm="There are no components to consume. Are you still sure you want to continue?"/>
<button name="action_confirm" attrs="{'invisible': [('state', '!=', 'draft')]}" string="Confirm" type="object" class="oe_highlight"/>
<button name="action_assign" attrs="{'invisible': ['|', ('state', 'in', ('draft', 'done', 'cancel')), ('reserve_visible', '=', False)]}" string="Check availability" type="object"/>
<button name="button_plan" attrs="{'invisible': ['|', '|', ('state', 'not in', ('confirmed', 'progress', 'to_close')), ('workorder_ids', '=', []), ('is_planned', '=', True)]}" type="object" string="Plan" class="oe_highlight"/>