mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
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:
committed by
Ben Stannard
parent
e7d1bb9116
commit
f2c73735d0
63
stock_inventory_lockdown/README.rst
Normal file
63
stock_inventory_lockdown/README.rst
Normal 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.
|
||||
5
stock_inventory_lockdown/__init__.py
Normal file
5
stock_inventory_lockdown/__init__.py
Normal 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
|
||||
17
stock_inventory_lockdown/__openerp__.py
Normal file
17
stock_inventory_lockdown/__openerp__.py
Normal 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,
|
||||
}
|
||||
32
stock_inventory_lockdown/i18n/fr.po
Normal file
32
stock_inventory_lockdown/i18n/fr.po
Normal 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"
|
||||
|
||||
31
stock_inventory_lockdown/i18n/stock_inventory_lockdown.pot
Normal file
31
stock_inventory_lockdown/i18n/stock_inventory_lockdown.pot
Normal 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 ""
|
||||
|
||||
BIN
stock_inventory_lockdown/images/location_locked.png
Normal file
BIN
stock_inventory_lockdown/images/location_locked.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
stock_inventory_lockdown/images/move_error.png
Normal file
BIN
stock_inventory_lockdown/images/move_error.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
7
stock_inventory_lockdown/models/__init__.py
Normal file
7
stock_inventory_lockdown/models/__init__.py
Normal 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
|
||||
29
stock_inventory_lockdown/models/stock_inventory.py
Normal file
29
stock_inventory_lockdown/models/stock_inventory.py
Normal 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()
|
||||
46
stock_inventory_lockdown/models/stock_location.py
Normal file
46
stock_inventory_lockdown/models/stock_location.py
Normal 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()
|
||||
58
stock_inventory_lockdown/models/stock_quant.py
Normal file
58
stock_inventory_lockdown/models/stock_quant.py
Normal 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
|
||||
5
stock_inventory_lockdown/tests/__init__.py
Normal file
5
stock_inventory_lockdown/tests/__init__.py
Normal 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
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user