[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:
Arnold Moyaux
2020-01-09 16:09:12 +00:00
parent bdc2280309
commit 56efb55057
9 changed files with 158 additions and 4 deletions
+19
View File
@@ -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
+1
View File
@@ -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,
+12
View File
@@ -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
+22 -1
View File
@@ -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):
+14 -1
View File
@@ -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>
+42 -2
View File
@@ -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">