Соглашение об оформлении кода в Odoo

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

Структура модуля

Каталоги

Модуль организован из важных каталогов. Они содержат бизнес-логику; Взглянув на них, вы поймете цель модуля.

  • data/ : демо-данные и файлы данных xml
  • models/ : здесь определяются модели
  • controllers/ : содержит контроллеры (http маршруты)
  • views/ : содержит представления и шаблоны
  • static/ : содержит веб ассеты, подразделяется на подкаталоги по содержимому css/, js/, img/, lib/, …

Модуль содержит дополнительные необязательные каталоги.

  • wizard/: содержит временные модели (models.TransientModel) и их представления
  • report/: содержит печатные отчеты и модели, основанные на представлениях SQL. Python объекты и представления XML включены в этот каталог
  • tests/: содержит тесты Python

Наименование файлов

Наименование файлов важно для быстрого поиска информации во всех модулях odoo. В этом разделе объясняется, как называть файлы в стандартном модуле odoo. В качестве примера мы используем приложение plant nursery . Он содержит две основные модели plant.nursery и plant.order.

Что касается моделей, разделите бизнес-логику на наборы моделей, принадлежащих к одной и той же основной модели. Каждый набор находится в данном файле с именем, основанным на его главной модели. Если существует только одна модель, ее имя будет совпадать с именем модуля. Каждая унаследованная модель должна быть в отдельном файле, чтобы помочь понять задействованные модели.

addons/plant_nursery/
|-- models/
|   |-- plant_nursery.py (first main model)
|   |-- plant_order.py (another main model)
|   |-- res_partner.py (inherited Odoo model)

Что касается безопасности, прав доступа и правил, следует использовать два основных файла. Первый - это определение прав доступа в файле ir.model.access.csv. Группы пользователей определены в <module> _groups.xml. Правила доступа определены в <model>_security.xml.

addons/plant_nursery/
|-- security/
|   |-- ir.model.access.csv
|   |-- plant_nusery_groups.xml
|   |-- plant_nusery_security.xml
|   |-- plant_order_security.xml

Что касается представлений, то внутренние представления должны быть разделены как и модели и дополнены суффиксом _views.xml. Бэкэнд-представления: list, form, kanban, activity, graph, pivot и т.д. Для упрощения разделения по моделям в представлениях основные меню, не связанные с конкретными действиями, могут быть извлечены в необязательный файл <module>_menus.xml. Шаблоны (страницы QWeb, используемые, в частности, для отображения портала/веб-сайта) и бандлы (импорт ассетов JS и CSS) помещаются в отдельные файлы <model>_templates.xml и assets.xml.

addons/plant_nursery/
|-- views/
|   | -- assets.xml (import of JS / CSS)
|   | -- plant_nursery_menus.xml (optional definition of main menus)
|   | -- plant_nursery_views.xml (backend views)
|   | -- plant_nursery_templates.xml (portal templates)
|   | -- plant_order_views.xml
|   | -- plant_order_templates.xml
|   | -- res_partner_views.xml

Что касается данных, разделите их по назначению (демо или необходимые данные) и основной модели. Имена файлов будут именами main_model с суффиксом _demo.xml или _data.xml. Например, для приложения, имеющего демо и необходимые данные для своей основной модели, а также подтипы, действия и почтовые шаблоны, связанные с модулем mail:

addons/plant_nursery/
|-- data/
|   |-- plant_nursery_data.xml
|   |-- plant_nursery_demo.xml
|   |-- mail_data.xml

Что касается * контроллеров *, то, как правило, все контроллеры принадлежат одному контроллеру, который содержится в файле с именем <имя_модуля> .py. Старое соглашение в Odoo - называть этот файл main.py, но он считается устаревшим. Если вам нужно унаследовать существующий контроллер от другого модуля, сделайте это в <inherited_module_name>.py. Например, добавление контроллера портала в приложение будет portal.py.

addons/plant_nursery/
|-- controllers/
|   |-- plant_nursery.py
|   |-- portal.py (inheriting portal/controllers/portal.py)
|   |-- main.py (deprecated, replaced by plant_nursery.py)

