راهنمای کدنویسی

این صفحه Odoo Coding Guidelines را معرفی می‌کند. آن‌ها هدف بهبود کیفیت کد Odoo Apps را دارند. در واقع کد مناسب خوانایی را بهبود می‌بخشد، نگهداری را آسان می‌کند، به debug کردن کمک می‌کند، پیچیدگی را کاهش می‌دهد و قابلیت اطمینان را افزایش می‌دهد. این دستورالعمل‌ها باید برای هر ماژول جدید و همه توسعه‌های جدید اعمال شوند.

هشدار

هنگام تغییر فایل‌های موجود در stable version، سبک فایل اصلی به‌طور کامل بر هر دستورالعمل سبک دیگری ارجحیت دارد. به‌عبارت دیگر، لطفاً هرگز فایل‌های موجود را برای اعمال این دستورالعمل‌ها تغییر ندهید. این از ایجاد اختلال در تاریخچه revision خطوط کد جلوگیری می‌کند. Diff باید حداقل نگه داشته شود. برای جزئیات بیشتر، به pull request guide ما مراجعه کنید.

هشدار

هنگام تغییر فایل‌های موجود در master (development) version، این دستورالعمل‌ها را به کد موجود فقط برای کد تغییریافته یا اگر بیشتر فایل تحت revision است اعمال کنید. به‌عبارت دیگر، ساختار فایل‌های موجود را فقط در صورتی تغییر دهید که تغییرات اصلی در حال انجام است. در آن صورت ابتدا یک commit move انجام دهید سپس تغییرات مربوط به ویژگی را اعمال کنید.

ساختار ماژول

هشدار

برای ماژول‌های توسعه‌داده‌شده توسط جامعه، به‌شدت توصیه می‌شود ماژول خود را با پیشوندی مانند نام شرکت خود نام‌گذاری کنید.

دایرکتوری‌ها

یک ماژول در دایرکتوری‌های مهم سازماندهی شده است. آن‌ها حاوی منطق business هستند؛ نگاه کردن به آن‌ها باید هدف ماژول را برای شما روشن کند.

  • data/ : XML داده‌ها و دموها

  • models/ : تعریف مدل‌ها

  • controllers/ : شامل کنترلرها (مسیرهای HTTP)

  • views/ : شامل نماها و قالب‌ها

  • static/ : شامل دارایی‌های وب، تفکیک‌شده به css/، js/، img/، lib/، ...

دایرکتوری‌های اختیاری دیگر، ماژول را تشکیل می‌دهند.

  • wizard/ : پنجره‌های پیکربندی (مدل‌های گذرا، models.TransientModel) و نماهای آن‌ها را گروه‌بندی می‌کند

  • report/: شامل گزارش‌های قابل چاپ و مدل‌های مبتنی بر SQL views است. اشیاء Python و نماهای XML در این دایرکتوری گنجانده شده‌اند

  • tests/ : شامل تست‌های Python

نام‌گذاری فایل‌ها

نام‌گذاری فایل برای یافتن سریع اطلاعات در تمام addons odoo مهم است. این بخش توضیح می‌دهد چگونه فایل‌ها را در یک ماژول استاندارد odoo نام‌گذاری کنید. به‌عنوان مثال از یک برنامه plant nursery استفاده می‌کنیم. این دو مدل اصلی plant.nursery و plant.order را در خود دارد.

در مورد models، منطق business را با مجموعه‌هایی از models متعلق به یک model اصلی یکسان تقسیم کنید. هر مجموعه در یک فایل معین قرار دارد که بر اساس model اصلی آن نام‌گذاری شده است. اگر فقط یک model وجود داشته باشد، نام آن همان نام ماژول است. هر مدل به ارث‌بری‌شده باید در فایل خود باشد تا به درک models تحت تأثیر کمک کند.

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

