mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
[10.0][MIG] port stock_available_mrp to 10.0
This commit is contained in:
committed by
Florian da Costa
parent
e6ffa45529
commit
e23de10ffd
@@ -69,6 +69,7 @@ Contributors
|
||||
* Lionel Sausin (Numérigraphe) <ls@numerigraphe.com>
|
||||
* many thanks to Graeme Gellatly for his advice and code review
|
||||
* Laurent Mignon <laurent.mignon@acsone.eu>
|
||||
* Cédric Pigeon <cedric.pigeon@acsone.eu>
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
{
|
||||
'name': 'Consider the production potential is available to promise',
|
||||
'version': '9.0.1.0.0',
|
||||
'version': '10.0.1.0.0',
|
||||
"author": u"Numérigraphe,"
|
||||
u"Odoo Community Association (OCA)",
|
||||
'category': 'Hidden',
|
||||
@@ -15,8 +15,8 @@
|
||||
'views/product_template_view.xml',
|
||||
],
|
||||
'demo': [
|
||||
'demo/mrp_bom.yml',
|
||||
'demo/mrp_data.xml',
|
||||
],
|
||||
'license': 'AGPL-3',
|
||||
'installable': False,
|
||||
'installable': True,
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
- Create a UoM in the category of PCE
|
||||
- !record {model: product.uom, id: thousand}:
|
||||
name: Thousand
|
||||
factor: 0.001
|
||||
rounding: 0.001
|
||||
uom_type: bigger
|
||||
category_id: product.product_uom_categ_unit
|
||||
|
||||
- Add a BOM whereby 0.042K "RAM SR2" can be replaced with 13 dozens "HDD-SH1" + 8 CPUa8 with 50% efficiency. This lets us test UoM conversions for the finished product and the raw materials, as well as the unfolding of phantom BoMs
|
||||
- !record {model: mrp.bom, id: sr2_from_hdd}:
|
||||
product_id: product.product_product_14
|
||||
product_tmpl_id: product.product_product_14_product_template
|
||||
product_uom: thousand
|
||||
product_qty: 0.042
|
||||
type: phantom
|
||||
sequence: -1
|
||||
product_efficiency: 0.5
|
||||
- !record {model: mrp.bom.line, id: sr2_from_hdd_line1}:
|
||||
bom_id: sr2_from_hdd
|
||||
product_id: product.product_product_18
|
||||
product_qty: 13
|
||||
product_uom: product.product_uom_dozen
|
||||
- !record {model: mrp.bom.line, id: sr2_from_hdd_line2}:
|
||||
bom_id: sr2_from_hdd
|
||||
product_id: product.product_product_23
|
||||
product_qty: 8
|
||||
product_uom: product.product_uom_unit
|
||||
38
stock_available_mrp/demo/mrp_data.xml
Normal file
38
stock_available_mrp/demo/mrp_data.xml
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="product_kit_1a" model="product.product">
|
||||
<field name="default_code">PCSC234-WHITE</field>
|
||||
<field name="product_tmpl_id"
|
||||
ref="mrp.product_product_build_kit_product_template"/>
|
||||
<field name="attribute_value_ids" eval="[(6,0,[ref('product.product_attribute_value_3')])]"/>
|
||||
</record>
|
||||
|
||||
<record id="mrp.product_product_build_kit" model="product.product">
|
||||
<field name="attribute_value_ids"
|
||||
eval="[(6,0,[ref('product.product_attribute_value_4')])]"/>
|
||||
</record>
|
||||
|
||||
<record id="mrp.mrp_bom_kit_line_2" model="mrp.bom.line">
|
||||
<field name="attribute_value_ids" eval="[(6,0,[ref('product.product_attribute_value_4')])]"/>
|
||||
</record>
|
||||
|
||||
<record id="product_product_9_white" model="product.product">
|
||||
<field name="name">Apple Wireless Keyboard</field>
|
||||
<field name="categ_id" ref="product.product_category_5"/>
|
||||
<field name="standard_price">10.0</field>
|
||||
<field name="list_price">47.0</field>
|
||||
<field name="type">consu</field>
|
||||
<field name="uom_id" ref="product.product_uom_unit"/>
|
||||
<field name="uom_po_id" ref="product.product_uom_unit"/>
|
||||
<field name="default_code">E-COM10-WHITE</field>
|
||||
</record>
|
||||
|
||||
<record id="mrp_bom_kit_line_3" model="mrp.bom.line">
|
||||
<field name="product_id" ref="product_product_9_white"/>
|
||||
<field name="product_qty">1</field>
|
||||
<field name="product_uom_id" ref="product.product_uom_unit"/>
|
||||
<field name="sequence">5</field>
|
||||
<field name="bom_id" ref="mrp.mrp_bom_kit"/>
|
||||
<field name="attribute_value_ids" eval="[(6,0,[ref('product.product_attribute_value_3')])]"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -2,133 +2,140 @@
|
||||
# © 2014 Numérigraphe SARL
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from collections import Counter
|
||||
|
||||
from openerp import models, fields, api
|
||||
from openerp.addons import decimal_precision as dp
|
||||
|
||||
from openerp.exceptions import AccessError
|
||||
from collections import Counter, defaultdict
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ProductProduct(models.Model):
|
||||
|
||||
_inherit = 'product.product'
|
||||
|
||||
potential_qty = fields.Float(
|
||||
compute='_get_potential_qty',
|
||||
type='float',
|
||||
digits_compute=dp.get_precision('Product Unit of Measure'),
|
||||
string='Potential',
|
||||
help="Quantity of this Product that could be produced using "
|
||||
"the materials already at hand.")
|
||||
|
||||
# Needed for fields dependencies
|
||||
# When self.potential_qty is compute, we want to force the ORM
|
||||
# to compute all the components potential_qty too.
|
||||
component_ids = fields.Many2many(
|
||||
comodel_name='product.product',
|
||||
compute='_get_component_ids',
|
||||
bom_id = fields.Many2one(
|
||||
'mrp.bom',
|
||||
compute='_compute_bom_id',
|
||||
string='Bill of Materials'
|
||||
)
|
||||
|
||||
@api.multi
|
||||
@api.depends('potential_qty')
|
||||
def _immediately_usable_qty(self):
|
||||
"""Add the potential quantity to the quantity available to promise.
|
||||
|
||||
This is the same implementation as for templates."""
|
||||
super(ProductProduct, self)._immediately_usable_qty()
|
||||
for product in self:
|
||||
product.immediately_usable_qty += product.potential_qty
|
||||
@api.depends('virtual_available')
|
||||
def _compute_available_quantities(self):
|
||||
super(ProductProduct, self)._compute_available_quantities()
|
||||
|
||||
@api.multi
|
||||
@api.depends('component_ids.potential_qty')
|
||||
def _get_potential_qty(self):
|
||||
"""Compute the potential qty based on the available components."""
|
||||
bom_obj = self.env['mrp.bom']
|
||||
uom_obj = self.env['product.uom']
|
||||
|
||||
def _compute_bom_id(self):
|
||||
domain = [('product_id', 'in', self.ids)]
|
||||
product_tmpl_ids = []
|
||||
bom_product_ids = self.env['mrp.bom'].search(domain)
|
||||
# find bom linked to a product
|
||||
bom_by_product_id = {
|
||||
b.product_id.id: b for b in bom_product_ids}
|
||||
product_id_found = bom_by_product_id.keys()
|
||||
for product in self:
|
||||
bom_id = bom_obj._bom_find(product_id=product.id)
|
||||
if not bom_id:
|
||||
product.potential_qty = 0.0
|
||||
continue
|
||||
if product.id not in product_id_found:
|
||||
product_tmpl_ids.append(product.product_tmpl_id.id)
|
||||
domain = [('product_id', '=', False),
|
||||
('product_tmpl_id', 'in', product_tmpl_ids)]
|
||||
# find boms linked to the product template
|
||||
bom_product_template = self.env['mrp.bom'].search(domain)
|
||||
bom_by_product_tmpl_id = {
|
||||
b.product_tmpl_id.id: b for b in bom_product_template}
|
||||
for product in self:
|
||||
product.bom_id = bom_by_product_id.get(
|
||||
product.id,
|
||||
bom_by_product_tmpl_id.get(product.product_tmpl_id.id)
|
||||
)
|
||||
|
||||
bom = bom_obj.browse(bom_id)
|
||||
@api.multi
|
||||
def _compute_available_quantities_dict(self):
|
||||
res = super(ProductProduct, self)._compute_available_quantities_dict()
|
||||
|
||||
# Need by product (same product can be in many BOM lines/levels)
|
||||
try:
|
||||
component_needs = self._get_components_needs(product, bom)
|
||||
except AccessError:
|
||||
# If user doesn't have access to BOM
|
||||
# he can't see potential_qty
|
||||
component_needs = None
|
||||
# compute qty for product with bom
|
||||
product_with_bom = self.filtered(lambda p: p.bom_id)
|
||||
|
||||
if not component_needs:
|
||||
# The BoM has no line we can use
|
||||
product.potential_qty = 0.0
|
||||
|
||||
else:
|
||||
# Find the lowest quantity we can make with the stock at hand
|
||||
components_potential_qty = min(
|
||||
[self._get_component_qty(component) // need
|
||||
for component, need in component_needs.items()]
|
||||
)
|
||||
|
||||
# Compute with bom quantity
|
||||
bom_qty = uom_obj._compute_qty_obj(
|
||||
bom.product_uom,
|
||||
bom.product_qty,
|
||||
bom.product_tmpl_id.uom_id
|
||||
)
|
||||
product.potential_qty = bom_qty * components_potential_qty
|
||||
|
||||
def _get_component_qty(self, component):
|
||||
""" Return the component qty to use based en company settings.
|
||||
|
||||
:type component: product_product
|
||||
:rtype: float
|
||||
"""
|
||||
if not product_with_bom:
|
||||
return res
|
||||
icp = self.env['ir.config_parameter']
|
||||
stock_available_mrp_based_on = icp.get_param(
|
||||
'stock_available_mrp_based_on', 'qty_available'
|
||||
)
|
||||
|
||||
return component[stock_available_mrp_based_on]
|
||||
# from here we start the computation of bom qties in an isolated
|
||||
# environment to avoid trouble with prefetch and cache
|
||||
product_with_bom = product_with_bom.with_context(
|
||||
unique=models.NewId()).with_prefetch(defaultdict(set))
|
||||
|
||||
def _get_components_needs(self, product, bom):
|
||||
""" Return the needed qty of each compoments in the *bom* of *product*.
|
||||
# explode all boms at once
|
||||
exploded_boms = product_with_bom._explode_boms()
|
||||
|
||||
:type product: product_product
|
||||
:type bom: mrp_bom
|
||||
# extract the list of product used as bom component
|
||||
product_components_ids = set()
|
||||
for exploded_components in exploded_boms.values():
|
||||
for bom_component in exploded_components:
|
||||
product_components_ids.add(bom_component[0].product_id.id)
|
||||
|
||||
# Compute stock for product components.
|
||||
# {'productid': {field_name: qty}}
|
||||
component_products = product_with_bom.browse(
|
||||
product_components_ids)
|
||||
if stock_available_mrp_based_on in res.keys():
|
||||
# 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)
|
||||
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 = (product.bom_id.product_qty *
|
||||
components_potential_qty)
|
||||
|
||||
res[product.id]['potential_qty'] = potential_qty
|
||||
res[product.id]['immediately_usable_qty'] += potential_qty
|
||||
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def _explode_boms(self):
|
||||
"""
|
||||
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
|
||||
|
||||
@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
|
||||
"""
|
||||
bom_obj = self.env['mrp.bom']
|
||||
uom_obj = self.env['product.uom']
|
||||
product_obj = self.env['product.product']
|
||||
|
||||
needs = Counter()
|
||||
for bom_component in bom_obj._bom_explode(bom, product, 1.0)[0]:
|
||||
product_uom = uom_obj.browse(bom_component['product_uom'])
|
||||
component = product_obj.browse(bom_component['product_id'])
|
||||
|
||||
component_qty = uom_obj._compute_qty_obj(
|
||||
product_uom,
|
||||
bom_component['product_qty'],
|
||||
component.uom_id,
|
||||
)
|
||||
needs += Counter(
|
||||
{component: component_qty}
|
||||
)
|
||||
for bom_component in exploded_components:
|
||||
component = bom_component[0].product_id
|
||||
needs += Counter({component: bom_component[1]['qty']})
|
||||
|
||||
return needs
|
||||
|
||||
def _get_component_ids(self):
|
||||
""" Compute component_ids by getting all the components for
|
||||
this product.
|
||||
"""
|
||||
bom_obj = self.env['mrp.bom']
|
||||
|
||||
bom_id = bom_obj._bom_find(product_id=self.id)
|
||||
if bom_id:
|
||||
bom = bom_obj.browse(bom_id)
|
||||
for bom_component in bom_obj._bom_explode(bom, self, 1.0)[0]:
|
||||
self.component_ids |= self.browse(bom_component['product_id'])
|
||||
|
||||
@@ -2,45 +2,17 @@
|
||||
# © 2014 Numérigraphe SARL
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from openerp import models, fields, api
|
||||
from openerp.addons import decimal_precision as dp
|
||||
from odoo import models, api
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = 'product.template'
|
||||
|
||||
potential_qty = fields.Float(
|
||||
compute='_get_potential_qty',
|
||||
type='float',
|
||||
digits_compute=dp.get_precision('Product Unit of Measure'),
|
||||
string='Potential',
|
||||
help="Quantity of this Product that could be produced using "
|
||||
"the materials already at hand. "
|
||||
"If the product has several variants, this will be the biggest "
|
||||
"quantity that can be made for a any single variant.")
|
||||
|
||||
@api.multi
|
||||
@api.depends('potential_qty')
|
||||
def _immediately_usable_qty(self):
|
||||
"""Add the potential quantity to the quantity available to promise.
|
||||
|
||||
This is the same implementation as for variants."""
|
||||
super(ProductTemplate, self)._immediately_usable_qty()
|
||||
for tmpl in self:
|
||||
tmpl.immediately_usable_qty += tmpl.potential_qty
|
||||
|
||||
@api.multi
|
||||
@api.depends('product_variant_ids.potential_qty')
|
||||
def _get_potential_qty(self):
|
||||
"""Compute the potential as the max of all the variants's potential.
|
||||
|
||||
We can't add the potential of variants: if they share components we
|
||||
may not be able to make all the variants.
|
||||
So we set the arbitrary rule that we can promise up to the biggest
|
||||
variant's potential.
|
||||
"""
|
||||
for tmpl in self:
|
||||
if not tmpl.product_variant_ids:
|
||||
continue
|
||||
tmpl.potential_qty = max(
|
||||
[v.potential_qty for v in tmpl.product_variant_ids])
|
||||
def _compute_available_quantities_dict(self):
|
||||
res = super(ProductTemplate, self)._compute_available_quantities_dict()
|
||||
for template in self:
|
||||
if template.bom_ids:
|
||||
res[template.id]['immediately_usable_qty'] =\
|
||||
res[template.id]['potential_qty']
|
||||
return res
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
# © 2014 Numérigraphe SARL
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from openerp.tests.common import TransactionCase
|
||||
from openerp.osv.expression import TRUE_LEAF
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.osv.expression import TRUE_LEAF
|
||||
|
||||
|
||||
class TestPotentialQty(TransactionCase):
|
||||
@@ -23,21 +23,19 @@ class TestPotentialQty(TransactionCase):
|
||||
def setup_demo_data(self):
|
||||
# An interesting product (multi-line BoM, variants)
|
||||
self.tmpl = self.browse_ref(
|
||||
'product.product_product_4_product_template')
|
||||
'mrp.product_product_build_kit_product_template')
|
||||
# First variant
|
||||
self.var1 = self.browse_ref('product.product_product_4c')
|
||||
self.var1 = self.browse_ref('mrp.product_product_build_kit')
|
||||
# Second variant
|
||||
self.var2 = self.browse_ref('product.product_product_4')
|
||||
self.var2 = self.browse_ref(
|
||||
'stock_available_mrp.product_kit_1a')
|
||||
# Components that can be used to make the product
|
||||
component_ids = [
|
||||
# CPUa8
|
||||
self.ref('product.product_product_23'),
|
||||
# RAM-SR2
|
||||
self.ref('product.product_product_14'),
|
||||
# HDD SH-2 replaces RAM-SR2 through our demo phantom BoM
|
||||
self.ref('product.product_product_18'),
|
||||
# RAM-SR3
|
||||
self.ref('product.product_product_15')]
|
||||
# KeyBoard
|
||||
self.ref('product.product_product_9'),
|
||||
# Mouse
|
||||
self.ref('product.product_product_12'),
|
||||
]
|
||||
|
||||
# Zero-out the inventory of all variants and components
|
||||
for component_id in (
|
||||
@@ -53,7 +51,7 @@ class TestPotentialQty(TransactionCase):
|
||||
inventory.action_done()
|
||||
|
||||
# A product without a BoM
|
||||
self.product_wo_bom = self.browse_ref('product.product_product_23')
|
||||
self.product_wo_bom = self.browse_ref('product.product_product_11')
|
||||
|
||||
# Record the initial quantity available for sale
|
||||
self.initial_usable_qties = {i.id: i.immediately_usable_qty
|
||||
@@ -92,14 +90,12 @@ class TestPotentialQty(TransactionCase):
|
||||
'product_tmpl_id': product.product_tmpl_id.id,
|
||||
'product_id': product.id,
|
||||
'product_qty': product_qty,
|
||||
'product_uom': self.ref('product.product_uom_unit'),
|
||||
'routing_id': routing_id,
|
||||
})
|
||||
self.bom_line_model.create({
|
||||
'bom_id': bom.id,
|
||||
'product_id': sub_product.id,
|
||||
'product_qty': sub_product_qty,
|
||||
'product_uom': self.ref('product.product_uom_unit'),
|
||||
})
|
||||
|
||||
return bom
|
||||
@@ -122,7 +118,7 @@ class TestPotentialQty(TransactionCase):
|
||||
def test_potential_qty_no_bom_for_company(self):
|
||||
chicago_id = self.ref('stock.res_company_1')
|
||||
|
||||
# Receive 1000x CPUa8s owned by Chicago
|
||||
# Receive 1000x CPUI5s owned by Chicago
|
||||
inventory = self.env['stock.inventory'].create(
|
||||
{'name': 'Receive CPUa8',
|
||||
'company_id': chicago_id,
|
||||
@@ -132,12 +128,12 @@ class TestPotentialQty(TransactionCase):
|
||||
self.env['stock.inventory.line'].create(
|
||||
{'inventory_id': inventory.id,
|
||||
'company_id': chicago_id,
|
||||
'product_id': self.ref('product.product_product_23'),
|
||||
'product_id': self.ref('product.product_product_9'),
|
||||
'location_id': self.wh_ch.lot_stock_id.id,
|
||||
'product_qty': 1000.0})
|
||||
inventory.action_done()
|
||||
|
||||
# Put RAM-SR3 owned by Chicago for 1000x the 1st variant in main WH
|
||||
# Put RAM-SR5 owned by Chicago for 1000x the 1st variant in main WH
|
||||
inventory = self.env['stock.inventory'].create(
|
||||
{'name': 'components for 1st variant',
|
||||
'company_id': chicago_id,
|
||||
@@ -147,7 +143,7 @@ class TestPotentialQty(TransactionCase):
|
||||
self.env['stock.inventory.line'].create(
|
||||
{'inventory_id': inventory.id,
|
||||
'company_id': chicago_id,
|
||||
'product_id': self.ref('product.product_product_15'),
|
||||
'product_id': self.ref('product.product_product_12'),
|
||||
'location_id': self.wh_ch.lot_stock_id.id,
|
||||
'product_qty': 1000.0})
|
||||
inventory.action_done()
|
||||
@@ -187,44 +183,21 @@ class TestPotentialQty(TransactionCase):
|
||||
self.assertPotentialQty(
|
||||
test_user_tmpl, 1000.0, '')
|
||||
|
||||
def test_group_mrp_missing(self):
|
||||
test_user = self.env['res.users'].create({
|
||||
'name': 'test_demo',
|
||||
'login': 'test_demo',
|
||||
'company_id': self.ref('base.main_company'),
|
||||
'company_ids': [(4, self.ref('base.main_company'))],
|
||||
'groups_id': [(4, self.ref('stock.group_stock_user'))],
|
||||
})
|
||||
|
||||
p1 = self.product_model.create({'name': 'Test P1'})
|
||||
p2 = self.product_model.create({'name': 'Test P2'})
|
||||
|
||||
self.create_simple_bom(p1, p2,
|
||||
routing_id=self.ref('mrp.mrp_routing_0'))
|
||||
self.create_inventory(p2.id, 1)
|
||||
|
||||
test_user_p1 = p1.sudo(test_user)
|
||||
# Test user doesn't have access to mrp_routing, can't compute potential
|
||||
self.assertEqual(0, test_user_p1.potential_qty)
|
||||
|
||||
test_user.groups_id = [(4, self.ref('mrp.group_mrp_user'))]
|
||||
self.assertEqual(1, test_user_p1.potential_qty)
|
||||
|
||||
def test_potential_qty(self):
|
||||
for i in [self.tmpl, self.var1, self.var2]:
|
||||
self.assertPotentialQty(
|
||||
i, 0.0,
|
||||
"The potential quantity should start at 0")
|
||||
|
||||
# Receive 1000x CPUa8s
|
||||
# Receive 1000x Mouses
|
||||
inventory = self.env['stock.inventory'].create(
|
||||
{'name': 'Receive CPUa8',
|
||||
{'name': 'Receive Mouses',
|
||||
'location_id': self.wh_main.lot_stock_id.id,
|
||||
'filter': 'partial'})
|
||||
inventory.prepare_inventory()
|
||||
self.env['stock.inventory.line'].create(
|
||||
{'inventory_id': inventory.id,
|
||||
'product_id': self.ref('product.product_product_23'),
|
||||
'product_id': self.ref('product.product_product_12'),
|
||||
'location_id': self.wh_main.lot_stock_id.id,
|
||||
'product_qty': 1000.0})
|
||||
inventory.action_done()
|
||||
@@ -234,7 +207,7 @@ class TestPotentialQty(TransactionCase):
|
||||
"Receiving a single component should not change the "
|
||||
"potential of %s" % i)
|
||||
|
||||
# Receive enough RAM-SR3 to make 1000x the 1st variant in main WH
|
||||
# Receive enough keyboard to make 1000x the 1st variant in main WH
|
||||
inventory = self.env['stock.inventory'].create(
|
||||
{'name': 'components for 1st variant',
|
||||
'location_id': self.wh_main.lot_stock_id.id,
|
||||
@@ -242,7 +215,8 @@ class TestPotentialQty(TransactionCase):
|
||||
inventory.prepare_inventory()
|
||||
self.env['stock.inventory.line'].create(
|
||||
{'inventory_id': inventory.id,
|
||||
'product_id': self.ref('product.product_product_15'),
|
||||
'product_id': self.ref(
|
||||
'product.product_product_9'),
|
||||
'location_id': self.wh_main.lot_stock_id.id,
|
||||
'product_qty': 1000.0})
|
||||
inventory.action_done()
|
||||
@@ -257,9 +231,7 @@ class TestPotentialQty(TransactionCase):
|
||||
"Receiving variant 1's component should not change "
|
||||
"variant 2's potential")
|
||||
|
||||
# Receive enough components to make 42X the 2nd variant at Chicago
|
||||
# need 13 dozens of HDD with 50% efficiency to build 42 RAM
|
||||
# So 313 HDD (with rounding) for 42 RAM
|
||||
# Receive enough components to make 313 the 2nd variant at Chicago
|
||||
inventory = self.env['stock.inventory'].create(
|
||||
{'name': 'components for 2nd variant',
|
||||
'location_id': self.wh_ch.lot_stock_id.id,
|
||||
@@ -267,12 +239,13 @@ class TestPotentialQty(TransactionCase):
|
||||
inventory.prepare_inventory()
|
||||
self.env['stock.inventory.line'].create(
|
||||
{'inventory_id': inventory.id,
|
||||
'product_id': self.ref('product.product_product_23'),
|
||||
'product_id': self.ref('product.product_product_12'),
|
||||
'location_id': self.wh_ch.lot_stock_id.id,
|
||||
'product_qty': 1000.0})
|
||||
self.env['stock.inventory.line'].create(
|
||||
{'inventory_id': inventory.id,
|
||||
'product_id': self.ref('product.product_product_18'),
|
||||
'product_id': self.ref(
|
||||
'stock_available_mrp.product_product_9_white'),
|
||||
'location_id': self.wh_ch.lot_stock_id.id,
|
||||
'product_qty': 313.0})
|
||||
inventory.action_done()
|
||||
@@ -284,14 +257,14 @@ class TestPotentialQty(TransactionCase):
|
||||
"Receiving variant 2's component should not change "
|
||||
"variant 1's potential")
|
||||
self.assertPotentialQty(
|
||||
self.var2, 42.0,
|
||||
self.var2, 313.0,
|
||||
"Wrong variant 2 potential after receiving components")
|
||||
# Check by warehouse
|
||||
self.assertPotentialQty(
|
||||
self.tmpl.with_context(warehouse=self.wh_main.id), 1000.0,
|
||||
"Wrong potential quantity in main WH")
|
||||
self.assertPotentialQty(
|
||||
self.tmpl.with_context(warehouse=self.wh_ch.id), 42.0,
|
||||
self.tmpl.with_context(warehouse=self.wh_ch.id), 313.0,
|
||||
"Wrong potential quantity in Chicago WH")
|
||||
# Check by location
|
||||
self.assertPotentialQty(
|
||||
@@ -301,7 +274,7 @@ class TestPotentialQty(TransactionCase):
|
||||
self.assertPotentialQty(
|
||||
self.tmpl.with_context(
|
||||
location=self.wh_ch.lot_stock_id.id),
|
||||
42.0,
|
||||
313.0,
|
||||
"Wrong potential quantity in Chicago WH location")
|
||||
|
||||
def test_multi_unit_recursive_bom(self):
|
||||
@@ -324,12 +297,10 @@ class TestPotentialQty(TransactionCase):
|
||||
'product_id': p1.id,
|
||||
})
|
||||
|
||||
# 1 dozen of component
|
||||
self.bom_line_model.create({
|
||||
'bom_id': bom_p1.id,
|
||||
'product_id': p3.id,
|
||||
'product_qty': 1,
|
||||
'product_uom': self.ref('product.product_uom_dozen'),
|
||||
})
|
||||
|
||||
# Two p2 which have a bom
|
||||
@@ -337,7 +308,6 @@ class TestPotentialQty(TransactionCase):
|
||||
'bom_id': bom_p1.id,
|
||||
'product_id': p2.id,
|
||||
'product_qty': 2,
|
||||
'product_uom': self.ref('product.product_uom_unit'),
|
||||
})
|
||||
|
||||
bom_p2 = self.bom_model.create({
|
||||
@@ -351,12 +321,11 @@ class TestPotentialQty(TransactionCase):
|
||||
'bom_id': bom_p2.id,
|
||||
'product_id': p3.id,
|
||||
'product_qty': 2,
|
||||
'product_uom': self.ref('product.product_uom_unit'),
|
||||
})
|
||||
|
||||
p1.refresh()
|
||||
|
||||
# Need a least 1 dozen + 2 * 2 = 16 units for one P1
|
||||
# Need a least 5 units for one P1
|
||||
self.assertEqual(0, p1.potential_qty)
|
||||
|
||||
self.create_inventory(p3.id, 1)
|
||||
@@ -364,89 +333,30 @@ class TestPotentialQty(TransactionCase):
|
||||
p1.refresh()
|
||||
self.assertEqual(0, p1.potential_qty)
|
||||
|
||||
self.create_inventory(p3.id, 15)
|
||||
self.create_inventory(p3.id, 3)
|
||||
p1.refresh()
|
||||
self.assertEqual(0, p1.potential_qty)
|
||||
|
||||
self.create_inventory(p3.id, 16)
|
||||
self.create_inventory(p3.id, 5)
|
||||
p1.refresh()
|
||||
self.assertEqual(1.0, p1.potential_qty)
|
||||
|
||||
self.create_inventory(p3.id, 25)
|
||||
self.create_inventory(p3.id, 6)
|
||||
p1.refresh()
|
||||
self.assertEqual(1.0, p1.potential_qty)
|
||||
|
||||
self.create_inventory(p3.id, 32)
|
||||
self.create_inventory(p3.id, 10)
|
||||
p1.refresh()
|
||||
self.assertEqual(2.0, p1.potential_qty)
|
||||
|
||||
def test_bom_qty_and_efficiency(self):
|
||||
|
||||
p1 = self.product_model.create({
|
||||
'name': 'Test product with BOM',
|
||||
})
|
||||
|
||||
p2 = self.product_model.create({
|
||||
'name': 'Test sub product with BOM',
|
||||
})
|
||||
|
||||
p3 = self.product_model.create({
|
||||
'name': 'Test component'
|
||||
})
|
||||
|
||||
# A bom produce 2 dozen of P1
|
||||
bom_p1 = self.bom_model.create({
|
||||
'product_tmpl_id': p1.product_tmpl_id.id,
|
||||
'product_id': p1.id,
|
||||
'product_qty': 2,
|
||||
'product_uom': self.ref('product.product_uom_dozen'),
|
||||
})
|
||||
|
||||
# Need 5 p2 for that
|
||||
self.bom_line_model.create({
|
||||
'bom_id': bom_p1.id,
|
||||
'product_id': p2.id,
|
||||
'product_qty': 5,
|
||||
'product_uom': self.ref('product.product_uom_unit'),
|
||||
'product_efficiency': 0.8,
|
||||
})
|
||||
|
||||
# Which need 1 dozen of P3
|
||||
bom_p2 = self.bom_model.create({
|
||||
'product_tmpl_id': p2.product_tmpl_id.id,
|
||||
'product_id': p2.id,
|
||||
'type': 'phantom',
|
||||
})
|
||||
self.bom_line_model.create({
|
||||
'bom_id': bom_p2.id,
|
||||
'product_id': p3.id,
|
||||
'product_qty': 1,
|
||||
'product_uom': self.ref('product.product_uom_dozen'),
|
||||
})
|
||||
|
||||
p1.refresh()
|
||||
self.assertEqual(0, p1.potential_qty)
|
||||
|
||||
self.create_inventory(p3.id, 60)
|
||||
|
||||
p1.refresh()
|
||||
self.assertEqual(0, p1.potential_qty)
|
||||
|
||||
# Need 5 * 1 dozen => 60
|
||||
# But 80% lost each dozen, need 3 more by dozen => 60 + 5 *3 => 75
|
||||
self.create_inventory(p3.id, 75)
|
||||
|
||||
p1.refresh()
|
||||
self.assertEqual(24, p1.potential_qty)
|
||||
|
||||
def test_component_stock_choice(self):
|
||||
# Test to change component stock for compute BOM stock
|
||||
|
||||
# Get a demo product with outgoing move (qty: 3)
|
||||
imac = self.browse_ref('product.product_product_8')
|
||||
prod = self.browse_ref('product.product_product_20')
|
||||
|
||||
# Set on hand qty
|
||||
self.create_inventory(imac.id, 3)
|
||||
self.create_inventory(prod.id, 3)
|
||||
|
||||
# Create a product with BOM
|
||||
p1 = self.product_model.create({
|
||||
@@ -456,15 +366,13 @@ class TestPotentialQty(TransactionCase):
|
||||
'product_tmpl_id': p1.product_tmpl_id.id,
|
||||
'product_id': p1.id,
|
||||
'product_qty': 1,
|
||||
'product_uom': self.ref('product.product_uom_unit'),
|
||||
})
|
||||
|
||||
# Need 1 iMac for that
|
||||
# Need 1 prod for that
|
||||
self.bom_line_model.create({
|
||||
'bom_id': bom_p1.id,
|
||||
'product_id': imac.id,
|
||||
'product_id': prod.id,
|
||||
'product_qty': 1,
|
||||
'product_uom': self.ref('product.product_uom_unit'),
|
||||
})
|
||||
|
||||
# Default component is qty_available
|
||||
@@ -479,25 +387,23 @@ class TestPotentialQty(TransactionCase):
|
||||
self.assertEqual(0.0, p1.potential_qty)
|
||||
|
||||
# If iMac has a Bom and can be manufactured
|
||||
imac_component = self.product_model.create({
|
||||
'name': 'iMac component',
|
||||
component = self.product_model.create({
|
||||
'name': 'component',
|
||||
})
|
||||
self.create_inventory(imac_component.id, 5)
|
||||
self.create_inventory(component.id, 5)
|
||||
|
||||
imac_bom = self.bom_model.create({
|
||||
'product_tmpl_id': imac.product_tmpl_id.id,
|
||||
'product_id': imac.id,
|
||||
'product_tmpl_id': prod.product_tmpl_id.id,
|
||||
'product_id': prod.id,
|
||||
'product_qty': 1,
|
||||
'product_uom': self.ref('product.product_uom_unit'),
|
||||
'type': 'phantom',
|
||||
})
|
||||
|
||||
# Need 1 imac_component for iMac
|
||||
# Need 1 component for prod
|
||||
self.bom_line_model.create({
|
||||
'bom_id': imac_bom.id,
|
||||
'product_id': imac_component.id,
|
||||
'product_id': component.id,
|
||||
'product_qty': 1,
|
||||
'product_uom': self.ref('product.product_uom_unit'),
|
||||
})
|
||||
|
||||
p1.refresh()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<odoo>
|
||||
<!-- Add the quantity available to promise in the product form -->
|
||||
<record id="view_product_form_potential_qty" model="ir.ui.view">
|
||||
<field name="name">Potential quantity on product form</field>
|
||||
@@ -23,5 +22,4 @@
|
||||
</data>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user