From c223ffefb576c3bc48e996fe17bbc382b99460e2 Mon Sep 17 00:00:00 2001 From: DavidJForgeFlow Date: Thu, 6 Oct 2022 15:28:51 +0200 Subject: [PATCH] [15.0][ADD] stock_inventory_adjustment --- .../odoo/addons/stock_inventory | 1 + setup/stock_inventory/setup.py | 6 + stock_inventory/README.rst | 0 stock_inventory/__init__.py | 1 + stock_inventory/__manifest__.py | 18 ++ stock_inventory/models/__init__.py | 3 + stock_inventory/models/stock_inventory.py | 126 +++++++++++ stock_inventory/models/stock_move_line.py | 7 + stock_inventory/models/stock_quant.py | 29 +++ stock_inventory/readme/CONTRIBUTORS.rst | 3 + stock_inventory/readme/DESCRIPTION.rst | 1 + stock_inventory/readme/USAGE.rst | 6 + stock_inventory/security/ir.model.access.csv | 3 + stock_inventory/tests/__init__.py | 1 + stock_inventory/tests/test_stock_inventory.py | 199 ++++++++++++++++++ stock_inventory/views/stock_inventory.xml | 134 ++++++++++++ stock_inventory/views/stock_move_line.xml | 29 +++ stock_inventory/views/stock_quant.xml | 16 ++ 18 files changed, 583 insertions(+) create mode 120000 setup/stock_inventory/odoo/addons/stock_inventory create mode 100644 setup/stock_inventory/setup.py create mode 100644 stock_inventory/README.rst create mode 100644 stock_inventory/__init__.py create mode 100644 stock_inventory/__manifest__.py create mode 100644 stock_inventory/models/__init__.py create mode 100644 stock_inventory/models/stock_inventory.py create mode 100644 stock_inventory/models/stock_move_line.py create mode 100644 stock_inventory/models/stock_quant.py create mode 100644 stock_inventory/readme/CONTRIBUTORS.rst create mode 100644 stock_inventory/readme/DESCRIPTION.rst create mode 100644 stock_inventory/readme/USAGE.rst create mode 100644 stock_inventory/security/ir.model.access.csv create mode 100644 stock_inventory/tests/__init__.py create mode 100644 stock_inventory/tests/test_stock_inventory.py create mode 100644 stock_inventory/views/stock_inventory.xml create mode 100644 stock_inventory/views/stock_move_line.xml create mode 100644 stock_inventory/views/stock_quant.xml diff --git a/setup/stock_inventory/odoo/addons/stock_inventory b/setup/stock_inventory/odoo/addons/stock_inventory new file mode 120000 index 000000000000..6a46dcae75fe --- /dev/null +++ b/setup/stock_inventory/odoo/addons/stock_inventory @@ -0,0 +1 @@ +../../../../stock_inventory \ No newline at end of file diff --git a/setup/stock_inventory/setup.py b/setup/stock_inventory/setup.py new file mode 100644 index 000000000000..28c57bb64031 --- /dev/null +++ b/setup/stock_inventory/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/stock_inventory/README.rst b/stock_inventory/README.rst new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/stock_inventory/__init__.py b/stock_inventory/__init__.py new file mode 100644 index 000000000000..0650744f6bc6 --- /dev/null +++ b/stock_inventory/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_inventory/__manifest__.py b/stock_inventory/__manifest__.py new file mode 100644 index 000000000000..18f7c836545c --- /dev/null +++ b/stock_inventory/__manifest__.py @@ -0,0 +1,18 @@ +{ + "name": "Stock Inventory Adjustment", + "version": "15.0.1.0.0", + "license": "LGPL-3", + "category": "Inventory/Inventory", + "summary": "Allows to do an easier follow up of the Inventory Adjustments", + "author": "ForgeFlow, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/stock-logistics-warehouse", + "depends": ["stock"], + "data": [ + "security/ir.model.access.csv", + "views/stock_inventory.xml", + "views/stock_quant.xml", + "views/stock_move_line.xml", + ], + "installable": True, + "application": False, +} diff --git a/stock_inventory/models/__init__.py b/stock_inventory/models/__init__.py new file mode 100644 index 000000000000..09732e334e94 --- /dev/null +++ b/stock_inventory/models/__init__.py @@ -0,0 +1,3 @@ +from . import stock_inventory +from . import stock_quant +from . import stock_move_line diff --git a/stock_inventory/models/stock_inventory.py b/stock_inventory/models/stock_inventory.py new file mode 100644 index 000000000000..cbd9c33c9be0 --- /dev/null +++ b/stock_inventory/models/stock_inventory.py @@ -0,0 +1,126 @@ +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class InventoryAdjustmentsGroup(models.Model): + _name = "stock.inventory" + _description = "Inventory Adjustment Group" + _order = "date desc, id desc" + + name = fields.Char( + required=True, default="Inventory ", string="Inventory Reference" + ) + + date = fields.Datetime(default=lambda self: fields.Datetime.now()) + + state = fields.Selection( + [("draft", "Draft"), ("in_progress", "In Progress"), ("done", "Done")], + default="draft", + ) + + location_ids = fields.Many2many( + "stock.location", string="Location", domain="[('usage', '=', 'internal')]" + ) + + product_selection = fields.Selection( + [("all", "All Products"), ("manual", "Manual Selection")], + default="all", + required=True, + ) + + product_ids = fields.Many2many("product.product", string="Products") + + stock_quant_ids = fields.Many2many("stock.quant", string="Inventory Adjustment") + + stock_move_ids = fields.One2many( + "stock.move.line", + "inventory_adjustment_id", + string="Inventory Adjustments Done", + ) + + count_stock_quants = fields.Integer( + compute="_compute_count_stock_quants", string="Adjustments" + ) + + count_stock_quants_string = fields.Char( + compute="_compute_count_stock_quants", string="Adjustments" + ) + + count_stock_moves = fields.Integer( + compute="_compute_count_stock_moves", string="Stock Moves Lines" + ) + + @api.depends("stock_quant_ids") + def _compute_count_stock_quants(self): + self.count_stock_quants = len(self.stock_quant_ids) + count_todo = len( + self.stock_quant_ids.search( + [("id", "in", self.stock_quant_ids.ids), ("to_do", "=", "True")] + ) + ) + self.count_stock_quants_string = "{} / {}".format( + count_todo, self.count_stock_quants + ) + + @api.depends("stock_move_ids") + def _compute_count_stock_moves(self): + sm_ids = self.mapped("stock_move_ids").ids + self.count_stock_moves = len(sm_ids) + + def action_state_to_in_progress(self): + active_rec = self.env["stock.inventory"].search([("state", "=", "in_progress")]) + if active_rec: + raise ValidationError( + _("There's already an Adjustment in Process: %s") % active_rec.name + ) + self.state = "in_progress" + if self.product_selection == "all": + for location in self._origin.location_ids: + self.stock_quant_ids = self.env["stock.quant"].search( + [ + "|", + ("location_id", "=", location.id), + ("location_id", "in", location.child_ids.ids), + ] + ) + else: + for location in self._origin.location_ids: + self.stock_quant_ids = self.env["stock.quant"].search( + [ + ("product_id", "in", self.product_ids.ids), + "|", + ("location_id", "=", location.id), + ("location_id", "in", location.child_ids.ids), + ] + ) + return + + def action_state_to_done(self): + self.state = "done" + for quant in self.stock_quant_ids: + quant.to_do = True + return + + def action_state_to_draft(self): + self.state = "draft" + for quant in self.stock_quant_ids: + quant.to_do = True + self.stock_quant_ids = None + return + + def action_view_inventory_adjustment(self): + result = self.env["stock.quant"].action_view_inventory() + ia_ids = self.mapped("stock_quant_ids").ids + result["domain"] = [("id", "in", ia_ids)] + result["search_view_id"] = self.env.ref("stock.quant_search_view").id + result["context"]["search_default_to_do"] = 1 + return result + + def action_view_stock_moves(self): + result = self.env["ir.actions.act_window"]._for_xml_id( + "stock_inventory.action_view_stock_move_line_inventory_tree" + ) + sm_ids = self.mapped("stock_move_ids").ids + result["domain"] = [("id", "in", sm_ids)] + result["context"] = [] + return result diff --git a/stock_inventory/models/stock_move_line.py b/stock_inventory/models/stock_move_line.py new file mode 100644 index 000000000000..9acebd9e526c --- /dev/null +++ b/stock_inventory/models/stock_move_line.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class StockMoveLine(models.Model): + _inherit = "stock.move.line" + + inventory_adjustment_id = fields.Many2one("stock.inventory") diff --git a/stock_inventory/models/stock_quant.py b/stock_inventory/models/stock_quant.py new file mode 100644 index 000000000000..2b7b0980a8d4 --- /dev/null +++ b/stock_inventory/models/stock_quant.py @@ -0,0 +1,29 @@ +from odoo import fields, models + + +class StockQuant(models.Model): + _inherit = "stock.quant" + + to_do = fields.Boolean(default=True) + + def _apply_inventory(self): + res = super()._apply_inventory() + record_moves = self.env["stock.move.line"] + adjustment = self.env["stock.inventory"].search([("state", "=", "in_progress")]) + for rec in self: + moves = record_moves.search( + [ + ("product_id", "=", rec.product_id.id), + ("lot_id", "=", rec.lot_id.id), + ("company_id", "=", rec.company_id.id), + "|", + ("location_id", "=", rec.location_id.id), + ("location_dest_id", "=", rec.location_id.id), + ] + ) + move = moves[len(moves) - 1] + adjustment.stock_move_ids += move + move.inventory_adjustment_id = adjustment + rec.to_do = False + + return res diff --git a/stock_inventory/readme/CONTRIBUTORS.rst b/stock_inventory/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000000..95cc88de027e --- /dev/null +++ b/stock_inventory/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `ForgeFlow `_: + + * David Jiménez diff --git a/stock_inventory/readme/DESCRIPTION.rst b/stock_inventory/readme/DESCRIPTION.rst new file mode 100644 index 000000000000..ca91fe8f2dd0 --- /dev/null +++ b/stock_inventory/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module allows to group Inventory Adjustments and have a group traceability (like before Odoo 15.0). diff --git a/stock_inventory/readme/USAGE.rst b/stock_inventory/readme/USAGE.rst new file mode 100644 index 000000000000..556dca26538b --- /dev/null +++ b/stock_inventory/readme/USAGE.rst @@ -0,0 +1,6 @@ +Go to Inventory / Operations / Inventory Adjustments. Here you can see the list of Adjustment Grouped. +If you create a new Group, you can choose 2 types of product selection: +- All Products (all products from theselected locations) +- Manual Selection (choose manually each product in location). +When you start the adjustment (only one at a time) clicking on adjustments gets you to the view where adjustments are made. +From the group view, if you click on Stock Moves you can see the movements done (includes the 0 qty moves). diff --git a/stock_inventory/security/ir.model.access.csv b/stock_inventory/security/ir.model.access.csv new file mode 100644 index 000000000000..0dd1ae475bd3 --- /dev/null +++ b/stock_inventory/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_stock_inventory_user,stock.inventory,model_stock_inventory,base.group_user,1,0,0,0 +access_stock_inventory_manager,stock.inventory,model_stock_inventory,base.group_system,1,1,1,1 diff --git a/stock_inventory/tests/__init__.py b/stock_inventory/tests/__init__.py new file mode 100644 index 000000000000..64feb514fcd7 --- /dev/null +++ b/stock_inventory/tests/__init__.py @@ -0,0 +1 @@ +from . import test_stock_inventory diff --git a/stock_inventory/tests/test_stock_inventory.py b/stock_inventory/tests/test_stock_inventory.py new file mode 100644 index 000000000000..44e2e43e7b2c --- /dev/null +++ b/stock_inventory/tests/test_stock_inventory.py @@ -0,0 +1,199 @@ +# Copyright 2022 ForgeFlow S.L +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase + + +class TestStockInventory(TransactionCase): + def setUp(self): + super(TestStockInventory, self).setUp() + self.quant_model = self.env["stock.quant"] + self.move_model = self.env["stock.move.line"] + self.inventory_model = self.env["stock.inventory"] + self.location_model = self.env["stock.location"] + self.product = self.env["product.product"].create( + { + "name": "Product 1 test", + "type": "product", + "tracking": "lot", + } + ) + self.product2 = self.env["product.product"].create( + { + "name": "Product 1 test", + "type": "product", + } + ) + self.lot_1 = self.env["stock.production.lot"].create( + { + "product_id": self.product.id, + "name": "Lot 1", + "company_id": self.env.company.id, + } + ) + self.lot_2 = self.env["stock.production.lot"].create( + { + "product_id": self.product.id, + "name": "Lot 2", + "company_id": self.env.company.id, + } + ) + self.lot_3 = self.env["stock.production.lot"].create( + { + "product_id": self.product.id, + "name": "Lot 3", + "company_id": self.env.company.id, + } + ) + self.location_src = self.env.ref("stock.stock_location_locations_virtual") + self.location_dst = self.env.ref("stock.stock_location_customers") + + self.location1 = self.location_model.create( + { + "name": "Location 1", + "usage": "internal", + "warehouse_id": self.location_src.id, + } + ) + self.location2 = self.location_model.create( + { + "name": "Location 2", + "usage": "internal", + "location_id": self.location_src.id, + } + ) + self.location3 = self.location_model.create( + { + "name": "Location 3", + "usage": "internal", + "location_id": self.location1.id, + } + ) + self.quant1 = self.quant_model.sudo().create( + { + "product_id": self.product.id, + "lot_id": self.lot_1.id, + "quantity": 100.0, + "location_id": self.location1.id, + } + ) + self.quant2 = self.quant_model.sudo().create( + { + "product_id": self.product.id, + "lot_id": self.lot_2.id, + "quantity": 100.0, + "location_id": self.location2.id, + } + ) + self.quant3 = self.quant_model.sudo().create( + { + "product_id": self.product.id, + "lot_id": self.lot_3.id, + "quantity": 100.0, + "location_id": self.location3.id, + } + ) + self.quant4 = self.quant_model.sudo().create( + { + "product_id": self.product2.id, + "quantity": 100.0, + "location_id": self.location3.id, + } + ) + + def test_01_all_locations(self): + x = self.inventory_model.search([("state", "=", "in_progress")]) + if x: + x.action_state_to_done() + inventory1 = self.inventory_model.create( + { + "name": "Inventory_Test_1", + "product_selection": "all", + "location_ids": [self.location1.id], + } + ) + inventory1.action_state_to_in_progress() + inventory2 = self.inventory_model.create( + { + "name": "Inventory_Test_2", + "product_selection": "all", + "location_ids": [self.location1.id], + } + ) + with self.assertRaises(ValidationError), self.cr.savepoint(): + inventory2.action_state_to_in_progress() + self.assertEqual( + inventory1.stock_quant_ids.ids, + [self.quant1.id, self.quant3.id, self.quant4.id], + ) + inventory1.action_state_to_draft() + self.assertEqual(inventory1.stock_quant_ids.ids, []) + inventory1.action_state_to_in_progress() + self.assertEqual(inventory1.count_stock_moves, 0) + self.assertEqual(inventory1.count_stock_quants, 3) + self.assertEqual(inventory1.count_stock_quants_string, "3 / 3") + inventory1.action_view_inventory_adjustment() + self.quant1.inventory_quantity = 92 + self.quant1.action_apply_inventory() + inventory1._compute_count_stock_quants() + inventory1.action_view_stock_moves() + self.assertEqual(inventory1.count_stock_moves, 1) + self.assertEqual(inventory1.count_stock_quants, 3) + self.assertEqual(inventory1.count_stock_quants_string, "2 / 3") + self.assertEqual(inventory1.stock_move_ids.qty_done, 8) + self.assertEqual(inventory1.stock_move_ids.product_id.id, self.product.id) + self.assertEqual(inventory1.stock_move_ids.lot_id.id, self.lot_1.id) + self.assertEqual(inventory1.stock_move_ids.location_id.id, self.location1.id) + inventory1.action_state_to_done() + + def test_02_manual_selection(self): + x = self.inventory_model.search([("state", "=", "in_progress")]) + if x: + x.action_state_to_done() + inventory1 = self.inventory_model.create( + { + "name": "Inventory_Test_1", + "product_selection": "manual", + "location_ids": [self.location1.id], + "product_ids": [self.product.id], + } + ) + inventory1.action_state_to_in_progress() + inventory2 = self.inventory_model.create( + { + "name": "Inventory_Test_2", + "product_selection": "all", + "location_ids": [self.location1.id], + } + ) + with self.assertRaises(ValidationError), self.cr.savepoint(): + inventory2.action_state_to_in_progress() + self.assertEqual( + inventory1.stock_quant_ids.ids, [self.quant1.id, self.quant3.id] + ) + inventory1.action_state_to_draft() + self.assertEqual(inventory1.stock_quant_ids.ids, []) + inventory1.action_state_to_in_progress() + self.assertEqual(inventory1.count_stock_moves, 0) + self.assertEqual(inventory1.count_stock_quants, 2) + self.assertEqual(inventory1.count_stock_quants_string, "2 / 2") + inventory1.action_view_inventory_adjustment() + self.quant3.inventory_quantity = 74 + self.quant3.action_apply_inventory() + inventory1._compute_count_stock_quants() + inventory1.action_view_stock_moves() + self.assertEqual(inventory1.count_stock_moves, 1) + self.assertEqual(inventory1.count_stock_quants, 2) + self.assertEqual(inventory1.count_stock_quants_string, "1 / 2") + self.assertEqual(inventory1.stock_move_ids.qty_done, 26) + self.assertEqual(inventory1.stock_move_ids.product_id.id, self.product.id) + self.assertEqual(inventory1.stock_move_ids.lot_id.id, self.lot_3.id) + self.assertEqual(inventory1.stock_move_ids.location_id.id, self.location3.id) + self.quant1.inventory_quantity = 65 + self.quant1.action_apply_inventory() + inventory1._compute_count_stock_quants() + self.assertEqual(inventory1.count_stock_moves, 2) + self.assertEqual(inventory1.count_stock_quants, 2) + self.assertEqual(inventory1.count_stock_quants_string, "0 / 2") + inventory1.action_state_to_done() diff --git a/stock_inventory/views/stock_inventory.xml b/stock_inventory/views/stock_inventory.xml new file mode 100644 index 000000000000..20f469b0c0a1 --- /dev/null +++ b/stock_inventory/views/stock_inventory.xml @@ -0,0 +1,134 @@ + + + + stock.inventory.form.view + stock.inventory + 1000 + +
+
+
+ + +
+ + +
+
+
+ + + + + + + + + + +
+
+
+
+ + + stock.inventory.tree.view + stock.inventory + 1000 + + + + + + + + + + + Inventory Adjustment Group + stock.inventory + tree,form + + + + + +
diff --git a/stock_inventory/views/stock_move_line.xml b/stock_inventory/views/stock_move_line.xml new file mode 100644 index 000000000000..10961216f5e0 --- /dev/null +++ b/stock_inventory/views/stock_move_line.xml @@ -0,0 +1,29 @@ + + + stock.move.line.tree.view.inventory + stock.move.line + 1000 + + + + + + + + + + + + + + + + Stock Move Lines + stock.move.line + tree,form + + + diff --git a/stock_inventory/views/stock_quant.xml b/stock_inventory/views/stock_quant.xml new file mode 100644 index 000000000000..efa28e8875b0 --- /dev/null +++ b/stock_inventory/views/stock_quant.xml @@ -0,0 +1,16 @@ + + + stock.quant.search.not.done + stock.quant + + + + + + + +