summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArnold Moyaux <arm@odoo.com>2021-03-03 13:29:33 +0000
committerArnold Moyaux <arm@odoo.com>2021-04-20 07:58:50 +0000
commitf939c7b2c4143d685c25d155a164d63070d36ed2 (patch)
treee2101657e1c0f03e2cefed89b9a417836a0ffd35
parent4cad0ab17ae1917b7acc71cfb0fe2255845cdc94 (diff)
[FIX] stock: replenishment flow
Usecase to reproduce: - create a product with 0 stock - Sale order 500 units - a RFQ is created for 500 units - in replenishments, you have 0 units to order, OK - RFQ --> confirm PO - in replenishments, now the quantity to order is 500 --> should still be 0, it leads to confusions It happens because the replenish action check the missing quantity for today. It result by -500 units since the delivery is planned for the next day (default supplier lead days = 1). But since the orderpoint exist, the quantity forecasted is already computed correctly with the right lead time. But the action will remove the 500 missing today from the quantity forecasted (The idea is to add the missing quantity on the main location. e.g. I have -10 units in WH/Stock/Shelf1 -20 units in WH/Stock The orderpoint will only look for its own location -> 20 to reorder The replenishment report look by warehouse and not by location -> 30 missing. So 30 missing + 20 incomming -> 10 to add (by default on WH/Stock) The fix is to add a step when a product is missing then compute its virtual available with lead days (to avoid missing confirmed moves during the lead time). opw-2456978 closes odoo/odoo#67278 Signed-off-by: Arnold Moyaux <amoyaux@users.noreply.github.com>
-rw-r--r--addons/stock/models/stock_orderpoint.py21
-rw-r--r--addons/stock/tests/test_report_stock_quantity.py79
2 files changed, 96 insertions, 4 deletions
diff --git a/addons/stock/models/stock_orderpoint.py b/addons/stock/models/stock_orderpoint.py
index f30af5dff15..d45d3d72d24 100644
--- a/addons/stock/models/stock_orderpoint.py
+++ b/addons/stock/models/stock_orderpoint.py
@@ -316,6 +316,27 @@ class StockWarehouseOrderpoint(models.Model):
if not to_refill:
return action
+ # Recompute the forecasted quantity for missing product today but at this time
+ # with their real lead days.
+ key_to_remove = []
+ for (product, warehouse), quantity in to_refill.items():
+ product = self.env['product.product'].browse(product)
+ warehouse = self.env['stock.warehouse'].browse(warehouse)
+ rules = product._get_rules_from_location(warehouse.lot_stock_id)
+ lead_days = rules._get_lead_days(product)[0]
+ virtual_available = product.with_context(
+ warehouse=warehouse.id,
+ to_date=fields.datetime.now() + relativedelta.relativedelta(days=lead_days)
+ ).virtual_available
+ if float_compare(virtual_available, 0, precision_rounding=product.uom_id.rounding) >= 0:
+ key_to_remove.append((product.id, warehouse.id))
+ else:
+ to_refill[(product.id, warehouse.id)] = virtual_available
+ for key in key_to_remove:
+ del to_refill[key]
+ if not to_refill:
+ return action
+
# Remove incoming quantity from other origin than moves (e.g RFQ)
product_ids, warehouse_ids = zip(*to_refill)
dummy, qty_by_product_wh = self.env['product.product'].browse(product_ids)._get_quantity_in_progress(warehouse_ids=warehouse_ids)
diff --git a/addons/stock/tests/test_report_stock_quantity.py b/addons/stock/tests/test_report_stock_quantity.py
index bce00974312..cdf0e8d596b 100644
--- a/addons/stock/tests/test_report_stock_quantity.py
+++ b/addons/stock/tests/test_report_stock_quantity.py
@@ -1,6 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import timedelta
+
from odoo import fields, tests
+from odoo.tests.common import Form
class TestReportStockQuantity(tests.TransactionCase):
@@ -14,7 +18,7 @@ class TestReportStockQuantity(tests.TransactionCase):
'tracking': 'lot',
'barcode': 'scan_me'
})
- wh = self.env['stock.warehouse'].create({
+ self.wh = self.env['stock.warehouse'].create({
'name': 'Base Warehouse',
'code': 'TESTWH'
})
@@ -26,7 +30,7 @@ class TestReportStockQuantity(tests.TransactionCase):
self.move1 = self.env['stock.move'].create({
'name': 'test_in_1',
'location_id': self.supplier_location.id,
- 'location_dest_id': wh.lot_stock_id.id,
+ 'location_dest_id': self.wh.lot_stock_id.id,
'product_id': self.product1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 100.0,
@@ -35,13 +39,13 @@ class TestReportStockQuantity(tests.TransactionCase):
})
self.quant1 = self.env['stock.quant'].create({
'product_id': self.product1.id,
- 'location_id': wh.lot_stock_id.id,
+ 'location_id': self.wh.lot_stock_id.id,
'quantity': 100.0,
})
# ship
self.move2 = self.env['stock.move'].create({
'name': 'test_out_1',
- 'location_id': wh.lot_stock_id.id,
+ 'location_id': self.wh.lot_stock_id.id,
'location_dest_id': self.customer_location.id,
'product_id': self.product1.id,
'product_uom': self.uom_unit.id,
@@ -73,3 +77,70 @@ class TestReportStockQuantity(tests.TransactionCase):
lazy=False)
forecast_report = [x['product_qty'] for x in report if x['state'] == 'forecast']
self.assertEqual(forecast_report, [-20, -20])
+
+ def test_replenishment_report_1(self):
+ self.product_replenished = self.env['product.product'].create({
+ 'name': 'Security razor',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+ # get auto-created pull rule from when warehouse is created
+ self.wh.reception_route_id.rule_ids.unlink()
+ self.env['stock.rule'].create({
+ 'name': 'Rule Supplier',
+ 'route_id': self.wh.reception_route_id.id,
+ 'location_id': self.wh.lot_stock_id.id,
+ 'location_src_id': self.env.ref('stock.stock_location_suppliers').id,
+ 'action': 'pull',
+ 'delay': 1.0,
+ 'procure_method': 'make_to_stock',
+ 'picking_type_id': self.wh.in_type_id.id,
+ })
+ delivery_picking = self.env['stock.picking'].create({
+ 'location_id': self.wh.lot_stock_id.id,
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
+ 'picking_type_id': self.ref('stock.picking_type_out'),
+ })
+ self.env['stock.move'].create({
+ 'name': 'Delivery',
+ 'product_id': self.product_replenished.id,
+ 'product_uom_qty': 500.0,
+ 'product_uom': self.uom_unit.id,
+ 'location_id': self.wh.lot_stock_id.id,
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
+ 'picking_id': delivery_picking.id,
+ })
+ delivery_picking.action_confirm()
+
+ # Trigger the manual orderpoint creation for missing product
+ self.env['stock.move'].flush()
+ self.env['stock.warehouse.orderpoint'].action_open_orderpoints()
+
+ orderpoint = self.env['stock.warehouse.orderpoint'].search([
+ ('product_id', '=', self.product_replenished.id)
+ ])
+ self.assertTrue(orderpoint)
+ self.assertEqual(orderpoint.location_id, self.wh.lot_stock_id)
+ self.assertEqual(orderpoint.qty_to_order, 500.0)
+ orderpoint.action_replenish()
+ self.env['stock.warehouse.orderpoint'].action_open_orderpoints()
+
+ move = self.env['stock.move'].search([
+ ('product_id', '=', self.product_replenished.id),
+ ('location_dest_id', '=', self.wh.lot_stock_id.id)
+ ])
+ # Simulate a supplier delay
+ move.date = fields.datetime.now() + timedelta(days=1)
+ orderpoint = self.env['stock.warehouse.orderpoint'].search([
+ ('product_id', '=', self.product_replenished.id)
+ ])
+ self.assertFalse(orderpoint)
+
+ orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
+ orderpoint_form.product_id = self.product_replenished
+ orderpoint_form.location_id = self.wh.lot_stock_id
+ orderpoint = orderpoint_form.save()
+
+ self.assertEqual(orderpoint.qty_to_order, 0.0)
+ self.env['stock.warehouse.orderpoint'].action_open_orderpoints()
+ self.assertEqual(orderpoint.qty_to_order, 0.0)