From 5bb0f96f1973fa7e19b6701944b4a29577f3314f Mon Sep 17 00:00:00 2001 From: yhu-odoo Date: Wed, 11 Jan 2023 10:15:54 +0000 Subject: [PATCH] [IMP] mrp: manual consumption improvement Previously we set a component on MO to be manual consumption by comparing the To Consume and Consumed, if they are not the same, we consider it a manual consumption. Now if any input activity in the Consumed cell, we will consider it a manual consumption. Note that in the code, we make the css class change happended in the list renderer instead of the field widget. We do that because we want to change the background color of the whole cell not just the text of the field. Task-3116125 Part-of: odoo/odoo#113538 --- addons/mrp/__manifest__.py | 3 + addons/mrp/models/stock_move.py | 6 - addons/mrp/static/src/scss/mrp_fields.scss | 4 - addons/mrp/static/src/widgets/mrp_consumed.js | 37 +++++ .../mrp_production_components_x2many.js | 28 ++++ .../mrp_production_components_x2many.scss | 3 + .../tests/tours/mrp_manual_consumption.js | 72 +++++++++ addons/mrp/tests/__init__.py | 1 + addons/mrp/tests/test_manual_consumption.py | 83 ++++++++++ addons/mrp/tests/test_order.py | 147 ------------------ addons/mrp/views/mrp_production_views.xml | 6 +- 11 files changed, 230 insertions(+), 160 deletions(-) create mode 100644 addons/mrp/static/src/widgets/mrp_consumed.js create mode 100644 addons/mrp/static/src/widgets/mrp_production_components_x2many.js create mode 100644 addons/mrp/static/src/widgets/mrp_production_components_x2many.scss create mode 100644 addons/mrp/static/tests/tours/mrp_manual_consumption.js create mode 100644 addons/mrp/tests/test_manual_consumption.py 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 @@