[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:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user