Что касается статических файлов, файлы Javascript следуют той же логике, что и модели Python. Каждый компонент должен быть в своем собственном файле со значимым именем. Например, виджеты активности находятся в activity.js модуля mail. Подкаталоги также могут быть созданы для структурирования «пакета» (см. Веб-модуль для более подробной информации). Та же логика должна применяться к шаблонам виджетов JS (статические файлы XML) и к их стилям (файлы scss). Не создавайте ссылки на ресурсы (изображения, библиотеки) за пределами Odoo: не используйте URL-адрес изображения, вместо этого скопируйте его в код.

Что касается wizards, для них соглашение об именах такое же, как и для моделей на python: <transient> .py и <transient> _views.xml. Оба файла помещены в каталог wizard. Это наименование происходит от старых приложений odoo, использующих ключевое слово wizard для временных моделей.

addons/plant_nursery/
|-- wizard/
|   |-- make_plant_order.py
|   |-- make_plant_order_views.xml

Относительно *статистических отчетов , сделанных с представлениями Python/SQL и классическими представлениями, правило именование следующее:

addons/plant_nursery/
|-- report/
|   |-- plant_order_report.py
|   |-- plant_order_report_views.xml

Что касается печатных отчетов, которые содержат в основном подготовку данных и имена шаблонов Qweb, правила именования такие:

addons/plant_nursery/
|-- report/
|   |-- plant_order_reports.xml (report actions, paperformat, ...)
|   |-- plant_order_templates.xml (xml report templates)

Полное дерево каталогов должно выглядеть следующим образом

addons/plant_nursery/
|-- __init__.py
|-- __manifest__.py
|-- controllers/
|   |-- __init__.py
|   |-- plant_nursery.py
|   |-- portal.py
|-- data/
|   |-- plant_nursery_data.xml
|   |-- plant_nursery_demo.xml
|   |-- mail_data.xml
|-- models/
|   |-- __init__.py
|   |-- plant_nursery.py
|   |-- plant_order.py
|   |-- res_partner.py
|-- report/
|   |-- __init__.py
|   |-- plant_order_report.py
|   |-- plant_order_report_views.xml
|   |-- plant_order_reports.xml (report actions, paperformat, ...)
|   |-- plant_order_templates.xml (xml report templates)
|-- security/
|   |-- ir.model.access.csv
|   |-- plant_nusery_groups.xml
|   |-- plant_nusery_security.xml
|   |-- plant_order_security.xml
|-- static/
|   |-- img/
|   |   |-- my_little_kitten.png
|   |   |-- troll.jpg
|   |-- lib/
|   |   |-- external_lib/
|   |-- src/
|   |   |-- js/
|   |   |   |-- widget_a.js
|   |   |   |-- widget_b.js
|   |   |-- scss/
|   |   |   |-- widget_a.scss
|   |   |   |-- widget_b.scss
|   |   |-- xml/
|   |   |   |-- widget_a.xml
|   |   |   |-- widget_a.xml
|-- views/
|   |-- assets.xml
|   |-- plant_nursery_menus.xml
|   |-- plant_nursery_views.xml
|   |-- plant_nursery_templates.xml
|   |-- plant_order_views.xml
|   |-- plant_order_templates.xml
|   |-- res_partner_views.xml
|-- wizard/
|   |--make_plant_order.py
|   |--make_plant_order_views.xml

XML файлы

Формат

Чтобы объявить запись в XML, рекомендуется использовать запись record (с использованием <record>):

  • Поставьте атрибут id перед атрибутом model
  • При объявления поля первым ставиться атрибут name. Затем поместите значение поля в тег field затем атрибут eval и, наконец, другие атрибуты (widget, options, …), следующие в порядке важности.
  • Попробуйте сгруппировать записи по модели. В случае зависимостей между действием/меню/представлениями это соглашение может быть неприменимым.
  • Использовать соглашение об создании имен, определенное далее
  • Тег <data> используется только для установки не обновляемых данных с помощью noupdate = 1. Если в файле присутствуют только не обновляемые данные, для тега <odoo> можно установить noupdate = 1 и не устанавливать тег <data>.
<record id="view_id" model="ir.ui.view">
    <field name="name">view.name</field>
    <field name="model">object_name</field>
    <field name="priority" eval="16"/>
    <field name="arch" type="xml">
        <tree>
            <field name="my_field_1"/>
            <field name="my_field_2" string="My Label" widget="statusbar" statusbar_visible="draft,sent,progress,done" />
        </tree>
    </field>