در مورد views، backend views باید مانند models تقسیم شوند و با _views.xml پسوند داده شوند. Backend views شامل نماهای list، form، kanban، activity، graph، pivot و... هستند. برای سهولت تقسیم بر اساس model در نماها، منوهای اصلی که به اقدامات خاص پیوند نخورده‌اند ممکن است به یک فایل اختیاری <module>_menus.xml استخراج شوند. قالب‌ها (QWeb pages که به‌ویژه برای نمایش portal / website استفاده می‌شوند) در فایل‌های جداگانه‌ای به نام <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) و مدل اصلی تقسیم کنید. نام‌های فایل، نام model اصلی با پسوند _demo.xml یا _data.xml خواهند بود. به‌عنوان مثال برای یک برنامه که دارای demo و data برای model اصلی خود و همچنین subtypes، activities و mail templates مرتبط با ماژول mail است:

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

در مورد controllers، به‌طور کلی همه controllers به یک controller واحد متعلق هستند که در فایلی به نام <module_name>.py قرار دارد. یک قرارداد قدیمی در Odoo این است که این فایل را main.py نام‌گذاری می‌کند اما این قدیمی محسوب می‌شود. اگر نیاز دارید یک controller موجود از ماژول دیگری را به ارث ببرید، این کار را در <inherited_module_name>.py انجام دهید. به‌عنوان مثال افزودن portal controller در یک برنامه در 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 files، فایل‌های Javascript کلاً منطقی مشابه python models را دنبال می‌کنند. هر component باید در فایل خود با نامی معنادار باشد. به‌عنوان مثال، widgets فعالیت در activity.js ماژول mail قرار دارند. زیردایرکتوری‌ها نیز می‌توانند برای ساختاردهی بسته‌ها ایجاد شوند (برای جزئیات بیشتر به ماژول web مراجعه کنید). همان منطق باید برای templates JS widgets (فایل‌های static XML) و سبک‌های آن‌ها (فایل‌های scss) اعمال شود. داده‌ها (image، libraries) را خارج از Odoo پیوند ندهید: از یک URL به یک تصویر استفاده نکنید بلکه آن را در codebase کپی کنید.

در مورد wizards، قرارداد نام‌گذاری همان python models است: <transient>.py و <transient>_views.xml. هر دو در دایرکتوری wizard قرار می‌گیرند. این نام‌گذاری از برنامه‌های قدیمی odoo که از کلیدواژه wizard برای transient models استفاده می‌کردند، می‌آید.

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

در مورد statistics reports انجام‌شده با python / SQL views و classic views، نام‌گذاری به شرح زیر است:

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

در مورد printable reports که عمدتاً شامل آماده‌سازی داده و Qweb templates هستند، نام‌گذاری به شرح زیر است:

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_] (حروف کوچک عددحرفی و _) باشند

هشدار

از سطح دسترسی صحیح فایل استفاده کنید: پوشه ۷۵۵ و فایل ۶۴۴.

فایل‌های XML

قالب

برای اعلان یک رکورد در XML، نماد record (با استفاده از <record>) توصیه می‌شود:

  • صفت id را قبل از model قرار دهید

  • برای اعلام یک فیلد، attribute name اول است. سپس value را یا در برچسب field یا در attribute eval قرار دهید، و در نهایت سایر attributes (widget، options، ...) به ترتیب اهمیت.

  • سعی کنید رکوردها را بر اساس model گروه‌بندی کنید. در صورت وابستگی بین action/منو/نماها، این قرارداد ممکن است قابل اعمال نباشد.

  • از قرارداد نام‌گذاری تعریف‌شده در نکته بعدی استفاده کنید

  • برچسب <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">
        <list>
            <field name="my_field_1"/>
            <field name="my_field_2" string="My Label" widget="statusbar" statusbar_visible="draft,sent,progress,done" />
        </list>
    </field>
</record>

Odoo از تگ‌های سفارشی که به عنوان شکر نحوی عمل می‌کنند پشتیبانی می‌کند:

  • menuitem: از آن به عنوان میانبر برای اعلان یک ir.ui.menu استفاده کنید

  • template: از آن برای اعلان یک نمای QWeb که فقط به بخش arch نما نیاز دارد استفاده کنید.

این برچسب‌ها به نماد record ترجیح داده می‌شوند.

شناسه‌ها و نام‌گذاری در XML

امنیت، نما و اقدام

