mirror of
https://github.com/bringout/oca-technical.git
synced 2026-04-19 22:32:04 +02:00
Initial commit: OCA Technical packages (595 packages)
This commit is contained in:
commit
2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions
18
odoo-bringout-oca-ddmrp-ddmrp/ddmrp/models/__init__.py
Normal file
18
odoo-bringout-oca-ddmrp-ddmrp/ddmrp/models/__init__.py
Normal 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
|
||||
198
odoo-bringout-oca-ddmrp-ddmrp/ddmrp/models/mrp_bom.py
Normal file
198
odoo-bringout-oca-ddmrp-ddmrp/ddmrp/models/mrp_bom.py
Normal 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
|
||||
)
|
||||
75
odoo-bringout-oca-ddmrp-ddmrp/ddmrp/models/mrp_production.py
Normal file
75
odoo-bringout-oca-ddmrp-ddmrp/ddmrp/models/mrp_production.py
Normal 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)]
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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."
|
||||
)
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
107
odoo-bringout-oca-ddmrp-ddmrp/ddmrp/models/purchase_order.py
Normal file
107
odoo-bringout-oca-ddmrp-ddmrp/ddmrp/models/purchase_order.py
Normal 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
|
||||
21
odoo-bringout-oca-ddmrp-ddmrp/ddmrp/models/res_company.py
Normal file
21
odoo-bringout-oca-ddmrp-ddmrp/ddmrp/models/res_company.py
Normal 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",
|
||||
)
|
||||
2127
odoo-bringout-oca-ddmrp-ddmrp/ddmrp/models/stock_buffer.py
Normal file
2127
odoo-bringout-oca-ddmrp-ddmrp/ddmrp/models/stock_buffer.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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.",
|
||||
)
|
||||
|
|
@ -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",
|
||||
)
|
||||
|
|
@ -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",
|
||||
)
|
||||
152
odoo-bringout-oca-ddmrp-ddmrp/ddmrp/models/stock_move.py
Normal file
152
odoo-bringout-oca-ddmrp-ddmrp/ddmrp/models/stock_move.py
Normal 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
|
||||
|
|
@ -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)
|
||||
20
odoo-bringout-oca-ddmrp-ddmrp/ddmrp/models/stock_picking.py
Normal file
20
odoo-bringout-oca-ddmrp-ddmrp/ddmrp/models/stock_picking.py
Normal 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
|
||||
92
odoo-bringout-oca-ddmrp-ddmrp/ddmrp/models/stock_rule.py
Normal file
92
odoo-bringout-oca-ddmrp-ddmrp/ddmrp/models/stock_rule.py
Normal 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
|
||||
|
|
@ -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.",
|
||||
)
|
||||
|
|
@ -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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue