[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) <arm@odoo.com>
This commit is contained in:
Adrien Widart
2022-06-24 11:32:48 +02:00
parent 9c8ddea062
commit 2244e9b127
4 changed files with 36 additions and 17 deletions
+2 -5
View File
@@ -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()
+13 -9
View File
@@ -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
+18
View File
@@ -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)
+3 -3
View File
@@ -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