[IMP] stock, mrp, purchase_stock: RR check forecasted stock
When the scheduler runs, each orderpoint will check the forecast stock for an infinite time. As example, if we plan a receipt of 10 products in 4 months and need to deliver 5 products next month, procurement will not be launched since we have a forecasted quantity of 5. In order to improve it, the orderpoint will compute the total lead days from rules, suppliers/production delays, security days,... Those lead days will be added to today in order to know at which date the forecast stock should be checked and trigger the replenish document for today in order to be delivered at the correct date. Task: 1849912
This commit is contained in:
@@ -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 += '<tr><td>%s</td><td>+ %d %s</td></tr>' % (_('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 += '<tr><td>%s</td><td>+ %d %s</td></tr>' % (_('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
|
||||
|
||||
@@ -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 += '<tr><td>%s</td><td>+ %d %s</td></tr>' % (_('Vendor Lead Time'), supplier_delay, _('day(s)'))
|
||||
security_delay = buy_rule.picking_type_id.company_id.po_lead
|
||||
delay_description += '<tr><td>%s</td><td>+ %d %s</td></tr>' % (_('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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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(['<tr><td>%s %s</td><td>+ %d %s</td></tr>' % (_('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):
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<div t-name="stock.leadDaysPopOver">
|
||||
<p>
|
||||
If the forecasted stock on the <t t-esc="lead_days_date"/> is below the minimum quantity, replenishment will be triggered.
|
||||
</p>
|
||||
<table t-if="lead_days_description" class="table table-borderless">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
Today
|
||||
</td>
|
||||
<td>
|
||||
<t t-esc="today"/>
|
||||
</td>
|
||||
</tr>
|
||||
<t t-raw="lead_days_description"/>
|
||||
<tr class="table-info">
|
||||
<td>
|
||||
Forecasted Date
|
||||
</td>
|
||||
<td>
|
||||
= <t t-esc="lead_days_date"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</templates>
|
||||
@@ -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):
|
||||
|
||||
@@ -225,6 +225,7 @@
|
||||
<div class="o_row">
|
||||
<field name="product_min_qty"/>
|
||||
<field name="product_uom_name"/>
|
||||
<field name="json_lead_days_popover" widget="popover_widget" attrs="{'invisible': [('id', '=', False)]}"/>
|
||||
</div>
|
||||
<label for="product_max_qty"/>
|
||||
<div class="o_row">
|
||||
|
||||
Reference in New Issue
Block a user