Hướng dẫn viết mã

Trang này giới thiệu Hướng dẫn viết mã của SoOn. Những hướng dẫn này nhằm cải thiện chất lượng mã của Ứng dụng SoOn. Thực tế, mã đúng cách cải thiện khả năng đọc, dễ bảo trì, giúp gỡ lỗi, giảm độ phức tạp và tăng tính tin cậy. Những hướng dẫn này nên được áp dụng cho mọi mô-đun mới và tất cả các phát triển mới.

Cảnh báo

Khi sửa đổi các tệp hiện có trong phiên bản ổn định, phong cách tệp gốc sẽ thay thế nghiêm ngặt bất kỳ hướng dẫn phong cách nào khác. Nói cách khác, xin đừng bao giờ sửa đổi các tệp hiện có để áp dụng những hướng dẫn này. Điều này tránh làm gián đoạn lịch sử sửa đổi của các dòng mã. Sự khác biệt 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 sửa đổi các tệp hiện có trong phiên bản master (phát triển), hãy áp dụng những hướng dẫn này cho mã hiện có chỉ đối với mã đã sửa đổi hoặc nếu phần lớn tệp đang được xem xét. Nói cách khác, sửa đổi cấu trúc tệp hiện có chỉ nếu nó đang trải qua thay đổi lớn. Trong trường hợp đó, đầu tiên hãy thực hiện một commit di chuyển sau đó áp dụng các thay đổi liên quan đến tính năng.

Cấu trúc mô-đun

Thư mục

Một mô-đun được tổ chức trong các thư mục quan trọng. Chúng chứa logic kinh doanh; nhìn vào chúng sẽ giúp bạn hiểu mục đích của mô-đun.

  • data/ : demo và dữ liệu xml

  • models/ : định nghĩa models

  • controllers/ : chứa controllers (các tuyến HTTP)

  • views/ : chứa các views và templates

  • static/ : chứa các tài sản web, tách thành css/, js/, img/, lib/, ...

Các thư mục tùy chọn khác tạo nên mô-đun.

  • wizard/ : tập hợp các mô hình tạm thời (models.TransientModel) và các views của chúng

  • report/ : chứa các báo cáo có thể in và các mô hình dựa trên SQL views. Các đối tượng Python và XML views được bao gồm trong thư mục này

  • tests/ : chứa các kiểm tra Python

Đặt tên tệp

Đặt tên tệp rất quan trọng để nhanh chóng tìm thông tin qua tất cả các addons của SoOn. Phần này giải thích cách đặt tên tệp trong một mô-đun SoOn tiêu chuẩn. Là một ví dụ, chúng tôi sử dụng ứng dụng vườn ươm cây. Nó giữ hai mô hình chính là plant.nurseryplant.order.

Liên quan đến models, chia logic kinh doanh theo các bộ models thuộc cùng một mô hình chính. Mỗi bộ nằm trong một tệp được đặt tên dựa trên mô hình chính của nó. Nếu chỉ có một mô hình, tên của nó giống với tên mô-đun. Mỗi mô hình được kế thừa nên nằm trong tệp riêng của nó để giúp hiểu rõ các mô hình 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)

Liên quan đến bảo mật, ba tệp chính nên được sử dụng:

  • Đầu tiên là định nghĩa quyền truy cập được thực hiện trong tệp ir.model.access.csv.

  • Nhóm người dùng được định nghĩa trong tệp <module>_groups.xml.

  • Quy tắc ghi chép được định nghĩa trong tệp <model>_security.xml.

addons/plant_nursery/
|-- security/
|   |-- ir.model.access.csv
|   |-- plant_nursery_groups.xml
|   |-- plant_nursery_security.xml
|   |-- plant_order_security.xml

Liên quan đến views, các views backend nên được tách ra như models và thêm hậu tố _views.xml. Các views backend là danh sách, biểu mẫu, kanban, hoạt động, biểu đồ, pivot, .. views. Để dễ dàng tách theo mô hình trong views, các menu chính không liên kết với hành động cụ thể có thể được trích xuất vào tệp <module>_menus.xml tùy chọn. Các mẫu (các trang QWeb được sử dụng đáng chú ý cho hiển thị cổng / trang web) được đặt trong các tệp riêng biệt 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

