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
This commit is contained in:
Lionel Sausin & Loïc Bellier
2016-03-21 17:12:39 +01:00
committed by Ben Stannard
parent e7d1bb9116
commit f2c73735d0
13 changed files with 380 additions and 0 deletions

View File

@@ -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
<https://github.com/OCA/stock-logistics-warehouse/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
<https://github.com/OCA/
stock-logistics-warehouse/issues/new?body=module:%20
stock_available_sale%0Aversion:%20
8.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Credits
=======
Contributors
------------
* Loïc Bellier (Numérigraphe) <lb@numerigraphe.com>
* Lionel Sausin (Numérigraphe) <ls@numerigraphe.com>
* Laetitia Gangloff (Acsone) <laetitia.gangloff@acsone.eu>
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.

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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"

View File

@@ -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 ""

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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()