diff --git a/addons/stock/models/stock_orderpoint.py b/addons/stock/models/stock_orderpoint.py index 27e634dcd63..6918a6b5d89 100644 --- a/addons/stock/models/stock_orderpoint.py +++ b/addons/stock/models/stock_orderpoint.py @@ -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) 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)