oca-technical/odoo-bringout-oca-ddmrp-ddmrp/ddmrp/models/mrp_bom.py
2025-08-29 15:43:03 +02:00

198 lines
7.1 KiB
Python

# Copyright 2017-24 ForgeFlow S.L. (http://www.forgeflow.com)
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import logging
from odoo import _, api, fields, models
_logger = logging.getLogger(__name__)
class MrpBom(models.Model):
_inherit = "mrp.bom"
is_buffered = fields.Boolean(
string="Buffered?",
compute="_compute_is_buffered",
help="True when the product has an DDMRP buffer associated.",
)
buffer_id = fields.Many2one(
comodel_name="stock.buffer",
string="Stock Buffer",
compute="_compute_buffer",
)
dlt = fields.Float(
string="Decoupled Lead Time (days)",
compute="_compute_dlt",
)
context_location_id = fields.Many2one(
comodel_name="stock.location",
string="Stock Location",
compute="_compute_context_location",
)
# This is a legacy field that can be removed in v17
location_id = fields.Many2one(related="context_location_id")
def _get_search_buffer_domain(self):
product = self.product_id
if not product and self.product_tmpl_id.product_variant_ids:
product = self.product_tmpl_id.product_variant_ids[0]
domain = [
("product_id", "=", product.id),
("location_id", "=", self.context_location_id.id),
]
if self.company_id:
domain.append(("company_id", "=", self.company_id.id))
return domain
@api.depends("product_id", "product_tmpl_id", "context_location_id")
def _compute_buffer(self):
for record in self:
domain = record._get_search_buffer_domain()
# NOTE: It can be possible to find multiple buffers.
# For example if the BoM has no location set, and there
# are buffers with the same product_id and buffer_profile_id
# You do not know which one the search function finds.
buffer = self.env["stock.buffer"].search(domain, limit=1)
record.buffer_id = buffer
@api.depends("buffer_id")
def _compute_is_buffered(self):
for bom in self:
bom.is_buffered = True if bom.buffer_id else False
@api.depends_context("location_id")
def _compute_context_location(self):
warehouse_model = self.env["stock.warehouse"]
for rec in self:
if self.env.context.get("location_id", None):
rec.context_location_id = self.env.context.get("location_id")
elif self.env.context.get("warehouse", None):
warehouse_id = self.env.context.get("warehouse")
rec.context_location_id = warehouse_model.browse(
warehouse_id
).lot_stock_id.id
else:
company_id = rec.company_id or self.env.company
warehouse_id = warehouse_model.search(
[("company_id", "=", company_id.id)], limit=1
)
rec.context_location_id = warehouse_id.lot_stock_id.id
def _get_produce_delay(self):
self.ensure_one()
return self.product_id.produce_delay or self.product_tmpl_id.produce_delay
def _get_longest_path(self):
if not self.bom_line_ids:
return 0.0
paths = [0] * len(self.bom_line_ids)
i = 0
for line in self.bom_line_ids:
if line.is_buffered:
i += 1
elif line.product_id.bom_ids:
# If the a component is manufactured we continue exploding.
location = line.context_location_id
line_boms = line.product_id.bom_ids
bom = line_boms.filtered(
lambda bom: bom.context_location_id == location
) or line_boms.filtered(lambda bom: not bom.context_location_id)
if bom:
paths[i] += bom[0]._get_produce_delay()
paths[i] += bom[0]._get_longest_path()
else:
_logger.info(
"ddmrp (dlt): Product %s has no BOM for location "
"%s." % (line.product_id.name, location.name)
)
i += 1
else:
# assuming they are purchased,
if line.product_id.seller_ids:
paths[i] = line.product_id.seller_ids[0].delay
else:
_logger.info(
"ddmrp (dlt): Product %s has no seller set."
% line.product_id.name
)
i += 1
return max(paths)
def _get_manufactured_dlt(self):
"""Computes the Decoupled Lead Time exploding all the branches of the
BOM until a buffered position and then selecting the greatest."""
self.ensure_one()
dlt = self._get_produce_delay()
dlt += self._get_longest_path()
return dlt
@api.depends("context_location_id")
@api.depends_context("location_id")
def _compute_dlt(self):
for rec in self:
rec.dlt = rec._get_manufactured_dlt()
def action_change_context_location(self):
return {
"type": "ir.actions.act_window",
"name": _("Change MRP BoM Location"),
"res_model": "mrp.bom.change.location",
"view_mode": "form",
"target": "new",
}
class MrpBomLine(models.Model):
_inherit = "mrp.bom.line"
is_buffered = fields.Boolean(
string="Buffered?",
compute="_compute_is_buffered",
help="True when the product has an DDMRP buffer associated.",
)
buffer_id = fields.Many2one(
comodel_name="stock.buffer",
string="Stock Buffer",
compute="_compute_is_buffered",
)
dlt = fields.Float(
string="Decoupled Lead Time (days)",
compute="_compute_dlt",
)
context_location_id = fields.Many2one(related="bom_id.context_location_id")
# This is a legacy field that can be removed in v17
location_id = fields.Many2one(related="context_location_id")
def _get_search_buffer_domain(self):
product = self.product_id
if not product and self.product_tmpl_id.product_variant_ids:
product = self.product_tmpl_id.product_variant_ids[0]
domain = [
("product_id", "=", product.id),
("location_id", "=", self.context_location_id.id),
]
if self.company_id:
domain.append(("company_id", "=", self.company_id.id))
return domain
@api.depends("context_location_id")
@api.depends_context("location_id")
def _compute_is_buffered(self):
for line in self:
domain = line._get_search_buffer_domain()
buffer = self.env["stock.buffer"].search(domain, limit=1)
line.buffer_id = buffer
line.is_buffered = True if buffer else False
@api.depends("product_id")
def _compute_dlt(self):
for rec in self:
if rec.product_id.bom_ids:
rec.dlt = rec.product_id.bom_ids[0].dlt
else:
rec.dlt = (
rec.product_id.seller_ids
and rec.product_id.seller_ids[0].delay
or 0.0
)