mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
Merge pull request #148 from cyrilgdn/9.0-stock_available_mrp
[9.0] stock_available_mrp migration
This commit is contained in:
@@ -1,30 +1,64 @@
|
||||
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
|
||||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||
:alt: License: AGPL-3
|
||||
|
||||
=========================================================
|
||||
Consider the production potential is available to promise
|
||||
=========================================================
|
||||
|
||||
This module takes the potential quantities available for Products in account in
|
||||
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.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
|
||||
:alt: Try me on Runbot
|
||||
:target: https://runbot.odoo-community.org/runbot/153/8.0
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
|
||||
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.
|
||||
To avoid overestimating, **only the first level** of Bill of Materials is
|
||||
|
||||
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.
|
||||
|
||||
* include all levels of BoM, using `bom_explode`. @gdgellatly gave an example
|
||||
of how to do it here: https://github.com/OCA/stock-logistics-warehouse/pull/5#issuecomment-66902191
|
||||
Ideally, we will want to take manufacturing delays into account: we can't
|
||||
promiss goods to customers if they want them delivered earlier that we can
|
||||
make them
|
||||
* 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.
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues
|
||||
<https://github.com/OCA/stock-logistics-warehouse/issues>`_. In case of trouble, please
|
||||
check there if your issue has already been reported. If you spotted it first,
|
||||
help us smashing it by providing a detailed and welcomed `feedback
|
||||
<https://github.com/OCA/
|
||||
stock-logistics-warehouse/issues/new?body=module:%20
|
||||
stock_available%0Aversion:%20
|
||||
8.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
Credits
|
||||
=======
|
||||
@@ -33,16 +67,20 @@ Contributors
|
||||
------------
|
||||
* 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>
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
|
||||
.. image:: http://odoo-community.org/logo.png
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: http://odoo-community.org
|
||||
:target: https://odoo-community.org
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
To contribute to this module, please visit http://odoo-community.org.
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# This module is copyright (C) 2014 Numérigraphe SARL. All Rights Reserved.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
# © 2014 Numérigraphe SARL
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from . import product
|
||||
from . import models
|
||||
|
||||
@@ -1,35 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# This module is copyright (C) 2014 Numérigraphe SARL. All Rights Reserved.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# © 2014 Numérigraphe SARL, Camptocamp
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
{
|
||||
'name': 'Consider the production potential is available to promise',
|
||||
'version': '2.0',
|
||||
"author": u"Numérigraphe,Odoo Community Association (OCA)",
|
||||
'version': '9.0.1.0.0',
|
||||
"author": u"Numérigraphe,"
|
||||
u"Odoo Community Association (OCA)",
|
||||
'category': 'Hidden',
|
||||
'depends': ['stock_available', 'mrp'],
|
||||
'data': [
|
||||
'product_view.xml',
|
||||
'depends': [
|
||||
'stock_available',
|
||||
'mrp'
|
||||
],
|
||||
'test': [
|
||||
'test/potential_qty.yml',
|
||||
'data': [
|
||||
'views/product_template_view.xml',
|
||||
],
|
||||
'demo': [
|
||||
'demo/mrp_bom.yml',
|
||||
],
|
||||
'license': 'AGPL-3',
|
||||
'installable': False
|
||||
'installable': True,
|
||||
}
|
||||
|
||||
27
stock_available_mrp/demo/mrp_bom.yml
Normal file
27
stock_available_mrp/demo/mrp_bom.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
- 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
|
||||
34
stock_available_mrp/i18n/de.po
Normal file
34
stock_available_mrp/i18n/de.po
Normal file
@@ -0,0 +1,34 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * stock_available_mrp
|
||||
#
|
||||
# Translators:
|
||||
# Rudolf Schnapka <rs@techno-flex.de>, 2016
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: stock-logistics-warehouse (8.0)\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-01-14 01:38+0000\n"
|
||||
"PO-Revision-Date: 2016-01-14 09:35+0000\n"
|
||||
"Last-Translator: Rudolf Schnapka <rs@techno-flex.de>\n"
|
||||
"Language-Team: German (http://www.transifex.com/oca/OCA-stock-logistics-warehouse-8-0/language/de/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: de\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#. module: stock_available_mrp
|
||||
#: model:ir.model,name:stock_available_mrp.model_product_product
|
||||
msgid "Product"
|
||||
msgstr "Produkt"
|
||||
|
||||
#. module: stock_available_mrp
|
||||
#: model:ir.model,name:stock_available_mrp.model_product_template
|
||||
msgid "Product Template"
|
||||
msgstr "Produktvorlage"
|
||||
|
||||
#. module: stock_available_mrp
|
||||
#: model:product.uom,name:stock_available_mrp.thousand
|
||||
msgid "Thousand"
|
||||
msgstr "Tausend"
|
||||
33
stock_available_mrp/i18n/es.po
Normal file
33
stock_available_mrp/i18n/es.po
Normal file
@@ -0,0 +1,33 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * stock_available_mrp
|
||||
#
|
||||
# Translators:
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: stock-logistics-warehouse (8.0)\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-01-14 01:38+0000\n"
|
||||
"PO-Revision-Date: 2016-01-13 16:35+0000\n"
|
||||
"Last-Translator: <>\n"
|
||||
"Language-Team: Spanish (http://www.transifex.com/oca/OCA-stock-logistics-warehouse-8-0/language/es/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: es\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#. module: stock_available_mrp
|
||||
#: model:ir.model,name:stock_available_mrp.model_product_product
|
||||
msgid "Product"
|
||||
msgstr "Producto"
|
||||
|
||||
#. module: stock_available_mrp
|
||||
#: model:ir.model,name:stock_available_mrp.model_product_template
|
||||
msgid "Product Template"
|
||||
msgstr "Plantilla de producto"
|
||||
|
||||
#. module: stock_available_mrp
|
||||
#: model:product.uom,name:stock_available_mrp.thousand
|
||||
msgid "Thousand"
|
||||
msgstr ""
|
||||
33
stock_available_mrp/i18n/fi.po
Normal file
33
stock_available_mrp/i18n/fi.po
Normal file
@@ -0,0 +1,33 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * stock_available_mrp
|
||||
#
|
||||
# Translators:
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: stock-logistics-warehouse (8.0)\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-01-14 01:38+0000\n"
|
||||
"PO-Revision-Date: 2016-01-13 16:35+0000\n"
|
||||
"Last-Translator: <>\n"
|
||||
"Language-Team: Finnish (http://www.transifex.com/oca/OCA-stock-logistics-warehouse-8-0/language/fi/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: fi\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#. module: stock_available_mrp
|
||||
#: model:ir.model,name:stock_available_mrp.model_product_product
|
||||
msgid "Product"
|
||||
msgstr "Tuote"
|
||||
|
||||
#. module: stock_available_mrp
|
||||
#: model:ir.model,name:stock_available_mrp.model_product_template
|
||||
msgid "Product Template"
|
||||
msgstr "Tuotteen malli"
|
||||
|
||||
#. module: stock_available_mrp
|
||||
#: model:product.uom,name:stock_available_mrp.thousand
|
||||
msgid "Thousand"
|
||||
msgstr ""
|
||||
@@ -1,34 +1,33 @@
|
||||
# Translation of OpenERP Server.
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * stock_available_mrp
|
||||
#
|
||||
# * stock_available_mrp
|
||||
#
|
||||
# Translators:
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: OpenERP Server 7.0\n"
|
||||
"Project-Id-Version: stock-logistics-warehouse (8.0)\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2014-07-30 19:29+0000\n"
|
||||
"PO-Revision-Date: 2014-07-30 19:29+0000\n"
|
||||
"POT-Creation-Date: 2016-01-14 01:38+0000\n"
|
||||
"PO-Revision-Date: 2016-01-13 16:35+0000\n"
|
||||
"Last-Translator: <>\n"
|
||||
"Language-Team: \n"
|
||||
"Language-Team: French (http://www.transifex.com/oca/OCA-stock-logistics-warehouse-8-0/language/fr/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
"Language: fr\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#. module: stock_available_mrp
|
||||
#: field:product.product,potential_qty:0
|
||||
msgid "Potential"
|
||||
msgstr "Potentiel"
|
||||
|
||||
#. module: stock_available_mrp
|
||||
#: code:_description:0
|
||||
#: model:ir.model,name:stock_available_mrp.model_product_product
|
||||
#, python-format
|
||||
msgid "Product"
|
||||
msgstr "Article"
|
||||
|
||||
#. module: stock_available_mrp
|
||||
#: help:product.product,potential_qty:0
|
||||
msgid "Quantity of this Product that could be produced using the materials already at hand, following a single level of the Bills of Materials."
|
||||
msgstr "Quantité de cet article que l'on pourrait produire en utilisant les produits déjà disponibles, en suivant un seul niveau de nomenclature."
|
||||
#: model:ir.model,name:stock_available_mrp.model_product_template
|
||||
msgid "Product Template"
|
||||
msgstr "Modèle de produit"
|
||||
|
||||
#. module: stock_available_mrp
|
||||
#: model:product.uom,name:stock_available_mrp.thousand
|
||||
msgid "Thousand"
|
||||
msgstr ""
|
||||
|
||||
34
stock_available_mrp/i18n/sl.po
Normal file
34
stock_available_mrp/i18n/sl.po
Normal file
@@ -0,0 +1,34 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * stock_available_mrp
|
||||
#
|
||||
# Translators:
|
||||
# Matjaž Mozetič <m.mozetic@matmoz.si>, 2016
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: stock-logistics-warehouse (8.0)\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-01-14 01:38+0000\n"
|
||||
"PO-Revision-Date: 2016-01-14 05:18+0000\n"
|
||||
"Last-Translator: Matjaž Mozetič <m.mozetic@matmoz.si>\n"
|
||||
"Language-Team: Slovenian (http://www.transifex.com/oca/OCA-stock-logistics-warehouse-8-0/language/sl/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: sl\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);\n"
|
||||
|
||||
#. module: stock_available_mrp
|
||||
#: model:ir.model,name:stock_available_mrp.model_product_product
|
||||
msgid "Product"
|
||||
msgstr "Proizvod"
|
||||
|
||||
#. module: stock_available_mrp
|
||||
#: model:ir.model,name:stock_available_mrp.model_product_template
|
||||
msgid "Product Template"
|
||||
msgstr "Predloga proizvoda"
|
||||
|
||||
#. module: stock_available_mrp
|
||||
#: model:product.uom,name:stock_available_mrp.thousand
|
||||
msgid "Thousand"
|
||||
msgstr "Tisoč"
|
||||
@@ -29,6 +29,5 @@ msgstr ""
|
||||
|
||||
#. module: stock_available_mrp
|
||||
#: help:product.product,potential_qty:0
|
||||
msgid "Quantity of this Product that could be produced using the materials already at hand, following a single level of the Bills of Materials."
|
||||
msgid "Quantity of this Product that could be produced using the materials already at hand."
|
||||
msgstr ""
|
||||
|
||||
|
||||
6
stock_available_mrp/models/__init__.py
Normal file
6
stock_available_mrp/models/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2014 Numérigraphe SARL
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from . import product_product
|
||||
from . import product_template
|
||||
134
stock_available_mrp/models/product_product.py
Normal file
134
stock_available_mrp/models/product_product.py
Normal file
@@ -0,0 +1,134 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 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
|
||||
|
||||
|
||||
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',
|
||||
)
|
||||
|
||||
@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.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']
|
||||
|
||||
for product in self:
|
||||
bom_id = bom_obj._bom_find(product_id=product.id)
|
||||
if not bom_id:
|
||||
product.potential_qty = 0.0
|
||||
continue
|
||||
|
||||
bom = bom_obj.browse(bom_id)
|
||||
|
||||
# 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
|
||||
|
||||
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
|
||||
"""
|
||||
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]
|
||||
|
||||
def _get_components_needs(self, product, bom):
|
||||
""" Return the needed qty of each compoments in the *bom* of *product*.
|
||||
|
||||
:type product: product_product
|
||||
:type bom: mrp_bom
|
||||
: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}
|
||||
)
|
||||
|
||||
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'])
|
||||
46
stock_available_mrp/models/product_template.py
Normal file
46
stock_available_mrp/models/product_template.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 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
|
||||
|
||||
|
||||
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])
|
||||
@@ -1,126 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# This module is copyright (C) 2014 Numérigraphe SARL. All Rights Reserved.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp import SUPERUSER_ID
|
||||
from openerp.osv import orm, fields
|
||||
import openerp.addons.decimal_precision as dp
|
||||
|
||||
|
||||
class product_product(orm.Model):
|
||||
"""Add the computation for the stock available to promise"""
|
||||
_inherit = 'product.product'
|
||||
|
||||
def _product_available(self, cr, uid, ids, field_names=None, arg=False,
|
||||
context=None):
|
||||
"""Quantity available to promise based on components at hand."""
|
||||
# Compute the core quantities
|
||||
res = super(product_product, self)._product_available(
|
||||
cr, uid, ids, field_names=field_names, arg=arg, context=context)
|
||||
# If we didn't get a field_names list, there's nothing to do
|
||||
if field_names is None:
|
||||
return res
|
||||
|
||||
if context is None:
|
||||
context = {}
|
||||
# Prepare an alternative context without 'uom', to avoid cross-category
|
||||
# conversions when reading the available stock of components
|
||||
if 'uom' in context:
|
||||
context_wo_uom = context.copy()
|
||||
del context_wo_uom['uom']
|
||||
else:
|
||||
context_wo_uom = context
|
||||
|
||||
# Compute the production capacity
|
||||
if any([f in field_names
|
||||
for f in ['potential_qty', 'immediately_usable_qty']]):
|
||||
# Compute the potential qty from BoMs with components available
|
||||
bom_obj = self.pool['mrp.bom']
|
||||
to_uom = 'uom' in context and self.pool['product.uom'].browse(
|
||||
cr, SUPERUSER_ID, context['uom'], context=context)
|
||||
|
||||
for product in self.browse(cr, uid, ids, context=context):
|
||||
# _bom_find() returns a single BoM id.
|
||||
# We will not check any other BoM for this product
|
||||
bom_id = bom_obj._bom_find(cr, SUPERUSER_ID, product.id,
|
||||
product.uom_id.id)
|
||||
if bom_id:
|
||||
min_qty = self._compute_potential_qty_from_bom(
|
||||
cr, uid, bom_id, to_uom or product.uom_id,
|
||||
context=context)
|
||||
|
||||
if 'potential_qty' in field_names:
|
||||
res[product.id]['potential_qty'] += min_qty
|
||||
if 'immediately_usable_qty' in field_names:
|
||||
res[product.id]['immediately_usable_qty'] += min_qty
|
||||
|
||||
return res
|
||||
|
||||
def _compute_potential_qty_from_bom(self, cr, uid, bom_id, to_uom,
|
||||
context=None):
|
||||
"""Compute the potential qty from BoMs with components available"""
|
||||
bom_obj = self.pool['mrp.bom']
|
||||
uom_obj = self.pool['product.uom']
|
||||
if context is None:
|
||||
context = {}
|
||||
if 'uom' in context:
|
||||
context_wo_uom = context.copy()
|
||||
del context_wo_uom['uom']
|
||||
else:
|
||||
context_wo_uom = context
|
||||
min_qty = False
|
||||
# Browse ignoring the UoM context to avoid cross-category conversions
|
||||
bom = bom_obj.browse(
|
||||
cr, uid, [bom_id], context=context_wo_uom)[0]
|
||||
|
||||
# store id of final product uom
|
||||
|
||||
for component in bom.bom_lines:
|
||||
# qty available in BOM line's UoM
|
||||
# XXX use context['uom'] instead?
|
||||
stock_component_qty = uom_obj._compute_qty_obj(
|
||||
cr, uid,
|
||||
component.product_id.uom_id,
|
||||
component.product_id.virtual_available,
|
||||
component.product_uom)
|
||||
# qty we can produce with this component, in the BoM's UoM
|
||||
bom_uom_qty = (stock_component_qty // component.product_qty
|
||||
) * bom.product_qty
|
||||
# Convert back to the reporting default UoM
|
||||
stock_product_uom_qty = uom_obj._compute_qty_obj(
|
||||
cr, uid, bom.product_uom, bom_uom_qty,
|
||||
to_uom)
|
||||
if min_qty is False:
|
||||
min_qty = stock_product_uom_qty
|
||||
elif stock_product_uom_qty < min_qty:
|
||||
min_qty = stock_product_uom_qty
|
||||
if min_qty < 0.0:
|
||||
min_qty = 0.0
|
||||
return min_qty
|
||||
|
||||
_columns = {
|
||||
'potential_qty': fields.function(
|
||||
_product_available, method=True, multi='qty_available',
|
||||
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, following a single level "
|
||||
"of the Bills of Materials."),
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<!-- Add the quantity available to promise in the product form -->
|
||||
<record id="view_product_form_potential_qty" model="ir.ui.view">
|
||||
<field name="name">product.form.potential_qty</field>
|
||||
<field name="model">product.product</field>
|
||||
<field name="type">form</field>
|
||||
<field name="inherit_id" ref="stock.view_normal_procurement_locations_form" />
|
||||
<field name="arch" type="xml">
|
||||
<data>
|
||||
<xpath expr="//field[@name='virtual_available']" position="after">
|
||||
<field name="potential_qty"/>
|
||||
</xpath>
|
||||
</data>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -1,70 +0,0 @@
|
||||
- Test the computation of the potential quantity on product_product_16, a product with several multi-line BoMs
|
||||
|
||||
- Create a UoM in the category of PCE
|
||||
- !record {model: product.uom, id: thousand}:
|
||||
name: Thousand
|
||||
factor: 0.001
|
||||
rounding: 0.0001
|
||||
uom_type: bigger
|
||||
category_id: product.product_uom_categ_unit
|
||||
|
||||
- Receive enough of the first component to run the BoM 1000x, and check that the potential is unchanged
|
||||
- !python {model: mrp.bom}: |
|
||||
bom = self.browse(
|
||||
cr, uid,
|
||||
self._bom_find(
|
||||
cr, uid, ref('product.product_product_16'),
|
||||
ref('product.product_uom_unit')))
|
||||
assert len(bom.bom_lines)>1, "The test BoM has a single line, two or more are needed for the test"
|
||||
initial_qty = bom.product_id.potential_qty
|
||||
component = bom.bom_lines[0]
|
||||
assert component.product_uom.category_id.id == ref('product.product_uom_categ_unit'), "The first component's UoM is in the wrong category can't test"
|
||||
self.pool['stock.move'].create(
|
||||
cr, uid,
|
||||
{
|
||||
'name': 'Receive first component',
|
||||
'product_id': component.product_id.id,
|
||||
'product_qty': component.product_qty * 1000.0,
|
||||
'product_uom': component.product_id.uom_id.id,
|
||||
'location_id': ref('stock.stock_location_suppliers'),
|
||||
'location_dest_id': ref('stock.stock_location_stock'),
|
||||
'state': 'done',
|
||||
})
|
||||
# Re-read the potential quantity
|
||||
bom.refresh()
|
||||
new_qty = bom.product_id.potential_qty
|
||||
assert new_qty == initial_qty, "Receiving a single component should not change the potential qty (%s instead of %s)" % (new_qty, initial_qty)
|
||||
|
||||
- Receive enough of all the components to run the BoM 1000x and check that the potential is correct
|
||||
- !python {model: mrp.bom}: |
|
||||
# Select a BoM for product_product_16
|
||||
bom = self.browse(
|
||||
cr, uid,
|
||||
self._bom_find(
|
||||
cr, uid, ref('product.product_product_16'),
|
||||
ref('product.product_uom_unit')))
|
||||
assert len(bom.bom_lines)>1, "The test BoM has a single line, two or more are needed for the test"
|
||||
initial_qty = bom.product_id.potential_qty
|
||||
for component in bom.bom_lines:
|
||||
assert component.product_uom.category_id.id == ref('product.product_uom_categ_unit'), "The first component's UoM is in the wrong category, can't test"
|
||||
self.pool['stock.move'].create(
|
||||
cr, uid,
|
||||
{
|
||||
'name': 'Receive all components',
|
||||
'product_id': component.product_id.id,
|
||||
'product_qty': component.product_qty * 1000.0,
|
||||
'product_uom': component.product_id.uom_id.id,
|
||||
'location_id': ref('stock.stock_location_suppliers'),
|
||||
'location_dest_id': ref('stock.stock_location_stock'),
|
||||
'state': 'done',
|
||||
})
|
||||
# Re-read the potential quantity
|
||||
bom.refresh()
|
||||
new_qty = bom.product_id.potential_qty
|
||||
right_qty = initial_qty + bom.product_qty * 1000.0
|
||||
assert new_qty == right_qty, "The potential qty is incorrect after receiveing all the components (%s instead of %s)" % (new_qty, right_qty)
|
||||
# Re-read the potential quantity with a different UoM in the context
|
||||
new_qty = self.browse(
|
||||
cr, uid, bom.id, context={'uom': ref('thousand')}).product_id.potential_qty
|
||||
right_qty = initial_qty / 1000.0 + bom.product_qty
|
||||
assert abs(new_qty - right_qty) < 0.0001, "The potential qty is incorrect with another UoM in the context (%s instead of %s)" % (new_qty, right_qty)
|
||||
5
stock_available_mrp/tests/__init__.py
Normal file
5
stock_available_mrp/tests/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2014 Numérigraphe SARL
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from . import test_potential_qty
|
||||
540
stock_available_mrp/tests/test_potential_qty.py
Normal file
540
stock_available_mrp/tests/test_potential_qty.py
Normal file
@@ -0,0 +1,540 @@
|
||||
# -*- coding: utf-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
|
||||
|
||||
|
||||
class TestPotentialQty(TransactionCase):
|
||||
"""Test the potential quantity on a product with a multi-line BoM"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestPotentialQty, self).setUp()
|
||||
|
||||
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.setup_demo_data()
|
||||
|
||||
def setup_demo_data(self):
|
||||
# An interesting product (multi-line BoM, variants)
|
||||
self.tmpl = self.browse_ref(
|
||||
'product.product_product_4_product_template')
|
||||
# First variant
|
||||
self.var1 = self.browse_ref('product.product_product_4c')
|
||||
# Second variant
|
||||
self.var2 = self.browse_ref('product.product_product_4')
|
||||
# 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')]
|
||||
|
||||
# Zero-out the inventory of all variants and components
|
||||
for component_id in (
|
||||
component_ids + [v.id
|
||||
for v in self.tmpl.product_variant_ids]):
|
||||
inventory = self.env['stock.inventory'].create(
|
||||
{'name': 'no components: %s' % component_id,
|
||||
'location_id': self.ref('stock.stock_location_locations'),
|
||||
'filter': 'product',
|
||||
'product_id': component_id})
|
||||
inventory.prepare_inventory()
|
||||
inventory.reset_real_qty()
|
||||
inventory.action_done()
|
||||
|
||||
# A product without a BoM
|
||||
self.product_wo_bom = self.browse_ref('product.product_product_23')
|
||||
|
||||
# Record the initial quantity available for sale
|
||||
self.initial_usable_qties = {i.id: i.immediately_usable_qty
|
||||
for i in [self.tmpl,
|
||||
self.var1,
|
||||
self.var2,
|
||||
self.product_wo_bom]}
|
||||
|
||||
# Get the warehouses
|
||||
self.wh_main = self.browse_ref('stock.warehouse0')
|
||||
self.wh_ch = self.browse_ref('stock.stock_warehouse_shop0')
|
||||
|
||||
def create_inventory(self, product_id, qty, location_id=None):
|
||||
if location_id is None:
|
||||
location_id = self.wh_main.lot_stock_id.id
|
||||
|
||||
inventory = self.env['stock.inventory'].create({
|
||||
'name': 'Test inventory',
|
||||
'location_id': location_id,
|
||||
'filter': 'partial'
|
||||
})
|
||||
inventory.prepare_inventory()
|
||||
|
||||
self.env['stock.inventory.line'].create({
|
||||
'inventory_id': inventory.id,
|
||||
'product_id': product_id,
|
||||
'location_id': location_id,
|
||||
'product_qty': qty
|
||||
})
|
||||
inventory.action_done()
|
||||
|
||||
def create_simple_bom(self, product, sub_product,
|
||||
product_qty=1, sub_product_qty=1,
|
||||
routing_id=False):
|
||||
bom = self.bom_model.create({
|
||||
'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
|
||||
|
||||
def assertPotentialQty(self, record, qty, msg):
|
||||
record.refresh()
|
||||
# Check the potential
|
||||
self.assertEqual(record.potential_qty, qty, msg)
|
||||
# Check the variation of quantity available for sale
|
||||
self.assertEqual(
|
||||
(record.immediately_usable_qty -
|
||||
self.initial_usable_qties[record.id]), qty, msg)
|
||||
|
||||
def test_potential_qty_no_bom(self):
|
||||
# Check the potential when there's no BoM
|
||||
self.assertPotentialQty(
|
||||
self.product_wo_bom, 0.0,
|
||||
"The potential without a BoM should be 0")
|
||||
|
||||
def test_potential_qty_no_bom_for_company(self):
|
||||
chicago_id = self.ref('stock.res_company_1')
|
||||
|
||||
# Receive 1000x CPUa8s owned by Chicago
|
||||
inventory = self.env['stock.inventory'].create(
|
||||
{'name': 'Receive CPUa8',
|
||||
'company_id': chicago_id,
|
||||
'location_id': self.wh_ch.lot_stock_id.id,
|
||||
'filter': 'partial'})
|
||||
inventory.prepare_inventory()
|
||||
self.env['stock.inventory.line'].create(
|
||||
{'inventory_id': inventory.id,
|
||||
'company_id': chicago_id,
|
||||
'product_id': self.ref('product.product_product_23'),
|
||||
'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
|
||||
inventory = self.env['stock.inventory'].create(
|
||||
{'name': 'components for 1st variant',
|
||||
'company_id': chicago_id,
|
||||
'location_id': self.wh_ch.lot_stock_id.id,
|
||||
'filter': 'partial'})
|
||||
inventory.prepare_inventory()
|
||||
self.env['stock.inventory.line'].create(
|
||||
{'inventory_id': inventory.id,
|
||||
'company_id': chicago_id,
|
||||
'product_id': self.ref('product.product_product_15'),
|
||||
'location_id': self.wh_ch.lot_stock_id.id,
|
||||
'product_qty': 1000.0})
|
||||
inventory.action_done()
|
||||
self.assertPotentialQty(
|
||||
self.tmpl, 1000.0,
|
||||
"Wrong template potential after receiving components")
|
||||
|
||||
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')),
|
||||
(4, self.ref('mrp.group_mrp_user'))]})
|
||||
|
||||
bom = self.env['mrp.bom'].search(
|
||||
[('product_tmpl_id', '=', self.tmpl.id)])
|
||||
|
||||
test_user_tmpl = self.tmpl.sudo(test_user)
|
||||
self.assertPotentialQty(
|
||||
test_user_tmpl, 1000.0,
|
||||
"Simple user can access to the potential_qty")
|
||||
|
||||
# Set the bom on the main company (visible to members of main company)
|
||||
# and all products without company (visible to all)
|
||||
# and the demo user on Chicago (child of main company)
|
||||
self.env['product.product'].search([
|
||||
TRUE_LEAF]).write({'company_id': False})
|
||||
test_user.write({'company_id': chicago_id,
|
||||
'company_ids': [(4, chicago_id)]})
|
||||
bom.company_id = self.ref('base.main_company')
|
||||
self.assertPotentialQty(
|
||||
test_user_tmpl, 0,
|
||||
"The bom should not be visible to non members of the bom's "
|
||||
"company or company child of the bom's company")
|
||||
bom.company_id = chicago_id
|
||||
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
|
||||
inventory = self.env['stock.inventory'].create(
|
||||
{'name': 'Receive CPUa8',
|
||||
'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'),
|
||||
'location_id': self.wh_main.lot_stock_id.id,
|
||||
'product_qty': 1000.0})
|
||||
inventory.action_done()
|
||||
for i in [self.tmpl, self.var1, self.var2]:
|
||||
self.assertPotentialQty(
|
||||
i, 0.0,
|
||||
"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
|
||||
inventory = self.env['stock.inventory'].create(
|
||||
{'name': 'components for 1st variant',
|
||||
'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_15'),
|
||||
'location_id': self.wh_main.lot_stock_id.id,
|
||||
'product_qty': 1000.0})
|
||||
inventory.action_done()
|
||||
self.assertPotentialQty(
|
||||
self.tmpl, 1000.0,
|
||||
"Wrong template potential after receiving components")
|
||||
self.assertPotentialQty(
|
||||
self.var1, 1000.0,
|
||||
"Wrong variant 1 potential after receiving components")
|
||||
self.assertPotentialQty(
|
||||
self.var2, 0.0,
|
||||
"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
|
||||
inventory = self.env['stock.inventory'].create(
|
||||
{'name': 'components for 2nd variant',
|
||||
'location_id': self.wh_ch.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'),
|
||||
'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'),
|
||||
'location_id': self.wh_ch.lot_stock_id.id,
|
||||
'product_qty': 313.0})
|
||||
inventory.action_done()
|
||||
self.assertPotentialQty(
|
||||
self.tmpl, 1000.0,
|
||||
"Wrong template potential after receiving components")
|
||||
self.assertPotentialQty(
|
||||
self.var1, 1000.0,
|
||||
"Receiving variant 2's component should not change "
|
||||
"variant 1's potential")
|
||||
self.assertPotentialQty(
|
||||
self.var2, 42.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,
|
||||
"Wrong potential quantity in Chicago WH")
|
||||
# Check by location
|
||||
self.assertPotentialQty(
|
||||
self.tmpl.with_context(
|
||||
location=self.wh_main.lot_stock_id.id), 1000.0,
|
||||
"Wrong potential quantity in main WH location")
|
||||
self.assertPotentialQty(
|
||||
self.tmpl.with_context(
|
||||
location=self.wh_ch.lot_stock_id.id),
|
||||
42.0,
|
||||
"Wrong potential quantity in Chicago WH location")
|
||||
|
||||
def test_multi_unit_recursive_bom(self):
|
||||
# Test multi-level and multi-units BOM
|
||||
|
||||
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'
|
||||
})
|
||||
|
||||
bom_p1 = self.bom_model.create({
|
||||
'product_tmpl_id': p1.product_tmpl_id.id,
|
||||
'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
|
||||
self.bom_line_model.create({
|
||||
'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({
|
||||
'product_tmpl_id': p2.product_tmpl_id.id,
|
||||
'product_id': p2.id,
|
||||
'type': 'phantom',
|
||||
})
|
||||
|
||||
# p2 need 2 unit of component
|
||||
self.bom_line_model.create({
|
||||
'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
|
||||
self.assertEqual(0, p1.potential_qty)
|
||||
|
||||
self.create_inventory(p3.id, 1)
|
||||
|
||||
p1.refresh()
|
||||
self.assertEqual(0, p1.potential_qty)
|
||||
|
||||
self.create_inventory(p3.id, 15)
|
||||
p1.refresh()
|
||||
self.assertEqual(0, p1.potential_qty)
|
||||
|
||||
self.create_inventory(p3.id, 16)
|
||||
p1.refresh()
|
||||
self.assertEqual(1.0, p1.potential_qty)
|
||||
|
||||
self.create_inventory(p3.id, 25)
|
||||
p1.refresh()
|
||||
self.assertEqual(1.0, p1.potential_qty)
|
||||
|
||||
self.create_inventory(p3.id, 32)
|
||||
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')
|
||||
|
||||
# Set on hand qty
|
||||
self.create_inventory(imac.id, 3)
|
||||
|
||||
# Create a product with BOM
|
||||
p1 = self.product_model.create({
|
||||
'name': 'Test product with BOM',
|
||||
})
|
||||
bom_p1 = self.bom_model.create({
|
||||
'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
|
||||
self.bom_line_model.create({
|
||||
'bom_id': bom_p1.id,
|
||||
'product_id': imac.id,
|
||||
'product_qty': 1,
|
||||
'product_uom': self.ref('product.product_uom_unit'),
|
||||
})
|
||||
|
||||
# Default component is qty_available
|
||||
p1.refresh()
|
||||
self.assertEqual(3.0, p1.potential_qty)
|
||||
|
||||
# Change to immediately usable
|
||||
self.config.set_param('stock_available_mrp_based_on',
|
||||
'immediately_usable_qty')
|
||||
|
||||
p1.refresh()
|
||||
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',
|
||||
})
|
||||
self.create_inventory(imac_component.id, 5)
|
||||
|
||||
imac_bom = self.bom_model.create({
|
||||
'product_tmpl_id': imac.product_tmpl_id.id,
|
||||
'product_id': imac.id,
|
||||
'product_qty': 1,
|
||||
'product_uom': self.ref('product.product_uom_unit'),
|
||||
'type': 'phantom',
|
||||
})
|
||||
|
||||
# Need 1 imac_component for iMac
|
||||
self.bom_line_model.create({
|
||||
'bom_id': imac_bom.id,
|
||||
'product_id': imac_component.id,
|
||||
'product_qty': 1,
|
||||
'product_uom': self.ref('product.product_uom_unit'),
|
||||
})
|
||||
|
||||
p1.refresh()
|
||||
self.assertEqual(5.0, p1.potential_qty)
|
||||
|
||||
# Changing to virtual (same as immediately in current config)
|
||||
self.config.set_param('stock_available_mrp_based_on',
|
||||
'virtual_available')
|
||||
p1.refresh()
|
||||
self.assertEqual(5.0, p1.potential_qty)
|
||||
|
||||
def test_potential_qty__list(self):
|
||||
# Try to highlight a bug when _get_potential_qty is called on
|
||||
# a recordset with multiple products
|
||||
# Recursive compute is not working
|
||||
|
||||
p1 = self.product_model.create({'name': 'Test P1'})
|
||||
p2 = self.product_model.create({'name': 'Test P2'})
|
||||
p3 = self.product_model.create({'name': 'Test P3'})
|
||||
|
||||
self.config.set_param('stock_available_mrp_based_on',
|
||||
'immediately_usable_qty')
|
||||
|
||||
# P1 need one P2
|
||||
self.create_simple_bom(p1, p2)
|
||||
# P2 need one P3
|
||||
self.create_simple_bom(p2, p3)
|
||||
|
||||
self.create_inventory(p3.id, 3)
|
||||
|
||||
self.product_model.invalidate_cache()
|
||||
|
||||
products = self.product_model.search(
|
||||
[('id', 'in', [p1.id, p2.id, p3.id])]
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
{p1.id: 3.0, p2.id: 3.0, p3.id: 0.0},
|
||||
{p.id: p.potential_qty for p in products}
|
||||
)
|
||||
27
stock_available_mrp/views/product_template_view.xml
Normal file
27
stock_available_mrp/views/product_template_view.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<!-- 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>
|
||||
<field name="model">product.template</field>
|
||||
<field name="type">form</field>
|
||||
<field name="inherit_id" ref="stock_available.view_stock_available_form" />
|
||||
<field name="arch" type="xml">
|
||||
<data>
|
||||
<xpath expr="//field[@name='immediately_usable_qty']/ancestor::button" position="after">
|
||||
<button type="action" name="%(stock.product_open_quants)d"
|
||||
attrs="{'invisible':[('type', '!=', 'product')]}"
|
||||
class="oe_stat_button" icon="fa-building-o">
|
||||
<div class="o_form_field o_stat_info">
|
||||
<field name="potential_qty"
|
||||
widget="statinfo" nolabel="1"/>
|
||||
<span class="o_stat_text">Potential</span>
|
||||
</div>
|
||||
</button>
|
||||
</xpath>
|
||||
</data>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
Reference in New Issue
Block a user