Skip to content
Browse files

[IMP] mrp_workorder: flexible component consumption

Purpose: The quantities to consume on Bill of Material lines should be
either strictly used or be taken as a reference more or less adjustable.

This commit adds a setting on BoM to specify if the consumption is 'strict'
or 'flexible'. This new option has the following impacts:

   On produce wizard: if consumption is set to 'strict', the done quantities
   are prefilled and checked when saving the wizard. If set to 'flexible',
   the production flow remains the same as present one.

   On workorders: if consumption is set to 'strict', the Validate button
   will save the consumed data, and propose to fill the remaining ones until
   the total is registered. If set to 'flexible', two button are displayed.
   'Validate' to register the current component and pass to next step either
   the quantity to consume is complete or not, and 'Continue Consumption'
   to registered the current component quantity but leaving the user the
   possibility to add more quantity (and possibly another lot number) for
   the current component

This commit also revert partially d3617fd
as the warning become some sort of an error

Task: 1889393
  • Loading branch information...
Whenrow committed Mar 25, 2019
1 parent a15b40a commit d5255efd63fa03b0bdc6ae7c28f29df3efc5bd65
@@ -17,6 +17,11 @@ class MrpAbstractWorkorder(models.AbstractModel):
product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure', required=True, readonly=True)
final_lot_id = fields.Many2one('stock.production.lot', string='Lot/Serial Number', domain="[('product_id', '=', product_id)]")
product_tracking = fields.Selection(related="product_id.tracking")
consumption = fields.Selection([
('strict', 'Strict'),
('flexible', 'Flexible')],

def _onchange_qty_producing(self):
@@ -205,6 +210,8 @@ def _update_finished_move(self):
def _update_raw_moves(self):
""" Once the production is done, the lots written on workorder lines
are saved on stock move lines"""
# Before writting produce quantities, we ensure they respect the bom strictness
vals_list = []
workorder_lines_to_process = self.workorder_line_ids.filtered(lambda line: line.qty_done > 0)
for line in workorder_lines_to_process:
@@ -215,6 +222,16 @@ def _update_raw_moves(self):

def _strict_consumption_check(self):
if self.consumption == 'strict':
for move in self.move_raw_ids:
lines = self.workorder_line_ids.filtered(lambda l: l.move_id == move)
qty_done = sum(lines.mapped('qty_done'))
qty_to_consume = sum(lines.mapped('qty_to_consume'))
rounding = self.product_uom_id.rounding
if float_compare(qty_done, qty_to_consume, precision_rounding=rounding) != 0:
raise UserError(_('You should consume the quantity of %s defined in the BoM. If you want to consume more or less components, change the consumption setting on the BoM.') % lines[0]

class MrpAbstractWorkorderLine(models.AbstractModel):
_name = "mrp.abstract.workorder.line"
@@ -61,6 +61,13 @@ def _get_default_product_uom_id(self):
'', 'Company',
default=lambda self: self.env['']._company_default_get(''),
consumption = fields.Selection([
('strict', 'Strict'),
('flexible', 'Flexible')],
help="Defines if you can consume more or less components than the quantity defined on the BoM.",

def onchange_product_id(self):
@@ -177,10 +177,6 @@ def _get_default_location_dest_id(self):
post_visible = fields.Boolean(
'Allowed to Post Inventory', compute='_compute_post_visible',
help='Technical field to check when we can post')
consumed_less_than_planned = fields.Boolean(
help='Technical field used to see if we have to display a warning or not when confirming an order.')

user_id = fields.Many2one('res.users', 'Responsible', default=lambda self: self._uid)
company_id = fields.Many2one(
'', 'Company',
@@ -340,19 +336,6 @@ def _compute_post_visible(self):
order.post_visible = order.is_locked and any((x.quantity_done > 0 and x.state not in ['done', 'cancel']) for x in order.move_finished_ids)

@api.depends('move_raw_ids.quantity_done', 'move_raw_ids.product_uom_qty')
def _compute_consumed_less_than_planned(self):
""" Display a warning to the user if a component of the BoM has less
quantity than planned.
for order in self:
order.consumed_less_than_planned = any(order.move_raw_ids.filtered(
lambda move: float_compare(move.quantity_done,
precision_rounding=move.product_uom.rounding) == -1)

@api.depends('workorder_ids.state', 'move_finished_ids', 'move_finished_ids.quantity_done', 'is_locked')
def _get_produced_qty(self):
@@ -740,6 +723,7 @@ def _workorders_create(self, bom, bom_data):
'state': len(workorders) == 0 and 'ready' or 'pending',
'qty_producing': quantity,
'capacity': operation.workcenter_id.capacity,
'consumption': self.bom_id.consumption,
if workorders:
workorders[-1].next_work_order_id =
@@ -5,6 +5,7 @@
from datetime import datetime, timedelta

from odoo.fields import Datetime as Dt
from odoo.exceptions import UserError
from odoo.addons.mrp.tests.common import TestMrpCommon

@@ -490,6 +491,7 @@ def test_product_produce_2(self):
mo, bom, p_final, p1, p2 = self.generate_mo(tracking_base_1='serial', qty_base_1=1, qty_final=2)
self.assertEqual(len(mo), 1, 'MO should have been created')

bom.consumption = 'flexible'
lot_p1_1 = self.env['stock.production.lot'].create({
'name': 'lot1',
@@ -535,6 +537,7 @@ def test_product_produce_3(self):
mo, _, p_final, p1, p2 = self.generate_mo(tracking_base_1='lot', qty_base_1=10, qty_final=1)
self.assertEqual(len(mo), 1, 'MO should have been created')

mo.bom_id.consumption = 'flexible'
first_lot_for_p1 = self.env['stock.production.lot'].create({
'name': 'lot1',
@@ -684,6 +687,7 @@ def test_product_produce_6(self):
mo, bom, p_final, p1, p2 = self.generate_mo(qty_final=1)
self.assertEqual(len(mo), 1, 'MO should have been created')

mo.bom_id.consumption = 'flexible'

produce_form = Form(self.env['mrp.product.produce'].with_context({
@@ -702,9 +706,65 @@ def test_product_produce_6(self):
# try adding another product that doesn't belong to the BoM
with as line:
line.product_id = self.product_4
line.qty_done = 1
produce_wizard =

def test_product_produce_7(self):
""" Check that no produce line are created when the consumed products are not tracked """
self.stock_location = self.env.ref('stock.stock_location_stock')
mo, bom, p_final, p1, p2 = self.generate_mo()
self.assertEqual(len(mo), 1, 'MO should have been created')

self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)


produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_ids': [],

with self.assertRaises(UserError):
# try adding another line for a bom product to increase the quantity
with as line:
line.product_id = p1
line.qty_done = 1
product_produce =

with self.assertRaises(UserError):
# Try updating qty_done
product_produce =
product_produce.workorder_line_ids[0].qty_done += 1

with self.assertRaises(UserError):
# try adding another product
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_ids': [],
with as line:
line.product_id = self.product_4
line.qty_done = 1
product_produce =

# try adding another line for a bom product but the total quantity is good
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_ids': [],
produce_form.qty_producing = 1
with as line:
line.product_id = p1
line.qty_done = 1
product_produce =
product_produce.workorder_line_ids[1].qty_done -= 1

def test_product_produce_uom(self):
plastic_laminate = self.env.ref('mrp.product_product_plastic_laminate')
bom = self.env.ref('mrp.mrp_bom_plastic_laminate')
@@ -25,6 +25,7 @@ def test_00_workorder_process(self):
self.env['stock.move'].search([('product_id', 'in', [,])])._do_unreserve()
(product_bolt + product_screw).write({'type': 'product'})

self.env.ref("mrp.mrp_bom_desk").consumption = 'flexible'
production_table_form = Form(self.env['mrp.production'])
production_table_form.product_id = dining_table
production_table_form.bom_id = self.env.ref("mrp.mrp_bom_desk")
@@ -128,6 +129,7 @@ def test_00b_workorder_process(self):

bom = self.env[''].browse(self.ref("mrp.mrp_bom_desk"))
bom.routing_id = self.ref('mrp.mrp_routing_1')
bom.consumption = 'flexible'

bom.bom_line_ids.filtered(lambda p: p.product_id == product_table_sheet).operation_id = bom.routing_id.operation_ids[0]
bom.bom_line_ids.filtered(lambda p: p.product_id == product_table_leg).operation_id = bom.routing_id.operation_ids[1]
@@ -73,6 +73,7 @@
<field name="sequence"/>
<field name="consumption" attrs="{'invisible': [('type','=','phantom')]}"/>
<field name="ready_to_produce" attrs="{'invisible': [('type','=','phantom')]}" string="Manufacturing Readiness"/>
@@ -35,9 +35,7 @@
<field name="arch" type="xml">
<form string="Manufacturing Orders">
<field name="consumed_less_than_planned" invisible="1"/>
<button name="button_mark_done" attrs="{'invisible': ['|', ('state', '!=', 'to_close'), ('consumed_less_than_planned', '=', True)]}" string="Mark as Done" type="object" class="oe_highlight"/>
<button name="button_mark_done" attrs="{'invisible': ['|', ('state', '!=', 'to_close'), ('consumed_less_than_planned', '=', False)]}" string="Mark as Done" type="object" class="oe_highlight" confirm="You have consumed less material than what was planned. Are you sure you want to close this MO?"/>
<button name="button_mark_done" attrs="{'invisible': [('state', '!=', 'to_close')]}" string="Mark as Done" type="object" class="oe_highlight"/>
<button name="action_confirm" attrs="{'invisible': ['|', ('state', '!=', 'draft'), ('is_locked', '=', False)]}" string="Mark as Todo" type="object" class="oe_highlight"/>
<button name="action_assign" attrs="{'invisible': ['|', '|', ('is_locked', '=', False), ('state', 'in', ('draft', 'done', 'cancel')), ('reservation_state', '=', 'assigned')]}" string="Check availability" type="object" class="oe_highlight"/>
<button name="button_plan" attrs="{'invisible': ['|', ('state', 'not in', ('confirmed', 'planned')), '|', ('routing_id', '=', False), '|', ('date_planned_start_wo', '!=', False), ('date_planned_finished_wo', '!=', False)]}" type="object" string="Plan" class="oe_highlight"/>
@@ -35,6 +35,8 @@ def default_get(self, fields):
res['serial'] = bool(serial_finished)
if 'qty_producing' in fields:
res['qty_producing'] = todo_quantity
if 'consumption' in fields:
res['consumption'] = production.bom_id.consumption
return res

serial = fields.Boolean('Requires Serial')

0 comments on commit d5255ef

Please sign in to comment.
You can’t perform that action at this time.