Merge lp:~numerigraphe-team/stock-logistic-warehouse/7.0-inventory-location into lp:stock-logistic-warehouse

Proposed by Loïc Bellier - Numérigraphe
Status: Superseded
Proposed branch: lp:~numerigraphe-team/stock-logistic-warehouse/7.0-inventory-location
Merge into: lp:stock-logistic-warehouse
Diff against target: 1345 lines (+1249/-0)
15 files modified
stock_inventory_location/__init__.py (+24/-0)
stock_inventory_location/__openerp__.py (+77/-0)
stock_inventory_location/exceptions.py (+26/-0)
stock_inventory_location/i18n/fr.po (+241/-0)
stock_inventory_location/stock_inventory_location.py (+351/-0)
stock_inventory_location/stock_inventory_location_demo.xml (+26/-0)
stock_inventory_location/stock_inventory_location_view.xml (+81/-0)
stock_inventory_location/test/inventory_exhaustive_test.yml (+128/-0)
stock_inventory_location/test/inventory_future_test.yml (+13/-0)
stock_inventory_location/test/inventory_standard_test.yml (+100/-0)
stock_inventory_location/wizard/__init__.py (+22/-0)
stock_inventory_location/wizard/stock_confirm_uninventoried_location.py (+62/-0)
stock_inventory_location/wizard/stock_confirm_uninventoried_location.xml (+33/-0)
stock_inventory_location/wizard/stock_fill_location_inventory.py (+47/-0)
stock_inventory_location/wizard/stock_fill_location_inventory_view.xml (+18/-0)
To merge this branch: bzr merge lp:~numerigraphe-team/stock-logistic-warehouse/7.0-inventory-location
Reviewer Review Type Date Requested Status
Lionel Sausin - Initiatives/Numérigraphe (community) Needs Resubmitting
Alexandre Fayolle - camptocamp code review, test Needs Fixing
Review via email: mp+210630@code.launchpad.net

This proposal has been superseded by a proposal from 2014-06-20.

Description of the change

This proposal feature obsoletes Tiny's "stock_alternative_inventory", and
implements exhaustive inventory in a much more practical way.

It lets you explicitly state which locations are being inventoried, and makes
sure the stock levels exactly reflect the physical inventory for those
locations.

It also checks that all locations been entered in the inventory lines,
and no other locations.

Finally it will block Stock Moves during the inventory, to keep
warehouse workers from changing the stock while the inventory is not
finished.

To post a comment you must log in.
Revision history for this message
Alexandre Fayolle - camptocamp (alexandre-fayolle-c2c) wrote :

Hello,

Thanks for this contribution, the functionality is great and I'd love to see it in OCA. And thanks for including tests.

A few minor changes are needed to bring the module to the coding standards

* use orm.Model / orm.TransientModel as base classes instead of osv.osv and osv.osv_memory
* model column default values: in OpenERP 7.0, not need to use a lambda for a constant value, just use the constant

Additionally:

* in __openerp__.py, I think you should use 'Warehouse Management' for the category (i.e. the same one as the official 'stock' addon
* no space before '!' in english (I'd remove the exclamation marks in exception messages altogether, that the UI job to display an icon)
* in confirm_uninventoried_location_wizard: you should add a 'if context is None: context = {}' at the beginning.
* in confirm_uninventoried_locations: you are ignoring an osv_except exception. Could you add a comment in the code explaining why this is safe? If this is about the "No product in this location" warning raised in the reimplementation of _fill_location_lines, then I think a check that we are ignoring the correct exception should be added. You could for instance subclass except_osv and raise + ignore the subclass. Otherwise I think the code may hide things we do not want to hide (or even fail because of an except_osv

review: Needs Fixing (code review, test)
Revision history for this message
Lionel Sausin - Initiatives/Numérigraphe (ls-initiatives) wrote :

Recent developments in trunk-wms implement some features of this proposal but the implementation is different.
I suggest we refactor this proposal and the following patches, to make it easy to port the module to v8 later on.
- Add locations as many2one instead of one2many
  http://bazaar.launchpad.net/~openerp-dev/openobject-addons/trunk-wms/revision/9596
- forbid inventories in the future
  http://bazaar.launchpad.net/~openerp-dev/openobject-addons/trunk-wms/revision/9598

review: Needs Resubmitting
36. By Loïc Bellier - Numérigraphe

[REF] update coding style for v7

37. By Loïc Bellier - Numérigraphe

[FIX]: Fix product on yaml test.

38. By Numérigraphe

[MERGE] only allow one location per inventory to make it easier to migrate to v8 when the time comes. code & views cleaned up.

Revision history for this message
Lionel Sausin - Initiatives/Numérigraphe (ls-initiatives) wrote :

Loïc and I are still working on this proposal. Good progress was made: we re-aligned the features with v8 in mind, and improved the code style and the views.
Acsone joined in and we're still making tests, and i hope Loïc can make a new proposal in the coming days.

39. By Numérigraphe

[FIX] test and add a test for the refactored function

40. By Numérigraphe

[FIX] wrong nesting in list comprehension

41. By Laetitia Gangloff (Acsone)

[FIX] read inventories as superuser to allow the control on locations even when the user has no permission on inventories. Courtesy of ACSONE.

42. By Laetitia Gangloff (Acsone)

[FIX] don't mutate the data received by the method, it causes an infinite recursion. Fix provided by ACSONE.

43. By Laetitia Gangloff (Acsone)

[FIX] missing test files from the previous fixes by ASCONE

44. By Laetitia Gangloff (Acsone)

[FIX] exhaustive was not passed to the wizard when emptying the locations that were not in the inventory

45. By Numérigraphe

[REF] remove TODO

46. By Laetitia Gangloff (Acsone)

[FIX] missing import

47. By Numérigraphe

[IMP] French translation

Unmerged revisions

47. By Numérigraphe

[IMP] French translation

46. By Laetitia Gangloff (Acsone)

[FIX] missing import

45. By Numérigraphe

[REF] remove TODO

44. By Laetitia Gangloff (Acsone)

[FIX] exhaustive was not passed to the wizard when emptying the locations that were not in the inventory

43. By Laetitia Gangloff (Acsone)

[FIX] missing test files from the previous fixes by ASCONE

42. By Laetitia Gangloff (Acsone)

[FIX] don't mutate the data received by the method, it causes an infinite recursion. Fix provided by ACSONE.

41. By Laetitia Gangloff (Acsone)

[FIX] read inventories as superuser to allow the control on locations even when the user has no permission on inventories. Courtesy of ACSONE.

40. By Numérigraphe

[FIX] wrong nesting in list comprehension

39. By Numérigraphe

[FIX] test and add a test for the refactored function

38. By Numérigraphe

[MERGE] only allow one location per inventory to make it easier to migrate to v8 when the time comes. code & views cleaned up.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== 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-12 16:47:03 +0000
@@ -0,0 +1,24 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# This module is copyright (C) 2013 Numérigraphe SARL. All Rights Reserved.
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18#
19##############################################################################
20
21import stock_inventory_location
22import wizard
23# Bring the main exception into the package's scope for easier reuse
24from .exceptions import ExhaustiveInventoryException
025
=== 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-12 16:47:03 +0000
@@ -0,0 +1,77 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# This module is copyright (C) 2013 Numérigraphe SARL. All Rights Reserved.
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18#
19##############################################################################
20
21{
22 "name": "Exhaustive Stock Inventories",
23 "version": "1.1",
24 "depends": ["stock"],
25 "author": u"Numérigraphe",
26 "category": "Warehouse Management",
27 "description": """
28Let users make exhaustive Inventories
29=====================================
30
31Standard Physical Inventories in OpenERP only contain a generic list of
32products by locations, which is well suited to partial Inventories and simple
33warehouses. When the a standard Inventory is confirmed, only the products in
34the inventory are checked. If a Product is present in the computed stock and
35not in the recorded Inventory, OpenERP will consider that it remains unchanged.
36
37But for exhaustive inventories in complex warehouses, it is not practical:
38 - you want to avoid Stock Moves to/from these Locations while counting goods
39 - you must make sure all the locations you want have been counted
40 - you must make sure no other location has been counted by mistake
41 - you want the computed stock to perfectly match the inventory when you
42 confirm it.
43
44This module lets choose whether an Physical Inventory is exhaustive or
45standard.
46For an exhaustive Inventory:
47 - in the state "Draft" you define the Location where goods must be counted.
48 - the new Inventory status "Open" lets you indicate that the list of Locations
49 is final and you are now counting the goods.
50 In that status, no Stock Moves can be recorded in/out of the Inventory's
51 Locations.
52 - if the Location or some of it's children have not been entered in the
53 Inventory Lines, OpenERP warns you when you confirm the Inventory.
54 - only the Inventory's Location or its children can be entered in the
55 Inventory Lines.
56 - every good that is not in the Inventory Lines is considered lost, and gets
57 moved out of the stock when you confirm the Inventory.
58""",
59 "data": [
60 "wizard/stock_confirm_uninventoried_location.xml",
61 "stock_inventory_location_view.xml",
62 "wizard/stock_fill_location_inventory_view.xml",
63 ],
64 "test": [
65 "test/inventory_standard_test.yml",
66 "test/inventory_exhaustive_test.yml",
67 "test/inventory_future_test.yml",
68 ],
69 "images": [
70 "images/inventory_form.png",
71 "inventory_empty_locations.png",
72 "images/move_error.png",
73 "images/location_locked.png",
74 "images/future_inventory.png",
75 ],
76 "demo": ["stock_inventory_location_demo.xml"]
77}
078
=== 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-12 16:47:03 +0000
@@ -0,0 +1,26 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# This module is copyright (C) 2013 Numérigraphe SARL. All Rights Reserved.
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18#
19##############################################################################
20
21from openerp.osv import orm
22
23
24class ExhaustiveInventoryException(orm.except_orm):
25 """The operation is not possible for an exhaustive inventory"""
26 pass
027
=== 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-12 16:47:03 +0000
@@ -0,0 +1,241 @@
1# Translation of OpenERP Server.
2# This file contains the translation of the following modules:
3# * stock_inventory_location
4#
5msgid ""
6msgstr ""
7"Project-Id-Version: OpenERP Server 6.0.4\n"
8"Report-Msgid-Bugs-To: support@openerp.com\n"
9"POT-Creation-Date: 2013-09-25 13:58+0000\n"
10"PO-Revision-Date: 2013-09-25 13:58+0000\n"
11"Last-Translator: <>\n"
12"Language-Team: \n"
13"MIME-Version: 1.0\n"
14"Content-Type: text/plain; charset=UTF-8\n"
15"Content-Transfer-Encoding: \n"
16"Plural-Forms: \n"
17
18#. module: stock_inventory_location
19#: code:addons/stock_inventory_location/stock_inventory_location.py:232
20#: constraint:stock.move:0
21#, python-format
22msgid "A Physical Inventory is being conducted at this location"
23msgstr "Un inventaire est en cours à cet emplacement"
24
25#. module: stock_inventory_location
26#: view:stock.inventory.uninventoried.locations:0
27msgid "Cancel"
28msgstr "Annuler"
29
30#. module: stock_inventory_location
31#: view:stock.inventory:0
32msgid "Cancel Inventory"
33msgstr "Annuler l'inventaire"
34
35#. module: stock_inventory_location
36#: help:stock.inventory,exhaustive:0
37msgid "Check the box if you are conducting an exhaustive Inventory.\n"
38"Leave the box unchecked if you are conducting a standard Inventory (partial inventory for example).\n"
39"For an exhaustive Inventory:\n"
40" - the status \"Draft\" lets you define the list of Locations where goods must be counted\n"
41" - 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"
42" - only the Inventory's Locations can be entered in the Inventory Lines\n"
43" - if some of the Inventory's Locations have not been entered in the Inventory Lines, OpenERP warns you when you confirm the Inventory\n"
44" - every good that is not in the Inventory Lines is considered lost, and gets moved out of the stock when you confirm the Inventory"
45msgstr "Cochez la case si vous effectuez un inventaire exhaustif.\n"
46"Laissez la case non-dochée si vous effectuez un inventaire standard (par exmple un inventaire partiel).\n"
47"Pour les inventaires exhaustifs :\n"
48" - le statut \"Brouillon\" permet d'indiquer la liste des emplacements dont la marchandise doit être comptée\n"
49" - 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"
50" - seuls les emplacements de l'inventaire peuvent être saisis dans les lignes d'inventaire\n"
51" - si certains emplacements sont absent des lignes d'inventaire, OpenERP vous en avertit lors de la confirmation de l'inventaire\n"
52" - 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"
53
54#. module: stock_inventory_location
55#: view:stock.inventory:0
56msgid "Confirm Inventory"
57msgstr "Confirmer l'inventaire"
58
59#. module: stock_inventory_location
60#: model:ir.model,name:stock_inventory_location.model_stock_inventory_uninventoried_locations
61msgid "Confirm the uninventoried Locations."
62msgstr "Confirmer les emplacements non inventoriés."
63
64#. module: stock_inventory_location
65#: view:stock.inventory.uninventoried.locations:0
66msgid "Confirm empty locations"
67msgstr "Confirmez les emplacements vides"
68
69#. module: stock_inventory_location
70#: model:ir.model,name:stock_inventory_location.model_stock_location
71msgid "Emplacement"
72msgstr "Emplacement"
73
74#. module: stock_inventory_location
75#: code:addons/stock_inventory_location/wizard/stock_fill_location_inventory.py:54
76#, python-format
77msgid "Error"
78msgstr "Erreur"
79
80#. module: stock_inventory_location
81#: code:addons/stock_inventory_location/wizard/stock_fill_location_inventory.py:75
82#, python-format
83msgid "Error: Empty location"
84msgstr "Erreur : Emplacement vide"
85
86#. module: stock_inventory_location
87#: code:addons/stock_inventory_location/stock_inventory_location.py:231
88#: code:addons/stock_inventory_location/stock_inventory_location.py:282
89#, python-format
90msgid "Error: Location locked down"
91msgstr "Erreur: emplacement en inventaire"
92
93#. module: stock_inventory_location
94#: constraint:stock.inventory:0
95msgid "Error: You can not create recursive inventories."
96msgstr "Erreur: Vous ne pouvez pas créer un inventaire récursif."
97
98#. module: stock_inventory_location
99#: constraint:stock.inventory.line:0
100msgid "Error: duplicates lines"
101msgstr "Erreur: lignes en double"
102
103#. module: stock_inventory_location
104#: view:stock.inventory:0
105#: field:stock.inventory,exhaustive:0
106msgid "Exhaustive"
107msgstr "Exhaustif"
108
109#. module: stock_inventory_location
110#: model:ir.model,name:stock_inventory_location.model_stock_inventory
111msgid "Gestion des stocks"
112msgstr "Gestion des stocks"
113
114#. module: stock_inventory_location
115#: view:stock.inventory.uninventoried.locations:0
116msgid "If you confirm the Inventory, these Locations will be considered empty and their content will be purged."
117msgstr "Si vous confirmez l'inventaire, ces emplacements seront considérés comme vides et leur contenu sera supprimé."
118
119#. module: stock_inventory_location
120#: model:ir.model,name:stock_inventory_location.model_stock_fill_inventory
121msgid "Importer un inventaire"
122msgstr "Importer un inventaire"
123
124#. module: stock_inventory_location
125#: view:stock.inventory.uninventoried.locations:0
126msgid "It could either mean that the Locations are empty, or that the Inventory is not yet complete."
127msgstr "Cela peut vouloir dire soit que l'inventaire n'est pas terminé, soit que ces emplacements sont effectivement vides."
128
129#. module: stock_inventory_location
130#: model:ir.model,name:stock_inventory_location.model_stock_inventory_line
131msgid "Ligne d'inventaire"
132msgstr "Ligne d'inventaire"
133
134#. module: stock_inventory_location
135#: code:addons/stock_inventory_location/stock_inventory_location.py:59
136#, python-format
137msgid "Location missing for this inventory."
138msgstr "Emplacement manquant pour cet inventaire."
139
140#. module: stock_inventory_location
141#: model:ir.model,name:stock_inventory_location.model_stock_move
142msgid "Mouvement de stock"
143msgstr "Mouvement de stock"
144
145#. module: stock_inventory_location
146#: code:addons/stock_inventory_location/wizard/stock_fill_location_inventory.py:75
147#, python-format
148msgid "No location to import.\n"
149"You must add a location on the locations list."
150msgstr "Pas d'emplacement à importer.\n"
151"Vous devez ajouter un emplacement dans la liste des emplacements."
152
153#. module: stock_inventory_location
154#: code:addons/stock_inventory_location/wizard/stock_fill_location_inventory.py:58
155#, python-format
156msgid "No locations found for the inventory."
157msgstr "Pas d'emplacement trouvé pour cet inventaire."
158
159#. module: stock_inventory_location
160#: code:addons/stock_inventory_location/stock_inventory_location.py:163
161#, python-format
162msgid "No product in this location."
163msgstr "Pas de produit à cet emplacement."
164
165#. module: stock_inventory_location
166#: code:addons/stock_inventory_location/stock_inventory_location.py:283
167#, python-format
168msgid "A Physical Inventory is being conducted at the following location(s):\n"
169"%s"
170msgstr "Les emplacements suivants sont en inventaire :\n"
171"%s"
172
173#. module: stock_inventory_location
174#: view:stock.inventory:0
175msgid "Open Inventory"
176msgstr "Ouvrir l'inventaire"
177
178#. module: stock_inventory_location
179#: constraint:stock.inventory:0
180msgid "Other Physical inventories are being conducted using the same Locations."
181msgstr "Erreur: certains emplacements sont déjà dans un autre inventaire."
182
183#. module: stock_inventory_location
184#: view:stock.inventory.uninventoried.locations:0
185msgid "Purge contents and confirm Inventory"
186msgstr "Supprimer le contenu et confirmer l'inventaire"
187
188#. module: stock_inventory_location
189#: view:stock.inventory.uninventoried.locations:0
190msgid "The following Stock Locations are part of the current Physical Inventory, but no Inventory Line has been recorded for them."
191msgstr "Les emplacements suivants font partie de l'inventaire en cours, mais aucune ligne d'inventaire les concernant n'a été enregistrée."
192
193#. module: stock_inventory_location
194#: field:stock.inventory.uninventoried.locations,location_ids:0
195msgid "Uninventoried location"
196msgstr "Emplacements non inventoriés"
197
198#. module: stock_inventory_location
199#: code:addons/stock_inventory_location/stock_inventory_location.py:59
200#: code:addons/stock_inventory_location/stock_inventory_location.py:163
201#: code:addons/stock_inventory_location/wizard/stock_fill_location_inventory.py:58
202#, python-format
203msgid "Warning"
204msgstr "Attention"
205
206#. module: stock_inventory_location
207#: code:addons/stock_inventory_location/stock_inventory_location.py:209
208#, python-format
209msgid "Warning: Wrong location"
210msgstr "Attention: emplacement incorrect"
211
212#. module: stock_inventory_location
213#: code:addons/stock_inventory_location/stock_inventory_location.py:210
214#, python-format
215msgid "You cannot record an Inventory Line for this Location.\n"
216"You must first add this Location to the list of affected Locations on the Inventory form."
217msgstr "Vous ne pouvez pas ajouter cet emplacement.\n"
218"Il ne fait pas partie de la liste des emplacements à inventorier"
219
220#. module: stock_inventory_location
221#: constraint:stock.move:0
222msgid "You must assign a production lot for this product"
223msgstr "Vous devez affecter un lot de fabrication à ce produit."
224
225#. module: stock_inventory_location
226#: constraint:stock.inventory.line:0
227msgid "You must not create same inventory line Product, Location, Lot on the same date"
228msgstr "Vous ne pouvez pas créer plusieurs lignes d'inventaires pour le même produit, lot, emplacement à la même date"
229
230#. module: stock_inventory_location
231#: constraint:stock.move:0
232msgid "You try to assign a lot which is not from the same product"
233msgstr "Vous essayez d'affecter un lot qui n'est pas pour ce produit."
234
235
236#. module: stock_inventory_location
237#: code:addons/stock_inventory_location/wizard/stock_fill_location_inventory.py:54
238#, python-format
239msgid "the inventory must be in \"Open\" state."
240msgstr "l'inventaire doit etre \"Ouvert\"."
241
0242
=== added directory 'stock_inventory_location/images'
=== added file 'stock_inventory_location/images/future_inventory.png'
1Binary 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-12 16:47:03 +0000 differ243Binary 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-12 16:47:03 +0000 differ
=== added file 'stock_inventory_location/images/inventory_empty_locations.png'
2Binary 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-12 16:47:03 +0000 differ244Binary 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-12 16:47:03 +0000 differ
=== added file 'stock_inventory_location/images/inventory_form.png'
3Binary 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-12 16:47:03 +0000 differ245Binary 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-12 16:47:03 +0000 differ
=== added file 'stock_inventory_location/images/location_locked.png'
4Binary 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-12 16:47:03 +0000 differ246Binary 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-12 16:47:03 +0000 differ
=== added file 'stock_inventory_location/images/move_error.png'
5Binary 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-12 16:47:03 +0000 differ247Binary 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-12 16:47:03 +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'
6Binary 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-12 16:47:03 +0000 differ248Binary 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-12 16:47:03 +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-12 16:47:03 +0000
@@ -0,0 +1,351 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# This module is copyright (C) 2013 Numérigraphe SARL. All Rights Reserved.
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18#
19##############################################################################
20
21import time
22from collections import Iterable
23
24from openerp.osv import orm, fields
25from openerp.tools.translate import _
26# The next 2 imports are only needed for a feature backported from trunk-wms
27# TODOv8! remove, feature is included upstream
28from openerp.osv import osv
29from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
30
31from .exceptions import ExhaustiveInventoryException
32
33
34class StockInventory(orm.Model):
35 """Add locations to the inventories"""
36 _inherit = 'stock.inventory'
37
38 INVENTORY_STATE_SELECTION = [
39 ('draft', 'Draft'),
40 ('open', 'Open'),
41 ('done', 'Done'),
42 ('confirm', 'Confirmed'),
43 ('cancel', 'Cancelled')
44 ]
45
46 _columns = {
47 # TODO v8: should we use "confirm" instead of adding "open"?
48 'state': fields.selection(
49 INVENTORY_STATE_SELECTION, 'State', readonly=True, select=True),
50 # TODO v8: remove this, should not be needed anymore
51 # Make the inventory lines read-only in all states except "Open",
52 # to ensure that no unwanted Location can be inserted
53 'inventory_line_id': fields.one2many(
54 'stock.inventory.line', 'inventory_id', 'Inventory lines',
55 readonly=True, states={'open': [('readonly', False)]}),
56 # TODO v8: remove this, it's backported from v8
57 'location_id': fields.many2one(
58 'stock.location', 'Inventoried Location',
59 readonly=True, states={'draft': [('readonly', False)]}),
60 'exhaustive': fields.boolean(
61 'Exhaustive', readonly=True,
62 states={'draft': [('readonly', False)]},
63 help="Check the box if you are conducting an exhaustive "
64 "Inventory.\n"
65 "Leave the box unchecked if you are conducting a standard "
66 "Inventory (partial inventory for example).\n"
67 "For an exhaustive Inventory:\n"
68 " - the status \"Draft\" lets you define the list of "
69 "Locations where goods must be counted\n"
70 " - the status \"Open\" indicates that the list of Locations "
71 "is definitive and you are now counting the goods. In that "
72 "status, no Stock Moves can be recorded in/out of the "
73 "Inventory's Locations\n"
74 " - only the Inventory's Locations can be entered in the "
75 "Inventory Lines\n"
76 " - if some of the Inventory's Locations have not been "
77 "entered in the Inventory Lines, OpenERP warns you "
78 "when you confirm the Inventory\n"
79 " - every good that is not in the Inventory Lines is "
80 "considered lost, and gets moved out of the stock when you "
81 "confirm the Inventory"),
82 }
83
84 def action_open(self, cr, uid, ids, context=None):
85 """Change the state of the inventory to 'open'"""
86 return self.write(cr, uid, ids, {'state': 'open'}, context=context)
87
88 # TODO v8: remove this method? the feature looks already done upstream
89 def action_done(self, cr, uid, ids, context=None):
90 """
91 Don't allow to make an inventory with a date in the future.
92
93 This makes sure no stock moves will be introduced between the
94 moment you finish the inventory and the date of the Stock Moves.
95 Backported from trunk-wms:
96 revid:qdp-launchpad@openerp.com-20140317090656-o7lo22tzm8yuv3r8
97
98 @raise osv.except_osv:
99 We raise this exception on purpose instead of
100 ExhaustiveInventoryException to ensure forward-compatibility
101 with v8.
102 """
103 for inventory in self.browse(cr, uid, ids, context=None):
104 if inventory.date > time.strftime(DEFAULT_SERVER_DATETIME_FORMAT):
105 raise osv.except_osv(
106 _('Error!'),
107 _('It\'s impossible to confirm an inventory in the '
108 'future. Please change the inventory date to proceed '
109 'further.'))
110 return super(StockInventory, self).action_done(cr, uid, ids,
111 context=context)
112
113 # TODO: remove this in v8
114 def _default_location(self, cr, uid, ids, context=None):
115 """Default stock location
116
117 @return: id of the stock location of the first warehouse of the
118 default company"""
119 location_id = False
120 company_id = self.pool['res.company']._company_default_get(
121 cr, uid, 'stock.warehouse', context=context)
122 warehouse_id = self.pool['stock.warehouse'].search(
123 cr, uid, [('company_id', '=', company_id)], limit=1)
124 if warehouse_id:
125 location_id = self.pool['stock.warehouse'].read(
126 cr, uid, warehouse_id[0], ['lot_stock_id'])['lot_stock_id'][0]
127 return location_id
128
129 _defaults = {
130 'state': 'draft',
131 'exhaustive': False,
132 # TODO: remove this in v8
133 'location_id': _default_location,
134 }
135
136 def _check_location_free_from_inventories(self, cr, uid, ids):
137 """
138 Verify that no other Inventory is being conducted on the same locations
139
140 Open Inventories are matched using the exact Location IDs,
141 excluding children.
142 """
143 # We don't get a context because we're called from a _constraint
144 for inventory in self.browse(cr, uid, ids):
145 if not inventory.exhaustive:
146 # never block standard inventories
147 continue
148 if self.search(cr, uid,
149 [('location_id', '=', inventory.location_id.id),
150 ('id', '!=', inventory.id),
151 ('date', '=', inventory.date),
152 ('exhaustive', '=', True)]):
153 # Quit as soon as one offending inventory is found
154 return False
155 return True
156
157 _constraints = [
158 (_check_location_free_from_inventories,
159 'Other Physical inventories are being conducted using the same '
160 'Locations.',
161 ['location_id', 'date', 'exhaustive'])
162 ]
163
164 def _get_locations_open_inventories(self, cr, uid, context=None):
165 """IDs of location in open exhaustive inventories, with children"""
166 inv_ids = self.search(
167 cr, uid, [('state', '=', 'open'), ('exhaustive', '=', True)],
168 context=context)
169 if not inv_ids:
170 # Early exit if no match found
171 return []
172 # List the Locations - normally all exhaustive inventories have one
173 location_ids = [inventory.location_id.id
174 for inventory in self.browse(cr, uid, inv_ids,
175 context=context)]
176 # Extend to the children Locations
177 return self.pool['stock.location'].search(
178 cr, uid,
179 [('location_id', 'child_of', set(location_ids)),
180 ('usage', '=', 'internal')],
181 context=context)
182
183 def get_missing_locations(self, cr, uid, ids, context=None):
184 """Compute the list of location_ids which are missing from the lines
185
186 Here, "missing" means the location is the inventory's location or one
187 of it's children, and the inventory does not contain any line with
188 this location."""
189 inventories = self.browse(cr, uid, ids, context=context)
190 # Find the locations of the inventories
191 inv_location_ids = [i.location_id.id for i in inventories]
192 # Extend to the children locations
193 inv_location_ids = set(self.pool['stock.location'].search(
194 cr, uid, [
195 ('location_id', 'child_of', inv_location_ids),
196 ('usage', '=', 'internal')], context=context))
197 # Find the locations already recorded in inventory lines
198 line_locations_ids = set([l.location_id.id
199 for i in inventories
200 for l in i.inventory_line_id])
201 return list(inv_location_ids - line_locations_ids)
202
203 def confirm_missing_locations(self, cr, uid, ids, context=None):
204 """Open wizard to confirm empty locations on exhaustive inventories"""
205 for inventory in self.browse(cr, uid, ids, context=context):
206 if (self.get_missing_locations(cr, uid, ids, context=context)
207 and inventory.exhaustive):
208 return {
209 'type': 'ir.actions.act_window',
210 'view_type': 'form',
211 'view_mode': 'form',
212 'res_model': 'stock.inventory.uninventoried.locations',
213 'target': 'new',
214 'context': dict(context,
215 active_ids=ids,
216 active_id=ids[0]),
217 'nodestroy': True,
218 }
219 return self.action_confirm(cr, uid, ids, context=context)
220
221
222class StockInventoryLine(orm.Model):
223 """Only allow the Inventory's Locations"""
224
225 _inherit = 'stock.inventory.line'
226
227 def _default_stock_location(self, cr, uid, context=None):
228 res = super(StockInventoryLine, self)._default_stock_location(
229 cr, uid, context=context)
230 if context is None or not context.get('location_id', False):
231 return res
232 else:
233 return context['location_id']
234
235 _defaults = {
236 'location_id': _default_stock_location
237 }
238
239 def onchange_location_id(self, cr, uid, ids, inventory_location_id,
240 exhaustive, location_id, context=None):
241 """Warn if the new is not in the location list for this inventory."""
242 if not exhaustive:
243 # Don't check if partial inventory
244 return True
245
246 # search children of location
247 if location_id not in self.pool['stock.location'].search(
248 cr, uid, [('location_id', 'child_of', inventory_location_id)],
249 context=context):
250 return {
251 'value': {'location_id': False},
252 'warning': {
253 'title': _('Warning: Wrong location'),
254 'message': _(
255 "You cannot record an Inventory Line for this "
256 "Location.\n"
257 "You must first add this Location to the list of "
258 "affected Locations on the Inventory form.")}
259 }
260 return True
261
262
263class StockLocation(orm.Model):
264 """Refuse changes during exhaustive Inventories"""
265 _inherit = 'stock.location'
266 _order = 'name'
267
268 # TODOv7: why not put this in an ORM "_constraint" instead?
269 def _check_inventory(self, cr, uid, ids, context=None):
270 """Error if an exhaustive Inventory is being conducted here"""
271 inv_obj = self.pool['stock.inventory']
272 location_inventory_open_ids = inv_obj._get_locations_open_inventories(
273 cr, uid, context=context)
274 if not isinstance(ids, Iterable):
275 ids = [ids]
276 for inv_id in ids:
277 if inv_id in location_inventory_open_ids:
278 raise ExhaustiveInventoryException(
279 _('Error: Location locked down'),
280 _('A Physical Inventory is being conducted at this '
281 'location'))
282 return True
283
284 def write(self, cr, uid, ids, vals, context=None):
285 """Refuse write if an inventory is being conducted"""
286 self._check_inventory(cr, uid, ids, context=context)
287 if not isinstance(ids, Iterable):
288 ids = [ids]
289 ids_to_check = ids
290 # If changing the parent, no inventory must conducted there either
291 if vals.get('location_id'):
292 ids_to_check.append(vals['location_id'])
293 self._check_inventory(cr, uid, ids_to_check, context=context)
294 return super(StockLocation, self).write(cr, uid, ids, vals,
295 context=context)
296
297 def create(self, cr, uid, vals, context=None):
298 """Refuse create if an inventory is being conducted at the parent"""
299 self._check_inventory(cr, uid, vals.get('location_id'),
300 context=context)
301 return super(StockLocation, self).create(cr, uid, vals,
302 context=context)
303
304 def unlink(self, cr, uid, ids, context=None):
305 """Refuse unlink if an inventory is being conducted"""
306 self._check_inventory(cr, uid, ids, context=context)
307 return super(StockLocation, self).unlink(cr, uid, ids, context=context)
308
309
310class StockMove(orm.Model):
311 """Refuse Moves during exhaustive Inventories"""
312
313 _inherit = 'stock.move'
314
315 # TODOv7: adapt this to match trunk-wms
316 def _check_open_inventory_location(self, cr, uid, ids, context=None):
317 """
318 Check that the locations are not locked by an open inventory
319
320 Standard inventories are not checked.
321
322 @raise ExhaustiveInventoryException: an error is raised if locations
323 are locked, instead of returning False, in order to pass an
324 extensive error message back to users.
325 """
326 message = ""
327 inv_obj = self.pool['stock.inventory']
328 locked_location_ids = inv_obj._get_locations_open_inventories(
329 cr, uid, context=context)
330 if not locked_location_ids:
331 # Nothing to verify
332 return True
333 for move in self.browse(cr, uid, ids, context=context):
334 if (move.location_id.usage != 'inventory'
335 and move.location_dest_id.id in locked_location_ids):
336 message += " - %s\n" % (move.location_dest_id.name)
337 if (move.location_dest_id.usage != 'inventory'
338 and move.location_id.id in locked_location_ids):
339 message += " - %s\n" % (move.location_id.name)
340 if message:
341 raise ExhaustiveInventoryException(
342 _('Error: Location locked down'),
343 _('A Physical Inventory is being conducted at the following '
344 'location(s):\n%s') % message)
345 return True
346
347 _constraints = [
348 (_check_open_inventory_location,
349 "A Physical Inventory is being conducted at this location",
350 ['location_id', 'location_dest_id']),
351 ]
0352
=== 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-12 16:47:03 +0000
@@ -0,0 +1,26 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3 <data noupdate="0">
4 <!-- Record a non exhaustive inventory -->
5 <record id="inventory_standard" model="stock.inventory">
6 <field name="name">Standard inventory</field>
7 <field name="state">draft</field>
8 </record>
9
10 <!-- Record an exhaustive inventory -->
11 <record id="inventory_exhaustive" model="stock.inventory">
12 <field name="name">Exhaustive inventory</field>
13 <field name="state">draft</field>
14 <field name="exhaustive">True</field>
15 <field name="location_id" model="stock.location" search="[('name', '=', 'Shelf 2')]" />
16 </record>
17
18 <!-- Record an inventory dated in the future -->
19 <!-- TODOv8: remove this entry, only useful for a test already in trunk-wms -->
20 <record id="inventory_future" model="stock.inventory">
21 <field name="name">Inventory in the future</field>
22 <field name="state">draft</field>
23 <field name="date">2020-03-01 00:00:00</field>
24 </record>
25 </data>
26</openerp>
027
=== 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-12 16:47:03 +0000
@@ -0,0 +1,81 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3 <data>
4
5 <record model="ir.ui.view" id="stock_inventory_location_form_view">
6 <field name="name">stock.inventory.location.form</field>
7 <field name="model">stock.inventory</field>
8 <field name="inherit_id" ref="stock.view_inventory_form"/>
9 <field name="priority" eval="10"/>
10 <field name="arch" type="xml">
11 <!-- Show the state 'done' in the statusbar -->
12 <xpath expr="/form//field[@name='state']" position="attributes">
13 <attribute name="statusbar_visible">draft,open,confirm</attribute>
14 </xpath>
15
16 <!-- TODO v8 place "exhaustive" next to "location_id" -->
17 <!-- Add type of inventory: standard or exhaustive. -->
18 <xpath expr="/form//field[@name='name']" position="after">
19 <field name="exhaustive"/>
20 <field name="location_id" groups="stock.group_locations"
21 domain="[('usage','in',('view', 'internal'))]"
22 attrs="{'invisible':[('exhaustive','!=',True)], 'required':[('exhaustive','=',True)]}"/>
23 </xpath>
24
25 <!-- Enable Fill Inventory button when state is open -->
26 <xpath expr="//button[@string='Fill Inventory']" position="attributes">
27 <attribute name="states">open</attribute>
28 <attribute name="context">{'default_exhaustive': exhaustive}</attribute>
29 </xpath>
30
31 <!-- Control locations added by user on inventory line -->
32 <xpath expr="/form//field[@name='inventory_line_id']"
33 position="attributes">
34 <attribute name="context">{'location_id': location_id}</attribute>
35 </xpath>
36 <xpath expr="/form//field[@name='inventory_line_id']/tree//field[@name='location_id']"
37 position="attributes">
38 <attribute name="on_change">onchange_location_id(parent.location_id, parent.exhaustive, location_id)</attribute>
39 </xpath>
40 <xpath expr="/form//field[@name='inventory_line_id']/form//field[@name='location_id']"
41 position="attributes">
42 <attribute name="on_change">onchange_location_id(parent.location_id, parent.exhaustive, location_id)</attribute>
43 </xpath>
44
45 <!-- Add button to open an inventory. Call wizard on confirm inventory -->
46 <xpath expr="/form//button[@name='action_cancel_inventory']" position="before">
47 <button name="action_open" states="draft" string="Open Inventory" type="object" class="oe_highlight" groups="stock.group_stock_user"/>
48 </xpath>
49 <!-- Show the "cancel" button in state "open" -->
50 <xpath expr="/form//button[@name='action_cancel_inventory']" position="attributes">
51 <attribute name="states">draft,open,confirm,done</attribute>
52 </xpath>
53 <!-- hijack the "confirm" button -->
54 <xpath expr="/form//button[@name='action_confirm']" position="attributes">
55 <attribute name="states">open</attribute>
56 <attribute name="name">confirm_missing_locations</attribute>
57 </xpath>
58 </field>
59 </record>
60
61 <!-- Add filter for complete or partial inventory -->
62 <record model="ir.ui.view" id="view_inventory_complete_filter">
63 <field name="name">complete.inventory.filter</field>
64 <field name="model">stock.inventory</field>
65 <field name="inherit_id" ref="stock.view_inventory_filter"/>
66 <field name="arch" type="xml">
67 <xpath expr="//field[@name='name']" position="before">
68 <filter icon="terp-check" name="exhaustive" string="Exhaustive" domain="[('exhaustive', '=', True)]"
69 help="Only select exhaustive Inventories." />
70 <separator orientation="vertical"/>
71 </xpath>
72 </field>
73 </record>
74
75 <!-- Show exhaustive inventories by default -->
76 <record id="stock.action_inventory_form" model="ir.actions.act_window">
77 <field name="context">{'full':'1', 'search_default_exhaustive':1}</field>
78 </record>
79
80 </data>
81</openerp>
0\ No newline at end of file82\ No newline at end of file
183
=== added directory 'stock_inventory_location/test'
=== added file 'stock_inventory_location/test/inventory_exhaustive_test.yml'
--- stock_inventory_location/test/inventory_exhaustive_test.yml 1970-01-01 00:00:00 +0000
+++ stock_inventory_location/test/inventory_exhaustive_test.yml 2014-06-12 16:47:03 +0000
@@ -0,0 +1,128 @@
1-
2 This file will test an exhaustive inventory.
3 I will call open_action method and check if state of inventories are 'open'.
4-
5 !python {model: stock.inventory}: |
6 self.action_open(cr, uid, [ref('inventory_exhaustive')])
7 inventory_state = self.read(cr, uid, [ref('inventory_exhaustive')], ['state'])[0]['state']
8 assert inventory_state == 'open', "Inventory in state '%s'. It should be 'open'" % inventory_state
9-
10 I will check that the function get_missing_locations return some locations.
11-
12 !python {model: stock.inventory}: |
13 missing_loc_ids = self.get_missing_locations(cr, uid, [ref('inventory_exhaustive')], context=context)
14 assert len(missing_loc_ids), "get_missing_locations did not return any ID."
15-
16 I create a wizard record for stock_confirm_uninventoried_location to verify that it contains the uninventoried locations
17-
18 !python {model: stock.inventory.uninventoried.locations}: |
19 ctx = dict(context, active_ids=[ref('inventory_exhaustive')])
20 wizard_id = self.create(cr, uid, {}, context=ctx)
21 wizard = self.browse(cr, uid, wizard_id, context=ctx)
22 assert len(wizard.location_ids) > 0 , "The wizard does not contain any lines."
23-
24 I add products to exhaustive inventory.
25 Adding 17” LCD Monitor.
26-
27 !record {model: stock.inventory.line, id: lines_inventory_location_pc1}:
28 product_id: product.product_product_7
29 product_uom: product.product_uom_unit
30 company_id: base.main_company
31 inventory_id: inventory_exhaustive
32 product_qty: 18.0
33 location_id: stock.stock_location_14
34
35-
36 Adding USB Keyboard, QWERTY.
37-
38 !record {model: stock.inventory.line, id: lines_inventory_location_pc3}:
39 product_id: product.product_product_8
40 product_uom: product.product_uom_unit
41 company_id: base.main_company
42 inventory_id: inventory_exhaustive
43 product_qty: 5.0
44 location_id: stock.stock_location_14
45
46-
47 I will call the function _get_locations_open_inventories and check the result.
48 The function will return only the location_id of the exhaustive inventory.
49-
50 !python {model: stock.inventory}: |
51 locations = self._get_locations_open_inventories(cr, uid)
52 assert len(locations) == 1, "Function return wrong results: %s" % locations
53 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])
54-
55 I will call the function onchange_location_id.
56 The function must return True in the first case, and return a warning dictionnary in the second test.
57-
58 !python {model: stock.inventory.line}: |
59 res = self.onchange_location_id(cr, uid, [], ref('stock.stock_location_14'), True, ref('stock.stock_location_14'))
60 assert res == True, "Exhaustive: The function 'onchange_location_id' should return True and return '%s'" % res
61 res = self.onchange_location_id(cr, uid, [], ref('stock.stock_location_14'), True, ref('stock.stock_location_components'))
62 assert res.get('warning', False) != False , "Function 'onchange_location_id': Warning not raise. ('%s)" % res
63
64-
65 I will check that the function get_missing_locations does not return any locations.
66-
67 !python {model: stock.inventory}: |
68 missing_loc_ids = self.get_missing_locations(cr, uid, [ref('inventory_exhaustive')], context=context)
69 assert not missing_loc_ids, "get_missing_locations should not return IDs but returned %s" % missing_loc_ids
70-
71 I create a wizard record for stock_confirm_uninventoried_location and validate it
72-
73 !python {model: stock.inventory.uninventoried.locations}: |
74 ctx = dict(context, active_ids=[ref('inventory_exhaustive')])
75 wizard_id = self.create(cr, uid, {}, context=ctx)
76 wizard = self.browse(cr, uid, wizard_id, context=ctx)
77 assert len(wizard.location_ids) == 0 , "The wizard should not contain any lines but contains %s." % wizard.location_ids
78 self.confirm_uninventoried_locations(cr, uid, wizard_id, context=ctx)
79-
80 Stock moves are not allowed in the locations during the inventory.
81-
82 !python {model: stock.move}: |
83 # TODOv8: remove this test, this is already part of trunk-wms
84 from stock_inventory_location import ExhaustiveInventoryException
85 try:
86 self.create(
87 cr,uid, {
88 'name': 'Bad move',
89 'location_id': ref('stock.stock_location_14'),
90 'location_dest_id': ref('stock.stock_location_3'),
91 'product_id': ref('product.product_product_8'),
92 'product_uom': ref('product.product_uom_unit'),
93 'date_expected': '2020-01-01 00:00:00'
94 })
95 except ExhaustiveInventoryException as e:
96 log("Good! The Stock Move was refused: %s" % e)
97-
98 I will confirm the exhaustive inventory
99-
100 !python {model: stock.inventory}: |
101 self.action_confirm(cr, uid, [ref('inventory_exhaustive')])
102 inventory_state = self.read(cr, uid, [ref('inventory_exhaustive')], ['state'])[0]['state']
103 assert inventory_state == 'confirm', "Exhaustive inventory is in state '%s'. It should be 'confirm'" % inventory_state
104
105-
106 I will validate the exhaustive inventory
107-
108 !python {model: stock.inventory}: |
109 self.action_done(cr, uid, [ref('inventory_exhaustive')])
110 inventory_state = self.read(cr, uid, [ref('inventory_exhaustive')], ['state'])[0]['state']
111 assert inventory_state == 'done', "Exhaustive inventory is in state '%s'. It should be 'done'" % inventory_state
112
113-
114 I will verify the quantity for each products.
115-
116 !python {model: product.product}: |
117 ctx = dict(context, location=[ref('stock.stock_location_14')])
118 prod_qty_avail = self.read(cr, uid, [ref('product.product_product_7')], ['qty_available'], context=ctx)[0]['qty_available']
119 assert prod_qty_avail == 18.0, "The stock of PC1 was not set to 18.0: %s" % prod_qty_avail
120
121 prod_qty_avail = self.read(cr, uid, [ref('product.product_product_8')], ['qty_available'], context=ctx)[0]['qty_available']
122 assert prod_qty_avail == 5.0, "The stock of PC3 was not set to 5.0: %s" % prod_qty_avail
123
124 prod_qty_avail = self.read(cr, uid, [ref('product.product_product_24')], ['qty_available'], context=ctx)[0]['qty_available']
125 assert prod_qty_avail == 0.0, "The stock of KEYA was not set to 0: %s" % prod_qty_avail
126
127 prod_qty_avail = self.read(cr, uid, [ref('product.product_product_25')], ['qty_available'], context=ctx)[0]['qty_available']
128 assert prod_qty_avail == 0.0, "The stock of MOU was not set to 0: %s" % prod_qty_avail
0129
=== added file 'stock_inventory_location/test/inventory_future_test.yml'
--- stock_inventory_location/test/inventory_future_test.yml 1970-01-01 00:00:00 +0000
+++ stock_inventory_location/test/inventory_future_test.yml 2014-06-12 16:47:03 +0000
@@ -0,0 +1,13 @@
1-
2 Check that an inventory with a date in the future cannot be opened.
3-
4 !python {model: stock.inventory}: |
5 # TODO v8: maybe this is already part of the new WMS
6 from osv.osv import except_osv
7 self.action_open(cr, uid, [ref('inventory_future')])
8 try:
9 self.action_done(cr, uid, [ref('inventory_future')])
10 except except_osv as e:
11 log("Good! The Inventory could not be opened: %s" % e)
12 inventory_state = self.read(cr, uid, [ref('inventory_future')], ['state'])[0]['state']
13 assert inventory_state != 'done', "Future inventory is done."
014
=== added file 'stock_inventory_location/test/inventory_standard_test.yml'
--- stock_inventory_location/test/inventory_standard_test.yml 1970-01-01 00:00:00 +0000
+++ stock_inventory_location/test/inventory_standard_test.yml 2014-06-12 16:47:03 +0000
@@ -0,0 +1,100 @@
1-
2 This file will test a non exhaustive inventory.
3 I will call open_action method and check if state of inventories are 'open'.
4-
5 !python {model: stock.inventory}: |
6 self.action_open(cr, uid, [ref('inventory_standard')])
7 inventory_state = self.read(cr, uid, [ref('inventory_standard')], ['state'])[0]['state']
8 assert inventory_state == 'open', "Partial inventory have '%s' state. It should be 'open'" % inventory_state
9
10-
11 In order, I add products to inventory.
12 Adding Azerty Keyboard.
13-
14 !record {model: stock.inventory.line, id: lines_inventory_location_kbaz}:
15 product_id: product.product_product_9
16 product_uom: product.product_uom_unit
17 company_id: base.main_company
18 inventory_id: inventory_standard
19 product_qty: 18.0
20 location_id: stock.stock_location_components
21
22-
23 Adding Optical Mouse.
24-
25 !record {model: stock.inventory.line, id: lines_inventory_location_optm}:
26 product_id: product.product_product_10
27 product_uom: product.product_uom_unit
28 company_id: base.main_company
29 inventory_id: inventory_standard
30 product_qty: 12.0
31 location_id: stock.stock_location_components
32
33-
34 Adding Multimedia Speakers.
35-
36 !record {model: stock.inventory.line, id: lines_inventory_location_grca}:
37 product_id: product.product_template_31
38 product_uom: product.product_uom_unit
39 company_id: base.main_company
40 inventory_id: inventory_standard
41 product_qty: 32.0
42 location_id: stock.stock_location_components
43
44-
45 I will call the function _get_locations_open_inventories and check the result.
46 The function will return no locations because it's not an exhaustive inventory.
47-
48 !python {model: stock.inventory}: |
49 locations = self._get_locations_open_inventories(cr, uid)
50 assert len(locations) == 0, "Function return wrong results: %s" % locations
51
52-
53 I will call the function onchange_location_id.
54 The function must to return true in all test case.
55-
56 !python {model: stock.inventory.line}: |
57 res = self.onchange_location_id(cr, uid, [], [(6,0,[ref('stock.stock_location_components')])], False, ref('stock.stock_location_components'))
58 assert res == True, "The function 'onchange_location_id' should return True and return '%s'" % res
59 res = self.onchange_location_id(cr, uid, [], [(6,0,[ref('stock.stock_location_components')])], False, ref('stock.stock_location_14'))
60 assert res == True, "The function 'onchange_location_id' should return True and return '%s'" % res
61
62-
63 I will call the function _check_inventory.
64 The function must return True in all test cases.
65-
66 !python {model: stock.location}: |
67 res = self._check_inventory(cr, uid, ref('stock.stock_location_components'))
68 assert res == True, "The function '_check_inventory' should return True and return '%s'" % res
69 res = self._check_inventory(cr, uid, ref('stock.stock_location_14'))
70 assert res == True, "The function '_check_inventory' should return True and return '%s'" % res
71
72-
73 I will confirm inventory.
74-
75 !python {model: stock.inventory}: |
76 self.action_confirm(cr, uid, [ref('inventory_standard')])
77 inventory_state = self.read(cr, uid, [ref('inventory_standard')], ['state'])[0]['state']
78 assert inventory_state == 'confirm', "Partial inventory have '%s' state. It should be 'confirm'" % inventory_state
79
80-
81 I will validate inventory
82-
83 !python {model: stock.inventory}: |
84 self.action_done(cr, uid, [ref('inventory_standard')])
85 inventory_state = self.read(cr, uid, [ref('inventory_standard')], ['state'])[0]['state']
86 assert inventory_state == 'done', "Partial inventory have '%s' state. It should be 'done'" % inventory_state
87
88-
89 I will verify the quantity for each products.
90-
91 !python {model: product.product}: |
92 ctx={'location': [ref('stock.stock_location_components')]}
93 prod_qty_avail = self.read(cr, uid, [ref('product.product_product_9')], ['qty_available'], context=ctx)[0]['qty_available']
94 assert prod_qty_avail == 18.0, "The stock of CPU1 was not set to 18.0: %s" % prod_qty_avail
95
96 prod_qty_avail = self.read(cr, uid, [ref('product.product_product_10')], ['qty_available'], context=ctx)[0]['qty_available']
97 assert prod_qty_avail == 12.0, "The stock of CPU3 was not set to 12.0: %s" % prod_qty_avail
98
99 prod_qty_avail = self.read(cr, uid, [ref('product.product_template_31')], ['qty_available'], context=ctx)[0]['qty_available']
100 assert prod_qty_avail == 32.0, "The stock of FAN was not set to 32.0: %s" % prod_qty_avail
0101
=== 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-12 16:47:03 +0000
@@ -0,0 +1,22 @@
1# -*- coding: utf-8 -*-1
2##############################################################################
3#
4# This module is copyright (C) 2013 Numérigraphe SARL. All Rights Reserved.
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18#
19##############################################################################
20
21import stock_fill_location_inventory
22import stock_confirm_uninventoried_location
023
=== 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-12 16:47:03 +0000
@@ -0,0 +1,62 @@
1# -*- encoding: utf-8 -*-
2##############################################################################
3#
4# This module is copyright (C) 2013 Numérigraphe SARL. All Rights Reserved.
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18#
19##############################################################################
20
21from openerp.osv import fields, orm
22
23
24class stock_inventory_uninventoried_location(orm.TransientModel):
25 _name = 'stock.inventory.uninventoried.locations'
26 _description = 'Confirm the uninventoried Locations.'
27
28 _columns = {
29 'location_ids': fields.many2many(
30 'stock.location',
31 'stock_inventory_uninventoried_location_rel',
32 'location_id', 'wizard_id',
33 'Uninventoried location', readonly=True),
34 }
35
36 def default_locations(self, cr, uid, context=None):
37 """Initialize view with the list of uninventoried locations."""
38 return self.pool['stock.inventory'].get_missing_locations(
39 cr, uid, context['active_ids'], context=context)
40
41 _defaults = {
42 'location_ids': default_locations,
43 }
44
45 def confirm_uninventoried_locations(self, cr, uid, ids, context=None):
46 """Add the missing inventory lines with qty=0 and confirm inventory"""
47 inventory_ids = context['active_ids']
48 inventory_obj = self.pool['stock.inventory']
49 wizard_obj = self.pool['stock.fill.inventory']
50 for inventory in inventory_obj.browse(cr, uid, inventory_ids,
51 context=context):
52 if inventory.exhaustive:
53 # silently run the import wizard with qty=0
54 wizard_id = wizard_obj.create(
55 cr, uid, {'location_id': inventory.location_id.id,
56 'recursive': True,
57 'set_stock_zero': True}, context=context)
58 wizard_obj.fill_inventory(cr, uid, [wizard_id],
59 context=context)
60
61 inventory_obj.action_confirm(cr, uid, inventory_ids, context=context)
62 return {'type': 'ir.actions.act_window_close'}
063
=== 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-12 16:47:03 +0000
@@ -0,0 +1,33 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3 <data>
4 <!-- The view definition is similar with stock_inventory_hierarchical_location/wizard/stock_inventory_missing_locations_view.xml,
5 but the code is different.
6 This wizard compare declared locations with locations on inventory lines to present the uninventoried (empty) locations to user. -->
7 <record id="view_confirm_uninventoried_location" model="ir.ui.view">
8 <field name="name">stock.inventory.uninventoried.locations.form</field>
9 <field name="model">stock.inventory.uninventoried.locations</field>
10 <field name="arch" type="xml">
11 <form string="Confirm empty locations" version="7.0">
12 <group colspan="4" col="1">
13 <separator string="The following Locations are empty"/>
14 <label string="The following Stock Locations are part of the current Physical Inventory, but no Inventory Line has been recorded for them."/>
15 <label string="It could either mean that the Locations are empty, or that the Inventory is not yet complete."/>
16 <label string="If you confirm the Inventory, these Locations will be considered empty and their content will be purged."/>
17 <field name="location_ids" nolabel="1">
18 <tree>
19 <field name="name"/>
20 </tree>
21 </field>
22 <separator string=""/>
23 </group>
24 <footer>
25 <button name="confirm_uninventoried_locations" string="Purge contents and confirm Inventory" type="object" class="oe_highlight"/>
26 or
27 <button string="Cancel" class="oe_link" special="cancel" default_focus="1"/>
28 </footer>
29 </form>
30 </field>
31 </record>
32 </data>
33</openerp>
0\ No newline at end of file34\ No newline at end of file
135
=== 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-12 16:47:03 +0000
@@ -0,0 +1,47 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# This module is copyright (C) 2013 Numérigraphe SARL. All Rights Reserved.
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18#
19##############################################################################
20
21from openerp.osv import fields, orm
22
23
24class FillInventoryWizard(orm.TransientModel):
25 """Add a field that lets the client make the location read-only"""
26 _inherit = 'stock.fill.inventory'
27
28 _columns = {
29 'exhaustive': fields.boolean('Exhaustive', readonly=True)
30 }
31
32 def default_get(self, cr, uid, fields, context=None):
33 """Get 'location_id' and 'exhaustive' from the inventory"""
34 if context is None:
35 context = {}
36 inv_id = context.get('active_id')
37
38 res = super(FillInventoryWizard, self).default_get(
39 cr, uid, fields, context=context)
40 if (context.get('active_model') == 'stock.inventory'
41 and inv_id
42 and 'location_id' in fields):
43 inventory = self.pool['stock.inventory'].browse(
44 cr, uid, context['active_id'], context=context)
45 res.update({'location_id': inventory.location_id.id,
46 'exhaustive': inventory.exhaustive})
47 return res
048
=== 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-12 16:47:03 +0000
@@ -0,0 +1,18 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3 <data>
4 <record id="view_stock_fill_inventory_location" model="ir.ui.view">
5 <field name="name">Import Inventory</field>
6 <field name="model">stock.fill.inventory</field>
7 <field name="inherit_id" ref="stock.view_stock_fill_inventory" />
8 <field name="arch" type="xml">
9 <xpath expr="//field[@name='location_id']" position="after">
10 <field name="exhaustive" invisible="1" />
11 </xpath>
12 <xpath expr="//field[@name='location_id']" position="attributes">
13 <attribute name="attrs">{'readonly':[('exhaustive','=',True)]}</attribute>
14 </xpath>
15 </field>
16 </record>
17 </data>
18</openerp>