از الگوی زیر استفاده کنید:

  • برای یک منو: <model_name>_menu، یا <model_name>_menu_do_stuff برای submenus.

  • برای یک نما: <model_name>_view_<view_type>، که view_type عبارت است از kanban، form، list، search، ...

  • برای یک action: action اصلی <model_name>_action را رعایت می‌کند. سایرین با _<detail> پسوند داده می‌شوند، که detail یک رشته lowercase است که به‌طور خلاصه action را توضیح می‌دهد. این فقط در صورتی استفاده می‌شود که چندین actions برای model اعلام شده باشند.

  • برای window actions: نام action را با اطلاعات نما خاص مانند <model_name>_action_view_<view_type> پسوند دهید.

  • برای یک group: <module_name>_group_<group_name> که group_name نام group است، به‌طور کلی 'user'، 'manager'، ...

  • برای یک rule: <model_name>_rule_<concerned_group> که concerned_group نام کوتاه گروه مربوطه است (user برای model_name_group_user، public برای کاربر public، company برای multi-company rules، ...).

Name باید با xml id با dots جایگزین underscores یکسان باشد. Actions باید نام‌گذاری واقعی داشته باشند زیرا به‌عنوان display name استفاده می‌شود.

<!-- 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 Ids از inheriting views باید از همان ID رکورد اصلی استفاده کنند. این به یافتن همه inheritance در یک نگاه کمک می‌کند. از آنجا که Xml Ids نهایی توسط ماژولی که آن‌ها را ایجاد می‌کند پیشوند داده می‌شوند، هیچ تداخلی وجود ندارد.

نام‌گذاری باید شامل یک پسوند .inherit.{details} باشد تا درک هدف override هنگام نگاه به نام آن آسان شود.

<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

هشدار

برای نوشتن کد امن، فراموش نکنید بخش Security Pitfalls را نیز بخوانید.

گزینه‌های PEP8

استفاده از یک linter می‌تواند به نمایش هشدارها یا خطاهای syntax و semantic کمک کند. کد منبع Odoo سعی می‌کند به استاندارد Python احترام بگذارد، اما برخی از آن‌ها را می‌توان نادیده گرفت.

  • E501: خط بیش از حد طولانی

  • E301: انتظار ۱ خط خالی، ۰ یافت شد

  • E302: انتظار ۲ خط خالی، ۱ یافت شد

ایمپورت‌ها

ایمپورت‌ها به صورت زیر مرتب می‌شوند

  1. کتابخانه‌های خارجی (هر کدام در یک خط، مرتب‌شده و تفکیک‌شده در کتابخانه استاندارد Python)

  2. ایمپورت‌های زیرماژول‌های odoo

  3. ایمپورت‌ها از افزونه‌های Odoo (به‌ندرت و فقط در صورت ضرورت)

در داخل این ۳ گروه، خطوط ایمپورت‌شده به ترتیب الفبایی مرتب می‌شوند.

# 1 : imports of python lib
import base64
import re
import time
from datetime import datetime
# 2 : imports of odoo
from odoo import Command, _, api, fields, models # ASCIIbetically ordered
from odoo.fields import Domain
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)
  • از نام‌های معنادار برای متغیر/کلاس/متد استفاده کنید

  • متغیر بی‌فایده: متغیرهای موقت می‌توانند با دادن نام به objects کد را واضح‌تر کنند، اما این بدان معنا نیست که باید همیشه متغیرهای موقت ایجاد کنید:

# 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
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') معانی بسیار متفاوتی دارند، مطمئن شوید که از مورد صحیح استفاده می‌کنید.

  • list comprehensions را یاد بگیرید: از list comprehension، dict comprehension و دستکاری ساده با 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]
  • Collections هم booleans هستند: در python، بسیاری از objects هنگام ارزیابی در یک context boolean (مانند یک if) ارزش "boolean-ish" دارند. در میان این‌ها collections (lists، dicts، sets، ...) هستند که هنگام خالی بودن "falsy" و هنگام داشتن items "truthy" هستند:

bool([]) is False
bool([1]) is True
bool([False]) is True

