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

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

هشدار

هنگام تغییر فایل‌های موجود در نسخه پایدار، سبک اصلی فایل به شدت بر هر دستورالعمل سبک دیگری ارجحیت دارد. به عبارت دیگر لطفا هرگز فایل‌های موجود را برای اعمال این دستورالعمل‌ها تغییر ندهید. این کار باعث جلوگیری از برهم خوردن تاریخچه بازبینی خطوط کد می‌شود. Diff باید به حداقل برسد. برای اطلاعات بیشتر، به راهنمای درخواست Pull ما مراجعه کنید.

هشدار

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

ساختار ماژول

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

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

  • data/ : دمو و داده‌های xml

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

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

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

  • static/ : شامل منابع وب است که به css/, js/, img/, lib/, ... تفکیک شده‌اند

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

  • wizard/ : مدل‌های موقت (models.TransientModel) و نماهای آنها را جمع‌آوری می‌کند

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

  • tests/ : شامل تست‌های پایتون

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

نام‌گذاری فایل برای یافتن سریع اطلاعات در تمام افزونه‌های اودو اهمیت دارد. این بخش توضیح می‌دهد که چگونه فایل‌ها را در یک ماژول استاندارد اودو نام‌گذاری کنیم. به‌عنوان مثال، ما از اپلیکیشن مهد گیاهان استفاده می‌کنیم. این اپلیکیشن شامل دو مدل اصلی 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)

Concerning security, three main files should be used:

  • First one is the definition of access rights done in a ir.model.access.csv file.

  • User groups are defined in <module>_groups.xml.

  • Record rules are defined in <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

Concerning data, split them by purpose (demo or data) and main model. Filenames will be the main_model name suffixed by _demo.xml or _data.xml. For instance for an application having demo and data for its main model as well as subtypes, activities and mail templates all related to mail module:

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

Concerning controllers, generally all controllers belong to a single controller contained in a file named <module_name>.py. An old convention in Odoo is to name this file main.py but it is considered as outdated. If you need to inherit an existing controller from another module do it in <inherited_module_name>.py. For example adding portal controller in an application is done in portal.py.

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

Concerning static files, Javascript files follow globally the same logic as python models. Each component should be in its own file with a meaningful name. For instance, the activity widgets are located in activity.js of mail module. Subdirectories can also be created to structure the 'package' (see web module for more details). The same logic should be applied for the templates of JS widgets (static XML files) and for their styles (scss files). Don't link data (image, libraries) outside Odoo: do not use an URL to an image but copy it in the codebase instead.

Concerning wizards, naming convention is the same of for python models: <transient>.py and <transient>_views.xml. Both are put in the wizard directory. This naming comes from old odoo applications using the wizard keyword for transient models.

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

Concerning statistics reports done with python / SQL views and classic views naming is the following :

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

Concerning printable reports which contain mainly data preparation and Qweb templates naming is the following :

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

The complete tree of our Odoo module therefore looks like

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

توجه

File names should only contain [a-z0-9_] (lowercase alphanumerics and _)

هشدار

Use correct file permissions : folder 755 and file 644.

XML files

Format

To declare a record in XML, the record notation (using <record>) is recommended:

  • Place id attribute before model

  • For field declaration, name attribute is first. Then place the value either in the field tag, either in the eval attribute, and finally other attributes (widget, options, ...) ordered by importance.

  • Try to group the record by model. In case of dependencies between action/menu/views, this convention may not be applicable.

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

  • تگ <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>

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

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

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

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

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

امنیت، نما و عمل

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

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

  • For a view: <model_name>_view_<view_type>, where view_type is kanban, form, list, search, ...

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

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

  • برای یک گروه: <module_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 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>

پایتون

هشدار

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

گزینه‌های PEP8

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

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

  • E301: انتظار ۱ خط خالی بود، ولی هیچ خط خالی یافت نشد.

  • E302: انتظار ۲ خط خالی بود، اما فقط ۱ خط خالی یافت شد.

واردات (Imports)

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

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

  2. واردات از odoo

  3. واردات از ماژول‌های Odoo (به‌ندرت و تنها در صورت نیاز)

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

# 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

اصطلاحات برنامه‌نویسی (پایتون)

  • همیشه خوانایی را بر اختصار یا استفاده از ویژگی‌های زبان یا اصطلاحات ترجیح دهید.

  • از .clone() استفاده نکنید.

# bad
new_dict = my_dict.clone()
new_list = old_list.clone()
# good
new_dict = dict(my_dict)
new_list = list(old_list)
  • دایره‌المعارف پایتون: ایجاد و به‌روزرسانی