Liên quan đến data, chia chúng theo mục đích (demo hoặc data) và mô hình 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ó demo và data cho mô hình chính của nó cũng như các loại phụ, hoạt động và mẫu thư tất cả liên quan đến module thư:

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

Liên quan đến controllers, nói chung tất cả các controllers thuộc về một controller duy nhất chứa trong một tệp có tên <module_name>.py. Một quy ước cũ trong SoOn là đặt tên tệp này là main.py nhưng nó được coi là lỗi thời. Nếu bạn cần kế thừa một controller hiện có từ một mô-đun khác, hãy thực hiện trong tệp <inherited_module_name>.py. Ví dụ, thêm controller cổng vào một ứng dụng được thực hiện trong tệp portal.py.

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

Liên quan đến tệp tĩnh, các tệp Javascript tuân theo logic chung như các mô hình python. Mỗi thành phần 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 tiện ích hoạt động nằm trong activity.js của module thư. Các thư mục con cũng có thể được tạo để cấu trúc 'gói' (xem mô-đun web để biết thêm chi tiết). Logic tương tự cũng nên được áp dụng cho các mẫu của các tiện ích JS (các tệp XML tĩnh) và cho các kiểu của chúng (các tệp scss). Đừng liên kết dữ liệu (hình ảnh, thư viện) bên ngoài SoOn: không sử dụng URL đến một hình ảnh mà hãy sao chép nó vào mã nguồn thay thế.

Liên quan đến wizards, quy ước đặt tên giống như cho các mô hình python: <transient>.py<transient>_views.xml. Cả hai đều được đặt trong thư mục wizard. Quy ước đặt tên này xuất phát từ các ứng dụng SoOn cũ sử dụng từ khóa wizard cho các mô hình tạm thời.

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

Liên quan đến báo cáo thống kê được thực hiện với các views python / SQL và các views cổ điển, quy ước đặt tên như sau:

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

Liên quan đến báo cáo có thể in chứa chủ yếu là chuẩn bị dữ liệu và các mẫu Qweb, quy ước đặt tên như sau:

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

Cây hoàn chỉnh của mô-đun SoOn của chúng tôi do đó 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_] (chữ số thường và _)

Cảnh báo

Sử dụng quyền tệp đúng: thư mục 755 và tệp 644.

Các tệp XML

Định dạng

Để khai báo một bản ghi trong XML, ký hiệu record (sử dụng <record>) được khuyến nghị:

  • Đặt thuộc tính id trước model

  • Đối với khai báo trường, thuộc tính name đặt trước. Sau đó đặt giá trị vào thẻ field hoặc vào thuộc tính eval, và cuối cùng là các thuộc tính khác (widget, options, ...) được sắp xếp theo thứ tự quan trọng.

  • Cố gắng nhóm bản ghi theo mô hình. Trong trường hợp có sự phụ thuộc giữa hành động/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 ở điểm tiếp theo

  • Thẻ <data> chỉ được sử dụng để đặt dữ liệu không cập nhật được với noupdate=1. Nếu chỉ có dữ liệu không cập nhật được trong tệp, noupdate=1 có thể được đặt trên thẻ <odoo> và không đặ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>

SoOn hỗ trợ các thẻ tùy chỉnh hoạt động như đường cú pháp:

  • menuitem: sử dụng nó như một lối tắt để khai báo một ir.ui.menu

  • template: sử dụng nó để khai báo một QWeb View chỉ yêu cầu phần arch của view.

Những thẻ này được ưa chuộng hơn ký hiệu record.

ID XML và quy ước đặt tên

Bảo mật, View và Hành động

