[IMP] stock,mrp: improve _compute_(products/component)_availability

Improve 'Component Availability' and 'Product Availability':
Add these fields in form and tree view (the performance issues
is addressed in next commits and these fields will be load lazily)

task-2579011

Part-of: odoo/odoo#77092
This commit is contained in:
Rémy Voet (ryv)
2021-09-24 14:00:22 +00:00
parent 99515038fa
commit da06104c61
5 changed files with 128 additions and 27 deletions
+23 -11
View File
@@ -282,20 +282,32 @@ class MrpProduction(models.Model):
for production in self:
production.mrp_production_backorder_count = len(production.procurement_group_id.mrp_production_ids)
@api.depends('move_raw_ids', 'state', 'date_planned_start', 'move_raw_ids.forecast_availability', 'move_raw_ids.forecast_expected_date')
@api.depends('state', 'reservation_state', 'date_planned_start', 'move_raw_ids', 'move_raw_ids.forecast_availability', 'move_raw_ids.forecast_expected_date')
def _compute_components_availability(self):
self.components_availability = False
self.components_availability_state = 'available'
productions = self.filtered(lambda mo: mo.state not in ['cancel', 'draft', 'done'])
productions.components_availability = _('Available')
for production in productions:
forecast_date = max(production.move_raw_ids.filtered('forecast_expected_date').mapped('forecast_expected_date'), default=False)
if any(float_compare(move.forecast_availability, move.product_qty, move.product_id.uom_id.rounding) == -1 for move in production.move_raw_ids):
productions = self.filtered(lambda mo: mo.state not in ('cancel', 'done', 'draft'))
productions.components_availability_state = 'available'
other_productions = self - productions
other_productions.components_availability = False
other_productions.components_availability_state = False
productions_ready = productions.filtered(lambda mo: mo.reservation_state == 'assigned')
productions_ready.components_availability = _('Ready')
productions_not_ready = (productions - productions_ready)
productions_not_ready.components_availability = _('Available')
all_raw_moves = productions_not_ready.move_raw_ids
# Force to prefetch more than 1000 by 1000
all_raw_moves._fields['forecast_availability'].compute_value(all_raw_moves)
for production in productions_not_ready:
if any(float_compare(move.forecast_availability, 0 if move.state == 'draft' else move.product_qty, move.product_id.uom_id.rounding) == -1 for move in production.move_raw_ids):
production.components_availability = _('Not Available')
production.components_availability_state = 'late'
elif forecast_date:
production.components_availability = _('Exp %s', format_date(self.env, forecast_date))
production.components_availability_state = 'late' if forecast_date > production.date_planned_start else 'expected'
else:
forecast_date = max(production.move_raw_ids.filtered('forecast_expected_date').mapped('forecast_expected_date'), default=False)
if forecast_date:
production.components_availability = _('Exp %s', format_date(self.env, forecast_date))
if production.date_planned_start:
production.components_availability_state = 'late' if forecast_date > production.date_planned_start else 'expected'
@api.depends('move_finished_ids.date_deadline')
def _compute_date_deadline(self):
+55 -3
View File
@@ -4,9 +4,10 @@
from odoo.tests import Form
from datetime import datetime, timedelta
from odoo.fields import Datetime as Dt
from odoo import fields
from odoo.exceptions import UserError
from odoo.addons.mrp.tests.common import TestMrpCommon
from odoo.tools.misc import format_date
class TestMrpOrder(TestMrpCommon):
@@ -54,7 +55,7 @@ class TestMrpOrder(TestMrpCommon):
'inventory_quantity': 500
}).action_apply_inventory()
test_date_planned = Dt.now() - timedelta(days=1)
test_date_planned = fields.Datetime.now() - timedelta(days=1)
test_quantity = 3.0
man_order_form = Form(self.env['mrp.production'].with_user(self.user_mrp_user))
man_order_form.product_id = self.product_4
@@ -1509,6 +1510,57 @@ class TestMrpOrder(TestMrpCommon):
self.assertEqual(mos.move_finished_ids.mapped('state'), ['done'] * 3)
self.assertEqual(mos.move_finished_ids.mapped('quantity_done'), [1] * 3)
def test_components_availability(self):
self.bom_2.unlink() # remove the kit bom of product_5
now = fields.Datetime.now()
mo_form = Form(self.env['mrp.production'])
mo_form.bom_id = self.bom_3 # product_5 (2), product_4 (8), product_2 (12)
mo_form.date_planned_start = now
mo = mo_form.save()
self.assertEqual(mo.components_availability, False) # no compute for draft
mo.action_confirm()
self.assertEqual(mo.components_availability, 'Not Available')
tommorrow = fields.Datetime.now() + timedelta(days=1)
after_tommorrow = fields.Datetime.now() + timedelta(days=2)
warehouse = self.env.ref('stock.warehouse0')
move1 = self._create_move(
self.product_5, self.env.ref('stock.stock_location_suppliers'), warehouse.lot_stock_id,
product_uom_qty=2, date=tommorrow
)
move2 = self._create_move(
self.product_4, self.env.ref('stock.stock_location_suppliers'), warehouse.lot_stock_id,
product_uom_qty=8, date=tommorrow
)
move3 = self._create_move(
self.product_2, self.env.ref('stock.stock_location_suppliers'), warehouse.lot_stock_id,
product_uom_qty=12, date=tommorrow
)
(move1 | move2 | move3)._action_confirm()
mo.invalidate_cache(['components_availability', 'components_availability_state'], mo.ids)
self.assertEqual(mo.components_availability, 'Exp %s' % format_date(self.env, tommorrow))
self.assertEqual(mo.components_availability_state, 'late')
mo.date_planned_start = after_tommorrow
self.assertEqual(mo.components_availability, 'Exp %s' % format_date(self.env, tommorrow))
self.assertEqual(mo.components_availability_state, 'expected')
(move1 | move2 | move3)._set_quantities_to_reservation()
(move1 | move2 | move3)._action_done()
mo.invalidate_cache(['components_availability', 'components_availability_state'], mo.ids)
self.assertEqual(mo.components_availability, 'Available')
self.assertEqual(mo.components_availability_state, 'available')
mo.action_assign()
self.assertEqual(mo.reservation_state, 'assigned')
self.assertEqual(mo.components_availability, 'Ready')
self.assertEqual(mo.components_availability_state, 'available')
def test_immediate_validate_6(self):
"""In a production for a tracked product, clicking on mark as done without filling any quantities should
pop up the immediate transfer wizard. Processing should choose a new lot for the finished product. """
@@ -1847,7 +1899,7 @@ class TestMrpOrder(TestMrpCommon):
})
# Next Monday at 6:00 am UTC
date_planned = (Dt.now() + timedelta(days=7 - Dt.now().weekday())).replace(hour=6, minute=0, second=0)
date_planned = (fields.Datetime.now() + timedelta(days=7 - fields.Datetime.now().weekday())).replace(hour=6, minute=0, second=0)
mo_form = Form(self.env['mrp.production'])
mo_form.bom_id = bom
mo_form.date_planned_start = date_planned
+14 -2
View File
@@ -22,7 +22,14 @@
<field name="activity_ids" widget="list_activity" optional="show"/>
<field name="origin" optional="show"/>
<field name="user_id" optional="hide" widget="many2one_avatar_user"/>
<field name="reservation_state" optional="show" decoration-danger="reservation_state == 'confirmed'" decoration-success="reservation_state == 'assigned'"/>
<field name="components_availability_state" invisible="1"/>
<field name="components_availability"
attrs="{'invisible': [('state', 'not in', ['confirmed', 'progress'])]}"
optional="show"
decoration-success="components_availability_state == 'available'"
decoration-danger="components_availability_state == 'late'"
decoration-warning="components_availability_state == 'expected'"/>
<field name="reservation_state" optional="hide" decoration-danger="reservation_state == 'confirmed'" decoration-success="reservation_state == 'assigned'"/>
<field name="product_qty" sum="Total Qty" string="Quantity" readonly="1" optional="show"/>
<field name="product_uom_id" string="UoM" options="{'no_open':True,'no_create':True}" groups="uom.group_uom" optional="show"/>
<field name="production_duration_expected" attrs="{'invisible': [('production_duration_expected', '=', 0)]}" groups="mrp.group_mrp_routings" widget="float_time" sum="Total expected duration" optional="show"/>
@@ -204,6 +211,11 @@
<field name="delay_alert_date" invisible="1"/>
<field nolabel="1" name="json_popover" widget="stock_rescheduling_popover" attrs="{'invisible': [('json_popover', '=', False)]}"/>
</div>
<field name="components_availability_state" invisible="1"/>
<field name="components_availability" attrs="{'invisible': [('state', 'not in', ['confirmed', 'progress'])]}"
decoration-warning="components_availability_state == 'expected'"
decoration-danger="components_availability_state == 'late'"
decoration-success="components_availability_state == 'available'"/>
<field name="user_id" domain="[('share', '=', False)]"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}" attrs="{'readonly': [('state', '!=', 'draft')]}" force_save="1"/>
<field name="show_final_lots" invisible="1"/>
@@ -506,7 +518,7 @@
<filter string="Cancelled" name="filter_cancel" domain="[('state', '=', 'cancel')]"/>
<separator/>
<filter string="Waiting for Component" name="waiting" domain="[('reservation_state', 'in', ('waiting', 'confirmed'))]"/>
<filter string="Component Available" name="filter_ready" domain="[('reservation_state', '=', 'assigned')]"/>
<filter string="Ready" name="filter_ready" domain="[('reservation_state', '=', 'assigned')]"/>
<separator/>
<filter string="Planning Issues" name="planning_issues" help="Late MO or Late delivery of components"
domain="['|', ('delay_alert_date', '!=', False), '&amp;', ('date_deadline', '&lt;', current_date), ('state', '=', 'confirmed')]"/>
+22 -10
View File
@@ -431,20 +431,32 @@ class Picking(models.Model):
for picking in self:
picking.is_signed = picking.signature
@api.depends('move_lines', 'state', 'picking_type_code', 'move_lines.forecast_availability', 'move_lines.forecast_expected_date')
@api.depends('state', 'picking_type_code', 'scheduled_date', 'move_lines', 'move_lines.forecast_availability', 'move_lines.forecast_expected_date')
def _compute_products_availability(self):
self.products_availability = False
self.products_availability_state = 'available'
pickings = self.filtered(lambda picking: picking.state not in ['cancel', 'draft', 'done'] and picking.picking_type_code == 'outgoing')
pickings.products_availability = _('Available')
pickings = self.filtered(lambda picking: picking.state in ('waiting', 'confirmed', 'assigned') and picking.picking_type_code == 'outgoing')
pickings.products_availability_state = 'available'
other_pickings = self - pickings
other_pickings.products_availability = False
other_pickings.products_availability_state = False
pickings_ready = pickings.filtered(lambda picking: picking.state == 'assigned')
pickings_ready.products_availability = _('Ready')
pickings_not_ready = pickings - pickings_ready
pickings_not_ready.products_availability = _('Available')
all_moves = pickings_not_ready.move_lines
# Force to prefetch more than 1000 by 1000
all_moves._fields['forecast_availability'].compute_value(all_moves)
for picking in pickings:
forecast_date = max(picking.move_lines.filtered('forecast_expected_date').mapped('forecast_expected_date'), default=False)
if any(float_compare(move.forecast_availability, move.product_qty, precision_rounding=move.product_id.uom_id.rounding) == -1 for move in picking.move_lines):
# In case of draft the behavior of forecast_availability is different : if forecast_availability < 0 then there is a issue else not.
if any(float_compare(move.forecast_availability, 0 if move.state == 'draft' else move.product_qty, precision_rounding=move.product_id.uom_id.rounding) == -1 for move in picking.move_lines):
picking.products_availability = _('Not Available')
picking.products_availability_state = 'late'
elif forecast_date:
picking.products_availability = _('Exp %s', format_date(self.env, forecast_date))
picking.products_availability_state = 'late' if picking.date_deadline and picking.date_deadline < forecast_date else 'expected'
else:
forecast_date = max(picking.move_lines.filtered('forecast_expected_date').mapped('forecast_expected_date'), default=False)
if forecast_date:
picking.products_availability = _('Exp %s', format_date(self.env, forecast_date))
picking.products_availability_state = 'late' if picking.scheduled_date and picking.scheduled_date < forecast_date else 'expected'
@api.depends('picking_type_id.show_operations')
def _compute_show_operations(self):
+14 -1
View File
@@ -202,10 +202,17 @@
<field name="is_signed" string="Signed" optional="hide" groups="stock.group_stock_sign_delivery"/>
<field name="user_id" optional="hide" widget="many2one_avatar_user"/>
<field name="scheduled_date" optional="show" widget="remaining_days" attrs="{'invisible':[('state', 'in', ('done', 'cancel'))]}"/>
<field name="picking_type_code" invisible="1"/>
<field name="products_availability_state" invisible="1"/>
<field name="products_availability"
optional="hide"
attrs="{'invisible': ['|', ('picking_type_code', '!=', 'outgoing'), ('state', 'not in', ['confirmed', 'waiting', 'assigned'])]}"
decoration-warning="products_availability_state == 'expected'"
decoration-danger="products_availability_state == 'late'"
decoration-success="products_availability_state == 'available'"/>
<field name="date_deadline" optional="hide" widget="remaining_days" attrs="{'invisible':[('state', 'in', ('done', 'cancel'))]}"/>
<field name="date_done" string="Effective Date" optional="hide"/>
<field name="origin" optional="show"/>
<field name="group_id" invisible="1"/>
<field name="backorder_id" optional="hide"/>
<field name="picking_type_id" optional="hide"/>
<field name="company_id" groups="base.group_multi_company" optional="show"/>
@@ -326,6 +333,12 @@
attrs="{'invisible': ['|', ('state', 'in', ('done', 'cancel')), ('date_deadline', '=', False)]}"
decoration-danger="date_deadline and date_deadline &lt; current_date"
decoration-bf="date_deadline and date_deadline &lt; current_date"/>
<field name="products_availability_state" invisible="1"/>
<field name="products_availability"
attrs="{'invisible': ['|', ('picking_type_code', '!=', 'outgoing'), ('state', 'not in', ['confirmed', 'waiting', 'assigned'])]}"
decoration-warning="products_availability_state == 'expected'"
decoration-danger="products_availability_state == 'late'"
decoration-success="products_availability_state == 'available'"/>
<field name="date_done" string="Effective Date" attrs="{'invisible': [('state', '!=', 'done')]}"/>
<field name="origin" placeholder="e.g. PO0032"/>
<field name="owner_id" groups="stock.group_tracking_owner"