</record>

Odoo поддерживает кастомные теги, действующие как синтаксический сахар:

  • menuitem: использовать его как ярлык для объявления ir.ui.menu
  • template: использовать его для объявления QWeb представления, требующего только раздел arch.
  • report: используется для объявления действия отчета
  • act_window: используется, если определение записи не может делать то, что вы хотите

The 4 first tags are prefered over the record notation.

XML ID и процедура именования

Безопасность, Представление и Действие

Используйте следующий шаблон:

  • Для меню: <model_name>_menu, или <model_name>_menu_do_stuff для вложенных элементов.
  • Для представления: <model_name>_view_<view_type>, где view_type может быть kanban, form, tree, search, …
  • Для действия: основное действие соответствует <model_name>_action. Остальные имеют суффикс _<detail>, где detail запись прописными буквами поясняющая действие. Суффиксы используются только для тех моделей, где есть объявление нескольких действий.
  • Для действий окон: добавьте суффикс имени действия к конкретной информации представления, например <model_name>_action_view_<view_type>.
  • Для группы пользователей: <model_name>_group_<group_name> где group_name является именем группы, как правило user, manager, …
  • Для правила безопасности: <model_name>_rule_<concerned_group> где concerned_group короткое имя соответствующей группы (user для model_name_group_user, public для неавторизованных пользователей, company для систем с множеством компаний, …).

Имя должно быть идентично идентификатору xml с точками, заменяющими подчеркивание. Действия должны иметь реальное имя, поскольку оно используется в качестве отображаемого имени.

<!-- views  -->
<record id="model_name_view_form" model="ir.ui.view">
    <field name="name">model.name.view.form</field>
    ...
</record>

<record id="model_name_view_kanban" model="ir.ui.view">
    <field name="name">model.name.view.kanban</field>
    ...
</record>

<!-- actions -->
<record id="model_name_action" model="ir.act.window">
    <field name="name">Model Main Action</field>
    ...
</record>

<record id="model_name_action_child_list" model="ir.actions.act_window">
    <field name="name">Model Access Childs</field>
</record>

<!-- menus and sub-menus -->
<menuitem
    id="model_name_menu_root"
    name="Main Menu"
    sequence="5"
/>
<menuitem
    id="model_name_menu_action"
    name="Sub Menu 1"
    parent="module_name.module_name_menu_root"
    action="model_name_action"
    sequence="10"
/>

<!-- security -->
<record id="module_name_group_user" model="res.groups">
    ...
</record>

<record id="model_name_rule_public" model="ir.rule">
    ...
</record>

<record id="model_name_rule_company" model="ir.rule">
    ...
</record>

Наследование XML

XML-идентификаторы наследующих представлений должны использовать тот же идентификатор, что и исходная запись. Это помогает найти все наследуемые объекты с первого взгляда. Поскольку конечные идентификаторы Xml имеют префикс модуля, который их создает, дублирования значений нет.

Именование должно содержать суффикс .inherit.{details} для облегчения понимания целевого объекта переопределения при взгляде на его имя.

<record id="model_view_form" model="ir.ui.view">
    <field name="name">model.view.form.inherit.module2</field>
    <field name="inherit_id" ref="module1.model_view_form"/>
    ...
</record>

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

<record id="module2.model_view_form" model="ir.ui.view">
    <field name="name">model.view.form.module2</field>
    <field name="inherit_id" ref="module1.model_view_form"/>
    <field name="mode">primary</field>
    ...
</record>

Python

Параметры PEP8

Использование linter может помочь в подсветке синтаксиса, семантических предупреждений или ошибок. Исходный код Odoo пытается уважать стандарты Python, но некоторые из них можно игнорировать.

  • E501: слишком длинная строка
  • E301: ожидается 1 пустая строка, найдено 0
  • E302: ожидается 2 пустые строки, найдено 1

Import

Порядок импорта библиотек определен следующим образом

  1. Внешние библиотеки (по одной в строке, отсортированные и разделенные в python stdlib)
  2. Импорт самого odoo
  3. Импорт из модулей Odoo (редко, и только при необходимости)

