From 971fb5c358e82e47f95122ae17fcb05cc263287e Mon Sep 17 00:00:00 2001 From: Atte Isopuro Date: Mon, 9 Apr 2018 12:22:14 +0300 Subject: [PATCH] stock_available_unreserved: allow searching by unreserved quantities --- stock_available_unreserved/README.rst | 1 + stock_available_unreserved/models/product.py | 23 ++- .../tests/test_stock_available_unreserved.py | 164 ++++++++++++++++++ .../views/product_view.xml | 11 ++ 4 files changed, 198 insertions(+), 1 deletion(-) diff --git a/stock_available_unreserved/README.rst b/stock_available_unreserved/README.rst index 43b879b86b0e..ad650e028bc3 100644 --- a/stock_available_unreserved/README.rst +++ b/stock_available_unreserved/README.rst @@ -48,6 +48,7 @@ Contributors * Jordi Ballester Alomar * Stefan Rijnhart * Mykhailo Panarin +* Atte Isopuro Maintainer diff --git a/stock_available_unreserved/models/product.py b/stock_available_unreserved/models/product.py index 5a400832ebfc..6447e4cea9a0 100644 --- a/stock_available_unreserved/models/product.py +++ b/stock_available_unreserved/models/product.py @@ -4,9 +4,11 @@ # (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.addons import decimal_precision as dp +from odoo.addons.stock.models.product import OPERATORS from odoo.tools.float_utils import float_round +from odoo.exceptions import UserError UNIT = dp.get_precision('Product Unit of Measure') @@ -18,6 +20,7 @@ class ProductTemplate(models.Model): string='Quantity On Hand Unreserved', digits=UNIT, compute='_compute_product_available_not_res', + search='_search_quantity_unreserved', ) @api.multi @@ -47,6 +50,11 @@ def action_open_quants_unreserved(self): } return result + def _search_quantity_unreserved(self, operator, value): + domain = [('qty_available_not_res', operator, value)] + product_variant_ids = self.env['product.product'].search(domain) + return [('product_variant_ids', 'in', product_variant_ids.ids)] + class ProductProduct(models.Model): _inherit = 'product.product' @@ -55,6 +63,7 @@ class ProductProduct(models.Model): string='Qty Available Not Reserved', digits=UNIT, compute='_compute_qty_available_not_reserved', + search="_search_quantity_unreserved", ) @api.multi @@ -101,3 +110,15 @@ def _compute_qty_available_not_reserved(self): qty = res[prod.id]['qty_available_not_res'] prod.qty_available_not_res = qty return res + + def _search_quantity_unreserved(self, operator, value): + if operator not in OPERATORS: + raise UserError(_('Invalid domain operator %s') % operator) + if not isinstance(value, (float, int)): + raise UserError(_('Invalid domain right operand %s') % value) + + ids = [] + for product in self.search([]): + if OPERATORS[operator](product.qty_available_not_res, value): + ids.append(product.id) + return [('id', 'in', ids)] diff --git a/stock_available_unreserved/tests/test_stock_available_unreserved.py b/stock_available_unreserved/tests/test_stock_available_unreserved.py index 19dade6139af..37638a98627a 100644 --- a/stock_available_unreserved/tests/test_stock_available_unreserved.py +++ b/stock_available_unreserved/tests/test_stock_available_unreserved.py @@ -43,6 +43,8 @@ def setUpClass(cls): 'uom_id': cls.uom_unit.id, }) + cls.productC = cls.templateAB.product_variant_ids + # Create product A and B cls.productA = cls.productObj.create({ 'name': 'product A', @@ -175,3 +177,165 @@ def test_more_than_one_quant(self): 'product_id': self.productA.id, 'quantity': 60.0}) self.compare_qty_available_not_res(self.productA, 80) + + def check_variants_found_correctly(self, operator, value, expected): + domain = [('id', 'in', self.templateAB.product_variant_ids.ids)] + return self.check_found_correctly(self.env['product.product'], + domain, operator, value, expected) + + def check_template_found_correctly(self, operator, value, expected): + # There may be other products already in the system: ignore those + domain = [('id', 'in', self.templateAB.ids)] + return self.check_found_correctly(self.env['product.template'], + domain, operator, value, expected) + + def check_found_correctly(self, model, domain, operator, value, expected): + found = model.search(domain + [ + ('qty_available_not_res', operator, value)] + ) + if found != expected: + self.fail( + "Searching for products failed: search for unreserved " + "quantity {operator} {value}; expected to find " + "{expected}, but found {found}".format( + operator=operator, + value=value, + expected=expected or "no products", + found=found, + ) + ) + + def test_stock_search(self): + all_variants = self.templateAB.product_variant_ids + a_and_b = self.productA + self.productB + b_and_c = self.productB + self.productC + a_and_c = self.productA + self.productC + no_variants = self.env['product.product'] + no_template = self.env['product.template'] + # Start: one template with three variants. + # All variants have zero unreserved stock + self.check_variants_found_correctly('=', 0, all_variants) + self.check_variants_found_correctly('>=', 0, all_variants) + self.check_variants_found_correctly('<=', 0, all_variants) + self.check_variants_found_correctly('>', 0, no_variants) + self.check_variants_found_correctly('<', 0, no_variants) + self.check_variants_found_correctly('!=', 0, no_variants) + + self.check_template_found_correctly('=', 0, self.templateAB) + self.check_template_found_correctly('>=', 0, self.templateAB) + self.check_template_found_correctly('<=', 0, self.templateAB) + self.check_template_found_correctly('>', 0, no_template) + self.check_template_found_correctly('<', 0, no_template) + self.check_template_found_correctly('!=', 0, no_template) + + self.pickingInA.action_confirm() + # All variants still have zero unreserved stock + self.check_variants_found_correctly('=', 0, all_variants) + self.check_variants_found_correctly('>=', 0, all_variants) + self.check_variants_found_correctly('<=', 0, all_variants) + self.check_variants_found_correctly('>', 0, no_variants) + self.check_variants_found_correctly('<', 0, no_variants) + self.check_variants_found_correctly('!=', 0, no_variants) + + self.check_template_found_correctly('=', 0, self.templateAB) + self.check_template_found_correctly('>=', 0, self.templateAB) + self.check_template_found_correctly('<=', 0, self.templateAB) + self.check_template_found_correctly('>', 0, no_template) + self.check_template_found_correctly('<', 0, no_template) + self.check_template_found_correctly('!=', 0, no_template) + + self.pickingInA.action_assign() + # All variants still have zero unreserved stock + self.check_variants_found_correctly('=', 0, all_variants) + self.check_variants_found_correctly('>=', 0, all_variants) + self.check_variants_found_correctly('<=', 0, all_variants) + self.check_variants_found_correctly('>', 0, no_variants) + self.check_variants_found_correctly('<', 0, no_variants) + self.check_variants_found_correctly('!=', 0, no_variants) + + self.check_template_found_correctly('=', 0, self.templateAB) + self.check_template_found_correctly('>=', 0, self.templateAB) + self.check_template_found_correctly('<=', 0, self.templateAB) + self.check_template_found_correctly('>', 0, no_template) + self.check_template_found_correctly('<', 0, no_template) + self.check_template_found_correctly('!=', 0, no_template) + + self.pickingInA.button_validate() + # product A has 2 unreserved stock, other variants have 0 + + self.check_variants_found_correctly('=', 2, self.productA) + self.check_variants_found_correctly('=', 0, b_and_c) + self.check_variants_found_correctly('>', 0, self.productA) + self.check_variants_found_correctly('<', 0, no_variants) + self.check_variants_found_correctly('!=', 0, self.productA) + self.check_variants_found_correctly('!=', 1, all_variants) + self.check_variants_found_correctly('!=', 2, b_and_c) + self.check_variants_found_correctly('<=', 0, b_and_c) + self.check_variants_found_correctly('<=', 1, b_and_c) + self.check_variants_found_correctly('>=', 0, all_variants) + self.check_variants_found_correctly('>=', 1, self.productA) + + self.check_template_found_correctly('=', 0, self.templateAB) + self.check_template_found_correctly('=', 1, no_template) + self.check_template_found_correctly('=', 2, self.templateAB) + self.check_template_found_correctly('!=', 0, self.templateAB) + self.check_template_found_correctly('!=', 1, self.templateAB) + self.check_template_found_correctly('!=', 2, self.templateAB) + self.check_template_found_correctly('>', -1, self.templateAB) + self.check_template_found_correctly('>', 0, self.templateAB) + self.check_template_found_correctly('>', 1, self.templateAB) + self.check_template_found_correctly('>', 2, no_template) + self.check_template_found_correctly('<', 3, self.templateAB) + self.check_template_found_correctly('<', 2, self.templateAB) + self.check_template_found_correctly('<', 1, self.templateAB) + self.check_template_found_correctly('<', 0, no_template) + self.check_template_found_correctly('>=', 0, self.templateAB) + self.check_template_found_correctly('>=', 1, self.templateAB) + self.check_template_found_correctly('>=', 2, self.templateAB) + self.check_template_found_correctly('>=', 3, no_template) + self.check_template_found_correctly('<=', 3, self.templateAB) + self.check_template_found_correctly('<=', 2, self.templateAB) + self.check_template_found_correctly('<=', 1, self.templateAB) + self.check_template_found_correctly('<=', 0, self.templateAB) + self.check_template_found_correctly('<=', -1, no_template) + + self.pickingInB.action_done() + # product A has 2 unreserved, product B has 3 unreserved and + # the remaining variant has 0 + + self.check_variants_found_correctly('=', 2, self.productA) + self.check_variants_found_correctly('=', 3, self.productB) + self.check_variants_found_correctly('=', 0, self.productC) + self.check_variants_found_correctly('>', 0, a_and_b) + self.check_variants_found_correctly('<', 0, no_variants) + self.check_variants_found_correctly('!=', 0, a_and_b) + self.check_variants_found_correctly('!=', 1, all_variants) + self.check_variants_found_correctly('!=', 2, b_and_c) + self.check_variants_found_correctly('!=', 3, a_and_c) + self.check_variants_found_correctly('<=', 0, self.productC) + self.check_variants_found_correctly('<=', 1, self.productC) + self.check_variants_found_correctly('>=', 0, all_variants) + self.check_variants_found_correctly('>=', 1, a_and_b) + self.check_variants_found_correctly('>=', 2, a_and_b) + self.check_variants_found_correctly('>=', 3, self.productB) + self.check_variants_found_correctly('>=', 4, no_variants) + + self.check_template_found_correctly('=', 0, self.templateAB) + self.check_template_found_correctly('=', 1, no_template) + self.check_template_found_correctly('=', 2, self.templateAB) + self.check_template_found_correctly('=', 3, self.templateAB) + self.check_template_found_correctly('!=', 0, self.templateAB) + self.check_template_found_correctly('!=', 2, self.templateAB) + self.check_template_found_correctly('!=', 3, self.templateAB) + self.check_template_found_correctly('>', 1, self.templateAB) + self.check_template_found_correctly('>', 2, self.templateAB) + # This part may seem a bit unintuitive, but this is the + # way it works in the Odoo core + # Searches are "deferred" to the variants, so while the template says + # it has a stock of 5, searching for a stock greater than 3 will not + # find anything because no singular variant has a higher stock + self.check_template_found_correctly('>', 3, no_template) + self.check_template_found_correctly('<', 3, self.templateAB) + self.check_template_found_correctly('<', 2, self.templateAB) + self.check_template_found_correctly('<', 1, self.templateAB) + self.check_template_found_correctly('<', 0, no_template) diff --git a/stock_available_unreserved/views/product_view.xml b/stock_available_unreserved/views/product_view.xml index 1d714ca54560..ea55a1cccb6c 100644 --- a/stock_available_unreserved/views/product_view.xml +++ b/stock_available_unreserved/views/product_view.xml @@ -14,6 +14,17 @@ + + product.template.search.stock.form.inherit + product.template + + + + + + + + Product Template Kanban Stock product.template