بنابراین می‌توانید به جای if len(some_collection): بنویسید if some_collection:.

  • روی iterable‌ها پیمایش کنید

# 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 برای متدها، کامنت ساده برای بخش‌های پیچیده کد)

  • علاوه بر این دستورالعمل‌ها، ممکن است لینک زیر را نیز جالب بیابید: https://david.goodger.org/projects/pycon/2007/idiomatic/handout.html (کمی قدیمی است، اما کاملاً مرتبط)

برنامه‌نویسی در Odoo

  • از ایجاد generator و decorator خودداری کنید: فقط از مواردی که توسط API Odoo ارائه شده استفاده کنید.

  • همانند Python، از متدهای filtered، mapped، sorted و ... برای سهولت خوانایی کد و کارایی استفاده کنید.

زمینه (context) را منتقل کنید

Context یک frozendict است که نمی‌توان آن را تغییر داد. برای فراخوانی یک method با یک context متفاوت، باید از method 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

هشدار

ارسال پارامتر در context می‌تواند عوارض جانبی خطرناکی داشته باشد.

از آنجا که مقادیر به‌طور خودکار منتشر می‌شوند، ممکن است برخی رفتارهای غیرمنتظره ظاهر شوند. فراخوانی method create() یک model با key default_my_field در context، مقدار پیش‌فرض my_field را برای model مربوطه تنظیم می‌کند. اما اگر در طول این ایجاد، objects دیگر (مانند sale.order.line، در ایجاد sale.order) که دارای فیلدی به نام my_field هستند ایجاد شوند، مقدار پیش‌فرض آن‌ها نیز تنظیم خواهد شد.

اگر نیاز دارید یک key context ایجاد کنید که بر رفتار برخی object تأثیر بگذارد، یک نام خوب انتخاب کنید و در نهایت آن را با نام ماژول پیشوند دهید تا تأثیر آن را جدا کنید. یک مثال خوب keys ماژول mail هستند: mail_create_nosubscribe، mail_notrack، mail_notify_user_signature، ...

قابل گسترش فکر کنید