# -- 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
  • توابع داخلی (builtins) خود را بشناسید: باید حداقل درکی ابتدایی از تمامی توابع داخلی پایتون داشته باشید (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]
  • مجموعه‌ها نیز بولین هستند: در پایتون، بسیاری از اشیاء در یک زمینه بولین (مانند if) ارزش "بولین-مانند" دارند. از جمله این اشیاء، مجموعه‌ها (لیست‌ها، دیکشنری‌ها، مجموعه‌ها و...) هستند که در صورت خالی بودن "کاذب" و در صورت داشتن موارد "صادق" هستند.

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 اودوو ارائه شده است.

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

گسترش دادن زمینه (Context)

زمینه (Context) یک 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

هشدار

عبور دادن پارامتر در زمینه می‌تواند اثرات جانبی خطرناکی داشته باشد.

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

اگر نیاز به ایجاد یک کلید زمینه (context) دارید که بر رفتار برخی از اشیاء تأثیر می‌گذارد، یک نام مناسب انتخاب کنید و در نهایت آن را با نام ماژول پیشوند دهید تا تأثیر آن جدا شود. یک مثال خوب کلیدهای ماژول 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() را در هر کجایی فراخوانی کنید، احتمال زیادی وجود دارد که سیستم را به روش‌های مختلف خراب کنید، زیرا این کار باعث تعهدهای جزئی و در نتیجه بازگشت‌های جزئی و نادرست خواهد شد و در میان دیگر موارد:

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

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

  3. تست‌هایی که نمی‌توان آن‌ها را به‌طور کامل بازگرداند و شروع به آلودگی پایگاه‌داده می‌کنند و خطاها را ایجاد می‌کنند (این حتی در صورتی که هیچ خطایی در طول تراکنش رخ ندهد هم صادق است)

اینجا یک قاعده بسیار ساده وجود دارد:

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

و اگر شما خودتان یک اشاره‌گر ایجاد کرده‌اید، باید موارد خطا و بازگشت صحیح را مدیریت کنید، و همچنین پس از اتمام کار، اشاره‌گر را به‌درستی ببندید.

و برخلاف باور عمومی، شما حتی نیازی به فراخوانی cr.commit() در موارد زیر ندارید: - در متد _auto_init() از شیء models.Model: این کار توسط متد راه‌اندازی افزودنی‌ها یا تراکنش ORM هنگام ایجاد مدل‌های سفارشی انجام می‌شود - در گزارش‌ها: commit() توسط چارچوب نیز مدیریت می‌شود، بنابراین می‌توانید حتی از داخل یک گزارش پایگاه‌داده را به‌روزرسانی کنید - در متدهای models.Transient: این متدها دقیقاً مانند متدهای عادی models.Model فراخوانی می‌شوند، در یک تراکنش و با فراخوانی cr.commit()/rollback() در انتها - و غیره (برای اطمینان به قاعده کلی بالا مراجعه کنید!)

تمامی فراخوانی‌های cr.commit() خارج از چارچوب سرور از این پس باید دارای یک توضیح صریح باشند که توضیح دهد چرا آن‌ها کاملاً ضروری هستند، چرا صحیح هستند و چرا تراکنش‌ها را خراب نمی‌کنند. در غیر این صورت می‌توانند و حذف خواهند شد!

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

Odoo uses a GetText-like method named "underscore" _() to indicate that a static string used in the code needs to be translated at runtime. That method is available at self.env._ using the language of the environment.

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

در اصل، این روش باید فقط برای رشته‌های استاتیک که به صورت دستی در کد نوشته شده‌اند استفاده شود. این روش برای ترجمه مقادیر فیلدها مانند نام محصولات و غیره کار نخواهد کرد. این کار باید به‌جای آن با استفاده از پرچم ترجمه روی فیلد مربوطه انجام شود.

The method accepts optional positional or named parameter The rule is very simple: calls to the underscore method should always be in the form self.env._('literal string') and nothing else:

_ = 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)

همچنین به یاد داشته باشید که مترجمان باید با مقادیر استاتیکی که به تابع underscore ارسال می‌شوند کار کنند، بنابراین لطفاً سعی کنید آن‌ها را به‌گونه‌ای بنویسید که فهم آن‌ها آسان باشد و کاراکترها و قالب‌بندی‌های زائد را به حداقل برسانید. مترجمان باید از الگوهای قالب‌بندی مانند %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)

