diff options
| author | Arnold Moyaux <arm@odoo.com> | 2021-03-03 13:29:33 +0000 |
|---|---|---|
| committer | Arnold Moyaux <arm@odoo.com> | 2021-04-20 07:58:50 +0000 |
| commit | f939c7b2c4143d685c25d155a164d63070d36ed2 (patch) | |
| tree | e2101657e1c0f03e2cefed89b9a417836a0ffd35 | |
| parent | 4cad0ab17ae1917b7acc71cfb0fe2255845cdc94 (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.py | 21 | ||||
| -rw-r--r-- | addons/stock/tests/test_report_stock_quantity.py | 79 |
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) |
