diff --git a/addons/mrp/models/stock_rule.py b/addons/mrp/models/stock_rule.py
index 22b5cd3bd65..b9a285c5c4a 100644
--- a/addons/mrp/models/stock_rule.py
+++ b/addons/mrp/models/stock_rule.py
@@ -110,6 +110,25 @@ class StockRule(models.Model):
date_planned = date_planned - relativedelta(days=company_id.manufacturing_lead)
return date_planned
+ def _get_lead_days(self, product):
+ """Add the product and company manufacture delay to the cumulative delay
+ and cumulative description.
+ """
+ delay, delay_description = super()._get_lead_days(product)
+ manufacture_rule = self.filtered(lambda r: r.action == 'manufacture')
+ if not manufacture_rule:
+ return delay, delay_description
+ manufacture_rule.ensure_one()
+ manufacture_delay = product.produce_delay
+ if manufacture_delay:
+ delay += manufacture_delay
+ delay_description += '
%s + %d %s ' % (_('Manufacturing Lead Time'), manufacture_delay, _('day(s)'))
+ security_delay = manufacture_rule.picking_type_id.company_id.manufacturing_lead
+ if security_delay:
+ delay += security_delay
+ delay_description += '%s + %d %s ' % (_('Company Manufacture Lead Time'), security_delay, _('day(s)'))
+ return delay, delay_description
+
def _push_prepare_move_copy_values(self, move_to_copy, new_date):
new_move_vals = super(StockRule, self)._push_prepare_move_copy_values(move_to_copy, new_date)
new_move_vals['production_id'] = False
diff --git a/addons/purchase_stock/models/stock_rule.py b/addons/purchase_stock/models/stock_rule.py
index 7f8158ed829..93624ea7cc6 100644
--- a/addons/purchase_stock/models/stock_rule.py
+++ b/addons/purchase_stock/models/stock_rule.py
@@ -128,6 +128,24 @@ class StockRule(models.Model):
procurement.values, po))
self.env['purchase.order.line'].sudo().create(po_line_values)
+ def _get_lead_days(self, product):
+ """Add the company security lead time, days to purchase and the supplier
+ delay to the cumulative delay and cumulative description. The days to
+ purchase and company lead time are always displayed for onboarding
+ purpose in order to indicate that those options are available.
+ """
+ delay, delay_description = super()._get_lead_days(product)
+ buy_rule = self.filtered(lambda r: r.action == 'buy')
+ if not buy_rule or not product._prepare_sellers(False):
+ return delay, delay_description
+ buy_rule.ensure_one()
+ supplier_delay = product._prepare_sellers()[0].delay
+ if supplier_delay:
+ delay_description += '%s + %d %s ' % (_('Vendor Lead Time'), supplier_delay, _('day(s)'))
+ security_delay = buy_rule.picking_type_id.company_id.po_lead
+ delay_description += '%s + %d %s ' % (_('Purchase Security Lead Time'), security_delay, _('day(s)'))
+ return delay + supplier_delay + security_delay, delay_description
+
@api.model
def _get_procurements_to_merge_groupby(self, procurement):
# Do not group procument from different orderpoint. 1. _quantity_in_progress
diff --git a/addons/stock/__manifest__.py b/addons/stock/__manifest__.py
index 98ba3d074f7..8991029f641 100644
--- a/addons/stock/__manifest__.py
+++ b/addons/stock/__manifest__.py
@@ -80,6 +80,7 @@
'static/src/xml/inventory_report.xml',
'static/src/xml/inventory_lines.xml',
'static/src/xml/popover_widget.xml',
+ 'static/src/xml/stock_orderpoint_lead_days_widget.xml',
'static/src/xml/stock_traceability_report_backend.xml',
],
'installable': True,
diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py
index f5056f25894..df95ce00d43 100644
--- a/addons/stock/models/product.py
+++ b/addons/stock/models/product.py
@@ -523,6 +523,18 @@ class Product(models.Model):
raise UserError(msg)
return res
+ def _get_rules_from_location(self, location, seen_rules=False):
+ if not seen_rules:
+ seen_rules = self.env['stock.rule']
+ rule = self.env['procurement.group']._get_rule(self, location, {'warehouse_id': location.get_warehouse()})
+ if not rule:
+ return seen_rules
+ if rule.procure_method == 'make_to_stock' or rule.action not in ('pull_push', 'pull'):
+ return seen_rules | rule
+ else:
+ return self._get_rules_from_location(rule.location_src_id, seen_rules | rule)
+
+
class ProductTemplate(models.Model):
_inherit = 'product.template'
_check_company_auto = True
diff --git a/addons/stock/models/stock_orderpoint.py b/addons/stock/models/stock_orderpoint.py
index e4369e169d2..b805de95ecb 100644
--- a/addons/stock/models/stock_orderpoint.py
+++ b/addons/stock/models/stock_orderpoint.py
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from datetime import datetime
+from datetime import datetime, time
from dateutil import relativedelta
+from json import dumps
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
@@ -64,6 +65,9 @@ class StockWarehouseOrderpoint(models.Model):
default=lambda self: self.env.company)
allowed_location_ids = fields.One2many(comodel_name='stock.location', compute='_compute_allowed_location_ids')
+ json_lead_days_popover = fields.Char(compute='_compute_lead_days')
+ lead_days_date = fields.Date(compute='_compute_lead_days')
+
_sql_constraints = [
('qty_multiple_check', 'CHECK( qty_multiple >= 0 )', 'Qty Multiple must be greater than or equal to zero.'),
]
@@ -81,6 +85,22 @@ class StockWarehouseOrderpoint(models.Model):
loc_domain = expression.AND([loc_domain, ['|', ('company_id', '=', False), ('company_id', '=', orderpoint.company_id.id)]])
orderpoint.allowed_location_ids = self.env['stock.location'].search(loc_domain)
+ @api.depends('product_id', 'location_id', 'company_id', 'warehouse_id',
+ 'product_id.seller_ids', 'product_id.seller_ids.delay')
+ def _compute_lead_days(self):
+ for orderpoint in self:
+ rules = orderpoint.product_id._get_rules_from_location(orderpoint.location_id)
+ lead_days, lead_days_description = rules._get_lead_days(orderpoint.product_id)
+ lead_days_date = fields.Date.today() + relativedelta.relativedelta(days=lead_days)
+ orderpoint.json_lead_days_popover = dumps({
+ 'title': _('Replenishment'),
+ 'popoverTemplate': 'stock.leadDaysPopOver',
+ 'lead_days_date': fields.Date.to_string(lead_days_date),
+ 'lead_days_description': lead_days_description,
+ 'today': fields.Date.to_string(fields.Date.today()),
+ })
+ orderpoint.lead_days_date = lead_days_date
+
def _quantity_in_progress(self):
"""Return Quantities that are not yet in virtual stock but should be deduced from orderpoint rule
(example: purchases created from orderpoints)"""
@@ -122,6 +142,7 @@ class StockWarehouseOrderpoint(models.Model):
self.ensure_one()
return {
'location': self.location_id.id,
+ 'to_date': datetime.combine(self.lead_days_date, time.max)
}
def _prepare_procurement_values(self, product_qty, date=False, group=False):
diff --git a/addons/stock/models/stock_rule.py b/addons/stock/models/stock_rule.py
index c1a05a3fae2..77b31e830d2 100644
--- a/addons/stock/models/stock_rule.py
+++ b/addons/stock/models/stock_rule.py
@@ -9,7 +9,7 @@ from psycopg2 import OperationalError
from odoo import api, fields, models, registry, SUPERUSER_ID, _
from odoo.osv import expression
-from odoo.tools import float_compare, float_round, frozendict
+from odoo.tools import float_compare, float_round, frozendict, html_escape
from odoo.exceptions import UserError
import logging
@@ -323,6 +323,19 @@ class StockRule(models.Model):
user_id=product_id.responsible_id.id or SUPERUSER_ID,
)
+ def _get_lead_days(self, product):
+ """Returns the cumulative delay and its description encountered by a
+ procurement going through the rules in `self`.
+
+ :param product: the product of the procurement
+ :type product: :class:`~odoo.addons.product.models.product.ProductProduct`
+ :return: the cumulative delay and cumulative delay's description
+ :rtype: tuple
+ """
+ delay = sum(self.filtered(lambda r: r.action in ['pull', 'pull_push']).mapped('delay'))
+ delay_description = ''.join(['%s %s + %d %s ' % (_('Delay on'), html_escape(rule.name), rule.delay, _('day(s)')) for rule in self if rule.action in ['pull', 'pull_push'] and rule.delay])
+ return delay, delay_description
+
class ProcurementGroup(models.Model):
"""
diff --git a/addons/stock/static/src/xml/stock_orderpoint_lead_days_widget.xml b/addons/stock/static/src/xml/stock_orderpoint_lead_days_widget.xml
new file mode 100644
index 00000000000..51214708a24
--- /dev/null
+++ b/addons/stock/static/src/xml/stock_orderpoint_lead_days_widget.xml
@@ -0,0 +1,29 @@
+
+
+
+
+ If the forecasted stock on the is below the minimum quantity, replenishment will be triggered.
+
+
+
+
+
+ Today
+
+
+
+
+
+
+
+
+ Forecasted Date
+
+
+ =
+
+
+
+
+
+
diff --git a/addons/stock/tests/test_proc_rule.py b/addons/stock/tests/test_proc_rule.py
index 334213ddeb2..2ea7b6c0331 100644
--- a/addons/stock/tests/test_proc_rule.py
+++ b/addons/stock/tests/test_proc_rule.py
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from datetime import datetime, timedelta
+from datetime import date, datetime, timedelta
-from odoo.tests.common import TransactionCase
+from odoo.tests.common import Form, TransactionCase
from odoo.tools import mute_logger
@@ -167,6 +167,46 @@ class TestProcRule(TransactionCase):
self.assertAlmostEqual(move_orig.date, datetime.now(), delta=timedelta(seconds=10), msg='date should be now')
self.assertAlmostEqual(move_dest.date_expected, move_dest_initial_date + timedelta(days=6), delta=timedelta(seconds=10), msg='date should be propagated')
+ def test_reordering_rule_1(self):
+ warehouse = self.env['stock.warehouse'].search([], limit=1)
+ orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
+ orderpoint_form.product_id = self.product
+ orderpoint_form.location_id = warehouse.lot_stock_id
+ orderpoint_form.product_min_qty = 0.0
+ orderpoint_form.product_max_qty = 5.0
+ orderpoint_form.save()
+
+ self.env['stock.rule'].create({
+ 'name': 'Rule Supplier',
+ 'route_id': warehouse.reception_route_id.id,
+ 'location_id': warehouse.lot_stock_id.id,
+ 'location_src_id': self.env.ref('stock.stock_location_suppliers').id,
+ 'action': 'pull',
+ 'delay': 9.0,
+ 'procure_method': 'make_to_stock',
+ 'picking_type_id': warehouse.in_type_id.id,
+ })
+
+ delivery_move = self.env['stock.move'].create({
+ 'name': 'Delivery',
+ 'date_expected': datetime.today() + timedelta(days=5),
+ 'product_id': self.product.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 12.0,
+ 'location_id': warehouse.lot_stock_id.id,
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
+ })
+ delivery_move._action_confirm()
+ self.env['procurement.group'].run_scheduler()
+
+ receipt_move = self.env['stock.move'].search([
+ ('product_id', '=', self.product.id),
+ ('location_id', '=', self.env.ref('stock.stock_location_suppliers').id)
+ ])
+ self.assertTrue(receipt_move)
+ self.assertEqual(receipt_move.date_expected.date(), date.today())
+ self.assertEqual(receipt_move.product_uom_qty, 17.0)
+
class TestProcRuleLoad(TransactionCase):
def setUp(cls):
diff --git a/addons/stock/views/stock_warehouse_views.xml b/addons/stock/views/stock_warehouse_views.xml
index 434a32d31b0..8cc27320955 100644
--- a/addons/stock/views/stock_warehouse_views.xml
+++ b/addons/stock/views/stock_warehouse_views.xml
@@ -225,6 +225,7 @@
+