diff --git a/addons/stock/models/stock_location.py b/addons/stock/models/stock_location.py
index 55118a9a95e..d34027d5217 100644
--- a/addons/stock/models/stock_location.py
+++ b/addons/stock/models/stock_location.py
@@ -70,6 +70,8 @@ class Location(models.Model):
help='Let this field empty if this location is shared between companies')
scrap_location = fields.Boolean('Is a Scrap Location?', default=False, help='Check this box to allow using this location to put scrapped/damaged goods.')
return_location = fields.Boolean('Is a Return Location?', help='Check this box to allow using this location as a return location.')
+ replenish_location = fields.Boolean('Replenish Location', copy=False, compute="_compute_replenish_location", readonly=False, store=True,
+ help='Activate this function to get all quantities to replenish at this particular location')
removal_strategy_id = fields.Many2one(
'product.removal', 'Removal Strategy',
help="Defines the default method used for suggesting the exact location (shelf) "
@@ -167,6 +169,21 @@ class Location(models.Model):
if self.usage not in ('internal', 'inventory'):
self.scrap_location = False
+ @api.depends('usage')
+ def _compute_replenish_location(self):
+ for loc in self:
+ if loc.usage != 'internal':
+ loc.replenish_location = False
+
+ @api.constrains('replenish_location', 'location_id', 'usage')
+ def _check_replenish_location(self):
+ for loc in self:
+ if loc.replenish_location:
+ # cannot have parent/child location set as replenish as well
+ replenish_wh_location = self.search([('id', '!=', loc.id), ('replenish_location', '=', True), '|', ('location_id', 'child_of', loc.id), ('location_id', 'parent_of', loc.id)], limit=1)
+ if replenish_wh_location:
+ raise ValidationError(_('Another parent/sub replenish location %s exists, if you wish to change it, uncheck it first', replenish_wh_location.name))
+
@api.constrains('scrap_location')
def _check_scrap_location(self):
for record in self:
diff --git a/addons/stock/models/stock_orderpoint.py b/addons/stock/models/stock_orderpoint.py
index d13389f1ea2..bdee4f74590 100644
--- a/addons/stock/models/stock_orderpoint.py
+++ b/addons/stock/models/stock_orderpoint.py
@@ -335,82 +335,62 @@ class StockWarehouseOrderpoint(models.Model):
orderpoints_removed = orderpoints._unlink_processed_orderpoints()
orderpoints = orderpoints - orderpoints_removed
to_refill = defaultdict(float)
- all_product_ids = []
- all_warehouse_ids = []
- # Take 3 months since it's the max for the forecast report
- to_date = add(fields.date.today(), months=3)
- qty_by_product_warehouse = self.env['report.stock.quantity']._read_group(
- [('date', '=', to_date), ('state', '=', 'forecast')],
- ['product_id', 'product_qty', 'warehouse_id'],
- ['product_id', 'warehouse_id'], lazy=False)
- for group in qty_by_product_warehouse:
- warehouse_id = group.get('warehouse_id') and group['warehouse_id'][0]
- if group['product_qty'] >= 0.0 or not warehouse_id:
- continue
- all_product_ids.append(group['product_id'][0])
- all_warehouse_ids.append(warehouse_id)
- to_refill[(group['product_id'][0], warehouse_id)] = group['product_qty']
- if not to_refill:
- return action
+ all_product_ids = self.env['product.product'].search([('type', '=', 'product')])
+ all_replenish_location_ids = self.env['stock.location'].search([('replenish_location', '=', True)])
+ ploc_per_day = defaultdict(set)
+ product_loc_by_qty = {}
+ # For each replenish location get products with negative virtual_available aka forecast
+ for loc in all_replenish_location_ids:
+ quantities = all_product_ids.with_context(location=loc.id).mapped('virtual_available')
+ for product, quantity in zip(all_product_ids, quantities):
+ if float_compare(quantity, 0, precision_rounding=product.uom_id.rounding) >= 0:
+ continue
+ product_loc_by_qty[(product, loc)] = quantity
- # Recompute the forecasted quantity for missing product today but at this time
- # with their real lead days.
- key_to_remove = []
- pwh_per_day = defaultdict(list)
- for (product, warehouse) in to_refill.keys():
- product = self.env['product.product'].browse(product).with_prefetch(all_product_ids)
- warehouse = self.env['stock.warehouse'].browse(warehouse).with_prefetch(all_warehouse_ids)
- rules = product._get_rules_from_location(warehouse.lot_stock_id)
- lead_days = rules.with_context(bypass_delay_description=True)._get_lead_days(product)[0]
- pwh_per_day[(lead_days, warehouse)].append(product.id)
- # group product by lead_days and warehouse in order to read virtual_available
+ # group product by lead_days and location in order to read virtual_available
# in batch
+ for (product, loc), qty in product_loc_by_qty.items():
+ rules = product._get_rules_from_location(loc)
+ lead_days = rules.with_context(bypass_delay_description=True)._get_lead_days(product)[0]
+ ploc_per_day[(lead_days, loc)].add(product.id)
+
+ # recompute virtual_available with lead days
today = fields.datetime.now().replace(hour=23, minute=59, second=59)
- for (days, warehouse), p_ids in pwh_per_day.items():
- products = self.env['product.product'].browse(p_ids)
+ for (days, loc), product_ids in ploc_per_day.items():
+ products = self.env['product.product'].browse(product_ids)
qties = products.with_context(
- warehouse=warehouse.id,
+ location=loc.id,
to_date=today + relativedelta.relativedelta(days=days)
).read(['virtual_available'])
for qty in qties:
- if float_compare(qty['virtual_available'], 0, precision_rounding=product.uom_id.rounding) >= 0:
- key_to_remove.append((qty['id'], warehouse.id))
- else:
- to_refill[(qty['id'], warehouse.id)] = qty['virtual_available']
-
- for key in key_to_remove:
- del to_refill[key]
+ if float_compare(qty['virtual_available'], 0, precision_rounding=product.uom_id.rounding) < 0:
+ to_refill[(qty['id'], loc.id)] = qty['virtual_available']
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)
+ product_ids, location_ids = zip(*to_refill)
+ qty_by_product_loc, dummy = self.env['product.product'].browse(product_ids)._get_quantity_in_progress(location_ids=location_ids)
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
- # Group orderpoint by product-warehouse
- orderpoint_by_product_warehouse = self.env['stock.warehouse.orderpoint']._read_group(
+ # Group orderpoint by product-location
+ orderpoint_by_product_location = self.env['stock.warehouse.orderpoint']._read_group(
[('id', 'in', orderpoints.ids)],
- ['product_id', 'warehouse_id', 'qty_to_order:sum'],
- ['product_id', 'warehouse_id'], lazy=False)
- orderpoint_by_product_warehouse = {
- (record.get('product_id')[0], record.get('warehouse_id')[0]): record.get('qty_to_order')
- for record in orderpoint_by_product_warehouse
+ ['product_id', 'location_id', 'qty_to_order:sum'],
+ ['product_id', 'location_id'], lazy=False)
+ orderpoint_by_product_location = {
+ (record.get('product_id')[0], record.get('location_id')[0]): record.get('qty_to_order')
+ for record in orderpoint_by_product_location
}
- for (product, warehouse), product_qty in to_refill.items():
- qty_in_progress = qty_by_product_wh.get((product, warehouse)) or 0.0
- qty_in_progress += orderpoint_by_product_warehouse.get((product, warehouse), 0.0)
- # Add qty to order for other orderpoint under this warehouse.
+ for (product, location), product_qty in to_refill.items():
+ qty_in_progress = qty_by_product_loc.get((product, location)) or 0.0
+ qty_in_progress += orderpoint_by_product_location.get((product, location), 0.0)
+ # Add qty to order for other orderpoint under this location.
if not qty_in_progress:
continue
- to_refill[(product, warehouse)] = product_qty + qty_in_progress
+ to_refill[(product, location)] = product_qty + qty_in_progress
to_refill = {k: v for k, v in to_refill.items() if float_compare(
v, 0.0, precision_digits=rounding) < 0.0}
- lot_stock_id_by_warehouse = self.env['stock.warehouse'].with_context(active_test=False).search_read([
- ('id', 'in', [g[1] for g in to_refill.keys()])
- ], ['lot_stock_id'])
- lot_stock_id_by_warehouse = {w['id']: w['lot_stock_id'][0] for w in lot_stock_id_by_warehouse}
-
# With archived ones to avoid `product_location_check` SQL constraints
orderpoint_by_product_location = self.env['stock.warehouse.orderpoint'].with_context(active_test=False)._read_group(
[('id', 'in', orderpoints.ids)],
@@ -422,17 +402,17 @@ class StockWarehouseOrderpoint(models.Model):
}
orderpoint_values_list = []
- for (product, warehouse), product_qty in to_refill.items():
- lot_stock_id = lot_stock_id_by_warehouse[warehouse]
- orderpoint_id = orderpoint_by_product_location.get((product, lot_stock_id))
+ for (product, location_id), product_qty in to_refill.items():
+ orderpoint_id = orderpoint_by_product_location.get((product, location_id))
if orderpoint_id:
self.env['stock.warehouse.orderpoint'].browse(orderpoint_id).qty_forecast += product_qty
else:
- orderpoint_values = self.env['stock.warehouse.orderpoint']._get_orderpoint_values(product, lot_stock_id)
+ orderpoint_values = self.env['stock.warehouse.orderpoint']._get_orderpoint_values(product, location_id)
+ warehouse_id = self.env['stock.location'].browse(location_id).warehouse_id
orderpoint_values.update({
'name': _('Replenishment Report'),
- 'warehouse_id': warehouse,
- 'company_id': self.env['stock.warehouse'].browse(warehouse).company_id.id,
+ 'warehouse_id': warehouse_id.id,
+ 'company_id': warehouse_id.company_id.id,
})
orderpoint_values_list.append(orderpoint_values)
diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py
index 9885cdc3ebd..cb56bf97842 100644
--- a/addons/stock/models/stock_warehouse.py
+++ b/addons/stock/models/stock_warehouse.py
@@ -601,6 +601,7 @@ class Warehouse(models.Model):
'name': _('Stock'),
'active': True,
'usage': 'internal',
+ 'replenish_location': True,
'barcode': self._valid_barcode(code + '-STOCK', company_id)
},
'wh_input_stock_loc_id': {
diff --git a/addons/stock/tests/test_proc_rule.py b/addons/stock/tests/test_proc_rule.py
index 9a144733199..5e81ae16822 100644
--- a/addons/stock/tests/test_proc_rule.py
+++ b/addons/stock/tests/test_proc_rule.py
@@ -326,7 +326,7 @@ class TestProcRule(TransactionCase):
self.assertAlmostEqual(picking_pick.move_ids.product_uom_qty, 5.0)
self.assertAlmostEqual(picking_ship.move_ids.product_uom_qty, 3.0)
- def test_orderpoint_replenishment_view(self):
+ def test_orderpoint_replenishment_view_1(self):
""" Create two warehouses + two moves
verify that the replenishment view is consistent"""
warehouse_1 = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
@@ -380,6 +380,43 @@ class TestProcRule(TransactionCase):
{'location_id': warehouse_3.lot_stock_id.id, 'route_id': route_3.id},
])
+ def test_orderpoint_replenishment_view_2(self):
+ """ Create a warehouse + location to replenish warehouse instead of main location
+ verify that the orderpoints created are for the replenish locations not the warehouse main location"""
+ warehouse_1 = self.env['stock.warehouse'].create({
+ 'name': 'Warehouse 1',
+ 'code': 'WH1',
+ })
+ warehouse_1.lot_stock_id.replenish_location = False
+ replenish_loc = self.env['stock.location'].create({
+ 'name': 'Replenish Location',
+ 'location_id': warehouse_1.lot_stock_id.id,
+ 'replenish_location': True,
+ })
+ product = self.env['product.product'].create({
+ 'name': 'Rep Product',
+ 'type': 'product',
+ })
+ move = self.env['stock.move'].create({
+ 'name': 'Move WH2',
+ 'location_id': replenish_loc.id,
+ 'location_dest_id': self.partner.property_stock_customer.id,
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': 3,
+ })
+ move._action_confirm()
+ # activate action of opening the replenishment view
+ self.env.flush_all()
+ self.env['stock.warehouse.orderpoint'].action_open_orderpoints()
+ replenishments = self.env['stock.warehouse.orderpoint'].search([
+ ('product_id', '=', product.id),
+ ])
+ # Verify the location and the qty
+ self.assertRecordValues(replenishments, [
+ {'location_id': replenish_loc.id, 'qty_to_order': 3},
+ ])
+
def test_orderpoint_compute_warehouse_location(self):
warehouse_a = self.env['stock.warehouse'].search([], limit=1)
warehouse_b = self.env['stock.warehouse'].create({
diff --git a/addons/stock/views/stock_location_views.xml b/addons/stock/views/stock_location_views.xml
index 3a50fff54f7..46611b5dd06 100644
--- a/addons/stock/views/stock_location_views.xml
+++ b/addons/stock/views/stock_location_views.xml
@@ -39,6 +39,7 @@
+
diff --git a/addons/stock/views/stock_orderpoint_views.xml b/addons/stock/views/stock_orderpoint_views.xml
index fada53a1b91..5e7339011af 100644
--- a/addons/stock/views/stock_orderpoint_views.xml
+++ b/addons/stock/views/stock_orderpoint_views.xml
@@ -92,6 +92,10 @@
+
+
+
+
@@ -211,8 +215,8 @@
action = model.with_context(
search_default_filter_to_reorder=True,
search_default_filter_not_snoozed=True,
- search_default_filter_creation_trigger=True,
- default_trigger='manual'
+ default_trigger='manual',
+ searchpanel_default_trigger='manual'
).action_open_orderpoints()