Files
odoo_source/addons/mrp/models/stock_orderpoint.py
T
Nicolas Pierre 87d33596d1 [IMP] mrp, purchase_stock: visibility days and days to prepare MO
- Add the concept of 'Days to Prepare MO' in  mrp, similar to 'Days to
Purchase' but defined at the product level. Both concepts are merged into
Days to Order at the orderpoint level, taking the value of Days to Purchase
for orderpoints with a buy route and Days to Prepare MO for orderpoint with a
manufacturing route.
 - Add the concept of 'Visibility Days' on orderpoints. The idea is to
avoid to create through reordering rules multiple small orders for  the
same product on a small time frame but rather to order directly a bigger
quantity. When Visibility Days are defined, the quantity of the orders
created by a RR is not the quantity forecasted at Today + Lead times
(quantity that triggered the order according to the RR), but the
quantity forecasted at Today + Lead Times + Visibility days.
The Visibility Days are different for purchase and manufacturing.
Changing the route of the orderpoint will change its value. They
can be defined at the company level and further fine-tuned on the
orderpoint. In case of change of the parameters at the company level,
only orderpoints without a specific value of Visibility Days  will
be changed.
 - Alignement between Manufacturing and Purchase Security Lead Time
(previously the Manufacturing Security Lead time didn't impact the
procurement date, instead the manufacturing time was made longer).

closes odoo/odoo#86682

Task-id: 2738838
Pr: https://github.com/odoo/odoo/pull/86682
Related: odoo/upgrade#3415
Signed-off-by: William Henrotin (whe) <whe@odoo.com>
2022-05-31 19:16:55 +02:00

156 lines
7.9 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
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):
_inherit = 'stock.warehouse.orderpoint'
show_bom = fields.Boolean('Show BoM column', compute='_compute_show_bom')
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()
domain = [('orderpoint_id', 'in', self.ids)]
if self.env.context.get('written_date'):
domain = AND([domain, [('write_date', '>', self.env.context.get('written_after'))]])
production = self.env['mrp.production'].search(domain, limit=1)
if production:
action = self.env.ref('mrp.action_mrp_production_form')
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('The following replenishment order has been generated'),
'message': '%s',
'links': [{
'label': production.name,
'url': f'#action={action.id}&id={production.id}&model=mrp.production'
}],
'sticky': False,
}
}
return super()._get_replenishment_order_notification()
@api.depends('route_id')
def _compute_show_bom(self):
manufacture_route = []
for res in self.env['stock.rule'].search_read([('action', '=', 'manufacture')], ['route_id']):
manufacture_route.append(res['route_id'][0])
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 = {
orderpoint: bom_kits[orderpoint.product_id]
for orderpoint in self
if orderpoint.product_id in bom_kits
}
orderpoints_without_kit = self - self.env['stock.warehouse.orderpoint'].concat(*bom_kit_orderpoints.keys())
res = super(StockWarehouseOrderpoint, orderpoints_without_kit)._quantity_in_progress()
for orderpoint in bom_kit_orderpoints:
dummy, bom_sub_lines = bom_kit_orderpoints[orderpoint].explode(orderpoint.product_id, 1)
ratios_qty_available = []
# total = qty_available + in_progress
ratios_total = []
for bom_line, bom_line_data in bom_sub_lines:
component = bom_line.product_id
if component.type != 'product' or float_is_zero(bom_line_data['qty'], precision_rounding=bom_line.product_uom_id.rounding):
continue
uom_qty_per_kit = bom_line_data['qty'] / bom_line_data['original_qty']
qty_per_kit = bom_line.product_uom_id._compute_quantity(uom_qty_per_kit, bom_line.product_id.uom_id, raise_if_failure=False)
if not qty_per_kit:
continue
qty_by_product_location, dummy = component._get_quantity_in_progress(orderpoint.location_id.ids)
qty_in_progress = qty_by_product_location.get((component.id, orderpoint.location_id.id), 0.0)
qty_available = component.qty_available / qty_per_kit
ratios_qty_available.append(qty_available)
ratios_total.append(qty_available + (qty_in_progress / qty_per_kit))
# For a kit, the quantity in progress is :
# (the quantity if we have received all in-progress components) - (the quantity using only available components)
product_qty = min(ratios_total or [0]) - min(ratios_qty_available or [0])
res[orderpoint.id] = orderpoint.product_id.uom_id._compute_quantity(product_qty, orderpoint.product_uom, round=False)
bom_manufacture = self.env['mrp.bom']._bom_find(orderpoints_without_kit.product_id, bom_type='normal')
bom_manufacture = self.env['mrp.bom'].concat(*bom_manufacture.values())
productions_group = self.env['mrp.production'].read_group(
[('bom_id', 'in', bom_manufacture.ids), ('state', '=', 'draft'), ('orderpoint_id', 'in', orderpoints_without_kit.ids)],
['orderpoint_id', 'product_qty', 'product_uom_id'],
['orderpoint_id', 'product_uom_id'], lazy=False)
for p in productions_group:
uom = self.env['uom.uom'].browse(p['product_uom_id'][0])
orderpoint = self.env['stock.warehouse.orderpoint'].browse(p['orderpoint_id'][0])
res[orderpoint.id] += uom._compute_quantity(
p['product_qty'], orderpoint.product_uom, round=False)
return res
def _get_qty_multiple_to_order(self):
""" Calculates the minimum quantity that can be ordered according to the qty and UoM of the BoM
"""
self.ensure_one()
qty_multiple_to_order = super()._get_qty_multiple_to_order()
if 'manufacture' in self.rule_ids.mapped('action'):
bom = self.env['mrp.bom']._bom_find(self.product_id, bom_type='normal')[self.product_id]
return bom.product_uom_id._compute_quantity(bom.product_qty, self.product_uom)
return qty_multiple_to_order
def _set_default_route_id(self):
route_id = self.env['stock.rule'].search([
('action', '=', 'manufacture')
]).route_id
orderpoint_wh_bom = self.filtered(lambda o: o.product_id.bom_ids)
if route_id and orderpoint_wh_bom:
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
return values
def _post_process_scheduler(self):
""" Confirm the productions only after all the orderpoints have run their
procurement to avoid the new procurement created from the production conflict
with them. """
self.env['mrp.production'].sudo().search([
('orderpoint_id', 'in', self.ids),
('move_raw_ids', '!=', False),
('state', '=', 'draft'),
]).action_confirm()
return super()._post_process_scheduler()