mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
stock_available_unreserved: allow searching by unreserved quantities
This commit is contained in:
@@ -48,6 +48,7 @@ Contributors
|
||||
* Jordi Ballester Alomar <jordi.ballester@eficent.com>
|
||||
* Stefan Rijnhart <stefan@opener.amsterdam>
|
||||
* Mykhailo Panarin <m.panarin@mobilunity.com>
|
||||
* Atte Isopuro <atte.isopuro@avoin.systems>
|
||||
|
||||
|
||||
Maintainer
|
||||
|
||||
@@ -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 @@ class ProductTemplate(models.Model):
|
||||
}
|
||||
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 @@ class ProductProduct(models.Model):
|
||||
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)]
|
||||
|
||||
@@ -43,6 +43,8 @@ class TestStockLogisticsWarehouse(SavepointCase):
|
||||
'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 @@ class TestStockLogisticsWarehouse(SavepointCase):
|
||||
'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)
|
||||
|
||||
@@ -14,6 +14,17 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_template_search_form_view_stock" model="ir.ui.view">
|
||||
<field name="name">product.template.search.stock.form.inherit</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="stock.product_template_search_form_view_stock"/>
|
||||
<field name="arch" type="xml">
|
||||
<filter name="real_stock_available" position="after">
|
||||
<filter name="real_stock_unreserved" string="Reservable Products" domain="[('qty_available_not_res','>',0)]"/>
|
||||
</filter>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="product_template_kanban_stock_view">
|
||||
<field name="name">Product Template Kanban Stock</field>
|
||||
<field name="model">product.template</field>
|
||||
|
||||
Reference in New Issue
Block a user