Functions و methods نباید بیش از حد منطق داشته باشند: داشتن تعداد زیادی method کوچک و ساده توصیه می‌شود تا داشتن تعدادی method بزرگ و پیچیده. یک قاعده تجربی خوب این است که یک method را به‌محض اینکه بیش از یک مسئولیت دارد تقسیم کنید (به 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')

کد فوق به‌خاطر مثال over extendable است اما خوانایی باید در نظر گرفته شود و باید یک trade-off ایجاد شود.

همچنین، functions خود را بر اساس آن نام‌گذاری کنید: functions کوچک و به‌درستی نام‌گذاری شده، نقطه شروع کد قابل‌خواندن/قابل‌نگهداری و مستندات محکم‌تر هستند.

این توصیه برای classes، files، modules و packages نیز مرتبط است. (همچنین به http://en.wikipedia.org/wiki/Cyclomatic_complexity مراجعه کنید)

هرگز تراکنش را کامیت نکنید

فریم‌ورک Odoo مسئول ارائه transactional context برای همه RPC calls است. همه فراخوانی‌های cr.commit() خارج از سرور framework باید یک comment صریح داشته باشند که توضیح دهد چرا کاملاً ضروری هستند، چرا واقعاً صحیح هستند، و چرا transactions را نمی‌شکنند. در غیر این صورت می‌توان آن‌ها را حذف کرد و خواهد شد!

اصل این است که یک database cursor جدید در ابتدای هر RPC call باز می‌شود، و وقتی call برگشت، درست قبل از انتقال پاسخ به RPC client، commit می‌شود، تقریباً به این صورت:

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:  # try to be more specific
        cr.rollback() # error, rollback everything atomically
        raise
    finally:
        cr.close() # always close cursor opened manually
    return res

اگر در طول اجرای RPC call خطایی رخ دهد، تراکنش به‌صورت atomic بازگشت داده می‌شود، که حالت سیستم را حفظ می‌کند.

به‌طور مشابه، سیستم در طول اجرای مجموعه تست‌ها و اقدامات زمان‌بندی‌شده نیز تراکنش اختصاصی ارائه می‌دهد.

نتیجه این است که اگر در هر جایی به‌صورت دستی cr.commit() را فراخوانی کنید، احتمال بسیار بالایی وجود دارد که سیستم را به روش‌های مختلف بشکنید، زیرا باعث partial commits و در نتیجه partial و unclean rollbacks خواهید شد، که از جمله موارد زیر را به همراه دارد:

  1. داده‌های کسب‌وکار ناسازگار، معمولاً از دست رفتن داده‌ها

  2. ناهماهنگی جریان کاری، مستندات به‌طور دائم گیر می‌کنند

  3. تست‌هایی که نمی‌توانند به‌طور تمیز rollback شوند، شروع به آلوده کردن پایگاه داده می‌کنند و خطا را فعال می‌کنند (این درست است حتی اگر در طول تراکنش خطایی رخ ندهد)

قانون بسیار ساده به شرح زیر است:

شما هرگز نباید خودتان cr.commit() یا cr.rollback() را فراخوانی کنید، مگر اینکه صریحاً database cursor خود را ایجاد کرده باشید! و موقعیت‌هایی که نیاز به انجام این کار دارید استثنایی هستند!

و به‌هرحال اگر cursor خود را ایجاد کردید، باید موارد خطا و rollback مناسب را مدیریت کنید، و همچنین وقتی با آن کار را تمام کردید cursor را به‌درستی ببندید.

و برخلاف باور عمومی، شما حتی نیازی به فراخوانی cr.commit() در موقعیت‌های زیر ندارید:

  • در method _auto_init() یک object models.Model: این توسط method addons initialization، یا توسط ORM در حین تراکنش هنگام ایجاد custom models رسیدگی می‌شود

  • در reports: commit() نیز توسط framework مدیریت می‌شود، بنابراین می‌توانید پایگاه داده را حتی از داخل یک گزارش به‌روزرسانی کنید

  • در methods models.Transient: این methods دقیقاً مانند methods models.Model معمولی فراخوانی می‌شوند، در یک تراکنش و با cr.commit()/rollback() مربوطه در پایان

  • و غیره (اگر در شک هستید، قانون کلی بالا را ببینید!)

از گرفتن استثناها (exception) خودداری کنید

فقط exceptions خاص را catch کنید، و از مدیریت بیش از حد گسترده exception اجتناب کنید. Uncaught exceptions به‌درستی توسط framework لاگ و مدیریت خواهند شد.

شما باید در مورد types که catch می‌کنید خاص باشید و آن‌ها را به‌طور مناسب مدیریت کنید، و scope بلوک try-catch خود را تا حد ممکن محدود کنید.

# BAD CODE
try:
    do_something()
except Exception as e:
    # if we caught a ValidationError, we did not rollback and we left the
    # ORM in an undefined state
    _logger.warning(e)

برای scheduled actions، اگر errors را catch می‌کنید و می‌خواهید ادامه دهید، باید تغییرات را rollback کنید. Scheduled actions در یک تراکنش جداگانه اجرا می‌شوند، بنابراین می‌توانید مستقیماً rollback یا commit کنید وقتی که progress را signal می‌دهید.

همچنین ببینید

Scheduled Actions (ir.cron)

اگر باید framework exceptions را مدیریت کنید، باید از savepoints استفاده کنید تا function خود را تا حد ممکن جدا کنید. این محاسبات را هنگام ورود به بلوک flush می‌کند و تغییرات را در صورت exceptions به‌درستی rollback می‌کند.

try:
    with self.env.cr.savepoint():
        do_stuff()
except ...:
    ...

هشدار

بعد از اینکه بیش از 64 savepoint در یک تراکنش واحد شروع کنید، PostgreSQL کند می‌شود. در همه موارد، اگر سرور replicas را اجرا می‌کند، savepoints هزینه سربار زیادی دارند. اگر records را در یک حلقه پردازش کنید و savepoint می‌کنید، به‌عنوان مثال هنگام پردازش records یک‌به‌یک برای یک batch، اندازه batch را محدود کنید. اگر records بیشتری دارید، function احتمالاً باید به یک scheduled job تبدیل شود یا شما باید جریمه عملکرد را بپذیرید.

از متد ترجمه به‌درستی استفاده کنید

Odoo از یک method شبیه GetText به نام "underscore" _() استفاده می‌کند تا نشان دهد یک static string استفاده‌شده در کد باید در runtime ترجمه شود. این method در self.env._ با استفاده از زبان environment در دسترس است.

چند قانون بسیار مهم باید هنگام استفاده از آن دنبال شود، تا آن کار کند و از پر کردن translations با چیزهای بی‌فایده جلوگیری شود.

اساساً، این method فقط باید برای static strings که به‌صورت دستی در کد نوشته شده‌اند استفاده شود، برای ترجمه مقادیر فیلد، مانند نام محصولات و غیره کار نخواهد کرد. این باید با استفاده از translate flag روی فیلد مربوطه انجام شود.

method اختیاری parameter موقعیتی یا نام‌گذاری شده را می‌پذیرد. قانون بسیار ساده است: فراخوانی‌های underscore method باید همیشه به شکل self.env._('literal string') باشد و نه چیز دیگری:

_ = self.env._

# 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)

همچنین، به یاد داشته باشید که translators باید با مقادیر literal که به function underscore منتقل می‌شوند کار کنند، بنابراین لطفاً سعی کنید آن‌ها را آسان برای درک کنید و کاراکترهای spurious و formatting را به حداقل برسانید. Translators باید آگاه باشند که الگوهای formatting مانند %s یا %d، newlines و غیره باید حفظ شوند، اما مهم است که از این‌ها به روشی منطقی و واضح استفاده شود:

# 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، هنگام دستکاری strings، % را به .format() ترجیح دهید (وقتی فقط یک متغیر باید در یک string جایگزین شود)، و %(varname) را به‌جای position ترجیح دهید (وقتی چندین متغیر باید جایگزین شوند). این ترجمه را برای community translators آسان‌تر می‌کند.

نمادها و قراردادها

  • نام مدل (با استفاده از نماد نقطه، با پیشوند نام ماژول):
    • هنگام تعریف یک Odoo Model: از شکل مفرد نام استفاده کنید (res.partner و sale.order به‌جای res.partnerS و saleS.orderS)

    • هنگام تعریف یک Odoo Transient (پنجرهٔ پیکربندی): از <related_base_model>.<action> استفاده کنید که related_base_model model پایه (تعریف‌شده در models/) مرتبط با transient است، و action نام کوتاه آن چیزی است که transient انجام می‌دهد. از کلمه wizard اجتناب کنید. به‌عنوان مثال: account.invoice.make، project.task.delegate.batch، ...

    • هنگام تعریف report model (SQL views یعنی): از <related_base_model>.report.<action> استفاده کنید، بر اساس قرارداد Transient.

  • کلاس Python Odoo: از Pascal case (سبک شیءگرا) استفاده کنید.

class AccountInvoice(models.Model):
    ...
  • نام متغیر:
    • برای متغیر مدل از Pascal case استفاده کنید

    • برای متغیرهای معمول از نماد زیرخط کوچک استفاده کنید.

    • اگر یک رکورد id یا فهرستی از id را در بر دارد، نام متغیر خود را با _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 method: الگوی selection method _selection_<field_name> است

    • Onchange method: الگوی onchange method _onchange_<field_name> است

    • Constraint method: الگوی constraint method _check_<constraint_name> است

    • Action method: یک object action method با action_ پیشوند داده می‌شود. از آنجا که فقط از یک رکورد استفاده می‌کند، self.ensure_one() را در ابتدای method اضافه کنید.

  • در یک مدل، ترتیب صفات باید چنین باشد
    1. صفات خصوصی (_name, _description, _inherit, ...)

    2. متد پیش‌فرض و default_get

    3. اعلان فیلدها

    4. قیود و ایندکس‌های SQL

    5. Compute، inverse و search methods به همان ترتیب اعلام فیلدها

    6. Selection method (methods استفاده‌شده برای برگرداندن مقادیر محاسبه‌شده برای selection fields)

    7. methods Constrains (@api.constrains) و methods onchange (@api.onchange)

    8. متدهای CRUD (override کردن‌های ORM)

    9. متدهای اقدام

    10. و در نهایت، سایر متدهای کسب‌وکار.

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
    @api.model
    def create(self, vals_list):
        ...

    # Action methods
    def action_validate(self):
        self.ensure_one()
        ...

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

جاوااسکریپت

سازماندهی فایل‌های استاتیک

Odoo addons در مورد نحوه ساختاردهی فایل‌های مختلف برخی قراردادها دارند. در اینجا با جزئیات بیشتر توضیح می‌دهیم که web assets قرار است چگونه سازماندهی شوند.

اولین چیزی که باید بدانید این است که سرور Odoo (به‌صورت static) همه فایل‌های موجود در یک پوشه static/ را serve خواهد کرد، اما با نام addon پیشوند داده می‌شود. بنابراین، به‌عنوان مثال، اگر یک فایل در addons/web/static/src/js/some_file.js قرار دارد، در url your-odoo-server.com/web/static/src/js/some_file.js به‌صورت static در دسترس خواهد بود

قرارداد این است که کد را طبق ساختار زیر سازماندهی کنید:

  • static: تمام فایل‌های استاتیک به طور کلی

    • static/lib: این مکانی است که js libs باید در یک sub folder قرار داشته باشند. بنابراین، به‌عنوان مثال، همه فایل‌های کتابخانه 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: اینجا جایی است که همه فایل‌های tour test را قرار می‌دهیم (نه tutorials).

راهنمای کدنویسی جاوااسکریپت

  • use strict; برای تمام فایل‌های جاوااسکریپت توصیه می‌شود

  • از یک linter (jshint و غیره) استفاده کنید

  • هرگز کتابخانه‌های جاوااسکریپت minified اضافه نکنید

  • برای اعلان کلاس از Pascal case استفاده کنید

دستورالعمل‌های JS دقیق‌تر در github wiki به‌تفصیل آمده‌اند. ممکن است همچنین با نگاه به Javascript References به API موجود در Javascript نگاهی بیندازید.

CSS و SCSS

نحو و قالب‌بندی

.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;
}
  • تورفتگی ۴ فاصله، بدون tab؛

  • ستون‌های حداکثر ۸۰ کاراکتری؛

  • براکت بازکننده ({): فاصله خالی پس از آخرین selector؛

  • براکت بستن (}): در خط جداگانه خودش؛

  • یک خط برای هر اعلان؛

  • استفاده معنادار از فاصله خالی.

"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 را از "بیرون" به داخل مرتب کنید، با شروع از position و پایان با قوانین decorative (font، filter و غیره).

Scoped SCSS variables و CSS variables باید در بالای صفحه قرار گیرند، و سپس با یک خط خالی از سایر اعلام‌ها جدا شوند.

.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);
}

