[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:
@@ -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"
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 @@ 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')
|
||||
|
||||
Reference in New Issue
Block a user