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 locked in read only mode. Changing the quantity to
   produce will, as done before, update the components to consume but the
   manufacturing user cannot change them by hand.
   If set to 'flexible', the production flow remains the same as present
   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 21, 2019
1 parent ed13e45 commit de54d091d30c8d54eea059afc25e1c8c8d9fb33c
@@ -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):
@@ -202,6 +207,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:
@@ -212,6 +219,21 @@ def _update_raw_moves(self):

def _strict_consumption_check(self):
mode = self._strict_consumption_mode()
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
message = ""
if mode == 'less or equals' and float_compare(qty_done, qty_to_consume, precision_rounding=rounding) == 1:
message = _('You should consume the quantity of components defined in the BoM. If you want to consume more or less components, change the consumption setting on the BoM.')
elif mode == 'equals' and float_compare(qty_done, qty_to_consume, precision_rounding=rounding) != 0:
message = _('You should consume the quantity of components defined in the BoM. If you want to consume more or less components, change the consumption setting on the BoM.')
if message:
raise UserError(message)

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 =
@@ -383,6 +383,12 @@ def _compute_qty_remaining(self):
for wo in self:
wo.qty_remaining = float_round(wo.qty_production - wo.qty_produced, precision_rounding=wo.production_id.product_uom_id.rounding)

def _strict_consumption_mode(self):
if self.consumption == 'strict':
return 'less or equals'
return 'none'

class MrpWorkorderLine(models.Model):
_name = 'mrp.workorder.line'
@@ -490,6 +490,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 +536,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',
@@ -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')
@@ -130,6 +132,11 @@ def _record_production(self):

def _strict_consumption_mode(self):
if self.consumption == 'strict':
return 'equals'
return 'none'

class MrpProductProduceLine(models.TransientModel):
_name = 'mrp.product.produce.line'

0 comments on commit de54d09

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