[ADD] stock_archive_constraint: Allows to block archiving products and locations with associated stock.quant or stock.move

This commit is contained in:
Víctor Martínez
2020-11-27 09:32:49 +01:00
committed by Marçal Isern
parent d86fe3b1be
commit f159e23a5b
15 changed files with 488 additions and 0 deletions

View File

View File

@@ -0,0 +1,3 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import models

View File

@@ -0,0 +1,15 @@
# Copyright 2020 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
"name": "Stock archive constraint",
"version": "12.0.1.0.0",
"license": "AGPL-3",
"website": "https://github.com/stock-logistics-warehouse",
"author": "Tecnativa, Odoo Community Association (OCA)",
"development_status": "Production/Stable",
"category": "Warehouse",
"depends": ["stock"],
"installable": True,
"maintainers": ["victoralmau"],
}

View File

@@ -0,0 +1,88 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * stock_archive_constraint
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 12.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-01-08 09:03+0000\n"
"PO-Revision-Date: 2021-01-08 10:06+0100\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: \n"
"X-Generator: Poedit 2.3\n"
#. module: stock_archive_constraint
#: model:ir.model,name:stock_archive_constraint.model_stock_location
msgid "Inventory Locations"
msgstr "Ubicaciones de inventario"
#. module: stock_archive_constraint
#: code:addons/stock_archive_constraint/models/stock_location.py:41
#, python-format
msgid ""
"It is not possible to archive location '%s' which has associated picking "
"lines."
msgstr ""
"No es posible archivar la ubicación '%s' que tiene líneas de albaranes "
"asociadas."
#. module: stock_archive_constraint
#: code:addons/stock_archive_constraint/models/stock_location.py:24
#, python-format
msgid ""
"It is not possible to archive location '%s' which has associated stock "
"quantities."
msgstr ""
"No es posible archivar la ubicación '%s' que tiene asociadas cantidades "
"de stock."
#. module: stock_archive_constraint
#: code:addons/stock_archive_constraint/models/stock_location.py:58
#, python-format
msgid ""
"It is not possible to archive location '%s' which has associated stock "
"reservations."
msgstr ""
"No es posible archivar la ubicación '%s' que tiene reservas de stock "
"asociadas."
#. module: stock_archive_constraint
#: code:addons/stock_archive_constraint/models/product_product.py:36
#, python-format
msgid ""
"It is not possible to archive product '%s' which has associated picking "
"lines."
msgstr ""
"No es posible archivar el producto '%s' que tiene líneas de albaranes "
"asociadas."
#. module: stock_archive_constraint
#: code:addons/stock_archive_constraint/models/product_product.py:22
#, python-format
msgid ""
"It is not possible to archive product '%s' which has associated stock "
"quantities."
msgstr ""
"No es posible archivar el producto '%s' que tiene asociadas cantidades "
"de stock."
#. module: stock_archive_constraint
#: code:addons/stock_archive_constraint/models/product_product.py:50
#, python-format
msgid ""
"It is not possible to archive product '%s' which has associated stock "
"reservations."
msgstr ""
"No es posible archivar el producto '%s' que tiene reservas de stock "
"asociadas."
#. module: stock_archive_constraint
#: model:ir.model,name:stock_archive_constraint.model_product_product
msgid "Product"
msgstr "Producto"

View File

