From f2c73735d0cc10323d0477e9abc9a3a74e039fb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lionel=20Sausin=20=26=20Lo=C3=AFc=20Bellier?= Date: Mon, 21 Mar 2016 17:12:39 +0100 Subject: [PATCH] New module stock_inventory_lockdown This module is a feature extracted from stock_inventory_location. Ported to v8 and new API Adopted latest OCA standards Added tests --- stock_inventory_lockdown/README.rst | 63 +++++++++++++ stock_inventory_lockdown/__init__.py | 5 + stock_inventory_lockdown/__openerp__.py | 17 ++++ stock_inventory_lockdown/i18n/fr.po | 32 +++++++ .../i18n/stock_inventory_lockdown.pot | 31 +++++++ .../images/location_locked.png | Bin 0 -> 12899 bytes .../images/move_error.png | Bin 0 -> 14298 bytes stock_inventory_lockdown/models/__init__.py | 7 ++ .../models/stock_inventory.py | 29 ++++++ .../models/stock_location.py | 46 +++++++++ .../models/stock_quant.py | 58 ++++++++++++ stock_inventory_lockdown/tests/__init__.py | 5 + .../tests/test_stock_inventory_lockdown.py | 87 ++++++++++++++++++ 13 files changed, 380 insertions(+) create mode 100644 stock_inventory_lockdown/README.rst create mode 100644 stock_inventory_lockdown/__init__.py create mode 100644 stock_inventory_lockdown/__openerp__.py create mode 100644 stock_inventory_lockdown/i18n/fr.po create mode 100644 stock_inventory_lockdown/i18n/stock_inventory_lockdown.pot create mode 100644 stock_inventory_lockdown/images/location_locked.png create mode 100644 stock_inventory_lockdown/images/move_error.png create mode 100644 stock_inventory_lockdown/models/__init__.py create mode 100644 stock_inventory_lockdown/models/stock_inventory.py create mode 100644 stock_inventory_lockdown/models/stock_location.py create mode 100644 stock_inventory_lockdown/models/stock_quant.py create mode 100644 stock_inventory_lockdown/tests/__init__.py create mode 100644 stock_inventory_lockdown/tests/test_stock_inventory_lockdown.py diff --git a/stock_inventory_lockdown/README.rst b/stock_inventory_lockdown/README.rst new file mode 100644 index 000000000..a8dd6b5cd --- /dev/null +++ b/stock_inventory_lockdown/README.rst @@ -0,0 +1,63 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +====================================== +Lock down locations during inventories +====================================== +This module lets you lock down the locations during an inventory. + +Usage +===== + +.. image:: images/location_locked.png + :alt: Error message + +.. image:: images/move_error.png + :alt: Error message + +While an inventory is in the state "In progress", no stock moves +can be recorded in/out of the inventory's location: users will get an error +message. +Creating or modifying a locations is also forbidden. + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/153/8.0 + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed `feedback +`_. + +Credits +======= + +Contributors +------------ + +* Loïc Bellier (Numérigraphe) +* Lionel Sausin (Numérigraphe) +* Laetitia Gangloff (Acsone) + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/stock_inventory_lockdown/__init__.py b/stock_inventory_lockdown/__init__.py new file mode 100644 index 000000000..ef0c464f0 --- /dev/null +++ b/stock_inventory_lockdown/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# © 2013-2016 Numérigraphe SARL +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import models diff --git a/stock_inventory_lockdown/__openerp__.py b/stock_inventory_lockdown/__openerp__.py new file mode 100644 index 000000000..09bca4a4b --- /dev/null +++ b/stock_inventory_lockdown/__openerp__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# © 2013-2016 Numérigraphe SARL +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Inventory lock down", + "summary": "Lock down stock locations during inventories.", + "version": "8.0.1.0.0", + "depends": ["stock"], + "author": u"Numérigraphe,Odoo Community Association (OCA)", + "category": "Warehouse Management", + "images": [ + "images/move_error.png", + "images/location_locked.png"], + 'license': 'AGPL-3', + "installable": True, +} diff --git a/stock_inventory_lockdown/i18n/fr.po b/stock_inventory_lockdown/i18n/fr.po new file mode 100644 index 000000000..2dcb99f55 --- /dev/null +++ b/stock_inventory_lockdown/i18n/fr.po @@ -0,0 +1,32 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_inventory_lockdown +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-03-21 18:49+0000\n" +"PO-Revision-Date: 2016-03-21 18:49+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_lockdown +#: code:addons/stock_inventory_lockdown/models/stock_quant.py:42 +#: code:addons/stock_inventory_lockdown/models/stock_quant.py:56 +#, python-format +msgid "An inventory is being conducted at the following location(s):\n" +"%s" +msgstr "Un inventaire est en cours aux emplacements suivants :\n" +"%s" + +#. module: stock_inventory_lockdown +#: code:addons/stock_inventory_lockdown/models/stock_location.py:22 +#, python-format +msgid "An inventory is being conducted at this location" +msgstr "Un inventaire est en cours à cet emplacement" + diff --git a/stock_inventory_lockdown/i18n/stock_inventory_lockdown.pot b/stock_inventory_lockdown/i18n/stock_inventory_lockdown.pot new file mode 100644 index 000000000..10e6d1ee2 --- /dev/null +++ b/stock_inventory_lockdown/i18n/stock_inventory_lockdown.pot @@ -0,0 +1,31 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_inventory_lockdown +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-03-21 18:49+0000\n" +"PO-Revision-Date: 2016-03-21 18:49+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_lockdown +#: code:addons/stock_inventory_lockdown/models/stock_quant.py:42 +#: code:addons/stock_inventory_lockdown/models/stock_quant.py:56 +#, python-format +msgid "An inventory is being conducted at the following location(s):\n" +"%s" +msgstr "" + +#. module: stock_inventory_lockdown +#: code:addons/stock_inventory_lockdown/models/stock_location.py:22 +#, python-format +msgid "An inventory is being conducted at this location" +msgstr "" + diff --git a/stock_inventory_lockdown/images/location_locked.png b/stock_inventory_lockdown/images/location_locked.png new file mode 100644 index 0000000000000000000000000000000000000000..fd4448561a97748dbca70d36708db87d8c8fedcd GIT binary patch literal 12899 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 nH_WN$7=zF2yl7kKICI#}kwe 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 Di~(0R literal 0 HcmV?d00001 diff --git a/stock_inventory_lockdown/models/__init__.py b/stock_inventory_lockdown/models/__init__.py new file mode 100644 index 000000000..6062634c0 --- /dev/null +++ b/stock_inventory_lockdown/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# © 2013-2016 Numérigraphe SARL +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import stock_quant +from . import stock_inventory +from . import stock_location diff --git a/stock_inventory_lockdown/models/stock_inventory.py b/stock_inventory_lockdown/models/stock_inventory.py new file mode 100644 index 000000000..4886be466 --- /dev/null +++ b/stock_inventory_lockdown/models/stock_inventory.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# © 2013-2016 Numérigraphe SARL +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openerp import models, api + + +class StockInventory(models.Model): + _inherit = 'stock.inventory' + + @api.model + def _get_locations_open_inventories(self): + """IDs of location in open exhaustive inventories, with children""" + inventories = self.search([('state', '=', 'confirm')]) + if not inventories: + # Early exit if no match found + return [] + location_ids = inventories.mapped('location_id') + + # Extend to the children Locations + return self.env['stock.location'].search( + [('location_id', 'child_of', location_ids.ids), + ('usage', 'in', ['internal', 'transit'])]) + + @api.multi + def action_done(self): + """Add value in the context to ignore the lockdown""" + return super(StockInventory, + self.with_context(bypass_lockdown=True)).action_done() diff --git a/stock_inventory_lockdown/models/stock_location.py b/stock_inventory_lockdown/models/stock_location.py new file mode 100644 index 000000000..a15b8210f --- /dev/null +++ b/stock_inventory_lockdown/models/stock_location.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# © 2016 Numérigraphe SARL +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openerp import models, api, _ +from openerp.exceptions import ValidationError + + +class StockLocation(models.Model): + """Refuse changes during exhaustive Inventories""" + _inherit = 'stock.location' + _order = 'name' + + @api.multi + def _check_inventory(self): + """Error if an inventory is being conducted here""" + location_inventory_open_ids = self.env['stock.inventory'].sudo( + )._get_locations_open_inventories() + for location in self: + if location in location_inventory_open_ids: + raise ValidationError( + _('An inventory is being conducted at this ' + 'location')) + + @api.multi + def write(self, vals): + """Refuse write if an inventory is being conducted""" + locations_to_check = self + # If changing the parent, no inventory must conducted there either + if vals.get('location_id'): + locations_to_check |= self.browse(vals['location_id']) + locations_to_check._check_inventory() + return super(StockLocation, self).write(vals) + + @api.model + def create(self, vals): + """Refuse create if an inventory is being conducted at the parent""" + if 'location_id' in vals: + self.browse(vals['location_id'])._check_inventory() + return super(StockLocation, self).create(vals) + + @api.multi + def unlink(self): + """Refuse unlink if an inventory is being conducted""" + self._check_inventory() + return super(StockLocation, self).unlink() diff --git a/stock_inventory_lockdown/models/stock_quant.py b/stock_inventory_lockdown/models/stock_quant.py new file mode 100644 index 000000000..ed3dbb3ad --- /dev/null +++ b/stock_inventory_lockdown/models/stock_quant.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# © 2016 Numérigraphe SARL +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openerp import models, api, _ +from openerp.exceptions import ValidationError + + +class StockQuant(models.Model): + _inherit = 'stock.quant' + + @api.multi + def write(self, vals): + """Check that the location is not locked by an open inventory. + Check both the location as it was (source) and the location as + it will be (destination). + We verify the locations even if they are unchanged, because changing + ie. the quantity is not acceptable either. + @raise ValidationError if they are. + """ + if not self.env.context.get('bypass_lockdown', False): + # Find the locked locations + locked_location_ids = self.env[ + 'stock.inventory']._get_locations_open_inventories() + if locked_location_ids and 'location_id' in vals.keys(): + messages = set() + # Find the destination locations + location_dest_id = self.env['stock.location'].browse( + vals['location_id']) + for quant in self: + # Source locations + location_id = quant.location_id + # Moving to a location locked down + if location_dest_id in locked_location_ids: + messages.add(location_dest_id.name) + # Moving from a location locked down + if location_id in locked_location_ids: + messages.add(location_id.name) + if len(messages): + raise ValidationError( + _('An inventory is being conducted at the following ' + 'location(s):\n%s') % "\n - ".join(messages)) + return super(StockQuant, self).write(vals) + + @api.model + def create(self, vals): + """Check that the locations are not locked by an open inventory. + @raise ValidationError if they are. + """ + quant = super(StockQuant, self).create(vals) + if not self.env.context.get('bypass_lockdown', False): + locked_location_ids = self.env[ + 'stock.inventory']._get_locations_open_inventories() + if quant.location_id in locked_location_ids: + raise ValidationError( + _('An inventory is being conducted at the following ' + 'location(s):\n%s') % " - " + quant.location_id.name) + return quant diff --git a/stock_inventory_lockdown/tests/__init__.py b/stock_inventory_lockdown/tests/__init__.py new file mode 100644 index 000000000..c1942bfd6 --- /dev/null +++ b/stock_inventory_lockdown/tests/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# © 2013-2016 Numérigraphe SARL +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import test_stock_inventory_lockdown diff --git a/stock_inventory_lockdown/tests/test_stock_inventory_lockdown.py b/stock_inventory_lockdown/tests/test_stock_inventory_lockdown.py new file mode 100644 index 000000000..905aad629 --- /dev/null +++ b/stock_inventory_lockdown/tests/test_stock_inventory_lockdown.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# © 2014 Acsone SA/NV (http://www.acsone.eu) +# © 2016 Numérigraphe SARL +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openerp.exceptions import ValidationError + +from openerp.addons.stock.tests.common import TestStockCommon + + +class StockInventoryLocationTest(TestStockCommon): + def setUp(self): + super(StockInventoryLocationTest, self).setUp() + # Make a new location + self.new_location = self.env['stock.location'].create( + {'name': 'Test location', + 'usage': 'internal'}) + self.new_sublocation = self.env['stock.location'].create( + {'name': 'Test sublocation', + 'usage': 'internal', + 'location_id': self.new_location.id}) + # Input goods + self.env['stock.quant'].create( + {'location_id': self.new_location.id, + 'product_id': self.productA.id, + 'qty': 10.0}) + # Prepare an inventory + self.inventory = self.env['stock.inventory'].create( + {'name': 'Lock down location', + 'filter': 'none', + 'location_id': self.new_location.id}) + self.inventory.prepare_inventory() + self.assertTrue(self.inventory.line_ids, 'The inventory is empty.') + + def test_update_parent_location(self): + """Updating the parent of a location is OK if no inv. in progress.""" + self.inventory.action_cancel_inventory() + self.inventory.location_id.location_id = self.env.ref( + 'stock.stock_location_4') + + def test_update_parent_location_locked_down(self): + """Updating the parent of a location must fail""" + with self.assertRaises(ValidationError): + self.inventory.location_id.location_id = self.env.ref( + 'stock.stock_location_4') + + def test_inventory(self): + """We must still be able to finish the inventory""" + self.assertTrue(self.inventory.line_ids) + self.inventory.line_ids.write({'product_qty': 42.0}) + for line in self.inventory.line_ids: + self.assertNotEqual(line.product_id.with_context( + location=line.location_id.id).qty_available, 42.0) + self.inventory.action_done() + for line in self.inventory.line_ids: + self.assertEqual(line.product_id.with_context( + location=line.location_id.id).qty_available, 42.0) + + def test_inventory_sublocation(self): + """We must be able to make an inventory in a sublocation""" + inventory_subloc = self.env['stock.inventory'].create( + {'name': 'Lock down location', + 'filter': 'partial', + 'location_id': self.new_sublocation.id}) + inventory_subloc.prepare_inventory() + line = self.env['stock.inventory.line'].create( + {'product_id': self.productA.id, + 'product_qty': 22.0, + 'location_id': self.new_sublocation.id, + 'inventory_id': inventory_subloc.id}) + self.assertTrue(inventory_subloc.line_ids) + inventory_subloc.action_done() + self.assertEqual(line.product_id.with_context( + location=line.location_id.id).qty_available, 22.0) + + def test_move(self): + """Stock move must be forbidden during inventory""" + move = self.env['stock.move'].create({ + 'name': 'Test move lock down', + 'product_id': self.productA.id, + 'product_uom_qty': 10.0, + 'product_uom': self.productA.uom_id.id, + 'location_id': self.inventory.location_id.id, + 'location_dest_id': self.customer_location + }) + with self.assertRaises(ValidationError): + move.action_done()