[MIG] stock_inventory_discrepancy: Migration to 13.0

This commit is contained in:
hveficent
2020-01-07 14:03:21 +01:00
committed by Mateu Griful
parent bc1b2c267f
commit 2edeafa057
14 changed files with 282 additions and 61 deletions

View File

@@ -1,3 +1,4 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import models from . import models
from .hooks import post_load_hook

View File

@@ -5,18 +5,20 @@
"summary": "Adds the capability to show the discrepancy of every line in " "summary": "Adds the capability to show the discrepancy of every line in "
"an inventory and to block the inventory validation when the " "an inventory and to block the inventory validation when the "
"discrepancy is over a user defined threshold.", "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)", "author": "ForgeFlow, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/stock-logistics-warehouse", "website": "https://github.com/OCA/stock-logistics-warehouse",
"category": "Warehouse Management", "category": "Warehouse",
"depends": ["stock"], "depends": ["stock"],
"data": [ "data": [
"security/stock_inventory_discrepancy_security.xml", "security/stock_inventory_discrepancy_security.xml",
"views/assets_backend.xml",
"views/stock_inventory_view.xml", "views/stock_inventory_view.xml",
"views/stock_warehouse_view.xml", "views/stock_warehouse_view.xml",
"views/stock_location_view.xml", "views/stock_location_view.xml",
], ],
"license": "AGPL-3", "license": "AGPL-3",
"post_load": "post_load_hook",
"installable": True, "installable": True,
"application": False, "application": False,
} }

View File

@@ -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)

View File

@@ -1,5 +1,5 @@
# Copyright 2017-2020 ForgeFlow S.L. # 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). # 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
@@ -9,16 +9,8 @@ from odoo.exceptions import UserError
class StockInventory(models.Model): 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"),
]
state = fields.Selection( state = fields.Selection(
selection=INVENTORY_STATE_SELECTION, selection_add=[("pending", "Pending to Approve"), ("done",)],
string="Status", string="Status",
readonly=True, readonly=True,
index=True, index=True,
@@ -37,7 +29,6 @@ class StockInventory(models.Model):
store=True, 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): def _compute_over_discrepancy_line_count(self):
for inventory in self: for inventory in self:
@@ -46,13 +37,12 @@ class StockInventory(models.Model):
) )
inventory.over_discrepancy_line_count = len(lines) inventory.over_discrepancy_line_count = len(lines)
@api.multi
def action_over_discrepancies(self): def action_over_discrepancies(self):
self.write({"state": "pending"}) self.write({"state": "pending"})
def _check_group_inventory_validation_always(self): def _check_group_inventory_validation_always(self):
grp_inv_val = self.env.ref( 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: if grp_inv_val in self.env.user.groups_id:
return True return True
@@ -70,13 +60,17 @@ class StockInventory(models.Model):
if inventory.over_discrepancy_line_count and inventory.line_ids.filtered( if inventory.over_discrepancy_line_count and inventory.line_ids.filtered(
lambda t: t.discrepancy_threshold > 0.0 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() inventory.action_over_discrepancies()
return True return True
else: else:
inventory._check_group_inventory_validation_always() inventory._check_group_inventory_validation_always()
return super(StockInventory, self)._action_done() return super(StockInventory, self)._action_done()
@api.multi
def action_force_done(self): def action_force_done(self):
return super(StockInventory, self)._action_done() return super(StockInventory, self)._action_done()

View File

@@ -1,11 +1,9 @@
# Copyright 2017-2020 ForgeFlow S.L. # 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). # 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.addons import decimal_precision as dp
class StockInventoryLine(models.Model): class StockInventoryLine(models.Model):
_inherit = "stock.inventory.line" _inherit = "stock.inventory.line"
@@ -15,7 +13,7 @@ class StockInventoryLine(models.Model):
compute="_compute_discrepancy", compute="_compute_discrepancy",
help="The difference between the actual qty counted and the " help="The difference between the actual qty counted and the "
"theoretical quantity on hand.", "theoretical quantity on hand.",
digits=dp.get_precision("Product Unit of Measure"), digits="Product Unit of Measure",
default=0, default=0,
) )
discrepancy_percent = fields.Float( discrepancy_percent = fields.Float(
@@ -32,7 +30,6 @@ class StockInventoryLine(models.Model):
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): def _compute_discrepancy(self):
for line in self: for line in self:
@@ -43,8 +40,9 @@ class StockInventoryLine(models.Model):
) )
elif not line.theoretical_qty and line.product_qty: elif not line.theoretical_qty and line.product_qty:
line.discrepancy_percent = 100.0 line.discrepancy_percent = 100.0
else:
line.discrepancy_percent = 0.0
@api.multi
def _compute_discrepancy_threshold(self): def _compute_discrepancy_threshold(self):
for line in self: for line in self:
whs = line.location_id.get_warehouse() whs = line.location_id.get_warehouse()

View File

