[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:
@@ -58,6 +58,9 @@
|
||||
'web.assets_backend': [
|
||||
'mrp/static/src/**/*',
|
||||
],
|
||||
'web.assets_tests': [
|
||||
'mrp/static/tests/tours/**/*',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'mrp/static/tests/**/*',
|
||||
],
|
||||
|
||||
@@ -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(),
|
||||
]});
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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'), '&', ('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', '<', 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', '>=', 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 > 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"
|
||||
|
||||
Reference in New Issue
Block a user