diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index ff9e74f2605..88fa2af2375 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -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): diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index 442dc1949d1..ef8ec4a34c1 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -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 diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml index 1898d66ec52..44cb55c7887 100644 --- a/addons/mrp/views/mrp_production_views.xml +++ b/addons/mrp/views/mrp_production_views.xml @@ -22,7 +22,14 @@ - + + + @@ -204,6 +211,11 @@ + + @@ -506,7 +518,7 @@ - + diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index 8f135895ee6..7d6a640aa45 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -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): diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml index a033886d978..05267039e7e 100644 --- a/addons/stock/views/stock_picking_views.xml +++ b/addons/stock/views/stock_picking_views.xml @@ -202,10 +202,17 @@ + + + - @@ -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"/> + +