← Back to team overview

openerp-community-reviewer team mailing list archive

[Merge] lp:~numerigraphe-team/stock-logistic-warehouse/7.0-inventory-hierarchical into lp:stock-logistic-warehouse

 

Loïc Bellier - Numérigraphe has proposed merging lp:~numerigraphe-team/stock-logistic-warehouse/7.0-inventory-hierarchical into lp:stock-logistic-warehouse with lp:~numerigraphe-team/stock-logistic-warehouse/7.0-inventory-location as a prerequisite.

Requested reviews:
  Lionel Sausin - Numérigraphe (lionel-sausin): co-author
  Laetitia Gangloff (Acsone) (laetitia-gangloff)
  Stock and Logistic Core Editors (stock-logistic-core-editors)

For more details, see:
https://code.launchpad.net/~numerigraphe-team/stock-logistic-warehouse/7.0-inventory-hierarchical/+merge/223882

Code cleanup and Bug fixes.

This proposal lets users compose inventories with sub-inventories in 
a hierarchical tree. 

This is useful in medium/large warehouses where 
several teams must work in parallel to make inventories.

The module stock_inventory_hierarchical_location depends of stock_inventory_hierarchical and
stock_inventory_location (lp:~numerigraphe-team/stock-logistic-warehouse/7.0-inventory-location).

Thanks to Laetitia GANGLOFF from Ascone for her contribution.
-- 
https://code.launchpad.net/~numerigraphe-team/stock-logistic-warehouse/7.0-inventory-hierarchical/+merge/223882
Your team Stock and Logistic Core Editors is requested to review the proposed merge of lp:~numerigraphe-team/stock-logistic-warehouse/7.0-inventory-hierarchical into lp:stock-logistic-warehouse.
=== added directory 'stock_inventory_hierarchical'
=== added file 'stock_inventory_hierarchical/__init__.py'
--- stock_inventory_hierarchical/__init__.py	1970-01-01 00:00:00 +0000
+++ stock_inventory_hierarchical/__init__.py	2014-06-30 09:28:51 +0000
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    This module is copyright (C) 2013 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/>.
+#
+##############################################################################
+
+# This package-wide list keeps the names of the field that must be
+# propagated from root inventories to their children.
+# Add field names in the Model's definition.
+PARENT_VALUES = []
+
+import hierarchical_inventory
+# Bring the main exception into the package's scope for easier reuse
+from .exceptions import HierarchicalInventoryException

=== added file 'stock_inventory_hierarchical/__openerp__.py'
--- stock_inventory_hierarchical/__openerp__.py	1970-01-01 00:00:00 +0000
+++ stock_inventory_hierarchical/__openerp__.py	2014-06-30 09:28:51 +0000
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    This module is copyright (C) 2013 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/>.
+#
+##############################################################################
+
+{
+    "name": "Hierarchical Physical Inventory",
+    "version": "1.1",
+    "depends": ["stock"],
+    "author": "Numérigraphe",
+    "category": "Warehouse Management",
+    "description": """
+Hierarchical structure for Physical Inventories and sub-Inventories
+===================================================================
+
+This module adds a parent-child relationship between Physical Inventories, to
+help users manage complex inventories.
+Using several inventories, you can distribute the counting to several persons
+and still keep a clear overview of global Inventory's status.
+
+OpenERP will make sure the status of the Inventory and it's Sub-Inventories are
+consistent.
+""",
+    "data": ["hierarchical_inventory_view.xml"],
+    "test": ["test/hierarchical_inventory_test.yml"],
+    "demo": ["hierarchical_inventory_demo.xml"],
+    "images": [
+        "inventory_form.png",
+        "inventory_form_actions.png",
+    ],
+}

=== added file 'stock_inventory_hierarchical/exceptions.py'
--- stock_inventory_hierarchical/exceptions.py	1970-01-01 00:00:00 +0000
+++ stock_inventory_hierarchical/exceptions.py	2014-06-30 09:28:51 +0000
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    This module is copyright (C) 2013 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
+
+
+class HierarchicalInventoryException(orm.except_orm):
+    """The operation is not possible for a hierarchical inventory"""
+    pass

=== added file 'stock_inventory_hierarchical/hierarchical_inventory.py'
--- stock_inventory_hierarchical/hierarchical_inventory.py	1970-01-01 00:00:00 +0000
+++ stock_inventory_hierarchical/hierarchical_inventory.py	2014-06-30 09:28:51 +0000
@@ -0,0 +1,202 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    This module is copyright (C) 2013 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
+from openerp.tools.translate import _
+
+from .exceptions import HierarchicalInventoryException
+
+# Add the date to the list of fields we must propagate to children inventories
+from . import PARENT_VALUES
+PARENT_VALUES.append('date')
+
+
+class HierarchicalInventory(orm.Model):
+    _inherit = 'stock.inventory'
+
+    _parent_store = True
+    _parent_order = 'date, name'
+    _order = 'parent_left'
+
+    def name_get(self, cr, uid, ids, context=None):
+        """Show the parent inventory's name in the name of the children
+
+        :param dict context: the ``inventory_display`` key can be
+                             used to select the short version of the
+                             inventory name (without the direct parent),
+                             when set to ``'short'``. The default is
+                             the long version."""
+        if context is None:
+            context = {}
+        if context.get('inventory_display') == 'short':
+            # Short name context: just do the usual stuff
+            return super(HierarchicalInventory, self).name_get(
+                cr, uid, ids, context=context)
+        if isinstance(ids, (list, tuple)) and not len(ids):
+            return []
+        if isinstance(ids, (long, int)):
+            ids = [ids]
+        reads = self.read(cr, uid, ids, ['name', 'parent_id'], context=context)
+        res = []
+        for record in reads:
+            name = record['name']
+            if record['parent_id']:
+                name = record['parent_id'][1] + ' / ' + name
+            res.append((record['id'], name))
+        return res
+
+    def name_search(self, cr, uid, name='', args=None, operator='ilike',
+                    context=None, limit=100):
+        """Enable search on value returned by name_get ("parent / child")"""
+        if not args:
+            args = []
+        if not context:
+            context = {}
+        if name:
+            # Make sure name_search is symmetric to name_get
+            name = name.split(' / ')[-1]
+            ids = self.search(cr, uid, [('name', operator, name)] + args,
+                              limit=limit, context=context)
+        else:
+            ids = self.search(cr, uid, args, limit=limit, context=context)
+        return self.name_get(cr, uid, ids, context=context)
+
+    def _complete_name(self, cr, uid, ids, field_name, arg, context=None):
+        """Function-field wrapper to get the complete name from name_get"""
+        res = self.name_get(cr, uid, ids, context=context)
+        return dict(res)
+
+    def _progress_rate(self, cr, uid, ids, field_name, arg, context=None):
+        """Rate of (sub)inventories done/total"""
+        rates = {}
+        for current_id in ids:
+            nb = self.search(
+                cr, uid, [('parent_id', 'child_of', current_id)],
+                context=context, count=True)
+            if not nb:
+                # No inventory, consider it's 0% done
+                rates[current_id] = 0
+                continue
+            nb_done = self.search(
+                cr, uid, [('parent_id', 'child_of', current_id),
+                          ('state', '=', 'done')],
+                context=context, count=True)
+            rates[current_id] = 100 * nb_done / nb
+        return rates
+
+    _columns = {
+        # name_get() only changes the default name of the record, not the
+        # content of the field "name" so we add another field for that
+        'complete_name': fields.function(
+            _complete_name, type="char",
+            string='Complete reference'),
+        'parent_id': fields.many2one(
+            'stock.inventory', 'Parent', ondelete='cascade', readonly=True,
+            states={'draft': [('readonly', False)]}),
+        'inventory_ids': fields.one2many(
+            'stock.inventory', 'parent_id', 'List of Sub-inventories',
+            readonly=True, states={'draft': [('readonly', False)]}),
+        'parent_left': fields.integer('Parent Left', select=1),
+        'parent_right': fields.integer('Parent Right', select=1),
+        'progress_rate': fields.function(
+            _progress_rate, string='Progress', type='float'),
+        }
+
+    _constraints = [
+        (orm.Model._check_recursion,
+         'Error: You can not create recursive inventories.',
+         ['parent_id']),
+    ]
+
+    def create(self, cr, uid, vals, context=None):
+        """Copy selected values from parent to child"""
+        if vals and vals.get('parent_id'):
+            existing_fields = self.fields_get_keys(cr, uid, context=context)
+            parent_values = self.read(cr, uid, [vals['parent_id']],
+                                      PARENT_VALUES, context=context)
+            vals = vals.copy()
+            vals.update({field: parent_values[0][field]
+                         for field in PARENT_VALUES
+                         if field in existing_fields})
+        return super(HierarchicalInventory, self).create(
+            cr, uid, vals, context=context)
+
+    def write(self, cr, uid, ids, vals, context=None):
+        """Copy selected values from parent to children"""
+        if context is None:
+            context = {}
+
+        values = super(HierarchicalInventory, self).write(
+            cr, uid, ids, vals, context=context)
+        if not vals or context.get('norecurse', False):
+            return values
+
+        # filter the fields we want to propagate
+        children_values = {
+            field: vals[field] for field in PARENT_VALUES if field in vals
+        }
+        if not children_values:
+            return values
+
+        if not isinstance(ids, list):
+            ids = [ids]
+        # The context disables recursion - children are already included
+        return self.write(
+            cr, uid, self.search(cr, uid, [('parent_id', 'child_of', ids)]),
+            children_values, context=dict(context, norecurse=True))
+
+    def action_cancel_inventory(self, cr, uid, ids, context=None):
+        """Cancel inventory only if all the parents are canceled"""
+        inventories = self.browse(cr, uid, ids, context=context)
+        for inventory in inventories:
+            while inventory.parent_id:
+                inventory = inventory.parent_id
+                if inventory.state != 'cancel':
+                    raise HierarchicalInventoryException(
+                        _('Warning'),
+                        _('One of the parent Inventories is not canceled.'))
+        return super(HierarchicalInventory,
+                     self).action_cancel_inventory(cr, uid, ids,
+                                                   context=context)
+
+    def action_confirm(self, cr, uid, ids, context=None):
+        """Confirm inventory only if all the children are confirmed"""
+        children_count = self.search(
+            cr, uid, [('parent_id', 'child_of', ids),
+                      ('state', 'not in', ['confirm', 'done'])],
+            context=context, count=True)
+        if children_count > 1:
+            raise HierarchicalInventoryException(
+                _('Warning'),
+                _('Some Sub-inventories are not confirmed.'))
+        return super(HierarchicalInventory, self).action_confirm(
+            cr, uid, ids, context=context)
+
+    def action_done(self, cr, uid, ids, context=None):
+        """Perform validation only if all the children states are 'done'."""
+        children_count = self.search(cr, uid, [('parent_id', 'child_of', ids),
+                                               ('state', '!=', 'done')],
+                                     context=context, count=True)
+        if children_count > 1:
+            raise HierarchicalInventoryException(
+                _('Warning'),
+                _('Some Sub-inventories are not validated.'))
+        return super(HierarchicalInventory, self).action_done(
+            cr, uid, ids, context=context)

