mirror of
https://github.com/OCA/manufacture.git
synced 2025-01-28 16:37:15 +02:00
When you procure a kit, it doesn't matter what the route configuration is on the product itself but on its children, so it is not needed to check anything else but the BoM to assign the 'phantom' supply method. Also do not assign a BoM when the supply method is not phantom or manufacture.
321 lines
12 KiB
Python
321 lines
12 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")
|
|
distribution_lead_time = fields.Float()
|
|
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"),
|
|
],
|
|
compute="_compute_supply_method",
|
|
)
|
|
supply_bom_id = fields.Many2one(
|
|
comodel_name="mrp.bom",
|
|
string="Supply BoM",
|
|
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")
|
|
distributed = self.filtered(
|
|
lambda r: r.supply_method in ("pull", "push", "pull_push")
|
|
)
|
|
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
|
|
for rec in distributed:
|
|
rec.mrp_lead_time = rec.distribution_lead_time
|
|
for rec in self - produced - purchased - distributed:
|
|
rec.mrp_lead_time = 0
|
|
|
|
def _compute_qty_available(self):
|
|
for rec in self:
|
|
rec.qty_available = rec.product_id.with_context(
|
|
location=rec._get_locations().ids
|
|
).qty_available
|
|
|
|
def _get_rule(self):
|
|
self.ensure_one()
|
|
group_obj = self.env["procurement.group"]
|
|
proc_loc = self.location_proc_id or self.location_id
|
|
values = {
|
|
"warehouse_id": self.mrp_area_id.warehouse_id,
|
|
"company_id": self.company_id,
|
|
}
|
|
rule = group_obj._get_rule(self.product_id, proc_loc, values)
|
|
if not rule:
|
|
return False
|
|
# 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(
|
|
self.product_id, rule.location_src_id, values
|
|
)
|
|
if not new_rule:
|
|
break
|
|
rule = new_rule
|
|
return rule
|
|
|
|
def _compute_supply_method(self):
|
|
boms_by_product = self.env["mrp.bom"]._bom_find(self.mapped("product_id"))
|
|
for rec in self:
|
|
rule = rec._get_rule()
|
|
bom = boms_by_product.get(rec.product_id, self.env["mrp.bom"])
|
|
if bom.type == "phantom":
|
|
rec.supply_method = "phantom"
|
|
rec.supply_bom_id = bom
|
|
elif not rule:
|
|
rec.supply_method = "none"
|
|
rec.supply_bom_id = False
|
|
elif rule.action == "manufacture":
|
|
rec.supply_method = rule.action
|
|
rec.supply_bom_id = bom
|
|
else:
|
|
rec.supply_method = rule.action
|
|
rec.supply_bom_id = False
|
|
|
|
@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)
|
|
).sorted(lambda s: (s.sequence, -s.min_qty, s.price, s.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].partner_id
|
|
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._get_locations()
|
|
return [
|
|
("product_id", "=", self.product_id.id),
|
|
("state", "not in", ["done", "cancel"]),
|
|
("product_qty", ">", 0.00),
|
|
"!",
|
|
("location_id", "child_of", locations.ids),
|
|
("location_dest_id", "child_of", locations.ids),
|
|
]
|
|
|
|
def _out_stock_moves_domain(self):
|
|
self.ensure_one()
|
|
locations = self._get_locations()
|
|
return [
|
|
("product_id", "=", self.product_id.id),
|
|
("state", "not in", ["done", "cancel"]),
|
|
("product_qty", ">", 0.00),
|
|
("location_id", "child_of", locations.ids),
|
|
"!",
|
|
("location_dest_id", "child_of", locations.ids),
|
|
]
|
|
|
|
def action_view_stock_moves(self, domain):
|
|
self.ensure_one()
|
|
action = self.env.ref("stock.stock_move_action").read()[0]
|
|
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"]
|
|
|
|
def _get_locations(self):
|
|
self.ensure_one()
|
|
return self.mrp_area_id._get_locations()
|
|
|
|
def _should_create_planned_order(self):
|
|
self.ensure_one()
|
|
return True
|