Initial commit: OCA Technical packages (595 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:03 +02:00
commit 2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions

View file

@ -0,0 +1,18 @@
from . import mrp_bom
from . import mrp_production
from . import procurement_group
from . import product_adu_calculation_method
from . import purchase_order
from . import stock_buffer_profile
from . import stock_buffer_profile_lead_time
from . import stock_buffer_profile_variability
from . import stock_rule
from . import stock_warehouse
from . import stock_buffer
from . import product_template
from . import stock_move
from . import stock_move_line
from . import stock_picking
from . import res_company
from . import product_product
from . import stock_warehouse_orderpoint

View file

@ -0,0 +1,198 @@
# 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
)

View file

@ -0,0 +1,75 @@
# Copyright 2016-20 ForgeFlow S.L. (http://www.forgeflow.com)
# Copyright 2016 Aleph Objects, Inc. (https://www.alephobjects.com/)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo import api, fields, models
from .stock_buffer import _PRIORITY_LEVEL
class MrpProduction(models.Model):
_inherit = "mrp.production"
buffer_id = fields.Many2one(
comodel_name="stock.buffer",
index=True,
string="Stock Buffer",
)
execution_priority_level = fields.Selection(
string="Buffer On-Hand Alert Level",
selection=_PRIORITY_LEVEL,
readonly=True,
)
on_hand_percent = fields.Float(
string="On Hand/TOR (%)",
)
@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
records._calc_execution_priority()
return records
def _calc_execution_priority(self):
"""Technical note: this method cannot be decorated with api.depends,
otherwise it would generate a infinite recursion."""
prods = self.filtered(
lambda r: r.buffer_id and r.state not in ["done", "cancel"]
)
for rec in prods:
rec.execution_priority_level = rec.buffer_id.execution_priority_level
rec.on_hand_percent = rec.buffer_id.on_hand_percent
to_update = (self - prods).filtered(
lambda r: r.execution_priority_level or r.on_hand_percent
)
if to_update:
to_update.write({"execution_priority_level": None, "on_hand_percent": None})
def _search_execution_priority(self, operator, value):
"""Search on the execution priority by evaluating on all
open manufacturing orders."""
all_records = self.search([("state", "not in", ["done", "cancel"])])
if operator == "=":
found_ids = [
a.id for a in all_records if a.execution_priority_level == value
]
elif operator == "in" and isinstance(value, list):
found_ids = [
a.id for a in all_records if a.execution_priority_level in value
]
elif operator in ("!=", "<>"):
found_ids = [
a.id for a in all_records if a.execution_priority_level != value
]
elif operator == "not in" and isinstance(value, list):
found_ids = [
a.id for a in all_records if a.execution_priority_level not in value
]
else:
raise NotImplementedError(
"Search operator {} not implemented for value {}".format(
operator, value
)
)
return [("id", "in", found_ids)]

View file

@ -0,0 +1,50 @@
# Copyright 2016-20 ForgeFlow S.L. (http://www.forgeflow.com)
# Copyright 2016 Aleph Objects, Inc. (https://www.alephobjects.com/)
# Copyright 2018 Camptocamp SA https://www.camptocamp.com
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo import api, models
class ProcurementGroup(models.Model):
_inherit = "procurement.group"
# UOM: (stock_orderpoint_uom):
@api.model
def run(self, procurements, raise_user_error=True):
Proc = self.env["procurement.group"].Procurement
indexes_to_pop = []
new_procs = []
for i, procurement in enumerate(procurements):
if "buffer_id" in procurement.values:
buffer = procurement.values.get("buffer_id")
if (
buffer.procure_uom_id
and procurement.product_uom != buffer.procure_uom_id
):
new_product_qty = procurement.product_uom._compute_quantity(
procurement.product_qty, buffer.procure_uom_id
)
new_product_uom = buffer.procure_uom_id
new_procs.append(
Proc(
procurement.product_id,
new_product_qty,
new_product_uom,
procurement.location_id,
procurement.name,
procurement.origin,
procurement.company_id,
procurement.values,
)
)
indexes_to_pop.append(i)
if new_procs:
indexes_to_pop.reverse()
for index in indexes_to_pop:
procurements.pop(index)
procurements.extend(new_procs)
return super(ProcurementGroup, self).run(
procurements, raise_user_error=raise_user_error
)