=== added file 'stock_inventory_hierarchical/hierarchical_inventory_demo.xml'
--- stock_inventory_hierarchical/hierarchical_inventory_demo.xml	1970-01-01 00:00:00 +0000
+++ stock_inventory_hierarchical/hierarchical_inventory_demo.xml	2014-06-30 09:28:51 +0000
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+    <data noupdate="0">
+        <!-- Example Inventory with Sub-Inventories. -->
+        <record id="stock_inventory_parent0" model="stock.inventory">
+            <field name="name">Main Inventory</field>
+        </record>
+        <record id="child_1_id" model="stock.inventory">
+            <field name="name">Sub-Inventory 1</field>
+            <field name="parent_id" ref="stock_inventory_parent0" />
+        </record>
+        <record id="child_2_id" model="stock.inventory">
+            <field name="name">Sub-Inventory 2</field>
+            <field name="parent_id" ref="stock_inventory_parent0" />
+        </record>
+    </data>
+</openerp>

=== added file 'stock_inventory_hierarchical/hierarchical_inventory_view.xml'
--- stock_inventory_hierarchical/hierarchical_inventory_view.xml	1970-01-01 00:00:00 +0000
+++ stock_inventory_hierarchical/hierarchical_inventory_view.xml	2014-06-30 09:28:51 +0000
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+    <data>
+        <!-- Add parent_id and number of Sub-inventories to form view -->
+        <record model="ir.ui.view" id="stock_inventory_hierarchical_tree_view">
+            <field name="name">hierarchical.inventory.tree</field>
+            <field name="model">stock.inventory</field>
+            <field name="inherit_id" ref="stock.view_inventory_tree" />
+            <field name="field_parent">inventory_ids</field>
+            <field name="arch" type="xml">
+                <xpath expr="//field[@name='name']" position="replace">
+                    <field name="complete_name" string="Reference"/>
+                </xpath>
+                <xpath expr="//field[@name='state']" position="after">
+                    <field name="progress_rate" widget="progressbar" />
+                </xpath>
+            </field>
+        </record>
+
+        <!-- Add the parent_id filter to search view  -->
+        <record model="ir.ui.view" id="view_inventory_subinventories_filter">
+            <field name="name">hierarchical.inventory.filter</field>
+            <field name="model">stock.inventory</field>
+            <field name="inherit_id" ref="stock.view_inventory_filter" />
+            <field name="arch" type="xml">
+                <xpath expr="//field[@name='name']" position="before">
+                    <filter icon="terp-check" name="main_inventories" string="Main inventories" domain="[('parent_id', '=', False)]" help="Only select inventories that have no parents." />
+                    <separator orientation="vertical"/>
+                </xpath>
+                <xpath expr="//field[@name='date']" position="after">
+                    <field name="parent_id" />
+                </xpath>
+            </field>
+        </record>
+        <!-- Show main inventories by default -->
+        <record id="stock.action_inventory_form" model="ir.actions.act_window">
+            <field name="context">{'full':'1', 'search_default_main_inventories':1}</field>
+        </record>
+
+        <record model="ir.ui.view" id="stock_inventory_hierarchical_form_view">
+            <field name="name">hierarchical.inventory.form</field>
+            <field name="model">stock.inventory</field>
+            <field name="inherit_id" ref="stock.view_inventory_form" />
+            <field name="arch" type="xml">
+                <xpath expr="/form//field[@name='name']" position="after">
+                    <field name="parent_id"/>
+                </xpath>
+                <xpath expr="/form//field[@name='date']" position="attributes">
+                    <attribute name="attrs">{'readonly':[('parent_id', '!=', False)]}</attribute>
+                </xpath>
+                <xpath
+                    expr="//page[@string='General Information']"
+                    position="after">
+                    <page string="Sub-inventories">
+                        <field name="inventory_ids" nolabel="1" context="{'default_parent_id': active_id}">
+                            <tree>
+                                <field name="name" />
+                                <field name="state" />
+                                <field name="progress_rate" widget="progressbar" />
+                            </tree>
+                        </field>
+                    </page>
+                </xpath>
+            </field>
+        </record>
+
+        <!-- Open the children of the current Inventory in a distinct list
+              to let users work in a normal window instead of a popup -->
+        <act_window id="action_view_sub_inventory"
+            name="View Sub-inventories"
+            res_model="stock.inventory"
+            src_model="stock.inventory"
+            view_mode="tree,form"
+            view_type="form"
+            domain="[('parent_id', 'child_of', active_id),('id', '!=', active_id)]"
+            context="{'full':1, 'search_default_main_inventories':0}"/>
+    </data>
+</openerp>