@@ -0,0 +1,63 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * stock_archive_constraint
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 12.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-01-08 09:03+0000\n"
"PO-Revision-Date: 2021-01-08 09:03+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_archive_constraint
#: model:ir.model,name:stock_archive_constraint.model_stock_location
msgid "Inventory Locations"
msgstr ""
#. module: stock_archive_constraint
#: code:addons/stock_archive_constraint/models/stock_location.py:41
#, python-format
msgid "It is not possible to archive location '%s' which has associated picking lines."
msgstr ""
#. module: stock_archive_constraint
#: code:addons/stock_archive_constraint/models/stock_location.py:24
#, python-format
msgid "It is not possible to archive location '%s' which has associated stock quantities."
msgstr ""
#. module: stock_archive_constraint
#: code:addons/stock_archive_constraint/models/stock_location.py:58
#, python-format
msgid "It is not possible to archive location '%s' which has associated stock reservations."
msgstr ""
#. module: stock_archive_constraint
#: code:addons/stock_archive_constraint/models/product_product.py:36
#, python-format
msgid "It is not possible to archive product '%s' which has associated picking lines."
msgstr ""
#. module: stock_archive_constraint
#: code:addons/stock_archive_constraint/models/product_product.py:22
#, python-format
msgid "It is not possible to archive product '%s' which has associated stock quantities."
msgstr ""
#. module: stock_archive_constraint
#: code:addons/stock_archive_constraint/models/product_product.py:50
#, python-format
msgid "It is not possible to archive product '%s' which has associated stock reservations."
msgstr ""
#. module: stock_archive_constraint
#: model:ir.model,name:stock_archive_constraint.model_product_product
msgid "Product"
msgstr ""

View File

@@ -0,0 +1,5 @@
# Copyright 2020 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import product_product
from . import stock_location

View File

@@ -0,0 +1,52 @@
# Copyright 2020 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import _, api, models
from odoo.exceptions import ValidationError
class ProductProduct(models.Model):
_inherit = "product.product"
@api.constrains("active")
def _check_active_stock_archive_constraint_stock_quant(self):
res = self.env['stock.quant'].search(
[
('location_id.usage', 'in', ('internal', 'transit')),
('product_id', 'in', self.filtered(lambda x: not x.active).ids),
('quantity', '!=', 0.0)
], limit=1
)
if res:
raise ValidationError(
_("It is not possible to archive product '%s' which has "
"associated stock quantities." % res[0].product_id.display_name)
)
@api.constrains("active")
def _check_active_stock_archive_constraint_stock_move(self):
res = self.env['stock.move'].search(
[
('product_id', 'in', self.filtered(lambda x: not x.active).ids),
('state', 'not in', ('done', 'cancel'))
], limit=1
)
if res:
raise ValidationError(
_("It is not possible to archive product '%s' which has "
"associated picking lines." % res[0].product_id.display_name)
)
@api.constrains("active")
def _check_active_stock_archive_constraint_stock_move_line(self):
res = self.env['stock.move.line'].search(
[
('product_id', 'in', self.filtered(lambda x: not x.active).ids),
('state', 'not in', ('done', 'cancel'))
], limit=1
)
if res:
raise ValidationError(
_("It is not possible to archive product '%s' which has "
"associated stock reservations." % res[0].product_id.display_name)
)

View File

@@ -0,0 +1,12 @@
# Copyright 2020 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import api, models
class ProductTemplate(models.Model):
_inherit = "product.template"
@api.constrains("active")
def _check_active_stock_archive_constraint(self):
self.product_variant_ids._check_active_stock_archive_constraint()

View File

