[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:
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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), '&', ('date_deadline', '<', current_date), ('state', '=', 'confirmed')]"/>
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 < current_date"
|
||||
decoration-bf="date_deadline and date_deadline < 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"
|
||||
|
||||
Reference in New Issue
Block a user