=== added directory 'stock_inventory_hierarchical/i18n'
=== added file 'stock_inventory_hierarchical/i18n/fr.po'
--- stock_inventory_hierarchical/i18n/fr.po	1970-01-01 00:00:00 +0000
+++ stock_inventory_hierarchical/i18n/fr.po	2014-06-30 09:28:51 +0000
@@ -0,0 +1,112 @@
+# Translation of OpenERP Server.
+# This file contains the translation of the following modules:
+#	* stock_inventory_hierarchical
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: OpenERP Server 6.0.4\n"
+"Report-Msgid-Bugs-To: support@xxxxxxxxxxx\n"
+"POT-Creation-Date: 2013-09-25 13:43+0000\n"
+"PO-Revision-Date: 2013-09-25 13:43+0000\n"
+"Last-Translator: <>\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: stock_inventory_hierarchical
+#: field:stock.inventory,complete_name:0
+msgid "Complete reference"
+msgstr "Réference complète"
+
+#. module: stock_inventory_hierarchical
+#: field:stock.inventory,progress_rate:0
+msgid "Done"
+msgstr "Terminé"
+
+#. module: stock_inventory_hierarchical
+#: code:addons/stock_inventory_hierarchical/hierarchical_inventory.py:108
+#: constraint:stock.inventory:0
+#, python-format
+msgid "Error: You can not create recursive inventories."
+msgstr "Erreur : Vous ne pouvez pas créer d'inventaire récursifs."
+
+#. module: stock_inventory_hierarchical
+#: model:ir.model,name:stock_inventory_hierarchical.model_stock_inventory
+msgid "Gestion des stocks"
+msgstr "Gestion des stocks"
+
+#. module: stock_inventory_hierarchical
+#: field:stock.inventory,inventory_ids:0
+msgid "List of Sub-inventories"
+msgstr "Liste des sous-inventaires"
+
+#. module: stock_inventory_hierarchical
+#: view:stock.inventory:0
+msgid "Main inventories"
+msgstr "Inventaires principaux"
+
+#. module: stock_inventory_hierarchical
+#: view:stock.inventory:0
+msgid "Number of Sub-inventories"
+msgstr "Nombre de sous-inventaires"
+
+#. module: stock_inventory_hierarchical
+#: code:addons/stock_inventory_hierarchical/hierarchical_inventory.py:180
+#, python-format
+msgid "One of the parent Inventories is not canceled."
+msgstr "Un des inventaires pères n'est pas annulé."
+
+#. module: stock_inventory_hierarchical
+#: constraint:stock.inventory:0
+msgid "Other Physical inventories are being conducted using the same Locations."
+msgstr "Certains emplacements sont déjà dans un autre inventaire."
+
+#. module: stock_inventory_hierarchical
+#: field:stock.inventory,parent_id:0
+msgid "Parent"
+msgstr "Parent"
+
+#. module: stock_inventory_hierarchical
+#: field:stock.inventory,parent_left:0
+msgid "Parent Left"
+msgstr "Parent gauche"
+
+#. module: stock_inventory_hierarchical
+#: field:stock.inventory,parent_right:0
+msgid "Parent Right"
+msgstr "Parent droit"
+
+#. module: stock_inventory_hierarchical
+#: code:addons/stock_inventory_hierarchical/hierarchical_inventory.py:188
+#, python-format
+msgid "Some Sub-inventories are not confirmed."
+msgstr "Certains sous-inventaires ne sont pas confirmés."
+
+#. module: stock_inventory_hierarchical
+#: code:addons/stock_inventory_hierarchical/hierarchical_inventory.py:196
+#, python-format
+msgid "Some Sub-inventories are not validated."
+msgstr "Certains sous-inventaires ne sont pas terminés."
+
+#. module: stock_inventory_hierarchical
+#: model:ir.actions.act_window,name:stock_inventory_hierarchical.action_view_sub_inventory
+#: view:stock.inventory:0
+msgid "View Sub-inventories"
+msgstr "Voir les sous-inventaires"
+
+#. module: stock_inventory_hierarchical
+#: code:addons/stock_inventory_hierarchical/hierarchical_inventory.py:130
+#, python-format
+msgid "Sub-inventory: %s"
+msgstr "Sous-inventaire : %s"
+
+#. module: stock_inventory_hierarchical
+#: code:addons/stock_inventory_hierarchical/hierarchical_inventory.py:180
+#: code:addons/stock_inventory_hierarchical/hierarchical_inventory.py:188
+#: code:addons/stock_inventory_hierarchical/hierarchical_inventory.py:196
+#, python-format
+msgid "Warning"
+msgstr "Attention"
+

=== added directory 'stock_inventory_hierarchical/images'
=== added file 'stock_inventory_hierarchical/images/inventory_form.png'
Binary files stock_inventory_hierarchical/images/inventory_form.png	1970-01-01 00:00:00 +0000 and stock_inventory_hierarchical/images/inventory_form.png	2014-06-30 09:28:51 +0000 differ
=== added file 'stock_inventory_hierarchical/images/inventory_form_actions.png'
Binary files stock_inventory_hierarchical/images/inventory_form_actions.png	1970-01-01 00:00:00 +0000 and stock_inventory_hierarchical/images/inventory_form_actions.png	2014-06-30 09:28:51 +0000 differ
=== added directory 'stock_inventory_hierarchical/static'
=== added directory 'stock_inventory_hierarchical/static/src'
=== added directory 'stock_inventory_hierarchical/static/src/img'
=== added file 'stock_inventory_hierarchical/static/src/img/icon.png'
Binary files stock_inventory_hierarchical/static/src/img/icon.png	1970-01-01 00:00:00 +0000 and stock_inventory_hierarchical/static/src/img/icon.png	2014-06-30 09:28:51 +0000 differ
=== added directory 'stock_inventory_hierarchical/test'
=== added file 'stock_inventory_hierarchical/test/hierarchical_inventory_test.yml'
--- stock_inventory_hierarchical/test/hierarchical_inventory_test.yml	1970-01-01 00:00:00 +0000
+++ stock_inventory_hierarchical/test/hierarchical_inventory_test.yml	2014-06-30 09:28:51 +0000
@@ -0,0 +1,96 @@
+-
+  In this file, i check rules about hierarchical inventories.
+  Children date must be the same of parent date for each state,
+  the state of parent and children can change only if conditions are correct.
+-
+  Check if date of children are the same as the parent's.
+-
+  !python {model: stock.inventory}: |
+    parent_date = self.read(
+      cr, uid, [ref('stock_inventory_parent0')], ['date'])[0]['date']
+    child_1_date = self.read(
+      cr, uid, [ref('child_1_id')], ['date'])[0]['date']
+    assert child_1_date == parent_date, "Date are different: %s - %s" % (parent_date, child_1_date)
+
+    child_2_date = self.read(
+      cr, uid, [ref('child_2_id')], ['date'])[0]['date']
+    assert child_2_date == parent_date, "Date are different: %s - %s" % (parent_date, child_2_date)
+
+-
+  Check if children cannot be canceled if the parent was not canceled.
+  I'll try to cancel both children inventory while parent inventory having "draft" state.
+  After, i'll verify the state of each inventory.
+-
+  !python {model: stock.inventory}: |
+    from stock_inventory_hierarchical import HierarchicalInventoryException
+    try:
+      self.action_cancel_inventory(cr, uid, [ref('child_1_id')])
+    except HierarchicalInventoryException as e:
+      log("Good ! The Inventory could not be canceled: %s" % e)
+    try:
+      self.action_cancel_inventory(cr, uid, [ref('child_2_id')])
+    except HierarchicalInventoryException as e:
+      log("Good ! The Inventory could not be canceled: %s" % e)
+    child_1_state = self.read(cr, uid, [ref('child_1_id')], ['state'])[0]['state']
+    assert child_1_state == 'draft', "Child inventory 1 have '%s' state. It should be 'draft'" % child_1_state
+    child_2_state = self.read(cr, uid, [ref('child_2_id')], ['state'])[0]['state']
+    assert child_2_state == 'draft', "Child inventory 2 have '%s' state. It should be 'draft'" % child_2_state
+
+-
+  Check if children inventory have confirm state before confirm parent inventory.
+  To check this, i'll try to confirm parent inventory when children inventory having "draft" state,
+  and i'll check if state is still 'draft'.
+-
+  !python {model: stock.inventory}: |
+    from stock_inventory_hierarchical import HierarchicalInventoryException
+    try:
+      self.action_confirm(cr, uid, [ref('stock_inventory_parent0')])
+    except HierarchicalInventoryException as e:
+      log("Good, the inventory could not be confirmed: %s", e)
+    parent_state = self.read(cr, uid, [ref('stock_inventory_parent0')], ['state'])[0]['state']
+    assert parent_state == 'draft', "Parent inventory have '%s' state. It should be 'draft'" % parent_state
+
+-
+  In order, i'll confirm the children inventories, and the parent inventory after.
+-
+  !python {model: stock.inventory}: |
+    self.action_confirm(cr, uid, [ref('child_1_id')])
+    child_1_state = self.read(cr, uid, [ref('child_1_id')], ['state'])[0]['state']
+    assert child_1_state == 'confirm', "Child inventory 1 have '%s' state. It should be 'confirm'" % child_1_state
+
+    self.action_confirm(cr, uid, [ref('child_2_id')])
+    child_2_state = self.read(cr, uid, [ref('child_2_id')], ['state'])[0]['state']
+    assert child_2_state == 'confirm', "Child inventory 2 have '%s' state. It should be 'confirm'" % child_2_state
+
+    self.action_confirm(cr, uid, [ref('stock_inventory_parent0')])
+    parent_state = self.read(cr, uid, [ref('stock_inventory_parent0')], ['state'])[0]['state']
+    assert parent_state == 'confirm', "Parent inventory have '%s' state. It should be 'confirm'" % parent_state
+
+-
+  Check if children inventory have done state before validate parent inventory.
+  I'll try to validate parent inventory before children.
+-
+  !python {model: stock.inventory}: |
+    from stock_inventory_hierarchical import HierarchicalInventoryException
+    try:
+      self.action_done(cr, uid, [ref('stock_inventory_parent0')])
+    except HierarchicalInventoryException as e:
+      log("Good, the inventory could not be validated: %s", e)
+    parent_state = self.read(cr, uid, [ref('stock_inventory_parent0')], ['state'])[0]['state']
+    assert parent_state == 'confirm', "Parent inventory have '%s' state. It should be 'confirm'" % parent_state
+
+-
+  Now, i'll validate all children inventory before validate the parent.
+-
+  !python {model: stock.inventory}: |
+    self.action_done(cr, uid, [ref('child_1_id')])
+    child_1_state = self.read(cr, uid, [ref('child_1_id')], ['state'])[0]['state']
+    assert child_1_state == 'done', "Child inventory 1 have '%s' state. It should be 'done'" % child_1_state
+
+    self.action_done(cr, uid, [ref('child_2_id')])
+    child_2_state = self.read(cr, uid, [ref('child_2_id')], ['state'])[0]['state']
+    assert child_2_state == 'done', "Child inventory 2 have '%s' state. It should be 'done'" % child_2_state
+
+    self.action_done(cr, uid, [ref('stock_inventory_parent0')])
+    parent_state = self.read(cr, uid, [ref('stock_inventory_parent0')], ['state'])[0]['state']
+    assert parent_state == 'done', "Parent inventory have '%s' state. It should be 'done'" % parent_state

