diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py
index fb1a1131def..b0cdaa89b2d 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -658,7 +658,7 @@ class MrpProduction(models.Model):
for production in self:
if not production.date_planned_start or production.is_planned:
continue
- days_delay = production.product_id.produce_delay + production.company_id.manufacturing_lead
+ days_delay = production.product_id.produce_delay
date_planned_finished = production.date_planned_start + relativedelta(days=days_delay)
if date_planned_finished == production.date_planned_start:
date_planned_finished = date_planned_finished + relativedelta(hours=1)
@@ -904,7 +904,6 @@ class MrpProduction(models.Model):
def _get_date_planned_finished(self):
date_planned_finished = self.date_planned_start + relativedelta(days=self.product_id.produce_delay)
- date_planned_finished = date_planned_finished + relativedelta(days=self.company_id.manufacturing_lead)
if date_planned_finished == self.date_planned_start:
date_planned_finished = date_planned_finished + relativedelta(hours=1)
return date_planned_finished
diff --git a/addons/mrp/models/product.py b/addons/mrp/models/product.py
index 787c5c5c8bd..eeaf8deba20 100644
--- a/addons/mrp/models/product.py
+++ b/addons/mrp/models/product.py
@@ -33,6 +33,10 @@ class ProductTemplate(models.Model):
'Manufacturing Lead Time', default=0.0,
help="Average lead time in days to manufacture this product. In the case of multi-level BOM, the manufacturing lead times of the components will be added. In case the product is subcontracted, this can be used to determine the date at which components should be sent to the subcontractor.")
is_kits = fields.Boolean(compute='_compute_is_kits', compute_sudo=False)
+ days_to_prepare_mo = fields.Float(
+ string="Days to prepare Manufacturing Order", default=0.0,
+ help="Create and confirm Manufacturing Orders these many days in advance, to have enough time to replenish components or manufacture semi-finished products.\n"
+ "Note that this does not affect the MO scheduled date, which still respects the just-in-time mechanism.")
def _compute_bom_count(self):
for product in self:
diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py
index 77660241770..3ae798e8048 100644
--- a/addons/mrp/models/stock_move.py
+++ b/addons/mrp/models/stock_move.py
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
+from dateutil.relativedelta import relativedelta
from odoo import api, Command, fields, models
from odoo.osv import expression
from odoo.tools import float_compare, float_round, float_is_zero, OrderedSet
@@ -547,3 +548,9 @@ class StockMove(models.Model):
res = super()._prepare_procurement_values()
res['bom_line_id'] = self.bom_line_id.id
return res
+
+ def _get_mto_procurement_date(self):
+ date = super()._get_mto_procurement_date()
+ if 'manufacture' in self.product_id._get_rules_from_location(self.location_id).mapped('action'):
+ date -= relativedelta(days=self.company_id.manufacturing_lead)
+ return date
diff --git a/addons/mrp/models/stock_orderpoint.py b/addons/mrp/models/stock_orderpoint.py
index 2e5022c7d27..eb4ac948bc3 100644
--- a/addons/mrp/models/stock_orderpoint.py
+++ b/addons/mrp/models/stock_orderpoint.py
@@ -4,6 +4,7 @@
from odoo import _, api, fields, models
from odoo.tools.float_utils import float_is_zero
from odoo.osv.expression import AND
+from dateutil.relativedelta import relativedelta
class StockWarehouseOrderpoint(models.Model):
@@ -13,6 +14,7 @@ class StockWarehouseOrderpoint(models.Model):
bom_id = fields.Many2one(
'mrp.bom', string='Bill of Materials', check_company=True,
domain="[('type', '=', 'normal'), '&', '|', ('company_id', '=', company_id), ('company_id', '=', False), '|', ('product_id', '=', product_id), '&', ('product_id', '=', False), ('product_tmpl_id', '=', product_tmpl_id)]")
+ manufacturing_visibility_days = fields.Float(default=0.0, help="Visibility Days applied on the manufacturing routes.")
def _get_replenishment_order_notification(self):
self.ensure_one()
@@ -45,6 +47,27 @@ class StockWarehouseOrderpoint(models.Model):
for orderpoint in self:
orderpoint.show_bom = orderpoint.route_id.id in manufacture_route
+ def _compute_visibility_days(self):
+ res = super()._compute_visibility_days()
+ for orderpoint in self:
+ if 'manufacture' in orderpoint.rule_ids.mapped('action'):
+ orderpoint.visibility_days = orderpoint.manufacturing_visibility_days
+ return res
+
+ def _set_visibility_days(self):
+ res = super()._set_visibility_days()
+ for orderpoint in self:
+ if 'manufacture' in orderpoint.rule_ids.mapped('action'):
+ orderpoint.manufacturing_visibility_days = orderpoint.visibility_days
+ return res
+
+ def _compute_days_to_order(self):
+ res = super()._compute_days_to_order()
+ for orderpoint in self:
+ if 'manufacture' in orderpoint.rule_ids.mapped('action'):
+ orderpoint.days_to_order = orderpoint.product_id.days_to_prepare_mo
+ return res
+
def _quantity_in_progress(self):
bom_kits = self.env['mrp.bom']._bom_find(self.product_id, bom_type='phantom')
bom_kit_orderpoints = {
@@ -109,6 +132,12 @@ class StockWarehouseOrderpoint(models.Model):
orderpoint_wh_bom.route_id = route_id[0].id
return super()._set_default_route_id()
+ def _get_orderpoint_procurement_date(self):
+ date = super()._get_orderpoint_procurement_date()
+ if any(rule.action == 'manufacture' for rule in self.rule_ids):
+ date -= relativedelta(days=self.company_id.manufacturing_lead)
+ return date
+
def _prepare_procurement_values(self, date=False, group=False):
values = super()._prepare_procurement_values(date=date, group=group)
values['bom_id'] = self.bom_id
diff --git a/addons/mrp/models/stock_rule.py b/addons/mrp/models/stock_rule.py
index 8af591ea519..82da0108cd5 100644
--- a/addons/mrp/models/stock_rule.py
+++ b/addons/mrp/models/stock_rule.py
@@ -109,7 +109,7 @@ class StockRule(models.Model):
def _prepare_mo_vals(self, product_id, product_qty, product_uom, location_dest_id, name, origin, company_id, values, bom):
date_planned = self._get_date_planned(product_id, company_id, values)
- date_deadline = values.get('date_deadline') or date_planned + relativedelta(days=company_id.manufacturing_lead) + relativedelta(days=product_id.produce_delay)
+ date_deadline = values.get('date_deadline') or date_planned + relativedelta(days=product_id.produce_delay)
mo_values = {
'origin': origin,
'product_id': product_id.id,
@@ -144,7 +144,6 @@ class StockRule(models.Model):
def _get_date_planned(self, product_id, company_id, values):
format_date_planned = fields.Datetime.from_string(values['date_planned'])
date_planned = format_date_planned - relativedelta(days=product_id.produce_delay)
- date_planned = date_planned - relativedelta(days=company_id.manufacturing_lead)
if date_planned == format_date_planned:
date_planned = date_planned - relativedelta(hours=1)
return date_planned
diff --git a/addons/mrp/views/product_views.xml b/addons/mrp/views/product_views.xml
index 644ed616540..5ea072a561e 100644
--- a/addons/mrp/views/product_views.xml
+++ b/addons/mrp/views/product_views.xml
@@ -12,6 +12,10 @@
days
+
+
+ days
+
diff --git a/addons/purchase_mrp/models/stock_move.py b/addons/purchase_mrp/models/stock_move.py
index e6e3921952a..57322213a58 100644
--- a/addons/purchase_mrp/models/stock_move.py
+++ b/addons/purchase_mrp/models/stock_move.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from dateutil.relativedelta import relativedelta
from odoo import _, models
from odoo.tools.float_utils import float_is_zero
from odoo.exceptions import UserError
@@ -30,3 +31,9 @@ class StockMove(models.Model):
if float_is_zero(valuation_total_qty, precision_rounding=related_aml.product_uom_id.rounding or related_aml.product_id.uom_id.rounding):
raise UserError(_('Odoo is not able to generate the anglo saxon entries. The total valuation of %s is zero.') % related_aml.product_id.display_name)
return valuation_price_unit_total, valuation_total_qty
+
+ def _get_mto_procurement_date(self):
+ date = super()._get_mto_procurement_date()
+ if 'buy' in self.product_id._get_rules_from_location(self.location_id).mapped('action'):
+ date -= relativedelta(days=self.company_id.po_lead)
+ return date
diff --git a/addons/purchase_stock/models/__init__.py b/addons/purchase_stock/models/__init__.py
index 85cc204cb90..4a14dca403a 100644
--- a/addons/purchase_stock/models/__init__.py
+++ b/addons/purchase_stock/models/__init__.py
@@ -4,8 +4,8 @@
from . import account_invoice
from . import product
from . import purchase
-from . import res_company
from . import res_config_settings
from . import res_partner
+from . import res_company
from . import stock
from . import stock_rule
diff --git a/addons/purchase_stock/models/res_config_settings.py b/addons/purchase_stock/models/res_config_settings.py
index 5f45996256d..f3370a8c909 100644
--- a/addons/purchase_stock/models/res_config_settings.py
+++ b/addons/purchase_stock/models/res_config_settings.py
@@ -10,7 +10,6 @@ class ResConfigSettings(models.TransientModel):
module_stock_dropshipping = fields.Boolean("Dropshipping")
days_to_purchase = fields.Float(
related='company_id.days_to_purchase', readonly=False)
-
is_installed_sale = fields.Boolean(string="Is the Sale Module Installed")
def get_values(self):
diff --git a/addons/purchase_stock/models/stock.py b/addons/purchase_stock/models/stock.py
index 5f035a59f13..beb00ce03e0 100644
--- a/addons/purchase_stock/models/stock.py
+++ b/addons/purchase_stock/models/stock.py
@@ -215,6 +215,7 @@ class Orderpoint(models.Model):
'product.supplierinfo', string='Product Supplier', check_company=True,
domain="['|', ('product_id', '=', product_id), '&', ('product_id', '=', False), ('product_tmpl_id', '=', product_tmpl_id)]")
vendor_id = fields.Many2one(related='supplier_id.partner_id', string="Vendor", store=True)
+ purchase_visibility_days = fields.Float(default=0.0, help="Visibility Days applied on the purchase routes.")
@api.depends('product_id.purchase_order_line_ids', 'product_id.purchase_order_line_ids.state')
def _compute_qty(self):
@@ -225,6 +226,26 @@ class Orderpoint(models.Model):
def _compute_lead_days(self):
return super()._compute_lead_days()
+ def _compute_visibility_days(self):
+ res = super()._compute_visibility_days()
+ for orderpoint in self:
+ if 'buy' in orderpoint.rule_ids.mapped('action'):
+ orderpoint.visibility_days = orderpoint.purchase_visibility_days
+ return res
+
+ def _set_visibility_days(self):
+ res = super()._set_visibility_days()
+ for orderpoint in self:
+ if 'buy' in orderpoint.rule_ids.mapped('action'):
+ orderpoint.purchase_visibility_days = orderpoint.visibility_days
+ return res
+
+ def _compute_days_to_order(self):
+ res = super()._compute_days_to_order()
+ for orderpoint in self:
+ if 'buy' in orderpoint.rule_ids.mapped('action'):
+ orderpoint.days_to_order = orderpoint.company_id.days_to_purchase
+ return res
@api.depends('route_id')
def _compute_show_suppplier(self):
buy_route = []
diff --git a/addons/purchase_stock/models/stock_rule.py b/addons/purchase_stock/models/stock_rule.py
index bda7561d6cb..b58783b38c7 100644
--- a/addons/purchase_stock/models/stock_rule.py
+++ b/addons/purchase_stock/models/stock_rule.py
@@ -152,9 +152,8 @@ class StockRule(models.Model):
self.env['purchase.order.line'].sudo().create(po_line_values)
def _get_lead_days(self, product, **values):
- """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
+ """Add the company security lead time and the supplier delay to the cumulative delay
+ and cumulative description. The company lead time is always displayed for onboarding
purpose in order to indicate that those options are available.
"""
delay, delay_description = super()._get_lead_days(product, **values)
@@ -170,10 +169,7 @@ class StockRule(models.Model):
security_delay = buy_rule.picking_type_id.company_id.po_lead
if not bypass_delay_description:
delay_description.append((_('Purchase Security Lead Time'), _('+ %d day(s)', security_delay)))
- days_to_purchase = buy_rule.company_id.days_to_purchase
- if not bypass_delay_description:
- delay_description.append((_('Days to Purchase'), _('+ %d day(s)', days_to_purchase)))
- return delay + supplier_delay + security_delay + days_to_purchase, delay_description
+ return delay + supplier_delay + security_delay, delay_description
@api.model
def _get_procurements_to_merge_groupby(self, procurement):
diff --git a/addons/purchase_stock/tests/test_purchase_lead_time.py b/addons/purchase_stock/tests/test_purchase_lead_time.py
index 5a5cfdd79c4..b67f9292d8e 100644
--- a/addons/purchase_stock/tests/test_purchase_lead_time.py
+++ b/addons/purchase_stock/tests/test_purchase_lead_time.py
@@ -296,6 +296,7 @@ class TestPurchaseLeadTime(PurchaseTestCommon):
orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
orderpoint_form.product_id = product
orderpoint_form.product_min_qty = 0.0
+ orderpoint_form.visibility_days = 1.0
orderpoint = orderpoint_form.save()
orderpoint_form = Form(self.env['stock.warehouse.orderpoint'].with_company(company2))
@@ -320,12 +321,12 @@ class TestPurchaseLeadTime(PurchaseTestCommon):
po_line = self.env['purchase.order.line'].search([('product_id', '=', product.id)])
self.assertEqual(fields.Date.to_date(po_line.order_id.date_order), fields.Date.today() + timedelta(days=2))
self.assertEqual(len(po_line), 1)
- self.assertEqual(po_line.product_uom_qty, 20.0)
+ self.assertEqual(po_line.product_uom_qty, 25.0)
self.assertEqual(len(po_line.order_id), 1)
orderpoint_form = Form(orderpoint)
orderpoint_form.save()
- self.mock_date.today.return_value = fields.Date.today() + timedelta(days=1)
+ self.mock_date.today.return_value = fields.Date.today() + timedelta(days=2)
orderpoint._compute_qty()
self.env['procurement.group'].run_scheduler()
po_line = self.env['purchase.order.line'].search([('product_id', '=', product.id)])
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index e6f9ec09b34..faa075dbfe9 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -1283,7 +1283,7 @@ class StockMove(models.Model):
product_id = self.product_id.with_context(lang=self._get_lang())
return {
'product_description_variants': self.description_picking and self.description_picking.replace(product_id._get_description(self.picking_type_id), ''),
- 'date_planned': self.date,
+ 'date_planned': self._get_mto_procurement_date(),
'date_deadline': self.date_deadline,
'move_dest_ids': self,
'group_id': group_id,
@@ -1294,6 +1294,9 @@ class StockMove(models.Model):
'product_packaging_id': self.product_packaging_id,
}
+ def _get_mto_procurement_date(self):
+ return self.date
+
def _prepare_move_line_vals(self, quantity=None, reserved_quant=None):
self.ensure_one()
vals = {
diff --git a/addons/stock/models/stock_orderpoint.py b/addons/stock/models/stock_orderpoint.py
index 21446258f4c..d3b5f3baf6d 100644
--- a/addons/stock/models/stock_orderpoint.py
+++ b/addons/stock/models/stock_orderpoint.py
@@ -97,6 +97,12 @@ class StockWarehouseOrderpoint(models.Model):
qty_forecast = fields.Float('Forecast', readonly=True, compute='_compute_qty')
qty_to_order = fields.Float('To Order', compute='_compute_qty_to_order', store=True, readonly=False)
+ days_to_order = fields.Float(compute='_compute_days_to_order', help="Numbers of days in advance that replenishments demands are created.")
+ visibility_days = fields.Float(
+ compute='_compute_visibility_days', inverse='_set_visibility_days', readonly=False,
+ help="Consider product forecast these many days in the future upon product replenishment, set to 0 for just-in-time.\n"
+ "The value depends on the type of the route (Buy or Manufacture)")
+
_sql_constraints = [
('qty_multiple_check', 'CHECK( qty_multiple >= 0 )', 'Qty Multiple must be greater than or equal to zero.'),
('product_location_check', 'unique (product_id, location_id, company_id)', 'A replenishment rule already exists for this product on this location.'),
@@ -134,6 +140,17 @@ class StockWarehouseOrderpoint(models.Model):
continue
orderpoint.rule_ids = orderpoint.product_id._get_rules_from_location(orderpoint.location_id, route_ids=orderpoint.route_id)
+ @api.depends('route_id', 'product_id')
+ def _compute_visibility_days(self):
+ self.visibility_days = 0
+
+ def _set_visibility_days(self):
+ return True
+
+ @api.depends('route_id', 'product_id')
+ def _compute_days_to_order(self):
+ self.days_to_order = 0
+
@api.constrains('product_id')
def _check_product_uom(self):
''' Check if the UoM has the same category as the product standard UoM '''
@@ -251,7 +268,7 @@ class StockWarehouseOrderpoint(models.Model):
orderpoint.qty_on_hand = products_qty[orderpoint.product_id.id]['qty_available']
orderpoint.qty_forecast = products_qty[orderpoint.product_id.id]['virtual_available'] + products_qty_in_progress[orderpoint.id]
- @api.depends('qty_multiple', 'qty_forecast', 'product_min_qty', 'product_max_qty')
+ @api.depends('qty_multiple', 'qty_forecast', 'product_min_qty', 'product_max_qty', 'visibility_days')
def _compute_qty_to_order(self):
for orderpoint in self:
if not orderpoint.product_id or not orderpoint.location_id:
@@ -260,8 +277,10 @@ class StockWarehouseOrderpoint(models.Model):
qty_to_order = 0.0
rounding = orderpoint.product_uom.rounding
if float_compare(orderpoint.qty_forecast, orderpoint.product_min_qty, precision_rounding=rounding) < 0:
- qty_to_order = max(orderpoint.product_min_qty, orderpoint.product_max_qty) - orderpoint.qty_forecast
-
+ # We want to know how much we should order to also satisfy the needs that gonna appear in the next (visibility) days
+ product_context = orderpoint._get_product_context(visibility_days=orderpoint.visibility_days)
+ qty_forecast_with_visibility = orderpoint.product_id.with_context(product_context).read(['virtual_available'])[0]['virtual_available'] + orderpoint._quantity_in_progress()[orderpoint.id]
+ qty_to_order = max(orderpoint.product_min_qty, orderpoint.product_max_qty) - qty_forecast_with_visibility
remainder = orderpoint.qty_multiple > 0 and qty_to_order % orderpoint.qty_multiple or 0.0
if float_compare(remainder, 0.0, precision_rounding=rounding) > 0:
qty_to_order += orderpoint.qty_multiple - remainder
@@ -291,14 +310,16 @@ class StockWarehouseOrderpoint(models.Model):
def _get_lead_days_values(self):
self.ensure_one()
- return dict()
+ return {
+ 'days_to_order': self.days_to_order,
+ }
- def _get_product_context(self):
+ def _get_product_context(self, visibility_days=0):
"""Used to call `virtual_available` when running an orderpoint."""
self.ensure_one()
return {
'location': self.location_id.id,
- 'to_date': datetime.combine(self.lead_days_date, time.max)
+ 'to_date': datetime.combine(self.lead_days_date + relativedelta.relativedelta(days=visibility_days), time.max)
}
def _get_orderpoint_action(self):
diff --git a/addons/stock/models/stock_rule.py b/addons/stock/models/stock_rule.py
index 0c4ae8071d6..912855dc55d 100644
--- a/addons/stock/models/stock_rule.py
+++ b/addons/stock/models/stock_rule.py
@@ -350,6 +350,7 @@ class StockRule(models.Model):
:rtype: tuple[int, list[str, str]]
"""
delay = sum(self.filtered(lambda r: r.action in ['pull', 'pull_push']).mapped('delay'))
+ days_to_order = values.get('days_to_order', 0)
if self.env.context.get('bypass_delay_description'):
delay_description = []
else:
@@ -358,7 +359,8 @@ class StockRule(models.Model):
for rule in self
if rule.action in ['pull', 'pull_push'] and rule.delay
]
- return delay, delay_description
+ delay_description.append((_('Days to Order'), _('+ %d day(s)', days_to_order)))
+ return delay + days_to_order, delay_description
class ProcurementGroup(models.Model):
diff --git a/addons/stock/views/stock_orderpoint_views.xml b/addons/stock/views/stock_orderpoint_views.xml
index 4c7d2a932d9..dea52f2e31f 100644
--- a/addons/stock/views/stock_orderpoint_views.xml
+++ b/addons/stock/views/stock_orderpoint_views.xml
@@ -48,6 +48,7 @@
+
@@ -68,29 +69,6 @@
-
- stock.warehouse.orderpoint.tree.editable.config
- stock.warehouse.orderpoint
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
stock.warehouse.orderpoint.reorder.searchstock.warehouse.orderpoint
@@ -182,6 +160,7 @@
+
@@ -212,7 +191,7 @@
stock.warehouse.orderpointir.actions.act_windowtree,kanban,form
-
+ {'search_default_trigger': 'auto'}