diff --git a/stock_archive_constraint/README.rst b/stock_archive_constraint/README.rst new file mode 100644 index 000000000..e69de29bb diff --git a/stock_archive_constraint/__init__.py b/stock_archive_constraint/__init__.py new file mode 100644 index 000000000..83e553ac4 --- /dev/null +++ b/stock_archive_constraint/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import models diff --git a/stock_archive_constraint/__manifest__.py b/stock_archive_constraint/__manifest__.py new file mode 100644 index 000000000..9632f204c --- /dev/null +++ b/stock_archive_constraint/__manifest__.py @@ -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"], +} diff --git a/stock_archive_constraint/i18n/es.po b/stock_archive_constraint/i18n/es.po new file mode 100644 index 000000000..7a95b8cc2 --- /dev/null +++ b/stock_archive_constraint/i18n/es.po @@ -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" diff --git a/stock_archive_constraint/i18n/stock_archive_constraint.pot b/stock_archive_constraint/i18n/stock_archive_constraint.pot new file mode 100644 index 000000000..258a518fc --- /dev/null +++ b/stock_archive_constraint/i18n/stock_archive_constraint.pot @@ -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 "" + diff --git a/stock_archive_constraint/models/__init__.py b/stock_archive_constraint/models/__init__.py new file mode 100644 index 000000000..1b6574480 --- /dev/null +++ b/stock_archive_constraint/models/__init__.py @@ -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 diff --git a/stock_archive_constraint/models/product_product.py b/stock_archive_constraint/models/product_product.py new file mode 100644 index 000000000..896b03d1d --- /dev/null +++ b/stock_archive_constraint/models/product_product.py @@ -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) + ) diff --git a/stock_archive_constraint/models/product_template.py b/stock_archive_constraint/models/product_template.py new file mode 100644 index 000000000..d6813c9f4 --- /dev/null +++ b/stock_archive_constraint/models/product_template.py @@ -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() diff --git a/stock_archive_constraint/models/stock_location.py b/stock_archive_constraint/models/stock_location.py new file mode 100644 index 000000000..59aea7cbc --- /dev/null +++ b/stock_archive_constraint/models/stock_location.py @@ -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) + ) diff --git a/stock_archive_constraint/readme/CONTRIBUTORS.rst b/stock_archive_constraint/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..48848770a --- /dev/null +++ b/stock_archive_constraint/readme/CONTRIBUTORS.rst @@ -0,0 +1,4 @@ +* `Tecnativa `_: + + * Carlos Daudén + * Víctor Martínez diff --git a/stock_archive_constraint/readme/DESCRIPTION.rst b/stock_archive_constraint/readme/DESCRIPTION.rst new file mode 100644 index 000000000..ccdf56f17 --- /dev/null +++ b/stock_archive_constraint/readme/DESCRIPTION.rst @@ -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. diff --git a/stock_archive_constraint/readme/USAGE.rst b/stock_archive_constraint/readme/USAGE.rst new file mode 100644 index 000000000..240a02124 --- /dev/null +++ b/stock_archive_constraint/readme/USAGE.rst @@ -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 diff --git a/stock_archive_constraint/static/description/icon.png b/stock_archive_constraint/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/stock_archive_constraint/static/description/icon.png differ diff --git a/stock_archive_constraint/tests/__init__.py b/stock_archive_constraint/tests/__init__.py new file mode 100644 index 000000000..6712ad66e --- /dev/null +++ b/stock_archive_constraint/tests/__init__.py @@ -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 diff --git a/stock_archive_constraint/tests/test_location_archive_constraint.py b/stock_archive_constraint/tests/test_location_archive_constraint.py new file mode 100644 index 000000000..a1ffe4580 --- /dev/null +++ b/stock_archive_constraint/tests/test_location_archive_constraint.py @@ -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