=== added directory 'stock_inventory_hierarchical_location'
=== added file 'stock_inventory_hierarchical_location/__init__.py'
--- stock_inventory_hierarchical_location/__init__.py	1970-01-01 00:00:00 +0000
+++ stock_inventory_hierarchical_location/__init__.py	2014-06-30 09:28:51 +0000
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    This module is copyright (C) 2013 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 . import inventory_hierarchical_location
+from . import wizard

=== added file 'stock_inventory_hierarchical_location/__openerp__.py'
--- stock_inventory_hierarchical_location/__openerp__.py	1970-01-01 00:00:00 +0000
+++ stock_inventory_hierarchical_location/__openerp__.py	2014-06-30 09:28:51 +0000
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    This module is copyright (C) 2013 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/>.
+#
+##############################################################################
+
+{
+    "name": "Exhaustive and hierarchical Stock Inventories",
+    "version": "1.1",
+    "depends": ["stock_inventory_hierarchical", "stock_inventory_location"],
+    "auto_install": True,
+    "author": u"Numérigraphe",
+    "category": "Hidden",
+    "description": """
+Make exhaustive Inventories aware of their Sub-Inventories.
+===========================================================
+
+This module allows an inventory to contain a general Location,
+and it's sub-inventories to contain some of it's sub-Locations.
+It will prevent you from setting the Inventories and sub-Inventories
+in inconsistent status.
+
+This module will be installed automatically if the modules
+"stock_inventory_location" and "stock_inventory_hierarchical" are both
+installed.
+You must keep this module installed to ensure proper functioning.
+
+    """,
+    "data": [
+        "inventory_hierarchical_location_view.xml",
+        "wizard/generate_inventory_view.xml",
+    ],
+    "test": ["tests/inventory_hierarchical_location_test.yml"],
+    "demo": ["inventory_hierarchical_location_demo.xml"],
+}

=== added directory 'stock_inventory_hierarchical_location/i18n'
=== added file 'stock_inventory_hierarchical_location/i18n/fr.po'
--- stock_inventory_hierarchical_location/i18n/fr.po	1970-01-01 00:00:00 +0000
+++ stock_inventory_hierarchical_location/i18n/fr.po	2014-06-30 09:28:51 +0000
@@ -0,0 +1,127 @@
+# Translation of OpenERP Server.
+# This file contains the translation of the following modules:
+#	* stock_inventory_hierarchical_location
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: OpenERP Server 6.0.4\n"
+"Report-Msgid-Bugs-To: support@xxxxxxxxxxx\n"
+"POT-Creation-Date: 2013-09-25 13:55+0000\n"
+"PO-Revision-Date: 2013-09-25 13:55+0000\n"
+"Last-Translator: <>\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: stock_inventory_hierarchical_location
+#: model:ir.actions.act_window,name:stock_inventory_hierarchical_location.action_view_stock_inventory_missing_location
+msgid "Confirm missing location"
+msgstr "Confirmer les emplacements manquants"
+
+#. module: stock_inventory_hierarchical_location
+#: view:stock.inventory.missing.location:0
+msgid "Confirm missing locations"
+msgstr "Confirmer les emplacements manquants"
+
+#. module: stock_inventory_hierarchical_location
+#: model:ir.model,name:stock_inventory_hierarchical_location.model_stock_inventory_uninventoried_locations
+msgid "Confirm the uninventoried Locations."
+msgstr "Confirmer les emplacements non inventoriés."
+
+#. module: stock_inventory_hierarchical_location
+#: view:stock.inventory.missing.location:0
+msgid "Do you want to continue ?"
+msgstr "Voulez-vous continuer ?"
+
+#. module: stock_inventory_hierarchical_location
+#: constraint:stock.inventory:0
+msgid "Error! You can not create recursive inventories."
+msgstr "Erreur! Vous ne pouvez pas créer un inventaire récursif."
+
+#. module: stock_inventory_hierarchical_location
+#: model:ir.model,name:stock_inventory_hierarchical_location.model_stock_inventory
+msgid "Gestion des stocks"
+msgstr "Gestion des stocks"
+
+#. module: stock_inventory_hierarchical_location
+#: code:addons/stock_inventory_hierarchical_location/inventory_hierarchical_location.py:70
+#, python-format
+msgid "Location missing for inventory \"%s\"."
+msgstr "Emplacement manquants dans l'inventaire \"%s\"."
+
+#. module: stock_inventory_hierarchical_location
+#: view:stock.inventory:0
+msgid "Locations"
+msgstr "Emplacements"
+
+#. module: stock_inventory_hierarchical_location
+#: field:stock.inventory.missing.location,location_ids:0
+msgid "Missing location"
+msgstr "Emplacements manquants"
+
+#. module: stock_inventory_hierarchical_location
+#: code:addons/stock_inventory_hierarchical_location/inventory_hierarchical_location.py:41
+#, python-format
+msgid "One of the parent inventories is not open."
+msgstr "Un des inventaire parent n'est pas ouvert."
+
+#. module: stock_inventory_hierarchical_location
+#: view:stock.inventory:0
+msgid "Open Inventory"
+msgstr "Ouvrir l'inventaire"
+
+#. module: stock_inventory_hierarchical_location
+#: constraint:stock.inventory:0
+msgid "Other Physical inventories are being conducted using the same Locations."
+msgstr "Erreur: certains emplacements sont déjà dans un autre inventaire."
+
+#. module: stock_inventory_hierarchical_location
+#: model:ir.model,name:stock_inventory_hierarchical_location.model_stock_inventory_missing_location
+msgid "Search on inventory tree for missing declared locations."
+msgstr "Recherche dans les inventaires les emplacements absents."
+
+#. module: stock_inventory_hierarchical_location
+#: code:addons/stock_inventory_hierarchical_location/inventory_hierarchical_location.py:122
+#, python-format
+msgid "Some Sub-inventories are not confirmed."
+msgstr "Au moins un sous-inventaire n'est pas confirmé."
+
+#. module: stock_inventory_hierarchical_location
+#: view:stock.inventory.missing.location:0
+msgid "This is the list of missing locations."
+msgstr "Voici la liste des emplacements manquants."
+
+#. module: stock_inventory_hierarchical_location
+#: code:addons/stock_inventory_hierarchical_location/inventory_hierarchical_location.py:58
+#, python-format
+msgid "This location is not declared on the parent inventory\n"
+"It cannot be added."
+msgstr "Cet emplacement n'est pas déclaré dans l'inventaire parent\n"
+"Vous ne pouvez pas l'ajouter."
+
+#. module: stock_inventory_hierarchical_location
+#: code:addons/stock_inventory_hierarchical_location/inventory_hierarchical_location.py:41
+#: code:addons/stock_inventory_hierarchical_location/inventory_hierarchical_location.py:70
+#: code:addons/stock_inventory_hierarchical_location/inventory_hierarchical_location.py:122
+#, python-format
+msgid "Warning !"
+msgstr "Attention !"
+
+#. module: stock_inventory_hierarchical_location
+#: code:addons/stock_inventory_hierarchical_location/inventory_hierarchical_location.py:57
+#, python-format
+msgid "Warning: Wrong location"
+msgstr "Attention: mauvais emplacement"
+
+#. module: stock_inventory_hierarchical_location
+#: view:stock.inventory.missing.location:0
+msgid "_Cancel"
+msgstr "_Annuler"
+
+#. module: stock_inventory_hierarchical_location
+#: view:stock.inventory.missing.location:0
+msgid "_Confirm missing locations"
+msgstr "_Confirmer les emplacements manquants"
+

