This is the documentation for older versions of Odoo (formerly OpenERP).

See the new Odoo user documentation.

See the new Odoo technical documentation.

2   Рекомендации, специфичные для OpenERP

2.1   Bazaar — ваш историк

Не используйте комментарии для отключения кода. Код управляется системой контроля версий Bazaar, которая, вне зависимости от вашего мнения, не потеряет историю файла.

Оставленные комментарии делают код смешанным и хуже читаемым. И не волнуйтесь о том, чтобы сделать ваши изменения очевидными. Для этого есть diff:

# no example for this one, code was really removed ;-)

2.2   Называйте рыбу рыбой

Give your variables a meaningful name all the time. You may know what it is referring to now, but you won't in 2 months, and others don't either. One-letter variables are acceptable only in lambda expressions and loop indices, or perhaps in pure maths expressions (and even there it doesn't hurt to use a real name):

# unclear and misleading
a = {}
ffields = {}

# better
results = {}
selected_fields = {}

2.3   Не пренебрегайте ORM

Вы ни в коем случае не должны использовать указатель базы данных напрямую, когда ORM может сделать то же самое! Поступая так, вы обходите все функции ORM, например контроль транзакций, прав доступа и так далее.

Существует также возможность, что Вы делаете код менее читаемым и возможно менее безопасным (смотрите также следующую рекомендацию):

# very very wrong
cr.execute('select id from auction_lots where auction_id in (' +
           ','.join(map(str,ids))+') and state=%s and obj_price>0',
           ('draft',))
auction_lots_ids = [x[0] for x in cr.fetchall()]

# no injection, but still wrong
cr.execute('select id from auction_lots where auction_id in %s '\
           'and state=%s and obj_price>0',
           (tuple(ids),'draft',))
auction_lots_ids = [x[0] for x in cr.fetchall()]

# better
auction_lots_ids = self.search(cr,uid,
                               [('auction_id','in',ids),
                                ('state','=','draft'),
                                ('obj_price','>',0)])

2.4   Пожалуйста, не надо SQL-инъекций!

Care must be taken not to introduce SQL injections vulnerabilities when using manual SQL queries. The vulnerability is present when user input is either incorrectly filtered or badly quoted which allow an attacker to introduce undesirable clauses to a SQL query (such as circumventing filters or executing UPDATE or DELETE commands).

Лучший путь к безопасности здесь — НИКОГДА не использовать конкатенацию строк Python (+) или интерполяцию строковых параметров (%) для передачи переменных в SQL-запрос.

Вторая, не менее важная причина, состоит в том, что задача форматирования параметров запроса лежит на слое абстракции базы данных (psycopg2), а не на вас. Например, psycopg2 известно, что, когда вы передаёте список значений, их надо представить в виде списка, разделённого запятыми и заключённого в скобки.

# the following is very bad:
#   - it's a SQL injection vulnerability
#   - it's unreadable
#   - it's not your job to format the list of ids
cr.execute('select distinct child_id from account_account_consol_rel ' +
           'where parent_id in ('+','.join(map(str, ids))+')')

# better
cr.execute('SELECT DISTINCT child_id '\
           'FROM account_account_consol_rel '\
           'WHERE parent_id IN %s',
           (tuple(ids),))

Это действительно очень важно, так что будьте внимательны при рефакторинге и не копируйте эти паттерны!

Вот памятный пример, который поможет вам запомнить, в чём там проблема (но не копируйте оттуда код).

Перед тем как продолжить, удостоверьтесь, что прочитали документацию pyscopg2 и научились правильно его использовать:

2.5   Выносите код

Если вы пишете один и тот же код снова и снова, и он длиннее одной строчки, вам стоит вынести его в отдельную функцию или метод:

# after writing this multiple times:
terp = get_module_resource(module, '__openerp__.py')
if not os.path.isfile(terp):
   terp = addons.get_module_resource(module, '__terp__.py')
   info = eval(tools.file_open(terp).read())

# make a function out of it
def _load_information_from_description_file(module):
    for filename in ['__openerp__.py', '__terp__.py']:
        description_file = addons.get_module_resource(module, filename)
        if os.path.isfile(description_file):
            return eval(tools.file_open(description_file).read())
    raise Exception('The module %s does not contain a description file!')

2.6   Неизвестный контекст

Не используйте необязательные объекты в качестве значений по умолчанию для функций. Они создаются константами (значение присваивается лишь однажды), так что можно с некоторой вероятностью получить сайд-эффект, если где-то придётся изменить его значение. Типовой пример в этом случае — аргумент context всех методов ORM:

# bad, this could have side-effects
def spam(eggs, context={}):
   setting = context.get('foo')
   #...

# this is better if your need to use the context
def spam(eggs, context=None):
   if context is None:
      context = {}
   setting = context.get('foo')
   #...

Также проявляйте осторожность с булевыми проверками списков и соответствий, потому что пустой словарь, список или кортеж будут восприняты как False:

# bad, you shadow the original context if it's empty
def spam(eggs, context=None):
   if not context:
      context = {}
   setting = context.get('foo')
   #...

Нормальным будет при передаче значения указать None и позволить вызываемому коду разбираться с этим:

# fine
def spam(eggs, context=None):
    setting = get_setting(True, context=context)

См. также ошибку 525808 на Launchpad.

2.7   Порой есть что-то лучше lambda-выражений

Вместо написания тривиальных lambda-выражений для вытаскивания элементов или атрибутов из списка или структуры данных, научиться воспринимать списки или конструкции operator.itemgetter и operator.attrgetter которые зачастую более читаемы и работают быстрее:

# not very readable
partner_tuples = map(lambda x: (x['id'], x['name']), partners)

# better with list comprehension for just one item/attribute
partner_ids = [partner['id'] for partner in partners]

# better with operator for many items/attributes
from operator import itemgetter
# ...
partner_tuples = map(itemgetter('id', 'name'), partners)

См. также http://docs.python.org/library/operator.html#operator.attrgetter

Начиная с версии 6.0 вы также можете использовать буквенные значения в качестве столбцов ORM, что означает, что можно прекратить писать подобное:

# lots of trivial one-liners in 5.0
_defaults = {
    'active': lambda *x: True,
    'state': lambda *x: 'draft',
}

# much simpler as of 6.0
_defaults = {
    'active': True,
    'state': 'draft',
}

Предупреждение

Будьте с этим осторожны, потому что не вызываемые умолчания принимают значение лишь однажды! Если вы хотите генерировать новые значения по-умолчанию для каждой записи — придётся оставить все lambda или сделать их вызываемыми.

Самая частая ошибка — это отметки времени, как в следующем примере:

# This will always give the server start time!
_defaults = {
    'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'),
}