View file

@ -0,0 +1,99 @@
# Copyright 2016-20 ForgeFlow S.L. (http://www.forgeflow.com)
# Copyright 2016 Aleph Objects, Inc. (https://www.alephobjects.com/)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class ProductAduCalculationMethod(models.Model):
_name = "product.adu.calculation.method"
_description = "Product Average Daily Usage calculation method"
@api.model
def _get_calculation_method(self):
return [
("fixed", _("Fixed ADU")),
("past", _("Past-looking")),
("future", _("Future-looking")),
("blended", _("Blended")),
]
@api.model
def _get_source_selection(self):
return [
("actual", "Use actual Stock Moves"),
("estimates", "Use Demand Estimates"),
("estimates_mrp", "Use Demand Estimates + Indirect Demand from MRP Moves"),
]
name = fields.Char(required=True)
method = fields.Selection(
selection="_get_calculation_method",
string="Calculation method",
)
source_past = fields.Selection(
selection="_get_source_selection",
string="Past Source",
help="Information source used for past calculation.",
)
horizon_past = fields.Float(
string="Past Horizon",
help="Length-of-period horizon in days looking past.",
)
factor_past = fields.Float(
string="Past Factor",
help="When using a blended method, this is the relative weight "
"assigned to the past part of the combination.",
default=0.5,
)
source_future = fields.Selection(
selection="_get_source_selection",
string="Future Source",
help="Information source used for future calculation.",
)
horizon_future = fields.Float(
string="Future Horizon",
help="Length-of-period horizon in days looking forward.",
)
factor_future = fields.Float(
string="Future Factor",
help="When using a blended method, this is the relative weight "
"assigned to the future part of the combination.",
default=0.5,
)
company_id = fields.Many2one(
comodel_name="res.company",
string="Company",
)
@api.constrains("method", "horizon_past", "horizon_future")
def _check_horizon(self):
for rec in self:
if rec.method in ["past", "blended"] and not rec.horizon_past:
raise ValidationError(_("Please indicate a Past Horizon."))
if rec.method in ["blended", "future"] and not rec.horizon_future:
raise ValidationError(_("Please indicate a Future Horizon."))
@api.constrains("method", "source_past", "source_future")
def _check_source(self):
for rec in self:
if rec.method in ["past", "blended"] and not rec.source_past:
raise ValidationError(_("Please indicate a Past Source."))
if rec.method in ["blended", "future"] and not rec.source_future:
raise ValidationError(_("Please indicate a Future Source."))
@api.constrains("method", "factor_past", "factor_future")
def _check_factor(self):
for rec in self.filtered(lambda r: r.method == "blended"):
if (
rec.factor_past + rec.factor_future != 1.0
or rec.factor_future < 0.0
or rec.factor_past < 0.0
):
raise ValidationError(
_(
"In blended method, past and future factors must be "
"positive and sum exactly 1,0."
)
)

View file

@ -0,0 +1,35 @@
# Copyright 2020 ForgeFlow S.L. (http://www.forgeflow.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo import fields, models
class Product(models.Model):
_inherit = "product.product"
buffer_ids = fields.One2many(
comodel_name="stock.buffer",
string="Stock Buffers",
inverse_name="product_id",
)
buffer_count = fields.Integer(compute="_compute_buffer_count")
def write(self, values):
res = super().write(values)
if values.get("active") is False:
buffers = (
self.env["stock.buffer"].sudo().search([("product_id", "in", self.ids)])
)
buffers.write({"active": False})
return res
def _compute_buffer_count(self):
for rec in self:
rec.buffer_count = len(rec.buffer_ids)
def action_view_stock_buffers(self):
action = self.env["ir.actions.actions"]._for_xml_id("ddmrp.action_stock_buffer")
action["context"] = {}
action["domain"] = [("id", "in", self.buffer_ids.ids)]
return action

View file