Внутри этих 3-х групп строки сортируются по алфавиту.

# 1 : imports of python lib
import base64
import re
import time
from datetime import datetime
# 2 : imports of odoo
import odoo
from odoo import api, fields, models, _ # alphabetically ordered
from odoo.tools.safe_eval import safe_eval as eval
# 3 : imports from odoo addons
from odoo.addons.website.models.website import slug
from odoo.addons.web.controllers.main import login_redirect

Идиоматика программирования (Python)

  • Каждый файл python должен иметь # -*- coding: utf-8 -*- в первой строке.
  • Всегда предпочитайте читаемость над краткостью или использование языковых функций или идиом.
  • Не используйте ``.clone()` `
# bad
new_dict = my_dict.clone()
new_list = old_list.clone()
# good
new_dict = dict(my_dict)
new_list = list(old_list)
  • Python словари: создание и обновление
# -- creation empty dict
my_dict = {}
my_dict2 = dict()

# -- creation with values
# bad
my_dict = {}
my_dict['foo'] = 3
my_dict['bar'] = 4
# good
my_dict = {'foo': 3, 'bar': 4}

# -- update dict
# bad
my_dict['foo'] = 3
my_dict['bar'] = 4
my_dict['baz'] = 5
# good
my_dict.update(foo=3, bar=4, baz=5)
my_dict = dict(my_dict, **my_dict2)
  • Используйте осмысленные имена переменных/классов/методов
  • Бесполезная переменная: временные переменные могут сделать код более понятным, указав имена для объектов, но это не означает, что вам нужно постоянно создавать временные переменные:
# pointless
schema = kw['schema']
params = {'schema': schema}
# simpler
params = {'schema': kw['schema']}
  • Несколько return это нормально в тех случаях, когда это делает код проще
# a bit complex and with a redundant temp variable
def axes(self, axis):
        axes = []
        if type(axis) == type([]):
                axes.extend(axis)
        else:
                axes.append(axis)
        return axes

 # clearer
def axes(self, axis):
        if type(axis) == type([]):
                return list(axis) # clone the axis
        else:
                return [axis] # single-element list
  • Знайте свои встроенные функции: вы должны по крайней мере иметь общее представление о всех стандартных функциях Python (http://docs.python.org/library/functions.html)
value = my_dict.get('key', None) # very very redundant
value = my_dict.get('key') # good

Кроме того, if 'key' in my_dict и if my_dict.get('key') имеют разное значение, убедитесь, что используете правильное.

  • Изучите списки: Использование возможностей списков, словарей базовые манипуляции с помощью map, filter, sum, … Они делают код более легким для чтения.
# not very good
cube = []
for i in res:
        cube.append((i['id'],i['name']))
# better
cube = [(i['id'], i['name']) for i in res]
  • Коллекции также булевы: в python многие объекты имеют значение типа «boolean-ish» при оценке в логическом контексте (например, if). Среди них есть коллекции (списки, словари, наборы, …), которые являются «falsy», когда они пусты и «truthy», когда содержат элементы:
bool([]) is False
bool([1]) is True
bool([False]) is True

Таким образом, вы можете написать if some_collection: вместо if len (some_collection):.

  • Перебор итерируемых объектов
# creates a temporary list and looks bar
for key in my_dict.keys():
        "do something..."
# better
for key in my_dict:
        "do something..."
# accessing the key,value pair
for key, value in my_dict.items():
        "do something..."
  • Используйте dict.setdefault
# longer.. harder to read
values = {}
for element in iterable:
    if element not in values:
        values[element] = []
    values[element].append(other_value)

# better.. use dict.setdefault method
values = {}
for element in iterable:
    values.setdefault(element, []).append(other_value)
  • Как хороший разработчик, документируйте свой код (docstring по методам, простые комментарии для сложной части кода)
  • В дополнение к этим рекомендациям вы также можете найти следующую ссылку интересную: http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html (немного устаревшая, но весьма актуальная)

Программирование в Odoo

  • Избегайте создания генераторов и декораторов: используйте только те, что предусмотрены API Odoo.
  • Как и в python, используйте методы filtered, mapped, sorted, …, чтобы облегчить чтение кода и производительность.

Убедитесь, что ваш метод работает в пакетном режиме

When adding a function, make sure it can process multiple records. Typically, such methods are decorated with the api.multi decorator. Then you will have to iterate on self to treat each record.

@api.multi
def my_method(self)
    for record in self:
        record.do_cool_stuff()

Avoid to use api.one decorator : this will probably not do what you expected, and extending a such method is not as easy than a api.multi method, since it returns a list of result (ordered by recordset ids).

For performance issue, when developping a „stat button“ (for instance), do not perform a search or a search_count in a loop in a api.multi method. It is recommended to use read_group method, to compute all value in only one request.

@api.multi
def _compute_equipment_count(self):
""" Count the number of equipement per category """
    equipment_data = self.env['hr.equipment'].read_group([('category_id', 'in', self.ids)], ['category_id'], ['category_id'])
    mapped_data = dict([(m['category_id'][0], m['category_id_count']) for m in equipment_data])
    for category in self:
        category.equipment_count = mapped_data.get(category.id, 0)

Распространение контекста

Контекст - это frozendict, который нельзя изменить. Для вызова метода с другим контекстом должен использоваться метод with_context:

records.with_context(new_context).do_stuff() # all the context is replaced
records.with_context(**additionnal_context).do_other_stuff() # additionnal_context values override native context ones

Если вам нужно создать ключ контекста, влияющий на поведение какого-либо объекта, выберите хорошее имя а лучше присвойте префикс с именем модуля, чтобы изолировать его влияние. Хорошим примером являются ключи модуля mail: mail_create_nosubscribe, mail_notrack*, mail_notify_user_signature, …

Не обходите ORM

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

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

# very very wrong
self.env.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 self.env.cr.fetchall()]