به‌طور کلی در اودوو، هنگام دستکاری رشته‌ها، ترجیحاً از % به‌جای .format() (وقتی فقط یک متغیر برای جایگزینی در رشته وجود دارد) و از %(varname) به‌جای موقعیت (وقتی چندین متغیر باید جایگزین شوند) استفاده کنید. این کار ترجمه را برای مترجمان جامعه آسان‌تر می‌کند.

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

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

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

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

  • کلاس‌های پایتون در اودوو: از 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_<field_name> است

    • متد محدودیت: الگوی متد محدودیت به شکل _check_<constraint_name> است

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

  • در یک مدل، ترتیب ویژگی‌ها باید به شکل زیر باشد
    1. ویژگی‌های خصوصی (_name, _description, _inherit, _sql_constraints, ...)

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

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

    4. روش‌های محاسبه، معکوس و جستجو به همان ترتیب تعریف فیلد

    5. روش انتخاب (روش‌هایی که برای بازگرداندن مقادیر محاسبه‌شده برای فیلدهای انتخابی استفاده می‌شود)

    6. روش‌های محدودیت (@api.constrains) و روش‌های تغییر (@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(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):
        ...

جاوا اسکریپت

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

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

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

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

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

    • static/lib: اینجا مکانی است که کتابخانه‌های جاوا اسکریپت باید قرار بگیرند، در یک زیرپوشه. برای مثال، همه فایل‌های کتابخانه 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: اینجا مکانی است که همه فایل‌های تست تور قرار می‌گیرند (نه آموزش‌ها).

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

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

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

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

  • برای اعلام کلاس‌ها از camelcase استفاده کنید

راهنمای جامع‌تری برای JS در github wiki  <https://github.com/odoo/odoo/wiki/Javascript-coding-guidelines> ارائه شده است. همچنین می‌توانید به API‌های موجود در جاوا اسکریپت نگاه کنید.

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

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

  • آکولاد باز ({): فضای خالی بعد از آخرین سلکتور؛

  • آکولاد بسته (}): در خط جدید خود؛

  • هر اعلامیه در یک خط؛

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

"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",
    }
},

ترتیب ویژگی‌ها

ترتیب ویژگی‌ها از بیرون به داخل، از position شروع کنید و با قوانین تزئینی (مانند font، filter و غیره) به پایان برسانید.

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

.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 در سخت‌گیری بیشتر، شفافیت و اطلاع‌رسانی بهتر کد بسیار مفید هستند.

از استفاده از انتخابگرهای id خودداری کنید و کلاس‌های خود را با پیشوند o_<module_name> مشخص کنید، که <module_name> نام فنی ماژول است (مانند sale، im_chat و ...).
تنها استثناء برای این قانون webclient است: به سادگی از پیشوند o_ استفاده می‌کند.

از ایجاد کلاس‌ها و نام‌های متغیر بیش از حد خاص خودداری کنید. هنگام نام‌گذاری عناصر تو در تو، روش "Grandchild" را انتخاب کنید.

مثال

نکنید

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

    یک تغییر‌دهنده اختیاری.

مثال

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

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

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

مثال

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

   margin-right: $-inner-gap;

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

همچنین ملاحظه نمائید

دامنه متغیرها در مستندات SASS

میکسین‌ها و توابع SCSS

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

استدلال‌های اختیاری را در قالب متغیرهای محدود نام‌گذاری کنید، به این صورت $-[argument].

مثال

@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

در اودو، استفاده از متغیرهای CSS به‌صورت دقیقاً مرتبط با DOM است. از آنها برای تطبیق طراحی و چیدمان به‌صورت متنی استفاده کنید.

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

  • [root]

    یا نام مؤلفه یا نام ماژول (مؤلفه‌ها اولویت دارند).

  • [element]

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

  • [property]

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

  • [modifier]

    یک تغییر‌دهنده اختیاری.

مثال

.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

در اودو، استفاده از متغیرهای CSS دقیقاً به DOM مرتبط است، یعنی برای تطبیق طراحی و چیدمان در متن خاص استفاده می‌شوند و نه برای مدیریت سیستم طراحی جهانی. این موارد معمولاً هنگامی که ویژگی‌های یک مؤلفه می‌توانند در متن‌های خاص یا شرایط دیگر متفاوت باشند استفاده می‌شود.

ما این ویژگی‌ها را در داخل بلوک اصلی مؤلفه تعریف می‌کنیم و مقدارهای پیش‌فرض را ارائه می‌دهیم.

مثال

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 در مستندات وب MDN

متغیرهای CSS و SCSS

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

در اودو، ما بهترین‌های هر دو جهان را می‌گیریم: از متغیرهای SCSS برای تعریف سیستم طراحی استفاده می‌کنیم و برای تطبیق‌های متنی از متغیرهای CSS بهره می‌بریم.

پیاده‌سازی مثال قبلی باید با افزودن متغیرهای SCSS بهبود یابد تا کنترل در سطح بالا حفظ شود و سازگاری با سایر مؤلفه‌ها تضمین شود.

مثال

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 بر روی کلاس کاذب :root روشی است که ما معمولاً در UI اودو استفاده نمی‌کنیم. این روش معمولاً برای دسترسی و تغییر متغیرهای CSS در سطح جهانی استفاده می‌شود. ما این کار را به جای آن با استفاده از SCSS انجام می‌دهیم.

استثنائات این قانون باید کاملاً مشخص باشند، مانند قالب‌های مشترک در بسته‌هایی که نیاز به سطح خاصی از آگاهی متنی دارند تا به درستی نمایش داده شوند.