# You need to keep it callable, e.g:
_defaults = {
    'timestamp': lambda *x: time.strftime('%Y-%m-%d %H:%M:%S'),
}

2.8   По возможности, сохраняйте методы короткими/простыми

Функции и методы не должны содержать множество логики. Наличие большого количества маленьких методов более рекомендуется, чем наличие нескольких больших и сложных методов. Хорошее правило — делить метод как только:

  • у него возникло более одной обязанности (см. http://ru.wikipedia.org/wiki/Принцип_единственной_обязанности)

  • он слишком велик, чтобы поместиться на экране.

Также, именуйте свои функции в соответствии с принципом что небольшие и правильно поименованные функции — отправная точка к читаемому и легко обслуживаемому коду и понятной документации.

Эта рекомендация также касается и классов, файлов, модулей и пакетов. (См. http://ru.wikipedia.org/wiki/Цикломатическая_сложность)

2.9   Никогда не подтверждайте транзакции

The OpenERP/OpenObject framework is in charge of providing the transactional context for all RPC calls. The principle is that a new database cursor is opened at the begining of each RPC call, and commited when the call has returned, just before transmitting the answer to the RPC client, approximately like this:

def execute(self, db_name, uid, obj, method, *args, **kw):
    db, pool = pooler.get_db_and_pool(db_name)
    # create transaction cursor
    cr = db.cursor()
    try:
        res = pool.execute_cr(cr, uid, obj, method, *args, **kw)
        cr.commit() # all good, we commit
    except Exception:
        cr.rollback() # error, rollback everything atomically
        raise
    finally:
        cr.close() # always close cursor opened manually
    return res

Если произойдёт какая-то ошибка во время выполнения вызова RPC, транзакция автоматически откатывается, фиксируя состояние системы.

Похожим образом система предоставляет отдельные транзакции на время выполнения наборов тестов, так что они могут быть возвращены в изначальное состояние вне зависимости от параметров запуска сервера.

Проблема заключается в том, что если вручную вызвать cr.commit() в каком-либо месте — очень велик шанс, что вы сломаете систему каким-либо способом, потому что вызов спровоцирует частичное подтверждение изменений и следовательно частичные и неверные схемы отката, помимо прочего вызывающие:

  • неконсистентность бизнес-данных, потерю данных;

  • рассинхронизацию рабочих потоков, простой документооборота;

  • невозможность откатить проверки, которые начнут портить базу данных и вызывать, обработчики ошибок (это так, даже если во время транзакции ошибок не произошло);

Вот очень простое правило:

Предупреждение

Вы НИКОГДА не должны вызывать cr.commit() сами, если не создали свой собственный указатель на базу данных! Ситуации в которых вам это понадобится — исключительные!

И кстати, если вы создали свой указатель, тогда вам надо обрабатывать ошибки и правильно откатывать транзакции, кроме того, чтобы правильно закрывать указатель, когда работа сделана.

И в пику бытующему мнению, вы никогда не должны вызывать cr.commit() в следующих ситуациях:

  • в методе _auto_init() объекта osv.osv: это забота модуля дополнений модуля инициализации, или транзакции ORM (при создании своих модулей);

  • в отчётах: commit() также обрабатывается средой, так что вы можете обновлять базу данных даже из самого отчёта;

  • в методах osv.osv_memory: эти методы вызываются, как и обычный osv.osv вместе с транзакцией и обязательными cr.commit() и rollback() в конце;

  • и т.д. (см. общее правило, если есть сомнения!)

И ещё одно очень простое правило:

Предупреждение

Все вызовы cr.commit(), исходящие не из рабочей среды сервера должны иметь однозначный комментарий, поясняющий, почему эти вызовы необходимы, почему они корректны и почему они не ломают механизм транзакций. В противном случае они будут удалены!

2.10   Корректно используйте методы gettext

OpenERP использует GetText-подобный метод, названные "подчёркивание" _( ), показывающий, что строка в исходном коде должна быть переведена во время выполнения на язык, указанный в контексте. Этот псевдо-метод доступен в вашем коде при импорте этой командой:

from tools.translate import _

Небольшое количество важных правил должно соблюдаться при его использовании, чтобы он работал и чтобы избежать замусоривания переводов.

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

Правило очень простое: вызовы метода подчёркивания должны всегда иметь форму _('строка символов') и ничего больше:

# Good: plain strings
error = _('This record is locked!')

# Good: strings with formatting patterns included
error = _('Record %s cannot be modified!') % record

# OK too: multi-line literal strings
error = _("""This is a bad multiline example
             about record %s!""") % record
error = _('Record %s cannot be modified' \
          'after being validated!') % record

# BAD: tries to translate after string formatting
#      (pay attention to brackets!)
# This does NOT work and messes up the translations!
error = _('Record %s cannot be modified!' % record)

# BAD: dynamic string, string concatenation, etc are forbidden!
# This does NOT work and messes up the translations!
error = _("'" + que_rec['question'] + "' \n")

# BAD: field values are automatically translated by the framework
# This is useless and will not work the way you think:
error = _("Product %s is out of stock!") % _(product.name)
# and the following will of course not work as already explained:
error = _("Product %s is out of stock!" % product.name)

# BAD: field values are automatically translated by the framework
# This is useless and will not work the way you think:
error = _("Product %s is not available!") % _(product.name)
# and the following will of course not work as already explained:
error = _("Product %s is not available!" % product.name)

# Instead you can do the following and everything will be translated,
# including the product name if its field definition has the
# translate flag properly set:
error = _("Product %s is not available!") % product.name

Помните также, что переводчики должны будут работать с буквенными значениями, которые передаются функции подчёркивания, так что пожалуйста, облегчите для них восприятие и оставляйте минимум спец. символов или знаков форматирования. Переводчики предупреждены, что паттерны форматирования, такие как %s или %d, переводы строк и т.д. должны сохраняться, но важно использоваться их только в понятной и очевидной форме:

# Bad: makes the translations hard to work with
error = "'" + question + _("' \nPlease enter an integer value ")

# Better (pay attention to position of the brackets too!)
error = _("Answer to question %s is not valid.\n" \
          "Please enter an integer value.") % question