[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
This commit is contained in:
yhu-odoo
2023-03-01 17:01:16 +01:00
parent 439ba7d945
commit 5bb0f96f19
11 changed files with 230 additions and 160 deletions
+3
View File
@@ -58,6 +58,9 @@
'web.assets_backend': [
'mrp/static/src/**/*',
],
'web.assets_tests': [
'mrp/static/tests/tours/**/*',
],
'web.qunit_suite_tests': [
'mrp/static/tests/**/*',
],
-6
View File
@@ -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)
@@ -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);
}
@@ -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);
@@ -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);
@@ -0,0 +1,3 @@
td.o_non_manual_consumption {
background-color: rgba($gray-200, .5) !important;
}
@@ -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(),
]});
+1
View File
@@ -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
@@ -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)
-147
View File
@@ -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])
# <field name="qty_producing" attrs="{'invisible': [('state', '=', 'draft')]}"/>
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)
+3 -3
View File
@@ -300,6 +300,7 @@
<notebook>
<page string="Components" name="components">
<field name="move_raw_ids"
widget="mrp_production_components_x2many"
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_warehouse_id': warehouse_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'), '&amp;', ('state', '=', 'done'), ('is_locked', '=', True)]}" options="{'delete': [('state', '=', 'draft')]}">
@@ -341,12 +342,11 @@
<button type="object" name="action_product_forecast_report" title="Forecast Report" icon="fa-area-chart" attrs="{'column_invisible': [('parent.state', '!=', 'draft')], 'invisible': [('forecast_availability', '&lt;', 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', '&gt;=', 0)]}"/>
<field name="forecast_availability" string="Reserved" attrs="{'column_invisible': [('parent.state', 'in', ('draft', 'done'))]}" widget="forecast_widget"/>
<field name="quantity_done" string="Consumed" class="o_manual_consumption"
decoration-bf="not manual_consumption"
<field name="quantity_done" string="Consumed"
decoration-success="not is_done and (quantity_done - should_consume_qty == 0)"
decoration-warning="not is_done and (quantity_done - should_consume_qty &gt; 0.0001)"
attrs="{'column_invisible': [('parent.state', '=', 'draft')], 'readonly': [('has_tracking', '!=','none')]}"
force_save="1"/>
force_save="1" widget="mrp_consumed"/>
<field name="manual_consumption" invisible="1" force_save="1"/>
<field name="show_details_visible" invisible="1"/>
<field name="lot_ids" widget="many2many_tags"