Hướng dẫn viết mã¶
Trang này giới thiệu Hướng dẫn viết mã của Odoo (Odoo Coding Guidelines). Các hướng dẫn này nhằm cải thiện chất lượng mã nguồn của các ứng dụng Odoo. Thật vậy, mã nguồn đúng chuẩn giúp cải thiện khả năng đọc hiểu, giảm bớt công sức bảo trì, hỗ trợ việc gỡ lỗi (debug), giảm độ phức tạp và nâng cao độ tin cậy. Các hướng dẫn này cần được áp dụng cho mọi module mới và cho tất cả các phát triển mới.
Cảnh báo
Khi chỉnh sửa các tệp hiện có trong phiên bản stable, phong cách gốc của tệp sẽ luôn được ưu tiên tuyệt đối hơn bất kỳ hướng dẫn phong cách nào khác. Nói cách khác, không bao giờ được chỉnh sửa các tệp hiện có chỉ để áp dụng các hướng dẫn này. Điều này giúp tránh làm xáo trộn lịch sử chỉnh sửa (revision history) của các dòng mã. Diff nên được giữ ở mức tối thiểu. Để biết thêm chi tiết, hãy xem hướng dẫn pull request của chúng tôi.
Cảnh báo
Khi chỉnh sửa các tệp hiện có trong phiên bản master (phát triển), chỉ áp dụng các hướng dẫn này cho phần mã đã được chỉnh sửa hoặc khi phần lớn tệp đang được sửa đổi. Nói cách khác, chỉ thay đổi cấu trúc của các tệp hiện có nếu tệp đó đang trải qua những thay đổi lớn. Trong trường hợp đó, trước tiên hãy thực hiện một commit move, sau đó mới áp dụng các thay đổi liên quan đến tính năng.
Cấu trúc module¶
Cảnh báo
Đối với các module do cộng đồng phát triển, chúng tôi khuyến nghị mạnh mẽ việc đặt tên module với một tiền tố (prefix) như tên công ty của bạn.
Các thư mục¶
Một module được tổ chức thành các thư mục quan trọng. Các thư mục này chứa logic nghiệp vụ; chỉ cần xem qua chúng là bạn có thể hiểu được mục đích của module.
data/ : các tệp xml demo và data
models/ : định nghĩa các model
controllers/ : chứa các controller (các route HTTP)
views/ : chứa các view và template
static/ : chứa các tài nguyên web (web assets), được phân tách thành css/, js/, img/, lib/, ...
Các thư mục tùy chọn khác cấu thành nên module.
wizard/ : tập hợp các transient model (
models.TransientModel) và các view của chúngreport/ : chứa các báo cáo có thể in (printable report) và các model dựa trên SQL view. Các đối tượng Python và các XML view được đặt trong thư mục này
tests/ : chứa các bài kiểm thử (test) Python
Đặt tên tệp¶
Việc đặt tên tệp rất quan trọng để nhanh chóng tìm thấy thông tin trong tất cả các addon của odoo. Phần này giải thích cách đặt tên tệp trong một module odoo chuẩn. Ví dụ, chúng ta sẽ sử dụng ứng dụng plant nursery. Ứng dụng này có hai model chính là plant.nursery và plant.order.
Về models, hãy chia logic nghiệp vụ thành các nhóm model thuộc cùng một model chính. Mỗi nhóm nằm trong một tệp riêng được đặt tên dựa trên model chính của nhóm đó. Nếu chỉ có một model, tên của tệp sẽ giống với tên module. Mỗi model kế thừa (inherited model) nên nằm trong một tệp riêng để giúp dễ hiểu các model bị ảnh hưởng.
addons/plant_nursery/
|-- models/
| |-- plant_nursery.py (first main model)
| |-- plant_order.py (another main model)
| |-- res_partner.py (inherited Odoo model)
Về security, nên sử dụng ba tệp chính sau:
Tệp đầu tiên là định nghĩa các quyền truy cập (access right), được thực hiện trong tệp
ir.model.access.csv.Các nhóm người dùng (user group) được định nghĩa trong
<module>_groups.xml.Các quy tắc bản ghi (record rule) được định nghĩa trong
<model>_security.xml.
addons/plant_nursery/
|-- security/
| |-- ir.model.access.csv
| |-- plant_nursery_groups.xml
| |-- plant_nursery_security.xml
| |-- plant_order_security.xml
Về views, các view backend nên được chia tương tự như models và có hậu tố _views.xml. Các view backend là các view list, form, kanban, activity, graph, pivot, ... Để dễ dàng chia theo model trong các view, các menu chính không liên kết với action cụ thể có thể được tách ra thành một tệp tùy chọn <module>_menus.xml. Các template (các trang QWeb được sử dụng đặc biệt cho hiển thị portal / website) được đặt trong các tệp riêng có tên <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
Về data, hãy chia chúng theo mục đích (demo hoặc data) và theo model chính. Tên tệp sẽ là tên main_model với hậu tố _demo.xml hoặc _data.xml. Ví dụ, đối với một ứng dụng có dữ liệu demo và data cho model chính của nó cũng như các subtype, activity và mail template đều liên quan đến module mail:
addons/plant_nursery/
|-- data/
| |-- plant_nursery_data.xml
| |-- plant_nursery_demo.xml
| |-- mail_data.xml
Về controllers, thông thường tất cả các controller thuộc về một controller duy nhất được chứa trong một tệp có tên <module_name>.py. Một quy ước cũ trong Odoo là đặt tên tệp này là main.py nhưng cách này hiện được xem là lỗi thời. Nếu bạn cần kế thừa một controller hiện có từ một module khác, hãy thực hiện việc đó trong <inherited_module_name>.py. Ví dụ, việc thêm controller portal trong một ứng dụng được thực hiện trong portal.py.
addons/plant_nursery/
|-- controllers/
| |-- plant_nursery.py
| |-- portal.py (inheriting portal/controllers/portal.py)
| |-- main.py (deprecated, replaced by plant_nursery.py)
Về static files, các tệp Javascript nhìn chung tuân theo cùng logic như các model python. Mỗi thành phần (component) nên nằm trong tệp riêng của nó với một tên có ý nghĩa. Ví dụ, các widget activity nằm trong activity.js của module mail. Các thư mục con cũng có thể được tạo để cấu trúc 'package' (xem module web để biết thêm chi tiết). Logic tương tự nên được áp dụng cho các template của các widget JS (các tệp XML tĩnh) và cho các style của chúng (các tệp scss). Không liên kết dữ liệu (hình ảnh, thư viện) bên ngoài Odoo: không sử dụng URL đến một hình ảnh mà thay vào đó hãy sao chép nó vào trong mã nguồn.
Về wizards, quy ước đặt tên giống như đối với các model python: <transient>.py và <transient>_views.xml. Cả hai đều được đặt trong thư mục wizard. Cách đặt tên này xuất phát từ các ứng dụng odoo cũ sử dụng từ khóa wizard cho các transient model.
addons/plant_nursery/
|-- wizard/
| |-- make_plant_order.py
| |-- make_plant_order_views.xml
Về statistics reports được thực hiện bằng python / SQL view và các view cổ điển, cách đặt tên như sau:
addons/plant_nursery/
|-- report/
| |-- plant_order_report.py
| |-- plant_order_report_views.xml
Về printable reports chủ yếu chứa việc chuẩn bị dữ liệu và các template Qweb, cách đặt tên như sau:
addons/plant_nursery/
|-- report/
| |-- plant_order_reports.xml (report actions, paperformat, ...)
| |-- plant_order_templates.xml (xml report templates)
Do đó, cây thư mục hoàn chỉnh của module Odoo của chúng ta sẽ trông như sau
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
Ghi chú
Tên tệp chỉ nên chứa [a-z0-9_] (các ký tự chữ và số viết thường cùng với _)
Cảnh báo
Sử dụng quyền tệp (file permission) đúng: thư mục 755 và tệp 644.
Các tệp XML¶
Định dạng¶
Để khai báo một record trong XML, nên sử dụng ký hiệu record (sử dụng <record>):
Đặt thuộc tính
idtrướcmodelĐối với khai báo field, thuộc tính
nameđứng trước tiên. Sau đó đặt value trong thẻfieldhoặc trong thuộc tínheval, và cuối cùng là các thuộc tính khác (widget, options, ...) được sắp xếp theo mức độ quan trọng.Cố gắng nhóm các record theo model. Trong trường hợp có sự phụ thuộc giữa action/menu/views, quy ước này có thể không áp dụng được.
Sử dụng quy ước đặt tên được định nghĩa ở phần tiếp theo
Thẻ <data> chỉ được sử dụng để thiết lập dữ liệu không thể cập nhật (not-updatable) với
noupdate=1. Nếu trong tệp chỉ có dữ liệu không thể cập nhật,noupdate=1có thể được đặt trên thẻ<odoo>và không cần đặt thẻ<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 hỗ trợ các thẻ tùy chỉnh (custom tag) đóng vai trò như syntactic sugar:
menuitem: sử dụng nó như một lối tắt để khai báo một
ir.ui.menutemplate: sử dụng nó để khai báo một QWeb View chỉ yêu cầu phần
archcủa view.
Các thẻ này được ưu tiên sử dụng hơn ký hiệu record.
XML ID và cách đặt tên¶
Security, View và Action¶
Sử dụng mẫu (pattern) sau:
Đối với một menu:
<model_name>_menu, hoặc<model_name>_menu_do_stuffcho các submenu.Đối với một view:
<model_name>_view_<view_type>, trong đó view_type làkanban,form,tree,search, ...Đối với một action: action chính tuân theo
<model_name>_action. Các action khác có hậu tố_<detail>, trong đó detail là một chuỗi ký tự viết thường mô tả ngắn gọn về action. Cách này chỉ được sử dụng nếu có nhiều action được khai báo cho model.Đối với các window action: thêm hậu tố cho tên action bằng thông tin view cụ thể như
<model_name>_action_view_<view_type>.Đối với một group:
<module_name>_group_<group_name>trong đó group_name là tên của group, thường là 'user', 'manager', ...Đối với một rule:
<model_name>_rule_<concerned_group>trong đó concerned_group là tên viết tắt của group liên quan ('user' cho 'model_name_group_user', 'public' cho người dùng công khai, 'company' cho các rule đa công ty, ...).
Tên (name) nên giống hệt với xml id, với dấu chấm thay thế cho dấu gạch dưới. Các action nên có tên thực sự vì nó được dùng làm tên hiển thị (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>
Kế thừa XML¶
Xml Id của các view kế thừa nên sử dụng cùng ID với record gốc. Điều này giúp dễ dàng tìm ra tất cả các kế thừa chỉ bằng một cái nhìn thoáng qua. Vì Xml Id cuối cùng được thêm tiền tố bởi module tạo ra chúng nên sẽ không có sự trùng lặp.
Tên nên chứa hậu tố .inherit.{details} để dễ hiểu mục đích ghi đè (override) khi nhìn vào tên.
<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>
Các view chính (primary view) mới không cần hậu tố inherit vì đó là các record mới được xây dựng dựa trên record đầu tiên.
<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¶
Cảnh báo
Đừng quên đọc thêm phần Những cạm bẫy về bảo mật để viết mã an toàn.
Các tùy chọn PEP8¶
Sử dụng một linter có thể giúp hiển thị các cảnh báo hoặc lỗi về cú pháp và ngữ nghĩa. Mã nguồn Odoo cố gắng tuân thủ chuẩn Python, nhưng một số quy tắc có thể được bỏ qua.
E501: dòng quá dài
E301: cần 1 dòng trống, nhưng không có dòng nào
E302: cần 2 dòng trống, nhưng chỉ có 1 dòng
Import¶
Các import được sắp xếp theo thứ tự như sau
Các thư viện bên ngoài (mỗi thư viện một dòng, được sắp xếp và tách khỏi thư viện chuẩn python - python stdlib)
Import của
odooImport từ các module Odoo (hiếm khi, và chỉ khi cần thiết)
Trong 3 nhóm này, các dòng import được sắp xếp theo thứ tự bảng chữ cái.
# 1 : imports of python lib
import base64
import re
import time
from datetime import datetime
# 2 : imports of odoo
import odoo
from odoo import Command, _, api, fields, models # ASCIIbetically 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
Các quy tắc lập trình thành thục (Python)¶
Luôn ưu tiên khả năng đọc hiểu (readability) hơn là tính ngắn gọn (conciseness) hoặc việc sử dụng các tính năng hay idiom của ngôn ngữ.
Không sử dụng
.clone()
# bad
new_dict = my_dict.clone()
new_list = old_list.clone()
# good
new_dict = dict(my_dict)
new_list = list(old_list)
Dictionary trong Python: tạo và cập nhật
# -- 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)
Sử dụng tên biến/class/method có ý nghĩa
Biến không cần thiết: Các biến tạm (temporary variable) có thể giúp mã nguồn rõ ràng hơn bằng cách đặt tên cho các đối tượng, nhưng điều đó không có nghĩa là bạn nên tạo biến tạm mọi lúc:
# pointless
schema = kw['schema']
params = {'schema': schema}
# simpler
params = {'schema': kw['schema']}
Có nhiều điểm return là hoàn toàn ổn, khi điều đó giúp đơn giản hơn
# 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
Nắm rõ các builtin: Bạn nên có ít nhất một hiểu biết cơ bản về tất cả các builtin của Python (http://docs.python.org/library/functions.html)
value = my_dict.get('key', None) # very very redundant
value = my_dict.get('key') # good
Ngoài ra, if 'key' in my_dict và if my_dict.get('key') có ý nghĩa rất khác nhau, hãy chắc chắn rằng bạn đang sử dụng đúng cách phù hợp.
Học cách sử dụng list comprehension: Sử dụng list comprehension, dict comprehension, và các thao tác cơ bản bằng
map,filter,sum, ... Chúng giúp mã nguồn dễ đọc hơn.
# not very good
cube = []
for i in res:
cube.append((i['id'],i['name']))
# better
cube = [(i['id'], i['name']) for i in res]
Collection cũng có thể được xem như boolean: Trong python, nhiều đối tượng có giá trị mang tính "boolean" khi được đánh giá trong ngữ cảnh boolean (chẳng hạn như trong câu lệnh if). Trong số đó có các collection (list, dict, set, ...) mang giá trị "falsy" khi rỗng và "truthy" khi chứa các phần tử:
bool([]) is False
bool([1]) is True
bool([False]) is True
Vì vậy, bạn có thể viết if some_collection: thay vì if len(some_collection):.
Duyệt qua (iterate) các 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..."
Sử dụng 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)
Là một lập trình viên tốt, hãy viết tài liệu cho mã nguồn của bạn (docstring cho các method, các comment đơn giản cho những phần mã phức tạp)
Ngoài các hướng dẫn này, bạn cũng có thể thấy liên kết sau đây thú vị: https://david.goodger.org/projects/pycon/2007/idiomatic/handout.html (hơi lỗi thời một chút, nhưng vẫn khá phù hợp)
Lập trình trong Odoo¶
Tránh tạo các generator và decorator: chỉ sử dụng những cái được cung cấp bởi Odoo API.
Cũng như trong python, hãy sử dụng các method
filtered,mapped,sorted, ... để giúp dễ đọc mã nguồn hơn và cải thiện hiệu năng.
Truyền (propagate) context¶
Context là một frozendict không thể chỉnh sửa. Để gọi một method với một context khác, nên sử dụng 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
Cảnh báo
Việc truyền tham số trong context có thể gây ra những tác dụng phụ (side-effect) nguy hiểm.
Vì các giá trị được truyền đi một cách tự động, một số hành vi ngoài ý muốn có thể xuất hiện. Việc gọi method create() của một model với khóa default_my_field trong context sẽ đặt giá trị mặc định cho my_field của model liên quan. Nhưng nếu trong quá trình tạo này, các đối tượng khác (chẳng hạn như sale.order.line, khi tạo sale.order) có một field tên là my_field cũng được tạo ra, thì giá trị mặc định của chúng cũng sẽ được thiết lập.
Nếu bạn cần tạo một khóa context ảnh hưởng đến hành vi của một đối tượng nào đó, hãy chọn một tên tốt, và nếu cần thì thêm tiền tố là tên của module để cô lập tác động của nó. Một ví dụ điển hình là các khóa của module mail: mail_create_nosubscribe, mail_notrack, mail_notify_user_signature, ...
Suy nghĩ theo hướng có thể mở rộng (extendable)¶
Các function và method không nên chứa quá nhiều logic: có nhiều method nhỏ và đơn giản sẽ tốt hơn là có ít method lớn và phức tạp. Một quy tắc chung tốt là hãy tách một method ngay khi nó đảm nhận nhiều hơn một trách nhiệm (xem http://en.wikipedia.org/wiki/Single_responsibility_principle).
Nên tránh việc hardcode logic nghiệp vụ trong một method vì điều đó khiến cho việc mở rộng bởi một submodule trở nên khó khăn.
# 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')
Đoạn mã trên được thiết kế có khả năng mở rộng quá mức nhằm mục đích minh họa, nhưng khả năng đọc hiểu cũng cần được xem xét và cần có một sự đánh đổi (tradeoff) hợp lý.
Ngoài ra, hãy đặt tên cho các function của bạn một cách phù hợp: các function nhỏ và được đặt tên đúng cách là điểm khởi đầu cho mã nguồn dễ đọc/dễ bảo trì và tài liệu chặt chẽ hơn.
Khuyến nghị này cũng phù hợp đối với các class, tệp, module và package. (Xem thêm http://en.wikipedia.org/wiki/Cyclomatic_complexity)
Không bao giờ commit transaction¶
Framework Odoo chịu trách nhiệm cung cấp ngữ cảnh giao dịch (transactional context) cho tất cả các lệnh gọi RPC. Nguyên tắc là một con trỏ cơ sở dữ liệu (database cursor) mới được mở ra khi bắt đầu mỗi lệnh gọi RPC, và được commit khi lệnh gọi đã trả về, ngay trước khi truyền kết quả trả lời cho client RPC, gần giống như sau:
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
Nếu có bất kỳ lỗi nào xảy ra trong quá trình thực thi lệnh gọi RPC, transaction sẽ được rollback một cách nguyên tử (atomically), giữ nguyên trạng thái của hệ thống.
Tương tự, hệ thống cũng cung cấp một transaction riêng biệt trong quá trình thực thi các bộ test (test suite), do đó nó có thể được rollback hoặc không tùy thuộc vào các tùy chọn khởi động server.
Hệ quả là nếu bạn tự gọi cr.commit() ở bất kỳ đâu, rất có khả năng bạn sẽ làm hỏng hệ thống theo nhiều cách khác nhau, vì bạn sẽ gây ra các commit không đầy đủ (partial commit), và do đó dẫn đến các rollback không đầy đủ và không sạch sẽ, gây ra, trong số những vấn đề khác:
dữ liệu nghiệp vụ không nhất quán, thường dẫn đến mất dữ liệu
workflow bị mất đồng bộ, các document bị kẹt vĩnh viễn
các test không thể được rollback một cách sạch sẽ, và sẽ bắt đầu làm ô nhiễm cơ sở dữ liệu, cũng như gây ra lỗi (điều này đúng ngay cả khi không có lỗi nào xảy ra trong quá trình transaction)
- Đây là một quy tắc rất đơn giản:
Bạn KHÔNG BAO GIỜ nên tự gọi
cr.commit(), TRỪ KHI bạn đã tự tạo một database cursor riêng của mình một cách tường minh! Và những tình huống mà bạn cần làm điều đó là rất hiếm gặp!Và nhân tiện, nếu bạn đã tự tạo cursor của riêng mình, thì bạn cần xử lý các trường hợp lỗi và thực hiện rollback đúng cách, cũng như đóng cursor đúng cách khi đã sử dụng xong.
Và trái ngược với suy nghĩ phổ biến, bạn thậm chí không cần gọi cr.commit() trong các tình huống sau: - trong method _auto_init() của một đối tượng models.Model: việc này đã được xử lý bởi phương thức khởi tạo addon, hoặc bởi transaction của ORM khi tạo các custom model - trong report: commit() cũng được framework xử lý, vì vậy bạn có thể cập nhật cơ sở dữ liệu ngay cả từ bên trong một report - trong các method của models.Transient: các method này được gọi hoàn toàn giống như các method của models.Model thông thường, trong một transaction và với cr.commit()/rollback() tương ứng ở cuối - v.v. (xem quy tắc chung ở trên nếu bạn còn nghi ngờ!)
Kể từ nay, tất cả các lệnh gọi cr.commit() nằm ngoài server framework đều phải có một comment tường minh giải thích tại sao chúng thực sự cần thiết, tại sao chúng thực sự đúng đắn, và tại sao chúng không phá vỡ các transaction. Nếu không, chúng có thể và sẽ bị loại bỏ!
Sử dụng phương thức dịch (translation method) đúng cách¶
Odoo sử dụng một phương thức giống GetText có tên là "underscore" _( ) để chỉ ra rằng một chuỗi tĩnh (static string) được sử dụng trong mã nguồn cần được dịch khi chạy (runtime) bằng ngôn ngữ của context. Pseudo-method này được truy cập trong mã nguồn của bạn bằng cách import như sau:
from odoo import _
Một vài quy tắc rất quan trọng cần phải tuân theo khi sử dụng nó, để nó hoạt động đúng và tránh làm các bản dịch bị lấp đầy bởi những nội dung rác vô ích.
Về cơ bản, phương thức này chỉ nên được sử dụng cho các chuỗi tĩnh được viết thủ công trong mã nguồn, nó sẽ không hoạt động để dịch các giá trị field, chẳng hạn như tên Sản phẩm, v.v. Việc này thay vào đó phải được thực hiện bằng cách sử dụng cờ (flag) translate trên field tương ứng.
Phương thức này chấp nhận tham số vị trí (positional) hoặc tham số có tên (named) tùy chọn. Quy tắc rất đơn giản: các lệnh gọi đến phương thức underscore phải luôn có dạng _('literal string') và không gì khác:
# 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)
Ngoài ra, hãy ghi nhớ rằng các dịch giả (translator) sẽ phải làm việc với các giá trị literal được truyền vào hàm underscore, vì vậy hãy cố gắng làm cho chúng dễ hiểu và giảm thiểu tối đa các ký tự cũng như định dạng thừa. Các dịch giả cần lưu ý rằng các mẫu định dạng (formatting pattern) như %s hoặc %d, các ký tự xuống dòng, v.v. cần được giữ nguyên, nhưng điều quan trọng là phải sử dụng chúng theo cách hợp lý và rõ ràng:
# 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)
Nói chung trong Odoo, khi thao tác với chuỗi (string), hãy ưu tiên sử dụng % thay vì .format() (khi chỉ có một biến cần thay thế trong chuỗi), và ưu tiên %(varname) thay vì theo vị trí (khi có nhiều biến cần được thay thế). Điều này giúp việc dịch thuật dễ dàng hơn cho các dịch giả cộng đồng.
Ký hiệu và Quy ước¶
- Tên Model (sử dụng ký hiệu dấu chấm, tiền tố bằng tên module):
Khi định nghĩa một Model Odoo: sử dụng dạng số ít của tên (res.partner và sale.order thay vì res.partnerS và saleS.orderS)
Khi định nghĩa một Transient (wizard) của Odoo: sử dụng
<related_base_model>.<action>trong đó related_base_model là model cơ sở (được định nghĩa trong models/) liên quan đến transient, còn action là tên ngắn gọn mô tả việc transient đó thực hiện. Tránh dùng từ wizard. Ví dụ:account.invoice.make,project.task.delegate.batch, ...Khi định nghĩa model report (ví dụ: SQL views): sử dụng
<related_base_model>.report.<action>, dựa trên quy ước của Transient.
Lớp Python của Odoo: sử dụng Pascal case (theo phong cách hướng đối tượng).
class AccountInvoice(models.Model):
...
- Tên biến:
sử dụng Pascal case cho biến model
sử dụng ký hiệu gạch dưới chữ thường cho biến thông thường.
thêm hậu tố _id hoặc _ids vào tên biến nếu nó chứa id của một record hoặc danh sách id. Không dùng
partner_idđể chứa một record của res.partner
Partner = self.env['res.partner']
partners = Partner.browse(ids)
partner_id = partners[0].id
Các trường
One2ManyvàMany2Manyluôn phải có hậu tố _ids (ví dụ: sale_order_line_ids)Các trường
Many2Onephải có hậu tố _id (ví dụ: partner_id, user_id, ...)- Quy ước phương thức
Trường Compute: mẫu tên phương thức compute là _compute_<field_name>
Phương thức Search: mẫu tên phương thức search là _search_<field_name>
Phương thức Default: mẫu tên phương thức default là _default_<field_name>
Phương thức Selection: mẫu tên phương thức selection là _selection_<field_name>
Phương thức Onchange: mẫu tên phương thức onchange là _onchange_<field_name>
Phương thức Constraint: mẫu tên phương thức constraint là _check_<constraint_name>
Phương thức Action: một phương thức action của object có tiền tố action_. Vì nó chỉ sử dụng một record duy nhất, hãy thêm
self.ensure_one()vào đầu phương thức.
- Trong một Model, thứ tự các thuộc tính nên là
Các thuộc tính riêng tư (
_name,_description,_inherit,_sql_constraints, ...)Phương thức Default và
default_getKhai báo trường
Các phương thức compute, inverse và search theo cùng thứ tự với khai báo trường
Phương thức Selection (các phương thức dùng để trả về giá trị được tính toán cho các trường selection)
Các phương thức constrains (
@api.constrains) và phương thức onchange (@api.onchange)Các phương thức CRUD (ghi đè ORM)
Các phương thức Action
Và cuối cùng là các phương thức nghiệp vụ khác.
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¶
Tổ chức tệp tĩnh¶
Các addon của Odoo có một số quy ước về cách tổ chức các tệp khác nhau. Ở đây chúng tôi giải thích chi tiết hơn cách các web asset nên được tổ chức.
Điều đầu tiên cần biết là máy chủ Odoo sẽ phục vụ (dưới dạng tĩnh) tất cả các tệp nằm trong thư mục static/, nhưng có tiền tố là tên addon. Vậy nên, ví dụ, nếu một tệp nằm ở addons/web/static/src/js/some_file.js, thì nó sẽ có thể truy cập tĩnh tại url your-odoo-server.com/web/static/src/js/some_file.js
Quy ước là tổ chức mã nguồn theo cấu trúc sau:
static: tất cả các tệp tĩnh nói chung
static/lib: đây là nơi các thư viện js nên được đặt, trong một thư mục con. Vậy nên, ví dụ, tất cả các tệp của thư viện jquery nằm trong addons/web/static/lib/jquery
static/src: thư mục mã nguồn tĩnh chung
static/src/css: tất cả các tệp css
static/fonts
static/img
static/src/js
static/src/js/tours: các tệp tour dành cho người dùng cuối (hướng dẫn, không phải test)
static/src/scss: các tệp scss
static/src/xml: tất cả các template qweb sẽ được render trong JS
static/tests: đây là nơi chúng ta đặt tất cả các tệp liên quan đến test.
static/tests/tours: đây là nơi chúng ta đặt tất cả các tệp test tour (không phải hướng dẫn).
Hướng dẫn viết mã Javascript¶
use strict;được khuyến nghị cho tất cả các tệp javascriptSử dụng một linter (jshint, ...)
Không bao giờ thêm các thư viện Javascript đã được minify
Sử dụng Pascal case để khai báo class
Các hướng dẫn JS chi tiết hơn được trình bày trong github wiki. Bạn cũng có thể tham khảo các API hiện có trong Javascript bằng cách xem Javascript References.
CSS và SCSS¶
Cú pháp và Định dạng¶
.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;
}
thụt lề bốn (4) khoảng trắng, không dùng tab;
mỗi dòng tối đa 80 ký tự;
dấu ngoặc mở (
{): có khoảng trắng sau selector cuối cùng;dấu ngoặc đóng (
}): nằm trên một dòng riêng;mỗi khai báo trên một dòng;
sử dụng khoảng trắng một cách có ý nghĩa.
"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",
}
},
Thứ tự thuộc tính¶
Sắp xếp các thuộc tính từ "bên ngoài" vào trong, bắt đầu từ position và kết thúc bằng các quy tắc trang trí (font, filter, v.v.).
Các biến SCSS có phạm vi (scoped) và các biến CSS phải được đặt ở trên cùng, theo sau là một dòng trống ngăn cách chúng với các khai báo khác.
.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);
}
Quy ước đặt tên¶
Các quy ước đặt tên trong CSS cực kỳ hữu ích trong việc giúp mã của bạn chặt chẽ, rõ ràng và giàu thông tin hơn.
id, và thêm tiền tố o_<module_name> cho các class của bạn, trong đó <module_name> là tên kỹ thuật của module (sale, im_chat, ...) hoặc route chính được module đó dành riêng (chủ yếu dành cho các module website, ví dụ: o_forum cho module website_forum).o_.Tránh tạo các class và tên biến quá cụ thể. Khi đặt tên cho các phần tử lồng nhau, hãy ưu tiên cách tiếp cận "Grandchild".
Example
Không nên
<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>
Nên
<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>
Bên cạnh việc gọn gàng hơn, cách tiếp cận này giúp việc bảo trì dễ dàng hơn vì nó hạn chế nhu cầu đổi tên khi có thay đổi xảy ra trên DOM.
Biến SCSS¶
Quy ước tiêu chuẩn của chúng ta là $o-[root]-[element]-[property]-[modifier], trong đó:
$o-Tiền tố.
[root]Là tên component hoặc tên module (component được ưu tiên).
[element]Một định danh tùy chọn cho các phần tử bên trong.
[property]Thuộc tính/hành vi được định nghĩa bởi biến.
[modifier]Một bổ nghĩa tùy chọn.
Example
$o-block-color: value;
$o-block-title-color: value;
$o-block-title-color-hover: value;
Biến SCSS (có phạm vi - scoped)¶
Các biến này được khai báo bên trong các block và không thể truy cập từ bên ngoài. Quy ước tiêu chuẩn của chúng ta là $-[variable name].
Example
.o_element {
$-inner-gap: compute-something;
margin-right: $-inner-gap;
.o_element_child {
margin-right: $-inner-gap * 0.5;
}
}
Xem thêm
Mixins và Functions trong SCSS¶
Quy ước tiêu chuẩn của chúng ta là o-[name]. Hãy sử dụng các tên mang tính mô tả. Khi đặt tên cho function, hãy dùng động từ ở dạng mệnh lệnh (ví dụ: get, make, apply...).
Đặt tên các tham số tùy chọn theo dạng biến có phạm vi (scoped), tức là $-[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);
}
Biến CSS¶
Trong Odoo, việc sử dụng biến CSS gắn chặt với DOM. Hãy dùng chúng để điều chỉnh thiết kế và bố cục một cách theo ngữ cảnh.
Quy ước tiêu chuẩn của chúng ta là BEM, tức là --[root]__[element]-[property]--[modifier], trong đó:
[root]Là tên component hoặc tên module (component được ưu tiên).
[element]Một định danh tùy chọn cho các phần tử bên trong.
[property]Thuộc tính/hành vi được định nghĩa bởi biến.
[modifier]Một bổ nghĩa tùy chọn.
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;
}
Cách sử dụng biến CSS¶
Trong Odoo, việc sử dụng biến CSS gắn chặt với DOM, nghĩa là chúng được dùng để điều chỉnh thiết kế và bố cục một cách theo ngữ cảnh thay vì để quản lý design-system toàn cục. Chúng thường được sử dụng khi các thuộc tính của một component có thể thay đổi tùy theo ngữ cảnh cụ thể hoặc các tình huống khác.
Chúng ta định nghĩa các thuộc tính này bên trong block chính của component, đồng thời cung cấp giá trị mặc định dự phòng (fallback).
Biến CSS và SCSS¶
Mặc dù nhìn có vẻ giống nhau, biến CSS và SCSS hoạt động rất khác nhau. Điểm khác biệt chính là, trong khi biến SCSS mang tính mệnh lệnh (imperative) và bị biên dịch mất đi, biến CSS mang tính khai báo (declarative) và được giữ lại trong kết quả cuối cùng.
Trong Odoo, chúng ta tận dụng ưu điểm của cả hai: sử dụng biến SCSS để định nghĩa design-system, đồng thời chọn biến CSS khi cần điều chỉnh theo ngữ cảnh.
Cách triển khai của ví dụ trước nên được cải thiện bằng cách thêm các biến SCSS nhằm có được sự kiểm soát ở cấp cao nhất và đảm bảo tính nhất quán với các component khác.
Pseudo-class :root¶
Việc định nghĩa biến CSS trên pseudo-class :root là một kỹ thuật mà chúng ta thường không sử dụng trong UI của Odoo. Cách làm này thường được dùng để truy cập và thay đổi biến CSS trên phạm vi toàn cục. Thay vào đó, chúng ta thực hiện việc này bằng SCSS.
Các ngoại lệ đối với quy tắc này thường khá rõ ràng, chẳng hạn như các template được chia sẻ giữa nhiều bundle và cần một mức độ nhận biết ngữ cảnh nhất định để có thể render đúng cách.