=== added file 'stock_inventory_hierarchical_location/inventory_hierarchical_location.py'
--- stock_inventory_hierarchical_location/inventory_hierarchical_location.py	1970-01-01 00:00:00 +0000
+++ stock_inventory_hierarchical_location/inventory_hierarchical_location.py	2014-06-30 09:28:51 +0000
@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    This module is copyright (C) 2013 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
+from openerp.tools.translate import _
+
+from stock_inventory_hierarchical import HierarchicalInventoryException
+
+# Add the date to the list of fields we must propagate to children inventories
+from stock_inventory_hierarchical import PARENT_VALUES
+PARENT_VALUES.append('exhaustive')
+
+
+class HierarchicalExhInventory(orm.Model):
+    """Add hierarchical structure features to exhaustive Inventories"""
+    _inherit = 'stock.inventory'
+
+    def action_open(self, cr, uid, ids, context=None):
+        """Open only if all the parents are Open."""
+        for inventory in self.browse(cr, uid, ids, context=context):
+            while inventory.parent_id:
+                inventory = inventory.parent_id
+                if inventory.state != 'open':
+                    raise HierarchicalInventoryException(
+                        _('Warning'),
+                        _('One of the parent inventories is not open.'))
+        return super(HierarchicalExhInventory, self).action_open(
+            cr, uid, ids, context=context)
+
+    def get_missing_locations(self, cr, uid, ids, context=None):
+        """Extend the list of inventories with their children"""
+        ids = self.search(
+            cr, uid, [('parent_id', 'child_of', ids)], context=context)
+        missing_ids = super(HierarchicalExhInventory,
+                            self).get_missing_locations(
+                cr, uid, ids, context=context)
+        # Find the locations already included in sub-inventories
+        inventories = self.browse(cr, uid, ids, context=context)
+        subinv_location_ids = [sub.location_id.id
+                               for i in inventories
+                                   for sub in i.inventory_ids]
+        if not subinv_location_ids:
+            return missing_ids
+        # Extend to the children locations
+        subinv_location_ids = set(self.pool['stock.location'].search(
+            cr, uid, [
+                ('location_id', 'child_of', subinv_location_ids),
+                ('usage', '=', 'internal')], context=context))
+        return list(set(missing_ids) - subinv_location_ids)
+
+    # TODO v8: probably only keep the state "done"
+    def confirm_missing_locations(self, cr, uid, ids, context=None):
+        """Do something only if children state are confirm or done."""
+        children_count = self.search(
+            cr, uid, [('parent_id', 'child_of', ids),
+                      ('id', 'not in', ids),
+                      ('state', 'not in', ['confirm', 'done'])],
+            context=context, count=True)
+        if children_count > 0:
+            raise HierarchicalInventoryException(
+                _('Warning'),
+                _('Some Sub-inventories are not confirmed.'))
+        return super(HierarchicalExhInventory,
+                     self).confirm_missing_locations(
+            cr, uid, ids, context=context)
+
+    def onchange_location_id(self, cr, uid, ids, location_id, context=None):
+        """Check if location is a child of parent inventory location"""
+        loc_obj = self.pool['stock.location']
+        for inventory in self.browse(cr, uid, ids, context=context):
+            if inventory.parent_id:
+                allowed_location_ids = loc_obj.search(
+                    cr, uid, [('location_id', 'child_of',
+                               inventory.parent_id.location_id.id)],
+                    context=context)
+                if location_id not in allowed_location_ids:
+                    return {
+                        'location_id': False,
+                        'warning': {
+                            'title': _('Warning: Wrong location'),
+                            'message': _("This location is not declared on "
+                                         "the parent inventory\n"
+                                         "It cannot be added.")}
+                    }
+        return {}

=== added file 'stock_inventory_hierarchical_location/inventory_hierarchical_location_demo.xml'
--- stock_inventory_hierarchical_location/inventory_hierarchical_location_demo.xml	1970-01-01 00:00:00 +0000
+++ stock_inventory_hierarchical_location/inventory_hierarchical_location_demo.xml	2014-06-30 09:28:51 +0000
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+    <data noupdate="0">
+
+        <!-- Record inventories we can use in the tests. -->
+        <!-- We need them in the demo data because test data is rolled back
+            whenever an exception is raised. -->
+
+        <!-- Record a hierarchical exhaustive inventory -->
+        <record id="parent_inventory" model="stock.inventory">
+            <field name="name">Hierarchical exhaustive inventory</field>
+            <field name="state">draft</field>
+            <field name="date">2020-04-15 00:00:00</field>
+            <field name="exhaustive">True</field>
+            <field name="location_id" model="stock.location" ref="stock.stock_location_stock"/>
+        </record>
+        <record id="child_1_id" model="stock.inventory">
+            <field name="name">Team A</field>
+            <field name="parent_id" ref="parent_inventory"/>
+            <field name="location_id" model="stock.location" ref="stock.stock_location_14"/>
+        </record>
+        <record id="child_2_id" model="stock.inventory">
+            <field name="name">Team B</field>
+            <field name="parent_id" ref="parent_inventory"/>
+            <field name="location_id" model="stock.location" ref="stock.stock_location_components"/>
+        </record>
+    </data>
+</openerp>

