Files
manufacture/mrp_multi_level/models/product_mrp_area.py
BernatPForgeFlow f5352325a2 [IMP] mrp_multi_level: Supply method computation for MRP Parameters
For each MRP Parameter, we will calculate its supply method based on the procurement rules path. We will go back in the rules until we find that the action is "buy" or "manufacture", or until the action is "Pull From" or "Pull & Push" and the supply method is "Take from Stock".

This means we will show what the latest rule is if we were to do a procurement. Although it would be possible that the final action does not end up being executed if stock already exists in one of the intermediate locations.
2023-02-08 16:53:50 +01:00

294 lines
11 KiB
Python

# Copyright 2016 Ucamco - Wim Audenaert <wim.audenaert@ucamco.com>
# Copyright 2016-19 ForgeFlow S.L. (https://www.forgeflow.com)
# - Jordi Ballester Alomar <jordi.ballester@forgeflow.com>
# - Lois Rilo Antelo <lois.rilo@forgeflow.com>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from math import ceil
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.osv import expression
class ProductMRPArea(models.Model):
_name = "product.mrp.area"
_description = "Product MRP Area"
active = fields.Boolean(default=True)
mrp_area_id = fields.Many2one(comodel_name="mrp.area", required=True)
company_id = fields.Many2one(
comodel_name="res.company",
related="mrp_area_id.warehouse_id.company_id",
store=True,
)
product_id = fields.Many2one(
comodel_name="product.product", required=True, string="Product"
)
product_tmpl_id = fields.Many2one(
comodel_name="product.template",
readonly=True,
related="product_id.product_tmpl_id",
store=True,
)
location_id = fields.Many2one(related="mrp_area_id.location_id")
location_proc_id = fields.Many2one(
string="Procure Location",
comodel_name="stock.location",
domain="[('location_id', 'child_of', location_id)]",
help="Set this if you need to procure from a different location"
"than area's location.",
)
# TODO: applicable and exclude... redundant??
mrp_applicable = fields.Boolean(string="MRP Applicable")
mrp_exclude = fields.Boolean(string="Exclude from MRP")
mrp_inspection_delay = fields.Integer(string="Inspection Delay")
mrp_maximum_order_qty = fields.Float(string="Maximum Order Qty", default=0.0)
mrp_minimum_order_qty = fields.Float(string="Minimum Order Qty", default=0.0)
mrp_minimum_stock = fields.Float(string="Safety Stock")
mrp_nbr_days = fields.Integer(
string="Nbr. Days",
default=0,
help="Number of days to group demand for this product during the "
"MRP run, in order to determine the quantity to order.",
)
mrp_qty_multiple = fields.Float(string="Qty Multiple", default=1.00)
mrp_transit_delay = fields.Integer(string="Transit Delay", default=0)
mrp_verified = fields.Boolean(
string="Verified for MRP",
help="Identifies that this product has been verified "
"to be valid for the MRP.",
)
mrp_lead_time = fields.Float(string="Lead Time", compute="_compute_mrp_lead_time")
main_supplier_id = fields.Many2one(
comodel_name="res.partner",
string="Main Supplier",
compute="_compute_main_supplier",
store=True,
index=True,
)
main_supplierinfo_id = fields.Many2one(
comodel_name="product.supplierinfo",
string="Supplier Info",
compute="_compute_main_supplier",
store=True,
)
supply_method = fields.Selection(
selection=[
("buy", "Buy"),
("none", "Undefined"),
("manufacture", "Produce"),
("phantom", "Kit"),
("pull", "Pull From"),
("push", "Push To"),
("pull_push", "Pull & Push"),
],
string="Supply Method",
compute="_compute_supply_method",
)
qty_available = fields.Float(
string="Quantity Available", compute="_compute_qty_available"
)
mrp_move_ids = fields.One2many(
comodel_name="mrp.move", inverse_name="product_mrp_area_id", readonly=True
)
planned_order_ids = fields.One2many(
comodel_name="mrp.planned.order",
inverse_name="product_mrp_area_id",
readonly=True,
)
mrp_planner_id = fields.Many2one("res.users")
_sql_constraints = [
(
"product_mrp_area_uniq",
"unique(product_id, mrp_area_id)",
"The product/MRP Area parameters combination must be unique.",
)
]
@api.constrains(
"mrp_minimum_order_qty",
"mrp_maximum_order_qty",
"mrp_qty_multiple",
"mrp_minimum_stock",
"mrp_nbr_days",
)
def _check_negatives(self):
values = self.read(
[
"mrp_minimum_order_qty",
"mrp_maximum_order_qty",
"mrp_qty_multiple",
"mrp_minimum_stock",
"mrp_nbr_days",
]
)
for rec in values:
if any(v < 0 for v in rec.values()):
raise ValidationError(_("You cannot use a negative number."))
def name_get(self):
return [
(
area.id,
"[{}] {}".format(area.mrp_area_id.name, area.product_id.display_name),
)
for area in self
]
@api.model
def _name_search(
self, name, args=None, operator="ilike", limit=100, name_get_uid=None
):
if operator in ("ilike", "like", "=", "=like", "=ilike"):
args = expression.AND(
[
args or [],
[
"|",
"|",
("product_id.name", operator, name),
("product_id.default_code", operator, name),
("mrp_area_id.name", operator, name),
],
]
)
return super(ProductMRPArea, self)._name_search(
name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid
)
def _compute_mrp_lead_time(self):
produced = self.filtered(lambda r: r.supply_method == "manufacture")
purchased = self.filtered(lambda r: r.supply_method == "buy")
for rec in produced:
rec.mrp_lead_time = rec.product_id.produce_delay
for rec in purchased:
rec.mrp_lead_time = rec.main_supplierinfo_id.delay
# TODO: 'move' supply method.
for rec in self - produced - purchased:
rec.mrp_lead_time = 0
def _compute_qty_available(self):
for rec in self:
rec.qty_available = rec.product_id.with_context(
{"location": rec.mrp_area_id.location_id.id}
).qty_available
def _compute_supply_method(self):
group_obj = self.env["procurement.group"]
for rec in self:
proc_loc = rec.location_proc_id or rec.mrp_area_id.location_id
values = {
"warehouse_id": rec.mrp_area_id.warehouse_id,
"company_id": rec.mrp_area_id.company_id,
}
rule = group_obj._get_rule(rec.product_id, proc_loc, values)
if not rule:
rec.supply_method = "none"
continue
# Keep getting the rule for the product and the source location until the
# action is "buy" or "manufacture". Or until the action is "Pull From" or
# "Pull & Push" and the supply method is "Take from Stock".
while rule.action not in ("buy", "manufacture") and rule.procure_method in (
"make_to_order",
"mts_else_mto",
):
new_rule = group_obj._get_rule(
rec.product_id, rule.location_src_id, values
)
if not new_rule:
break
rule = new_rule
# Determine the supply method based on the final rule.
rec.supply_method = (
"phantom"
if rule.action == "manufacture"
and rec.product_id.product_tmpl_id.bom_ids
and rec.product_id.product_tmpl_id.bom_ids[0].type == "phantom"
else rule.action
)
@api.depends(
"mrp_area_id", "supply_method", "product_id.route_ids", "product_id.seller_ids"
)
def _compute_main_supplier(self):
"""Simplified and similar to procurement.rule logic."""
for rec in self.filtered(lambda r: r.supply_method == "buy"):
suppliers = rec.product_id.seller_ids.filtered(
lambda r: (not r.product_id or r.product_id == rec.product_id)
and (not r.company_id or r.company_id == rec.company_id)
)
if not suppliers:
rec.main_supplierinfo_id = False
rec.main_supplier_id = False
continue
rec.main_supplierinfo_id = suppliers[0]
rec.main_supplier_id = suppliers[0].name
for rec in self.filtered(lambda r: r.supply_method != "buy"):
rec.main_supplierinfo_id = False
rec.main_supplier_id = False
def _adjust_qty_to_order(self, qty_to_order):
self.ensure_one()
if (
not self.mrp_maximum_order_qty
and not self.mrp_minimum_order_qty
and self.mrp_qty_multiple == 1.0
):
return qty_to_order
if qty_to_order < self.mrp_minimum_order_qty:
return self.mrp_minimum_order_qty
if self.mrp_qty_multiple:
multiplier = ceil(qty_to_order / self.mrp_qty_multiple)
qty_to_order = multiplier * self.mrp_qty_multiple
if self.mrp_maximum_order_qty and qty_to_order > self.mrp_maximum_order_qty:
return self.mrp_maximum_order_qty
return qty_to_order
def update_min_qty_from_main_supplier(self):
for rec in self.filtered(
lambda r: r.main_supplierinfo_id and r.supply_method == "buy"
):
rec.mrp_minimum_order_qty = rec.main_supplierinfo_id.min_qty
def _in_stock_moves_domain(self):
self.ensure_one()
locations = self.mrp_area_id._get_locations()
return [
("product_id", "=", self.product_id.id),
("state", "not in", ["done", "cancel"]),
("product_qty", ">", 0.00),
("location_id", "not in", locations.ids),
("location_dest_id", "in", locations.ids),
]
def _out_stock_moves_domain(self):
self.ensure_one()
locations = self.mrp_area_id._get_locations()
return [
("product_id", "=", self.product_id.id),
("state", "not in", ["done", "cancel"]),
("product_qty", ">", 0.00),
("location_id", "in", locations.ids),
("location_dest_id", "not in", locations.ids),
]
def action_view_stock_moves(self, domain):
self.ensure_one()
xmlid = "stock.stock_move_action"
action = self.env["ir.actions.act_window"]._for_xml_id(xmlid)
action["domain"] = domain
action["context"] = {}
return action
def action_view_incoming_stock_moves(self):
return self.action_view_stock_moves(self._in_stock_moves_domain())
def action_view_outgoing_stock_moves(self):
return self.action_view_stock_moves(self._out_stock_moves_domain())
def _to_be_exploded(self):
self.ensure_one()
return self.supply_method in ["manufacture", "phantom"]