Інструкції з кодування¶
Ця сторінка представляє інструкції з кодування Odoo. Вони спрямовані на покращення якості коду Odoo Apps. Насправді правильний код покращує читабельність, полегшує обслуговування, допомагає налагоджувати, знижує складність і підвищує надійність. Ці рекомендації слід застосовувати до кожного нового модуля та всіх нових розробок.
Попередження
Під час модифікації існуючих файлів у стабільній версії оригінальний стиль файлу суворо замінює будь-які інші правила стилю. Іншими словами, будь ласка, ніколи не змінюйте існуючі файли, щоб застосувати ці вказівки. Це дозволяє уникнути порушення історії переглядів рядків коду. Різниця має бути мінімальною. Щоб отримати докладнішу інформацію, перегляньте наш посібник із запитами на витягування.
Попередження
Змінюючи існуючі файли в master (для розробки) версії, застосовуйте ці вказівки до існуючого коду лише для зміненого коду або якщо більша частина файлу переглядається. Іншими словами, змінюйте існуючу структуру файлів, лише якщо вона зазнає серйозних змін. У такому випадку спочатку виконайте фіксацію переміщення, а потім застосуйте зміни, пов’язані з функцією.
Структура модуля¶
Каталоги¶
Модуль організований у важливих каталогах. Вони містять бізнес-логіку; Подивившись на них, ви повинні зрозуміти мету модуля.
data/ : demo та 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_nursery_groups.xml
| |-- plant_nursery_security.xml
| |-- plant_order_security.xml
Concerning views, backend views should be split like models and suffixed
by _views.xml
. Backend views are list, form, kanban, activity, graph, pivot, ..
views. To ease split by model in views main menus not linked to specific actions
may be extracted into an optional <module>_menus.xml
file. Templates (QWeb
pages used notably for portal / website display) are put in separate files named
<model>_templates.xml
.
addons/plant_nursery/
|-- views/
| | -- 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
Що стосується data, розділіть їх за призначенням (demo або data) і основною моделлю. Назвами файлів буде назва main_model із суфіксом _demo.xml
або _data.xml
. Наприклад, для додатку, який має demo та data для основної моделі, а також підтипи, активності та шаблони пошти, пов’язані з модулем пошти:
addons/plant_nursery/
|-- data/
| |-- plant_nursery_data.xml
| |-- plant_nursery_demo.xml
| |-- mail_data.xml
Що стосується controllers, зазвичай усі контролери належать до одного контролера, який міститься у файлі під назвою <module_name>.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)
Стосовно static файлів, файли Javascript загалом дотримуються тієї ж логіки, що й моделі python. Кожен компонент має бути у власному файлі зі змістовною назвою. Наприклад, віджети активності знаходяться в activity.js
поштового модуля. Також можна створювати підкаталоги для структурування „пакета“ (додаткову інформацію див. у веб-модулі). Таку саму логіку слід застосувати до шаблонів JS-віджетів (статичних XML-файлів) і їх стилів (scss-файлів). Не пов’язуйте дані (зображення, бібліотеки) за межами Odoo: не використовуйте URL-адресу зображення, а скопіюйте його в кодову базу.
Що стосується wizards, угода про найменування така сама, як і для моделей python: <transient>.py
і <transient>_views.xml
. Обидва поміщаються в каталог майстра. Це найменування походить від старих додатків 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)
Отже, повне дерево нашого модуля Odoo виглядає так
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_nursery_groups.xml
| |-- plant_nursery_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/
| |-- 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
Примітка
Назви файлів мають містити лише [a-z0-9_]
(маленькі літери, цифри та _
)
Попередження
Використовуйте правильні дозволи для файлів: папка 755 і файл 644.
Файли XML¶
Формат¶
Щоб оголосити запис у XML, рекомендується нотація record (з використанням <record>):
Розмістіть атрибут
id
передmodel
Для оголошення поля першим є атрибут
name
. Потім помістіть value або в тегfield
, або вeval
атрибут і, нарешті, інші атрибути (віджет, параметри, …), упорядковані за важливістю.Спробуйте згрупувати запис за моделлю. У випадку залежностей між дією/меню/представленнями ця умова може бути незастосовною.
Використовуйте угоду про найменування, визначену в наступному пункті
Тег <data> використовується лише для встановлення неоновлюваних даних за допомогою
noupdate=1
. Якщо у файлі є лише дані, які не оновлюються,noupdate=1
можна встановити для тегу<odoo>
і не встановлювати тег<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 View, який вимагає лише розділу
arch
представлення.
Ці теги є кращими перед позначенням record.
Ідентифікатори XML та найменування¶
Безпека, Представлення і Дія¶
Використовуйте такий шаблон:
Для меню:
<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>
.Для групи:
<module_name>_group_<group_name>
, де group_name - це назва групи, як правило, „користувач“, „менеджер“, …Для правила:
<model_name>_rule_<concerned_group>
, де concerned_group - коротка назва відповідної групи („user“ для „model_name_group_user“, „public“ для публічного користувача, „ компанія“ для правил кількох компаній, …).
Назва має бути ідентичним ідентифікатору 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 Children</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¶
Використання лінтера може допомогти показати синтаксичні та семантичні попередження або помилки. Вихідний код Odoo намагається дотримуватися стандарту Python, але деякі з них можна ігнорувати.
E501: line too long
E301: expected 1 blank line, found 0
E302: expected 2 blank lines, found 1
Імпорти¶
Імпорт упорядковується як
Зовнішні бібліотеки (по одній на рядок, відсортовані та розділені в python stdlib)
Імпорт
odoo
Імпортує з модулів 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.web.controllers.main import login_redirect
from odoo.addons.website.models.website import slug
Ідіоматика програмування (Python)¶
Завжди віддавайте перевагу читабельності над лаконізмом або використанням мовних особливостей чи ідіом.
Не використовуйте
.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']}
Кілька точок повернення - це добре, коли вони простіші
# 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' в my_dict
і if my_dict.get('key')
мають дуже різні значення, переконайтеся, що ви використовуєте правильний.
Вивчіть розуміння списків : використовуйте розуміння списків, розуміння dict і базові маніпуляції за допомогою
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» при оцінці в логічному контексті (наприклад, якщо). Серед них є колекції (списки, словники, набори, …), які є «фальшивими», коли порожні, і «правдивими», коли містять елементи:
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)
Як хороший розробник, задокументуйте свій код (документація методів, прості коментарі для складної частини коду)
Окрім цих вказівок, вам також може бути цікавим таке посилання: http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html (трохи застаріле, але цілком актуальне)
Програмування в Odoo¶
Уникайте створення генераторів і декораторів: використовуйте лише ті, що надаються Odoo API.
Як і в Python, використовуйте методи
filtered
,mapped
,sorted
, … для полегшення читання коду та продуктивності.
Поширюйте контекст¶
Контекст є замороженим
, який не можна змінити. Щоб викликати метод з іншим контекстом, слід використовувати метод 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
Попередження
Передача параметра в контексті може мати небезпечні побічні ефекти.
Оскільки значення поширюються автоматично, може виникнути деяка несподівана поведінка. Виклик методу create()
моделі з ключем default_my_field у контексті встановить значення за замовчуванням my_field для відповідної моделі. Але якщо під час цього створення будуть створені інші об’єкти (такі як sale.order.line, on sale.order create) з назвою поля my_field, їх значення за замовчуванням також буде встановлено.
Якщо вам потрібно створити ключовий контекст, що впливає на поведінку якогось об’єкта, виберіть гарну назву та, зрештою, додайте до нього назву модуля, щоб виділити його вплив. Гарним прикладом є ключі модуля mail
: mail_create_nosubscribe, mail_notrack, mail_notify_user_signature, …
Подумайте, що можна розширити¶
Функції та методи не повинні містити занадто багато логіки: мати багато маленьких і простих методів є більш доцільним, ніж мати кілька великих і складних методів. Хорошим емпіричним правилом є розділення методу, щойно він має більше ніж одну відповідальність (див. 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()
будь-де, існує дуже висока ймовірність того, що ви зламаєте систему різними способами, тому що ви спричините часткові коміти, а отже, часткові та нечисті відкати, викликаючи серед інші:
суперечливі бізнес-дані, як правило, втрата даних
десинхронізація робочого процесу, документи застрягли назавжди
тести, які не можна чітко відкотити, і вони почнуть забруднювати базу даних і викликати помилку (це вірно, навіть якщо під час транзакції не виникає жодної помилки)
- Ось дуже просте правило:
Вам слід НІКОЛИ не викликати
cr.commit()
самостійно, ЯКЩО ви явно не створили власний курсор бази даних! А ситуації, коли це потрібно зробити, виняткові!І, до речі, якщо ви створили свій власний курсор, то вам потрібно обробити випадки помилок і належний відкат, а також правильно закрити курсор, коли ви закінчите з ним.
І всупереч поширеній думці, вам навіть не потрібно викликати cr.commit()
у таких ситуаціях: - у методі _auto_init()
об’єкта models.Model: це використовується догляд за допомогою методу ініціалізації аддонів або транзакції ORM під час створення користувальницьких моделей - у звітах: commit()
також обробляється фреймворком, тому ви можете оновлювати базу даних навіть із звіту - у методах models.Transient: ці методи викликаються так само, як звичайні models.Model, у межах транзакції та з відповідним cr.commit()/rollback()
наприкінці тощо (див. загальне правило вище, якщо ви сумніваєтеся!)
Відтепер усі виклики ``cr.commit()` поза межами сервера повинні мати явний коментар, що пояснює, чому вони абсолютно необхідні, чому вони справді правильні та чому вони не порушують транзакції. Інакше можуть і будуть видалені!
Використовуйте метод перекладу правильно¶
Odoo використовує метод, подібний до GetText, під назвою «підкреслення» _( )
, щоб вказати, що статичний рядок, який використовується в коді, потрібно перекладати під час виконання за допомогою мови контексту. Цей псевдометод доступний у вашому коді шляхом імпорту наступним чином:
from odoo import _
Під час його використання необхідно дотримуватися кількох дуже важливих правил, щоб він працював і щоб уникнути заповнення перекладів непотрібним мотлохом.
Загалом, цей метод слід використовувати лише для статичних рядків, написаних вручну в коді, він не працюватиме для перекладу значень полів, таких як назви продуктів тощо. Це потрібно зробити замість цього за допомогою позначки перекладу у відповідному полі.
Метод приймає необов’язковий позиційний або іменований параметр. Правило дуже просте: виклики методу підкреслення завжди мають бути у формі _('літеральний рядок')
і нічого іншого:
# 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: formatting outside of translation
# This won't benefit from fallback mechanism in case of bad translation
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)
# 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 ")
# Ok (pay attention to position of the brackets too!)
error = _("Answer to question %s is not valid.\n" \
"Please enter an integer value.", question)
# Better
error = _("Answer to question %(title)s is not valid.\n" \
"Please enter an integer value.", title=question)
Загалом в Odoo, маніпулюючи рядками, віддайте перевагу %
над .format()
(якщо в рядку потрібно замінити лише одну змінну) і віддайте перевагу %(variname)
замість position (коли необхідно замінити декілька змінних). Це полегшує переклад для перекладачів community.
Символи та умовні позначення¶
- Назва моделі (з використанням нотації крапка, префікс до назви модуля):
Під час визначення моделі Odoo: використовуйте форму однини (res.partner і sale.order замість res.partnerS і saleS.orderS)
Під час визначення перехідного процесу Odoo (wizard): використовуйте
<related_base_model>.<action>
, де related_base_model - базова модель (визначена в models/), пов’язана з перехідним процесом, а action - коротка назва того, що роблять тимчасові. Уникайте слова wizard. Наприклад:account.invoice.make
,project.task.delegate.batch
, …Під час визначення моделі report (представлення SQL, наприклад): використовуйте
<related_base_model>.report.<action>
на основі угоди про перехідні процеси.
Odoo Python Class: використовуйте camelcase (об’єктно-орієнтований стиль).
class AccountInvoice(models.Model):
...
- Назва змінної:
використовуйте camelcase для змінної моделі
використовуйте позначення підкреслення в нижньому регістрі для загальної змінної.
додайте до назви змінної _id або _ids, якщо вона містить ідентифікатор запису або список ідентифікаторів. Не використовуйте
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_name>
Метод пошуку: шаблон методу пошуку _search_<field_name>
Метод за замовчуванням: типовим шаблоном методу є _default_<field_name>
Метод вибору: шаблон методу вибору - _selection_<field_name>
Метод Onchange: шаблон методу onchange є _onchange_<field_name>
Метод обмеження: шаблон методу обмеження _check_<constraint_name>
Метод дії: метод дії об’єкта має префікс action_. Оскільки він використовує лише один запис, додайте
self.ensure_one()
на початку методу.
- У моделі має бути порядок атрибутів
Приватні атрибути (
_name
,_description
,_inherit
,_sql_constraints
, …)Метод за замовчуванням і
default_get
Оголошення полів
Методи обчислення, інверсії та пошуку в тому ж порядку, що й оголошення поля
Метод вибору (методи, що використовуються для повернення обчислених значень для полів вибору)
Методи обмежень (
@api.constrains
) і методи onchange (@api.onchange
)Методи CRUD (перевизначення ORM)
Методи дії
І, нарешті, інші методи ведення бізнесу.
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(string='Reserved Seats', store=True
readonly=True, compute='_compute_seats')
seats_available = fields.Integer(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.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_search, _search, ...) overrides
def create(self, values):
...
# Action methods
def action_validate(self):
self.ensure_one()
...
# Business methods
def mail_user_confirm(self):
...
Javascript¶
Організація статичних файлів¶
Аддони Odoo мають певні умови щодо структурування різних файлів. Тут ми докладніше пояснюємо, як мають бути організовані веб-активи.
Перше, що потрібно знати, це те, що сервер Odoo обслуговуватиме (статично) усі файли, розташовані в папці static/, але з префіксом назви аддона. Так, наприклад, якщо файл знаходиться в addons/web/static/src/js/some_file.js, він буде статично доступний за URL-адресою 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/fonts
static/img
static/src/js
static/src/js/tours: файли турів для кінцевих користувачів (навчальні посібники, а не тести)
static/src/scss: файли scss
static/src/xml: усі шаблони qweb, які відображатимуться в JS
static/tests: сюди ми розміщуємо всі пов’язані з тестами файли.
static/tests/tours: сюди ми розміщуємо всі тестові файли турів (не навчальні посібники).
Інструкції кодування Javascript¶
use strict;
рекомендується для всіх файлів javascriptВикористовуйте лінтер (jshint, …)
Ніколи не додавайте мінімізовані бібліотеки Javascript
Використовуйте camelcase для оголошення класу
Більш точні вказівки щодо JS детально описано у github wiki. Ви також можете переглянути існуючий API у Javascript, переглянувши посилання на Javascript.
CSS and SCSS¶
Syntax and Formatting¶
.o_foo, .o_foo_bar, .o_baz {
height: $o-statusbar-height;
.o_qux {
height: $o-statusbar-height * 0.5;
}
}
.o_corge {
background: $o-list-footer-bg-color;
}
.o_foo, .o_foo_bar, .o_baz {
height: 32px;
}
.o_foo .o_quux, .o_foo_bar .o_quux, .o_baz .o_qux {
height: 16px;
}
.o_corge {
background: #EAEAEA;
}
four (4) space indents, no tabs;
columns of max. 80 characters wide;
opening brace (
{
): empty space after the last selector;closing brace (
}
): on its own new line;one line for each declaration;
meaningful use of whitespace.
"stylelint.config": {
"rules": {
// https://stylelint.io/user-guide/rules
// Avoid errors
"block-no-empty": true,
"shorthand-property-no-redundant-values": true,
"declaration-block-no-shorthand-property-overrides": true,
// Stylistic conventions
"indentation": 4,
"function-comma-space-after": "always",
"function-parentheses-space-inside": "never",
"function-whitespace-after": "always",
"unit-case": "lower",
"value-list-comma-space-after": "always-single-line",
"declaration-bang-space-after": "never",
"declaration-bang-space-before": "always",
"declaration-colon-space-after": "always",
"declaration-colon-space-before": "never",
"block-closing-brace-empty-line-before": "never",
"block-opening-brace-space-before": "always",
"selector-attribute-brackets-space-inside": "never",
"selector-list-comma-space-after": "always-single-line",
"selector-list-comma-space-before": "never-single-line",
}
},
Properties order¶
Order properties from the «outside» in, starting from position
and ending with decorative rules
(font
, filter
, etc.).
Scoped SCSS variables and CSS variables must be placed at the very top, followed by an empty line separating them from other declarations.
.o_element {
$-inner-gap: $border-width + $legend-margin-bottom;
--element-margin: 1rem;
--element-size: 3rem;
@include o-position-absolute(1rem);
display: block;
margin: var(--element-margin);
width: calc(var(--element-size) + #{$-inner-gap});
border: 0;
padding: 1rem;
background: blue;
font-size: 1rem;
filter: blur(2px);
}
Naming Conventions¶
Naming conventions in CSS are incredibly useful in making your code more strict, transparent and informative.
id
selectors, and prefix your classes with o_<module_name>
, where <module_name>
is the
technical name of the module (sale
, im_chat
, …) or the main route reserved by the module
(for website modules mainly, i.e. : o_forum
for the website_forum
module).o_
prefix.Avoid creating hyper-specific classes and variable names. When naming nested elements, opt for the «Grandchild» approach.
Example
Don’t
<div class=“o_element_wrapper”>
<div class=“o_element_wrapper_entries”>
<span class=“o_element_wrapper_entries_entry”>
<a class=“o_element_wrapper_entries_entry_link”>Entry</a>
</span>
</div>
</div>
Do
<div class=“o_element_wrapper”>
<div class=“o_element_entries”>
<span class=“o_element_entry”>
<a class=“o_element_link”>Entry</a>
</span>
</div>
</div>
Besides being more compact, this approach eases maintenance because it limits the need of renaming when changes occur at the DOM.
SCSS Variables¶
Our standard convention is $o-[root]-[element]-[property]-[modifier]
, with:
$o-
The prefix.
[root]
Either the component or the module name (components take priority).
[element]
An optional identifier for inner elements.
[property]
The property/behavior defined by the variable.
[modifier]
An optional modifier.
Example
$o-block-color: value;
$o-block-title-color: value;
$o-block-title-color-hover: value;
SCSS Variables (scoped)¶
These variables are declared within blocks and are not accessible from the outside.
Our standard convention is $-[variable name]
.
Example
.o_element {
$-inner-gap: compute-something;
margin-right: $-inner-gap;
.o_element_child {
margin-right: $-inner-gap * 0.5;
}
}
Перегляньте також
SCSS Mixins and Functions¶
Our standard convention is o-[name]
. Use descriptive names. When naming functions, use verbs in
the imperative form (e.g.: get
, make
, apply
…).
Name optional arguments in the scoped variables form, so $-[argument]
.
Example
@mixin o-avatar($-size: 1.5em, $-radius: 100%) {
width: $-size;
height: $-size;
border-radius: $-radius;
}
@function o-invert-color($-color, $-amount: 100%) {
$-inverse: change-color($-color, $-hue: hue($-color) + 180);
@return mix($-inverse, $-color, $-amount);
}
Перегляньте також
CSS Variables¶
In Odoo, the use of CSS variables is strictly DOM-related. Use them to contextually adapt the design and layout.
Our standard convention is BEM, so --[root]__[element]-[property]--[modifier]
, with:
[root]
Either the component or the module name (components take priority).
[element]
An optional identifier for inner elements.
[property]
The property/behavior defined by the variable.
[modifier]
An optional modifier.
Example
.o_kanban_record {
--KanbanRecord-width: value;
--KanbanRecord__picture-border: value;
--KanbanRecord__picture-border--active: value;
}
// Adapt the component when rendered in another context.
.o_form_view {
--KanbanRecord-width: another-value;
--KanbanRecord__picture-border: another-value;
--KanbanRecord__picture-border--active: another-value;
}
Use of CSS Variables¶
In Odoo, the use of CSS variables is strictly DOM-related, meaning that are used to contextually adapt the design and layout rather than to manage the global design-system. These are typically used when a component’s properties can vary in specific contexts or in other circumstances.
We define these properties inside the component’s main block, providing default fallbacks.
Example
my_component.scss
¶.o_MyComponent {
color: var(--MyComponent-color, #313131);
}
my_dashboard.scss
¶.o_MyDashboard {
// Adapt the component in this context only
--MyComponent-color: #017e84;
}
Перегляньте також
CSS and SCSS Variables¶
Despite being apparently similar, CSS
and SCSS
variables behave very differently. The main
difference is that, while SCSS
variables are imperative and compiled away, CSS
variables are
declarative and included in the final output.
Перегляньте також
In Odoo, we take the best of both worlds: using the SCSS
variables to define the design-system
while opting for the CSS
ones when it comes to contextual adaptations.
The implementation of the previous example should be improved by adding SCSS variables in order to gain control at the top-level and ensure consistency with other components.
The :root
pseudo-class¶
Defining CSS variables on the :root
pseudo-class is a technique we normally don’t use in
Odoo’s UI. The practice is commonly used to access and modify CSS variables globally. We perform
this using SCSS instead.
Exceptions to this rule should be fairly apparent, such as templates shared across bundles that require a certain level of contextual awareness in order to be rendered properly.