[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:
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'), '&', ('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', '>', 0), ('state', '=', 'cancel'), '&', ('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', '<', 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', '>=', 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 > 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
|
||||
|
||||
Reference in New Issue
Block a user