قراردادهای نام‌گذاری

قراردادهای نام‌گذاری در CSS به‌طور باورنکردنی در دقیق‌تر، شفاف‌تر و آموزنده‌تر کردن کد شما مفید هستند.

از selectors id اجتناب کنید، و classes خود را با o_<module_name> پیشوند دهید، که <module_name> نام فنی ماژول است (sale، im_chat، ...) یا مسیر اصلی رزرو‌شده توسط ماژول (برای ماژول‌های website عمدتاً، یعنی: o_forum برای ماژول website_forum).
تنها استثنا برای این قانون webclient است: به‌سادگی از پیشوند o_ استفاده می‌کند.

از ایجاد classes hyper-specific و نام‌های متغیر اجتناب کنید. هنگام نام‌گذاری عناصر تو در تو، رویکرد "Grandchild" را انتخاب کنید.

Example

نکنید

<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>

بکنید

<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>

علاوه بر فشرده‌تر بودن، این رویکرد نگهداری را آسان می‌کند زیرا نیاز به تغییر نام را هنگام تغییرات در DOM محدود می‌کند.

متغیرهای SCSS

قرارداد استاندارد ما $o-[root]-[element]-[property]-[modifier] است، با:

  • $o-

    پیشوند.

  • [root]

    یا نام کامپوننت یا نام ماژول (کامپوننت‌ها اولویت دارند).

  • [element]

    یک شناسه اختیاری برای عناصر داخلی.

  • [property]

    ویژگی/رفتار تعریف‌شده توسط متغیر.

  • [modifier]

    یک اصلاح‌کننده اختیاری.