@ -0,0 +1,39 @@
# Copyright 2019-20 ForgeFlow S.L. (http://www.forgeflow.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class ProductTemplate(models.Model):
_inherit = "product.template"
buffer_count = fields.Integer(compute="_compute_buffer_count")
def _compute_buffer_count(self):
for rec in self:
rec.buffer_count = sum(
variant.buffer_count for variant in rec.product_variant_ids
)
# UOM: (stock_orderpoint_uom):
@api.constrains("uom_id")
def _check_buffer_procure_uom(self):
for rec in self:
buffer = self.env["stock.buffer"].search(
[
("procure_uom_id.category_id", "!=", rec.uom_id.category_id.id),
("product_id", "in", rec.product_variant_ids.ids),
],
limit=1,
)
if buffer:
raise ValidationError(
_(
"At least one stock buffer for this product has a "
"different Procurement unit of measure category."
)
)
def action_view_stock_buffers(self):
return self.product_variant_ids.action_view_stock_buffers()

View file

@ -0,0 +1,107 @@
# Copyright 2017-20 ForgeFlow S.L. (http://www.forgeflow.com)
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo import fields, models
from .stock_buffer import _PRIORITY_LEVEL
class PurchaseOrder(models.Model):
_inherit = "purchase.order"
ddmrp_comment = fields.Text(string="Follow-up Notes")
class PurchaseOrderLine(models.Model):
_inherit = "purchase.order.line"
buffer_ids = fields.Many2many(
comodel_name="stock.buffer",
string="Stock Buffers",
copy=False,
readonly=True,
)
execution_priority_level = fields.Selection(
string="Buffer On-Hand Status Level",
selection=_PRIORITY_LEVEL,
readonly=True,
)
on_hand_percent = fields.Float(
string="On Hand/TOR (%)",
readonly=True,
)
ddmrp_comment = fields.Text(related="order_id.ddmrp_comment", readonly=False)
def create(self, vals):
record = super().create(vals)
if record.product_id:
record._find_buffer_link()
record._calc_execution_priority()
return record
def write(self, vals):
res = super().write(vals)
for rec in self:
if rec.product_id:
rec._find_buffer_link()
return res
def _product_id_change(self):
res = super()._product_id_change()
if self.product_id:
self._find_buffer_link()
return res
def _calc_execution_priority(self):
# TODO: handle serveral buffers? worst scenario, average?
to_compute = self.filtered(
lambda r: r.buffer_ids and r.state not in ["done", "cancel"]
)
for rec in to_compute:
rec.execution_priority_level = rec.buffer_ids[0].execution_priority_level
rec.on_hand_percent = rec.buffer_ids[0].on_hand_percent
(self - to_compute).write(
{"execution_priority_level": None, "on_hand_percent": None}
)
def _get_domain_buffer_link(self):
self.ensure_one()
if not self.product_id:
# Return impossible domain -> no buffer.
return [(0, "=", 1)]
return [
("product_id", "=", self.product_id.id),
("company_id", "=", self.order_id.company_id.id),
("buffer_profile_id.item_type", "=", "purchased"),
("warehouse_id", "=", self.order_id.picking_type_id.warehouse_id.id),
]
def _find_buffer_link(self):
buffer_model = self.env["stock.buffer"]
move_model = self.env["stock.move"]
for rec in self.filtered(lambda r: not r.buffer_ids):
mto_move = move_model.search(
[("created_purchase_line_id", "=", rec.id)], limit=1
)
if mto_move:
# MTO lines are not accounted in MTS stock buffers.
continue
domain = rec._get_domain_buffer_link()
buffer = buffer_model.search(domain, limit=1)
if buffer:
rec.buffer_ids = buffer
rec._calc_execution_priority()
def _prepare_purchase_order_line_from_procurement(
self, product_id, product_qty, product_uom, company_id, values, po
):
vals = super()._prepare_purchase_order_line_from_procurement(
product_id, product_qty, product_uom, company_id, values, po
)
# If the procurement was run directly by a reordering rule.
if "buffer_id" in values:
vals["buffer_ids"] = [(4, values["buffer_id"].id)]
# If the procurement was run by a stock move.
elif "buffer_ids" in values:
vals["buffer_ids"] = [(4, o.id) for o in values["buffer_ids"]]
return vals

View file

