diff --git a/stock_inventory_discrepancy/README.rst b/stock_inventory_discrepancy/README.rst new file mode 100644 index 000000000..d08a07a18 --- /dev/null +++ b/stock_inventory_discrepancy/README.rst @@ -0,0 +1,81 @@ +.. 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 + +=========================== +Stock Inventory Discrepancy +=========================== + +Adds the capability to show the discrepancy of every line in an inventory and +to block the inventory validation (setting it as 'Pending to Approve') when the +discrepancy is greater than a user defined threshold. + +Only new group "Validate All inventory Adjustments" will be able to force the +validation of those blocked inventories. By default Stock manager will belong +to this group. In addition, Stock Users can validate inventories under the +threshold now. + + +Configuration +============= + +You can configure the threshold as described below: + +#. Go to "Inventory > Warehouse Management" > Warehouses" or to "Inventory > + Warehouse Management" > Locations". +#. Modify the "Maximum Discrepancy Rate Threshold" either in a Warehouse or + in a location. If set to 0.0 the threshold is disabled. + +Usage +===== + +If you configured a "Maximum Discrepancy Rate Threshold". + +* When validating an Inventory Adjustment if some line exceed the Discrepancy + Threshold the system will set the inventory's state to 'Pending to Approve' + and show the quantity of lines that exceed the threshold. +* If both WH and location thresholds are configured, the location one has + preference. +* The user with "Validate All inventory Adjustments" rights can force the + validation of an inventory pending to approve. + + +.. 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/9.0 + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of +trouble, please check there if your issue has already been reported. If you +spotted it first, help us smash it by providing detailed and welcomed feedback. + + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Lois Rilo + + +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. diff --git a/stock_inventory_discrepancy/__init__.py b/stock_inventory_discrepancy/__init__.py new file mode 100644 index 000000000..e50f9b656 --- /dev/null +++ b/stock_inventory_discrepancy/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import models diff --git a/stock_inventory_discrepancy/__openerp__.py b/stock_inventory_discrepancy/__openerp__.py new file mode 100644 index 000000000..ef00174d8 --- /dev/null +++ b/stock_inventory_discrepancy/__openerp__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Stock Inventory Discrepancy", + "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": "9.0.1.0.0", + "author": "Eficent, " + "Odoo Community Association (OCA)", + "website": "https://github.com/OCA/stock-logistics-warehouse", + "category": "Warehouse Management", + "depends": ["stock"], + "data": [ + 'views/stock_inventory_view.xml', + 'views/stock_warehouse_view.xml', + 'views/stock_location_view.xml', + 'security/stock_inventory_discrepancy_security.xml' + ], + "license": "AGPL-3", + 'installable': True, + 'application': False, +} diff --git a/stock_inventory_discrepancy/models/__init__.py b/stock_inventory_discrepancy/models/__init__.py new file mode 100644 index 000000000..0a01a2d36 --- /dev/null +++ b/stock_inventory_discrepancy/models/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import stock_inventory +from . import stock_inventory_line +from . import stock_warehouse +from . import stock_location diff --git a/stock_inventory_discrepancy/models/stock_inventory.py b/stock_inventory_discrepancy/models/stock_inventory.py new file mode 100644 index 000000000..28af0aea9 --- /dev/null +++ b/stock_inventory_discrepancy/models/stock_inventory.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from openerp import _, api, fields, models +from openerp.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')] + + @api.one + @api.depends('line_ids.product_qty', 'line_ids.theoretical_qty') + def _compute_over_discrepancy_line_count(self): + lines = self.line_ids + self.over_discrepancy_line_count = sum( + d.discrepancy_percent > d.discrepancy_threshold + for d in lines) + + state = fields.Selection( + selection=INVENTORY_STATE_SELECTION, + string='Status', readonly=True, index=True, copy=False, + help="States of the Inventory Adjustment:\n" + "- Draft: Inventory not started.\n" + "- In Progress: Inventory in execution.\n" + "- Pending to Approve: Inventory have some discrepancies " + "greater than the predefined threshold and it's waiting for the " + "Control Manager approval.\n" + "- Validated: Inventory Approved.") + over_discrepancy_line_count = fields.Integer( + string='Number of Discrepancies Over Threshold', + compute=_compute_over_discrepancy_line_count, + store=True) + + @api.model + def action_over_discrepancies(self): + self.state = 'pending' + + def _check_group_inventory_validation_always(self): + grp_inv_val = self.env.ref( + 'stock_inventory_discrepancy.group_' + 'stock_inventory_validation_always') + if grp_inv_val in self.env.user.groups_id: + return True + else: + raise UserError( + _('The Qty Update is over the Discrepancy Threshold.\n ' + 'Please, contact a user with rights to perform ' + 'this action.') + ) + + @api.one + def action_done(self): + if self.over_discrepancy_line_count and self.line_ids.filtered( + lambda t: t.discrepancy_threshold > 0.0): + if self.env.context.get('normal_view', False): + self.action_over_discrepancies() + return True + else: + self._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 new file mode 100644 index 000000000..b13e99d43 --- /dev/null +++ b/stock_inventory_discrepancy/models/stock_inventory_line.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from openerp import api, fields, models + + +class StockInventoryLine(models.Model): + _inherit = 'stock.inventory.line' + + @api.one + def _compute_discrepancy(self): + self.discrepancy_qty = self.product_qty - self.theoretical_qty + if self.theoretical_qty: + self.discrepancy_percent = 100 * abs( + (self.product_qty - self.theoretical_qty) / + self.theoretical_qty) + elif not self.theoretical_qty and self.product_qty: + self.discrepancy_percent = 100.0 + + @api.one + def _get_discrepancy_threshold(self): + wh_id = self.location_id.get_warehouse(self.location_id) + wh = self.env['stock.warehouse'].browse(wh_id) + if self.location_id.discrepancy_threshold > 0.0: + self.discrepancy_threshold = self.location_id.discrepancy_threshold + elif wh.discrepancy_threshold > 0.0: + self.discrepancy_threshold = wh.discrepancy_threshold + else: + self.discrepancy_threshold = False + + discrepancy_qty = fields.Float( + string='Discrepancy', + compute=_compute_discrepancy, + help="The difference between the actual qty counted and the " + "theoretical quantity on hand.") + discrepancy_percent = fields.Float( + string='Discrepancy percent (%)', + compute=_compute_discrepancy, + digits=(3, 2), + help="The discrepancy expressed in percent with theoretical quantity " + "as basis") + discrepancy_threshold = fields.Float( + string='Threshold (%)', + digits=(3, 2), + help="Maximum Discrepancy Rate Threshold", + compute=_get_discrepancy_threshold) diff --git a/stock_inventory_discrepancy/models/stock_location.py b/stock_inventory_discrepancy/models/stock_location.py new file mode 100644 index 000000000..d040ecbd4 --- /dev/null +++ b/stock_inventory_discrepancy/models/stock_location.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from openerp import fields, models + + +class StockLocation(models.Model): + _inherit = 'stock.location' + + discrepancy_threshold = fields.Float( + string='Maximum Discrepancy Rate Threshold', + digits=(3, 2), + help="Maximum Discrepancy Rate allowed for any product when doing " + "an Inventory Adjustment. Thresholds defined in Locations have " + "preference over Warehouse's ones.") diff --git a/stock_inventory_discrepancy/models/stock_warehouse.py b/stock_inventory_discrepancy/models/stock_warehouse.py new file mode 100644 index 000000000..717c79cf3 --- /dev/null +++ b/stock_inventory_discrepancy/models/stock_warehouse.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from openerp import fields, models + + +class StockWarehouse(models.Model): + _inherit = 'stock.warehouse' + + discrepancy_threshold = fields.Float( + string='Maximum Discrepancy Rate Threshold', + digits=(3, 2), + help="Maximum Discrepancy Rate allowed for any product when doing " + "an Inventory Adjustment. Threshold defined in involved Location " + "has preference.") diff --git a/stock_inventory_discrepancy/security/stock_inventory_discrepancy_security.xml b/stock_inventory_discrepancy/security/stock_inventory_discrepancy_security.xml new file mode 100644 index 000000000..527dad6b3 --- /dev/null +++ b/stock_inventory_discrepancy/security/stock_inventory_discrepancy_security.xml @@ -0,0 +1,29 @@ + + + + + + + Validate Inventory Adjustments Under Threshold + + + + Validate All inventory Adjustments + + + + + + + + + + + + + + diff --git a/stock_inventory_discrepancy/tests/__init__.py b/stock_inventory_discrepancy/tests/__init__.py new file mode 100644 index 000000000..51849fb70 --- /dev/null +++ b/stock_inventory_discrepancy/tests/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import test_inventory_discrepancy diff --git a/stock_inventory_discrepancy/tests/test_inventory_discrepancy.py b/stock_inventory_discrepancy/tests/test_inventory_discrepancy.py new file mode 100644 index 000000000..4dd9c2cff --- /dev/null +++ b/stock_inventory_discrepancy/tests/test_inventory_discrepancy.py @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from openerp.tests.common import TransactionCase +from openerp.exceptions import UserError + + +class TestInventoryDiscrepancy(TransactionCase): + def setUp(self, *args, **kwargs): + super(TestInventoryDiscrepancy, self).setUp(*args, **kwargs) + self.obj_wh = self.env['stock.warehouse'] + self.obj_location = self.env['stock.location'] + 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', + }) + self.product2 = self.obj_product.create({ + 'name': 'Test Product 2', + 'type': 'product', + 'default_code': 'PROD2', + }) + self.test_loc = self.obj_location.create({ + 'name': 'Test Location', + 'usage': 'internal', + 'discrepancy_threshold': 0.1 + }) + self.test_wh = self.obj_warehouse.create({ + 'name': 'Test WH', + 'code': 'T', + 'discrepancy_threshold': 0.2 + }) + self.obj_location._parent_store_compute() + + # 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') + self.manager = self.env['res.users'].create({ + 'name': 'Test Manager', + 'login': 'manager', + 'email': 'test.manager@example.com', + 'groups_id': [(6, 0, [group_stock_man.id, group_inventory_all.id])] + }) + group_stock_user = self.env.ref('stock.group_stock_user') + self.user = self.env['res.users'].create({ + 'name': 'Test User', + 'login': 'user', + 'email': 'test.user@example.com', + 'groups_id': [(6, 0, [group_stock_user.id])] + }) + + starting_inv = self.obj_inventory.create({ + 'name': 'Starting inventory', + 'filter': 'product', + 'line_ids': [ + (0, 0, { + 'product_id': self.product1.id, + 'product_uom_id': self.env.ref( + "product.product_uom_unit").id, + 'product_qty': 2.0, + 'location_id': self.test_loc.id, + }), + (0, 0, { + 'product_id': self.product2.id, + 'product_uom_id': self.env.ref( + "product.product_uom_unit").id, + 'product_qty': 4.0, + 'location_id': self.test_loc.id, + }), + ], + }) + starting_inv.action_force_done() + + def test_compute_discrepancy(self): + """Tests if the discrepancy is correctly computed. + """ + inventory = self.obj_inventory.create({ + 'name': 'Test Discrepancy Computation', + 'location_id': self.test_loc.id, + 'filter': 'none', + 'line_ids': [ + (0, 0, { + 'product_id': self.product1.id, + 'product_uom_id': self.env.ref( + "product.product_uom_unit").id, + 'product_qty': 3.0, + 'location_id': self.test_loc.id, + }), + (0, 0, { + 'product_id': self.product2.id, + 'product_uom_id': self.env.ref( + "product.product_uom_unit").id, + 'product_qty': 3.0, + 'location_id': self.test_loc.id, + }) + ], + }) + self.assertEqual(inventory.line_ids[0].discrepancy_qty, 1.0, + 'Wrong Discrepancy qty computation.') + self.assertEqual(inventory.line_ids[1].discrepancy_qty, - 1.0, + 'Wrong Discrepancy qty computation.') + + def test_discrepancy_validation(self): + """Tests the new workflow""" + inventory = self.obj_inventory.create({ + 'name': 'Test Forcing Validation Method', + 'location_id': self.test_loc.id, + 'filter': 'none', + 'line_ids': [ + (0, 0, { + 'product_id': self.product1.id, + 'product_uom_id': self.env.ref( + "product.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_context({'normal_view': True}).action_done() + self.assertEqual(inventory.over_discrepancy_line_count, 1, + 'Computation of over-discrepancies failed.') + self.assertEqual(inventory.state, 'pending', + 'Inventory Adjustment not changing to Pending to ' + 'Approve.') + inventory.sudo(self.manager).action_force_done() + self.assertEqual(inventory.state, 'done', + 'Forcing the validation of the inventory adjustment ' + 'not working properly.') + + 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', + 'line_ids': [ + (0, 0, { + 'product_id': self.product1.id, + 'product_uom_id': self.env.ref( + "product.product_uom_unit").id, + 'product_qty': 3.0, + 'location_id': self.test_wh.lot_stock_id.id, + }), + ], + }) + self.assertEqual(inventory.line_ids.discrepancy_threshold, 0.2, + '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.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, + }) + with self.assertRaises(UserError): + upd_qty.sudo(self.user).change_product_qty() diff --git a/stock_inventory_discrepancy/views/stock_inventory_view.xml b/stock_inventory_discrepancy/views/stock_inventory_view.xml new file mode 100644 index 000000000..a5b845887 --- /dev/null +++ b/stock_inventory_discrepancy/views/stock_inventory_view.xml @@ -0,0 +1,49 @@ + + + + + + + Inventory form view - discrepancy extension + stock.inventory + + + + + draft,confirm,pending,done + + + {"pending":"red"} + + + + stock_inventory_discrepancy.group_stock_inventory_validation + {'normal_view': True} + + +