[IMP] mrp: enable manual consumption on components

Currently, when changing qty_producing on a MO, consumed number of each
component will always be updated. In this commit, we add a new boolean
field manual_consumption to bom.line. If checked, that line won't be
updated when changing qty_producing.
The same but invisible field manual_consumption is also added to each
line on MO. It will use the value on BOM as defualt value. When user
manually set the consued qty, it will be checked automatically. Also,
when user manually update the SML, the field on SM will also be set to
true.

Task-2687494

closes odoo/odoo#80118

Related: odoo/enterprise#25607
Signed-off-by: William Henrotin (whe) <whe@odoo.com>
This commit is contained in:
yhu-odoo
2022-03-31 17:24:44 +02:00
parent 06cbb5793d
commit 8d0b087525
7 changed files with 189 additions and 3 deletions
+10
View File
@@ -70,6 +70,7 @@ class MrpBom(models.Model):
help="Defines if you can consume more or less components than the quantity defined on the BoM:\n"
" * Allowed: allowed for all manufacturing users.\n"
" * Allowed with warning: allowed for all manufacturing users with summary of consumption differences when closing the manufacturing order.\n"
" Note that in the case of component Manual Consumption, where consumption is registered manually exclusively, consumption warnings will still be issued when appropriate also.\n"
" * Blocked: only a manager can close a manufacturing order when the BoM consumption is not respected.",
default='warning',
string='Flexible Consumption',
@@ -392,6 +393,11 @@ class MrpBomLine(models.Model):
'mrp.bom.line', string="BOM lines of the referred bom",
compute='_compute_child_line_ids')
attachments_count = fields.Integer('Attachments Count', compute='_compute_attachments_count')
tracking = fields.Selection(related='product_id.tracking')
manual_consumption = fields.Boolean(
'Manual Consumption', default=False, compute='_compute_manual_consumption', store=True, copy=True,
help="When activated, then the registration of consumption for that component is recorded manually exclusively.\n"
"If not activated, and any of the components consumption is edited manually on the manufacturing order, Odoo assumes manual consumption also.")
_sql_constraints = [
('bom_qty_zero', 'CHECK (product_qty>=0)', 'All product quantities must be greater or equal to 0.\n'
@@ -399,6 +405,10 @@ class MrpBomLine(models.Model):
'You should install the mrp_byproduct module if you want to manage extra products on BoMs !'),
]
@api.depends('product_id', 'tracking')
def _compute_manual_consumption(self):
self.filtered(lambda m: m.tracking != 'none').manual_consumption = True
@api.depends('product_id', 'bom_id')
def _compute_child_bom_id(self):
for line in self:
+26 -1
View File
@@ -140,6 +140,17 @@ class StockMove(models.Model):
product_qty_available = fields.Float('Product On Hand Quantity', related='product_id.qty_available')
product_virtual_available = fields.Float('Product Forecasted Quantity', related='product_id.virtual_available')
description_bom_line = fields.Char('Kit', compute='_compute_description_bom_line')
manual_consumption = fields.Boolean(
'Manual Consumption', compute='_compute_manual_consumption', store=True,
help="When activated, then the registration of consumption for that component is recorded manually exclusively.\n"
"If not activated, and any of the components consumption is edited manually on the manufacturing order, Odoo assumes manual consumption also.")
@api.depends('state', 'product_id', 'operation_id')
def _compute_manual_consumption(self):
for move in self:
if move.state != 'draft':
continue
move.manual_consumption = move.bom_line_id.manual_consumption or move.product_id.tracking != 'none'
@api.depends('bom_line_id')
def _compute_description_bom_line(self):
@@ -226,6 +237,12 @@ class StockMove(models.Model):
mo = self.raw_material_production_id
self._update_quantity_done(mo)
@api.onchange('quantity_done')
def _onchange_quantity_done(self):
if self.raw_material_production_id and not self.manual_consumption and \
self.should_consume_qty != self.quantity_done:
self.manual_consumption = True
@api.model
def default_get(self, fields_list):
defaults = super(StockMove, self).default_get(fields_list)
@@ -250,6 +267,9 @@ class StockMove(models.Model):
- Moves from a copied MO
- Backorders
"""
if self.env.context.get('force_manual_consumption'):
for vals in vals_list:
vals['manual_consumption'] = True
mo_id_to_mo = defaultdict(lambda: self.env['mrp.production'])
product_id_to_product = defaultdict(lambda: self.env['product.product'])
for values in vals_list:
@@ -278,6 +298,8 @@ class StockMove(models.Model):
return super().create(vals_list)
def write(self, vals):
if self.env.context.get('force_manual_consumption'):
vals['manual_consumption'] = True
if 'product_uom_qty' in vals and 'move_line_ids' in vals:
# first update lines then product_uom_qty as the later will unreserve
# so possibly unlink lines
@@ -340,6 +362,7 @@ class StockMove(models.Model):
if self.raw_material_production_id:
action['views'] = [(self.env.ref('mrp.view_stock_move_operations_raw').id, 'form')]
action['context']['show_destination_location'] = False
action['context']['force_manual_consumption'] = True
elif self.production_id:
action['views'] = [(self.env.ref('mrp.view_stock_move_operations_finished').id, 'form')]
action['context']['show_source_location'] = False
@@ -395,6 +418,7 @@ class StockMove(models.Model):
return {
'state': 'confirmed',
'reservation_date': self.reservation_date,
'manual_consumption': self.bom_line_id.manual_consumption or self.product_id.tracking != 'none',
'move_orig_ids': [Command.link(m.id) for m in self.mapped('move_orig_ids')],
'move_dest_ids': [Command.link(m.id) for m in self.mapped('move_dest_ids')]
}
@@ -424,7 +448,8 @@ class StockMove(models.Model):
# Do not update extra product quantities
if float_is_zero(self.product_uom_qty, precision_rounding=self.product_uom.rounding):
return True
if self.has_tracking != 'none' or self.state == 'done':
if self.has_tracking != 'none' or self.state == 'done' or \
self.manual_consumption or self._origin.manual_consumption:
return True
return False
@@ -17,3 +17,7 @@
filter: brightness(0.9);
}
}
.o_manual_consumption.font-weight-bold {
background-color: #D3D3D370;
}
+142
View File
@@ -395,6 +395,148 @@ class TestMrpOrder(TestMrpCommon):
production = production_form.save()
production.button_mark_done()
def test_update_quantity_6(self):
"""Test manual consumption mechanism. Test when manual consumption is
True, quantity_done won't be updated automatically. Bom line with tracked
products or operations should be set to manual consumption automatically.
Also test that when manually change quantity_done, manual consumption
will be set to True. Also test when create backorder, the namual consumption
should be set according to the bom.
"""
Product = self.env['product.product']
product_nt = Product.create({
'name': 'No tracking',
'type': 'product',
'tracking': 'none',})
product_sn = Product.create({
'name': 'Serial',
'type': 'product',
'tracking': 'serial',})
product_lot = Product.create({
'name': 'Lot',
'type': 'product',
'tracking': 'lot',})
bom = self.env['mrp.bom'].create({
'product_id': self.product_6.id,
'product_tmpl_id': self.product_6.product_tmpl_id.id,
'product_qty': 1,
'product_uom_id': self.product_6.uom_id.id,
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': product_nt.id, 'product_qty': 1}),
(0, 0, {'product_id': product_sn.id, 'product_qty': 1}),
(0, 0, {'product_id': product_lot.id, 'product_qty': 1}),
],
})
# Bom linse with tracking products or operations should have manual_consumption set
self.assertEqual(bom.bom_line_ids.mapped('manual_consumption'), [False, True, True])
production_form = Form(self.env['mrp.production'])
production_form.product_id = self.product_6
production_form.bom_id = bom
production_form.product_qty = 10
production_form.product_uom_id = self.product_6.uom_id
production = production_form.save()
production.action_confirm()
production.action_assign()
production.is_locked = False
# test no updating
production_form = Form(production)
production_form.qty_producing = 5
production = production_form.save()
move_nt, move_sn, move_lot = production.move_raw_ids
self.assertEqual(move_nt.manual_consumption, False)
self.assertEqual(move_nt.quantity_done, 5)
self.assertEqual(move_sn.manual_consumption, True)
self.assertEqual(move_sn.quantity_done, 0)
self.assertEqual(move_lot.manual_consumption, True)
self.assertEqual(move_lot.quantity_done, 0)
# test manual change
with production_form.move_raw_ids.edit(0) as move_product_2_line:
move_product_2_line.quantity_done = 6
production = production_form.save()
self.assertEqual(move_nt.manual_consumption, True)
production_form.qty_producing = 8
production = production_form.save()
self.assertEqual(move_nt.quantity_done, 6)
# test manual consumption on backorder
action = production.button_mark_done()
warning = Form(self.env['mrp.consumption.warning'].with_context(**action['context'])).save()
action = warning.action_confirm()
backorder_wizard = Form(self.env['mrp.production.backorder'].with_context(**action['context'])).save()
backorder_wizard.action_backorder()
backorder = production.procurement_group_id.mrp_production_ids[-1]
self.assertEqual(backorder.move_raw_ids.mapped('manual_consumption'), [False, True, True])
def test_update_quantity_7(self):
"""Test manual consumption mechanism without bom. Test when manual consumption
is True, quantity_done won't be updated automatically. Component line with
tracked products should be set to manual consumption automatically.
Also test that when manually change quantity_done, manual consumption
will be set to True. Also test when create backorder, the namual consumption
should be set according to the product's tracking.
"""
Product = self.env['product.product']
product_nt = Product.create({
'name': 'No tracking',
'type': 'product',
'tracking': 'none',})
product_sn = Product.create({
'name': 'Serial',
'type': 'product',
'tracking': 'serial',})
product_lot = Product.create({
'name': 'Lot',
'type': 'product',
'tracking': 'lot',})
production_form = Form(self.env['mrp.production'])
production_form.product_id = self.product_6
production_form.bom_id = self.env['mrp.bom']
production_form.product_qty = 10
production_form.product_uom_id = self.product_6.uom_id
for product in [product_nt, product_sn, product_lot]:
with production_form.move_raw_ids.new() as line:
line.product_id = product
line.product_uom_qty = 10
production = production_form.save()
self.assertEqual(production.move_raw_ids.mapped('manual_consumption'), [False, True, True])
# test no updating
production_form = Form(production)
production_form.qty_producing = 5
production = production_form.save()
move_nt, move_sn, move_lot = production.move_raw_ids
self.assertEqual(move_nt.manual_consumption, False)
self.assertEqual(move_nt.quantity_done, 5)
self.assertEqual(move_sn.manual_consumption, True)
self.assertEqual(move_sn.quantity_done, 0)
self.assertEqual(move_lot.manual_consumption, True)
self.assertEqual(move_lot.quantity_done, 0)
# test manual change
with production_form.move_raw_ids.edit(0) as move_product_2_line:
move_product_2_line.quantity_done = 6
production = production_form.save()
self.assertEqual(move_nt.manual_consumption, True)
production_form.qty_producing = 8
production = production_form.save()
self.assertEqual(move_nt.quantity_done, 6)
# test manual consumption on backorder
action = production.button_mark_done()
warning = Form(self.env['mrp.consumption.warning'].with_context(**action['context'])).save()
action = warning.action_confirm()
backorder_wizard = Form(self.env['mrp.production.backorder'].with_context(**action['context'])).save()
backorder_wizard.action_backorder()
backorder = production.procurement_group_id.mrp_production_ids[-1]
self.assertEqual(backorder.move_raw_ids.mapped('manual_consumption'), [False, True, True])
def test_update_plan_date(self):
"""Editing the scheduled date after planning the MO should unplan the MO, and adjust the date on the stock moves"""
planned_date = datetime(2023, 5, 15, 9, 0)
+2
View File
@@ -96,6 +96,8 @@
<field name="bom_product_template_attribute_value_ids" optional="hide" widget="many2many_tags" options="{'no_create': True}" attrs="{'column_invisible': [('parent.product_id', '!=', False)]}" groups="product.group_product_variant"/>
<field name="allowed_operation_ids" invisible="1"/>
<field name="operation_id" groups="mrp.group_mrp_routings" optional="hidden" attrs="{'column_invisible': [('parent.type','not in', ('normal', 'phantom'))]}" options="{'no_quick_create':True,'no_create_edit':True}"/>
<field name="tracking" invisible="1"/>
<field name="manual_consumption" optional="hide" width="1.0" attrs="{'readonly': [('tracking', '!=', 'none')]}"/>
</tree>
</field>
</page>
+4 -2
View File
@@ -274,7 +274,7 @@
<field name="move_raw_ids"
context="{'default_date': date_planned_start, 'default_date_deadline': date_planned_start, 'default_location_id': location_src_id, 'default_location_dest_id': production_location_id, 'default_state': 'draft', 'default_raw_material_production_id': id, 'default_picking_type_id': picking_type_id, 'default_company_id': company_id}"
attrs="{'readonly': ['|', ('state', '=', 'cancel'), '&amp;', ('state', '=', 'done'), ('is_locked', '=', True)]}" options="{'delete': [('state', '=', 'draft')]}">
<tree default_order="is_done,sequence" editable="bottom">
<tree default_order="is_done, manual_consumption desc, sequence" editable="bottom">
<field name="product_id" force_save="1" required="1" context="{'default_detailed_type': 'product'}" attrs="{'readonly': ['|', '|', ('move_lines_count', '&gt;', 0), ('state', '=', 'cancel'), '&amp;', ('state', '!=', 'draft'), ('additional', '=', False) ]}"/>
<field name="location_id" string="From" readonly="1" groups="stock.group_stock_multi_locations" optional="show"/>
<field name="propagate_cancel" invisible="1"/>
@@ -310,10 +310,12 @@
<button type="object" name="action_product_forecast_report" title="Forecast Report" icon="fa-area-chart" attrs="{'column_invisible': [('parent.state', '!=', 'draft')], 'invisible': [('forecast_availability', '&lt;', 0)]}"/>
<button type="object" name="action_product_forecast_report" title="Forecast Report" icon="fa-area-chart text-danger" attrs="{'column_invisible': [('parent.state', '!=', 'draft')], 'invisible': [('forecast_availability', '&gt;=', 0)]}"/>
<field name="forecast_availability" string="Reserved" attrs="{'column_invisible': [('parent.state', 'in', ('draft', 'done'))]}" widget="forecast_widget"/>
<field name="quantity_done" string="Consumed"
<field name="quantity_done" string="Consumed" class="o_manual_consumption"
decoration-bf="not manual_consumption"
decoration-success="not is_done and (quantity_done - should_consume_qty == 0)"
decoration-warning="not is_done and (quantity_done - should_consume_qty &gt; 0.0001)"
attrs="{'column_invisible': [('parent.state', '=', 'draft')], 'readonly': [('has_tracking', '!=','none')]}"/>
<field name="manual_consumption" invisible="1" force_save="1"/>
<field name="show_details_visible" invisible="1"/>
<field name="lot_ids" widget="many2many_tags"
groups="stock.group_production_lot" optional="hide"
@@ -168,6 +168,7 @@ class TestMrpAccount(TestMrpCommon):
quants.action_apply_inventory()
bom = self.mrp_bom_desk.copy()
bom.bom_line_ids.manual_consumption = False
bom.operation_ids = False
production_table_form = Form(self.env['mrp.production'])
production_table_form.product_id = self.dining_table