@ -0,0 +1,21 @@
# Copyright 2020 ForgeFlow S.L. (http://www.forgeflow.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo import fields, models
class ResCompany(models.Model):
_inherit = "res.company"
ddmrp_auto_update_nfp = fields.Boolean(
string="Update NFP on Stock Buffers on relevant events.",
help="Transfer status changes can trigger the update of relevant "
"buffer's NFP.",
)
ddmrp_adu_calc_include_scrap = fields.Boolean(
string="Include scrap locations in ADU calculation",
)
ddmrp_qty_multiple_tolerance = fields.Float(
string="Qty Multiple Tolerance",
)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,77 @@
# Copyright 2016-20 ForgeFlow S.L. (http://www.forgeflow.com)
# Copyright 2016 Aleph Objects, Inc. (https://www.alephobjects.com/)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo import api, fields, models
_REPLENISH_METHODS = [
("replenish", "Replenished"),
("replenish_override", "Replenished Override"),
("min_max", "Min-max"),
]
_ITEM_TYPES = [
("manufactured", "Manufactured"),
("purchased", "Purchased"),
("distributed", "Distributed"),
]
class StockBufferProfile(models.Model):
_name = "stock.buffer.profile"
_description = "Stock Buffer Profile"
@api.depends(
"item_type",
"lead_time_id",
"lead_time_id.name",
"lead_time_id.factor",
"variability_id",
"variability_id.name",
"variability_id.factor",
"distributed_reschedule_max_proc_time",
)
def _compute_name(self):
"""Get the right summary for this job."""
for rec in self:
name = "{} {}, {}({}), {}({})".format(
rec.replenish_method,
rec.item_type,
rec.lead_time_id.name,
rec.lead_time_id.factor,
rec.variability_id.name,
rec.variability_id.factor,
)
if rec.distributed_reschedule_max_proc_time > 0.0:
name += ", {}min".format(rec.distributed_reschedule_max_proc_time)
rec.name = name
name = fields.Char(compute="_compute_name", store=True)
replenish_method = fields.Selection(
string="Replenishment method", selection=_REPLENISH_METHODS, required=True
)
item_type = fields.Selection(selection=_ITEM_TYPES, required=True)
lead_time_id = fields.Many2one(
comodel_name="stock.buffer.profile.lead.time", string="Lead Time Factor"
)
variability_id = fields.Many2one(
comodel_name="stock.buffer.profile.variability", string="Variability Factor"
)
company_id = fields.Many2one(
"res.company",
"Company",
)
replenish_distributed_limit_to_free_qty = fields.Boolean(
string="Limit replenishment to free quantity",
default=False,
help="When activated, the recommended quantity will be maxed at "
"the quantity available in the replenishment source location.",
)
distributed_reschedule_max_proc_time = fields.Float(
string="Re-Schedule Procurement Max Proc. Time (minutes)",
default=0.0,
help="When you request procurement from a buffer, their scheduled"
" date is rescheduled to now + this procurement time (in minutes)."
" Their scheduled date represents the latest the transfers should"
" be done, and therefore, past this timestamp, considered late.",
)

View file

@ -0,0 +1,17 @@
# Copyright 2016-20 ForgeFlow S.L. (http://www.forgeflow.com)
# Copyright 2016 Aleph Objects, Inc. (https://www.alephobjects.com/)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo import fields, models
class StockBufferProfileLeadTime(models.Model):
_name = "stock.buffer.profile.lead.time"
_description = "Stock Buffer Profile Lead Time Factor"
name = fields.Char(required=True)
factor = fields.Float(string="Lead Time Factor", required=True)
company_id = fields.Many2one(
"res.company",
"Company",
)

View file

@ -0,0 +1,17 @@
# Copyright 2016-20 ForgeFlow S.L. (http://www.forgeflow.com)
# Copyright 2016 Aleph Objects, Inc. (https://www.alephobjects.com/)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo import fields, models
class StockBufferProfileVariability(models.Model):
_name = "stock.buffer.profile.variability"
_description = "Stock Buffer Profile Variability Factor"
name = fields.Char(required=True)
factor = fields.Float(string="Variability Factor", required=True)
company_id = fields.Many2one(
"res.company",
"Company",
)

View file

