Skip to content
Permalink
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
   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 21, 2019
1 parent ebd0c3f commit f24b4d492117593f4a04bb69505ceec3a382a727
@@ -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')],
required=True,
)

@api.onchange('qty_producing')
def _onchange_qty_producing(self):
@@ -204,6 +209,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
self._strict_consumption_check()
vals_list = []
workorder_lines_to_process = self.workorder_line_ids.filtered(lambda line: line.qty_done > 0)
for line in workorder_lines_to_process:
@@ -214,6 +221,21 @@ def _update_raw_moves(self):
self.workorder_line_ids.unlink()
self.env['stock.move.line'].create(vals_list)

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):
'res.company', 'Company',
default=lambda self: self.env['res.company']._company_default_get('mrp.bom'),
required=True)
consumption = fields.Selection([
('strict', 'Strict'),
('flexible', 'Flexible')],
help="Defines if you can consume more or less components than the quantity defined on the BoM.",
default='strict',
string='Consumption'
)

@api.onchange('product_id')
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(
compute='_compute_consumed_less_than_planned',
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(
'res.company', 'Company',
@@ -340,19 +336,6 @@ def _compute_post_visible(self):
else:
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.multi
@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,
move.product_uom_qty,
precision_rounding=move.product_uom.rounding) == -1)
)

@api.multi
@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 = workorder.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'
else:
return 'none'


class MrpWorkorderLine(models.Model):
_name = 'mrp.workorder.line'
@@ -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',
'product_id': p1.id,
@@ -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',
'product_id': p1.id,
@@ -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'
mo.action_assign()

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 produce_form.workorder_line_ids.new() as line:
line.product_id = self.product_4
line.qty_done = 1
produce_wizard = produce_form.save()
produce_wizard.do_produce()

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)

mo.action_assign()

produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))

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

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

with self.assertRaises(UserError):
# try adding another product
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
with produce_form.workorder_line_ids.new() as line:
line.product_id = self.product_4
line.qty_done = 1
product_produce = produce_form.save()
product_produce.do_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_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 1
with produce_form.workorder_line_ids.new() as line:
line.product_id = p1
line.qty_done = 1
product_produce = produce_form.save()
product_produce.workorder_line_ids[1].qty_done -= 1
product_produce.do_produce()

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')
@@ -73,6 +73,7 @@
<group>
<group>
<field name="sequence"/>
<field name="consumption" attrs="{'invisible': [('type','=','phantom')]}"/>
</group>
<group>
<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">
<header>
<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):
'date_start': datetime.now(),
})

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

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

0 comments on commit f24b4d4

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