Odoo 11: повышение производительности вычисляемого метода поиска (когда нет прямой связи)? - PullRequest
0 голосов
/ 13 сентября 2018

Я наткнулся на проблему, возникающую при реализации поиска по вычисляемому полю, в том, что в определенных случаях поиск по вычисляемому полю не имеет прямой связи (насколько я знаю), что означает, что я не могу написать домен, чтобы найти то, что вычисляется.

В этом случае мне пришлось по умолчанию выполнить очень неэффективный поиск, где я ищу все записи и затем фильтрую соответственно.

Поскольку я знал, что этот поиск плохо масштабируется, я провел тест с ~ 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. Если кому-то интересно, почему я пытаюсь вычислить, а не хранить это поле. Ранее я действительно сохранял их вместе с другими (имеющими некоторую дополнительную функциональность) записями модели, но это значительно замедлило резервирование производственного заказа (потому что при каждом резервировании необходимо будет заново создавать или перезаписывать эти записи). Теперь с этим новым подходом резервирование выполняется так же быстро, как и без моей функциональности, но поиск стал узким местом.

...