Initial commit: Odoo 18.0-20251222 extra-addons
This commit is contained in:
5
sale_mrp_bom/models/__init__.py
Executable file
5
sale_mrp_bom/models/__init__.py
Executable file
@@ -0,0 +1,5 @@
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import procurement_group
|
||||
from . import sale_order_line
|
||||
from . import stock_move
|
||||
95
sale_mrp_bom/models/procurement_group.py
Executable file
95
sale_mrp_bom/models/procurement_group.py
Executable file
@@ -0,0 +1,95 @@
|
||||
# Copyright 2025 360ERP (<https://www.360erp.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"
|
||||
|
||||
@api.model
|
||||
def run(self, procurements, raise_user_error=True):
|
||||
"""
|
||||
Handle phantom BoM (kit) procurements linked to sale order lines.
|
||||
|
||||
If a procurement is for a kit product associated with a sale order line,
|
||||
this method explodes the kit into its components and generates new
|
||||
procurements for those components. Original procurements that are not
|
||||
kits, or not linked to a sale order line with a phantom BoM, are passed through.
|
||||
|
||||
:param procurements: A list of Procurement namedtuples
|
||||
:param raise_user_error: Whether to raise UserError on failure
|
||||
:return: Result of the original run method
|
||||
"""
|
||||
# Collect unique sale_line_ids from procurements that have them
|
||||
sale_line_ids = list(
|
||||
set(
|
||||
p.values.get("sale_line_id")
|
||||
for p in procurements
|
||||
if p.values.get("sale_line_id")
|
||||
)
|
||||
)
|
||||
|
||||
# Pre-fetch sale lines and create a mapping for quick access
|
||||
sale_lines = self.env["sale.order.line"].browse(sale_line_ids)
|
||||
sale_lines_map = {sl.id: sl for sl in sale_lines}
|
||||
|
||||
procurements_without_kit = []
|
||||
for procurement in procurements:
|
||||
sale_line_id = procurement.values.get("sale_line_id")
|
||||
sale_line = sale_lines_map.get(sale_line_id)
|
||||
|
||||
bom_kit = (
|
||||
sale_line.bom_id.filtered(
|
||||
lambda bm, pr=procurement: bm.type == "phantom"
|
||||
and (
|
||||
# If BoM has product_id, match the procurement's product_id
|
||||
(bm.product_id and bm.product_id == pr.product_id)
|
||||
or
|
||||
# Otherwise (if BoM has no product_id), match the template_id
|
||||
(
|
||||
not bm.product_id
|
||||
and bm.product_tmpl_id == pr.product_id.product_tmpl_id
|
||||
)
|
||||
)
|
||||
)
|
||||
if sale_line
|
||||
else False
|
||||
)
|
||||
if bom_kit:
|
||||
order_qty = procurement.product_uom._compute_quantity(
|
||||
procurement.product_qty, bom_kit.product_uom_id, round=False
|
||||
)
|
||||
qty_to_produce = order_qty / bom_kit.product_qty
|
||||
_dummy, bom_sub_lines = bom_kit.explode(
|
||||
procurement.product_id,
|
||||
qty_to_produce,
|
||||
never_attribute_values=procurement.values.get(
|
||||
"never_product_template_attribute_value_ids"
|
||||
),
|
||||
)
|
||||
for bom_line, bom_line_data in bom_sub_lines:
|
||||
bom_line_uom = bom_line.product_uom_id
|
||||
quant_uom = bom_line.product_id.uom_id
|
||||
# recreate dict of values since each child has its own bom_line_id
|
||||
values = dict(procurement.values, bom_line_id=bom_line.id)
|
||||
component_qty, procurement_uom = (
|
||||
bom_line_uom._adjust_uom_quantities(
|
||||
bom_line_data["qty"], quant_uom
|
||||
)
|
||||
)
|
||||
procurements_without_kit.append(
|
||||
self.env["procurement.group"].Procurement(
|
||||
bom_line.product_id,
|
||||
component_qty,
|
||||
procurement_uom,
|
||||
procurement.location_id,
|
||||
procurement.name,
|
||||
procurement.origin,
|
||||
procurement.company_id,
|
||||
values,
|
||||
)
|
||||
)
|
||||
else:
|
||||
procurements_without_kit.append(procurement)
|
||||
return super().run(procurements_without_kit, raise_user_error=raise_user_error)
|
||||
36
sale_mrp_bom/models/sale_order_line.py
Executable file
36
sale_mrp_bom/models/sale_order_line.py
Executable file
@@ -0,0 +1,36 @@
|
||||
# Copyright 2020 Akretion Renato Lima <renato.lima@akretion.com.br>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = "sale.order.line"
|
||||
|
||||
bom_id = fields.Many2one(
|
||||
comodel_name="mrp.bom",
|
||||
string="BoM",
|
||||
domain="[('product_tmpl_id.product_variant_ids', '=', product_id),"
|
||||
"'|', ('product_id', '=', product_id), "
|
||||
"('product_id', '=', False)]",
|
||||
)
|
||||
|
||||
@api.constrains("bom_id", "product_id")
|
||||
def _check_match_product_variant_ids(self):
|
||||
for line in self:
|
||||
if not line.bom_id:
|
||||
continue
|
||||
bom_product = line.bom_id.product_id
|
||||
bom_product_tmpl = line.bom_id.product_tmpl_id
|
||||
|
||||
if bom_product and bom_product == line.product_id:
|
||||
continue
|
||||
if not bom_product and bom_product_tmpl == line.product_template_id:
|
||||
continue
|
||||
raise ValidationError(
|
||||
_(
|
||||
"Please select a BoM that matches the product %(product)s",
|
||||
product=line.product_id.display_name,
|
||||
)
|
||||
)
|
||||
14
sale_mrp_bom/models/stock_move.py
Executable file
14
sale_mrp_bom/models/stock_move.py
Executable file
@@ -0,0 +1,14 @@
|
||||
# Copyright 2020 Akretion Renato Lima <renato.lima@akretion.com.br>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class StockMove(models.Model):
|
||||
_inherit = "stock.move"
|
||||
|
||||
def _prepare_procurement_values(self):
|
||||
values = super()._prepare_procurement_values()
|
||||
if self.sale_line_id and self.sale_line_id.bom_id:
|
||||
values["bom_id"] = self.sale_line_id.bom_id
|
||||
return values
|
||||
Reference in New Issue
Block a user