Я наткнулся на проблему, возникающую при реализации поиска по вычисляемому полю, в том, что в определенных случаях поиск по вычисляемому полю не имеет прямой связи (насколько я знаю), что означает, что я не могу написать домен, чтобы найти то, что вычисляется.
В этом случае мне пришлось по умолчанию выполнить очень неэффективный поиск, где я ищу все записи и затем фильтрую соответственно.
Поскольку я знал, что этот поиск плохо масштабируется, я провел тест с ~ 2000 производственными заказами (этот поиск выполняется по модели mrp.production). Вот некоторые характеристики (mo_search
обернуто self.env['mrp.production'].search
с декоратором синхронизации):
In [12]: len(mo_search([('main_availability', '!=', 'none')]))
func:'search' args:[([('main_availability', '!=', 'none')],), {}] took: 0.0013 min
Out[12]: 3528
In [13]: len(mo_search([('main_availability', '!=', 'released')]))
func:'search' args:[([('main_availability', '!=', 'released')],), {}] took: 0.0029 min
Out[13]: 3047
In [14]: len(mo_search([('main_availability', '=', 'released')]))
func:'search' args:[([('main_availability', '=', 'released')],), {}] took: 0.0012 min
Out[14]: 788
In [15]: len(mo_search([('main_availability', '=', 'available')]))
func:'search' args:[([('main_availability', '=', 'available')],), {}] took: 1.6445 min
Out[15]: 1460
Таким образом, части (смотри метод _search_main_availability
), которые оптимизированы, завершают поиск менее чем за секунду (части if / elif), а неоптимизированные части завершают поиск почти за две минуты (условие else). Так что это огромная разница, и с большим количеством записей это будет еще медленнее.
Теперь, как это выглядит:
import operator as op
from odoo import models, fields, api, _
OPS = {
'=': op.eq,
'!=': op.ne,
'in': op.contains,
'not in': lambda a, b: not op.contains(a, b),
}
class MrpProduction(models.Model):
"""Extend to add materials main location.
It is used to show available materials on that location which are
demanded on manufacturing order.
"""
_inherit = 'mrp.production'
location_main_src_id = fields.Many2one(
'stock.location',
"Materials Main Location",
domain=[('usage', '=', 'internal')],
default=_get_default_location_main_src_id)
main_availability = fields.Selection([
('none', 'None'),
('waiting', 'Waiting'),
('partially_available', 'Partially Available'),
('available', 'Available'),
('released', 'Materials Released')],
string="Materials Availability (Main Location)",
search='_search_main_availability',
compute='_compute_main_availability_fields',
help="Availability of materials that are on main location.\nMaterials "
"Released state means, there is at least one not cancelled not draft\n"
"Transfer that was created for this manufacturing order.")
def _search_main_availability(self, operator, value):
# We can shortcut to non existing domain if value is falsy,
# because every MO is expected to have truthy value.
if not value and operator == '=':
return [('id', '=', False)]
if value == 'released':
if operator in ('=', 'in'):
# released state.
recs = self.search(
[
('picking_mo_ids', '!=', False),
(
'picking_mo_ids.state',
'not in',
('draft', 'cancel')
)
]
)
elif operator in ('!=', 'not in'):
# not released state.
recs = self.search(
[
'|',
('picking_mo_ids', '=', False),
(
'picking_mo_ids.state',
'in',
('draft', 'cancel')
)
]
)
elif value == 'none':
# Looking for none state.
if operator in ('=', 'in'):
recs = self.search(
[
'|',
('location_main_src_id', '=', False),
('move_raw_ids', '=', False)]
)
# Looking for not none states.
elif operator in ('!=', 'not in'):
recs = self.search(
[
('location_main_src_id', '!=', False),
('move_raw_ids', '!=', False)]
)
else:
# THIS PART IS BOTTLENECK.
# We can't use proper domain when checking actual
# availability states ('waiting', 'available',
# 'partially_available'), because domain can't tell
# availability in this case. Instead we brute search all
# possible MOs and then filter it accordingly.
recs = self.search([])
op_method = OPS[operator]
recs = recs.filtered(
lambda r: op_method(value, r.main_availability))
return [('id', 'in', recs.ids)]
def _prepare_main_availability(self):
self.ensure_one()
data = {}
location = self.location_main_src_id
if not location:
return {}
StockQuant = self.env['stock.quant']
for move in self.move_raw_ids:
product = move.product_id
qty_available = StockQuant._get_available_quantity(
product, location)
qty_demand = move.product_uom_qty
line_data = {
'product': product,
'qty_available': qty_available,
'qty_demand': qty_demand,
'qty_left': qty_available - qty_demand,
}
data[move.id] = line_data
return data
def _get_main_availability_state(self, data):
if self.picking_mo_ids.filtered(
lambda r: r.state not in ('draft', 'cancel')):
return 'released'
if not data:
return 'none'
if len(data) == len([i for i in data.values() if i['qty_left'] < 0]):
return 'waiting'
if len(data) == len(
[i for i in data.values() if i['qty_left'] >= 0]):
return 'available'
return 'partially_available'
То есть _get_main_availability_state
используется для вычисления значения для упомянутого поля (main_availability
). Использованы данные, которые генерируются с использованием метода _prepare_main_availability
. Итак, вы можете увидеть, как эти значения вычисляются.
Теперь с поисковой частью, поскольку поле не сохраняется, нам нужно написать домен для этих вычисленных значений. Со значениями released
и none
это легко, потому что есть связь с mrp.production
. С другими значениями я не вижу никакой связи, поэтому я ищу все возможные записи и затем фильтрую их, что, как я уже говорил, плохо масштабируется.
Так кто-нибудь знает какие-либо идеи, если этот поиск может быть улучшен?
P.S. Если кому-то интересно, почему я пытаюсь вычислить, а не хранить это поле. Ранее я действительно сохранял их вместе с другими (имеющими некоторую дополнительную функциональность) записями модели, но это значительно замедлило резервирование производственного заказа (потому что при каждом резервировании необходимо будет заново создавать или перезаписывать эти записи). Теперь с этим новым подходом резервирование выполняется так же быстро, как и без моей функциональности, но поиск стал узким местом.