Sử dụng mẫu sau:

  • Đối với menu: <model_name>_menu, hoặc <model_name>_menu_do_stuff cho các menu con.

  • Đối với một view: <model_name>_view_<view_type>, nơi view_typekanban, form, tree, search, ...

  • Đối với một hành động: hành động chính tuân theo <model_name>_action. Những hành động khác được thêm hậu tố _<detail>, nơi detail là một chuỗi chữ thường giải thích ngắn gọn về hành động. Điều này chỉ được sử dụng nếu nhiều hành động được khai báo cho mô hình.

  • Đối với hành động cửa sổ: thêm hậu tố tên hành động bằng thông tin view cụ thể như <model_name>_action_view_<view_type>.

  • Đối với một nhóm: <module_name>_group_<group_name> nơi group_name là tên của nhóm, thường là 'user', 'manager', ...

  • Đối với một quy tắc: <model_name>_rule_<concerned_group> nơi concerned_group là tên ngắn của nhóm liên quan ('user' cho 'model_name_group_user', 'public' cho người dùng công cộng, 'company' cho các quy tắc đa công ty, ...).

Tên nên giống với id xml với các dấu chấm thay thế dấu gạch dưới. Các hành động nên có tên thật vì nó được sử dụng như tên hiển thị.

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

Các ID Xml của các views kế thừa nên sử dụng cùng ID như bản ghi gốc. Điều này giúp tìm thấy tất cả các kế thừa trong nháy mắt. Vì các ID Xml cuối cùng được tiền tố bởi mô-đun tạo ra chúng nên không có sự trùng lặp.

Tên nên chứa hậu tố .inherit.{details} để dễ dàng hiểu mục đích ghi đè khi nhìn vào tên của 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 views chính mới không yêu cầu hậu tố kế thừa vì chúng là các bản ghi mới dựa trên bản đầ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 phần Những cạm bẫy bảo mật để viết mã an toàn.

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 cú pháp và ngữ nghĩa. Mã nguồn SoOn cố gắng tuân thủ tiêu chuẩn Python, nhưng một số trong số đó có thể được bỏ qua.

  • E501: dòng quá dài

  • E301: mong đợi 1 dòng trống, tìm thấy 0

  • E302: mong đợi 2 dòng trống, tìm thấy 1

Nhập khẩu

Các mục nhập được sắp xếp theo thứ tự

  1. Thư viện bên ngoài (một dòng mỗi dòng được sắp xếp và tách trong stdlib python)

  2. Nhập khẩu của odoo

  3. Nhập khẩu từ các mô-đun SoOn (hiếm khi và chỉ khi cần thiết)

Bên trong ba nhóm này, các dòng nhập khẩu được sắp xếp theo thứ tự 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 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

Ngữ pháp của Lập trình (Python)

  • Luôn ưu tiên khả năng đọc hơn sự cô đọng hoặc sử dụng các tính năng ngôn ngữ hoặc thành ngữ.

  • Đừ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)
  • Từ điển 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 các tên biến/lớp/phương thức có ý nghĩa

  • Biến vô ích: Các biến tạm thời có thể làm cho mã 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 các biến tạm thời mọi lúc:

# pointless
schema = kw['schema']
params = {'schema': schema}
# simpler
params = {'schema': kw['schema']}
  • Các điểm trả về nhiều là OK, khi chúng đơ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
value = my_dict.get('key', None) # very very redundant
value = my_dict.get('key') # good

Ngoài ra, if 'key' in my_dictif 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.

  • Học list comprehensions: Sử dụng list comprehension, dict comprehension và các thao tác cơ bản bằng cách sử dụng map, filter, sum, ... Chúng làm cho mã 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]
  • Các bộ sưu tập cũng là các giá trị boolean: Trong python, nhiều đối tượng có giá trị "boolean-ish" khi được đánh giá trong ngữ cảnh boolean (chẳng hạn như một điều kiện if). Trong số này có các bộ sưu tập (lists, dicts, sets, ...) là "falsy" khi trống và "truthy" khi chứa các mục:

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):.

  • Lặp qua các đối tượng có thể lặp lại

# 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 nhà phát triển giỏi, hãy tài liệu hóa mã của bạn (chuỗi docstring trên các phương thức, các bình luận đơn giản cho phần mã khó hiểu)

  • Ngoài những hướng dẫn này, bạn cũng có thể thấy liên kết sau đây thú vị: http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html (một chút lỗi thời, nhưng khá liên quan)

Lập trình trong SoOn

  • Tránh tạo các generators và decorators: chỉ sử dụng những cái được cung cấp bởi API của SoOn.

  • Như trong python, sử dụng các phương thức filtered, mapped, sorted, ... để dễ dàng đọc mã và cải thiện hiệu suất.