@ -0,0 +1,152 @@
# Copyright 2019-20 ForgeFlow S.L. (http://www.forgeflow.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo import _, api, fields, models
class StockMove(models.Model):
_inherit = "stock.move"
buffer_ids = fields.Many2many(
comodel_name="stock.buffer",
string="Linked Stock Buffers",
)
# Add an index as '_find_buffer_link' method is using it as search criteria
created_purchase_line_id = fields.Many2one(index=True)
def _prepare_procurement_values(self):
res = super(StockMove, self)._prepare_procurement_values()
if self.buffer_ids:
res["buffer_ids"] = self.buffer_ids
return res
def _merge_moves_fields(self):
res = super(StockMove, self)._merge_moves_fields()
res["buffer_ids"] = [(4, m.id) for m in self.mapped("buffer_ids")]
return res
def write(self, vals):
res = super(StockMove, self).write(vals)
if self and self.env.company.ddmrp_auto_update_nfp:
# Stock moves changes can be triggered by users without
# access to write stock buffers, thus we do it with sudo.
if "state" in vals:
self.sudo()._update_ddmrp_nfp()
elif "location_id" in vals or "location_dest_id" in vals:
self.sudo().filtered(
lambda m: m.state
in ("confirmed", "partially_available", "assigned")
)._update_ddmrp_nfp()
return res
@api.model_create_multi
def create(self, vals_list):
moves = super(StockMove, self).create(vals_list)
# TODO should we use @api.model_create_single instead?
moves_to_update_ids = []
for vals, move in zip(vals_list, moves):
if (
"state" in vals
and move.state not in ("draft", "cancel")
and self.env.company.ddmrp_auto_update_nfp
):
moves_to_update_ids.append(move.id)
# Stock moves state changes can be triggered by users without
# access to write stock buffers, thus we do it with sudo.
if moves_to_update_ids:
self.browse(moves_to_update_ids).sudo()._update_ddmrp_nfp()
return moves
def _find_buffers_to_update_nfp(self):
# Find buffers that can be affected. `out_buffers` will see the move as
# outgoing and `in_buffers` as incoming.
out_buffers = in_buffers = self.env["stock.buffer"]
for move in self:
out_buffers |= move.mapped("product_id.buffer_ids").filtered(
lambda buffer: (
move.location_id.is_sublocation_of(buffer.location_id)
and not move.location_dest_id.is_sublocation_of(buffer.location_id)
)
)
in_buffers |= move.mapped("product_id.buffer_ids").filtered(
lambda buffer: (
not move.location_id.is_sublocation_of(buffer.location_id)
and move.location_dest_id.is_sublocation_of(buffer.location_id)
)
)
return out_buffers, in_buffers
def _update_ddmrp_nfp(self):
if self.env.context.get("no_ddmrp_auto_update_nfp"):
return True
out_buffers, in_buffers = self._find_buffers_to_update_nfp()
for buffer in out_buffers.with_context(no_ddmrp_history=True):
buffer.cron_actions(only_nfp="out")
for buffer in in_buffers.with_context(no_ddmrp_history=True):
buffer.cron_actions(only_nfp="in")
def _get_all_linked_moves(self):
"""Retrieve all linked moves both origin and destination recursively."""
def get_moves(move_set, attr):
new_moves = move_set.mapped(attr)
while new_moves:
move_set |= new_moves
new_moves = new_moves.mapped(attr)
return move_set
all_moves = (
self | get_moves(self, "move_orig_ids") | get_moves(self, "move_dest_ids")
)
return all_moves
def _get_source_field_candidates(self):
"""Extend for more source field candidates."""
return [
"sale_line_id.order_id",
"purchase_line_id.order_id",
"production_id",
"raw_material_production_id",
"unbuild_id",
"repair_id",
"rma_line_id",
"picking_id",
]
def _has_nested_field(self, field):
"""Check if an object has a nested chain of fields."""
current_object = self
try:
for field in field.split("."):
current_object = getattr(current_object, field)
return True
except AttributeError:
return False
def _get_source_record(self):
"""Find the first source record in the field candidates linked with the moves,
prioritizing the order of field candidates."""
moves = self._get_all_linked_moves()
field_candidates = self._get_source_field_candidates()
# Iterate over the prioritized list of candidate fields
for field in field_candidates:
if self._has_nested_field(field):
for move in moves:
record = move.mapped(field)
if record:
return record
return False
def action_open_stock_move_source(self):
"""Open the source record of the stock move, if it exists."""
self.ensure_one()
record = self._get_source_record()
if record:
return {
"name": getattr(record, "name", _("Stock Move Source")),
"view_mode": "form",
"res_model": record._name,
"type": "ir.actions.act_window",
"res_id": record.id,
}
return False

View file

