[IMP] stock_inventory_discrepancy: black, isort

This commit is contained in:
hveficent
2020-01-07 09:34:06 +01:00
parent bc8f725162
commit bf4dfcdab6
11 changed files with 269 additions and 213 deletions

View File

@@ -1,24 +1,22 @@
# Copyright 2017 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)
# Copyright 2017-2020 ForgeFlow S.L. (http://www.forgeflow.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.",
"an inventory and to block the inventory validation when the "
"discrepancy is over a user defined threshold.",
"version": "12.0.1.0.0",
"author": "Eficent, "
"Odoo Community Association (OCA)",
"author": "ForgeFlow, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/stock-logistics-warehouse",
"category": "Warehouse Management",
"depends": ["stock"],
"data": [
'security/stock_inventory_discrepancy_security.xml',
'views/stock_inventory_view.xml',
'views/stock_warehouse_view.xml',
'views/stock_location_view.xml',
"security/stock_inventory_discrepancy_security.xml",
"views/stock_inventory_view.xml",
"views/stock_warehouse_view.xml",
"views/stock_location_view.xml",
],
"license": "AGPL-3",
'installable': True,
'application': False,
"installable": True,
"application": False,
}

View File

@@ -1,69 +1,76 @@
# Copyright 2017 Eficent Business and IT Consulting Services S.L.
# Copyright 2017-2020 ForgeFlow S.L.
# (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import api, fields, models, _
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class StockInventory(models.Model):
_inherit = 'stock.inventory'
_inherit = "stock.inventory"
INVENTORY_STATE_SELECTION = [
('draft', 'Draft'),
('cancel', 'Cancelled'),
('confirm', 'In Progress'),
('pending', 'Pending to Approve'),
('done', 'Validated')]
("draft", "Draft"),
("cancel", "Cancelled"),
("confirm", "In Progress"),
("pending", "Pending to Approve"),
("done", "Validated"),
]
state = fields.Selection(
selection=INVENTORY_STATE_SELECTION,
string='Status', readonly=True, index=True, copy=False,
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.")
"- 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)
string="Number of Discrepancies Over Threshold",
compute="_compute_over_discrepancy_line_count",
store=True,
)
@api.multi
@api.depends('line_ids.product_qty', 'line_ids.theoretical_qty')
@api.depends("line_ids.product_qty", "line_ids.theoretical_qty")
def _compute_over_discrepancy_line_count(self):
for inventory in self:
lines = inventory.line_ids.filtered(
lambda line: line.discrepancy_percent > line.
discrepancy_threshold
lambda line: line.discrepancy_percent > line.discrepancy_threshold
)
inventory.over_discrepancy_line_count = len(lines)
@api.multi
def action_over_discrepancies(self):
self.write({'state': 'pending'})
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
else:
raise UserError(
_('The Qty Update is over the Discrepancy Threshold.\n '
'Please, contact a user with rights to perform '
'this action.')
_(
"The Qty Update is over the Discrepancy Threshold.\n "
"Please, contact a user with rights to perform "
"this action."
)
)
def _action_done(self):
for inventory in self:
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 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):
inventory.action_over_discrepancies()
return True
else:

View File

