openerp-community-reviewer team mailing list archive
-
openerp-community-reviewer team
-
Mailing list archive
-
Message #07458
[Merge] lp:~acsone-openerp/stock-logistic-warehouse/7.0-inventory-hierarchical-location-fix-subinventory-locations into lp:stock-logistic-warehouse
Laetitia Gangloff (Acsone) has proposed merging lp:~acsone-openerp/stock-logistic-warehouse/7.0-inventory-hierarchical-location-fix-subinventory-locations into lp:stock-logistic-warehouse.
Requested reviews:
Stock and Logistic Core Editors (stock-logistic-core-editors)
For more details, see:
https://code.launchpad.net/~acsone-openerp/stock-logistic-warehouse/7.0-inventory-hierarchical-location-fix-subinventory-locations/+merge/224774
Add exhaustive parameter for fill inventory.
And encapsulate in try catch to confirm even if there is no product to add
--
https://code.launchpad.net/~acsone-openerp/stock-logistic-warehouse/7.0-inventory-hierarchical-location-fix-subinventory-locations/+merge/224774
Your team Stock and Logistic Core Editors is requested to review the proposed merge of lp:~acsone-openerp/stock-logistic-warehouse/7.0-inventory-hierarchical-location-fix-subinventory-locations 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-27 09:18:58 +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-27 09:18:58 +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-27 09:18:58 +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-27 09:18:58 +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-27 09:18:58 +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-27 09:18:58 +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-27 09:18:58 +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-27 09:18:58 +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-27 09:18:58 +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-27 09:18:58 +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-27 09:18:58 +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-27 09:18:58 +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-27 09:18:58 +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-27 09:18:58 +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-27 09:18:58 +0000
@@ -0,0 +1,100 @@
+# -*- 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 = set(
+ 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]
+ # 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(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-27 09:18:58 +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-27 09:18:58 +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-27 09:18:58 +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-27 09:18:58 +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-27 09:18:58 +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-27 09:18:58 +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-27 09:18:58 +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-27 09:18:58 +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-27 09:18:58 +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-27 09:18:58 +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 orm.except_orm(_('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:
=== added directory 'stock_inventory_location'
=== added file 'stock_inventory_location/__init__.py'
--- stock_inventory_location/__init__.py 1970-01-01 00:00:00 +0000
+++ stock_inventory_location/__init__.py 2014-06-27 09:18:58 +0000
@@ -0,0 +1,24 @@
+# -*- 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/>.
+#
+##############################################################################
+
+import stock_inventory_location
+import wizard
+# Bring the main exception into the package's scope for easier reuse
+from .exceptions import ExhaustiveInventoryException
=== added file 'stock_inventory_location/__openerp__.py'
--- stock_inventory_location/__openerp__.py 1970-01-01 00:00:00 +0000
+++ stock_inventory_location/__openerp__.py 2014-06-27 09:18:58 +0000
@@ -0,0 +1,77 @@
+# -*- 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 Stock Inventories",
+ "version": "1.1",
+ "depends": ["stock"],
+ "author": u"Numérigraphe",
+ "category": "Warehouse Management",
+ "description": """
+Let users make exhaustive Inventories
+=====================================
+
+Standard Physical Inventories in OpenERP only contain a generic list of
+products by locations, which is well suited to partial Inventories and simple
+warehouses. When the a standard Inventory is confirmed, only the products in
+the inventory are checked. If a Product is present in the computed stock and
+not in the recorded Inventory, OpenERP will consider that it remains unchanged.
+
+But for exhaustive inventories in complex warehouses, it is not practical:
+ - you want to avoid Stock Moves to/from these Locations while counting goods
+ - you must make sure all the locations you want have been counted
+ - you must make sure no other location has been counted by mistake
+ - you want the computed stock to perfectly match the inventory when you
+ confirm it.
+
+This module lets choose whether an Physical Inventory is exhaustive or
+standard.
+For an exhaustive Inventory:
+ - in the state "Draft" you define the Location where goods must be counted.
+ - the new Inventory status "Open" lets you indicate that the list of Locations
+ is final and you are now counting the goods.
+ In that status, no Stock Moves can be recorded in/out of the Inventory's
+ Locations.
+ - if the Location or some of it's children have not been entered in the
+ Inventory Lines, OpenERP warns you when you confirm the Inventory.
+ - only the Inventory's Location or its children can be entered in the
+ Inventory Lines.
+ - every good that is not in the Inventory Lines is considered lost, and gets
+ moved out of the stock when you confirm the Inventory.
+""",
+ "data": [
+ "wizard/stock_confirm_uninventoried_location.xml",
+ "stock_inventory_location_view.xml",
+ "wizard/stock_fill_location_inventory_view.xml",
+ ],
+ "test": [
+ "tests/inventory_standard_test.yml",
+ "tests/inventory_exhaustive_test.yml",
+ "tests/inventory_future_test.yml",
+ ],
+ "images": [
+ "images/inventory_form.png",
+ "inventory_empty_locations.png",
+ "images/move_error.png",
+ "images/location_locked.png",
+ "images/future_inventory.png",
+ ],
+ "demo": ["stock_inventory_location_demo.xml"]
+}
=== added file 'stock_inventory_location/exceptions.py'
--- stock_inventory_location/exceptions.py 1970-01-01 00:00:00 +0000
+++ stock_inventory_location/exceptions.py 2014-06-27 09:18:58 +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 ExhaustiveInventoryException(orm.except_orm):
+ """The operation is not possible for an exhaustive inventory"""
+ pass
=== added directory 'stock_inventory_location/i18n'
=== added file 'stock_inventory_location/i18n/fr.po'
--- stock_inventory_location/i18n/fr.po 1970-01-01 00:00:00 +0000
+++ stock_inventory_location/i18n/fr.po 2014-06-27 09:18:58 +0000
@@ -0,0 +1,241 @@
+# Translation of OpenERP Server.
+# This file contains the translation of the following modules:
+# * stock_inventory_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:58+0000\n"
+"PO-Revision-Date: 2013-09-25 13:58+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_location
+#: code:addons/stock_inventory_location/stock_inventory_location.py:232
+#: constraint:stock.move:0
+#, python-format
+msgid "A Physical Inventory is being conducted at this location"
+msgstr "Un inventaire est en cours à cet emplacement"
+
+#. module: stock_inventory_location
+#: view:stock.inventory.uninventoried.locations:0
+msgid "Cancel"
+msgstr "Annuler"
+
+#. module: stock_inventory_location
+#: view:stock.inventory:0
+msgid "Cancel Inventory"
+msgstr "Annuler l'inventaire"
+
+#. module: stock_inventory_location
+#: help:stock.inventory,exhaustive:0
+msgid "Check the box if you are conducting an exhaustive Inventory.\n"
+"Leave the box unchecked if you are conducting a standard Inventory (partial inventory for example).\n"
+"For an exhaustive Inventory:\n"
+" - the status \"Draft\" lets you define the list of Locations where goods must be counted\n"
+" - the status \"Open\" indicates that the list of Locations is definitive and you are now counting the goods. In that status, no Stock Moves can be recorded in/out of the Inventory's Locations\n"
+" - only the Inventory's Locations can be entered in the Inventory Lines\n"
+" - if some of the Inventory's Locations have not been entered in the Inventory Lines, OpenERP warns you when you confirm the Inventory\n"
+" - every good that is not in the Inventory Lines is considered lost, and gets moved out of the stock when you confirm the Inventory"
+msgstr "Cochez la case si vous effectuez un inventaire exhaustif.\n"
+"Laissez la case non-dochée si vous effectuez un inventaire standard (par exmple un inventaire partiel).\n"
+"Pour les inventaires exhaustifs :\n"
+" - le statut \"Brouillon\" permet d'indiquer la liste des emplacements dont la marchandise doit être comptée\n"
+" - le statut \"Ouvert\" indiqueque la liste des emplacements est définitive, et que le comptage est en cours. Dans ce statut, aucun mouvement de stock ne peut être enregistré depuis/vers les emplacements de l'inventaire\n"
+" - seuls les emplacements de l'inventaire peuvent être saisis dans les lignes d'inventaire\n"
+" - si certains emplacements sont absent des lignes d'inventaire, OpenERP vous en avertit lors de la confirmation de l'inventaire\n"
+" - toutes les marchandises qui ne sont pas dans les lignes d'inventaire sont considérées comme perdues, et sont supprimées lors de la confirmation de l'inventaire"
+
+#. module: stock_inventory_location
+#: view:stock.inventory:0
+msgid "Confirm Inventory"
+msgstr "Confirmer l'inventaire"
+
+#. module: stock_inventory_location
+#: model:ir.model,name:stock_inventory_location.model_stock_inventory_uninventoried_locations
+msgid "Confirm the uninventoried Locations."
+msgstr "Confirmer les emplacements non inventoriés."
+
+#. module: stock_inventory_location
+#: view:stock.inventory.uninventoried.locations:0
+msgid "Confirm empty locations"
+msgstr "Confirmez les emplacements vides"
+
+#. module: stock_inventory_location
+#: model:ir.model,name:stock_inventory_location.model_stock_location
+msgid "Emplacement"
+msgstr "Emplacement"
+
+#. module: stock_inventory_location
+#: code:addons/stock_inventory_location/wizard/stock_fill_location_inventory.py:54
+#, python-format
+msgid "Error"
+msgstr "Erreur"
+
+#. module: stock_inventory_location
+#: code:addons/stock_inventory_location/wizard/stock_fill_location_inventory.py:75
+#, python-format
+msgid "Error: Empty location"
+msgstr "Erreur : Emplacement vide"
+
+#. module: stock_inventory_location
+#: code:addons/stock_inventory_location/stock_inventory_location.py:231
+#: code:addons/stock_inventory_location/stock_inventory_location.py:282
+#, python-format
+msgid "Error: Location locked down"
+msgstr "Erreur: emplacement en inventaire"
+
+#. module: stock_inventory_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_location
+#: constraint:stock.inventory.line:0
+msgid "Error: duplicates lines"
+msgstr "Erreur: lignes en double"
+
+#. module: stock_inventory_location
+#: view:stock.inventory:0
+#: field:stock.inventory,exhaustive:0
+msgid "Exhaustive"
+msgstr "Exhaustif"
+
+#. module: stock_inventory_location
+#: model:ir.model,name:stock_inventory_location.model_stock_inventory
+msgid "Gestion des stocks"
+msgstr "Gestion des stocks"
+
+#. module: stock_inventory_location
+#: view:stock.inventory.uninventoried.locations:0
+msgid "If you confirm the Inventory, these Locations will be considered empty and their content will be purged."
+msgstr "Si vous confirmez l'inventaire, ces emplacements seront considérés comme vides et leur contenu sera supprimé."
+
+#. module: stock_inventory_location
+#: model:ir.model,name:stock_inventory_location.model_stock_fill_inventory
+msgid "Importer un inventaire"
+msgstr "Importer un inventaire"
+
+#. module: stock_inventory_location
+#: view:stock.inventory.uninventoried.locations:0
+msgid "It could either mean that the Locations are empty, or that the Inventory is not yet complete."
+msgstr "Cela peut vouloir dire soit que l'inventaire n'est pas terminé, soit que ces emplacements sont effectivement vides."
+
+#. module: stock_inventory_location
+#: model:ir.model,name:stock_inventory_location.model_stock_inventory_line
+msgid "Ligne d'inventaire"
+msgstr "Ligne d'inventaire"
+
+#. module: stock_inventory_location
+#: code:addons/stock_inventory_location/stock_inventory_location.py:59
+#, python-format
+msgid "Location missing for this inventory."
+msgstr "Emplacement manquant pour cet inventaire."
+
+#. module: stock_inventory_location
+#: model:ir.model,name:stock_inventory_location.model_stock_move
+msgid "Mouvement de stock"
+msgstr "Mouvement de stock"
+
+#. module: stock_inventory_location
+#: code:addons/stock_inventory_location/wizard/stock_fill_location_inventory.py:75
+#, python-format
+msgid "No location to import.\n"
+"You must add a location on the locations list."
+msgstr "Pas d'emplacement à importer.\n"
+"Vous devez ajouter un emplacement dans la liste des emplacements."
+
+#. module: stock_inventory_location
+#: code:addons/stock_inventory_location/wizard/stock_fill_location_inventory.py:58
+#, python-format
+msgid "No locations found for the inventory."
+msgstr "Pas d'emplacement trouvé pour cet inventaire."
+
+#. module: stock_inventory_location
+#: code:addons/stock_inventory_location/stock_inventory_location.py:163
+#, python-format
+msgid "No product in this location."
+msgstr "Pas de produit à cet emplacement."
+
+#. module: stock_inventory_location
+#: code:addons/stock_inventory_location/stock_inventory_location.py:283
+#, python-format
+msgid "A Physical Inventory is being conducted at the following location(s):\n"
+"%s"
+msgstr "Les emplacements suivants sont en inventaire :\n"
+"%s"
+
+#. module: stock_inventory_location
+#: view:stock.inventory:0
+msgid "Open Inventory"
+msgstr "Ouvrir l'inventaire"
+
+#. module: stock_inventory_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_location
+#: view:stock.inventory.uninventoried.locations:0
+msgid "Purge contents and confirm Inventory"
+msgstr "Supprimer le contenu et confirmer l'inventaire"
+
+#. module: stock_inventory_location
+#: view:stock.inventory.uninventoried.locations:0
+msgid "The following Stock Locations are part of the current Physical Inventory, but no Inventory Line has been recorded for them."
+msgstr "Les emplacements suivants font partie de l'inventaire en cours, mais aucune ligne d'inventaire les concernant n'a été enregistrée."
+
+#. module: stock_inventory_location
+#: field:stock.inventory.uninventoried.locations,location_ids:0
+msgid "Uninventoried location"
+msgstr "Emplacements non inventoriés"
+
+#. module: stock_inventory_location
+#: code:addons/stock_inventory_location/stock_inventory_location.py:59
+#: code:addons/stock_inventory_location/stock_inventory_location.py:163
+#: code:addons/stock_inventory_location/wizard/stock_fill_location_inventory.py:58
+#, python-format
+msgid "Warning"
+msgstr "Attention"
+
+#. module: stock_inventory_location
+#: code:addons/stock_inventory_location/stock_inventory_location.py:209
+#, python-format
+msgid "Warning: Wrong location"
+msgstr "Attention: emplacement incorrect"
+
+#. module: stock_inventory_location
+#: code:addons/stock_inventory_location/stock_inventory_location.py:210
+#, python-format
+msgid "You cannot record an Inventory Line for this Location.\n"
+"You must first add this Location to the list of affected Locations on the Inventory form."
+msgstr "Vous ne pouvez pas ajouter cet emplacement.\n"
+"Il ne fait pas partie de la liste des emplacements à inventorier"
+
+#. module: stock_inventory_location
+#: constraint:stock.move:0
+msgid "You must assign a production lot for this product"
+msgstr "Vous devez affecter un lot de fabrication à ce produit."
+
+#. module: stock_inventory_location
+#: constraint:stock.inventory.line:0
+msgid "You must not create same inventory line Product, Location, Lot on the same date"
+msgstr "Vous ne pouvez pas créer plusieurs lignes d'inventaires pour le même produit, lot, emplacement à la même date"
+
+#. module: stock_inventory_location
+#: constraint:stock.move:0
+msgid "You try to assign a lot which is not from the same product"
+msgstr "Vous essayez d'affecter un lot qui n'est pas pour ce produit."
+
+
+#. module: stock_inventory_location
+#: code:addons/stock_inventory_location/wizard/stock_fill_location_inventory.py:54
+#, python-format
+msgid "the inventory must be in \"Open\" state."
+msgstr "l'inventaire doit etre \"Ouvert\"."
+
=== added directory 'stock_inventory_location/images'
=== added file 'stock_inventory_location/images/future_inventory.png'
Binary files stock_inventory_location/images/future_inventory.png 1970-01-01 00:00:00 +0000 and stock_inventory_location/images/future_inventory.png 2014-06-27 09:18:58 +0000 differ
=== added file 'stock_inventory_location/images/inventory_empty_locations.png'
Binary files stock_inventory_location/images/inventory_empty_locations.png 1970-01-01 00:00:00 +0000 and stock_inventory_location/images/inventory_empty_locations.png 2014-06-27 09:18:58 +0000 differ
=== added file 'stock_inventory_location/images/inventory_form.png'
Binary files stock_inventory_location/images/inventory_form.png 1970-01-01 00:00:00 +0000 and stock_inventory_location/images/inventory_form.png 2014-06-27 09:18:58 +0000 differ
=== added file 'stock_inventory_location/images/location_locked.png'
Binary files stock_inventory_location/images/location_locked.png 1970-01-01 00:00:00 +0000 and stock_inventory_location/images/location_locked.png 2014-06-27 09:18:58 +0000 differ
=== added file 'stock_inventory_location/images/move_error.png'
Binary files stock_inventory_location/images/move_error.png 1970-01-01 00:00:00 +0000 and stock_inventory_location/images/move_error.png 2014-06-27 09:18:58 +0000 differ
=== added directory 'stock_inventory_location/static'
=== added directory 'stock_inventory_location/static/src'
=== added directory 'stock_inventory_location/static/src/img'
=== added file 'stock_inventory_location/static/src/img/icon.png'
Binary files stock_inventory_location/static/src/img/icon.png 1970-01-01 00:00:00 +0000 and stock_inventory_location/static/src/img/icon.png 2014-06-27 09:18:58 +0000 differ
=== added file 'stock_inventory_location/stock_inventory_location.py'
--- stock_inventory_location/stock_inventory_location.py 1970-01-01 00:00:00 +0000
+++ stock_inventory_location/stock_inventory_location.py 2014-06-27 09:18:58 +0000
@@ -0,0 +1,353 @@
+# -*- 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/>.
+#
+##############################################################################
+
+import time
+from collections import Iterable
+
+from openerp.osv import orm, fields
+from openerp.tools.translate import _
+# The next 2 imports are only needed for a feature backported from trunk-wms
+# TODOv8! remove, feature is included upstream
+from openerp.osv import osv
+from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
+from openerp import SUPERUSER_ID
+
+from .exceptions import ExhaustiveInventoryException
+
+
+class StockInventory(orm.Model):
+ """Add locations to the inventories"""
+ _inherit = 'stock.inventory'
+
+ INVENTORY_STATE_SELECTION = [
+ ('draft', 'Draft'),
+ ('open', 'Open'),
+ ('done', 'Done'),
+ ('confirm', 'Confirmed'),
+ ('cancel', 'Cancelled')
+ ]
+
+ _columns = {
+ # TODO v8: should we use "confirm" instead of adding "open"?
+ 'state': fields.selection(
+ INVENTORY_STATE_SELECTION, 'State', readonly=True, select=True),
+ # TODO v8: remove this, should not be needed anymore
+ # Make the inventory lines read-only in all states except "Open",
+ # to ensure that no unwanted Location can be inserted
+ 'inventory_line_id': fields.one2many(
+ 'stock.inventory.line', 'inventory_id', 'Inventory lines',
+ readonly=True, states={'open': [('readonly', False)]}),
+ # TODO v8: remove this, it's backported from v8
+ 'location_id': fields.many2one(
+ 'stock.location', 'Inventoried Location',
+ readonly=True, states={'draft': [('readonly', False)]}),
+ 'exhaustive': fields.boolean(
+ 'Exhaustive', readonly=True,
+ states={'draft': [('readonly', False)]},
+ help="Check the box if you are conducting an exhaustive "
+ "Inventory.\n"
+ "Leave the box unchecked if you are conducting a standard "
+ "Inventory (partial inventory for example).\n"
+ "For an exhaustive Inventory:\n"
+ " - the status \"Draft\" lets you define the list of "
+ "Locations where goods must be counted\n"
+ " - the status \"Open\" indicates that the list of Locations "
+ "is definitive and you are now counting the goods. In that "
+ "status, no Stock Moves can be recorded in/out of the "
+ "Inventory's Locations\n"
+ " - only the Inventory's Locations can be entered in the "
+ "Inventory Lines\n"
+ " - if some of the Inventory's Locations have not been "
+ "entered in the Inventory Lines, OpenERP warns you "
+ "when you confirm the Inventory\n"
+ " - every good that is not in the Inventory Lines is "
+ "considered lost, and gets moved out of the stock when you "
+ "confirm the Inventory"),
+ }
+
+ def action_open(self, cr, uid, ids, context=None):
+ """Change the state of the inventory to 'open'"""
+ return self.write(cr, uid, ids, {'state': 'open'}, context=context)
+
+ # TODO v8: remove this method? the feature looks already done upstream
+ def action_done(self, cr, uid, ids, context=None):
+ """
+ Don't allow to make an inventory with a date in the future.
+
+ This makes sure no stock moves will be introduced between the
+ moment you finish the inventory and the date of the Stock Moves.
+ Backported from trunk-wms:
+ revid:qdp-launchpad@xxxxxxxxxxx-20140317090656-o7lo22tzm8yuv3r8
+
+ @raise osv.except_osv:
+ We raise this exception on purpose instead of
+ ExhaustiveInventoryException to ensure forward-compatibility
+ with v8.
+ """
+ for inventory in self.browse(cr, uid, ids, context=None):
+ if inventory.date > time.strftime(DEFAULT_SERVER_DATETIME_FORMAT):
+ raise osv.except_osv(
+ _('Error!'),
+ _('It\'s impossible to confirm an inventory in the '
+ 'future. Please change the inventory date to proceed '
+ 'further.'))
+ return super(StockInventory, self).action_done(cr, uid, ids,
+ context=context)
+
+ # TODO: remove this in v8
+ 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 = {
+ 'state': 'draft',
+ 'exhaustive': False,
+ # TODO: remove this in v8
+ 'location_id': _default_location,
+ }
+
+ def _check_location_free_from_inventories(self, cr, uid, ids):
+ """
+ Verify that no other Inventory is being conducted on the same locations
+
+ Open Inventories are matched using the exact Location IDs,
+ excluding children.
+ """
+ # We don't get a context because we're called from a _constraint
+ for inventory in self.browse(cr, uid, ids):
+ if not inventory.exhaustive:
+ # never block standard inventories
+ continue
+ if self.search(cr, uid,
+ [('location_id', '=', inventory.location_id.id),
+ ('id', '!=', inventory.id),
+ ('date', '=', inventory.date),
+ ('exhaustive', '=', True)]):
+ # Quit as soon as one offending inventory is found
+ return False
+ return True
+
+ _constraints = [
+ (_check_location_free_from_inventories,
+ 'Other Physical inventories are being conducted using the same '
+ 'Locations.',
+ ['location_id', 'date', 'exhaustive'])
+ ]
+
+ def _get_locations_open_inventories(self, cr, uid, context=None):
+ """IDs of location in open exhaustive inventories, with children"""
+ inv_ids = self.search(
+ cr, uid, [('state', '=', 'open'), ('exhaustive', '=', True)],
+ context=context)
+ if not inv_ids:
+ # Early exit if no match found
+ return []
+ # List the Locations - normally all exhaustive inventories have one
+ location_ids = [inventory.location_id.id
+ for inventory in self.browse(cr, uid, inv_ids,
+ context=context)]
+ # Extend to the children Locations
+ return self.pool['stock.location'].search(
+ cr, uid,
+ [('location_id', 'child_of', set(location_ids)),
+ ('usage', '=', 'internal')],
+ context=context)
+
+ def get_missing_locations(self, cr, uid, ids, context=None):
+ """Compute the list of location_ids which are missing from the lines
+
+ Here, "missing" means the location is the inventory's location or one
+ of it's children, and the inventory does not contain any line with
+ this location."""
+ inventories = self.browse(cr, uid, ids, context=context)
+ # Find the locations of the inventories
+ inv_location_ids = [i.location_id.id for i in inventories]
+ # Extend to the children locations
+ inv_location_ids = set(self.pool['stock.location'].search(
+ cr, uid, [
+ ('location_id', 'child_of', inv_location_ids),
+ ('usage', '=', 'internal')], context=context))
+ # Find the locations already recorded in inventory lines
+ line_locations_ids = set([l.location_id.id
+ for i in inventories
+ for l in i.inventory_line_id])
+ return list(inv_location_ids - line_locations_ids)
+
+ def confirm_missing_locations(self, cr, uid, ids, context=None):
+ """Open wizard to confirm empty locations on exhaustive inventories"""
+ for inventory in self.browse(cr, uid, ids, context=context):
+ if (self.get_missing_locations(cr, uid, ids, context=context)
+ and inventory.exhaustive):
+ return {
+ 'type': 'ir.actions.act_window',
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_model': 'stock.inventory.uninventoried.locations',
+ 'target': 'new',
+ 'context': dict(context,
+ active_ids=ids,
+ active_id=ids[0]),
+ 'nodestroy': True,
+ }
+ return self.action_confirm(cr, uid, ids, context=context)
+
+
+class StockInventoryLine(orm.Model):
+ """Only allow the Inventory's Locations"""
+
+ _inherit = 'stock.inventory.line'
+
+ def _default_stock_location(self, cr, uid, context=None):
+ res = super(StockInventoryLine, self)._default_stock_location(
+ cr, uid, context=context)
+ if context is None or not context.get('location_id', False):
+ return res
+ else:
+ return context['location_id']
+
+ _defaults = {
+ 'location_id': _default_stock_location
+ }
+
+ def onchange_location_id(self, cr, uid, ids, inventory_location_id,
+ exhaustive, location_id, context=None):
+ """Warn if the new is not in the location list for this inventory."""
+ if not exhaustive:
+ # Don't check if partial inventory
+ return True
+
+ # search children of location
+ if location_id not in self.pool['stock.location'].search(
+ cr, uid, [('location_id', 'child_of', inventory_location_id)],
+ context=context):
+ return {
+ 'value': {'location_id': False},
+ 'warning': {
+ 'title': _('Warning: Wrong location'),
+ 'message': _(
+ "You cannot record an Inventory Line for this "
+ "Location.\n"
+ "You must first add this Location to the list of "
+ "affected Locations on the Inventory form.")}
+ }
+ return True
+
+
+class StockLocation(orm.Model):
+ """Refuse changes during exhaustive Inventories"""
+ _inherit = 'stock.location'
+ _order = 'name'
+
+ def _check_inventory(self, cr, uid, ids, context=None):
+ """Error if an exhaustive Inventory is being conducted here"""
+ inv_obj = self.pool['stock.inventory']
+ location_inventory_open_ids = inv_obj._get_locations_open_inventories(
+ cr, SUPERUSER_ID, context=context)
+ if not isinstance(ids, Iterable):
+ ids = [ids]
+ for inv_id in ids:
+ if inv_id in location_inventory_open_ids:
+ raise ExhaustiveInventoryException(
+ _('Error: Location locked down'),
+ _('A Physical Inventory is being conducted at this '
+ 'location'))
+ return True
+
+ def write(self, cr, uid, ids, vals, context=None):
+ """Refuse write if an inventory is being conducted"""
+ self._check_inventory(cr, uid, ids, context=context)
+ if not isinstance(ids, Iterable):
+ ids_to_check = [ids]
+ else:
+ # Copy the data to avoid changing 'ids', it would trigger an infinite recursion
+ ids_to_check = list(ids)
+ # If changing the parent, no inventory must conducted there either
+ if vals.get('location_id'):
+ ids_to_check.append(vals['location_id'])
+ self._check_inventory(cr, uid, ids_to_check, context=context)
+ return super(StockLocation, self).write(cr, uid, ids, vals,
+ context=context)
+
+ def create(self, cr, uid, vals, context=None):
+ """Refuse create if an inventory is being conducted at the parent"""
+ self._check_inventory(cr, uid, vals.get('location_id'),
+ context=context)
+ return super(StockLocation, self).create(cr, uid, vals,
+ context=context)
+
+ def unlink(self, cr, uid, ids, context=None):
+ """Refuse unlink if an inventory is being conducted"""
+ self._check_inventory(cr, uid, ids, context=context)
+ return super(StockLocation, self).unlink(cr, uid, ids, context=context)
+
+
+class StockMove(orm.Model):
+ """Refuse Moves during exhaustive Inventories"""
+
+ _inherit = 'stock.move'
+
+ # TODOv7: adapt this to match trunk-wms
+ def _check_open_inventory_location(self, cr, uid, ids, context=None):
+ """
+ Check that the locations are not locked by an open inventory
+
+ Standard inventories are not checked.
+
+ @raise ExhaustiveInventoryException: an error is raised if locations
+ are locked, instead of returning False, in order to pass an
+ extensive error message back to users.
+ """
+ message = ""
+ inv_obj = self.pool['stock.inventory']
+ locked_location_ids = inv_obj._get_locations_open_inventories(
+ cr, SUPERUSER_ID, context=context)
+ if not locked_location_ids:
+ # Nothing to verify
+ return True
+ for move in self.browse(cr, uid, ids, context=context):
+ if (move.location_id.usage != 'inventory'
+ and move.location_dest_id.id in locked_location_ids):
+ message += " - %s\n" % (move.location_dest_id.name)
+ if (move.location_dest_id.usage != 'inventory'
+ and move.location_id.id in locked_location_ids):
+ message += " - %s\n" % (move.location_id.name)
+ if message:
+ raise ExhaustiveInventoryException(
+ _('Error: Location locked down'),
+ _('A Physical Inventory is being conducted at the following '
+ 'location(s):\n%s') % message)
+ return True
+
+ _constraints = [
+ (_check_open_inventory_location,
+ "A Physical Inventory is being conducted at this location",
+ ['location_id', 'location_dest_id']),
+ ]
=== added file 'stock_inventory_location/stock_inventory_location_demo.xml'
--- stock_inventory_location/stock_inventory_location_demo.xml 1970-01-01 00:00:00 +0000
+++ stock_inventory_location/stock_inventory_location_demo.xml 2014-06-27 09:18:58 +0000
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+ <data noupdate="0">
+ <!-- Record a non exhaustive inventory -->
+ <record id="inventory_standard" model="stock.inventory">
+ <field name="name">Standard inventory</field>
+ <field name="state">draft</field>
+ </record>
+
+ <!-- Record an exhaustive inventory -->
+ <record id="inventory_exhaustive" model="stock.inventory">
+ <field name="name">Exhaustive inventory</field>
+ <field name="state">draft</field>
+ <field name="exhaustive">True</field>
+ <field name="location_id" model="stock.location" search="[('name', '=', 'Shelf 2')]" />
+ </record>
+
+ <!-- Record an inventory dated in the future -->
+ <!-- TODOv8: remove this entry, only useful for a test already in trunk-wms -->
+ <record id="inventory_future" model="stock.inventory">
+ <field name="name">Inventory in the future</field>
+ <field name="state">draft</field>
+ <field name="date">2020-03-01 00:00:00</field>
+ </record>
+ </data>
+</openerp>
=== added file 'stock_inventory_location/stock_inventory_location_view.xml'
--- stock_inventory_location/stock_inventory_location_view.xml 1970-01-01 00:00:00 +0000
+++ stock_inventory_location/stock_inventory_location_view.xml 2014-06-27 09:18:58 +0000
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+ <data>
+
+ <record model="ir.ui.view" id="stock_inventory_location_form_view">
+ <field name="name">stock.inventory.location.form</field>
+ <field name="model">stock.inventory</field>
+ <field name="inherit_id" ref="stock.view_inventory_form"/>
+ <field name="priority" eval="10"/>
+ <field name="arch" type="xml">
+ <!-- Show the state 'done' in the statusbar -->
+ <xpath expr="/form//field[@name='state']" position="attributes">
+ <attribute name="statusbar_visible">draft,open,confirm</attribute>
+ </xpath>
+
+ <!-- TODO v8 place "exhaustive" next to "location_id" -->
+ <!-- Add type of inventory: standard or exhaustive. -->
+ <xpath expr="/form//field[@name='name']" position="after">
+ <field name="exhaustive"/>
+ <field name="location_id" groups="stock.group_locations"
+ domain="[('usage','in',('view', 'internal'))]"
+ attrs="{'invisible':[('exhaustive','!=',True)], 'required':[('exhaustive','=',True)]}"/>
+ </xpath>
+
+ <!-- Enable Fill Inventory button when state is open -->
+ <xpath expr="//button[@string='Fill Inventory']" position="attributes">
+ <attribute name="states">open</attribute>
+ <attribute name="context">{'default_exhaustive': exhaustive}</attribute>
+ </xpath>
+
+ <!-- Control locations added by user on inventory line -->
+ <xpath expr="/form//field[@name='inventory_line_id']"
+ position="attributes">
+ <attribute name="context">{'location_id': location_id}</attribute>
+ </xpath>
+ <xpath expr="/form//field[@name='inventory_line_id']/tree//field[@name='location_id']"
+ position="attributes">
+ <attribute name="on_change">onchange_location_id(parent.location_id, parent.exhaustive, location_id)</attribute>
+ </xpath>
+ <xpath expr="/form//field[@name='inventory_line_id']/form//field[@name='location_id']"
+ position="attributes">
+ <attribute name="on_change">onchange_location_id(parent.location_id, parent.exhaustive, location_id)</attribute>
+ </xpath>
+
+ <!-- Add button to open an inventory. Call wizard on confirm inventory -->
+ <xpath expr="/form//button[@name='action_cancel_inventory']" position="before">
+ <button name="action_open" states="draft" string="Open Inventory" type="object" class="oe_highlight" groups="stock.group_stock_user"/>
+ </xpath>
+ <!-- Show the "cancel" button in state "open" -->
+ <xpath expr="/form//button[@name='action_cancel_inventory']" position="attributes">
+ <attribute name="states">draft,open,confirm,done</attribute>
+ </xpath>
+ <!-- hijack the "confirm" button -->
+ <xpath expr="/form//button[@name='action_confirm']" position="attributes">
+ <attribute name="states">open</attribute>
+ <attribute name="name">confirm_missing_locations</attribute>
+ </xpath>
+ </field>
+ </record>
+
+ <!-- Add filter for complete or partial inventory -->
+ <record model="ir.ui.view" id="view_inventory_complete_filter">
+ <field name="name">complete.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="exhaustive" string="Exhaustive" domain="[('exhaustive', '=', True)]"
+ help="Only select exhaustive Inventories." />
+ <separator orientation="vertical"/>
+ </xpath>
+ </field>
+ </record>
+
+ <!-- Show exhaustive inventories by default -->
+ <record id="stock.action_inventory_form" model="ir.actions.act_window">
+ <field name="context">{'full':'1', 'search_default_exhaustive':1}</field>
+ </record>
+
+ </data>
+</openerp>
\ No newline at end of file
=== added directory 'stock_inventory_location/tests'
=== added file 'stock_inventory_location/tests/__init__.py'
--- stock_inventory_location/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ stock_inventory_location/tests/__init__.py 2014-06-27 09:18:58 +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 stock_inventory_location_test
+
+fast_suite = [
+]
+
+checks = [
+ stock_inventory_location_test,
+]
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
=== added file 'stock_inventory_location/tests/inventory_exhaustive_test.yml'
--- stock_inventory_location/tests/inventory_exhaustive_test.yml 1970-01-01 00:00:00 +0000
+++ stock_inventory_location/tests/inventory_exhaustive_test.yml 2014-06-27 09:18:58 +0000
@@ -0,0 +1,128 @@
+-
+ This file will test an exhaustive inventory.
+ I will call open_action method and check if state of inventories are 'open'.
+-
+ !python {model: stock.inventory}: |
+ self.action_open(cr, uid, [ref('inventory_exhaustive')])
+ inventory_state = self.read(cr, uid, [ref('inventory_exhaustive')], ['state'])[0]['state']
+ assert inventory_state == 'open', "Inventory in state '%s'. It should be 'open'" % inventory_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('inventory_exhaustive')], context=context)
+ assert len(missing_loc_ids), "get_missing_locations did not return any ID."
+-
+ I create a wizard record for stock_confirm_uninventoried_location to verify that it contains the uninventoried locations
+-
+ !python {model: stock.inventory.uninventoried.locations}: |
+ ctx = dict(context, active_ids=[ref('inventory_exhaustive')])
+ wizard_id = self.create(cr, uid, {}, context=ctx)
+ wizard = self.browse(cr, uid, wizard_id, context=ctx)
+ assert len(wizard.location_ids) > 0 , "The wizard does not contain any lines."
+-
+ I add products to exhaustive inventory.
+ 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: inventory_exhaustive
+ 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: inventory_exhaustive
+ product_qty: 5.0
+ location_id: stock.stock_location_14
+
+-
+ I will call the function _get_locations_open_inventories and check the result.
+ The function will return only the location_id of the exhaustive inventory.
+-
+ !python {model: stock.inventory}: |
+ locations = self._get_locations_open_inventories(cr, uid)
+ assert len(locations) == 1, "Function return wrong results: %s" % locations
+ assert locations[0] == ref('stock.stock_location_14'), "Function '_get_locations_open_inventories' return wrong location_id. Should be '%s': '%s'" % (stock.stock_location_14, locations[0])
+-
+ I will call the function onchange_location_id.
+ The function must return True in the first case, and return a warning dictionnary in the second test.
+-
+ !python {model: stock.inventory.line}: |
+ res = self.onchange_location_id(cr, uid, [], ref('stock.stock_location_14'), True, ref('stock.stock_location_14'))
+ assert res == True, "Exhaustive: The function 'onchange_location_id' should return True and return '%s'" % res
+ res = self.onchange_location_id(cr, uid, [], ref('stock.stock_location_14'), True, ref('stock.stock_location_components'))
+ assert res.get('warning', False) != False , "Function 'onchange_location_id': Warning not raise. ('%s)" % res
+
+-
+ I will check that the function get_missing_locations does not return any locations.
+-
+ !python {model: stock.inventory}: |
+ missing_loc_ids = self.get_missing_locations(cr, uid, [ref('inventory_exhaustive')], context=context)
+ assert not missing_loc_ids, "get_missing_locations should not return IDs but returned %s" % missing_loc_ids
+-
+ I create a wizard record for stock_confirm_uninventoried_location and validate it
+-
+ !python {model: stock.inventory.uninventoried.locations}: |
+ ctx = dict(context, active_ids=[ref('inventory_exhaustive')])
+ wizard_id = self.create(cr, uid, {}, context=ctx)
+ wizard = self.browse(cr, uid, wizard_id, context=ctx)
+ assert len(wizard.location_ids) == 0 , "The wizard should not contain any lines but contains %s." % wizard.location_ids
+ self.confirm_uninventoried_locations(cr, uid, wizard_id, context=ctx)
+-
+ Stock moves are not allowed in the locations during the inventory.
+-
+ !python {model: stock.move}: |
+ # TODOv8: remove this test, this is already part of trunk-wms
+ from stock_inventory_location import ExhaustiveInventoryException
+ try:
+ self.create(
+ cr,uid, {
+ 'name': 'Bad move',
+ 'location_id': ref('stock.stock_location_14'),
+ 'location_dest_id': ref('stock.stock_location_3'),
+ 'product_id': ref('product.product_product_8'),
+ 'product_uom': ref('product.product_uom_unit'),
+ 'date_expected': '2020-01-01 00:00:00'
+ })
+ except ExhaustiveInventoryException as e:
+ log("Good! The Stock Move was refused: %s" % e)
+-
+ I will confirm the exhaustive inventory
+-
+ !python {model: stock.inventory}: |
+ self.action_confirm(cr, uid, [ref('inventory_exhaustive')])
+ inventory_state = self.read(cr, uid, [ref('inventory_exhaustive')], ['state'])[0]['state']
+ assert inventory_state == 'confirm', "Exhaustive inventory is in state '%s'. It should be 'confirm'" % inventory_state
+
+-
+ I will validate the exhaustive inventory
+-
+ !python {model: stock.inventory}: |
+ self.action_done(cr, uid, [ref('inventory_exhaustive')])
+ inventory_state = self.read(cr, uid, [ref('inventory_exhaustive')], ['state'])[0]['state']
+ assert inventory_state == 'done', "Exhaustive inventory is in state '%s'. It should be 'done'" % inventory_state
+
+-
+ I will verify the quantity for each products.
+-
+ !python {model: product.product}: |
+ ctx = dict(context, location=[ref('stock.stock_location_14')])
+ prod_qty_avail = self.read(cr, uid, [ref('product.product_product_7')], ['qty_available'], context=ctx)[0]['qty_available']
+ assert prod_qty_avail == 18.0, "The stock of PC1 was not set to 18.0: %s" % prod_qty_avail
+
+ prod_qty_avail = self.read(cr, uid, [ref('product.product_product_8')], ['qty_available'], context=ctx)[0]['qty_available']
+ assert prod_qty_avail == 5.0, "The stock of PC3 was not set to 5.0: %s" % prod_qty_avail
+
+ prod_qty_avail = self.read(cr, uid, [ref('product.product_product_24')], ['qty_available'], context=ctx)[0]['qty_available']
+ assert prod_qty_avail == 0.0, "The stock of KEYA was not set to 0: %s" % prod_qty_avail
+
+ prod_qty_avail = self.read(cr, uid, [ref('product.product_product_25')], ['qty_available'], context=ctx)[0]['qty_available']
+ assert prod_qty_avail == 0.0, "The stock of MOU was not set to 0: %s" % prod_qty_avail
=== added file 'stock_inventory_location/tests/inventory_future_test.yml'
--- stock_inventory_location/tests/inventory_future_test.yml 1970-01-01 00:00:00 +0000
+++ stock_inventory_location/tests/inventory_future_test.yml 2014-06-27 09:18:58 +0000
@@ -0,0 +1,13 @@
+-
+ Check that an inventory with a date in the future cannot be opened.
+-
+ !python {model: stock.inventory}: |
+ # TODO v8: maybe this is already part of the new WMS
+ from osv.osv import except_osv
+ self.action_open(cr, uid, [ref('inventory_future')])
+ try:
+ self.action_done(cr, uid, [ref('inventory_future')])
+ except except_osv as e:
+ log("Good! The Inventory could not be opened: %s" % e)
+ inventory_state = self.read(cr, uid, [ref('inventory_future')], ['state'])[0]['state']
+ assert inventory_state != 'done', "Future inventory is done."
=== added file 'stock_inventory_location/tests/inventory_standard_test.yml'
--- stock_inventory_location/tests/inventory_standard_test.yml 1970-01-01 00:00:00 +0000
+++ stock_inventory_location/tests/inventory_standard_test.yml 2014-06-27 09:18:58 +0000
@@ -0,0 +1,100 @@
+-
+ This file will test a non exhaustive inventory.
+ I will call open_action method and check if state of inventories are 'open'.
+-
+ !python {model: stock.inventory}: |
+ self.action_open(cr, uid, [ref('inventory_standard')])
+ inventory_state = self.read(cr, uid, [ref('inventory_standard')], ['state'])[0]['state']
+ assert inventory_state == 'open', "Partial inventory have '%s' state. It should be 'open'" % inventory_state
+
+-
+ In order, I add products to inventory.
+ Adding Azerty Keyboard.
+-
+ !record {model: stock.inventory.line, id: lines_inventory_location_kbaz}:
+ product_id: product.product_product_9
+ product_uom: product.product_uom_unit
+ company_id: base.main_company
+ inventory_id: inventory_standard
+ product_qty: 18.0
+ location_id: stock.stock_location_components
+
+-
+ Adding Optical Mouse.
+-
+ !record {model: stock.inventory.line, id: lines_inventory_location_optm}:
+ product_id: product.product_product_10
+ product_uom: product.product_uom_unit
+ company_id: base.main_company
+ inventory_id: inventory_standard
+ product_qty: 12.0
+ location_id: stock.stock_location_components
+
+-
+ Adding Multimedia Speakers.
+-
+ !record {model: stock.inventory.line, id: lines_inventory_location_grca}:
+ product_id: product.product_template_31
+ product_uom: product.product_uom_unit
+ company_id: base.main_company
+ inventory_id: inventory_standard
+ product_qty: 32.0
+ location_id: stock.stock_location_components
+
+-
+ I will call the function _get_locations_open_inventories and check the result.
+ The function will return no locations because it's not an exhaustive inventory.
+-
+ !python {model: stock.inventory}: |
+ locations = self._get_locations_open_inventories(cr, uid)
+ assert len(locations) == 0, "Function return wrong results: %s" % locations
+
+-
+ I will call the function onchange_location_id.
+ The function must to return true in all test case.
+-
+ !python {model: stock.inventory.line}: |
+ res = self.onchange_location_id(cr, uid, [], [(6,0,[ref('stock.stock_location_components')])], False, ref('stock.stock_location_components'))
+ assert res == True, "The function 'onchange_location_id' should return True and return '%s'" % res
+ res = self.onchange_location_id(cr, uid, [], [(6,0,[ref('stock.stock_location_components')])], False, ref('stock.stock_location_14'))
+ assert res == True, "The function 'onchange_location_id' should return True and return '%s'" % res
+
+-
+ I will call the function _check_inventory.
+ The function must return True in all test cases.
+-
+ !python {model: stock.location}: |
+ res = self._check_inventory(cr, uid, ref('stock.stock_location_components'))
+ assert res == True, "The function '_check_inventory' should return True and return '%s'" % res
+ res = self._check_inventory(cr, uid, ref('stock.stock_location_14'))
+ assert res == True, "The function '_check_inventory' should return True and return '%s'" % res
+
+-
+ I will confirm inventory.
+-
+ !python {model: stock.inventory}: |
+ self.action_confirm(cr, uid, [ref('inventory_standard')])
+ inventory_state = self.read(cr, uid, [ref('inventory_standard')], ['state'])[0]['state']
+ assert inventory_state == 'confirm', "Partial inventory have '%s' state. It should be 'confirm'" % inventory_state
+
+-
+ I will validate inventory
+-
+ !python {model: stock.inventory}: |
+ self.action_done(cr, uid, [ref('inventory_standard')])
+ inventory_state = self.read(cr, uid, [ref('inventory_standard')], ['state'])[0]['state']
+ assert inventory_state == 'done', "Partial inventory have '%s' state. It should be 'done'" % inventory_state
+
+-
+ I will verify the quantity for each products.
+-
+ !python {model: product.product}: |
+ ctx={'location': [ref('stock.stock_location_components')]}
+ prod_qty_avail = self.read(cr, uid, [ref('product.product_product_9')], ['qty_available'], context=ctx)[0]['qty_available']
+ assert prod_qty_avail == 18.0, "The stock of CPU1 was not set to 18.0: %s" % prod_qty_avail
+
+ prod_qty_avail = self.read(cr, uid, [ref('product.product_product_10')], ['qty_available'], context=ctx)[0]['qty_available']
+ assert prod_qty_avail == 12.0, "The stock of CPU3 was not set to 12.0: %s" % prod_qty_avail
+
+ prod_qty_avail = self.read(cr, uid, [ref('product.product_template_31')], ['qty_available'], context=ctx)[0]['qty_available']
+ assert prod_qty_avail == 32.0, "The stock of FAN was not set to 32.0: %s" % prod_qty_avail
=== added file 'stock_inventory_location/tests/stock_inventory_location_test.py'
--- stock_inventory_location/tests/stock_inventory_location_test.py 1970-01-01 00:00:00 +0000
+++ stock_inventory_location/tests/stock_inventory_location_test.py 2014-06-27 09:18:58 +0000
@@ -0,0 +1,47 @@
+# -*- 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 stock_inventory_location_test(common.TransactionCase):
+
+ def setUp(self):
+ super(stock_inventory_location_test, self).setUp()
+
+ def test_update_parent_location(self):
+ """
+ Test the update of the parent of a location (no inventory in progress
+ """
+ self.registry('stock.location').write(self.cr, self.uid, self.ref('stock.stock_location_5'), {'location_id': self.ref('stock.stock_location_4')})
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
=== added directory 'stock_inventory_location/wizard'
=== added file 'stock_inventory_location/wizard/__init__.py'
--- stock_inventory_location/wizard/__init__.py 1970-01-01 00:00:00 +0000
+++ stock_inventory_location/wizard/__init__.py 2014-06-27 09:18:58 +0000
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-1
+##############################################################################
+#
+# 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/>.
+#
+##############################################################################
+
+import stock_fill_location_inventory
+import stock_confirm_uninventoried_location
=== added file 'stock_inventory_location/wizard/stock_confirm_uninventoried_location.py'
--- stock_inventory_location/wizard/stock_confirm_uninventoried_location.py 1970-01-01 00:00:00 +0000
+++ stock_inventory_location/wizard/stock_confirm_uninventoried_location.py 2014-06-27 09:18:58 +0000
@@ -0,0 +1,68 @@
+# -*- encoding: 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 fields, orm, osv
+
+
+class stock_inventory_uninventoried_location(orm.TransientModel):
+ _name = 'stock.inventory.uninventoried.locations'
+ _description = 'Confirm the uninventoried Locations.'
+
+ _columns = {
+ 'location_ids': fields.many2many(
+ 'stock.location',
+ 'stock_inventory_uninventoried_location_rel',
+ 'location_id', 'wizard_id',
+ 'Uninventoried location', readonly=True),
+ }
+
+ def default_locations(self, cr, uid, context=None):
+ """Initialize view with the list of uninventoried locations."""
+ return self.pool['stock.inventory'].get_missing_locations(
+ cr, uid, context['active_ids'], context=context)
+
+ _defaults = {
+ 'location_ids': default_locations,
+ }
+
+ def confirm_uninventoried_locations(self, cr, uid, ids, context=None):
+ """Add the missing inventory lines with qty=0 and confirm inventory"""
+ inventory_ids = context['active_ids']
+ inventory_obj = self.pool['stock.inventory']
+ wizard_obj = self.pool['stock.fill.inventory']
+ for inventory in inventory_obj.browse(cr, uid, inventory_ids,
+ context=context):
+ if inventory.exhaustive:
+ # silently run the import wizard with qty=0
+ try:
+ # on parent inventory it is possible that fill inventory fail with no product
+ wizard_id = wizard_obj.create(
+ cr, uid, {'location_id': inventory.location_id.id,
+ 'recursive': True,
+ 'set_stock_zero': True,
+ 'exhaustive': True}, context=context)
+ wizard_obj.fill_inventory(cr, uid, [wizard_id],
+ context=context)
+ except osv.except_osv, e:
+ if e.value == _('No product in this location. Please select a location in the product form.'):
+ pass
+
+ inventory_obj.action_confirm(cr, uid, inventory_ids, context=context)
+ return {'type': 'ir.actions.act_window_close'}
=== added file 'stock_inventory_location/wizard/stock_confirm_uninventoried_location.xml'
--- stock_inventory_location/wizard/stock_confirm_uninventoried_location.xml 1970-01-01 00:00:00 +0000
+++ stock_inventory_location/wizard/stock_confirm_uninventoried_location.xml 2014-06-27 09:18:58 +0000
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+ <data>
+ <!-- The view definition is similar with stock_inventory_hierarchical_location/wizard/stock_inventory_missing_locations_view.xml,
+ but the code is different.
+ This wizard compare declared locations with locations on inventory lines to present the uninventoried (empty) locations to user. -->
+ <record id="view_confirm_uninventoried_location" model="ir.ui.view">
+ <field name="name">stock.inventory.uninventoried.locations.form</field>
+ <field name="model">stock.inventory.uninventoried.locations</field>
+ <field name="arch" type="xml">
+ <form string="Confirm empty locations" version="7.0">
+ <group colspan="4" col="1">
+ <separator string="The following Locations are empty"/>
+ <label string="The following Stock Locations are part of the current Physical Inventory, but no Inventory Line has been recorded for them."/>
+ <label string="It could either mean that the Locations are empty, or that the Inventory is not yet complete."/>
+ <label string="If you confirm the Inventory, these Locations will be considered empty and their content will be purged."/>
+ <field name="location_ids" nolabel="1">
+ <tree>
+ <field name="name"/>
+ </tree>
+ </field>
+ <separator string=""/>
+ </group>
+ <footer>
+ <button name="confirm_uninventoried_locations" string="Purge contents and confirm Inventory" type="object" class="oe_highlight"/>
+ or
+ <button string="Cancel" class="oe_link" special="cancel" default_focus="1"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+ </data>
+</openerp>
\ No newline at end of file
=== added file 'stock_inventory_location/wizard/stock_fill_location_inventory.py'
--- stock_inventory_location/wizard/stock_fill_location_inventory.py 1970-01-01 00:00:00 +0000
+++ stock_inventory_location/wizard/stock_fill_location_inventory.py 2014-06-27 09:18:58 +0000
@@ -0,0 +1,47 @@
+# -*- 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 fields, orm
+
+
+class FillInventoryWizard(orm.TransientModel):
+ """Add a field that lets the client make the location read-only"""
+ _inherit = 'stock.fill.inventory'
+
+ _columns = {
+ 'exhaustive': fields.boolean('Exhaustive', readonly=True)
+ }
+
+ def default_get(self, cr, uid, fields, context=None):
+ """Get 'location_id' and 'exhaustive' from the inventory"""
+ if context is None:
+ context = {}
+ inv_id = context.get('active_id')
+
+ res = super(FillInventoryWizard, self).default_get(
+ cr, uid, fields, context=context)
+ if (context.get('active_model') == 'stock.inventory'
+ and inv_id
+ and 'location_id' in fields):
+ inventory = self.pool['stock.inventory'].browse(
+ cr, uid, context['active_id'], context=context)
+ res.update({'location_id': inventory.location_id.id,
+ 'exhaustive': inventory.exhaustive})
+ return res
=== added file 'stock_inventory_location/wizard/stock_fill_location_inventory_view.xml'
--- stock_inventory_location/wizard/stock_fill_location_inventory_view.xml 1970-01-01 00:00:00 +0000
+++ stock_inventory_location/wizard/stock_fill_location_inventory_view.xml 2014-06-27 09:18:58 +0000
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+ <data>
+ <record id="view_stock_fill_inventory_location" model="ir.ui.view">
+ <field name="name">Import Inventory</field>
+ <field name="model">stock.fill.inventory</field>
+ <field name="inherit_id" ref="stock.view_stock_fill_inventory" />
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='location_id']" position="after">
+ <field name="exhaustive" invisible="1" />
+ </xpath>
+ <xpath expr="//field[@name='location_id']" position="attributes">
+ <attribute name="attrs">{'readonly':[('exhaustive','=',True)]}</attribute>
+ </xpath>
+ </field>
+ </record>
+ </data>
+</openerp>
Follow ups