Migrate stock_available_mrp to v12

This commit is contained in:
Florian da Costa
2019-07-06 17:28:05 +02:00
parent e6a9e9bf43
commit 7d864341dd
7 changed files with 101 additions and 58 deletions

View File

@@ -2,7 +2,7 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{ {
'name': 'Consider the production potential is available to promise', 'name': 'Consider the production potential is available to promise',
'version': '11.0.1.0.0', 'version': '12.0.1.0.0',
"author": "Numérigraphe," "author": "Numérigraphe,"
"Odoo Community Association (OCA)", "Odoo Community Association (OCA)",
'website': 'https://github.com/OCA/stock-logistics-warehouse', 'website': 'https://github.com/OCA/stock-logistics-warehouse',

View File

@@ -3,11 +3,11 @@
<record id="product_kit_1a" model="product.product"> <record id="product_kit_1a" model="product.product">
<field name="default_code">PCSC234-WHITE</field> <field name="default_code">PCSC234-WHITE</field>
<field name="product_tmpl_id" <field name="product_tmpl_id"
ref="mrp.product_product_build_kit_product_template"/> ref="mrp.product_product_table_kit_product_template"/>
<field name="attribute_value_ids" eval="[(6,0,[ref('product.product_attribute_value_3')])]"/> <field name="attribute_value_ids" eval="[(6,0,[ref('product.product_attribute_value_3')])]"/>
</record> </record>
<record id="mrp.product_product_build_kit" model="product.product"> <record id="mrp.product_product_table_kit" model="product.product">
<field name="attribute_value_ids" <field name="attribute_value_ids"
eval="[(6,0,[ref('product.product_attribute_value_4')])]"/> eval="[(6,0,[ref('product.product_attribute_value_4')])]"/>
</record> </record>
@@ -16,21 +16,21 @@
<field name="attribute_value_ids" eval="[(6,0,[ref('product.product_attribute_value_4')])]"/> <field name="attribute_value_ids" eval="[(6,0,[ref('product.product_attribute_value_4')])]"/>
</record> </record>
<record id="product_product_9_white" model="product.product"> <record id="product_computer_desk_bolt_white" model="product.product">
<field name="name">Apple Wireless Keyboard</field> <field name="name">Bolt</field>
<field name="categ_id" ref="product.product_category_5"/> <field name="categ_id" ref="product.product_category_5"/>
<field name="standard_price">10.0</field> <field name="standard_price">1.0</field>
<field name="list_price">47.0</field> <field name="list_price">5.0</field>
<field name="type">product</field> <field name="type">product</field>
<field name="uom_id" ref="product.product_uom_unit"/> <field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="product.product_uom_unit"/> <field name="uom_po_id" ref="uom.product_uom_unit"/>
<field name="default_code">E-COM10-WHITE</field> <field name="default_code">BOLT-WHITE</field>
</record> </record>
<record id="mrp_bom_kit_line_3" model="mrp.bom.line"> <record id="mrp_bom_kit_line_3" model="mrp.bom.line">
<field name="product_id" ref="product_product_9_white"/> <field name="product_id" ref="product_computer_desk_bolt_white"/>
<field name="product_qty">1</field> <field name="product_qty">4</field>
<field name="product_uom_id" ref="product.product_uom_unit"/> <field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="sequence">5</field> <field name="sequence">5</field>
<field name="bom_id" ref="mrp.mrp_bom_kit"/> <field name="bom_id" ref="mrp.mrp_bom_kit"/>
<field name="attribute_value_ids" eval="[(6,0,[ref('product.product_attribute_value_3')])]"/> <field name="attribute_value_ids" eval="[(6,0,[ref('product.product_attribute_value_3')])]"/>

View File

@@ -13,7 +13,7 @@ class ProductProduct(models.Model):
bom_id = fields.Many2one( bom_id = fields.Many2one(
'mrp.bom', 'mrp.bom',
compute='_compute_bom_id', compute='_compute_bom_id',
string='Bill of Materials' string='BOM'
) )
@api.depends('virtual_available', 'bom_id', 'bom_id.product_qty') @api.depends('virtual_available', 'bom_id', 'bom_id.product_qty')

View File

@@ -0,0 +1,5 @@
* Loïc Bellier (Numérigraphe) <lb@numerigraphe.com>
* 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>

View File

@@ -0,0 +1,6 @@
This module takes the potential quantities available for Products into account in
the quantity available to promise, where the "Potential quantity" is the
quantity that can be manufactured with the components immediately at hand.
By configuration, the "Potential quantity" can be computed based on other product field.
For example, "Potential quantity" can be the quantity that can be manufactured
with the components available to promise.

View File

@@ -0,0 +1,24 @@
Known issues
------------
The manufacturing delays are not taken into account : this module assumes that
if you have components in stock goods, you can manufacture finished goods
quickly enough.
As a consequence, and to avoid overestimating, **only the first level** of Bill of Materials is
considered.
However Sets (a.k.a "phantom" BoMs) are taken into account: if a component must be replaced with a set, it's the stock of the set's product which will decide the potential.
If a product has several variants, only the variant with the biggest potential will be taken into account when reporting the production potential.
For example, even if you actually have enough components to make 10 iPads 16Go AND 42 iPads 32Go, we'll consider that you can promise only 42 iPads.
Removed features
----------------
Previous versions of this module used to let programmers demand to get the potential quantity in an arbitrary Unit of Measure using the `context`. This feature was present in the standard computations too until v8.0, but it has been dropped from the standard from v8.0 on.
For the sake of consistency the potential quantity is now always reported in the product's main Unit of Measure too.
Roadmap
-------
Possible improvements for future versions:
* take manufacturing delays into account: we should not promise goods to customers if they want them delivered earlier that we can make them
* Compute the quantity of finished product that can be made directly on each Bill of Material: this would be useful for production managers, and may make the computations faster by avoiding to compute the same BoM several times when several variants share the same BoM
* add an option (probably as a sub-module) to consider all raw materials as available if they can be bought from the suppliers in time for the manufacturing.

View File

@@ -29,20 +29,28 @@ class TestPotentialQty(TransactionCase):
def setup_demo_data(self): def setup_demo_data(self):
#  An interesting product (multi-line BoM, variants) #  An interesting product (multi-line BoM, variants)
self.tmpl = self.browse_ref( self.tmpl = self.browse_ref(
'mrp.product_product_build_kit_product_template') 'mrp.product_product_table_kit_product_template')
#  First variant #  First variant
self.var1 = self.browse_ref('mrp.product_product_build_kit') self.var1 = self.browse_ref('mrp.product_product_table_kit')
self.var1.type = 'product' self.var1.type = 'product'
#  Second variant #  Second variant
self.var2 = self.browse_ref( self.var2 = self.browse_ref(
'stock_available_mrp.product_kit_1a') 'stock_available_mrp.product_kit_1a')
self.var2.type = 'product' self.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_moves = self.env['stock.move'].search(
[('product_id', '=', bolt.id),
('state', 'not in', ('done', 'cancel'))])
bolt_moves._do_unreserve()
bolt.type = 'product'
# Components that can be used to make the product # Components that can be used to make the product
component_ids = [ component_ids = [
# KeyBoard # Bolt
self.ref('product.product_product_9'), bolt.id,
# Mouse # Wood Panel
self.ref('product.product_product_12'), self.ref('mrp.product_product_wood_panel'),
] ]
# Zero-out the inventory of all variants and components # Zero-out the inventory of all variants and components
@@ -81,7 +89,7 @@ class TestPotentialQty(TransactionCase):
'location_id': location_id, 'location_id': location_id,
'product_qty': qty 'product_qty': qty
}) })
inventory.action_done() inventory._action_done()
def create_simple_bom(self, product, sub_product, def create_simple_bom(self, product, sub_product,
product_qty=1, sub_product_qty=1, product_qty=1, sub_product_qty=1,
@@ -118,7 +126,7 @@ class TestPotentialQty(TransactionCase):
def test_potential_qty_no_bom_for_company(self): def test_potential_qty_no_bom_for_company(self):
chicago_id = self.ref('stock.res_company_1') chicago_id = self.ref('stock.res_company_1')
# Receive 1000x CPUI5s owned by Chicago # Receive 1000x Wood Panel owned by Chicago
inventory = self.env['stock.inventory'].create( inventory = self.env['stock.inventory'].create(
{'name': 'Receive CPUa8', {'name': 'Receive CPUa8',
'company_id': chicago_id, 'company_id': chicago_id,
@@ -128,12 +136,12 @@ class TestPotentialQty(TransactionCase):
self.env['stock.inventory.line'].create( self.env['stock.inventory.line'].create(
{'inventory_id': inventory.id, {'inventory_id': inventory.id,
'company_id': chicago_id, 'company_id': chicago_id,
'product_id': self.ref('product.product_product_9'), 'product_id': self.ref('mrp.product_product_wood_panel'),
'location_id': self.wh_ch.lot_stock_id.id, 'location_id': self.wh_ch.lot_stock_id.id,
'product_qty': 1000.0}) 'product_qty': 1000.0})
inventory.action_done() inventory._action_done()
# Put RAM-SR5 owned by Chicago for 1000x the 1st variant in main WH # Put Bolt owned by Chicago for 1000x the 1st variant in main WH
inventory = self.env['stock.inventory'].create( inventory = self.env['stock.inventory'].create(
{'name': 'components for 1st variant', {'name': 'components for 1st variant',
'company_id': chicago_id, 'company_id': chicago_id,
@@ -143,12 +151,12 @@ class TestPotentialQty(TransactionCase):
self.env['stock.inventory.line'].create( self.env['stock.inventory.line'].create(
{'inventory_id': inventory.id, {'inventory_id': inventory.id,
'company_id': chicago_id, 'company_id': chicago_id,
'product_id': self.ref('product.product_product_12'), 'product_id': self.ref('mrp.product_product_computer_desk_bolt'),
'location_id': self.wh_ch.lot_stock_id.id, 'location_id': self.wh_ch.lot_stock_id.id,
'product_qty': 1000.0}) 'product_qty': 1000.0})
inventory.action_done() inventory._action_done()
self.assertPotentialQty( self.assertPotentialQty(
self.tmpl, 1000.0, self.tmpl, 250.0,
"Wrong template potential after receiving components") "Wrong template potential after receiving components")
test_user = self.env['res.users'].create( test_user = self.env['res.users'].create(
@@ -164,7 +172,7 @@ class TestPotentialQty(TransactionCase):
test_user_tmpl = self.tmpl.sudo(test_user) test_user_tmpl = self.tmpl.sudo(test_user)
self.assertPotentialQty( self.assertPotentialQty(
test_user_tmpl, 1000.0, test_user_tmpl, 250.0,
"Simple user can access to the potential_qty") "Simple user can access to the potential_qty")
# Set the bom on the main company (visible to members of main company) # Set the bom on the main company (visible to members of main company)
@@ -181,7 +189,7 @@ class TestPotentialQty(TransactionCase):
"company or company child of the bom's company") "company or company child of the bom's company")
bom.company_id = chicago_id bom.company_id = chicago_id
self.assertPotentialQty( self.assertPotentialQty(
test_user_tmpl, 1000.0, '') test_user_tmpl, 250.0, '')
def test_potential_qty(self): def test_potential_qty(self):
for i in [self.tmpl, self.var1, self.var2]: for i in [self.tmpl, self.var1, self.var2]:
@@ -189,7 +197,7 @@ class TestPotentialQty(TransactionCase):
i, 0.0, i, 0.0,
"The potential quantity should start at 0") "The potential quantity should start at 0")
# Receive 1000x Mouses # Receive 1000x Wood Panel
inventory = self.env['stock.inventory'].create( inventory = self.env['stock.inventory'].create(
{'name': 'Receive Mouses', {'name': 'Receive Mouses',
'location_id': self.wh_main.lot_stock_id.id, 'location_id': self.wh_main.lot_stock_id.id,
@@ -197,17 +205,17 @@ class TestPotentialQty(TransactionCase):
inventory.action_start() inventory.action_start()
self.env['stock.inventory.line'].create( self.env['stock.inventory.line'].create(
{'inventory_id': inventory.id, {'inventory_id': inventory.id,
'product_id': self.ref('product.product_product_12'), 'product_id': self.ref('mrp.product_product_wood_panel'),
'location_id': self.wh_main.lot_stock_id.id, 'location_id': self.wh_main.lot_stock_id.id,
'product_qty': 1000.0}) 'product_qty': 1000.0})
inventory.action_done() inventory._action_done()
for i in [self.tmpl, self.var1, self.var2]: for i in [self.tmpl, self.var1, self.var2]:
self.assertPotentialQty( self.assertPotentialQty(
i, 0.0, i, 0.0,
"Receiving a single component should not change the " "Receiving a single component should not change the "
"potential of %s" % i) "potential of %s" % i)
# Receive enough keyboard to make 1000x the 1st variant in main WH # Receive enough bolt to make 1000x the 1st variant in main WH
inventory = self.env['stock.inventory'].create( inventory = self.env['stock.inventory'].create(
{'name': 'components for 1st variant', {'name': 'components for 1st variant',
'location_id': self.wh_main.lot_stock_id.id, 'location_id': self.wh_main.lot_stock_id.id,
@@ -216,22 +224,22 @@ class TestPotentialQty(TransactionCase):
self.env['stock.inventory.line'].create( self.env['stock.inventory.line'].create(
{'inventory_id': inventory.id, {'inventory_id': inventory.id,
'product_id': self.ref( 'product_id': self.ref(
'product.product_product_9'), 'mrp.product_product_computer_desk_bolt'),
'location_id': self.wh_main.lot_stock_id.id, 'location_id': self.wh_main.lot_stock_id.id,
'product_qty': 1000.0}) 'product_qty': 1000.0})
inventory.action_done() inventory._action_done()
self.assertPotentialQty( self.assertPotentialQty(
self.tmpl, 1000.0, self.tmpl, 250.0,
"Wrong template potential after receiving components") "Wrong template potential after receiving components")
self.assertPotentialQty( self.assertPotentialQty(
self.var1, 1000.0, self.var1, 250.0,
"Wrong variant 1 potential after receiving components") "Wrong variant 1 potential after receiving components")
self.assertPotentialQty( self.assertPotentialQty(
self.var2, 0.0, self.var2, 0.0,
"Receiving variant 1's component should not change " "Receiving variant 1's component should not change "
"variant 2's potential") "variant 2's potential")
# Receive enough components to make 313 the 2nd variant at Chicago # Receive enough components to make 213 the 2nd variant at Chicago
inventory = self.env['stock.inventory'].create( inventory = self.env['stock.inventory'].create(
{'name': 'components for 2nd variant', {'name': 'components for 2nd variant',
'location_id': self.wh_ch.lot_stock_id.id, 'location_id': self.wh_ch.lot_stock_id.id,
@@ -239,64 +247,64 @@ class TestPotentialQty(TransactionCase):
inventory.action_start() inventory.action_start()
self.env['stock.inventory.line'].create( self.env['stock.inventory.line'].create(
{'inventory_id': inventory.id, {'inventory_id': inventory.id,
'product_id': self.ref('product.product_product_12'), 'product_id': self.ref('mrp.product_product_wood_panel'),
'location_id': self.wh_ch.lot_stock_id.id, 'location_id': self.wh_ch.lot_stock_id.id,
'product_qty': 1000.0}) 'product_qty': 1000.0})
self.env['stock.inventory.line'].create( self.env['stock.inventory.line'].create(
{'inventory_id': inventory.id, {'inventory_id': inventory.id,
'product_id': self.ref( 'product_id': self.ref(
'stock_available_mrp.product_product_9_white'), 'stock_available_mrp.product_computer_desk_bolt_white'),
'location_id': self.wh_ch.lot_stock_id.id, 'location_id': self.wh_ch.lot_stock_id.id,
'product_qty': 313.0}) 'product_qty': 852.0})
inventory.action_done() inventory._action_done()
self.assertPotentialQty( self.assertPotentialQty(
self.tmpl, 1000.0, self.tmpl.with_context(test=True), 250.0,
"Wrong template potential after receiving components") "Wrong template potential after receiving components")
self.assertPotentialQty( self.assertPotentialQty(
self.var1, 1000.0, self.var1, 250.0,
"Receiving variant 2's component should not change " "Receiving variant 2's component should not change "
"variant 1's potential") "variant 1's potential")
self.assertPotentialQty( self.assertPotentialQty(
self.var2, 313.0, self.var2, 213.0,
"Wrong variant 2 potential after receiving components") "Wrong variant 2 potential after receiving components")
# Check by warehouse # Check by warehouse
self.assertPotentialQty( self.assertPotentialQty(
self.tmpl.with_context(warehouse=self.wh_main.id), 1000.0, self.tmpl.with_context(warehouse=self.wh_main.id), 250.0,
"Wrong potential quantity in main WH") "Wrong potential quantity in main WH")
self.assertPotentialQty( self.assertPotentialQty(
self.tmpl.with_context(warehouse=self.wh_ch.id), 313.0, self.tmpl.with_context(warehouse=self.wh_ch.id), 213.0,
"Wrong potential quantity in Chicago WH") "Wrong potential quantity in Chicago WH")
# Check by location # Check by location
self.assertPotentialQty( self.assertPotentialQty(
self.tmpl.with_context( self.tmpl.with_context(
location=self.wh_main.lot_stock_id.id), 1000.0, location=self.wh_main.lot_stock_id.id), 250.0,
"Wrong potential quantity in main WH location") "Wrong potential quantity in main WH location")
self.assertPotentialQty( self.assertPotentialQty(
self.tmpl.with_context( self.tmpl.with_context(
location=self.wh_ch.lot_stock_id.id), location=self.wh_ch.lot_stock_id.id),
313.0, 213.0,
"Wrong potential quantity in Chicago WH location") "Wrong potential quantity in Chicago WH location")
def test_multi_unit_recursive_bom(self): def test_multi_unit_recursive_bom(self):
# Test multi-level and multi-units BOM # Test multi-level and multi-units BOM
uom_unit = self.env.ref('product.product_uom_unit') uom_unit = self.env.ref('uom.product_uom_unit')
uom_unit.rounding = 1.0 uom_unit.rounding = 1.0
p1 = self.product_model.create({ p1 = self.product_model.create({
'name': 'Test product with BOM', 'name': 'Test product with BOM',
'type': 'product', 'type': 'product',
'uom_id': self.env.ref('product.product_uom_unit').id, 'uom_id': self.env.ref('uom.product_uom_unit').id,
}) })
p2 = self.product_model.create({ p2 = self.product_model.create({
'name': 'Test sub product with BOM', 'name': 'Test sub product with BOM',
'type': 'product', 'type': 'product',
'uom_id': self.env.ref('product.product_uom_unit').id, 'uom_id': self.env.ref('uom.product_uom_unit').id,
}) })
p3 = self.product_model.create({ p3 = self.product_model.create({
'name': 'Test component', 'name': 'Test component',
'type': 'product', 'type': 'product',
'uom_id': self.env.ref('product.product_uom_unit').id, 'uom_id': self.env.ref('uom.product_uom_unit').id,
}) })
bom_p1 = self.bom_model.create({ bom_p1 = self.bom_model.create({
@@ -308,7 +316,7 @@ class TestPotentialQty(TransactionCase):
'bom_id': bom_p1.id, 'bom_id': bom_p1.id,
'product_id': p3.id, 'product_id': p3.id,
'product_qty': 1, 'product_qty': 1,
'product_uom_id': self.env.ref('product.product_uom_unit').id, 'product_uom_id': self.env.ref('uom.product_uom_unit').id,
}) })
@@ -317,7 +325,7 @@ class TestPotentialQty(TransactionCase):
'bom_id': bom_p1.id, 'bom_id': bom_p1.id,
'product_id': p2.id, 'product_id': p2.id,
'product_qty': 2, 'product_qty': 2,
'product_uom_id': self.env.ref('product.product_uom_unit').id, 'product_uom_id': self.env.ref('uom.product_uom_unit').id,
}) })
@@ -332,7 +340,7 @@ class TestPotentialQty(TransactionCase):
'bom_id': bom_p2.id, 'bom_id': bom_p2.id,
'product_id': p3.id, 'product_id': p3.id,
'product_qty': 2, 'product_qty': 2,
'product_uom_id': self.env.ref('product.product_uom_unit').id, 'product_uom_id': self.env.ref('uom.product_uom_unit').id,
}) })
@@ -365,7 +373,7 @@ class TestPotentialQty(TransactionCase):
# Test to change component stock for compute BOM stock # Test to change component stock for compute BOM stock
# Get a demo product with outgoing move (qty: 3) # Get a demo product with outgoing move (qty: 3)
prod = self.browse_ref('product.product_product_20') prod = self.browse_ref('product.product_product_16')
# Set on hand qty # Set on hand qty
self.create_inventory(prod.id, 3) self.create_inventory(prod.id, 3)