@@ -1,41 +1,46 @@
# Copyright 2017 Eficent Business and IT Consulting Services S.L.
# Copyright 2017-2020 ForgeFlow S.L.
# (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo.addons import decimal_precision as dp
from odoo import api, fields, models
from odoo.addons import decimal_precision as dp
class StockInventoryLine(models.Model):
_inherit = 'stock.inventory.line'
_inherit = "stock.inventory.line"
discrepancy_qty = fields.Float(
string='Discrepancy',
compute='_compute_discrepancy',
string="Discrepancy",
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'), default=0)
"theoretical quantity on hand.",
digits=dp.get_precision("Product Unit of Measure"),
default=0,
)
discrepancy_percent = fields.Float(
string='Discrepancy percent (%)',
compute='_compute_discrepancy',
string="Discrepancy percent (%)",
compute="_compute_discrepancy",
digits=(3, 2),
help="The discrepancy expressed in percent with theoretical quantity "
"as basis")
"as basis",
)
discrepancy_threshold = fields.Float(
string='Threshold (%)',
string="Threshold (%)",
digits=(3, 2),
help="Maximum Discrepancy Rate Threshold",
compute='_compute_discrepancy_threshold')
compute="_compute_discrepancy_threshold",
)
@api.multi
@api.depends('theoretical_qty', 'product_qty')
@api.depends("theoretical_qty", "product_qty")
def _compute_discrepancy(self):
for line in self:
line.discrepancy_qty = line.product_qty - line.theoretical_qty
if line.theoretical_qty:
line.discrepancy_percent = 100 * abs(
(line.product_qty - line.theoretical_qty) /
line.theoretical_qty)
(line.product_qty - line.theoretical_qty) / line.theoretical_qty
)
elif not line.theoretical_qty and line.product_qty:
line.discrepancy_percent = 100.0
@@ -44,8 +49,7 @@ class StockInventoryLine(models.Model):
for line in self:
whs = line.location_id.get_warehouse()
if line.location_id.discrepancy_threshold > 0.0:
line.discrepancy_threshold = line.location_id.\
discrepancy_threshold
line.discrepancy_threshold = line.location_id.discrepancy_threshold
elif whs.discrepancy_threshold > 0.0:
line.discrepancy_threshold = whs.discrepancy_threshold
else:

View File

@@ -1,4 +1,4 @@
# Copyright 2017 Eficent Business and IT Consulting Services S.L.
# Copyright 2017-2020 ForgeFlow S.L.
# (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
@@ -6,11 +6,12 @@ from odoo import fields, models
class StockLocation(models.Model):
_inherit = 'stock.location'
_inherit = "stock.location"
discrepancy_threshold = fields.Float(
string='Maximum Discrepancy Rate Threshold',
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.")
"an Inventory Adjustment. Thresholds defined in Locations have "
"preference over Warehouse's ones.",
)

View File

@@ -1,4 +1,4 @@
# Copyright 2017 Eficent Business and IT Consulting Services S.L.
# Copyright 2017-2020 ForgeFlow S.L.
# (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
@@ -6,11 +6,12 @@ from odoo import fields, models
class StockWarehouse(models.Model):
_inherit = 'stock.warehouse'
_inherit = "stock.warehouse"
discrepancy_threshold = fields.Float(
string='Maximum Discrepancy Rate Threshold',
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.")
"an Inventory Adjustment. Threshold defined in involved Location "
"has preference.",
)

View File

@@ -1,3 +1,3 @@
* Lois Rilo <lois.rilo@eficent.com>
* Lois Rilo <lois.rilo@forgeflow.com>
* Andreas Dian Sukarno Putro <andreasdian777@gmail.com>
* Bhavesh Odedra <bodedra@opensourceintegrators.com>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 Eficent Business and IT Consulting Services S.L.
<!-- Copyright 2017-2020 ForgeFlow S.L.
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo noupdate="1">

View File

@@ -1,172 +1,217 @@
# Copyright 2017 Eficent Business and IT Consulting Services S.L.
# Copyright 2017-2020 ForgeFlow S.L.
# (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError
from odoo.tests.common import TransactionCase
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.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.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_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])]
})
"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(
"uom.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(
"uom.product_uom_unit").id,
'product_qty': 4.0,
'location_id': self.test_loc.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("uom.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("uom.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(
"uom.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(
"uom.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.')
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("uom.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("uom.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(
"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_context({'normal_view': True}).action_validate()
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 = 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("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_context({"normal_view": True}).action_validate()
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.')
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(
"uom.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.')
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("uom.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.sudo(self.user).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,
})
upd_qty = self.obj_upd_qty_wizard.sudo(self.user).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.change_product_qty()

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 Eficent Business and IT Consulting Services S.L.
<!-- Copyright 2017-2020 ForgeFlow S.L.
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 Eficent Business and IT Consulting Services S.L.
<!-- Copyright 2017-2020 ForgeFlow S.L.
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 Eficent Business and IT Consulting Services S.L.
<!-- Copyright 2017-2020 ForgeFlow S.L.
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>