[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#69527

X-original-commit: f939c7b2c4143d685c25d155a164d63070d36ed2
Signed-off-by: Arnold Moyaux <amoyaux@users.noreply.github.com>
This commit is contained in:
Arnold Moyaux
2021-04-27 07:40:29 +00:00
parent aef42d683e
commit 90affa1301
2 changed files with 96 additions and 4 deletions
+21
View File
@@ -330,6 +330,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) in to_refill.keys():
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)
@@ -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)