mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
Normal BoM products (those which are manufactured) are regular stored products and their immediately_usable_qty will be summed with potential stock. This was the expected behavior of the module and it was lost at some point. Phantom BoM products (kits) don't have real stock so their available to promise quantity will be the same as the potential. As an improvement, we've added the possibility to override the sum of potential and available to promise. In some cases such addition doesn't make sense as we don't know how long can take to manufacture those potential units. TT35589
185 lines
7.4 KiB
Python
185 lines
7.4 KiB
Python
# Copyright 2014 Numérigraphe SARL
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
|
|
|
from collections import Counter
|
|
|
|
from odoo import api, models
|
|
from odoo.fields import first
|
|
from odoo.tools import float_round
|
|
|
|
|
|
class ProductProduct(models.Model):
|
|
_inherit = "product.product"
|
|
|
|
@api.depends("virtual_available", "bom_ids", "bom_ids.product_qty")
|
|
def _compute_available_quantities(self):
|
|
super()._compute_available_quantities()
|
|
|
|
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_ids")
|
|
|
|
if not product_with_bom:
|
|
return res, stock_dict
|
|
icp = self.env["ir.config_parameter"]
|
|
stock_available_mrp_based_on = icp.sudo().get_param(
|
|
"stock_available_mrp_based_on", "qty_available"
|
|
)
|
|
|
|
# explode all boms at once
|
|
exploded_boms = product_with_bom._explode_boms()
|
|
|
|
# extract the list of product used as bom component
|
|
component_products = self.env["product.product"].browse()
|
|
for exploded_components in exploded_boms.values():
|
|
for bom_component in exploded_components:
|
|
component_products |= first(bom_component).product_id
|
|
|
|
# Compute stock for product components.
|
|
# {'productid': {field_name: qty}}
|
|
if res and stock_available_mrp_based_on in list(res.values())[0]:
|
|
# If the qty is computed by the same method use it to avoid
|
|
# stressing the cache
|
|
component_qties, _ = component_products._compute_available_quantities_dict()
|
|
else:
|
|
# The qty is a field computed by an other method than the
|
|
# current one. Take the value on the record.
|
|
component_qties = {
|
|
p.id: {stock_available_mrp_based_on: p[stock_available_mrp_based_on]}
|
|
for p in component_products
|
|
}
|
|
|
|
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:
|
|
# The BoM has no line we can use
|
|
potential_qty = 0.0
|
|
else:
|
|
# Find the lowest quantity we can make with the stock at hand
|
|
components_potential_qty = min(
|
|
[
|
|
component_qties[component.id][stock_available_mrp_based_on]
|
|
/ need
|
|
for component, need in component_needs.items()
|
|
]
|
|
)
|
|
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 = bom_id.product_uom_id._compute_quantity(
|
|
potential_qty,
|
|
bom_id.product_tmpl_id.uom_id,
|
|
rounding_method="DOWN",
|
|
)
|
|
|
|
res[product.id]["potential_qty"] = potential_qty
|
|
# Normal BoM products (those which are manufactured) are regular stored
|
|
# products and their immediately_usable_qty will be the sum of its regular
|
|
# available to promise quanity as storable product and its potential qty
|
|
# Phantom BoM products (kits) don't have real stock so their available
|
|
# to promise quantity will be the same as the potential.
|
|
if bom_id.type == "phantom":
|
|
res[product.id]["immediately_usable_qty"] = potential_qty
|
|
# We can override at BoM level to add the potential quantity to the
|
|
# manufactured product. This could be the case when the manufacturing
|
|
# proccess is uncertain and the promising such quantities could be
|
|
# missleading.
|
|
elif not bom_id.add_potential_exception:
|
|
res[product.id]["immediately_usable_qty"] += potential_qty
|
|
return res, stock_dict
|
|
|
|
def _explode_boms(self):
|
|
"""
|
|
return a dict by product_id of exploded bom lines
|
|
:return:
|
|
"""
|
|
return self.explode_bom_quantities()
|
|
|
|
@api.model
|
|
def _get_components_needs(self, exploded_components):
|
|
"""Return the needed qty of each compoments in the exploded_components
|
|
|
|
:type exploded_components
|
|
:rtype: collections.Counter
|
|
"""
|
|
needs = Counter()
|
|
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
|