[IMP] mrp: 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 d3617fd852
as the warning become some sort of an error

Task: 1889393
This commit is contained in:
William Henrotin
2019-03-27 15:16:55 +00:00
parent 32ab3422f0
commit 291eb73ce1
9 changed files with 93 additions and 20 deletions
+1
View File
@@ -304,6 +304,7 @@
<field name="product_tmpl_id" ref="product_product_computer_desk_product_template"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="sequence">3</field>
<field name="consumption">flexible</field>
<field name="routing_id" ref="mrp_routing_3"/>
</record>
@@ -19,6 +19,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):
@@ -217,6 +222,8 @@ class MrpAbstractWorkorder(models.AbstractModel):
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:
@@ -227,6 +234,16 @@ class MrpAbstractWorkorder(models.AbstractModel):
self.workorder_line_ids.unlink()
self.env['stock.move.line'].create(vals_list)
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].product_id.name)
class MrpAbstractWorkorderLine(models.AbstractModel):
_name = "mrp.abstract.workorder.line"
+7
View File
@@ -61,6 +61,13 @@ class MrpBom(models.Model):
'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):
+1 -17
View File
@@ -177,10 +177,6 @@ class MrpProduction(models.Model):
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 @@ class MrpProduction(models.Model):
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 @@ class MrpProduction(models.Model):
'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
+61
View File
@@ -5,6 +5,7 @@ from odoo.tests import Form
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
@@ -504,6 +505,7 @@ class TestMrpOrder(TestMrpCommon):
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,
@@ -549,6 +551,7 @@ class TestMrpOrder(TestMrpCommon):
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,
@@ -701,6 +704,7 @@ class TestMrpOrder(TestMrpCommon):
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 4)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 1)
mo.bom_id.consumption = 'flexible'
mo.action_assign()
produce_form = Form(self.env['mrp.product.produce'].with_context({
@@ -723,6 +727,7 @@ class TestMrpOrder(TestMrpCommon):
# 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()
@@ -841,6 +846,62 @@ class TestMrpOrder(TestMrpCommon):
self.assertEqual(self.env['stock.quant']._gather(p2, self.stock_location, strict=True).quantity, -1)
self.assertEqual(self.env['stock.quant']._gather(p_final, self.stock_location, strict=True).quantity, 3)
def test_product_produce_9(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')
@@ -25,6 +25,7 @@ class TestWorkOrderProcess(common.TransactionCase):
self.env['stock.move'].search([('product_id', 'in', [product_bolt.id, product_screw.id])])._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 @@ class TestWorkOrderProcess(common.TransactionCase):
bom = self.env['mrp.bom'].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]
+1
View File
@@ -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"/>
+1 -3
View File
@@ -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"/>
+2
View File
@@ -35,6 +35,8 @@ class MrpProductProduce(models.TransientModel):
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')