=== added file 'stock_inventory_hierarchical_location/inventory_hierarchical_location_view.xml'
--- stock_inventory_hierarchical_location/inventory_hierarchical_location_view.xml	1970-01-01 00:00:00 +0000
+++ stock_inventory_hierarchical_location/inventory_hierarchical_location_view.xml	2014-06-30 09:28:51 +0000
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+    <data>
+        <record model="ir.ui.view" id="stock_inventory_hierarchical_location_form_view">
+            <field name="name">hierarchical.inventory.location.form</field>
+            <field name="model">stock.inventory</field>
+            <field name="inherit_id" ref="stock.view_inventory_form" />
+            <field name="arch" type="xml">
+
+                 <xpath expr="/form//field[@name='exhaustive']" position="attributes">
+                    <attribute name="attrs">{'readonly':[('parent_id', '!=', False)]}</attribute>
+                </xpath>
+
+                <xpath expr="/form//field[@name='location_id']" position="attributes">
+                    <attribute name="on_change">onchange_location_id(location_id)</attribute>
+                </xpath>
+
+            </field>
+        </record>
+
+        <record model="ir.ui.view" id="stock_ihl_exhautive_form_view">
+            <field name="name">hierarchical.inventory.location.exhautive.form</field>
+            <field name="model">stock.inventory</field>
+            <field name="inherit_id" ref="stock_inventory_hierarchical.stock_inventory_hierarchical_form_view" />
+            <field name="arch" type="xml">
+                <xpath expr="//field[@name='inventory_ids']" position="attributes">
+                    <attribute name="context">{'default_parent_id': active_id, 'default_exhaustive': exhaustive}</attribute>
+                </xpath>
+            </field>
+        </record>
+
+        <!-- Show hierarchical exhaustive inventories by default -->
+        <record id="stock.action_inventory_form" model="ir.actions.act_window">
+            <field name="context">{'full':'1', 'search_default_exhaustive':1, 'search_default_main_inventories':1}</field>
+        </record>
+    </data>
+</openerp>

=== added directory 'stock_inventory_hierarchical_location/static'
=== added directory 'stock_inventory_hierarchical_location/static/src'
=== added directory 'stock_inventory_hierarchical_location/static/src/img'
=== added file 'stock_inventory_hierarchical_location/static/src/img/icon.png'
Binary files stock_inventory_hierarchical_location/static/src/img/icon.png	1970-01-01 00:00:00 +0000 and stock_inventory_hierarchical_location/static/src/img/icon.png	2014-06-30 09:28:51 +0000 differ
=== added directory 'stock_inventory_hierarchical_location/tests'
=== added file 'stock_inventory_hierarchical_location/tests/__init__.py'
--- stock_inventory_hierarchical_location/tests/__init__.py	1970-01-01 00:00:00 +0000
+++ stock_inventory_hierarchical_location/tests/__init__.py	2014-06-30 09:28:51 +0000
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+#
+#
+#    Authors: Laetitia Gangloff
+#    Copyright (c) 2014 Acsone SA/NV (http://www.acsone.eu)
+#    All Rights Reserved
+#
+#    WARNING: This program as such is intended to be used by professional
+#    programmers who take the whole responsibility of assessing all potential
+#    consequences resulting from its eventual inadequacies and bugs.
+#    End users who are looking for a ready-to-use solution with commercial
+#    guarantees and support are strongly advised to contact a Free Software
+#    Service Company.
+#
+#    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/>.
+#
+#
+
+import fill_inventory_test
+
+fast_suite = [
+]
+
+checks = [
+    fill_inventory_test,
+]
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

=== added file 'stock_inventory_hierarchical_location/tests/fill_inventory_test.py'
--- stock_inventory_hierarchical_location/tests/fill_inventory_test.py	1970-01-01 00:00:00 +0000
+++ stock_inventory_hierarchical_location/tests/fill_inventory_test.py	2014-06-30 09:28:51 +0000
@@ -0,0 +1,118 @@
+# -*- coding: utf-8 -*-
+#
+#
+#    Authors: Laetitia Gangloff
+#    Copyright (c) 2014 Acsone SA/NV (http://www.acsone.eu)
+#    All Rights Reserved
+#
+#    WARNING: This program as such is intended to be used by professional
+#    programmers who take the whole responsibility of assessing all potential
+#    consequences resulting from its eventual inadequacies and bugs.
+#    End users who are looking for a ready-to-use solution with commercial
+#    guarantees and support are strongly advised to contact a Free Software
+#    Service Company.
+#
+#    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/>.
+#
+#
+
+import openerp.tests.common as common
+
+DB = common.DB
+ADMIN_USER_ID = common.ADMIN_USER_ID
+
+
+class fill_inventory_test(common.TransactionCase):
+
+    def setUp(self):
+        super(fill_inventory_test, self).setUp()
+
+    def test_missing_location(self):
+        """
+        Test that when confirm a parent inventory, the child location are not in the confirmation result
+        """
+        parent_inventory_id = self.ref('stock_inventory_hierarchical_location.parent_inventory')
+        self.registry('stock.inventory').action_open(self.cr, self.uid, [parent_inventory_id])
+        # confirm shelf 1 inventory
+        inventory_id = self.ref('stock_inventory_hierarchical_location.child_2_id')
+        self.registry('stock.inventory').action_open(self.cr, self.uid, [inventory_id])
+        missing_location = self.registry('stock.inventory').get_missing_locations(self.cr, self.uid, [inventory_id])
+        self.assertEqual(len(missing_location), 1, "1 missing location should be find, because the inventory is empty")
+        wizard_id = self.registry('stock.inventory.uninventoried.locations').create(self.cr, self.uid, {}, context={'active_ids': [inventory_id]})
+        self.registry('stock.inventory.uninventoried.locations').confirm_uninventoried_locations(self.cr, self.uid, wizard_id, context={'active_ids': [inventory_id]})
+        missing_location = self.registry('stock.inventory').get_missing_locations(self.cr, self.uid, [inventory_id])
+        self.assertEqual(len(missing_location), 0, "No missing location should be find, because the inventory is confirmed")
+        # confirm shelf 2 inventory
+        inventory_id = self.ref('stock_inventory_hierarchical_location.child_1_id')
+        self.registry('stock.inventory').action_open(self.cr, self.uid, [inventory_id])
+        missing_location = self.registry('stock.inventory').get_missing_locations(self.cr, self.uid, [inventory_id])
+        self.assertEqual(len(missing_location), 1, "1 missing location should be fine, because the inventory is empty")
+        self.registry('stock.inventory.line').create(self.cr, self.uid, {'product_id': self.ref('product.product_product_7'),
+                                                                         'product_uom': self.ref('product.product_uom_unit'),
+                                                                         'company_id': self.ref('base.main_company'),
+                                                                         'inventory_id': inventory_id,
+                                                                         'product_qty': 18.0,
+                                                                         'location_id': self.ref('stock.stock_location_14')})
+        missing_location = self.registry('stock.inventory').get_missing_locations(self.cr, self.uid, [inventory_id])
+        self.assertEqual(len(missing_location), 0, "No missing location should be find, because the inventory is filled")
+        wizard_id = self.registry('stock.inventory.uninventoried.locations').create(self.cr, self.uid, {}, context={'active_ids': [inventory_id]})
+        wizard = self.registry('stock.inventory.uninventoried.locations').browse(self.cr, self.uid, wizard_id, context={'active_ids': [inventory_id]})
+        self.assertEqual(len(wizard.location_ids), 0, "The wizard should not contain any lines but contains %s." % wizard.location_ids)
+        self.registry('stock.inventory.uninventoried.locations').confirm_uninventoried_locations(self.cr, self.uid, wizard_id, context={'active_ids': [inventory_id]})
+        # confirm parent inventory
+        missing_location = self.registry('stock.inventory').get_missing_locations(self.cr, self.uid, [parent_inventory_id])
+        self.assertEqual(len(missing_location), 1, "Only 1 missing location should be find, because there is some location in child inventory")
+
+    def test_fill_inventory(self):
+        """
+        Test that when fill a parent inventory, the child location are not in the result
+        """
+        parent_inventory_id = self.ref('stock_inventory_hierarchical_location.parent_inventory')
+        self.registry('stock.inventory').action_open(self.cr, self.uid, [parent_inventory_id])
+        # confirm shelf 1 inventory
+        inventory_id = self.ref('stock_inventory_hierarchical_location.child_2_id')
+        self.registry('stock.inventory').action_open(self.cr, self.uid, [inventory_id])
+        wizard_id = self.registry('stock.fill.inventory').create(self.cr, self.uid, {'location_id': self.ref('stock.stock_location_components'),
+                                                                                     'recursive': True,
+                                                                                     'exhaustive': True,
+                                                                                     'set_stock_zero': True}, context={'active_ids': [inventory_id]})
+        self.registry('stock.fill.inventory').fill_inventory(self.cr, self.uid, [wizard_id], context={'active_ids': [inventory_id]})
+        inventory_line_ids = self.registry('stock.inventory.line').search(self.cr, self.uid, [('inventory_id', '=', inventory_id)])
+        self.assertEqual(len(inventory_line_ids), 12, "12 inventory line is fount after filling inventory")
+        # confirm shelf 2 inventory
+        inventory_id = self.ref('stock_inventory_hierarchical_location.child_1_id')
+        self.registry('stock.inventory').action_open(self.cr, self.uid, [inventory_id])
+        wizard_id = self.registry('stock.fill.inventory').create(self.cr, self.uid, {'location_id': self.ref('stock.stock_location_14'),
+                                                                                     'recursive': True,
+                                                                                     'exhaustive': True,
+                                                                                     'set_stock_zero': True}, context={'active_ids': [inventory_id]})
+        self.registry('stock.fill.inventory').fill_inventory(self.cr, self.uid, [wizard_id], context={'active_ids': [inventory_id]})
+        inventory_line_ids = self.registry('stock.inventory.line').search(self.cr, self.uid, [('inventory_id', '=', inventory_id)])
+        self.assertEqual(len(inventory_line_ids), 4, "1 inventory line is fount after filling inventory")
+        # confirm parent inventory
+        wizard_id = self.registry('stock.fill.inventory').create(self.cr, self.uid, {'location_id': self.ref('stock.stock_location_stock'),
+                                                                                     'recursive': True,
+                                                                                     'exhaustive': True,
+                                                                                     'set_stock_zero': True}, context={'active_ids': [parent_inventory_id]})
+        try:
+            self.registry('stock.fill.inventory').fill_inventory(self.cr, self.uid, [wizard_id], context={'active_ids': [parent_inventory_id]})
+        except Exception, e:
+            self.assertEqual(e.value, 'No product in this location. Please select a location in the product form.', "The message should be ''No product in this location. Please select a location in the product form.''")
+            exception_happened = True
+            pass
+        self.assertTrue(exception_happened)
+        inventory_line_ids = self.registry('stock.inventory.line').search(self.cr, self.uid, [('inventory_id', '=', parent_inventory_id)])
+        self.assertEqual(len(inventory_line_ids), 0, "No inventory line is fount after filling inventory")
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

