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