@ -0,0 +1,12 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
from odoo import fields, models
class StockMoveLine(models.Model):
_inherit = "stock.move.line"
# Override to make '_calc_product_available_qty' method of
# 'stock.buffer' more efficient.
state = fields.Selection(index=True)

View file

@ -0,0 +1,20 @@
# Copyright 2020 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
from odoo import models
class StockPicking(models.Model):
_inherit = "stock.picking"
def action_stock_buffer_open(self):
"""Open a stock.buffer list related to products of the stock.picking."""
self.ensure_one()
domain = [
("product_id", "in", self.mapped("move_ids.product_id.id")),
("company_id", "=", self.company_id.id),
]
action = self.env["ir.actions.actions"]._for_xml_id("ddmrp.action_stock_buffer")
action["domain"] = domain
action["context"] = {"search_default_procure_recommended": 1}
return action

View file

@ -0,0 +1,92 @@
# Copyright 2018 Camptocamp (https://www.camptocamp.com)
# Copyright 2019-20 ForgeFlow S.L. (http://www.forgeflow.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo import models
class StockRule(models.Model):
_inherit = "stock.rule"
def _prepare_mo_vals(
self,
product_id,
product_qty,
product_uom,
location_id,
name,
origin,
company_id,
values,
bom,
):
result = super(StockRule, self)._prepare_mo_vals(
product_id,
product_qty,
product_uom,
location_id,
name,
origin,
company_id,
values,
bom,
)
# TODO: stock_orderpoint_mrp_link: tests!
if "buffer_id" in values:
result["buffer_id"] = values["buffer_id"].id
elif "buffer_ids" in values:
# We take the always first value as in case of chain procurements,
# the procurements are resolved first and then the moves are
# merged. Thus here we are going to have only one buffer in
# in buffer_ids.
result["buffer_id"] = values["buffer_ids"][0].id
return result
def _run_manufacture(self, procurements):
super()._run_manufacture(procurements)
for procurement, _rule in procurements:
buffer = procurement.values.get("buffer_id")
if buffer:
buffer.sudo().cron_actions()
return True
# TODO: stock_orderpoint_move_link: tests!
def _get_stock_move_values(
self,
product_id,
product_qty,
product_uom,
location_id,
name,
origin,
company_id,
values,
):
vals = super()._get_stock_move_values(
product_id,
product_qty,
product_uom,
location_id,
name,
origin,
company_id,
values,
)
if "buffer_id" in values:
vals["buffer_ids"] = [(4, values["buffer_id"].id)]
elif "buffer_ids" in values:
vals["buffer_ids"] = [(4, o.id) for o in values["buffer_ids"]]
return vals
def _update_purchase_order_line(
self, product_id, product_qty, product_uom, company_id, values, line
):
vals = super()._update_purchase_order_line(
product_id, product_qty, product_uom, company_id, values, line
)
if "buffer_id" in values:
vals["buffer_ids"] = [(4, values["buffer_id"].id)]
# If the procurement was run by a stock move.
elif "buffer_ids" in values:
vals["buffer_ids"] = [(4, o.id) for o in values["buffer_ids"]]
return vals

View file

@ -0,0 +1,15 @@
# Copyright 2018-20 ForgeFlow S.L. (http://www.forgeflow.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo import fields, models
class StockWarehouse(models.Model):
_inherit = "stock.warehouse"
nfp_incoming_safety_factor = fields.Float(
"Net Flow Position Incoming Safety Factor",
help="Factor used to compute the number of days to look into the "
"future for incoming shipments for the purposes of the Net "
"Flow position calculation.",
)

View file

@ -0,0 +1,30 @@
# Copyright 2024 ForgeFlow S.L. (http://www.forgeflow.com)
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo import api, models
class StockWarehouseOrderpoint(models.Model):
_inherit = "stock.warehouse.orderpoint"
def _get_orderpoint_action(self):
return super(
StockWarehouseOrderpoint, self.with_context(_from_get_op_action=True)
)._get_orderpoint_action()
@api.model_create_multi
def create(self, vals_list):
if self.env.context.get("_from_get_op_action"):
new_vals_list = []
for vals in vals_list:
buffer = self.env["stock.buffer"].search(
[
("product_id", "=", vals.get("product_id")),
("location_id", "=", vals.get("location_id")),
],
limit=1,
)
if not buffer:
new_vals_list.append(vals)
vals_list = new_vals_list
return super().create(vals_list)