=== added file 'stock_inventory_hierarchical_location/tests/inventory_hierarchical_location_test.yml'
--- stock_inventory_hierarchical_location/tests/inventory_hierarchical_location_test.yml	1970-01-01 00:00:00 +0000
+++ stock_inventory_hierarchical_location/tests/inventory_hierarchical_location_test.yml	2014-06-30 09:28:51 +0000
@@ -0,0 +1,56 @@
+-
+  Check that the exhaustive field of parent inventory has been propagated to children.
+-
+  !python {model: stock.inventory}: |
+    exhaustive = self.read(cr, uid, [ref("child_1_id")], ['exhaustive'])[0]['exhaustive']
+    assert exhaustive, "Exhaustive field not propagated to child inventory"
+
+-
+  Check that I can't open child inventory while parent inventory is open.
+-
+  !python {model: stock.inventory}: |
+    from stock_inventory_hierarchical import HierarchicalInventoryException
+    parent_state = self.read(cr, uid, [ref("parent_inventory")], ['state'])[0]['state']
+    assert parent_state == 'draft', "Parent inventory in state '%s'. It should be 'draft'" % parent_state
+    try:
+      self.action_open(cr, uid, [ref("child_1_id")])
+    except HierarchicalInventoryException as e:
+      log("Good ! The Inventory could not be opened: %s" % e)
+    child_1_state = self.read(cr, uid, [ref("child_1_id")], ['state'])[0]['state']
+    assert child_1_state == 'draft', "Child inventory 1 have '%s' state. It should be 'draft'" % child_1_state
+
+-
+  I will check that the function get_missing_locations return some locations.
+-
+  !python {model: stock.inventory}: |
+    missing_loc_ids = self.get_missing_locations(cr, uid, [ref('parent_inventory')], context=context)
+    assert len(missing_loc_ids)==3, "get_missing_locations did not return any ID."
+
+-
+  I will fill the inventory and check that the function get_missing_locations return no locations.
+  Adding 17” LCD Monitor.
+-
+  !record {model: stock.inventory.line, id: lines_inventory_location_pc1}:
+    product_id: product.product_product_7
+    product_uom: product.product_uom_unit
+    company_id: base.main_company
+    inventory_id: child_1_id
+    product_qty: 18.0
+    location_id: stock.stock_location_14
+
+-
+  Adding USB Keyboard, QWERTY.
+-
+  !record {model: stock.inventory.line, id: lines_inventory_location_pc3}:
+    product_id: product.product_product_8
+    product_uom: product.product_uom_unit
+    company_id: base.main_company
+    inventory_id: child_2_id
+    product_qty: 5.0
+    location_id: stock.stock_location_components
+
+-
+  !python {model: stock.inventory}: |
+    
+    missing_loc_ids = self.get_missing_locations(cr, uid, [ref('parent_inventory')], context=context)
+    assert set(missing_loc_ids)==set([ref('stock.stock_location_stock')]), "get_missing_locations should return only %s but returned %s" % ([ref('stock.stock_location_stock')], missing_loc_ids)

=== added directory 'stock_inventory_hierarchical_location/wizard'
=== added file 'stock_inventory_hierarchical_location/wizard/__init__.py'
--- stock_inventory_hierarchical_location/wizard/__init__.py	1970-01-01 00:00:00 +0000
+++ stock_inventory_hierarchical_location/wizard/__init__.py	2014-06-30 09:28:51 +0000
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+#    This module is copyright (C) 2013 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 . import stock_fill_location_inventory
+from . import generate_inventory

