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 @@
+