Truyền bá ngữ cảnh

Ngữ cảnh là một frozendict không thể sửa đổi. Để gọi một phương thức với một ngữ cảnh khác, phương thức with_context nên được sử dụng:

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 ngữ cảnh có thể gây ra các tác dụng phụ nguy hiểm.

Vì các giá trị được truyền tự động, một số hành vi không mong đợi có thể xuất hiện. Việc gọi phương thức create() của một mô hình với khóa default_my_field trong ngữ cảnh sẽ đặt giá trị mặc định của my_field cho mô hình 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ó tên trường my_field được tạo, giá trị mặc định của chúng cũng sẽ được đặt.

Nếu bạn cần tạo một khóa ngữ cảnh ảnh hưởng đến hành vi của một số đối tượng, hãy chọn một tên tốt, và cuối cùng tiền tố nó bằng tên của mô-đun để cô lập tác động của nó. Một ví dụ tốt là các khóa của mô-đun mail: mail_create_nosubscribe, mail_notrack, mail_notify_user_signature, ...

Nghĩ đến khả năng mở rộng

Các hàm và phương thức không nên chứa quá nhiều logic: có nhiều phương thức nhỏ và đơn giản được khuyến nghị hơn là có ít phương thức lớn và phức tạp. Một quy tắc chung tốt là chia một phương thức ngay khi nó có hơn một trách nhiệm (xem http://en.wikipedia.org/wiki/Single_responsibility_principle).

Việc mã hóa cứng logic kinh doanh trong một phương thức nên tránh vì nó ngăn không cho dễ dàng mở rộng bởi một mô-đun phụ.

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

Mã ở trên quá khả năng mở rộng vì ví dụ nhưng khả năng đọc phải được xem xét và một sự thỏa hiệp phải được thực hiện.

Ngoài ra, đặt tên các hàm của bạn một cách phù hợp: các hàm nhỏ và được đặt tên đúng cách là điểm khởi đầu của mã dễ đọc/dễ bảo trì và tài liệu chặt chẽ hơn.

Khuyến nghị này cũng liên quan đến các lớp, tệp, mô-đun và gói. (Xem thêm http://en.wikipedia.org/wiki/Cyclomatic_complexity)

Không bao giờ commit giao dịch

Khung làm việc SoOn chịu trách nhiệm cung cấp ngữ cảnh giao dịch cho tất cả các cuộc gọi RPC. Nguyên tắc là một con trỏ cơ sở dữ liệu mới được mở khi bắt đầu mỗi cuộc gọi RPC, và commit khi cuộc gọi đã trả lời, ngay trước khi truyền câu trả lời đến client RPC, đại khái 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 hiện cuộc gọi RPC, giao dịch sẽ được quay lại một cách nguyên tử, bảo vệ trạng thái của hệ thống.

Tương tự, hệ thống cũng cung cấp một giao dịch dành riêng trong quá trình thực hiện các bộ kiểm tra, vì vậy nó có thể được quay lại hoặc không tùy thuộc vào các tùy chọn khởi động của máy chủ.

Hậu quả là nếu bạn gọi thủ công cr.commit() ở bất cứ đâ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 một phần, và do đó là các rollbacks một phần và không sạch, gây ra nhiều hậu quả khác nhau:

  1. dữ liệu kinh doanh không nhất quán, thường là mất dữ liệu

  2. mất đồng bộ quy trình công việc, tài liệu bị kẹt vĩnh viễn

  3. các bài kiểm tra không thể quay lại một cách sạch sẽ, và sẽ bắt đầu làm ô nhiễm cơ sở dữ liệu, và gây ra lỗi (điều này đúng ngay cả khi không có lỗi xảy ra trong giao dịch)

Đây là 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ạo con trỏ cơ sở dữ liệu của riêng mình một cách rõ ràng! Và các tình huống mà bạn cần làm điều đó là rất đặc biệt!

Và nhân tiện, nếu bạn đã tạo con trỏ của riêng mình, thì bạn cần xử lý các trường hợp lỗi và quay lại đúng cách, cũng như đóng con trỏ đúng cách khi bạn đã xong.

Và trái với niềm tin 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 phương thức _auto_init() của một đối tượng models.Model: điều này được chăm sóc bởi phương thức khởi tạo addons, hoặc bởi giao dịch ORM khi tạo các mô hình tùy chỉnh - trong báo cáo: commit() cũng được xử lý bởi khung, vì vậy bạn có thể cập nhật cơ sở dữ liệu ngay cả trong một báo cáo - trong các phương thức models.Transient: các phương thức này được gọi chính xác như các phương thức models.Model thông thường, trong một giao dịch và với cr.commit()/rollback() tương ứng ở cuối - v.v. (xem quy tắc chung ở trên nếu bạn nghi ngờ!)

Tất cả các cuộc gọi cr.commit() bên ngoài khung máy chủ từ bây giờ phải có một bình luận rõ ràng giải thích lý do tại sao chúng là hoàn toàn cần thiết, tại sao chúng thực sự chính xác, và tại sao chúng không phá vỡ các giao dịch. Nếu không, chúng có thể và sẽ bị loại bỏ!

Sử dụng phương pháp dịch đúng cách

SoOn sử dụng một phương pháp tương tự GetText có tên là "underscore" _( ) để chỉ ra rằng một chuỗi tĩnh được sử dụng trong mã cần được dịch tại thời gian chạy bằng ngôn ngữ của ngữ cảnh. Phương pháp giả này được truy cập trong mã của bạn bằng cách nhập khẩu như sau:

from odoo import _

Một số quy tắc rất quan trọng phải được tuân thủ khi sử dụng nó, để nó hoạt động và tránh làm đầy các bản dịch với rác không cần thiết.

Cơ bản, phương pháp này chỉ nên được sử dụng cho các chuỗi tĩnh được viết thủ công trong mã, nó sẽ không hoạt động để dịch các giá trị trường, chẳng hạn như tên Sản phẩm, v.v. Thay vào đó, việc này phải được thực hiện bằng cách sử dụng cờ dịch trên trường tương ứng.

Phương pháp này chấp nhận các tham số vị trí hoặc tên tùy chọn. Quy tắc rất đơn giản: các cuộc gọi đến phương pháp underscore luôn nên ở dạng _('literal string') và không có 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 nhớ rằng các dịch giả sẽ phải làm việc với các giá trị thực tế đượ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ữ các ký tự không cần thiết và định dạng ở mức tối thiểu. Các dịch giả phải nhận thức được rằng các mẫu định dạng như %s hoặc %d, các dòng mới, v.v. cần được giữ lại, nhưng điều quan trọng là sử dụng chúng một 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 SoOn, khi thao tác với các chuỗi, ưa thích % hơn .format() (khi chỉ có một biến cần thay thế trong một chuỗi), và ưa thích %(varname) thay vì vị trí (khi nhiều biến phải được thay thế). Điều này làm cho việc dịch dễ dàng hơn cho các dịch giả cộng đồng.

Các ký hiệu và Quy ước

  • Tên mô hình (sử dụng ký hiệu dấu chấm, tiền tố bằng tên mô-đun):
    • Khi định nghĩa một SoOn Model: sử dụng dạng số ít của tên (res.partnersale.order thay vì res.partnerSsaleS.orderS)

    • Khi định nghĩa một SoOn Transient (wizard): sử dụng <related_base_model>.<action> nơi related_base_model là mô hình cơ bản (được định nghĩa trong models/) liên quan đến transient, và action là tên ngắn của những gì transient thực hiện. Tránh từ wizard. Ví dụ: account.invoice.make, project.task.delegate.batch, ...

    • Khi định nghĩa mô hình report (SQL views e.i.): sử dụng <related_base_model>.report.<action>, dựa trên quy ước Transient.

  • Lớp Python SoOn: sử dụng camelcase (Phong cách hướng đối tượng).

class AccountInvoice(models.Model):
    ...
  • Tên biến:
    • sử dụng camelcase cho biến mô hình

    • 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ố tên biến của bạn với _id hoặc _ids nếu nó chứa một id bản ghi hoặc danh sách id. Đừng sử dụng partner_id để chứa một bản ghi của res.partner

Partner = self.env['res.partner']
partners = Partner.browse(ids)
partner_id = partners[0].id
  • các trường One2ManyMany2Many luôn nên có hậu tố _ids (ví dụ: sale_order_line_ids)

  • các trường Many2One nên có hậu tố _id (ví dụ: partner_id, user_id, ...)

  • Quy ước phương pháp
    • Trường tính toán: mẫu phương thức tính toán là _compute_<field_name>

    • Phương pháp tìm kiếm: mẫu phương pháp tìm kiếm là _search_<field_name>

    • Phương pháp mặc định: mẫu phương pháp mặc định là _default_<field_name>

    • Phương pháp lựa chọn: mẫu phương pháp lựa chọn là _selection_<field_name>

    • Phương pháp onchange: mẫu phương pháp onchange là _onchange_<field_name>

    • Phương pháp ràng buộc: mẫu phương pháp ràng buộc là _check_<constraint_name>

    • Phương pháp hành động: một phương pháp hành động đối tượng có tiền tố là action_. Vì nó chỉ sử dụng một bản ghi, hãy thêm self.ensure_one() vào đầu phương thức.

  • Trong một thuộc tính Mô hình, thứ tự nên là
    1. Thuộc tính riêng tư (_name, _description, _inherit, _sql_constraints, ...)

    2. Phương pháp mặc định và default_get

    3. Khai báo trường

    4. Các phương pháp tính toán, ngược và tìm kiếm theo thứ tự tương tự như khai báo trường

    5. Phương pháp lựa chọn (các phương pháp được sử dụng để trả về các giá trị tính toán cho các trường lựa chọn)

    6. Phương pháp ràng buộc (@api.constrains) và phương pháp onchange (@api.onchange)

    7. Phương pháp CRUD (ghi đè ORM)

    8. Phương pháp hành động

    9. Và cuối cùng, các phương pháp kinh doanh 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 các tệp tĩnh

Các addons của SoOn có một số quy ước về cách cấu trúc các tệp khác nhau. Chúng tôi giải thích ở đây chi tiết hơn cách các tài sản web dự kiến sẽ được tổ chức.

Điều đầu tiên cần biết là máy chủ SoOn sẽ phục vụ (tĩnh) tất cả các tệp nằm trong thư mục static/, nhưng được tiền tố bằng tên addon. Vì vậy, ví dụ, nếu một tệp nằm trong addons/web/static/src/js/some_file.js, thì nó sẽ có sẵn ở URL your-odoo-server.com/web/static/src/js/some_file.js

Quy ước là tổ chức mã 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ì vậy, ví dụ, tất cả các tệp từ 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 hướng dẫn cho người dùng cuối (hướng dẫn, không phải kiểm tra)

      • static/src/scss: các tệp scss

      • static/src/xml: tất cả các mẫu qweb sẽ được hiển thị trong JS

    • static/tests: đây là nơi chúng tôi đặt tất cả các tệp liên quan đến kiểm tra.

      • static/tests/tours: đây là nơi chúng tôi đặt tất cả các tệp kiểm tra tour (không phải hướng dẫn).

Hướng dẫn mã hóa Javascript

  • use strict; được khuyến nghị cho tất cả các tệp javascript

  • Sử dụng linter (jshint, ...)

  • Không bao giờ thêm các thư viện Javascript đã được rút gọn

  • Sử dụng camelcase cho khai báo lớp

Các hướng dẫn JS chi tiết hơn có trong github wiki. Bạn cũng có thể xem API hiện có trong Javascript bằng cách xem Tham chiếu Javascript.

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;
}
  • thụt lề bốn (4) khoảng trắng, không dùng tab;

  • cột tối đa rộng 80 ký tự;

  • dấu ngoặc mở ({): khoảng trống sau bộ chọn cuối cùng;

  • dấu ngoặc đóng (}): trên dòng mới của nó;

  • một dòng cho mỗi khai báo;

  • 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, bắt đầu từ position và kết thúc với các quy tắc trang trí (font, filter, v.v.).

Biến SCSS có phạm viBiến CSS phải được đặt ở trên cùng, tiếp theo 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 vô cùng hữu ích trong việc làm cho mã của bạn trở nên nghiêm ngặt, rõ ràng và thông tin hơn.

Tránh các bộ chọn id, và thêm tiền tố cho các lớp của bạn với o_<module_name>, nơi <module_name> là tên kỹ thuật của mô-đun (sale, im_chat, ...) hoặc tuyến đường chính được đặt trước bởi mô-đun (chủ yếu cho các mô-đun trang web, tức là: o_forum cho mô-đun website_forum).
Ngoại lệ duy nhất cho quy tắc này là webclient: nó chỉ sử dụng tiền tố o_.

Tránh tạo các lớp và tên biến siêu cụ thể. Khi đặt tên cho các phần tử lồng nhau, hãy chọn cách tiếp cận "Cháu".

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>

Ngoài việc gọn gàng hơn, cách tiếp cận này còn dễ bảo trì hơn vì nó giới hạn nhu cầu đổi tên khi có sự thay đổi ở DOM.

Biến SCSS

Quy ước tiêu chuẩn của chúng tôi là $o-[root]-[element]-[property]-[modifier], với:

  • $o-

    Tiền tố.

  • [root]

    Hoặc tên thành phần hoặc tên mô-đun (các thành phần đượ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 xác định bởi biến.

  • [modifier]

    Một bộ điều chỉnh 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)

Các biến này được khai báo trong các khối và không thể truy cập từ bên ngoài. Quy ước tiêu chuẩn của chúng tôi là $-[variable name].

Example

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

   margin-right: $-inner-gap;

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

SCSS Mixins và Functions

Quy ước tiêu chuẩn của chúng tôi là o-[name]. Sử dụng tên mô tả. Khi đặt tên chức năng, sử dụng động từ ở dạng mệnh lệnh (ví dụ: get, make, apply...).

Đặt tên cho các đối số tùy chọn trong dạng biến có phạm vi, vì vậy $-[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 SoOn, việc sử dụng các biến CSS là liên quan chặt chẽ đến DOM. Sử dụng chúng để ngữ cảnh hóa thiết kế và bố cục.

Quy ước tiêu chuẩn của chúng tôi là BEM, vì vậy --[root]__[element]-[property]--[modifier], với:

  • [root]

    Hoặc tên thành phần hoặc tên mô-đun (các thành phần đượ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 xác định bởi biến.

  • [modifier]

    Một bộ điều chỉnh 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;
}

Sử dụng Biến CSS

Trong SoOn, việc sử dụng các biến CSS là liên quan chặt chẽ đến DOM, có nghĩa là chúng được sử dụng để ngữ cảnh hóa thiết kế và bố cục thay vì quản lý hệ thống thiết kế toàn cầu. Chúng thường được sử dụng khi các thuộc tính của một thành phần có thể thay đổi trong các ngữ cảnh cụ thể hoặc trong các hoàn cảnh khác.

Chúng tôi định nghĩa các thuộc tính này bên trong khối chính của thành phần, cung cấp các giá trị dự phòng mặc định.

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

Biến CSS và SCSS

Mặc dù có vẻ giống nhau, các biến CSSSCSS lại hành xử rất khác nhau. Sự khác biệt chính là, trong khi các biến SCSSmệnh lệnh và được biên dịch đi, thì các biến CSStuyên bố và được bao gồm trong đầu ra cuối cùng.

Trong SoOn, chúng tôi lấy những điều tốt nhất của cả hai thế giới: sử dụng các biến SCSS để định nghĩa hệ thống thiết kế trong khi lựa chọn các biến CSS khi nói đến các điều chỉnh ngữ cảnh.

Việc triển khai ví dụ trước đó nên được cải thiện bằng cách thêm các biến SCSS để đạt đượ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 thành phần khác.

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

Pseudo-class :root

Định nghĩa các biến CSS trên pseudo-class :root là một kỹ thuật mà chúng tôi thường không sử dụng trong giao diện người dùng của SoOn. Thực tiễn này thường được sử dụng để truy cập và sửa đổi các biến CSS trên toàn cầu. Chúng tôi thực hiện điều này bằng cách sử dụng SCSS thay thế.

Các ngoại lệ đối với quy tắc này nên rõ ràng, chẳng hạn như các mẫu được chia sẻ qua các gói mà cần một mức độ nhận thức ngữ cảnh nhất định để được hiển thị đúng cách.