160 lines
6.5 KiB
Python
160 lines
6.5 KiB
Python
# © 2020 Acsone (http://www.acsone.eu)
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
|
|
|
import fnmatch
|
|
from odoo import fields, models
|
|
from odoo.osv import expression
|
|
|
|
LIKE_COMPARATORS = (
|
|
'like',
|
|
'ilike',
|
|
'=like',
|
|
'=ilike',
|
|
'not ilike',
|
|
'not like',
|
|
)
|
|
|
|
|
|
class Base(models.AbstractModel):
|
|
|
|
_inherit = 'base'
|
|
|
|
def partition(self, accessor):
|
|
"""Returns a dictionary forming a partition of self into a dictionary
|
|
value/recordset for each value obtained from the accessor.
|
|
The accessor itself can be either a string that can be passed to mapped,
|
|
or an arbitrary function.
|
|
Note that it is always at least as fast to pass a function,
|
|
hence the current implementation.
|
|
If we have a 'field.subfield' accessor such that subfield is not a relational
|
|
then the result is a list (not hashable). Then the str(key) are used.
|
|
In the general case a value could both not be hashable nor stringifiable,
|
|
in a which case this function would crash.
|
|
"""
|
|
partition = {}
|
|
|
|
if isinstance(accessor, str):
|
|
if "." not in accessor:
|
|
func = lambda r: r[accessor] # noqa: E731
|
|
else:
|
|
func = lambda r: r.mapped(accessor) # noqa: E731
|
|
else:
|
|
func = accessor
|
|
|
|
for record in self:
|
|
key = func(record)
|
|
if not key.__hash__:
|
|
key = str(key)
|
|
if key not in partition:
|
|
partition[key] = record
|
|
else:
|
|
partition[key] += record
|
|
|
|
return partition
|
|
|
|
def filtered_domain(self, domain):
|
|
"""Backport from standard.
|
|
"""
|
|
if not domain:
|
|
return self
|
|
result = []
|
|
for d in reversed(domain):
|
|
if d == '|':
|
|
result.append(result.pop() | result.pop())
|
|
elif d == '!':
|
|
result.append(self - result.pop())
|
|
elif d == '&':
|
|
result.append(result.pop() & result.pop())
|
|
elif d == expression.TRUE_LEAF:
|
|
result.append(self)
|
|
elif d == expression.FALSE_LEAF:
|
|
result.append(self.browse())
|
|
else:
|
|
(key, comparator, value) = d
|
|
if key.endswith('.id'):
|
|
key = key[:-3]
|
|
if key == 'id':
|
|
key = ''
|
|
# determine the field with the final type for values
|
|
field = None
|
|
if key:
|
|
model = self.browse()
|
|
for fname in key.split('.'):
|
|
field = model._fields[fname]
|
|
model = model[fname]
|
|
|
|
if comparator in LIKE_COMPARATORS:
|
|
value_esc = (
|
|
value.replace('_', '?')
|
|
.replace('%', '*')
|
|
.replace('[', '?')
|
|
)
|
|
records = self.browse()
|
|
for rec in self:
|
|
data = rec.mapped(key)
|
|
if comparator in ('child_of', 'parent_of'):
|
|
records = data.search([(data._parent_name, comparator, value)])
|
|
value = records.ids
|
|
comparator = 'in'
|
|
if isinstance(data, models.BaseModel):
|
|
v = value
|
|
if isinstance(value, (list, tuple)) and len(value):
|
|
v = value[0]
|
|
if isinstance(v, str):
|
|
data = data.mapped('display_name')
|
|
else:
|
|
data = data.ids if data else [False]
|
|
elif field and field.type in ('date', 'datetime'):
|
|
# convert all date and datetime values to datetime
|
|
normalize = fields.Datetime.to_datetime
|
|
if isinstance(value, (list, tuple)):
|
|
value = [normalize(v) for v in value]
|
|
else:
|
|
value = normalize(value)
|
|
data = [normalize(d) for d in data]
|
|
if comparator in ('in', 'not in'):
|
|
if not (isinstance(value, list) or isinstance(value, tuple)):
|
|
value = [value]
|
|
|
|
if comparator == '=':
|
|
ok = value in data
|
|
elif comparator == 'in':
|
|
ok = any(map(lambda x: x in data, value))
|
|
elif comparator == '<':
|
|
ok = any(map(lambda x: x is not None and x < value, data))
|
|
elif comparator == '>':
|
|
ok = any(map(lambda x: x is not None and x > value, data))
|
|
elif comparator == '<=':
|
|
ok = any(map(lambda x: x is not None and x <= value, data))
|
|
elif comparator == '>=':
|
|
ok = any(map(lambda x: x is not None and x >= value, data))
|
|
elif comparator in ('!=', '<>'):
|
|
ok = value not in data
|
|
elif comparator == 'not in':
|
|
ok = all(map(lambda x: x not in data, value))
|
|
elif comparator == 'not ilike':
|
|
ok = all(map(lambda x: value.lower() not in x.lower(), data))
|
|
elif comparator == 'ilike':
|
|
data = [x.lower() for x in data]
|
|
match = fnmatch.filter(data, '*'+(value_esc or '').lower()+'*')
|
|
ok = bool(match)
|
|
elif comparator == 'not like':
|
|
ok = all(map(lambda x: value not in x, data))
|
|
elif comparator == 'like':
|
|
ok = bool(fnmatch.filter(data, value and '*'+value_esc+'*'))
|
|
elif comparator == '=?':
|
|
ok = (value in data) or not value
|
|
elif comparator == '=like':
|
|
ok = bool(fnmatch.filter(data, value_esc))
|
|
elif comparator == '=ilike':
|
|
data = [x.lower() for x in data]
|
|
ok = bool(fnmatch.filter(data, value and value_esc.lower()))
|
|
else:
|
|
raise ValueError
|
|
if ok:
|
|
records |= rec
|
|
result.append(records)
|
|
while len(result) > 1:
|
|
result.append(result.pop() & result.pop())
|
|
return result[0]
|