# no injection, but still wrong
self.env.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 self.env.cr.fetchall()]

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

Никаких инъекций SQL, пожалуйста!

Следует проявлять осторожность, чтобы не вводить уязвимости SQL-инъекций при использовании запросов SQL вручную. Уязвимость появляется, когда пользовательский ввод либо неправильно фильтруется или содержит неверные кавычки, что позволяет злоумышленнику вводить нежелательные предложения в SQL-запрос (например, обходить фильтры или выполнять команды UPDATE или DELETE).

Самый лучший способ сохранять безопасность - никогда, НИКОГДА не использовать конкатенацию строковых констант (+) или строковых параметров 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
self.env.cr.execute('SELECT distinct child_id FROM account_account_consol_rel ' +
           'WHERE parent_id IN ('+','.join(map(str, ids))+')')

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

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

Вот незабываемый пример, который поможет вам вспомнить, в чем проблема (но не копируйте код там). Прежде чем продолжить, обязательно прочтите онлайн-документацию pyscopg2, чтобы узнать, как правильно ее использовать:

Думайте «расширяемо»

Функции и методы не должны содержать слишком много логики: иметь много маленьких и простых методов лучше, чем иметь несколько больших и сложных методов. Хорошее эмпирическое правило состоит в том, чтобы разбить метод,как только на него возложено несколько обязанностей (см. http://en.wikipedia.org/wiki/Single_responsibility_principle) - он слишком велик, чтобы уместиться на одном экране.

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

# do not do this
# modifying the domain or criteria implies overriding whole method
def action(self):
    ...  # long method
    partners = self.env['res.partner'].search(complex_domain)
    emails = partners.filtered(lambda r: arbitrary_criteria).mapped('email')

# better but do not do this either
# modifying the logic forces to duplicate some parts of the code
def action(self):
    ...
    partners = self._get_partners()
    emails = partners._get_emails()

# better
# minimum override
def action(self):
    ...
    partners = self.env['res.partner'].search(self._get_partner_domain())
    emails = partners.filtered(lambda r: r._filter_partners()).mapped('email')

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

Кроме того, называйте свои функции следующим образом: маленькие и правильно названные функции являются отправной точкой кода для чтения/сопровождения и более жесткой документации.

Эта рекомендация также относится к классам, файлам, модулям и пакетам. (См. Также http://en.wikipedia.org/wiki/Cyclomatic_complexity)

Никогда не совершайте транзакции

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

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(), является очень высокая вероятность краха системы по разным причинам, т.к. вы будете совершать частичные коммиты и, следовательно, частичные и грязные откаты, вызывая следом другие проблемы:

  1. несогласованные бизнес-данные, обычно потеря данных
  2. автоматическая десинхронизация рабочего процесса, постоянно застрявшие документы
  3. тесты, которые нельзя откатить чисто, и они начнут загрязнять базу данных, и инициируют ошибку (это так, даже если во время транзакции не возникла ошибка)
Вот самое простое правило:

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

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

И вопреки распространенному мнению, вам даже не нужно вызывать метод cr.commit() в следующих ситуациях: - в методе _auto_init() объекта models.Model: это сделано с помощью механизма инициализации модулей или транзакцией ORM при создании пользовательских моделей - в отчетах: commit() обрабатывается фреймворком, так что вы можете обновлять базу данных даже изнутри отчета - в пределах Models.Transient: эти методы называются точно так же, как и обычные models.Transient, внутри транзакции и с соответствующим cr.commit()/rollback() в конце - и т.д. (См. Общее правило Выше, если у вас есть сомнения!)

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

Правильно используйте метод перевода

В Odoo используется похожий на GetText метод под названием «underscore» _(), чтобы указать, что статическая строка, используемая в коде, должна быть переведена во время выполнения, используя язык контекста. Этот псевдо-метод доступен в вашем коде путем импорта:

from odoo import _

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

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

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

# 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

Вообще в Odoo, когда при работе со строками, предпочитаете % `` вместо.format()`` (когда только одна переменная заменяется в строке), и старайтесь использовать ключевые занчения ``%(varname) `` вместо позиционных ( Когда необходимо заменить несколько переменных). Это облегчает работу переводчиков сообщества.

Символы и обозначения

  • Название модели (с использованием dot-нотации, префикс по имени модуля):
    • При определении модели Odoo: используйте форму единственного числа (res.partner и sale.order вместо res.partnerS и saleS.orderS)
    • При определении временной модели Odoo (wizard): используйте <related_base_model>.<action> где related_base_model - базовая модель (определенная в models/), относящаяся ко временной модели, а action - это короткое имя того, что делает данная временная модель. Например: account.invoice.make, project.task.delegate.batch, …
    • При определении модели report (SQL представление): используйте <related_base_model>.report.<action>, исходя из соглашения для временных моделей.
  • Odoo Python Class : используйте camelcase (Object-oriented стиль).
class AccountInvoice(models.Model):
    ...
  • Имя переменной:
    • Используйте camelcase при определении переменных в модели
    • Используйте нижний регистр и подчеркивание при определении обычных переменных.
    • добавьте суффикс имени вашей переменной _id или _ids, если он содержит id записи или их список. Не используйте partner_id для хранения записи res.partner
Partner = self.env['res.partner']
partners = Partner.browse(ids)
partner_id = partners[0].id
  • Поля One2Many и Many2Many всегда должны иметь _ids в качестве суффикса (например: sale_order_line_ids)
  • Поля Many2One должны иметь _id в качестве суффикса (пример: partner_id, user_id, …)
  • Соглашения о методах
    • Compute Field : паттерн метода вычисляемого поля _compute_<field_name>
    • Search method: паттерн метода поиска _search_<field_name>
    • Default method: паттерн метода по умолчанию _default_<field_name>
    • Selection method: паттерн метода выбора _selection_<field_name>
    • Onchange method: паттерн метода onchange _onchange_<field_name>
    • Constraint method : паттерн метода ограничения _check_<constraint_name>
    • Action method : an object action method is prefix with action_. Its decorator is @api.multi, but since it use only one record, add self.ensure_one() at the beginning of the method.
  • В атрибутах модели данных порядок должен быть следующим
    1. Приватные атрибуты (_name, _description, _inherit, …)
    2. Метод по умолчанию и _default_get
    3. Объявления полей
    4. Методы поиска и вычисления и инверсии. Они должны идти в том же порядке, что и объявленные поля
    5. Метод выбора (методы, используемые для возврата вычисленных значений для полей selection)
    6. Методы ограничений (@api.constrains) и onchange методы (@api.onchange)
    7. Методы CRUD (переопределения ORM)
    8. Методы действий
    9. И, наконец, другие методы, описывающие бизнес-логику.
class Event(models.Model):
    # Private attributes
    _name = 'event.event'
    _description = 'Event'

    # Default methods
    def _default_name(self):
        ...

    # Fields declaration
    name = fields.Char(string='Name', default=_default_name)
    seats_reserved = fields.Integer(oldname='register_current', string='Reserved Seats',
        store=True, readonly=True, compute='_compute_seats')
    seats_available = fields.Integer(oldname='register_avail', string='Available Seats',
        store=True, readonly=True, compute='_compute_seats')
    price = fields.Integer(string='Price')
    event_type = fields.Selection(string="Type", selection='_selection_type')

    # compute and search fields, in the same order of fields declaration
    @api.multi
    @api.depends('seats_max', 'registration_ids.state', 'registration_ids.nb_register')
    def _compute_seats(self):
        ...

    @api.model
    def _selection_type(self):
        return []

    # Constraints and onchanges
    @api.constrains('seats_max', 'seats_available')
    def _check_seats_limit(self):
        ...

    @api.onchange('date_begin')
    def _onchange_date_begin(self):
        ...

    # CRUD methods (and name_get, name_search, ...) overrides
    def create(self, values):
        ...

    # Action methods
    @api.multi
    def action_validate(self):
        self.ensure_one()
        ...

    # Business methods
    def mail_user_confirm(self):
        ...

Javascript и CSS

Организация статических файлов

У модулей Odoo есть соглашения о том, как структурировать различные файлы. Здесь мы объясняем более подробно, как веб ассеты должны быть организованы.

Первое, что нужно знать, это то, что сервер Odoo будет обслуживать (статически) все файлы, находящиеся в папке static/, но с префиксом имени дополнения. Так, например, если файл находится в*addons/web/static/src/js/some_file.js*, он будет статически доступен по адресу your-odoo-server.com/web/static/src/js/some_file.js

Соглашение состоит в том, чтобы организовать код в соответствии со следующей структурой:

  • static: здесь находятся все статические файлы

    • static/lib: это место, где должны находиться js-библиотеки. Так, например, все файлы из библиотеки jquery находятся в addons/web/static/lib/jquery
    • static/src: общая папка с исходным кодом

      • static/src/css: все css файлы
      • static/src/fonts
      • static/src/img
      • static/src/js
      • static/src/scss: scss файлы
      • static/src/xml: все qweb шаблоны, которые будут отрендерены JS
    • static/tests: все файлы относящиеся к тестам.

Соглашения JavaScript

  • use strict; рекомендуется использовать во всех файлах javascript
  • Используйте linter (jshint, …)
  • Никогда не добавляйте минифицированные библиотеки Javascript
  • Используйте camelcase для объявления класса

Более точные рекомендации JS подробно описаны в github wiki. Вы также можете взглянуть на существующий API в Javascript, просмотрев ссылки Javascript в Odoo.

Рекомендации написания CSS

  • Префикс всех ваших классов с o_<module_name> где module_name - техническое имя модуля («sale», «im_chat», …) или основной url-маршрут, зарезервированный модулем (для модуля сайта главным образом, т.е. : «o_forum» для модуля website_forum). Единственным исключением для этого правила является webclient: он просто использует префикс o_.
  • Избегайте использования id тега
  • Используйте родные классы Bootstrap
  • Используйте нижний регистр для обозначения класса

Git

Настройте ваш git

Основываясь на опыте предков и устной традиции, следующие вещи имеют большое значение для того, чтобы сделать ваши коммиты более полезными:

  • Обязательно укажите и user.email, и user.name в вашей локальной конфигурации git

    git config --global <var> <value>
    
  • Не забудьте добавить свое полное имя в свой профиль Github здесь. Пожалуйста, чувствуйте себя комфортно и добавьте свою команду, аватар, свою любимую цитату и еще много чего ;-)

Структура месседжей в коммитах

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

[TAG] module: describe your change in a short sentence (ideally < 50 chars)

Long version of the change description, including the rationale for the change,
or a summary of the feature being introduced.

Please spend a lot more time describing WHY the change is being done rather
than WHAT is being changed. This is usually easy to grasp by actually reading
the diff. WHAT should be explained only if there are technical choices
or decision involved. In that case explain WHY this decision was taken.

End the message with references, such as task or bug numbers, PR numbers, and
OPW tickets, following the suggested format:
task-123 (related to task)
Fixes #123  (close related issue on Github)
Closes #123  (close related PR on Github)
opw-123 (related to ticket)

Тэг и имя модуля

Теги используются для префикса вашего коммита. Они должны быть из этого списка

  • [FIX] для исправления ошибок: в основном используется в стабильной версии, но также действует, если вы исправляете недавнюю ошибку в версии для разработчиков;
  • [REF] для рефакторинга: когда объект сильно переписан;
  • [ADD] для добавления новых модулей;
  • [REM] для удаления ресурсов: удаление мертвого кода, удаление представлений, удаление модулей, …;
  • **[REV]**для отмены коммита: если коммит вызывает проблемы или нежелателен, то откат выполняется с помощью этого тега;
  • [MOV] для перемещения файлов (не меняйте содержимое перемещенного файла, иначе Git потеряет след, а история будет потеряна!), Или просто переместите код из одного файла в другой;
  • [REL] для коммитов релиза: новые мажорные или минорные стабильные версии;
  • [IMP] для улучшений: большинство изменений, внесенных в разрабатываемую версию, являются постепенными улучшениями, не связанными с другим тегом;
  • [MERGE] для коммитов слияния: используется в прямом порте исправлений ошибок, а также как основной коммит для функции, включающей несколько отдельных коммитов;
  • [CLA] для подписи индивидуальной авторской лицензии Odoo;
  • [I18N] для изменений в файлах перевода;

После тега становится измененным именем модуля. Используйте техническое имя, так как функциональное имя может меняться со временем. Если несколько модулей изменены, перечислите их или используйте различные, чтобы чтобы рассказать, что это кросс-модули. Если это действительно не требуется то избегайте модификации кода в нескольких модулями в одном коммите. Это затрудняет понимание истории модуля.

Заголовок сообщения коммита

После тега и имени модуля следует содержательный заголовок сообщения о коммите. Он должен быть самоочевидным и включать причину изменения. Не используйте отдельные слова, такие как «bugfix» или «улучшения». Попробуйте ограничить длину заголовка примерно до 50 символов для удобства чтения.

Заголовок сообщения коммита должен составлять правильное предложение после объединения с если применяется, этот коммит будет <header>. Например, [IMP] base: предотвращает архивирование пользователей, связанных с активными партнерами, является правильным, поскольку является правильным предложением если оно применяется, этот коммит не позволит пользователям архивировать ....

Полное описание месседжа коммита

В описании сообщения укажите часть кода, на которую повлияли ваши изменения (имя модуля, lib, transversal object, …) и описание изменений.

Сначала объясните, ПОЧЕМУ вы модифицируете код. Что важно, если кто-то вернется к вашему коммиту через 4 десятилетия (или 3 дня), это то, почему вы это сделали. Указывайте цель изменений.

То, что именно вы сделали, можно найти в самом коммите. Если были какие-то технические решения, было бы неплохо объяснить это также в сообщении к коммиту после объяснения почему. Для разработчиков Odoo R&D «команда ПО попросила меня сделать это», не вполне понятно почему, кстати.

Please avoid commits which simultaneously impact multiple modules. Try to split into different commits where impacted modules are different. It will be helpful if we need to revert changes in a given module separately.

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

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

Если вы являетесь разработчиком Odoo R&D, ПОЧЕМУ должно быть целью задачи, над которой вы работаете. Полные спецификации составляют ядро сообщения коммита. Если вы работаете над задачей, которая не имеет цели и спецификаций, пожалуйста, перед тем как продолжить, проясните их.

Наконец, вот несколько примеров правильных сообщений коммита:

[REF] models: use `parent_path` to implement parent_store

 This replaces the former modified preorder tree traversal (MPTT) with the
 fields `parent_left`/`parent_right`[...]

[FIX] account: remove frenglish

 [...]

 Closes #22793
 Fixes #22769

[FIX] website: remove unused alert div, fixes look of input-group-btn

 Bootstrap's CSS depends on the input-group-btn
 element being the first/last child of its parent.
 This was not the case because of the invisible
 and useless alert.