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 @@