@@ -0,0 +1,60 @@
# Copyright 2020 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import _, api, models
from odoo.exceptions import ValidationError
class StockLocation(models.Model):
_inherit = "stock.location"
@api.constrains("active")
def _check_active_stock_archive_constraint_stock_quant(self):
res = self.env['stock.quant'].search(
[
'&',
('location_id.usage', 'in', ('internal', 'transit')),
'|',
('location_id', 'in', self.filtered(lambda x: not x.active).ids),
('location_id', 'child_of', self.filtered(lambda x: not x.active).ids),
], limit=1
)
if res:
raise ValidationError(
_("It is not possible to archive location '%s' which has "
"associated stock quantities." % res[0].display_name)
)
@api.constrains("active")
def _check_active_stock_archive_constraint_stock_move(self):
res = self.env['stock.move'].search(
[
'&',
('state', 'not in', ('done', 'cancel')),
'|',
('location_id', 'in', self.filtered(lambda x: not x.active).ids),
('location_id', 'child_of', self.filtered(lambda x: not x.active).ids)
], limit=1
)
if res:
raise ValidationError(
_("It is not possible to archive location '%s' which has "
"associated picking lines." % res[0].display_name)
)
@api.constrains("active")
def _check_active_stock_archive_constraint_stock_move_line(self):
res = self.env['stock.move.line'].search(
[
'&',
('state', 'not in', ('done', 'cancel')),
'|',
('location_id', 'in', self.filtered(lambda x: not x.active).ids),
('location_id', 'child_of', self.filtered(lambda x: not x.active).ids)
], limit=1
)
if res:
raise ValidationError(
_("It is not possible to archive location '%s' which has "
"associated stock reservations." % res[0].display_name)
)

View File

@@ -0,0 +1,4 @@
* `Tecnativa <https://www.tecnativa.com>`_:
* Carlos Daudén
* Víctor Martínez

View File

@@ -0,0 +1,2 @@
Allows to block archiving products with associated stock.quant or stock.move.
Allows to block archiving locations with associated stock.quant or stock.move.

View File

@@ -0,0 +1,2 @@
#. Go to Settings,> Users> Edit a user and check the "Manage Multiple Stock Locations" permission
#. Go to Inventory> Settings> Locations and disable one

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,4 @@
# Copyright 2020 Tecnativa - Víctor Martínez
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from . import test_location_archive_constraint

View File