Example

$o-block-color: value;
$o-block-title-color: value;
$o-block-title-color-hover: value;

متغیرهای SCSS (محدود به scope)

این متغیرها درون blocks اعلام می‌شوند و از خارج قابل دسترسی نیستند. قرارداد استاندارد ما $-[variable name] است.

Example

.o_element {
   $-inner-gap: compute-something;

   margin-right: $-inner-gap;

   .o_element_child {
      margin-right: $-inner-gap * 0.5;
   }
}

Mixinها و توابع SCSS

قرارداد استاندارد ما o-[name] است. از نام‌های توصیفی استفاده کنید. هنگام نام‌گذاری functions، از افعال در شکل امری استفاده کنید (به‌عنوان مثال: get، make، apply...).

آرگومان‌های اختیاری را به شکل scoped variables نام‌گذاری کنید، بنابراین $-[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

در Odoo، استفاده از متغیرهای CSS کاملاً DOM-related است. از آن‌ها برای contextually سازگار کردن design و layout استفاده کنید.

قرارداد استاندارد ما BEM است، بنابراین --[root]__[element]-[property]--[modifier]، با:

  • [root]

    یا نام کامپوننت یا نام ماژول (کامپوننت‌ها اولویت دارند).

  • [element]

    یک شناسه اختیاری برای عناصر داخلی.

  • [property]

    ویژگی/رفتار تعریف‌شده توسط متغیر.

  • [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;
}

استفاده از متغیرهای CSS

در Odoo، استفاده از متغیرهای CSS کاملاً DOM-related است، به این معنی که برای contextually سازگار کردن design و layout استفاده می‌شوند نه برای مدیریت global design-system. این‌ها معمولاً زمانی استفاده می‌شوند که properties یک component می‌توانند در contexts خاص یا در شرایط دیگر متفاوت باشند.

ما این properties را درون block اصلی component تعریف می‌کنیم، که 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 variables on MDN web docs

متغیرهای CSS و SCSS

علی‌رغم اینکه ظاهراً مشابه هستند، متغیرهای CSS و SCSS رفتار بسیار متفاوتی دارند. تفاوت اصلی این است که، در حالی که متغیرهای SCSS imperative هستند و compile می‌شوند، متغیرهای CSS declarative هستند و در خروجی نهایی گنجانده می‌شوند.

در Odoo، بهترین هر دو دنیا را می‌گیریم: استفاده از متغیرهای SCSS برای تعریف design-system در حالی که هنگام تطبیق‌های contextual CSS را انتخاب می‌کنیم.

اجرای مثال قبلی باید با افزودن متغیرهای SCSS بهبود یابد تا کنترل در سطح بالا به دست آید و سازگاری با سایر components تضمین شود.

Example

secondary_variables.scss
$o-component-color: $o-main-text-color;
$o-dashboard-color: $o-info;
// [...]
component.scss
.o_component {
   color: var(--MyComponent-color, #{$o-component-color});
}
dashboard.scss
.o_dashboard {
   --MyComponent-color: #{$o-dashboard-color};
}

شبه‌کلاس :root

تعریف متغیرهای CSS روی pseudo-class :root تکنیکی است که ما معمولاً در UI Odoo استفاده نمی‌کنیم. این روش معمولاً برای دسترسی و تغییر متغیرهای CSS به‌صورت سراسری استفاده می‌شود. ما این کار را به‌جای آن با استفاده از SCSS انجام می‌دهیم.

استثناهای این قانون باید نسبتاً واضح باشند، مانند templates به اشتراک گذاشته‌شده در bundles که برای render شدن صحیح نیاز به سطح خاصی از آگاهی contextual دارند.