=== added file 'stock_inventory_hierarchical_location/wizard/generate_inventory.py'
--- stock_inventory_hierarchical_location/wizard/generate_inventory.py	1970-01-01 00:00:00 +0000
+++ stock_inventory_hierarchical_location/wizard/generate_inventory.py	2014-06-30 09:28:51 +0000
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+#
+#
+#    Authors: Laetitia Gangloff
+#    Copyright (c) 2014 Acsone SA/NV (http://www.acsone.eu)
+#    All Rights Reserved
+#
+#    WARNING: This program as such is intended to be used by professional
+#    programmers who take the whole responsibility of assessing all potential
+#    consequences resulting from its eventual inadequacies and bugs.
+#    End users who are looking for a ready-to-use solution with commercial
+#    guarantees and support are strongly advised to contact a Free Software
+#    Service Company.
+#
+#    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 fields, orm
+from openerp.tools.translate import _
+
+
+class GenerateInventoryWizard(orm.TransientModel):
+    """ This wizard generate an inventory and all related sub-inventories for the specified location and level
+    Example: location = Stock / level = 1 => 1 inventory on Stock (similar to create)
+             location = Stock / level = 2 => 1 inventory on Stock, 1 sub-inventory on Shelf1, 1 sub-inventory on Shelf2
+    """
+
+    _name = "stock.generate.inventory"
+    _description = "Generate Inventory"
+
+    _columns = {
+        'prefix_inv_name': fields.char('Inventory prefix', help="Optional prefix for all created inventory"),
+        'location_id': fields.many2one('stock.location', 'Location', required=True),
+        'level': fields.integer("Level", help="number of level between inventory on location_id and sub-inventory"),
+        'only_view': fields.boolean('Only view', help="If set, only inventory on view location can be created"),
+    }
+
+    def _default_location(self, cr, uid, ids, context=None):
+        """Default stock location
+
+        @return: id of the stock location of the first warehouse of the
+        default company"""
+        location_id = False
+        company_id = self.pool['res.company']._company_default_get(
+            cr, uid, 'stock.warehouse', context=context)
+        warehouse_id = self.pool['stock.warehouse'].search(
+            cr, uid, [('company_id', '=', company_id)], limit=1)
+        if warehouse_id:
+            location_id = self.pool['stock.warehouse'].read(
+                cr, uid, warehouse_id[0], ['lot_stock_id'])['lot_stock_id'][0]
+        return location_id
+
+    _defaults = {
+        'location_id': _default_location,
+        'level': 1,
+        'only_view': True,
+    }
+
+    _sql_constraints = [
+        ('level', 'CHECK (level>0)', 'Level must be positive!'),
+    ]
+
+    def _create_subinventory(self, cr, uid, inventory_ids, prefix_inv_name, only_view, context):
+        new_inventory_ids = []
+        for inventory_id in inventory_ids:
+            location_id = self.pool['stock.inventory'].read(cr, uid, inventory_id, ['location_id'], context=context)['location_id'][0]
+            domain = [('location_id', '=', location_id)]
+            if only_view:
+                domain.append(('usage', '=', 'view'))
+            location_ids = self.pool['stock.location'].search(cr, uid, domain, context=context)
+            for location_id in location_ids:
+                location_name = self.pool['stock.location'].read(cr, uid, location_id, ['name'], context=context)['name']
+                new_inventory_ids.append(self.pool['stock.inventory'].create(cr, uid, {'name': prefix_inv_name + location_name,
+                                                                                       'exhaustive': True,
+                                                                                       'location_id': location_id,
+                                                                                       'parent_id': inventory_id}, context=context))
+        return new_inventory_ids
+
+    def generate_inventory(self, cr, uid, ids, context=None):
+        """ Generate inventory and sub-inventories for specified location and level
+
+        @param self: The object pointer.
+        @param cr: A database cursor
+        @param uid: ID of the user currently logged in
+        @param ids: the ID or list of IDs if we want more than one
+        @param context: A standard dictionary
+        @return:
+        """
+        if context is None:
+            context = {}
+
+        if ids and len(ids):
+            ids = ids[0]
+        else:
+            return {'type': 'ir.actions.act_window_close'}
+        generate_inventory = self.browse(cr, uid, ids, context=context)
+        # create first level inventory
+        prefix_inv_name = generate_inventory.prefix_inv_name or ''
+        location_id = generate_inventory.location_id.id
+        only_view = generate_inventory.only_view
+        parent_inventory_id = self.pool['stock.inventory'].create(cr, uid, {'name': prefix_inv_name + generate_inventory.location_id.name,
+                                                                            'exhaustive': True,
+                                                                            'location_id': location_id}, context=context)
+
+        inventory_ids = [parent_inventory_id]
+        for i in range(1, generate_inventory.level):
+            inventory_ids = self._create_subinventory(cr, uid, inventory_ids, prefix_inv_name, only_view, context)
+
+        mod_obj = self.pool['ir.model.data']
+        result = mod_obj.get_object_reference(cr, uid, 'stock', 'view_inventory_form')
+        view_id = result and result[1] or False
+        return {'name': _('Inventory generated'),
+                'view_mode': 'form',
+                'view_type': 'form',
+                'res_model': 'stock.inventory',
+                'type': 'ir.actions.act_window',
+                'view_id': view_id,
+                'res_id': int(parent_inventory_id),
+                }
+
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

=== added file 'stock_inventory_hierarchical_location/wizard/generate_inventory_view.xml'
--- stock_inventory_hierarchical_location/wizard/generate_inventory_view.xml	1970-01-01 00:00:00 +0000
+++ stock_inventory_hierarchical_location/wizard/generate_inventory_view.xml	2014-06-30 09:28:51 +0000
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+    <data>
+           <record id="view_stock_generate_inventory" model="ir.ui.view">
+            <field name="name">Generate Inventory</field>
+            <field name="model">stock.generate.inventory</field>
+            <field name="arch" type="xml">
+              <form string="Generate Inventory" version="7.0">
+                  <separator string="Generate inventory"/>
+                  <group>
+                      <field name="prefix_inv_name"/>
+                      <field name="location_id"/>
+                      <field name="only_view"/>
+                      <field name="level"/>
+                  </group>
+                  <footer>
+                      <button name="generate_inventory" string="Generate Inventory" type="object" class="oe_highlight"/>
+                      or
+                      <button string="Cancel" class="oe_link" special="cancel" />
+                  </footer>
+              </form>
+            </field>
+        </record>
+
+        <record id="action_view_stock_generate_inventory" model="ir.actions.act_window">
+            <field name="name">Generate Inventory</field>
+            <field name="type">ir.actions.act_window</field>
+            <field name="res_model">stock.generate.inventory</field>
+            <field name="view_type">form</field>
+            <field name="view_mode">form</field>
+            <field name="view_id" ref="view_stock_generate_inventory"/>
+            <field name="target">new</field>
+        </record>
+
+    <menuitem action="action_view_stock_generate_inventory"
+              id="menu_action_stock_generate_inventory_form"
+              parent="stock.menu_stock_inventory_control"
+              sequence="20"
+              groups="stock.group_locations"/>
+
+    </data>
+</openerp>

=== added file 'stock_inventory_hierarchical_location/wizard/stock_fill_location_inventory.py'
--- stock_inventory_hierarchical_location/wizard/stock_fill_location_inventory.py	1970-01-01 00:00:00 +0000
+++ stock_inventory_hierarchical_location/wizard/stock_fill_location_inventory.py	2014-06-30 09:28:51 +0000
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+#
+#
+#    Authors: Laetitia Gangloff
+#    Copyright (c) 2014 Acsone SA/NV (http://www.acsone.eu)
+#    All Rights Reserved
+#
+#    WARNING: This program as such is intended to be used by professional
+#    programmers who take the whole responsibility of assessing all potential
+#    consequences resulting from its eventual inadequacies and bugs.
+#    End users who are looking for a ready-to-use solution with commercial
+#    guarantees and support are strongly advised to contact a Free Software
+#    Service Company.
+#
+#    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, osv
+from openerp.tools.translate import _
+
+
+class FillInventoryWizard(orm.TransientModel):
+    """If inventory as sub inventories, do not fill with sub inventories location"""
+    _inherit = 'stock.fill.inventory'
+
+    def fill_inventory(self, cr, uid, ids, context=None):
+        """ To Import stock inventory according to products available in the location and not already in a sub inventory
+
+        We split fill_inventory on many fill_inventory (one for each location)
+        @param self: The object pointer.
+        @param cr: A database cursor
+        @param uid: ID of the user currently logged in
+        @param ids: the ID or list of IDs if we want more than one
+        @param context: A standard dictionary
+        @return:
+        """
+        if context is None:
+            context = {}
+
+        if ids and len(ids):
+            ids = ids[0]
+        else:
+            return {'type': 'ir.actions.act_window_close'}
+        fill_inventory = self.browse(cr, uid, ids, context=context)
+        if fill_inventory.recursive and fill_inventory.exhaustive:
+            exclude_location_ids = []
+            for i in self.pool['stock.inventory'].browse(cr, uid, context['active_ids']):
+                for sub_inventory in i.inventory_ids:
+                    # exclude these location
+                    exclude_location_ids.append(sub_inventory.location_id.id)
+            domain = [('location_id', 'child_of', [fill_inventory.location_id.id])]
+            if exclude_location_ids:
+                domain.append('!')
+                domain.append(('location_id', 'child_of', exclude_location_ids))
+            location_ids = self.pool['stock.location'].search(cr, uid, domain,
+                                                              order="id",
+                                                              context=context)
+            all_in_exception = 0
+            for location_id in location_ids:
+                try:
+                    super(FillInventoryWizard, self).fill_inventory(cr, uid,
+                                                                    [self.copy(cr, uid, ids, {'location_id': location_id,
+                                                                                              'recursive': False, }, context=context)],
+                                                                    context=context)
+                except osv.except_osv, e:
+                    if e.value == _('No product in this location. Please select a location in the product form.'):
+                        all_in_exception = all_in_exception + 1
+                        pass
+                    else:
+                        raise e
+            if all_in_exception == len(location_ids):
+                raise osv.except_osv(_('Warning!'), _('No product in this location. Please select a location in the product form.'))
+            return {'type': 'ir.actions.act_window_close'}
+        else:
+            return super(FillInventoryWizard, self).fill_inventory(cr, uid, [ids], context=context)
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:


Follow ups