openerp-community-reviewer team mailing list archive
-
openerp-community-reviewer team
-
Mailing list archive
-
Message #06933
[Merge] lp:~numerigraphe-team/stock-logistic-warehouse/7.0-add-stock-available-mrp into lp:stock-logistic-warehouse
Lionel Sausin - Numérigraphe has proposed merging lp:~numerigraphe-team/stock-logistic-warehouse/7.0-add-stock-available-mrp into lp:stock-logistic-warehouse.
Requested reviews:
Stock and Logistic Core Editors (stock-logistic-core-editors)
For more details, see:
https://code.launchpad.net/~numerigraphe-team/stock-logistic-warehouse/7.0-add-stock-available-mrp/+merge/220763
Add stock_available_mrp: take immediate manufaturing capability into account in the stock quantity available to promise
This branch builds upon lp:~numerigraphe-team/stock-logistic-warehouse/7.0-add-stock-available, which adds a generic module to compute the stock available to promise in a configurable way.
It also includes lp:~numerigraphe-team/stock-logistic-warehouse/7.0-add-stock-available-sale, which adds another //unrelated// implementation. This is only to avoid merge conflicts in case both are accepted, but they are independent works I can rebase this branch if needed.
Module by Loïc Bellier with contributions from your humble servant.
--
https://code.launchpad.net/~numerigraphe-team/stock-logistic-warehouse/7.0-add-stock-available-mrp/+merge/220763
Your team Stock and Logistic Core Editors is requested to review the proposed merge of lp:~numerigraphe-team/stock-logistic-warehouse/7.0-add-stock-available-mrp into lp:stock-logistic-warehouse.
=== added directory 'stock_available'
=== added file 'stock_available/__init__.py'
--- stock_available/__init__.py 1970-01-01 00:00:00 +0000
+++ stock_available/__init__.py 2014-05-23 07:57:52 +0000
@@ -0,0 +1,24 @@
+# -*- 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 . import product
+from . import res_config
+
+from .product import _product_available_fnct
=== added file 'stock_available/__openerp__.py'
--- stock_available/__openerp__.py 1970-01-01 00:00:00 +0000
+++ stock_available/__openerp__.py 2014-05-23 07:57:52 +0000
@@ -0,0 +1,42 @@
+# -*- 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/>.
+#
+##############################################################################
+
+{
+ 'name': 'Stock available to promise',
+ 'version': '2.0',
+ 'author': u'Numérigraphe',
+ 'category': 'Stock',
+ 'depends': ['stock'],
+ 'description': """
+Stock available to promise
+==========================
+This module proposes several options to compute the quantity available to
+promise for each product.
+This quantity is based on the projected stock and, depending on the
+configuration, it can account for various data such as sales quotations or
+immediate production capacity.
+This can be configured in the menu Settings > Configuration > Warehouse.
+""",
+ 'license': 'AGPL-3',
+ 'data': [
+ 'product_view.xml',
+ 'res_config_view.xml',
+ ]
+}
=== added file 'stock_available/product.py'
--- stock_available/product.py 1970-01-01 00:00:00 +0000
+++ stock_available/product.py 2014-05-23 07:57:52 +0000
@@ -0,0 +1,125 @@
+# -*- 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.osv import orm, fields
+import openerp.addons.decimal_precision as dp
+
+
+# Expose the method as a function, like when the fields are defined,
+# and use the pool to call the method from the other modules too.
+def _product_available_fnct(self, cr, uid, ids, field_names=None, arg=False,
+ context=None):
+ return self.pool['product.product']._product_available(
+ cr, uid, ids, field_names=field_names, arg=arg, context=context)
+
+
+class ProductProduct(orm.Model):
+ """Add a field for the stock available to promise.
+
+ Useful implementations need to be installed through the Settings menu or by
+ installing one of the modules stock_available_*
+ """
+ _inherit = 'product.product'
+
+ def __init__(self, pool, cr):
+ """Use _product_available_fnct to compute all the quantities."""
+ # Doing this lets us change the function and not redefine fields
+ super(ProductProduct, self).__init__(pool, cr)
+ for coldef in self._columns.values():
+ if (isinstance(coldef, fields.function)
+ and coldef._multi == 'qty_available'):
+ coldef._fnct = _product_available_fnct
+
+ def _product_available(self, cr, uid, ids, field_names=None, arg=False,
+ context=None):
+ """Dummy field for the stock available to promise.
+
+ Must be overridden by another module that actually implement
+ computations.
+ The sub-modules MUST call super()._product_available BEFORE their own
+ computations with
+ context['virtual_is_immediately_usable_qty']=False
+ AND call _update_virtual_available() AFTER their own computations
+ with the context from the caller.
+
+ @param context: see _update_virtual_available()"""
+ if context is None:
+ context = {}
+ if field_names is None:
+ field_names = []
+ else:
+ # We don't want to change the caller's list
+ field_names = list(field_names)
+
+ # Load virtual_available if it's not already asked for
+ # We need it to compute immediately_usable_qty
+ if ('virtual_available' not in field_names
+ and 'immediately_usable_qty' in field_names):
+ field_names.append('virtual_available')
+ if context.get('virtual_is_immediately_usable', False):
+ # _update_virtual_available will get/set these fields
+ if 'virtual_available' not in field_names:
+ field_names.append('virtual_available')
+ if 'immediately_usable_qty' not in field_names:
+ field_names.append('immediately_usable_qty')
+
+ # Compute the core quantities
+ res = super(ProductProduct, self)._product_available(
+ cr, uid, ids, field_names=field_names, arg=arg, context=context)
+
+ # By default, available to promise = forecasted quantity
+ if ('immediately_usable_qty' in field_names):
+ for stock_qty in res.itervalues():
+ stock_qty['immediately_usable_qty'] = \
+ stock_qty['virtual_available']
+
+ return self.pool['product.product']._update_virtual_available(
+ cr, uid, res, context=context)
+
+ def _update_virtual_available(self, cr, uid, res, context=None):
+ """Copy immediately_usable_qty to virtual_available if context asks
+
+ @param context: If the key virtual_is_immediately_usable is True,
+ then the virtual stock is computed as the stock
+ available to promise. This lets existing code base
+ their computations on the new value with a minimum of
+ change (i.e.: warn salesmen when the stock available
+ for sale is insufficient to honor a quotation)"""
+ if (context is None
+ or not context.get('virtual_is_immediately_usable', False)):
+ return res
+ for stock_qty in res.itervalues():
+ # _product_available makes sure both fields are loaded
+ # We're changing the caller's state but it's not be a problem
+ stock_qty['virtual_available'] = \
+ stock_qty['immediately_usable_qty']
+ return res
+
+ _columns = {
+ 'immediately_usable_qty': fields.function(
+ _product_available_fnct, multi='qty_available',
+ type='float',
+ digits_compute=dp.get_precision('Product Unit of Measure'),
+ string='Available to promise',
+ help="Stock for this Product that can be safely proposed "
+ "for sale to Customers.\n"
+ "The definition of this value can be configured to suit "
+ "your needs"),
+ }
=== renamed file 'stock_available_immediately/product_view.xml' => 'stock_available/product_view.xml'
--- stock_available_immediately/product_view.xml 2014-01-24 17:11:21 +0000
+++ stock_available/product_view.xml 2014-05-23 07:57:52 +0000
@@ -1,27 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
-
-<!--
- stock available_immediately for OpenERP
- Author Guewen Baconnier. Copyright Camptocamp SA
- Copyright (C) 2011 Akretion Sébastien BEAU <sebastien.beau@xxxxxxxxxxxx>
- The licence is in the file __openerp__.py
--->
-
<openerp>
<data>
- <record model="ir.ui.view" id="view_normal_stock_active_qty_form">
- <field name="name">product.normal.stock.active.qty.form.inherit</field>
+ <record model="ir.ui.view" id="view_stock_available_form">
+ <field name="name">Stock available to promise (form)</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="stock.view_normal_procurement_locations_form"/>
<field name="arch" type="xml">
<field name="virtual_available" position="after">
<newline/>
<field name="immediately_usable_qty" />
- </field>
+ </field>
</field>
</record>
- <record model="ir.ui.view" id="product_product_tree_view">
+ <record model="ir.ui.view" id="view_stock_available_tree">
<field name="name">product_immediately_usable.product_product_tree_view</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="product.product_product_tree_view"/>
@@ -30,12 +22,22 @@
<tree position="attributes">
<attribute name="colors">red:immediately_usable_qty<0;blue:immediately_usable_qty>=0 and state in ('draft', 'end', 'obsolete');black:immediately_usable_qty>=0 and state not in ('draft', 'end', 'obsolete')</attribute>
</tree>
- <field name="virtual_available" position="replace">
+ <field name="virtual_available" position="after">
<field name="immediately_usable_qty" />
</field>
</data>
</field>
</record>
+ <record model="ir.ui.view" id="view_stock_available_kanban">
+ <field name="name">Product Kanban Stock</field>
+ <field name="model">product.product</field>
+ <field name="inherit_id" ref="stock.product_kanban_stock_view"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='virtual_available']/.." position="replace">
+ <li t-if="record.type.raw_value != 'service'">Available to promise: <field name="immediately_usable_qty"/> <field name="uom_id"/></li>
+ </xpath>
+ </field>
+ </record>
</data>
</openerp>
=== added file 'stock_available/res_config.py'
--- stock_available/res_config.py 1970-01-01 00:00:00 +0000
+++ stock_available/res_config.py 2014-05-23 07:57:52 +0000
@@ -0,0 +1,49 @@
+# -*- 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 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+from openerp.osv import orm, fields
+
+
+class StockConfig(orm.TransientModel):
+ """Add options to easily install the submodules"""
+ _inherit = 'stock.config.settings'
+
+ _columns = {
+ 'module_stock_available_immediately': fields.boolean(
+ 'Exclude incoming goods',
+ help="This will subtract incoming quantities from the quantities"
+ "available to promise.\n"
+ "This installs the module stock_available_immediately."),
+ 'module_stock_available_sale': fields.boolean(
+ 'Exclude goods already in sale quotations',
+ help="This will subtract quantities from the sale quotations from"
+ "the quantities available to promise.\n"
+ "This installs the modules stock_available_sale.\n"
+ "If the modules sale and sale_delivery_date are not "
+ "installed, this will install them too"),
+ 'module_stock_available_mrp': fields.boolean(
+ 'Include the production potential',
+ help="This will add the quantities of goods that can be"
+ "immediately manufactured, to the quantities available to"
+ "promise.\n"
+ "This installs the module stock_available_mrp.\n"
+ "If the module mrp is not installed, this will install it "
+ "too"),
+ }
=== added file 'stock_available/res_config_view.xml'
--- stock_available/res_config_view.xml 1970-01-01 00:00:00 +0000
+++ stock_available/res_config_view.xml 2014-05-23 07:57:52 +0000
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+ <data>
+ <record id="view_stock_configuration" model="ir.ui.view">
+ <field name="name">Stock settings: quantity available to promise</field>
+ <field name="model">stock.config.settings</field>
+ <field name="inherit_id" ref="stock.view_stock_config_settings" />
+ <field name="arch" type="xml">
+ <data>
+ <xpath expr="//group[last()]" position="after">
+ <group>
+ <label for="id" string="Stock available to promise" />
+ <div>
+ <div>
+ <field name="module_stock_available_immediately" class="oe_inline" />
+ <label for="module_stock_available_immediately" />
+ </div>
+ <div>
+ <field name="module_stock_available_sale" class="oe_inline" />
+ <label for="module_stock_available_sale" />
+ </div>
+ <div>
+ <field name="module_stock_available_mrp" class="oe_inline" />
+ <label for="module_stock_available_mrp" />
+ </div>
+ </div>
+ </group>
+ </xpath>
+ </data>
+ </field>
+ </record>
+ </data>
+</openerp>
\ No newline at end of file
=== modified file 'stock_available_immediately/__openerp__.py'
--- stock_available_immediately/__openerp__.py 2014-01-24 17:11:21 +0000
+++ stock_available_immediately/__openerp__.py 2014-05-23 07:57:52 +0000
@@ -20,21 +20,26 @@
#
#
-
{
- "name": "Immediately Usable Stock Quantity",
- "version": "1.0",
- "depends": ["product", "stock", ],
+ "name": "Ignore planned receptions in quantity available to promise",
+ "version": "2.0",
+ "depends": ["stock_available"],
"author": "Camptocamp",
"license": "AGPL-3",
- "description": """
-Compute the immediately usable stock.
-Immediately usable is computed : Quantity on Hand - Outgoing Stock.
+ "description": u"""
+Ignore planned receptions in quantity available to promise
+----------------------------------------------------------
+
+Normally the quantity available to promise is based on the virtual stock,
+which includes both planned outgoing and incoming goods.
+This module will subtract the planned receptions from the quantity available to
+promise.
+
+Contributors
+------------
+ * Author: Guewen Baconnier (Camptocamp SA)
+ * Sébastien BEAU (Akretion) <sebastien.beau@xxxxxxxxxxxx>
+ * Lionel Sausin (Numérigraphe) <ls@xxxxxxxxxxxxxxxx>
""",
- "website": "http://tinyerp.com/module_account.html",
- "category": "Generic Modules/Stock",
- "data": ["product_view.xml",
- ],
- "active": False,
- "installable": True
+ "category": "Hidden",
}
=== modified file 'stock_available_immediately/product.py'
--- stock_available_immediately/product.py 2014-02-19 11:43:35 +0000
+++ stock_available_immediately/product.py 2014-05-23 07:57:52 +0000
@@ -19,124 +19,37 @@
#
##############################################################################
-from openerp.addons import decimal_precision as dp
-
-from openerp.osv import orm, fields
+from openerp.osv import orm
class product_immediately_usable(orm.Model):
- """
- Inherit Product in order to add an "immediately usable quantity"
- stock field
- Immediately usable quantity is : real stock - outgoing qty
- """
+ """Subtract incoming qty from immediately_usable_qty
+
+ We don't need to override the function fields, the module stock_available
+ takes of it for us."""
_inherit = 'product.product'
def _product_available(self, cr, uid, ids, field_names=None,
arg=False, context=None):
- """
- Get super() _product_available and compute immediately_usable_qty
- """
- # We need available and outgoing quantities to compute
+ """Ignore the incoming goods in the quantity available to promise"""
+ # We need available and incoming quantities to compute
# immediately usable quantity.
# When immediately_usable_qty is displayed but
- # not qty_available and outgoing_qty,
+ # not qty_available and incoming_qty,
# they are not computed in the super method so we cannot
# compute immediately_usable_qty.
# To avoid this issue, we add the 2 fields in
# field_names to compute them.
if 'immediately_usable_qty' in field_names:
field_names.append('qty_available')
- field_names.append('outgoing_qty')
+ field_names.append('incoming_qty')
res = super(product_immediately_usable, self)._product_available(
cr, uid, ids, field_names, arg, context)
if 'immediately_usable_qty' in field_names:
- for product_id, stock_qty in res.iteritems():
- res[product_id]['immediately_usable_qty'] = \
- stock_qty['qty_available'] + stock_qty['outgoing_qty']
-
- return res
-
- _columns = {
- 'qty_available': fields.function(
- _product_available,
- multi='qty_available',
- type='float',
- digits_compute=dp.get_precision('Product UoM'),
- string='Quantity On Hand',
- help="Current quantity of products.\n"
- "In a context with a single Stock Location, this includes "
- "goods stored at this Location, or any of its children.\n"
- "In a context with a single Warehouse, this includes "
- "goods stored in the Stock Location of this Warehouse, "
- "or any "
- "of its children.\n"
- "In a context with a single Shop, this includes goods "
- "stored in the Stock Location of the Warehouse of this Shop, "
- "or any of its children.\n"
- "Otherwise, this includes goods stored in any Stock Location "
- "typed as 'internal'."),
- 'virtual_available': fields.function(
- _product_available,
- multi='qty_available',
- type='float',
- digits_compute=dp.get_precision('Product UoM'),
- string='Quantity Available',
- help="Forecast quantity (computed as Quantity On Hand "
- "- Outgoing + Incoming)\n"
- "In a context with a single Stock Location, this includes "
- "goods stored at this Location, or any of its children.\n"
- "In a context with a single Warehouse, this includes "
- "goods stored in the Stock Location of this Warehouse, "
- "or any "
- "of its children.\n"
- "In a context with a single Shop, this includes goods "
- "stored in the Stock Location of the Warehouse of this Shop, "
- "or any of its children.\n"
- "Otherwise, this includes goods stored in any Stock Location "
- "typed as 'internal'."),
- 'incoming_qty': fields.function(
- _product_available,
- multi='qty_available',
- type='float',
- digits_compute=dp.get_precision('Product UoM'),
- string='Incoming',
- help="Quantity of products that are planned to arrive.\n"
- "In a context with a single Stock Location, this includes "
- "goods arriving to this Location, or any of its children.\n"
- "In a context with a single Warehouse, this includes "
- "goods arriving to the Stock Location of this Warehouse, or "
- "any of its children.\n"
- "In a context with a single Shop, this includes goods "
- "arriving to the Stock Location of the Warehouse of this "
- "Shop, or any of its children.\n"
- "Otherwise, this includes goods arriving to any Stock "
- "Location typed as 'internal'."),
- 'outgoing_qty': fields.function(
- _product_available,
- multi='qty_available',
- type='float',
- digits_compute=dp.get_precision('Product UoM'),
- string='Outgoing',
- help="Quantity of products that are planned to leave.\n"
- "In a context with a single Stock Location, this includes "
- "goods leaving from this Location, or any of its children.\n"
- "In a context with a single Warehouse, this includes "
- "goods leaving from the Stock Location of this Warehouse, or "
- "any of its children.\n"
- "In a context with a single Shop, this includes goods "
- "leaving from the Stock Location of the Warehouse of this "
- "Shop, or any of its children.\n"
- "Otherwise, this includes goods leaving from any Stock "
- "Location typed as 'internal'."),
- 'immediately_usable_qty': fields.function(
- _product_available,
- digits_compute=dp.get_precision('Product UoM'),
- type='float',
- string='Immediately Usable',
- multi='qty_available',
- help="Quantity of products really available for sale." \
- "Computed as: Quantity On Hand - Outgoing."),
- }
+ for stock_qty in res.itervalues():
+ stock_qty['immediately_usable_qty'] -= \
+ stock_qty['incoming_qty']
+
+ return self._update_virtual_available(cr, uid, res, context=context)
=== added directory 'stock_available_mrp'
=== added file 'stock_available_mrp/__init__.py'
--- stock_available_mrp/__init__.py 1970-01-01 00:00:00 +0000
+++ stock_available_mrp/__init__.py 2014-05-23 07:57:52 +0000
@@ -0,0 +1,21 @@
+# -*- 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 . import product
=== added file 'stock_available_mrp/__openerp__.py'
--- stock_available_mrp/__openerp__.py 1970-01-01 00:00:00 +0000
+++ stock_available_mrp/__openerp__.py 2014-05-23 07:57:52 +0000
@@ -0,0 +1,39 @@
+# -*- 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/>.
+#
+##############################################################################
+
+{
+ 'name': 'Consider the production potential is available to promise',
+ 'version': '2.0',
+ 'author': u'Numérigraphe',
+ 'category': 'Hidden',
+ 'depends': ['stock_available', 'mrp'],
+ 'description': """
+This module takes the potential quantities available for Products in account in
+the quantity available to promise, where the "Potential quantity" is the
+quantity that can be manufactured with the components immediately at hand,
+following a single level of Bill of Materials.""",
+ 'data': [
+ 'product_view.xml',
+ ],
+ 'test': [
+ 'test/potential_qty.yml',
+ ],
+ 'license': 'AGPL-3',
+}
=== added file 'stock_available_mrp/product.py'
--- stock_available_mrp/product.py 1970-01-01 00:00:00 +0000
+++ stock_available_mrp/product.py 2014-05-23 07:57:52 +0000
@@ -0,0 +1,121 @@
+# -*- 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.osv import orm, fields
+import 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):
+ """Compute the potential quantities available to promise."""
+ # Check the context
+ 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
+
+ if field_names is None:
+ field_names = []
+
+ # Compute the core quantities
+ res = super(product_product, self)._product_available(
+ cr, uid, ids, field_names=field_names, arg=arg, context=context)
+
+ # Compute the quantities quoted/potential/available to promise
+ if ('potential_qty' in field_names):
+ # 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, uid, 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, uid, 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)
+
+ res[product.id]['potential_qty'] += min_qty
+ if ('immediately_usable_qty' in field_names):
+ res[product.id]['immediately_usable_qty'] += min_qty
+
+ return self._update_virtual_available(cr, uid, res, context=context)
+
+ def _compute_potential_qty_from_bom(self, cr, uid, bom_id, to_uom,
+ context):
+ """Compute the potential qty from BoMs with components available"""
+ bom_obj = self.pool['mrp.bom']
+ uom_obj = self.pool['product.uom']
+ 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
+ final_product = bom_obj.browse(
+ cr, uid, [bom_id], context=context_wo_uom)[0]
+
+ # store id of final product uom
+
+ for component in final_product.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
+ recipe_uom_qty = (stock_component_qty // component.product_qty
+ ) * final_product.product_qty
+ # Convert back to the reporting default UoM
+ stock_product_uom_qty = uom_obj._compute_qty_obj(
+ cr, uid, final_product.product_uom, recipe_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."),
+ }
=== added file 'stock_available_mrp/product_view.xml'
--- stock_available_mrp/product_view.xml 1970-01-01 00:00:00 +0000
+++ stock_available_mrp/product_view.xml 2014-05-23 07:57:52 +0000
@@ -0,0 +1,19 @@
+<?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>
=== added directory 'stock_available_mrp/test'
=== added file 'stock_available_mrp/test/potential_qty.yml'
--- stock_available_mrp/test/potential_qty.yml 1970-01-01 00:00:00 +0000
+++ stock_available_mrp/test/potential_qty.yml 2014-05-23 07:57:52 +0000
@@ -0,0 +1,68 @@
+- 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
+ new_qty = self.browse(cr, uid, bom.id).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"
+ x = {
+ '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',
+ }
+ self.pool['stock.move'].create(
+ cr, uid,x)
+ # Re-read the potential quantity
+ new_qty = self.browse(cr, uid, bom.id).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)
=== added directory 'stock_available_sale'
=== added file 'stock_available_sale/__init__.py'
--- stock_available_sale/__init__.py 1970-01-01 00:00:00 +0000
+++ stock_available_sale/__init__.py 2014-05-23 07:57:52 +0000
@@ -0,0 +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/>.
+#
+##############################################################################
+
+from . import product
+from . import sale_stock
=== added file 'stock_available_sale/__openerp__.py'
--- stock_available_sale/__openerp__.py 1970-01-01 00:00:00 +0000
+++ stock_available_sale/__openerp__.py 2014-05-23 07:57:52 +0000
@@ -0,0 +1,48 @@
+# -*- 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/>.
+#
+##############################################################################
+
+{
+ 'name': 'Quotations in quantity available to promise',
+ 'version': '2.0',
+ 'author': u'Numérigraphe SÀRL',
+ 'category': 'Hidden',
+ 'depends': [
+ 'stock_available',
+ 'sale_order_dates',
+ 'sale_stock',
+ ],
+ 'description': """
+This module computes the quoted quantity of the Products, and subtracts it from
+the quantities available to promise .
+
+"Quoted" is defined as the sum of the quantities of this product in Quotations,
+taking the context's shop or warehouse into account.
+
+When entering sale orders, the salesperson get warned if the quantity available
+to promise is insufficient (instead when the virtual stock is
+insufficient).""",
+ 'data': [
+ 'product_view.xml',
+ ],
+ 'test': [
+ 'test/quoted_qty.yml',
+ ],
+ 'license': 'AGPL-3',
+}
=== added file 'stock_available_sale/product.py'
--- stock_available_sale/product.py 1970-01-01 00:00:00 +0000
+++ stock_available_sale/product.py 2014-05-23 07:57:52 +0000
@@ -0,0 +1,232 @@
+# -*- 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.osv import orm, fields
+import decimal_precision as dp
+
+# Function which uses the pool to call the method from the other modules too.
+from openerp.addons.stock_available import _product_available_fnct
+
+
+class ProductProduct(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):
+ """Compute the quantities in Quotations."""
+ # Check the context
+ 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
+
+ if field_names is None:
+ field_names = []
+
+ # Compute the core quantities
+ res = super(ProductProduct, self)._product_available(
+ cr, uid, ids, field_names=field_names, arg=arg, context=context)
+
+ # Compute the quantities quoted/available to promise
+ if ('quoted_qty' in field_names
+ or 'immediately_usable_qty' in field_names):
+ date_str, date_args = self._get_dates(cr, uid, ids,
+ context=context)
+
+ # Limit the search to some shops according to the context
+ shop_str, shop_args = self._get_shops(cr, uid, ids,
+ context=context)
+
+ # Query the total by Product and UoM
+ cr.execute(
+ """
+ SELECT sum(product_uom_qty), product_id, product_uom
+ FROM sale_order_line
+ INNER JOIN sale_order
+ ON (sale_order_line.order_id = sale_order.id)
+ WHERE product_id in %s
+ AND sale_order_line.state = 'draft' """
+ + date_str + shop_str +
+ "GROUP BY sale_order_line.product_id, product_uom",
+ (tuple(ids),) + date_args + shop_args)
+ results = cr.fetchall()
+
+ # Get the UoM resources we'll need for conversion
+ # UoMs from the products
+ uoms_o = {}
+ product2uom = {}
+ for product in self.browse(cr, uid, ids, context=context):
+ product2uom[product.id] = product.uom_id
+ uoms_o[product.uom_id.id] = product.uom_id
+ # UoM from the results and the context
+ uom_obj = self.pool['product.uom']
+ uoms = map(lambda stock_product_uom_qty: stock_product_uom_qty[2],
+ results)
+ if context.get('uom', False):
+ uoms.append(context['uom'])
+ uoms = filter(lambda stock_product_uom_qty:
+ stock_product_uom_qty not in uoms_o.keys(), uoms)
+ if uoms:
+ uoms = uom_obj.browse(cr, uid, list(set(uoms)),
+ context=context)
+ for o in uoms:
+ uoms_o[o.id] = o
+
+ # Compute the quoted quantity
+ for (amount, prod_id, prod_uom) in results:
+ # Convert the amount to the product's UoM without rounding
+ amount = amount / uoms_o[prod_uom].factor
+ if ('quoted_qty' in field_names):
+ res[prod_id]['quoted_qty'] -= amount
+ if ('immediately_usable_qty' in field_names):
+ res[prod_id]['immediately_usable_qty'] -= amount
+
+ # Round and optionally convert the results to the requested UoM
+ for prod_id, stock_qty in res.iteritems():
+ if context.get('uom', False):
+ # Convert to the requested UoM
+ res_uom = uoms_o[context['uom']]
+ else:
+ # The conversion is unneeded but we do need the rounding
+ res_uom = product2uom[prod_id]
+ if ('quoted_qty' in field_names):
+ stock_qty['quoted_qty'] = uom_obj._compute_qty_obj(
+ cr, uid, product2uom[prod_id],
+ stock_qty['quoted_qty'],
+ res_uom)
+ if ('immediately_usable_qty' in field_names):
+ stock_qty['immediately_usable_qty'] = \
+ uom_obj._compute_qty_obj(
+ cr, uid, product2uom[prod_id],
+ stock_qty['immediately_usable_qty'],
+ res_uom)
+ return self._update_virtual_available(cr, uid, res, context=context)
+
+ def _get_shops(self, cr, uid, ids, context=None):
+ """Find the shops matching the current context
+
+ See the helptext for the field quoted_qty for details"""
+ shop_ids = []
+ # Account for one or several locations in the context
+ # Take any shop using any warehouse that has these locations as stock
+ # location
+ if context.get('location', False):
+ # Either a single or multiple locations can be in the context
+ if not isinstance(context['location'], list):
+ location_ids = [context['location']]
+ else:
+ location_ids = context['location']
+ # Add the children locations
+ if context.get('compute_child', True):
+ child_location_ids = self.pool['stock.location'].search(
+ cr, uid, [('location_id', 'child_of', location_ids)])
+ location_ids = child_location_ids or location_ids
+ # Get the corresponding Shops
+ cr.execute(
+ """
+ SELECT id FROM sale_shop
+ WHERE warehouse_id IN (
+ SELECT id
+ FROM stock_warehouse
+ WHERE lot_stock_id IN %s)""",
+ (tuple(location_ids),))
+ res_location = cr.fetchone()
+ if res_location:
+ shop_ids.append(res_location)
+
+ # Account for a warehouse in the context
+ # Take any draft order in any shop using this warehouse
+ if context.get('warehouse', False):
+ cr.execute("SELECT id "
+ "FROM sale_shop "
+ "WHERE warehouse_id = %s",
+ (int(context['warehouse']),))
+ res_wh = cr.fetchone()
+ if res_wh:
+ shop_ids.append(res_wh)
+
+ # If we are in a single Shop context, only count the quotations from
+ # this shop
+ if context.get('shop', False):
+ shop_ids.append(context['shop'])
+ # Build the SQL to restrict to the selected shops
+ shop_str = ''
+ if shop_ids:
+ shop_str = 'AND sale_order.shop_id IN %s'
+
+ if shop_ids:
+ shop_ids = (tuple(shop_ids),)
+ else:
+ shop_ids = ()
+ return shop_str, shop_ids
+
+ def _get_dates(self, cr, uid, ids, context=None):
+ """Build SQL criteria to match the context's from/to dates"""
+ # If we are in a context with dates, only consider the quotations to be
+ # delivered at these dates.
+ # If no delivery date was entered, use the order date instead
+ if not context:
+ return '', ()
+
+ from_date = context.get('from_date', False)
+ to_date = context.get('to_date', False)
+ date_str = ''
+ date_args = []
+ if from_date:
+ date_str = """AND COALESCE(
+ sale_order.requested_date,
+ sale_order.date_order) >= %s """
+ date_args.append(from_date)
+ if to_date:
+ date_str += """AND COALESCE(
+ sale_order.requested_date,
+ sale_order.date_order) <= %s """
+ date_args.append(to_date)
+
+ if date_args:
+ date_args = (tuple(date_args),)
+ else:
+ date_args = ()
+ return date_str, date_args
+
+ _columns = {
+ 'quoted_qty': fields.function(
+ _product_available_fnct, method=True, multi='qty_available',
+ type='float',
+ digits_compute=dp.get_precision('Product Unit of Measure'),
+ string='Quoted',
+ help="Total quantity of this Product that have been included in "
+ "Quotations (Draft Sale Orders).\n"
+ "In a context with a single Shop, this includes the "
+ "Quotation processed at this Shop.\n"
+ "In a context with a single Warehouse, this includes "
+ "Quotation processed in any Shop using this Warehouse.\n"
+ "In a context with a single Stock Location, this includes "
+ "Quotation processed at any shop using any Warehouse using "
+ "this Location, or any of its children, as it's Stock "
+ "Location.\n"
+ "Otherwise, this includes every Quotation."),
+ }
=== added file 'stock_available_sale/product_view.xml'
--- stock_available_sale/product_view.xml 1970-01-01 00:00:00 +0000
+++ stock_available_sale/product_view.xml 2014-05-23 07:57:52 +0000
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<openerp>
+ <data>
+ <!-- Add the quantity available to promise in the product form -->
+ <record id="view_product_form_quoted_qty" model="ir.ui.view">
+ <field name="name">product.form.quoted_qty</field>
+ <field name="model">product.product</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']" position="after">
+ <field name="quoted_qty"/>
+ </xpath>
+ </data>
+ </field>
+ </record>
+ </data>
+</openerp>
=== added file 'stock_available_sale/sale_stock.py'
--- stock_available_sale/sale_stock.py 1970-01-01 00:00:00 +0000
+++ stock_available_sale/sale_stock.py 2014-05-23 07:57:52 +0000
@@ -0,0 +1,43 @@
+# -*- 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.osv import osv
+
+
+class sale_order_line(osv.osv):
+ _inherit = "sale.order.line"
+
+ def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
+ uom=False, qty_uos=0, uos=False, name='',
+ partner_id=False, lang=False, update_tax=True,
+ date_order=False, packaging=False,
+ fiscal_position=False, flag=False, context=None):
+ """Base the stock checking on the quantity available to promise
+
+ This is done by tweaking the context, to keep the impact minimum"""
+ if context is None:
+ context = {}
+ context = dict(context, virtual_is_immediately_usable=True)
+ return super(sale_order_line, self).product_id_change(
+ cr, uid, ids, pricelist, product, qty=qty,
+ uom=uom, qty_uos=qty_uos, uos=uos, name=name,
+ partner_id=partner_id, lang=lang, update_tax=update_tax,
+ date_order=date_order, packaging=packaging,
+ fiscal_position=fiscal_position, flag=flag, context=context)
=== added directory 'stock_available_sale/test'
=== added file 'stock_available_sale/test/quoted_qty.yml'
--- stock_available_sale/test/quoted_qty.yml 1970-01-01 00:00:00 +0000
+++ stock_available_sale/test/quoted_qty.yml 2014-05-23 07:57:52 +0000
@@ -0,0 +1,66 @@
+- Test the computation of the quoted quantity on product.product_product_10
+
+- Create a UoM in the category of PCE
+- !record {model: product.uom, id: thousand}:
+ name: Thousand
+ factor: 0.001
+ rounding: 0.00
+ uom_type: bigger
+ category_id: product.product_uom_categ_unit
+
+- Cancel all the previous Quotations
+- !python {model: sale.order}: |
+ line_ids = self.pool['sale.order.line'].search(
+ cr, uid, [('product_id', '=', ref('product.product_product_10')),
+ ('state', '=', 'draft')])
+ ids = [l.order_id.id for l in self.pool['sale.order.line'].browse(cr, uid, line_ids)]
+ if ids:
+ self.action_cancel(cr, uid, ids)
+- The quoted quantity should be 0
+- !assert {model: product.product, id: product.product_product_10, string: "Check quoted_qty"}:
+ - quoted_qty == 0.0
+
+- Enter a Quotation
+- !record {model: sale.order, id: order1}:
+ order_line:
+ - name: Quotation 1
+ product_uom: product.product_uom_unit
+ product_uom_qty: 107.0
+ state: draft
+ product_id: product.product_product_10
+ partner_id: base.res_partner_2
+ partner_invoice_id: base.res_partner_address_8
+ partner_shipping_id: base.res_partner_address_8
+ pricelist_id: product.list0
+- The quoted qty should match the single quotation
+- !assert {model: product.product, id: product.product_product_10, string: "Check quoted_qty"}:
+ - quoted_qty == -107.0
+
+- Enter another Quotation
+- !record {model: sale.order, id: order2}:
+ order_line:
+ - name: Quotation 1
+ product_uom: thousand
+ product_uom_qty: 0.613
+ state: draft
+ product_id: product.product_product_10
+ partner_id: base.res_partner_2
+ partner_invoice_id: base.res_partner_address_9
+ partner_shipping_id: base.res_partner_address_9
+ pricelist_id: product.list0
+- The quoted qty should match the total of the quotations
+- !assert {model: product.product, id: product.product_product_10, string: "Check quoted quantity"}:
+ - quoted_qty == -720.0
+- Use the context to report in another UoM
+- !assert {model: product.product, id: product.product_product_10, string: "Check in other UoM", context: "{'uom': ref('thousand')}"}:
+ - quoted_qty == -0.72
+- Use the context to report in the default UoM
+- !assert {model: product.product, id: product.product_product_10, string: "Check in False UoM", context: "{'uom': False}"}:
+ - quoted_qty == -720.0
+
+- Confirm one of the Quotations
+- !workflow {model: sale.order, action: order_confirm, ref: order1}
+- The quoted qty should match the remaining quotation
+- !assert {model: product.product, id: product.product_product_10, string: "Check quoted quantity"}:
+ - quoted_qty == -613.0
+