diff --git a/stock_inventory_discrepancy/__init__.py b/stock_inventory_discrepancy/__init__.py index 4b76c7b2d..e3bed0638 100644 --- a/stock_inventory_discrepancy/__init__.py +++ b/stock_inventory_discrepancy/__init__.py @@ -1,3 +1,4 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from . import models +from .hooks import post_load_hook diff --git a/stock_inventory_discrepancy/__manifest__.py b/stock_inventory_discrepancy/__manifest__.py index 619e4cf29..951c3df47 100644 --- a/stock_inventory_discrepancy/__manifest__.py +++ b/stock_inventory_discrepancy/__manifest__.py @@ -5,18 +5,20 @@ "summary": "Adds the capability to show the discrepancy of every line in " "an inventory and to block the inventory validation when the " "discrepancy is over a user defined threshold.", - "version": "12.0.1.0.0", + "version": "13.0.1.0.0", "author": "ForgeFlow, Odoo Community Association (OCA)", "website": "https://github.com/OCA/stock-logistics-warehouse", - "category": "Warehouse Management", + "category": "Warehouse", "depends": ["stock"], "data": [ "security/stock_inventory_discrepancy_security.xml", + "views/assets_backend.xml", "views/stock_inventory_view.xml", "views/stock_warehouse_view.xml", "views/stock_location_view.xml", ], "license": "AGPL-3", + "post_load": "post_load_hook", "installable": True, "application": False, } diff --git a/stock_inventory_discrepancy/hooks.py b/stock_inventory_discrepancy/hooks.py new file mode 100644 index 000000000..3a4356c77 --- /dev/null +++ b/stock_inventory_discrepancy/hooks.py @@ -0,0 +1,79 @@ +# Copyright 2019 Eficent Business and IT Consulting Services S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + + +from odoo import _ +from odoo.exceptions import UserError +from odoo.tools.float_utils import float_compare + +from odoo.addons.stock.models.stock_inventory import Inventory + + +def post_load_hook(): + def action_validate_discrepancy(self): + """Override method to avoid inline group validation""" + if not self.exists(): + return + self.ensure_one() + # START HOOK: - Allow specific group to validate inventory + # - Allow validate on pending status + if ( + not self.user_has_groups("stock.group_stock_manager") + and not self.user_has_groups( + "stock_inventory_discrepancy.group_stock_inventory_validation" + ) + and not self.user_has_groups( + "stock_inventory_discrepancy.group_stock_inventory_validation_always" + ) + ): + raise UserError( + _("Only a stock manager can validate an inventory adjustment.") + ) + if self.state not in ["confirm", "pending"]: + raise UserError( + _( + "You can't validate the inventory '%s', maybe this inventory " + + "has been already validated or isn't ready." + ) + % (self.name) + ) + # END HOOK + inventory_lines = self.line_ids.filtered( + lambda l: l.product_id.tracking in ["lot", "serial"] + and not l.prod_lot_id + and l.theoretical_qty != l.product_qty + ) + lines = self.line_ids.filtered( + lambda l: float_compare( + l.product_qty, 1, precision_rounding=l.product_uom_id.rounding + ) + > 0 + and l.product_id.tracking == "serial" + and l.prod_lot_id + ) + if inventory_lines and not lines: + wiz_lines = [ + (0, 0, {"product_id": product.id, "tracking": product.tracking}) + for product in inventory_lines.mapped("product_id") + ] + wiz = self.env["stock.track.confirmation"].create( + {"inventory_id": self.id, "tracking_line_ids": wiz_lines} + ) + return { + "name": _("Tracked Products in Inventory Adjustment"), + "type": "ir.actions.act_window", + "view_mode": "form", + "views": [(False, "form")], + "res_model": "stock.track.confirmation", + "target": "new", + "res_id": wiz.id, + } + self._action_done() + self.line_ids._check_company() + self._check_company() + return True + + if not hasattr(Inventory, "action_validate_original"): + Inventory.action_validate_original = Inventory.action_validate + + Inventory._patch_method("action_validate", action_validate_discrepancy) diff --git a/stock_inventory_discrepancy/models/stock_inventory.py b/stock_inventory_discrepancy/models/stock_inventory.py index d2c39234f..fe44f56f7 100644 --- a/stock_inventory_discrepancy/models/stock_inventory.py +++ b/stock_inventory_discrepancy/models/stock_inventory.py @@ -1,5 +1,5 @@ # Copyright 2017-2020 ForgeFlow S.L. -# (http://www.eficent.com) +# (http://www.forgeflow.com) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import _, api, fields, models @@ -9,16 +9,8 @@ from odoo.exceptions import UserError class StockInventory(models.Model): _inherit = "stock.inventory" - INVENTORY_STATE_SELECTION = [ - ("draft", "Draft"), - ("cancel", "Cancelled"), - ("confirm", "In Progress"), - ("pending", "Pending to Approve"), - ("done", "Validated"), - ] - state = fields.Selection( - selection=INVENTORY_STATE_SELECTION, + selection_add=[("pending", "Pending to Approve"), ("done",)], string="Status", readonly=True, index=True, @@ -37,7 +29,6 @@ class StockInventory(models.Model): store=True, ) - @api.multi @api.depends("line_ids.product_qty", "line_ids.theoretical_qty") def _compute_over_discrepancy_line_count(self): for inventory in self: @@ -46,13 +37,12 @@ class StockInventory(models.Model): ) inventory.over_discrepancy_line_count = len(lines) - @api.multi def action_over_discrepancies(self): self.write({"state": "pending"}) def _check_group_inventory_validation_always(self): grp_inv_val = self.env.ref( - "stock_inventory_discrepancy.group_" "stock_inventory_validation_always" + "stock_inventory_discrepancy.group_stock_inventory_validation_always" ) if grp_inv_val in self.env.user.groups_id: return True @@ -70,13 +60,17 @@ class StockInventory(models.Model): if inventory.over_discrepancy_line_count and inventory.line_ids.filtered( lambda t: t.discrepancy_threshold > 0.0 ): - if inventory.env.context.get("normal_view", False): + if self.user_has_groups( + "stock_inventory_discrepancy.group_stock_inventory_validation" + ) and not self.user_has_groups( + "stock_inventory_discrepancy." + "group_stock_inventory_validation_always" + ): inventory.action_over_discrepancies() return True else: inventory._check_group_inventory_validation_always() return super(StockInventory, self)._action_done() - @api.multi def action_force_done(self): return super(StockInventory, self)._action_done() diff --git a/stock_inventory_discrepancy/models/stock_inventory_line.py b/stock_inventory_discrepancy/models/stock_inventory_line.py index 456200849..a1a0fceda 100644 --- a/stock_inventory_discrepancy/models/stock_inventory_line.py +++ b/stock_inventory_discrepancy/models/stock_inventory_line.py @@ -1,11 +1,9 @@ # Copyright 2017-2020 ForgeFlow S.L. -# (http://www.eficent.com) +# (http://www.forgeflow.com) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import api, fields, models -from odoo.addons import decimal_precision as dp - class StockInventoryLine(models.Model): _inherit = "stock.inventory.line" @@ -15,7 +13,7 @@ class StockInventoryLine(models.Model): compute="_compute_discrepancy", help="The difference between the actual qty counted and the " "theoretical quantity on hand.", - digits=dp.get_precision("Product Unit of Measure"), + digits="Product Unit of Measure", default=0, ) discrepancy_percent = fields.Float( @@ -32,7 +30,6 @@ class StockInventoryLine(models.Model): compute="_compute_discrepancy_threshold", ) - @api.multi @api.depends("theoretical_qty", "product_qty") def _compute_discrepancy(self): for line in self: @@ -43,8 +40,9 @@ class StockInventoryLine(models.Model): ) elif not line.theoretical_qty and line.product_qty: line.discrepancy_percent = 100.0 + else: + line.discrepancy_percent = 0.0 - @api.multi def _compute_discrepancy_threshold(self): for line in self: whs = line.location_id.get_warehouse() diff --git a/stock_inventory_discrepancy/models/stock_location.py b/stock_inventory_discrepancy/models/stock_location.py index 9da29738d..96f927673 100644 --- a/stock_inventory_discrepancy/models/stock_location.py +++ b/stock_inventory_discrepancy/models/stock_location.py @@ -1,5 +1,5 @@ # Copyright 2017-2020 ForgeFlow S.L. -# (http://www.eficent.com) +# (http://www.forgeflow.com) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import fields, models @@ -15,3 +15,22 @@ class StockLocation(models.Model): "an Inventory Adjustment. Thresholds defined in Locations have " "preference over Warehouse's ones.", ) + propagate_discrepancy_threshold = fields.Boolean( + string="Propagate discrepancy threshold", + help="Propagate Maximum Discrepancy Rate Threshold to child locations", + ) + + def write(self, values): + res = super().write(values) + # Set the discrepancy threshold for all child locations + if values.get("discrepancy_threshold", False): + for location in self.filtered( + lambda loc: loc.propagate_discrepancy_threshold and loc.child_ids + ): + location.child_ids.write( + { + "discrepancy_threshold": values["discrepancy_threshold"], + "propagate_discrepancy_threshold": True, + } + ) + return res diff --git a/stock_inventory_discrepancy/models/stock_warehouse.py b/stock_inventory_discrepancy/models/stock_warehouse.py index 7284603e1..6f1f4ec31 100644 --- a/stock_inventory_discrepancy/models/stock_warehouse.py +++ b/stock_inventory_discrepancy/models/stock_warehouse.py @@ -1,5 +1,5 @@ # Copyright 2017-2020 ForgeFlow S.L. -# (http://www.eficent.com) +# (http://www.forgeflow.com) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import fields, models diff --git a/stock_inventory_discrepancy/readme/CONTRIBUTORS.rst b/stock_inventory_discrepancy/readme/CONTRIBUTORS.rst index 43adb24a7..0fea89874 100644 --- a/stock_inventory_discrepancy/readme/CONTRIBUTORS.rst +++ b/stock_inventory_discrepancy/readme/CONTRIBUTORS.rst @@ -1,3 +1,4 @@ * Lois Rilo * Andreas Dian Sukarno Putro * Bhavesh Odedra +* Héctor Villarreal diff --git a/stock_inventory_discrepancy/static/src/js/inventory_validate_button_controller.js b/stock_inventory_discrepancy/static/src/js/inventory_validate_button_controller.js new file mode 100644 index 000000000..d47f31035 --- /dev/null +++ b/stock_inventory_discrepancy/static/src/js/inventory_validate_button_controller.js @@ -0,0 +1,39 @@ +odoo.define('stock_inventory_discrepancy.InventoryValidationController', function (require) { + "use strict"; + + var core = require('web.core'); + var InventoryValidationController = require('stock.InventoryValidationController'); + + var _t = core._t; + + InventoryValidationController.include({ + + /** + * @override + * @see displayNotification + */ + do_notify: function (title, message, sticky, className) { + var self = this; + if (this.modelName === "stock.inventory.line") { + this._rpc({ + model: 'stock.inventory', + method: 'read', + args: [this.inventory_id, ['state']], + }).then(function (res) { + if (res[0].state === "pending") { + title = _t("Pending to Approve"); + message = _t("The inventory needs to be approved"); + } + }).finally(function () { + return self.displayNotification({ + type: 'warning', + title: title, + message: message, + sticky: sticky, + className: className, + }); + }); + } + }, + }); +}); diff --git a/stock_inventory_discrepancy/tests/test_inventory_discrepancy.py b/stock_inventory_discrepancy/tests/test_inventory_discrepancy.py index c48da0460..07b894086 100644 --- a/stock_inventory_discrepancy/tests/test_inventory_discrepancy.py +++ b/stock_inventory_discrepancy/tests/test_inventory_discrepancy.py @@ -1,5 +1,5 @@ # Copyright 2017-2020 ForgeFlow S.L. -# (http://www.eficent.com) +# (http://www.forgeflow.com) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo.exceptions import UserError @@ -14,7 +14,6 @@ class TestInventoryDiscrepancy(TransactionCase): self.obj_inventory = self.env["stock.inventory"] self.obj_product = self.env["product.product"] self.obj_warehouse = self.env["stock.warehouse"] - self.obj_upd_qty_wizard = self.env["stock.change.product.qty"] self.product1 = self.obj_product.create( {"name": "Test Product 1", "type": "product", "default_code": "PROD1"} @@ -33,8 +32,10 @@ class TestInventoryDiscrepancy(TransactionCase): # Create Stock manager able to force validation on inventories. group_stock_man = self.env.ref("stock.group_stock_manager") group_inventory_all = self.env.ref( - "stock_inventory_discrepancy." "group_stock_inventory_validation_always" + "stock_inventory_discrepancy.group_stock_inventory_validation_always" ) + group_employee = self.env.ref("base.group_user") + self.manager = self.env["res.users"].create( { "name": "Test Manager", @@ -53,10 +54,27 @@ class TestInventoryDiscrepancy(TransactionCase): } ) + self.user_2 = self.env["res.users"].create( + { + "name": "Test User 2", + "login": "user_2", + "email": "test2.user@example.com", + "groups_id": [(6, 0, [group_stock_user.id, group_inventory_all.id])], + } + ) + + self.no_user = self.env["res.users"].create( + { + "name": "No User", + "login": "no_user", + "email": "test.no_user@example.com", + "groups_id": [(6, 0, [group_employee.id])], + } + ) + starting_inv = self.obj_inventory.create( { "name": "Starting inventory", - "filter": "product", "line_ids": [ ( 0, @@ -89,8 +107,7 @@ class TestInventoryDiscrepancy(TransactionCase): inventory = self.obj_inventory.create( { "name": "Test Discrepancy Computation", - "location_id": self.test_loc.id, - "filter": "none", + "location_ids": [(4, self.test_loc.id)], "line_ids": [ ( 0, @@ -131,8 +148,7 @@ class TestInventoryDiscrepancy(TransactionCase): inventory = self.obj_inventory.create( { "name": "Test Forcing Validation Method", - "location_id": self.test_loc.id, - "filter": "none", + "location_ids": [(4, self.test_loc.id)], "line_ids": [ ( 0, @@ -155,7 +171,8 @@ class TestInventoryDiscrepancy(TransactionCase): 0.1, "Threshold wrongly computed in Inventory Line.", ) - inventory.with_context({"normal_view": True}).action_validate() + inventory.with_user(self.user).action_start() + inventory.with_user(self.user).action_validate() self.assertEqual( inventory.over_discrepancy_line_count, 1, @@ -166,7 +183,7 @@ class TestInventoryDiscrepancy(TransactionCase): "pending", "Inventory Adjustment not changing to Pending to " "Approve.", ) - inventory.sudo(self.manager).action_force_done() + inventory.with_user(self.manager).action_force_done() self.assertEqual( inventory.state, "done", @@ -174,13 +191,56 @@ class TestInventoryDiscrepancy(TransactionCase): "not working properly.", ) + def test_discrepancy_validation_always(self): + """Tests the new workflow""" + inventory = self.obj_inventory.create( + { + "name": "Test Forcing Validation Method", + "location_ids": [(4, self.test_loc.id)], + "line_ids": [ + ( + 0, + 0, + { + "product_id": self.product1.id, + "product_uom_id": self.env.ref("uom.product_uom_unit").id, + "product_qty": 3.0, + "location_id": self.test_loc.id, + }, + ) + ], + } + ) + self.assertEqual( + inventory.state, "draft", "Testing Inventory wrongly configurated" + ) + self.assertEqual( + inventory.line_ids.discrepancy_threshold, + 0.1, + "Threshold wrongly computed in Inventory Line.", + ) + inventory.with_user(self.user_2).action_start() + # User with no privileges can't validate a Inventory Adjustment. + with self.assertRaises(UserError): + inventory.with_user(self.no_user).action_validate() + inventory.with_user(self.user_2).action_validate() + self.assertEqual( + inventory.over_discrepancy_line_count, + 1, + "Computation of over-discrepancies failed.", + ) + self.assertEqual( + inventory.state, + "done", + "Stock Managers belongs to group Validate All inventory Adjustments", + ) + def test_warehouse_threshold(self): """Tests the behaviour if the threshold is set on the WH.""" inventory = self.obj_inventory.create( { "name": "Test Threshold Defined in WH", - "location_id": self.test_wh.view_location_id.id, - "filter": "none", + "location_ids": [(4, self.test_wh.view_location_id.id)], "line_ids": [ ( 0, @@ -201,17 +261,23 @@ class TestInventoryDiscrepancy(TransactionCase): "Threshold wrongly computed in Inventory Line.", ) - def test_update_qty_user_error(self): - """Test if a user error raises when a stock user tries to update the - qty for a product and the correction is a discrepancy over the - threshold.""" - upd_qty = self.obj_upd_qty_wizard.sudo(self.user).create( + def test_propagate_discrepancy_threshold(self): + view_test_loc = self.obj_location.create( + {"name": "Test Location", "usage": "view", "discrepancy_threshold": 0.1} + ) + child_test_loc = self.obj_location.create( { - "product_id": self.product1.id, - "product_tmpl_id": self.product1.product_tmpl_id.id, - "new_quantity": 10.0, - "location_id": self.test_loc.id, + "name": "Child Test Location", + "usage": "internal", + "discrepancy_threshold": 0.2, + "location_id": view_test_loc.id, } ) - with self.assertRaises(UserError): - upd_qty.change_product_qty() + view_test_loc.write( + {"discrepancy_threshold": 0.3, "propagate_discrepancy_threshold": True} + ) + self.assertEqual( + child_test_loc.discrepancy_threshold, + 0.3, + "Threshold Discrepancy wrongly propagated", + ) diff --git a/stock_inventory_discrepancy/views/assets_backend.xml b/stock_inventory_discrepancy/views/assets_backend.xml new file mode 100644 index 000000000..4ec4d1381 --- /dev/null +++ b/stock_inventory_discrepancy/views/assets_backend.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/stock_inventory_discrepancy/views/stock_inventory_view.xml b/stock_inventory_discrepancy/views/stock_inventory_view.xml index 7f688ac69..4007b2852 100644 --- a/stock_inventory_discrepancy/views/stock_inventory_view.xml +++ b/stock_inventory_discrepancy/views/stock_inventory_view.xml @@ -20,7 +20,6 @@ stock_inventory_discrepancy.group_stock_inventory_validation - {'normal_view': True} @@ -30,19 +29,31 @@ groups="stock_inventory_discrepancy.group_stock_inventory_validation_always" attrs="{'invisible': ['|',('state', '!=', 'pending'),('over_discrepancy_line_count', '=', 0)]}"/> - + + pending,confirm + + + pending,confirm + + - - - - - - - theoretical_qty < 0 or discrepancy_percent > discrepancy_threshold - + + stock.inventory.line.tree2 + stock.inventory.line + + + + + + + + + theoretical_qty < 0 or discrepancy_percent > discrepancy_threshold or "product_qty != theoretical_qty" + + + diff --git a/stock_inventory_discrepancy/views/stock_location_view.xml b/stock_inventory_discrepancy/views/stock_location_view.xml index f04ace36e..e745df75e 100644 --- a/stock_inventory_discrepancy/views/stock_location_view.xml +++ b/stock_inventory_discrepancy/views/stock_location_view.xml @@ -9,11 +9,12 @@ stock.location - + diff --git a/stock_inventory_discrepancy/views/stock_warehouse_view.xml b/stock_inventory_discrepancy/views/stock_warehouse_view.xml index bc617867a..c939180c3 100644 --- a/stock_inventory_discrepancy/views/stock_warehouse_view.xml +++ b/stock_inventory_discrepancy/views/stock_warehouse_view.xml @@ -4,7 +4,7 @@ - + Warehouse form - Inventory Discrepancy extension stock.warehouse