@@ -0,0 +1,178 @@
# Copyright 2020 Tecnativa - Víctor Martínez
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo.tests.common import SavepointCase, Form
from odoo.exceptions import ValidationError
class TestLocationArchiveConstraint(SavepointCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.product_1 = cls._create_product(cls, 'Product 1')
cls.product_2 = cls._create_product(cls, 'Product 2')
stock_location_stock = cls.env.ref('stock.stock_location_stock')
cls.stock_location = cls._create_stock_location(
cls, "%s (Copy)" % (stock_location_stock.name)
)
cls.stock_location_child = cls._create_stock_location(
cls, "%s (Child)" % (cls.stock_location.name)
)
cls.stock_location_child.location_id = cls.stock_location
def _create_product(self, name):
product_form = Form(self.env['product.product'])
product_form.name = name
product_form.type = 'product'
return product_form.save()
def _create_stock_location(self, name):
stock_location_form = Form(self.env['stock.location'])
stock_location_form.name = name
stock_location_form.usage = self.env.ref('stock.stock_location_stock').usage
return stock_location_form.save()
def _create_stock_inventory(self, location_id, product_id, qty):
stock_inventory_form = Form(self.env['stock.inventory'])
stock_inventory_form.name = 'INV: %s' % product_id.display_name
stock_inventory_form.filter = 'product'
stock_inventory_form.product_id = product_id
stock_inventory_form.location_id = location_id
stock_inventory = stock_inventory_form.save()
stock_inventory.action_start()
for line_id in stock_inventory.line_ids:
line_id.product_qty = qty
stock_inventory.action_validate()
def _create_stock_move(self, location_id, location_dest_id, product_id, qty):
stock_move_form = Form(self.env['stock.move'])
stock_move_form.name = product_id.display_name
stock_move_form.location_id = location_id
stock_move_form.location_dest_id = location_dest_id
stock_move_form.product_id = product_id
stock_move_form.product_uom_qty = qty
stock_move = stock_move_form.save()
stock_move._action_done()
def _create_stock_move_line(self, location_id, location_dest_id, product_id, qty):
stock_move_line_form = Form(self.env['stock.move.line'])
stock_move_line_form.location_id = location_id
stock_move_line_form.location_dest_id = location_dest_id
stock_move_line_form.product_id = product_id
stock_move_line_form.product_uom_qty = qty
stock_move_line_form.qty_done = qty
stock_move_line_form.state = 'done'
stock_move_line_form.save()
def _create_stock_picking(self, location_id, location_dest_id, product_id, qty):
stock_picking_form = Form(self.env['stock.picking'])
stock_picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
with stock_picking_form.move_ids_without_package.new() as line:
line.product_id = product_id
line.product_uom_qty = qty
stock_picking = stock_picking_form.save()
stock_picking.write({
'location_id': location_id.id,
'location_dest_id': location_dest_id.id,
})
stock_picking.action_confirm()
for line in stock_picking.move_ids_without_package:
line.quantity_done = line.product_uom_qty
stock_picking.button_validate()
def test_archive_product_ok(self):
self.product_1.active = False
self.assertFalse(self.product_1.active)
self.product_2.active = False
self.assertFalse(self.product_2.active)
def test_archive_unarchive_product(self):
self.product_1.active = False
self.assertFalse(self.product_1.active)
self.product_1.active = True
self.assertTrue(self.product_1.active)
def test_archive_product_with_stock_move_in(self):
self._create_stock_move(
self.env.ref('stock.stock_location_suppliers'),
self.stock_location, self.product_2, 20.00
)
self.product_1.active = False
self.assertFalse(self.product_1.active)
with self.assertRaises(ValidationError):
self.product_2.active = False
def test_archive_product_with_stock_move_line_in(self):
self._create_stock_move_line(
self.env.ref('stock.stock_location_suppliers'),
self.stock_location, self.product_2, 20.00
)
self.product_1.active = False
self.assertFalse(self.product_1.active)
with self.assertRaises(ValidationError):
self.product_2.active = False
def test_archive_product_with_stock_picking_in(self):
self._create_stock_picking(
self.env.ref('stock.stock_location_suppliers'),
self.stock_location, self.product_2, 20.00
)
self.product_1.active = False
self.assertFalse(self.product_1.active)
with self.assertRaises(ValidationError):
self.product_2.active = False
def test_archive_product_with_stock_picking_in_out(self):
self._create_stock_picking(
self.env.ref('stock.stock_location_suppliers'),
self.stock_location, self.product_2, 20.00
)
self._create_stock_picking(
self.stock_location,
self.env.ref('stock.stock_location_customers'), self.product_2, 20.00
)
self.product_1.active = False
self.assertFalse(self.product_1.active)
self.product_2.active = False
self.assertFalse(self.product_2.active)
def test_archive_product_stock_location(self):
self._create_stock_inventory(self.stock_location, self.product_2, 20.00)
self.product_1.active = False
self.assertFalse(self.product_1.active)
with self.assertRaises(ValidationError):
self.product_2.active = False
def test_archive_product_stock_location_child(self):
self._create_stock_inventory(self.stock_location_child, self.product_2, 20.00)
self.product_1.active = False
self.assertFalse(self.product_1.active)
with self.assertRaises(ValidationError):
self.product_2.active = False
def test_archive_unarchive_stock_location(self):
self.stock_location.active = False
self.assertFalse(self.stock_location.active)
self.stock_location.active = True
self.assertTrue(self.stock_location.active)
def test_archive_stock_location_ok(self):
self.stock_location.active = False
self.assertFalse(self.stock_location.active)
def test_archive_stock_location(self):
self._create_stock_inventory(self.stock_location, self.product_2, 20.00)
with self.assertRaises(ValidationError):
self.stock_location.active = False
def test_archive_unarchive_stock_location_child(self):
self.stock_location_child.active = False
self.assertFalse(self.stock_location_child.active)
self.stock_location_child.active = True
self.assertTrue(self.stock_location_child.active)
def test_archive_stock_location_child(self):
self._create_stock_inventory(self.stock_location_child, self.product_2, 20.00)
with self.assertRaises(ValidationError):
self.stock_location.active = False