diff --git a/addons/mrp/__manifest__.py b/addons/mrp/__manifest__.py
index a67cad5ccc3..449bfd63000 100644
--- a/addons/mrp/__manifest__.py
+++ b/addons/mrp/__manifest__.py
@@ -58,6 +58,9 @@
'web.assets_backend': [
'mrp/static/src/**/*',
],
+ 'web.assets_tests': [
+ 'mrp/static/tests/tours/**/*',
+ ],
'web.qunit_suite_tests': [
'mrp/static/tests/**/*',
],
diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py
index b704be99b38..02df5a27315 100644
--- a/addons/mrp/models/stock_move.py
+++ b/addons/mrp/models/stock_move.py
@@ -231,12 +231,6 @@ 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)
diff --git a/addons/mrp/static/src/scss/mrp_fields.scss b/addons/mrp/static/src/scss/mrp_fields.scss
index 1c57f30e72d..fc4ef86fa4c 100644
--- a/addons/mrp/static/src/scss/mrp_fields.scss
+++ b/addons/mrp/static/src/scss/mrp_fields.scss
@@ -1,7 +1,3 @@
.o_field_widget.o_embed_url_viewer iframe {
aspect-ratio: 3/2;
}
-
-.o_manual_consumption.fw-bold {
- background-color: rgba($gray-200, .5);
-}
diff --git a/addons/mrp/static/src/widgets/mrp_consumed.js b/addons/mrp/static/src/widgets/mrp_consumed.js
new file mode 100644
index 00000000000..098274c51ac
--- /dev/null
+++ b/addons/mrp/static/src/widgets/mrp_consumed.js
@@ -0,0 +1,37 @@
+/** @odoo-module */
+
+import { registry } from "@web/core/registry";
+import { FloatField, floatField } from '@web/views/fields/float/float_field';
+
+const { useRef, useEffect } = owl;
+
+export class MrpConsumed extends FloatField {
+ setup() {
+ super.setup();
+ const inputRef = useRef("numpadDecimal");
+
+ useEffect(
+ (inputEl) => {
+ if (inputEl) {
+ inputEl.addEventListener("input", this.onInput.bind(this));
+ return () => {
+ inputEl.removeEventListener("input", this.onInput.bind(this));
+ };
+ }
+ },
+ () => [inputRef.el]
+ );
+ }
+
+ onInput(ev) {
+ this.props.setDirty(true);
+ return this.props.record.update({ manual_consumption: true });
+ }
+}
+
+export const mrpConsumed = {
+ ...floatField,
+ component: MrpConsumed,
+};
+
+registry.category('fields').add('mrp_consumed', mrpConsumed);
diff --git a/addons/mrp/static/src/widgets/mrp_production_components_x2many.js b/addons/mrp/static/src/widgets/mrp_production_components_x2many.js
new file mode 100644
index 00000000000..5f018887408
--- /dev/null
+++ b/addons/mrp/static/src/widgets/mrp_production_components_x2many.js
@@ -0,0 +1,28 @@
+/** @odoo-module **/
+
+import { registry } from "@web/core/registry";
+import { X2ManyField, x2ManyField } from "@web/views/fields/x2many/x2many_field";
+import { ListRenderer } from "@web/views/list/list_renderer";
+
+export class MrpProductionComponentsListRenderer extends ListRenderer {
+ getCellClass(column, record) {
+ let classNames = super.getCellClass(...arguments);
+ if (column.name == "quantity_done" && !record.data.manual_consumption) {
+ classNames += ' o_non_manual_consumption';
+ }
+ return classNames;
+ }
+}
+
+export class MrpProductionComponentsX2ManyField extends X2ManyField {}
+MrpProductionComponentsX2ManyField.components = {
+ ...X2ManyField.components,
+ ListRenderer: MrpProductionComponentsListRenderer
+};
+
+export const mrpProductionComponentsX2ManyField = {
+ ...x2ManyField,
+ component: MrpProductionComponentsX2ManyField,
+};
+
+registry.category("fields").add("mrp_production_components_x2many", mrpProductionComponentsX2ManyField);
diff --git a/addons/mrp/static/src/widgets/mrp_production_components_x2many.scss b/addons/mrp/static/src/widgets/mrp_production_components_x2many.scss
new file mode 100644
index 00000000000..84c510fc348
--- /dev/null
+++ b/addons/mrp/static/src/widgets/mrp_production_components_x2many.scss
@@ -0,0 +1,3 @@
+td.o_non_manual_consumption {
+ background-color: rgba($gray-200, .5) !important;
+}
diff --git a/addons/mrp/static/tests/tours/mrp_manual_consumption.js b/addons/mrp/static/tests/tours/mrp_manual_consumption.js
new file mode 100644
index 00000000000..df3470e393a
--- /dev/null
+++ b/addons/mrp/static/tests/tours/mrp_manual_consumption.js
@@ -0,0 +1,72 @@
+/** @odoo-module **/
+
+import { registry } from "@web/core/registry";
+import { stepUtils } from '@web_tour/js/tour_step_utils';
+
+registry.category("web_tour.tours").add('test_mrp_manual_consumption', {
+ test: true,
+ steps: [
+ {
+ trigger: 'div[name=move_raw_ids] td[name="quantity_done"]:last:contains("5.00")',
+ run: () => {},
+ },
+ {
+ trigger: 'div[name=move_raw_ids] td[name="quantity_done"]:last',
+ run: 'click',
+ },
+ {
+ trigger: 'div[name="quantity_done"] input',
+ run: 'text 6.0'
+ },
+ {
+ content: "Click Pager",
+ trigger: ".o_pager_value:first()",
+ },
+ {
+ trigger: "input[id='qty_producing']",
+ run: 'text 8.0',
+ },
+ {
+ content: "Click Pager",
+ trigger: ".o_pager_value:first()",
+ },
+ {
+ trigger: 'div[name=move_raw_ids] td[name="quantity_done"]:last:contains("6.00")',
+ run: () => {},
+ },
+ {
+ trigger: 'button[name=button_mark_done]',
+ run: 'click',
+ },
+ {
+ trigger: 'button[name=action_confirm]',
+ extra_trigger: '.o_technical_modal',
+ run: 'click',
+ },
+ {
+ trigger: 'button[name=action_backorder]',
+ run: 'click',
+ },
+ {
+ trigger: "input[id='qty_producing']",
+ run: 'text 2.0',
+ },
+ {
+ content: "Click Pager",
+ trigger: ".o_pager_value:first()",
+ },
+ {
+ trigger: 'div[name=move_raw_ids] td[name="quantity_done"]:last:contains("2.00")',
+ run: () => {},
+ },
+ {
+ trigger: 'button[name=button_mark_done]',
+ run: 'click',
+ },
+ {
+ trigger: 'button[name=action_confirm]',
+ extra_trigger: '.o_technical_modal',
+ run: 'click',
+ },
+ ...stepUtils.saveForm(),
+]});
diff --git a/addons/mrp/tests/__init__.py b/addons/mrp/tests/__init__.py
index 7238c72b624..0bb9a9bf327 100644
--- a/addons/mrp/tests/__init__.py
+++ b/addons/mrp/tests/__init__.py
@@ -16,3 +16,4 @@ from . import test_backorder
from . import test_smp
from . import test_performance
from . import test_consume_tracked_component
+from . import test_manual_consumption
diff --git a/addons/mrp/tests/test_manual_consumption.py b/addons/mrp/tests/test_manual_consumption.py
new file mode 100644
index 00000000000..a6858990e64
--- /dev/null
+++ b/addons/mrp/tests/test_manual_consumption.py
@@ -0,0 +1,83 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+# -*- coding: utf-8 -*-
+
+from odoo.tests import tagged, Form, HttpCase
+
+
+@tagged('post_install', '-at_install')
+class TestTourManualConsumption(HttpCase):
+ def test_mrp_manual_consumption(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 manual consumption
+ should be set according to the bom.
+ """
+ Product = self.env['product.product']
+ product_finish = Product.create({
+ 'name': 'finish',
+ 'type': 'product',
+ 'tracking': 'none',})
+ 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': product_finish.id,
+ 'product_tmpl_id': product_finish.product_tmpl_id.id,
+ 'product_qty': 1,
+ '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}),
+ ],
+ })
+
+ mo_form = Form(self.env['mrp.production'])
+ mo_form.product_id = product_finish
+ mo_form.bom_id = bom
+ mo_form.product_qty = 10
+ mo = mo_form.save()
+ mo.action_confirm()
+ mo.action_assign()
+
+ # test no updating
+ mo_form = Form(mo)
+ mo_form.qty_producing = 5
+ mo = mo_form.save()
+ move_nt, move_sn, move_lot = mo.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)
+
+ action_id = self.env.ref('mrp.menu_mrp_production_action').action
+ url = "/web#model=mrp.production&view_type=form&action=%s&id=%s" % (str(action_id.id), str(mo.id))
+ self.start_tour(url, "test_mrp_manual_consumption", login="admin", timeout=200)
+
+ self.assertEqual(move_nt.manual_consumption, True)
+ self.assertEqual(move_nt.quantity_done, 6.0)
+ 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)
+
+ backorder = mo.procurement_group_id.mrp_production_ids - mo
+ move_nt = backorder.move_raw_ids.filtered(lambda m: m.product_id == product_nt)
+ move_sn = backorder.move_raw_ids.filtered(lambda m: m.product_id == product_sn)
+ move_lot = backorder.move_raw_ids.filtered(lambda m: m.product_id == product_lot)
+ self.assertEqual(move_nt.manual_consumption, False)
+ self.assertEqual(move_sn.manual_consumption, True)
+ self.assertEqual(move_lot.manual_consumption, True)
diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py
index 7e6dfadd963..e763cc34d2a 100644
--- a/addons/mrp/tests/test_order.py
+++ b/addons/mrp/tests/test_order.py
@@ -483,153 +483,6 @@ 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])
-
- #
- 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_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_production_views.xml b/addons/mrp/views/mrp_production_views.xml
index 1687c86e7df..29985fce59f 100644
--- a/addons/mrp/views/mrp_production_views.xml
+++ b/addons/mrp/views/mrp_production_views.xml
@@ -300,6 +300,7 @@
@@ -341,12 +342,11 @@
-
+ force_save="1" widget="mrp_consumed"/>