Merge PR #1267 into 14.0

Signed-off-by dreispt
This commit is contained in:
OCA-git-bot
2021-12-04 17:27:32 +00:00
3 changed files with 122 additions and 73 deletions

View File

@@ -67,6 +67,9 @@ class ProductProduct(models.Model):
"the materials already at hand.",
)
def _get_search_immediately_usable_qty_domain(self):
return [("type", "=", "product")]
@api.model
def _search_immediately_usable_qty(self, operator, value):
"""Search function for the immediately_usable_qty field.
@@ -76,9 +79,10 @@ class ProductProduct(models.Model):
:param value: str
:return: list of tuple (domain)
"""
products = self.search([])
# Force prefetch
products.mapped("immediately_usable_qty")
product_domain = self._get_search_immediately_usable_qty_domain()
products = self.with_context(prefetch_fields=False).search(
product_domain, order="id"
)
product_ids = []
for product in products:
if OPERATORS[operator](product.immediately_usable_qty, value):

View File

@@ -3,51 +3,22 @@
from collections import Counter
from odoo import api, fields, models
from odoo import api, models
from odoo.fields import first
from odoo.tools import float_round
class ProductProduct(models.Model):
_inherit = "product.product"
bom_id = fields.Many2one(
comodel_name="mrp.bom", compute="_compute_bom_id", string="BOM"
)
@api.depends("virtual_available", "bom_id", "bom_id.product_qty")
@api.depends("virtual_available", "bom_ids", "bom_ids.product_qty")
def _compute_available_quantities(self):
super()._compute_available_quantities()
def _get_bom_id_domain(self):
"""
Real multi domain
:return:
"""
return [
"|",
("product_id", "in", self.ids),
"&",
("product_id", "=", False),
("product_tmpl_id", "in", self.mapped("product_tmpl_id.id")),
]
@api.depends("product_tmpl_id")
def _compute_bom_id(self):
bom_obj = self.env["mrp.bom"]
boms = bom_obj.search(self._get_bom_id_domain(), order="sequence, product_id")
for product in self:
product.bom_id = product.bom_id
product_boms = boms.filtered(
lambda b: b.product_id == product
or (not b.product_id and b.product_tmpl_id == product.product_tmpl_id)
)
if product_boms:
product.bom_id = first(product_boms)
def _compute_available_quantities_dict(self):
res, stock_dict = super()._compute_available_quantities_dict()
# compute qty for product with bom
product_with_bom = self.filtered("bom_id")
product_with_bom = self.filtered("bom_ids")
if not product_with_bom:
return res, stock_dict
@@ -81,6 +52,7 @@ class ProductProduct(models.Model):
for product in product_with_bom:
# Need by product (same product can be in many BOM lines/levels)
bom_id = first(product.bom_ids)
exploded_components = exploded_boms[product.id]
component_needs = product._get_components_needs(exploded_components)
if not component_needs:
@@ -95,21 +67,19 @@ class ProductProduct(models.Model):
for component, need in component_needs.items()
]
)
potential_qty = product.bom_id.product_qty * components_potential_qty
potential_qty = bom_id.product_qty * components_potential_qty
potential_qty = potential_qty > 0.0 and potential_qty or 0.0
# We want to respect the rounding factor of the potential_qty
# Rounding down as we want to be pesimistic.
potential_qty = product.bom_id.product_uom_id._compute_quantity(
potential_qty = bom_id.product_uom_id._compute_quantity(
potential_qty,
product.bom_id.product_tmpl_id.uom_id,
bom_id.product_tmpl_id.uom_id,
rounding_method="DOWN",
)
res[product.id]["potential_qty"] = potential_qty
immediately_usable_qty = (
potential_qty if product.bom_id.type != "phantom" else 0
)
immediately_usable_qty = potential_qty if bom_id.type != "phantom" else 0
res[product.id]["immediately_usable_qty"] += immediately_usable_qty
return res, stock_dict
@@ -119,10 +89,7 @@ class ProductProduct(models.Model):
return a dict by product_id of exploded bom lines
:return:
"""
exploded_boms = {}
for rec in self:
exploded_boms[rec.id] = rec.bom_id.explode(rec, 1.0)[1]
return exploded_boms
return self.explode_bom_quantities()
@api.model
def _get_components_needs(self, exploded_components):
@@ -132,8 +99,76 @@ class ProductProduct(models.Model):
:rtype: collections.Counter
"""
needs = Counter()
for bom_component in exploded_components:
component = bom_component[0].product_id
needs += Counter({component: bom_component[1]["qty"]})
for bom_line, bom_qty in exploded_components:
component = bom_line.product_id
needs += Counter({component: bom_qty})
return needs
def explode_bom_quantities(self):
"""Explode a bill of material with quantities to consume
It returns a dict with the exploded bom lines and
the quantity they consume. Example::
{
<product-id>: [
(<bom-line-id>, <quantity>)
(<bom-line-id>, <quantity>)
]
}
The 'MrpBom.explode()' method includes the same information, with other
things, but is under-optimized to be used for the purpose of this
module. The killer is particularly the call to `_bom_find()` which can
generate thousands of SELECT for searches.
"""
result = {}
for product in self:
lines_done = []
bom_lines = [
(first(product.bom_ids), bom_line, product, 1.0)
for bom_line in first(product.bom_ids).bom_line_ids
]
while bom_lines:
(current_bom, current_line, current_product, current_qty) = bom_lines[0]
bom_lines = bom_lines[1:]
if current_line._skip_bom_line(current_product):
continue
line_quantity = current_qty * current_line.product_qty
sub_bom = first(current_line.product_id.bom_ids)
if sub_bom.type == "phantom":
product_uom = current_line.product_uom_id
converted_line_quantity = product_uom._compute_quantity(
line_quantity / sub_bom.product_qty,
sub_bom.product_uom_id,
)
bom_lines = [
(
sub_bom,
line,
current_line.product_id,
converted_line_quantity,
)
for line in sub_bom.bom_line_ids
] + bom_lines
else:
# We round up here because the user expects that if he has
# to consume a little more, the whole UOM unit should be
# consumed.
rounding = current_line.product_uom_id.rounding
line_quantity = float_round(
line_quantity,
precision_rounding=rounding,
rounding_method="UP",
)
lines_done.append((current_line, line_quantity))
result[product.id] = lines_done
return result

View File

@@ -3,34 +3,45 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo.osv.expression import TRUE_LEAF
from odoo.tests.common import TransactionCase
from odoo.tests.common import SavepointCase
class TestPotentialQty(TransactionCase):
class TestPotentialQty(SavepointCase):
"""Test the potential quantity on a product with a multi-line BoM"""
def setUp(self):
super().setUp()
@classmethod
def setUpClass(cls):
super(TestPotentialQty, cls).setUpClass()
self.product_model = self.env["product.product"]
self.bom_model = self.env["mrp.bom"]
self.bom_line_model = self.env["mrp.bom.line"]
self.stock_quant_model = self.env["stock.quant"]
self.config = self.env["ir.config_parameter"]
self.location = self.env["stock.location"]
self.main_company = self.browse_ref("base.main_company")
cls.product_model = cls.env["product.product"]
cls.bom_model = cls.env["mrp.bom"]
cls.bom_line_model = cls.env["mrp.bom.line"]
cls.stock_quant_model = cls.env["stock.quant"]
cls.config = cls.env["ir.config_parameter"]
cls.location = cls.env["stock.location"]
cls.main_company = cls.env.ref("base.main_company")
# Get the warehouses
self.wh_main = self.browse_ref("stock.warehouse0")
self.wh_ch = self.browse_ref("stock.stock_warehouse_shop0")
cls.wh_main = cls.env.ref("stock.warehouse0")
cls.wh_ch = cls.env.ref("stock.stock_warehouse_shop0")
# We need to compute parent_left and parent_right of the locations as
# they are used to compute qty_available of the product.
cls.location._parent_store_compute()
cls.setup_demo_data()
@classmethod
def setup_demo_data(cls):
#  An interesting product (multi-line BoM, variants)
self.tmpl = self.browse_ref("mrp.product_product_table_kit_product_template")
cls.tmpl = cls.env.ref("mrp.product_product_table_kit_product_template")
#  First variant
self.var1 = self.browse_ref("mrp.product_product_table_kit")
cls.var1 = cls.env.ref("mrp.product_product_table_kit")
cls.var1.type = "product"
#  Second variant
self.var2 = self.browse_ref("stock_available_mrp.product_kit_1a")
cls.var2 = cls.env.ref("stock_available_mrp.product_kit_1a")
cls.var2.type = "product"
# Make bolt a stockable product to be able to change its stock
# we need to unreserve the existing move before being able to do it.
bolt = self.env.ref("mrp.product_product_computer_desk_bolt")
bolt = cls.env.ref("mrp.product_product_computer_desk_bolt")
bolt.stock_move_ids._do_unreserve()
bolt.type = "product"
# Components that can be used to make the product
@@ -38,11 +49,10 @@ class TestPotentialQty(TransactionCase):
# Bolt
bolt,
# Wood Panel
self.browse_ref("mrp.product_product_wood_panel"),
cls.env.ref("mrp.product_product_wood_panel"),
]
# Zero-out the inventory of all variants and components
for component in components + [v for v in self.tmpl.product_variant_ids]:
for component in components + [v for v in cls.tmpl.product_variant_ids]:
moves = component.stock_move_ids.filtered(
lambda mo: mo.state not in ("done", "cancel")
)
@@ -50,13 +60,13 @@ class TestPotentialQty(TransactionCase):
component.stock_quant_ids.unlink()
#  A product without a BoM
self.product_wo_bom = self.browse_ref("product.product_product_11")
# A product without a BoM
cls.product_wo_bom = cls.env.ref("product.product_product_11")
# Record the initial quantity available for sale
self.initial_usable_qties = {
cls.initial_usable_qties = {
i.id: i.immediately_usable_qty
for i in [self.tmpl, self.var1, self.var2, self.product_wo_bom]
for i in [cls.tmpl, cls.var1, cls.var2, cls.product_wo_bom]
}
def _create_inventory(self, location_id, company_id):