diff --git a/addons/mrp/models/mrp_bom.py b/addons/mrp/models/mrp_bom.py
index 004283a1fc8..7288f7a54d7 100644
--- a/addons/mrp/models/mrp_bom.py
+++ b/addons/mrp/models/mrp_bom.py
@@ -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:
diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py
index 89dbad21c84..8d460f68a6d 100644
--- a/addons/mrp/models/stock_move.py
+++ b/addons/mrp/models/stock_move.py
@@ -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
diff --git a/addons/mrp/static/src/scss/mrp_fields.scss b/addons/mrp/static/src/scss/mrp_fields.scss
index e442db35271..e952f35588d 100644
--- a/addons/mrp/static/src/scss/mrp_fields.scss
+++ b/addons/mrp/static/src/scss/mrp_fields.scss
@@ -17,3 +17,7 @@
filter: brightness(0.9);
}
}
+
+.o_manual_consumption.font-weight-bold {
+ background-color: #D3D3D370;
+}
diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py
index f919181fd5f..c58488c8fb5 100644
--- a/addons/mrp/tests/test_order.py
+++ b/addons/mrp/tests/test_order.py
@@ -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)
diff --git a/addons/mrp/views/mrp_bom_views.xml b/addons/mrp/views/mrp_bom_views.xml
index 67226f38c60..cf799305be3 100644
--- a/addons/mrp/views/mrp_bom_views.xml
+++ b/addons/mrp/views/mrp_bom_views.xml
@@ -96,6 +96,8 @@
+
+
diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml
index c0bad5d35d7..e29d9b80e0e 100644
--- a/addons/mrp/views/mrp_production_views.xml
+++ b/addons/mrp/views/mrp_production_views.xml
@@ -274,7 +274,7 @@
-
+
@@ -310,10 +310,12 @@
-
+