@@ -1,5 +1,5 @@
# Copyright 2017-2020 ForgeFlow S.L. # 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). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import fields, models from odoo import fields, models
@@ -15,3 +15,22 @@ class StockLocation(models.Model):
"an Inventory Adjustment. Thresholds defined in Locations have " "an Inventory Adjustment. Thresholds defined in Locations have "
"preference over Warehouse's ones.", "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

View File

@@ -1,5 +1,5 @@
# Copyright 2017-2020 ForgeFlow S.L. # 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). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import fields, models from odoo import fields, models

View File

@@ -1,3 +1,4 @@
* Lois Rilo <lois.rilo@forgeflow.com> * Lois Rilo <lois.rilo@forgeflow.com>
* Andreas Dian Sukarno Putro <andreasdian777@gmail.com> * Andreas Dian Sukarno Putro <andreasdian777@gmail.com>
* Bhavesh Odedra <bodedra@opensourceintegrators.com> * Bhavesh Odedra <bodedra@opensourceintegrators.com>
* Héctor Villarreal <hector.villarreal@forgeflow.com>

View File

@@ -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,
});
});
}
},
});
});

View File

@@ -1,5 +1,5 @@
# Copyright 2017-2020 ForgeFlow S.L. # 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). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo.exceptions import UserError from odoo.exceptions import UserError
@@ -14,7 +14,6 @@ class TestInventoryDiscrepancy(TransactionCase):
self.obj_inventory = self.env["stock.inventory"] self.obj_inventory = self.env["stock.inventory"]
self.obj_product = self.env["product.product"] self.obj_product = self.env["product.product"]
self.obj_warehouse = self.env["stock.warehouse"] self.obj_warehouse = self.env["stock.warehouse"]
self.obj_upd_qty_wizard = self.env["stock.change.product.qty"]
self.product1 = self.obj_product.create( self.product1 = self.obj_product.create(
{"name": "Test Product 1", "type": "product", "default_code": "PROD1"} {"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. # 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( 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( self.manager = self.env["res.users"].create(
{ {
"name": "Test Manager", "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( starting_inv = self.obj_inventory.create(
{ {
"name": "Starting inventory", "name": "Starting inventory",
"filter": "product",
"line_ids": [ "line_ids": [
( (
0, 0,
@@ -89,8 +107,7 @@ class TestInventoryDiscrepancy(TransactionCase):
inventory = self.obj_inventory.create( inventory = self.obj_inventory.create(
{ {
"name": "Test Discrepancy Computation", "name": "Test Discrepancy Computation",
"location_id": self.test_loc.id, "location_ids": [(4, self.test_loc.id)],
"filter": "none",
"line_ids": [ "line_ids": [
( (
0, 0,
@@ -131,8 +148,7 @@ class TestInventoryDiscrepancy(TransactionCase):
inventory = self.obj_inventory.create( inventory = self.obj_inventory.create(
{ {
"name": "Test Forcing Validation Method", "name": "Test Forcing Validation Method",
"location_id": self.test_loc.id, "location_ids": [(4, self.test_loc.id)],
"filter": "none",
"line_ids": [ "line_ids": [
( (
0, 0,
@@ -155,7 +171,8 @@ class TestInventoryDiscrepancy(TransactionCase):
0.1, 0.1,
"Threshold wrongly computed in Inventory Line.", "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( self.assertEqual(
inventory.over_discrepancy_line_count, inventory.over_discrepancy_line_count,
1, 1,
@@ -166,7 +183,7 @@ class TestInventoryDiscrepancy(TransactionCase):
"pending", "pending",
"Inventory Adjustment not changing to Pending to " "Approve.", "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( self.assertEqual(
inventory.state, inventory.state,
"done", "done",
@@ -174,13 +191,56 @@ class TestInventoryDiscrepancy(TransactionCase):
"not working properly.", "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): def test_warehouse_threshold(self):
"""Tests the behaviour if the threshold is set on the WH.""" """Tests the behaviour if the threshold is set on the WH."""
inventory = self.obj_inventory.create( inventory = self.obj_inventory.create(
{ {
"name": "Test Threshold Defined in WH", "name": "Test Threshold Defined in WH",
"location_id": self.test_wh.view_location_id.id, "location_ids": [(4, self.test_wh.view_location_id.id)],
"filter": "none",
"line_ids": [ "line_ids": [
( (
0, 0,
@@ -201,17 +261,23 @@ class TestInventoryDiscrepancy(TransactionCase):
"Threshold wrongly computed in Inventory Line.", "Threshold wrongly computed in Inventory Line.",
) )
def test_update_qty_user_error(self): def test_propagate_discrepancy_threshold(self):
"""Test if a user error raises when a stock user tries to update the view_test_loc = self.obj_location.create(
qty for a product and the correction is a discrepancy over the {"name": "Test Location", "usage": "view", "discrepancy_threshold": 0.1}
threshold.""" )
upd_qty = self.obj_upd_qty_wizard.sudo(self.user).create( child_test_loc = self.obj_location.create(
{ {
"product_id": self.product1.id, "name": "Child Test Location",
"product_tmpl_id": self.product1.product_tmpl_id.id, "usage": "internal",
"new_quantity": 10.0, "discrepancy_threshold": 0.2,
"location_id": self.test_loc.id, "location_id": view_test_loc.id,
} }
) )
with self.assertRaises(UserError): view_test_loc.write(
upd_qty.change_product_qty() {"discrepancy_threshold": 0.3, "propagate_discrepancy_threshold": True}
)
self.assertEqual(
child_test_loc.discrepancy_threshold,
0.3,
"Threshold Discrepancy wrongly propagated",
)

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="stock_assets_backend" name="stock_inventory assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/stock_inventory_discrepancy/static/src/js/inventory_validate_button_controller.js"></script>
</xpath>
</template>
</odoo>

View File

@@ -20,7 +20,6 @@
<xpath expr="//button[@name='action_validate']" <xpath expr="//button[@name='action_validate']"
position="attributes"> position="attributes">
<attribute name="groups">stock_inventory_discrepancy.group_stock_inventory_validation</attribute> <attribute name="groups">stock_inventory_discrepancy.group_stock_inventory_validation</attribute>
<attribute name="context">{'normal_view': True}</attribute>
</xpath> </xpath>
<xpath expr="//button[@name='action_validate']" <xpath expr="//button[@name='action_validate']"
position="after"> position="after">
@@ -30,19 +29,31 @@
groups="stock_inventory_discrepancy.group_stock_inventory_validation_always" groups="stock_inventory_discrepancy.group_stock_inventory_validation_always"
attrs="{'invisible': ['|',('state', '!=', 'pending'),('over_discrepancy_line_count', '=', 0)]}"/> attrs="{'invisible': ['|',('state', '!=', 'pending'),('over_discrepancy_line_count', '=', 0)]}"/>
</xpath> </xpath>
<field name="date" position="after"> <xpath expr="//button[@name='action_open_inventory_lines']" position="attributes">
<attribute name="states">pending,confirm</attribute>
</xpath>
<xpath expr="//button[@name='action_cancel_draft'][2]" position="attributes">
<attribute name="states">pending,confirm</attribute>
</xpath>
<field name="company_id" position="before">
<field name="over_discrepancy_line_count" attrs="{'invisible': [('state', '!=', 'pending')]}"/> <field name="over_discrepancy_line_count" attrs="{'invisible': [('state', '!=', 'pending')]}"/>
</field> </field>
<xpath expr="//field[@name='line_ids']/tree/field[@name='product_qty']"
position="after">
<field name="discrepancy_qty"/>
<field name="discrepancy_percent"/>
<field name="discrepancy_threshold"/>
</xpath>
<xpath expr="//field[@name='line_ids']/tree" position="attributes">
<attribute name="decoration-danger">theoretical_qty &lt; 0 or discrepancy_percent &gt; discrepancy_threshold</attribute>
</xpath>
</field> </field>
</record> </record>
<record id="stock_inventory_line_tree2" model="ir.ui.view">
<field name="name">stock.inventory.line.tree2</field>
<field name="model">stock.inventory.line</field>
<field name="inherit_id" ref="stock.stock_inventory_line_tree2"/>
<field name="arch" type="xml">
<field name="product_qty" position="after">
<field name="discrepancy_qty"/>
<field name="discrepancy_percent"/>
<field name="discrepancy_threshold"/>
</field>
<xpath expr="//tree" position="attributes">
<attribute name="decoration-danger">theoretical_qty &lt; 0 or discrepancy_percent &gt; discrepancy_threshold or "product_qty != theoretical_qty"</attribute>
</xpath>
</field>
</record>
</odoo> </odoo>

View File

@@ -9,11 +9,12 @@
<field name="model">stock.location</field> <field name="model">stock.location</field>
<field name="inherit_id" ref="stock.view_location_form"/> <field name="inherit_id" ref="stock.view_location_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<field name="partner_id" position="after"> <field name="return_location" position="after">
<label for="discrepancy_threshold"/> <label for="discrepancy_threshold"/>
<div> <div>
<field name="discrepancy_threshold" class="oe_inline"/> % <field name="discrepancy_threshold" class="oe_inline"/> %
</div> </div>
<field name="propagate_discrepancy_threshold"/>
</field> </field>
</field> </field>
</record> </record>

View File

@@ -4,7 +4,7 @@
<odoo> <odoo>
<record id="view_warehouse_form" model="ir.ui.view"> <record id="view_warehouse" model="ir.ui.view">
<field name="name">Warehouse form - Inventory Discrepancy <field name="name">Warehouse form - Inventory Discrepancy
extension</field> extension</field>
<field name="model">stock.warehouse</field> <field name="model">stock.warehouse</field>