← Back to team overview

openerp-community-reviewer team mailing list archive

[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&lt;0;blue:immediately_usable_qty&gt;=0 and state in ('draft', 'end', 'obsolete');black:immediately_usable_qty&gt;=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
+