From 2244e9b1275bc67ad202f4d000f081a331f7f8ae Mon Sep 17 00:00:00 2001 From: Adrien Widart Date: Mon, 23 May 2022 07:47:58 +0000 Subject: [PATCH] [FIX] stock, mrp: display the forecast symbol for inter-wh SM To reproduce the issue: (Let WH01 be the existing warehouse) 1. In Settings, enable "Storage Locations" 2. Create a second warehouse WH02 3. Create a storable product P 4. Create a planned and internal transfer: - From: WH01/Stock - To: WH02/Stock - With: 1 x P 5. Save the transfer - Error01: the forecast symbol is green, it should be red 6. Confirm the transfer - Error02: idem Error 01: Because the picking type is internal, we don't consider the picking as a consuming one. Therefore, we don't reach the line that define the forecast availability as negative: https://github.com/odoo/odoo/blob/892232b5aef42dd2706cb9d1d027b1c463ca50dd/addons/stock/models/stock_move.py#L446-L448 Error 02: Fixing the first error is not enough. Thanks to the first correction, we reach this line: https://github.com/odoo/odoo/blob/892232b5aef42dd2706cb9d1d027b1c463ca50dd/addons/stock/models/stock_move.py#L449-L450 And we will then call another method to compute the forecast availability: https://github.com/odoo/odoo/blob/892232b5aef42dd2706cb9d1d027b1c463ca50dd/addons/stock/models/stock_move.py#L457-L463 However, `_get_forecast_availability_outgoing` won't define any forecast availability for the move (it won't find any quantity to fulfill the need). So, when getting the value (`forecast_info[move]`), we will a have the default values: https://github.com/odoo/odoo/blob/892232b5aef42dd2706cb9d1d027b1c463ca50dd/addons/stock/models/stock_move.py#L2035 where `result` is the dict returned by `_get_forecast_availability_outgoing`. Later on, when using the field `forecast_availability` to render the view: in case of an outgoing transfer, we use the forecast widget: https://github.com/odoo/odoo/blob/73ab94402878a16c34c0e131818ffc0d2e8da3da/addons/stock/views/stock_picking_views.xml#L393-L394 And it correctly works because we compare the forecast availability with the demand: https://github.com/odoo/odoo/blob/6eaa4a2ae3b12b244f4c4277ef9cbc172f492f0a/addons/stock/static/src/js/forecast_widget.js#L34 However, in case of an internal transfer, we don't use the forecast widget: https://github.com/odoo/odoo/blob/3a2ee95c3ddfa0b2cf9383772ba48ebf6d9d5bb2/addons/stock/views/stock_picking_views.xml#L388-L391 And, if `forecast_availability` is equal to zero, it should mean that there will be just enough stock to fulfill the need [1]. This explains why the symbol is green. So, the issue comes from the values returned by `_get_forecast_availability_outgoing`: it should not set the forecast availability to zero if there isn't any stock available. [1] Some tests need to be fixed to respect this definition of `forecast_availability` task-2822157 closes odoo/odoo#94332 X-original-commit: 7610664cfbcfa89b0a9608663952544f8c9f195d Signed-off-by: Arnold Moyaux (arm) --- addons/mrp/models/stock_move.py | 7 ++----- addons/stock/models/stock_move.py | 22 +++++++++++++--------- addons/stock/tests/test_move.py | 18 ++++++++++++++++++ addons/stock/tests/test_report.py | 6 +++--- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 5b2cfdc743b..7b269c210ef 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -403,11 +403,8 @@ class StockMove(models.Model): vals['state'] = 'assigned' return vals - @api.model - def _consuming_picking_types(self): - res = super()._consuming_picking_types() - res.append('mrp_operation') - return res + def _is_consuming(self): + return super()._is_consuming() or self.picking_type_id.code == 'mrp_operation' def _get_backorder_move_vals(self): self.ensure_one() diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index faa075dbfe9..8be6934a9cd 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -432,11 +432,11 @@ class StockMove(models.Model): warehouse_id = move.location_dest_id.warehouse_id.id if incoming else move.location_id.warehouse_id.id return warehouse_id, max(move.date, now) - # Prefetch efficiently virtual_available for _consuming_picking_types draft move. + # Prefetch efficiently virtual_available for _is_consuming draft move. prefetch_virtual_available = defaultdict(set) virtual_available_dict = {} for move in product_moves: - if move.picking_type_id.code in self._consuming_picking_types() and move.state == 'draft': + if move._is_consuming() and move.state == 'draft': prefetch_virtual_available[key_virtual_available(move)].add(move.product_id.id) elif move.picking_type_id.code == 'incoming': prefetch_virtual_available[key_virtual_available(move, incoming=True)].add(move.product_id.id) @@ -445,12 +445,12 @@ class StockMove(models.Model): virtual_available_dict[key_context] = {res['id']: res['virtual_available'] for res in read_res} for move in product_moves: - if move.picking_type_id.code in self._consuming_picking_types(): + if move._is_consuming(): if move.state == 'assigned': move.forecast_availability = move.product_uom._compute_quantity( move.reserved_availability, move.product_id.uom_id, rounding_method='HALF-UP') elif move.state == 'draft': - # for move _consuming_picking_types and in draft -> the forecast_availability > 0 if in stock + # for move _is_consuming and in draft -> the forecast_availability > 0 if in stock move.forecast_availability = virtual_available_dict[key_virtual_available(move)][move.product_id.id] - move.product_qty elif move.state in ('waiting', 'confirmed', 'partially_available'): outgoing_unreserved_moves_per_warehouse[move.location_id.warehouse_id].add(move.id) @@ -723,7 +723,7 @@ class StockMove(models.Model): 'active_model': 'product.product', 'move_to_match_ids': self.ids, } - if self.picking_type_id.code in self._consuming_picking_types(): + if self._is_consuming(): warehouse = self.location_id.warehouse_id else: warehouse = self.location_dest_id.warehouse_id @@ -1787,9 +1787,11 @@ class StockMove(models.Model): for state, moves_ids in moves_state_to_write.items(): self.browse(moves_ids).filtered(lambda m: m.state != state).state = state - @api.model - def _consuming_picking_types(self): - return ['outgoing'] + def _is_consuming(self): + self.ensure_one() + from_wh = self.location_id.warehouse_id + to_wh = self.location_dest_id.warehouse_id + return self.picking_type_id.code == 'outgoing' or (from_wh and to_wh and from_wh != to_wh) def _get_lang(self): """Determine language to use for translated description""" @@ -2087,6 +2089,8 @@ class StockMove(models.Model): unreconciled_outs.append((demand, out)) for demand, out in unreconciled_outs: - _reconcile_out_with_ins(result, out, ins_per_product[product.id], demand, product_rounding, only_matching_move_dest=False) + remaining = _reconcile_out_with_ins(result, out, ins_per_product[product.id], demand, product_rounding, only_matching_move_dest=False) + if not float_is_zero(remaining, precision_rounding=out.product_id.uom_id.rounding) and out not in result: + result[out] = (-remaining, False) return result diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py index 1724c5d6480..4bac10b48b2 100644 --- a/addons/stock/tests/test_move.py +++ b/addons/stock/tests/test_move.py @@ -5723,3 +5723,21 @@ class StockMove(TransactionCase): line.qty_done = 1 self.assertEqual(picking.move_line_ids_without_package.location_dest_id, self.stock_location.child_ids[0]) + + def test_inter_wh_and_forecast_availability(self): + dest_wh = self.env['stock.warehouse'].create({ + 'name': 'Second Warehouse', + 'code': 'WH02', + }) + + move = self.env['stock.move'].create({ + 'name': 'test_interwh', + 'location_id': self.stock_location.id, + 'location_dest_id': dest_wh.lot_stock_id.id, + 'product_id': self.product.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 1.0, + }) + self.assertEqual(move.forecast_availability, -1) + move._action_confirm() + self.assertEqual(move.forecast_availability, -1) diff --git a/addons/stock/tests/test_report.py b/addons/stock/tests/test_report.py index ad67df12749..01ccbcfc337 100644 --- a/addons/stock/tests/test_report.py +++ b/addons/stock/tests/test_report.py @@ -977,7 +977,7 @@ class TestReports(TestReportsCommon): receipt.action_confirm() # Test compute _compute_forecast_information - self.assertEqual(delivery.move_ids.forecast_availability, 0.0) + self.assertEqual(delivery.move_ids.forecast_availability, -100) self.assertEqual(delivery2.move_ids.forecast_availability, 200) self.assertFalse(delivery.move_ids.forecast_expected_date) self.assertEqual(delivery2.move_ids.forecast_expected_date, receipt.move_ids.date) @@ -1209,7 +1209,7 @@ class TestReports(TestReportsCommon): self.assertEqual(lines[3]['document_out'].id, delivery_manual.id) all_delivery = delivery_by_date | delivery_at_confirm | delivery_by_date_priority | delivery_manual - self.assertEqual(all_delivery.move_ids.mapped("forecast_availability"), [0, 0, 0, 0]) + self.assertEqual(all_delivery.move_ids.mapped("forecast_availability"), [-3.0, -3.0, -3.0, -3.0]) # Creation of one receipt to fulfill the 2 first deliveries delivery_by_date and delivery_at_confirm receipt_form = Form(self.env['stock.picking']) @@ -1222,7 +1222,7 @@ class TestReports(TestReportsCommon): receipt1 = receipt_form.save() receipt1.action_confirm() - self.assertEqual(all_delivery.move_ids.mapped("forecast_availability"), [3, 3, 0, 0]) + self.assertEqual(all_delivery.move_ids.mapped("forecast_availability"), [3, 3, -3.0, -3.0]) def test_report_reception_1_one_receipt(self): """ Create 2 deliveries and 1 receipt where some of the products being received