Application Models

Ở phần trước, chúng ta đã đi qua phần Creating Odoo Add-On Modules. Trong phần này, chúng ta sẽ đi sâu vào phía cơ sở dữ liệu của module: thêm model mới (bảng cơ sở dữ liệu), các trường mới, ràng buộc, các kiểu kế thừa...

Xác định trình tự và biểu diễn Model

Model có các thuộc tính cấu trúc để xác định hành vi của nó. Các thuộc tính này sẽ được bắt đầu bằng dấu gạch dưới. Thuộc tính quan trọng nhất của model là _name, vì nó sẽ xác định mã định danh của model. Odoo sẽ sử dụng thuộc tính _name để khởi tạo một bảng trong cơ sở dữ liệu.

Ví dụ, với đoạn code _name = 'education.student', thì Odoo sẽ tạo một bảng tương ứng là education_student trong cơ sở dữ liệu. Đây là lý do thuộc tính _name phải là duy nhất khi khai báo model.

Ngoài ra, còn có 2 thuộc tính khác có thể sử dụng trên model:

  • _rec_name: được sử dụng để làm tên đại diện hoặc tiêu đề cho bản ghi.

  • _order: được sử dụng để xác định thứ tự các bản ghi được trình bày.

Chuẩn bị

Chúng ta sẽ sử dụng module viin_education từ phần trước.

Các bước thực hiện

Chúng ta sử dụng file models/student_level.py của module viin_education để thực hành các thuộc tính này:

  1. Định nghĩa model, theo sau đoạn code sau:

    class StudentLevel(models.Model):
        _name = 'student.level'
    
  2. Thêm mô tả cho model:

    _description = 'Student Level'
    
  3. Để sắp xếp các bản ghi (theo thứ tự trường Sequence, sau đó đến name), theo sau đoạn code sau:

    _order = 'sequence, name'
    
  4. Sử dụng trường code để làm tên đại diện cho bản ghi, theo sau đoạn code sau:

    _rec_name = 'code'
    
    code = fields.Char(string='Code', required=True)
    
  5. Hoàn tất viết code cho file models/student_level.py của module viin_education:

    from odoo import fields, models
    
    class StudentLevel(models.Model):
       _name = 'student.level'
       _description = 'Student Level'
       _order = 'sequence, name'
       _rec_name = 'code'
    
       code = fields.Char(string='Code', required=True)
       name = fields.Char(string='Name', translate=True, required=True)
       sequence = fields.Integer(string='Sequence', default=1)
    
  6. Thêm giao diện form vào file views/student_level_views.xml của module viin_education:

    <odoo>
        <record id="student_level_view_form" model="ir.ui.view">
            <field name="name">student.level.form</field>
            <field name="model">student.level</field>
            <field name="arch" type="xml">
                <form string="Student Level Form">
                    <sheet>
                        <group>
                                <group>
                                    <field name="name"/>
                                    <field name="sequence"/>
                                </group>
                                <group>
                                    <field name="code"/>
                                </group>
                        </group>
                    </sheet>
                </form>
            </field>
        </record>
    </odoo>
    

Sau đó, chúng ta nâng cấp module để áp dụng những thay đổi này. Để nâng cấp module, Bạn có thể mở menu Ứng dụng, tìm kiếm module viin_education, nhấn vào biểu tượng 3 dấu chấm dọc và nhấn vào Nâng cấp. Hoặc bạn có thể thêm dòng -u viin_education trong cấu hình odoo-bin.

Cơ chế hoạt động

Với mô tả của module, điều này là không bắt buộc, nhưng nó có thể được sử dụng trong một số tiện ích bổ sung khác. Bạn có thể tham khảo thêm tại Managing Emails in Odoo.

Theo mặc định, Odoo sắp xếp thứ tự các bản ghi theo trường id được tạo tự động. Tuy nhiên, điều này có thể thay đổi bằng cách cung cấp thuộc tính _order với 1 chuỗi danh sách các trường cần sắp xếp, được ngăn cách bằng dấu phẩy. Có thể thêm chuỗi desc vào sau tên trường để sắp xếp theo thứ tự giảm dần.

Ghi chú

Chỉ những trường được lưu trữ trong cơ sở dữ liệu mới có thể được sử dụng ở thuộc tính _order. Các trường không được lưu trữ sẽ không thể sử dụng để sắp xếp các bản ghi.

Cú pháp cho thuộc tính _order này tương tự như ORDER BY clause trong SQL.

Bản ghi của model sử dụng tên dại diện khi chúng được tham chiếu từ các bản ghi khác. Khi được hiển thị ở giao diện Form, Odoo sẽ hiển thị tên đại diện của bản ghi thay cho id. Tóm lại, _rec_name được dùng để xác định tên đại diện cho các bản ghi của model.

Theo mặc định, trường name sẽ được sử dụng làm tên đại diện, nó tương đương với _rec_name = 'name'. Theo như ví dụ trên, lẽ ra trường name sẽ được sử dụng để làm tên đại diện cho các bản ghi. Tuy nhiên, chúng ta đã thay đổi tên đại diện của bản ghi thông qua việc đặt giá trị _rec_name là trường code như ảnh bên dưới.

Tên đại diện

Cảnh báo

Nếu Model của bạn không có trường name và bạn không chỉ định giá trị cho thuộc tính _rec_name của model, tên hiển thị của bản ghi sẽ là recordset với giá trị id tương ứng.

Tham khảo thêm về trường tên hiển thị

Tên đại diện (tên hiển thị) của bản ghi có sẵn trong trường tính toán display_name và được thêm tự động vào các models kể từ phiên bản 8.0. Các giá trị đó được sử dụng thông qua việc sử dụng phương thức name_get().

Theo mặc định, phương thức name_get() sử dụng thuộc tính _rec_name để tạo tên hiển thị. Nhưng bạn có thể tùy chỉnh tên hiện thị này bằng cách ghi đè logic của phương thức name_get(). Phương thức này trả về danh sách các bộ giá trị gồm 2 phần tử là id, display_name: [(1, 'name 1'), (2, 'name 2'), ...]

Đây là 1 ví dụ tùy chỉnh tên hiển thị của Odoo:

@api.multi
def name_get(self):
  return [(order.id, '%s %s' % (_('Lunch Order'), '#%d' % order.id)) for order in self]

Thêm trường dữ liệu vào Model

Model dùng để lưu trữ dữ liệu và dữ liệu này được cấu trúc bởi các trường trong model. Tại phần này, chúng ta sẽ giới thiệu về một số kiểu dữ liệu có thể được lưu trữ trong các trường và cách thêm nó vào một model.

Chuẩn bị

Chúng ta sẽ sử dụng module viin_education từ phần trước.

Các bước thực hiện

Trong module viin_education, chúng ta xác định các trường cho model Học Sinh tại file models/education_student.py:

  1. Thêm các trường vào model:

    from odoo import models, fields
    
    class EducationStudent(models.Model):
       _name = 'education.student'
       _description = 'Education Student'
    
       name = fields.Char(string='Name')
       student_code = fields.Char(string='Student Code')
       gender = fields.Selection([('male', 'Male'), ('female', 'Female'), ('other', 'Other')], string='Gender')
       date_of_birth = fields.Date(string='Date of Birth')
       age = fields.Integer(string='Age')
       active = fields.Boolean(string='Active', default=True)
       notes = fields.Text(string='Internal Notes')
       description = fields.Html(string='Description')
       attached_file = fields.Binary('Attached File')
       total_score = fields.Float(string='Total Score')
       write_date = fields.Datetime(string='Last Updated on')
    
  2. Sau khi thêm các trường vào model, chúng ta cần thêm các trường này vào giao diện Form:

    <odoo>
       <record id="education_student_view_form" model="ir.ui.view">
          <field name="name">education.student.form</field>
          <field name="model">education.student</field>
          <field name="arch" type="xml">
             <form string="Student Form">
                <sheet>
                   <group>
                      <group>
                          <field name="name"/>
                          <field name="student_code"/>
                          <field name="gender"/>
                          <field name="date_of_birth"/>
                         <field name="age"/>
                      </group>
                      <group>
                         <field name="total_score"/>
                         <field name="attached_file"/>
                         <field name="write_date"/>
                      </group>
                   </group>
                   <group>
                      <field name="notes"/>
                   </group>
                   <group>
                      <field name="description"/>
                   </group>
                </sheet>
             </form>
          </field>
       </record>
    </odoo>
    

Thực hiện nâng cấp model để áp dụng thay đổi trên.

Thực hiện thay đổi một số thuộc tính của trường, để cung cấp thêm nhiều ý tưởng tốt hơn về khai báo trường:

name = fields.Char(string='Name', required=True, translate=True)
student_code = fields.Char(string='Student Code', required=True, index=True,
                           help="Student ID is unique")
gender = fields.Selection([('male', 'Male'), ('female', 'Female'), ('other', 'Other')],
                           string='Gender', default='male')
attached_file = fields.Binary('Attached File', groups='base.group_user')
description = fields.Html(string='Description', sanitize=True, strip_style=False)

Cơ chế hoạt động

Các trường được thêm vào các model bằng cách xác định các thuộc tính của class. Các trường không quan hệ có sẵn như sau:

  • Char: Được sử dụng cho giá trị chuỗi.

  • Text: Được sử dụng cho giá trị chuỗi nhiều dòng.

  • Selection: Được sử dụng cho 1 danh sách lựa chọn. Nó có 1 danh sách các cặp keyvalue, key là giá trị được lưu trong cơ sở dữ liệu (có thể là chuỗi hoặc số nguyên), value là giá trị sẽ được dịch tự động.

    Quan trọng

    Với trường Selection, bạn có thể sử dụng các key là số nguyên, nhưng Odoo sẽ ngầm hiểu số 0 là không được đặt bên trong danh sách lựa chọn và sẽ không hiển thị mô tả nếu key là 0.

  • Html: Tương tự như trường Text, nhưng sẽ lưu trữ văn bản ở dạng html.

  • Binary: Trường lưu trữ các file nhị phân, ví dụ như ảnh, tệp,..

  • Boolean: Trường lưu trữ giá trị True/False.

  • Date: Trường lưu trữ giá trị ngày trong cơ sở dữ liệu. ORM Odoo đã xử lý chúng dưới dạng đối tương (date object). Vì vậy, bạn có thể sử dụng fields.Date.today() để lấy ra ngày hiện tại, cũng như để làm giá trị mặc định cho trường Date.

  • Datetime: Trường lưu trữ giá trị ngày giờ trong cơ sở dữ liệu. ORM Odoo đã xử lý chúng dưới dạng đối tương (datetime object). Vì vậy, bạn có thể sử dụng fields.Date.now() để lấy ra ngày giờ hiện tại, cũng như để làm giá trị mặc định cho trường Datetime.

  • Integer: Trường lưu trữ giá trị số nguyên.

  • Float: Trường lưu trữ giá trị numeric trong cơ sở dữ liệu, độ chính xác số thập phân của chúng có thể được xác định tùy ý.

  • Monetary: Có thể lưu trữ số tiền bằng một loại tiền tệ nhất định. Điều này sẽ được giải thích chi tiết trong phần Thêm trường Monetary vào Model.

Ở phần trên, chúng ta đã thêm một số thuộc tính vào các trường để xác định hiện thị các trường tốt hơn. Dưới đây là giải thích cho các thuộc tính trường được sử dụng:

  • string: Là tiêu đề của trường, và được sử dụng trong nhãn của UI views.

  • translate: Khi đặt là True, trường này có thể dịch được. Nó có thể giữ một giá trị khác nhau, tùy thuộc vào ngôn ngữ giao diện người dùng.

  • default: Giá trị mặc định của trường, nó cũng có thể là một hàm được sử dụng để tính toán các giá trị mặc định. Ví dụ: default=_get_start_of_month , _get_start_of_month là một phương thức đã được xác định trên model trước khi định nghĩa ra trường.

  • help: Là văn bản giải thích được hiển thị trong công cụ giao diện người dùng (UI tooltips).

  • groups: Trường chỉ khả dụng cho một số nhóm được phân quyền. Nó là một giá trị chuỗi chưa danh sách các xml id (res.groups) được phân cách với nhau bởi dấu phẩy. Điều này được đề cập chi tiết hơn trong phần Security Access.

  • states: Trạng thái cho phép trên giao diện người dùng (UI) có thể đặt giá trị thuộc tính như readonly, required, invisible phụ thuộc vào trường trạng thái (state field). Do đó nó yêu cầu một trường trạng thái và được sử dụng trong giao diện Form.

  • copy: Đánh dấu giá trị của trường có được sao chép khi bản ghi được nhân bản hay không. Theo mặc đinh, nó là True cho trường không quan hệ và trường Many2one, và là False cho các trường One2many và các trường computed.

  • index: Khi được đặt thành True, Odoo sẽ tạo một chỉ mục cơ sở dữ liệu cho trường, nó có thể sẽ cho phép tìm kiềm nhanh hơn.

  • readonly: Đánh dấu trường chỉ đọc mặc định trong giao diện người dùng.

  • required: Đánh dấu trường bắt buộc mặc định trong giao diện người dùng.

  • company_dependent: Đánh dấu lưu trữ trường với các giá trị khác nhau cho mỗi công ty. Nó thay thế cho trường thuộc tính Property không dùng nữa.

  • group_operator: Là một hàm tổng hợp được sử dụng để hiển thị kết quả trong nhóm theo chế độ hiển thị. Các giá trị có thể là count, count_distinct, array_agg , bool_and , bool_or , max , min , avg , and sum. Các trường Integer, Float, Monetary có giá trị mặc định là sum.

  • sanitize: đánh dấu được sử dụng bởi trường Html và tách nội dung của nó khỏi các thẻ có khẳ năng không an toàn. Một số trường thuộc tính khác có thể hoạt động nếu đặt sanitize=True:

    • sanitize_tags=True: để xóa các thẻ không thuộc danh sách trắng (đây là mặc định).

    • sanitize_attributes=True: để xóa các thuộc tính thẻ không thuộc danh sách trắng.

    • sanitize_style=True: để xóa các thuộc tính phong cách không nằm trong danh sách trắng.

    • strip_style=True: để xóa tất cả các kiểu phong cách của thẻ.

    • strip_class=True: để xóa các thuộc tính của lớp.

Còn nữa

Trường Selection cũng chấp nhận một hàm được tham chiếu làm thuộc tính selection thay vì một danh sách giá trị cố định. Điều này cho phép 1 danh sách các lựa chọn được tạo động.

Các đối tượng trường Date, Datetime cũng cung cấp một số phương thức tiện ích thuận tiện trong quá trình sử dụng:

Đối với đối tượng Date:

  • fields.Date.to_date(string_value): phân tích chuỗi thành đối tượng date.

  • fields.Date.to_string(date_value): phân tích đối tượng date thành chuỗi.

  • fields.Date.today(): trả về ngày hiện tại.

  • fields.Date.context_today(record, timestamp): trả về ngày của timestamp (hoặc ngày hiện tại nếu timestamp bị bỏ qua) ở định dạng chuỗi, theo múi giờ trong ngữ cảnh của bản ghi.

Đối với đối tượng Datetime:

  • fields.Datetime.to_date(string_value): phân tích chuỗi thành đối tượng datetime.

  • fields.Datetime.to_string(date_value): phân tích đối tượng datetime thành chuỗi.

  • fields.Datetime.today(): trả về ngày giờ hiện tại.

  • fields.Datetime.context_today(record, timestamp): chuyển đổi đối tượng timestamp datetime thành đói tượng datetime sử dụng múi giờ trong ngữ cảnh bản ghi.

Một số trường tự động được thêm trong model, vì vậy chúng ta không nên sử dụng những trường này khi định nghĩa các trường:

  • id: Id của bản ghi.

  • create_date Datetime - thời gian tạo bản ghi.

  • create_uid Integer - người dùng đã tạo bản ghi.

  • write_date Datetime - thời gian chỉnh sửa bản ghi cuối cùng.

  • write_uid: Integer - người dùng đã chỉnh sửa bản ghi lần cuối.

Có thể tắt tính năng tự dộng tạo nhật ký các trường này bằng cách đặt thuộc tính của model: _log_access=False.

Một trường đặc biệt khác có thể thêm vào model, kiểu Boolean, cho phép người dùng đánh dấu các bản ghi là không hoạt động. Nó được sử dụng để kích hoạt tính năng lưu trữ/ hủy lưu trữ trên các bản ghi. Định nghĩa nó như sau:

active = fields.Boolean(string='Active', default=True)

Theo mặc định, nếu được định nghĩa, giá trị của nó là False và chỉ các bản ghi có trường active đặt đặt là True mới được hiển thị. Để truy xuất, chúng ta có thể thêm vào domain với mệnh đề ('active', '=', False).

Ngoài ra, nếu giá trị 'active_test': False được thêm vào ngữ cảnh của môi trường, ORM sẽ lọc ra các bản ghi không hoạt động (đã lưu trữ). Đây là 1 ví dụ để lấy ra các bản ghi hoạt động và bản ghi đã lưu trữ:

self.env['education.student'].with_context(active_test=False).search([])
hoặc
self.env['education.student'].search(['|', ('active', '=', False), ('active', '=', True)])

Cảnh báo

[('active', 'in' (True, False))] không hoạt động như mong đợi. Odoo đang tìm kiếm một cách rõ ràng bằng cách thêm mệnh đề ('active', '=', False) vào trong domain.

Sử dụng trường Float với độ chính xác có thể cấu hình

Khi sử dụng trường Float, người dùng có thể cấu hình độ chính xác chữ số thập phân. Trong phần này, ta sẽ thêm trường total_score kiểu Float vào model Học sinh (education.student) với độ chính xác thập phân do người dùng cấu hình.

Chuẩn bị

Chúng ta sẽ sử dụng module viin_education từ phần trước.

Các bước thực hiện

Thực hiện các bước sau để áp dụng độ chính xác thập phân cho trường total_score:

  1. Kích hoạt chế độ nhà phát triển (Installing the Odoo Development Environment).

  2. Truy cập vào cấu hình Độ chính xác thập phân bằng cách: Vào Thiết lập ‣ Kỹ thuật ‣ Cấu trúc cơ sở dữ liệu ‣ Độ chính xác thập phân. Ở đây sẽ xuất hiện danh sách các cấu hình độ chính xác thập phân.

  3. Thêm cấu hình mới với thông tin như sau: Cách dùngScoreChữ số1.

    độ chính xác thập phân
  4. Thêm trường total_score sử dụng độ chính xác thập phân vừa tạo bên trên Score trong model education.student và sử dụng thuộc tính digits của trường kiểu Float.

    # Thêm trường total_score vào model `education.student`
    class EducationStudent(models.Model):
       _name = 'education.student'
    
       total_score = fields.Float(string='Total Score', digits='Score')
    
  5. Thêm trường total_score trên gio diện form của model education.student. Sau đó nâng cấp module viin_education.

Cơ chế hoạt động

Khi bạn thêm giá trị chuỗi vào thuộc tính digits của trường, Odoo sẽ tra cứu chuỗi đó với danh sách độ chính xác thập phân và trả về một bộ giá trị chính xác 16 chữ số và số lượng chữ số thập phân đã được xác định trong cấu hình. Điều này cho phép người dùng thay vì phải viết code đặt giá trị cố định, họ có thể cấu hình theo nhu cầu của họ.

Mẹo

Nếu bạn sử dụng phiên bản nhỏ hơn 13, độ chính xác thập phân có sẵn trong module riêng biệt decimal_precision.

Để bật độ chính xác thập phân trong trường của bạn, bạn cần sử dụng phương thức get_precision() của module decimal_precision.

total_score = fields.Float(string='Total Score', digits=dp.get_precision('Score'))

Thêm trường Monetary vào Model

Odoo có hỗ trợ về giá trị tiền tệ liên quan đến một kiểu tiền tệ. Sử dụng trường Monetary để linh động hơn trong việc hiển thị đơn vị tiền tệ.

Chuẩn bị

Chúng ta sẽ sử dụng module viin_education từ phần trước.

Các bước thực hiện

Trường Monetary cần một trường tiền tệ bổ sung để lưu trữ tiền tệ cho số tiền. Vì vậy chúng ta sẽ tạo 2 trường để đảm bảo giá trị tiền tệ có thể hoạt động. Thực hiện theo các bước sau để áp dụng giá trị tiền tệ.

  1. Thêm một trường tiền tệ, kiểu Many2one đến model res.currency, và trường này được lưu trữ vào model education.student.

    class EducationStudent(models.Model):
       _name = 'education.student'
    
       # ...
       currency_id = fields.Many2one('res.currency', string='Currency')
    
  2. Thêm trường amount_paid với kiểu Monetary vào model education.student.

    class EducationStudent(models.Model):
    
    _name = 'education.student'
    
       # ...
       currency_id = fields.Many2one('res.currency', string='Currency')
       amount_paid = fields.Monetary('Amount Paid')
       # or amount_paid = fields.Monetary('Amount Paid', currency_field='currency_id')
    
  3. Thêm trường amount_paid và trường currency_id trên giao diện form của model education.student và sau đó nâng cấp module viin_education để xem kết quả.

    <field name="currency_id"/>
    <field name="amount_paid"/>
    
    Trường Monetary

Cơ chế hoạt động

Trường Monetary cũng tương tự như trường Float. Odoo có thể thể hiện chính xác tiền tệ trong giao diện người dùng thông qua thuộc tính currency_field của trường Monetary. Thuộc tính currency_field sẽ tham chiếu đến bất kỳ 1 trường trường tiền tệ nào mà chúng ta thích.

Mẹo

Nếu bạn sử dụng trường tiện tệ với tên trường là currency_id. Bạn có thể bỏ qua thuộc tính currency_field khỏi trường Monetary.

Thêm các trường quan hệ vào Model

Mối quan hệ giữa các model trong Odoo được biểu diễn thông qua các trường quan hệ:

  • Many2one: mối quan hệ nhiều-một, hay được viết tắt là m2o.

  • One2many: mối quan hệ một-nhiều, hay được viết tắt là o2m.

  • Many2many: mối quan hệ nhiều-nhiều, hay được viết tắt là m2m.

Chúng ta có thể tạo một số ví dụ cho các kiểu quan hệ trên thông qua một số model như sau: Lớp học - Trường học:

  • Mỗi Lớp học sẽ có một trường học, là mối quan hệ nhiều-một.

  • Một trường học sẽ có nhiều lớp học, là mối quan hệ một-nhiều, điều này là ngụ ý ngược lại của quan hệ nhiều-một.

  • Một lớp học có thể có nhiều giáo viên và giáo viên có thể có nhiều lớp học, là mối quan hệ nhiều-nhiều.

Chuẩn bị

Chúng ta sẽ sử dụng module viin_education từ phần trước.

Các bước thực hiện

  1. Đầu tiên, chúng ta cần xác định model Trường học tại models/education_school.py để có thể thêm trường school_id trong model Lớp học:

    from odoo import fields, models
    
    class EductionSchool(models.Model):
    
       _name = 'education.school'
       _description = 'School'
    
       name = fields.Char(string='Name', translate=True, required=True)
       code = fields.Char(string='Code', copy=False)
       # ...
    
  2. Thêm trường school_id kiểu quan hệ Many2one vào model Lớp học tại file models/education_class.py:

    from odoo import fields, models
    
    class EducationClass(models.Model):
       _name = 'education.class'
       _description = 'Education Class'
    
       name = fields.Char(string='Name', required=True)
       # ... (some fields)
    
       #  relational fields
       school_id = fields.Many2one('education.school', string='School', required=True)
    
  3. Thêm trường class_ids kiểu One2many vào model Trường học tại file models/education_school.py, nhưng trước đó hãy đảm bảo bạn đã thêm trường school_id vào file models/education_class.py:

    class EductionSchool(models.Model):
       # ...
    
       class_ids = fields.One2many('education.class', 'school_id', string='Classes')
    
  4. Bây giờ, chúng ta sẽ tạo quan hệ Many2many cho trường teacher_ids trong model Lớp học. Các giáo viên sẽ được lấy từ model có sẵn res.partner.

    class EducationClass(models.Model):
       # ...
       #  relational fields
       school_id = fields.Many2one('education.school', string='School', required=True)
       teacher_ids = fields.Many2many('res.partner', string='Teachers')
    

Bây giờ, chúng ta sẽ cần khai báo đầy đủ các model files trong file models/__init__.py và sau đó sẽ nâng cấp module viin_education. Bạn có thể kiểm tra việc thay đổi trên của các model trong Thiết lập ‣ Kỹ thuật ‣ Cấu trúc dữ liệu ‣ Đối tượng với chế độ nhà phát triển.

Cơ chế hoạt động

Các trường Many2one thêm một cột vào bảng của model trong cơ sở dữ liệu. Nó lưu trữ id cơ sở dữ liệu của bản ghi quan hệ có liên quan đấy. Ở tầng cơ sở dữ liệu, một ràng buộc khóa ngoại cũng sẽ được tạo để đảm bảo rằng các id của cột vừa thêm là một tham chiếu hợp lệ đến một bản ghi trong bảng quan hệ.

Một số thuộc tính có thể sử dụng cho các trường Many2one:

  • index: mặc định sẽ không có chỉ mục nào được đặt, nhưng bạn có thể thêm nó bằng cách thêm thuộc tính index=True khi định nghĩa trường.

  • ondelete: xác định điều gì sẽ xảy ra khi bản ghi liên quan bị xóa. Theo sau một số giá trị của thuộc tính dưới đây:

    • 'set null': là giá trị mặc định, đặt giá trị rỗng cho trường.

    • 'restrict': ngăn không cho bản ghi được liên kết bị xóa.

    • 'cascade': bản ghi liên kết cũng bị xóa theo.

  • contextdomain: cũng hợp lệ cho các trường quan hệ khác, nó chủ yếu được sử dụng ở phía máy khách và ở cấp model, hoạt động như các giá trị mặc định sẽ được sử dụng ở gioa diện xem phía máy khách. Bạn có xem chi tiết hơn về 2 thuộc tính này trong phần Backend Views:

    • context: thêm các biến vào ngữ cảnh máy khách khi nhấp qua trường để xem các bản ghi liên kết. ví dụ, chúng ta có thể đặt các giá trị mặc định khi tạo các bản khi mới thông qua các giao diện xem đó.

    • domain: là một bộ lọc tìm kiếm được sử dụng để giới hạn các bản ghi liên kết có sẵn.

Các trường One2many là đảo ngược của quan hệ Many2one, nhưng chúng sẽ không được lưu trữ trong cơ sở dữ liệu. Thay vào đó, nó giống như là các phím tắt có lập trình và cho phép sử dụng thông qua ORM hay các giao diện. Điều đó có nghĩa là trường One2many cần phải có trường Many2one trong model tham chiếu.

Các trường quan hệ Many2many cũng không thêm cột vào bảng của model trong cơ sở dữ liệu. Thay vào đó, kiểu quan hệ này sẽ tạo ra một bảng trung gian với 2 cột để lưu trữ id của 2 bảng quan hệ với nhau. Với ví dụ trên, một bảng trung gian education_class_res_partner_rel sẽ được tạo ra với các cột là education_class_idres_partner_id. Các giá trị tên bảng và tên cột này là trị mặc định của odoo. Tên bảng sẽ là tên của 2 model quan hệ với nhau, được sắp xếp theo bảng chữ cái, và có hậu tố là _rel. Các cột sẽ là tên bảng và thêm hậu tố _id. Tuy nhiên, chúng ta có thể ghi đè lại các giá trị này bằng cách sử dụng các thuộc tính quan hệ. Điều này sẽ được giải thích chi tiết ở bên dưới.

Còn nữa

Trường Many2one hỗ trợ thuộc tính auto_join. Đây là một cờ cho phép ORM sử dụng các phép SQL joins trên trường này. Do đó, nó sẽ bỏ qua kiểm soát ORM thông thường, chẳng hạn như kiểm soát truy cập của người dùng và các quy tắc truy cập bản ghi. Ngoài ra nó có thể sử dụng để giải quyết các vấn đề về hiệu suất, nhưng bạn vẫn nên tránh sử dụng thuộc tính này.

Chúng ta đã trình bày cách ngắn nhất để xác định các trường quan hệ. Hãy xem xét các thuộc tính cụ thể cho các trường này.
Các thuộc tính trường One2many như sau:
  • comodel_name: mã định danh của model liên kết, là bắt buộc, có thể định nghĩa theo vị trí mà không cần từ khóa.

  • inverse_name: điều này chỉ áp dụng cho One2many và là tên trường trong mô hình đích cho quan hệ Many2one ngược.

  • limit: điều này áp dụng cho cả One2manyMany2many, đồng thời đặt giới hạn tùy chọn về số lượng bản ghi cần đọc được sử dụng ở giao diện người dùng.

Các thuộc tính trường Many2many như sau:

  • comodel_name: tương tự như trường One2many.

  • relation: đây là tên để sử dụng cho bảng trung gian, không bắt buộc.

  • column1: tên của trường Many2one trong bảng quan hệ liên kết đến model này, không bắt buộc.

  • column2: tên của trường Many2one trong bảng quan hệ liên kết đến comodel, không nắt buộc.

Các thuộc tính trên trên có thể định nghĩa theo vị trí mà không cần từ khóa.

Đối với quan hệ Many2many, trong hầu hết các trường hợp, ORM sẽ quan tâm đến các giá trị mặc định cho các thuộc tính này. Nó thậm chí còn có khả năng phát hiện các quan hệ Many2many ngược, phát hiện bảng quan hệ đã tồn tại và đảo các giá trị column1column2 một cách thích hợp.

Tuy nhiên có một số trường hợp chúng ta cần cung cấp thuộc tính với các giá trị riêng cho trường:

  • Nếu có nhiều hơn một quan hệ Many2many giữa 2 model giống nhau, chúng ta cần cung cấp thuộc tính relation với giá trị riêng, khác với tên của quan hệ trước.

  • Tên cơ sở dữ liệu của 2 bảng liên kết không quá 63 ký tự theo PostgreSQL.

Bảng quan hệ trung gian này cũng tạo chỉ mục cho khóa chính của nó với cú pháp bên dưới, và khóa này cũng cần đáp ứng không vượt quá 63 ký tự, nếu vượt quá, bạn cần thiết lập thuộc tính relation thủ công:
<model1>_<model2>_rel_<model1>_id_<model2>_id_key

Thêm hệ thống phân cấp vào Model

Cấu trúc phân cấp được biểu diễn giống như một model có quan hệ đến chính model đấy. Mỗi bản ghi sẽ có 1 bản ghi cha và nhiều bản ghi con trong cùng 1 model. Điều này có thể đạt được bằng cách sử dụng quan hệ Many2One giữa model và chính nó.

Tuy nhiên, Odoo cũng cung cấp hỗ trợ cải thiện cho trường này bằng cách sử dụng model tập hợp lồng nhau (Xem thêm tại đây: https://en.wikipedia.org/wiki/Nested_set_model). Khi được kích hoạt, nếu các truy vấn sử dụng các toán tử child_of, parent_of trong domain sẽ chạy nhan hơn đáng kể.

Chuẩn bị

Chúng ta sẽ sử dụng module viin_education từ phần trước.

Các bước thực hiện

Chúng ta sẽ sử dụng model Nhóm lớp tại models/education_class_group.py và sẽ thêm nó vào models/__init__.py.

from . import education_class_group
  1. Khai báo model Nhóm lớp:

    from odoo import fields, models, _
    
    class EductionClassGroup(models.Model):
       _name = 'education.class.group'
       _description = 'Education Class Group'
    
       name = fields.Char(string='Name', translate=True, required=True)
       parent_id = fields.Many2one('education.class.group', string='Parent Group', ondelete='restrict')
       child_ids = fields.One2many('education.class.group', string='Child Groups', ondelete='restrict')
    
  2. Để bật hỗ trợ phân cấp đặc biệt, hãy thêm đoạn code sau:

    _parent_store = True
    _parent_name = "parent_id" # tùy chọn nếu trường là cấp cha
    
    parent_path = fields.Char(index=True)
    
  3. Để ngăn lặp vô tận, hãy thêm đoạn code ràng buộc sau:

    from odoo.exceptions import ValidationError
    # ...
    @api.constraints('parent_id')
    def _check_hierarchy(self):
       if not self._check_recursion():
          raise ValidationError('Error! You cannot create recursivecategories.')
    

Cuối cùng, Nâng cấp module để áp dụng các thay đổi trên.

Để hiển thị các trường của model trên giao diện người dùng, bạn cần phải thêm menu. views, security rules. Để biết rõ hơn, bạn có thể tham khảo tại Creating Odoo Add-On Modules.

Cơ chế hoạt động

Tại bước 1, đã tạo model với quan hệ phân cấp. Trường parent_id kiểu Many2one để tham chiếu đến bản ghi cấp cha. Để truy xuất bản ghi con nhanh hơn, trường này được đánh dấu chỉ mục index. Đồng thời trường này cũng cần đặt thuộc tính ondeletecascade hoặc là restrict. Trường child_ids kiểu One2many chỉ để cung cấp lối tắt để truy cập các bản ghi là bản ghi con của nó.

Tại bước 2, Chúng ta bổ sung các hỗ trợ đặc biệt dành cho cấu trúc phân cấp. Nó hữu ích cho các lệnh đọc nhiều và hạn chế ghi vì nó sẽ giúp duyệt dữ liệu nhanh hơn. Điều này được thực hiện bằng cách thêm trường tên là parent_path và thuộc tính model _parent_store = True. Khi thuộc tính này được bật, trình trợ giúp sẽ được sử dụng để lưu trữ trong các tìm kiếm trong cây phân cấp.

Ghi chú

Theo mặc định, trường parent_id là trường danh riêng cho cho cấu trúc phân cấp, tuy nhiên, bạn cũng có thay thế trường khác bằng cách khai báo giá trị trường cho thuộc tính _parent_name.

Tại bước 3, chúng ta đã thêm một ràng buộc và được khuyến nghị sử dụng để đảm bảo lỗi vòng lặp phân cấp vô tận. Phương thức _check_recursion() là một tiện ích giúp chúng ta xử lý việc đó.

Ngoài ra, như đã giới thiệu ở trên, khi kích hoạt cấu trúc phân cấp, các toán tử child_of, parent_of sẽ có sẵn trong các mệnh đề của domain.

Thêm Constraint vào Model

Các model có thể ngăn chặn một số trường hợp dữ liệu không mong muốn. Odoo hỗ trợ 2 kiểu ràng buộc khác nhau:

  • Kiểm tra ràng buộc ở tầng cơ sở dữ liệu.

  • Kiểm tra ràng buộc ở tầng máy chủ.

Các ràng buộc ở tầng cơ sở dữ liệu được giới hạn trong các ràng buộc được hỗ trợ bởi PostgreSQL. Các ràng buộc thường hay được sử dụng là UNIQUE, CHECK, EXCLUDE... Nếu những điều này không đủ cho nhu cầu của chúng ta, chúng ta có thể sử dụng các ràng buộc ở cấp máy chủ bằng Python code. Điều này có thể xử lý các ràng buộc phức tạp hơn.

Chuẩn bị

Để thực hiện điều này, chúng ta sẽ sử dụng model Học sinh của module viin_education. Và chúng ta mong đợi file model này đã có sẵn các trường sau models/education_student.py:

from odoo import models, fields

class EducationStudent(models.Model):
   _name = 'education.student'
   # ...
   student_code = fields.Char(string='Student Code', copy=False)
   total_score = fields.Float(string='Total Score', digits='Score')
   date_of_birth = fields.Date(string='Date of Birth')

Các bước thực hiện

Trong models/education_student.py, chúng ta sẽ thêm các ràng buộc:

  1. Ràng buộc ở tầng cơ sở dữ liệu, thêm một thuộc tính vào model như sau:

    _sql_constraints = [
       ('student_code_unique', 'unique(student_code)', "The student code must be unique!"),
       ('check_total_score', 'CHECK(total_score >= 0)', "The Total Score must be greater than 0!")
    ]
    
  2. Ràng buộc ở tầng máy chủ, thêm một phương thức vào model như sau:

    from odoo import api, models, fields
    from odoo.exceptions import ValidationError
    
    class EducationStudent(models.Model):
    # ...
    
       @api.constrains('date_of_birth')
       def _check_date_of_birth(self):
          for r in self:
             if r.date_of_birth > fields.Date.today():
                raise ValidationError('Date of Birth must be in the past')
    

Cuối cùng, nâng cấp module để áp dụng các thay đổi trên.

Cơ chế hoạt động

Tại bước 1, tạo một ràng buộc cơ sở dữ liệu trên bảng của model. Nó được thực thi ở tầng cơ sở dữ liệu. Thuộc tính model _sql_constraints chấp nhận một danh sách các ràng buộc, được xác định bởi một tuple gồm 3 phần tử. Chúng được liệt kê như sau:

  • Chuỗi định danh ràng buộc.

  • Biểu thức ràng buộc bảng theo PostgreSQL.

  • Một thông báo cho người dùng khi ràng buộc bị vi phạm.

Trong ví dụ trên của chúng ta ở bước 1, chúng ta đã sử dụng student_code_unique và tên ràng buộc trong của bảng trong cơ sở dữ liệu là education_student_student_code_unique. Và 1 ràng buộc check_total_score để kiểm tra trường total_score lớn hơn hoặc bằng 0.

Để biết thêm thông tin về các ràng buộc PostgreSQL, bạn có thể xem them tại đây https://www.postgresql.org/docs/current/ddl-constraints.html.

Tại bước thứ 2, chúng ta đã thêm một phương phức vào model để thực hiện xác thực dữ liệu. Nó được trang trí bằng @api.constrains. Có nghĩa là nó sẽ thực thi kiểm tra khi các trường trong danh sách đối số thay đổi. Nếu việc kiểm tra không thành công, một ngoại lệ ValidationError sẽ được thông báo.

Thêm trường Computed vào Model

Đôi khi chúng chúng ta cần một số trường có giá trị được tính toán thông qua một số trường trong bản ghi hoặc các bản ghi liên quan. Trong Odoo, điều này có thể thực hiện bằng cách sử dụng các trường computed.

Chuẩn bị

Chúng ta sẽ sử dụng module viin_education từ phần trước.

Các bước thực hiện

Chúng ta sẽ chỉnh sửa lại model models/education_student.py để thêm trường và các phương thức liên quan.

  1. Trong các ví dụ trước, chúng ta sẽ chỉnh sửa lại trường sau:

    class EducationStudent(models.Model):
    # ...
       age = fields.Integer(string='Age', compute='_compute_age', inverse='_inverse_age', search='_search_age',
                         store=False, # tùy chọn
                         compute_sudo=True) # Tùy chọn
    
  2. Tiếp theo, chúng ta sẽ thêm phương thức cho thuộc tính compute với logic tính toán trường:

    from odoo import models, fields, api
    #...
       class EducationStudent(models.Model):
       # ...
    
          @api.depends('date_of_birth')
          def _compute_age(self):
             curent_year = fields.Date.today().year
             for r in self:
                if r.date_of_birth:
                   r.age = curent_year - r.date_of_birth.year
                else:
                   r.age = 0
    
  3. Tiếp theo, chúng ta sẽ thêm phương thức cho thuộc tính inverse với logic tính toán trường:

    from datetime import date
    class EducationStudent(models.Model):
    # ...
       def _inverse_age(self):
          for r in self:
             if r.age and r.date_of_birth:
                curent_year = fields.Date.today().year
                dob_year = curent_year - r.age
                dob_month = r.date_of_birth.month
                dob_day = r.date_of_birth.day
                date_of_birth = date(dob_year, dob_month, dob_day)
                r.date_of_birth = date_of_birth
    
  4. Tiếp theo, chúng ta sẽ thêm phương thức cho thuộc tính search với logic tính toán trường:

    class EducationStudent(models.Model):
    # ...
       def _search_age(self, operator, value):
          new_year = fields.Date.today().year - value
          new_value = date(1, 1, new_year)
          # age > value => date_of_birth < new_value
          operator_map = {'>': '<', '>=': '<=', '<': '>', '<=': '>='}
          new_operator = operator_map.get(operator, operator)
          return [('date_of_birth', new_operator, new_value)]
    

Cuối cùng, hãy nâng caaso module viin_education để áp dụng thay đổi này.

Cơ chế hoạt động

Định nghĩa của trường computed giống với định nghĩa trường thông thường, ngoại trừ thuộc tính compute được thêm vào để chỉ định cho phương thức sẽ tính toán nó. Các trường được tính toán được tính động trong thời gian chạy nên chúng không được lưu trữ trong cơ sở dữ liệu. Bạn sẽ không thể tìm kiếm và ghi các trường tính toán theo mặc đinh. Vì vậy, bạn cần làm một số việc để kích hoạt việc hỗ trợ việc ghi và tìm kiếm cho các trường tính toán này.

Hàm tính toán computed tính toán động trong thời gian chạy, nhưng ORM đã sử dụng bộ nhớ đệm để tránh tính toán lại mỗi khi giá trị của nó được truy cập. Vì vậy, nó cần biết các trường khác mà nó phụ thuộc vào để tính toán. Nó là trình trang trí @depends để phát hiện khi nào các giá trị được lưu trong bộ nhớ đệm được tính toán lại.

Hãy đảm bảo rằng trong hàm tính toán luôn luôn đặt giá trị cho trường được tính toán. Nếu không, một thông báo lỗi sẽ suất hiện. Điều này có thể xảy ra trong một số trường hợp như trong hàm có điều kiện if và bản ghi đó không thể đi vào và đặt giá trị. Vì vậy trong trường hợp này bạn nên đặt thêm else để đặt giá trị cho trường được tính toán.

Hỗ trợ ghi có thể được thêm vào thông qua các hàm nghịch đảo inverse. Điều này được sử dụng giá trị gán cho trường được tính toán để tính cập nhật là trường gốc. Trong ví dụ trên, chúng ta có thể đặt trường date_of_birth bằng cách chỉnh sửa giá trị vào trường tính toán age. Khi sửa trường age, nó sẽ chạy vào phương thức _inverse_age đã được đặt ở thuộc tính inverse của trường và tính toán, cập nhật giá trị cho trường date_of_birth. Thuộc tính nghịch đảo inverse là tùy chọn, bạn có thể bỏ qua nó nếu không có nhu cầu.

Bạn cũng có thể tìm kiếm các trường được tính toán (không được lưu trữ) có thể tìm kiếm được bằng cách đặt thuộc tính search với một phương thức. Trên thực tế, nó nhận toán tử và giá trị được sử dụng để tìm kiếm dưới dạng tham số và trả về kết quả là một domain với các mệnh đề điều kiện thay thế để sử dụng.

Trong ví dụ trên, chúng ta đã viết code để có thể tìm kiếm các bản ghi theo trường age. Phương thức này nhận toán tử và giá trị theo trường age, sau đó chúng ta xử lý nó để tìm kiếm theo giá trị domain với mệnh đề liên quan đến trường date_of_birth.

# ...
def _search_age(self, operator, value):
   # ...
   return [('date_of_birth', new_operator, new_value)]

Cũng giống thuộc tính nghịch đảo inverse, thuộc tính này là tùy chọn, bạn cũng có thể bỏ qua nó nếu không có nhu cầu tìm kiếm nó.

Một tùy chọn khác, trường tính toán có thể lưu trữ trong cơ sở dữ liệu bằng cách đặt thuộc tính của trường tính toán là store=True. Với thuộc tính này, sau khi được tính toán, các giá trị sẽ được lưu trữ và từ đó, các trường này được truy xuất giống như các trường thông thường thay vì phải tính toán lại. Nhờ trình trang trí @api.depends, ORM sẽ biết được khi nào giá trị của trường tính toán (được lưu trữ) sẽ được tính toán lại và cập nhật. Ngoài ra, nếu bạn đặt thuộc tính store=True vào trường tính toán của mình, trường tính toán này sẽ tìm kiếm được thay vì phải thêm thuộc tính search với một phương thức tìm kiếm.

Cờ compute_sudo=True được sử dụng trong một số trường hợp với các đặc quyền nâng cao, ví dụ như khi tính toán cần sử dụng dữ liệu mà người dùng cuối không truy cập được.

Quan trọng

Giá trị mặc định của thuộc tính compute_sudo là False nếu phiên bản Odoo nhỏ hơn v13. Kể từ phiên bản Odoo v13, giá trị mặc định của thuộc tính này sẽ phụ thuộc vào thuộc tính store. Nếu store=True thì compute_sudo=True và ngược lại là False.

Còn nữa

Từ v13, Odoo đã giới thiệu thêm về bộ nhớ đệm chung. vì vậy, nếu bạn có một trường được tính toán phụ thuộc các giá trị ngữ cảnh, thì đôi khi bạn sẽ không nhận được giá trị chính xác. Để khắc phụ điều này, bạn cần sử dụng trình trang trí @api.depends_context. Tham khảo ví dụ sau:

@api.depends('school_id')
@api.depends_context('company_id')
def _compute_value(self):
   company_id = self.env.context.get('company_id')
   #...

Bạn có thể thấy trong ví dụ rằng tính toán của chúng ta đang sử dụng company_id từ ngữ cảnh. Bằng cách sử dụng company_id trong trình trang trí phụ thuộc, chúng ta đảm bảo rằng giá trị trường sẽ được tính lại dựa trên giá trị của company_id trong ngữ cảnh.

Chuẩn bị

Chúng ta sẽ sử dụng module viin_education từ phần trước.

Các bước thực hiện

  1. Tại model education.school, chúng ta có các trường sau: name, code, address...

    class EductionSchool(models.Model):
       _name = 'education.school'
    
       # ...
       name = fields.Char(string='Name', translate=True, required=True)
       code = fields.Char(string='Code')
       address = fields.Char(string='Address')
    
  2. Tại model education.student, đảm bảo rằng có một trường quan hệ Many2one đến model education.school, và các trường related.

    class EductionStudent(models.Model):
       _name = 'education.student'
    
       # ...
       school_id = fields.Many2one('education.school', string='School')
       school_code = fields.Char(related='school_id.code', string='School Code')
       school_address = fields.Char(string='school_id.address', string='School Address')
    

Cuối cùng, chúng ta sẽ nâng cấp module viin_education để bổ sung các trường mới vào model.

Cơ chế hoạt động

Các trường related cũng giống các trường thông thường, nhưng các trường này có một thuộc tính bổ sung related với một chuỗi các trường được phân tách bởi dấu chấm.

Trong hướng dẫn bên trên, tại model education.student, dữ liệu trường school_codeschool_address được lấy từ trường codeaddress của model education.school thông qua trường school_id. Chúng ta có thể có các chuỗi liên quan dài hơn, ví dụ như school_id.country_id.code.

Theo mặc định, các trường related sẽ không được lưu trữ và chỉ đọc (store=False, readonly=True), để tránh việc người dùng có thể thay đổi giá trị của nó.

Nếu chúng ta sửa thành trường liên quan readonly=True:

# ...
school_address = fields.Char(string='school_id.address', string='School Address', readonly=False)

Thì khi thay đổi giá trị trường school_address trên model education.student, giá trị trường address trên model education.school cũng sẽ thay đổi theo. Vì vậy, cần thận trọng khi đặt thuộc tính readonly=False cho trường related.

Còn nữa

Trên thực tế, các trường related là các trường computed. Chúng chỉ cung cấp một cú pháp thuận tiện để đọc các giá trị trường từ model liên quan. Vì là trường được tính toán, nên mặc định sẽ có thuộc tính store=False. Chúng có tất cả các thuộc tính từ trường được tham chiếu. ví dụ như string, required, translate, selection...

Ngoài ra, các trường related cũng hỗ trợ thuộc tính related_sudo tương tự như compute_sudo. Khi được đặt là True, chuỗi trường được đọc mà không cần kiểm tra quyền người dùng.

Việc sử dụng các trường related trong phương thức create() có thể ảnh hưởng đến hiệu suất, vì quá trình tính toán các trường này bị trì hoãn cho đến khi kết thúc quá trình tạo.

Thêm quan hệ động bằng cách sử dụng trường Reference

Với các trường quan hệ, chúng ta cần xác định trước các model (hoặc co-model) của quan hệ. Tuy nhiên, đôi khi chúng ta cần để lại việc xác định model này cho người dùng và trước tiên hãy chọn model chúng ta muốn, sau đó là bản ghi mà chúng ta liên kết. Điều này có thể đạt được bằng cách sử dụng các trường Reference.

Chuẩn bị

Trong phần này, chúng ta sẽ giới thiệu về một ví dụ về trường tham chiếu của Odoo.

Các bước thực hiện

  1. Đầu tiên, chúng ta có thể xem định nghĩa trường Reference của Odoo tại ~odoo/odoo/addons/base/models/ir_ui_menu.py.

    # ...
    class IrUiMenu(models.Model):
       # ...
       action = fields.Reference(selection=[
          ('ir.actions.report', 'ir.actions.report'),
          ('ir.actions.act_window', 'ir.actions.act_window'),
          ('ir.actions.act_url', 'ir.actions.act_url'),
          ('ir.actions.server', 'ir.actions.server'),
          ('ir.actions.client', 'ir.actions.client')])
    
  2. Bạn có xem cách trường này thể hiện trên giao diện người dùng thông qua các bước với chế độ nhà phát triển Thiết lập ‣ Kỹ thuật ‣ Trình đơn (Menu).

    Giao diện trường reference v1
  3. Tại trường Hành động, bạn có thể chọn 1 lựa chọn trong danh sách đã khai báo ở bước 1. Sau khi chọn một giá trị, một mục sẽ xuất hiện ở bên cạnh, nó sẽ cho bạn chọn 1 bản ghi theo model mà bạn đã chọn ở đằng trước.

    Giao diện trường reference v2
  4. Nhập nội dung cho trường Trình đơn và lưu form. Bạn sẽ thấy kết quả của trường Hành động.

    Giao diện trường reference v3

Cơ chế hoạt động

Các trường Reference tương tự như các trường Many2one, ngoại trừ việc chúng cho phép người dùng chọn model để liên kết. Danh sách các model truyền vào cũng giống danh sách lựa chọn của trường Selection, là một sanh sách các tuple gồm 2 phẩn tử. Phần tử đầu tiên là mã định danh, phần tử thứ 2 là giá trị.

Với ví dụ trên, chúng ta đã cung cấp một danh sách 5 lựa chọn. Tuy nhiên, bạn cũng có thể tạo 1 danh sách động thông qua một hàm. Theo sau ví dụ sau:

class IrUiMenu(models.Model):
   # ...
   @api.model
   def _referen_models(self):
      models = self.env['ir.model'].search([])
      return [(x.model, x.name) for x in models]

   action = fields.Reference(selection='_referen_models')

với đoạn code này, tất cả model sẽ có sẵn trong danh sách lựa chọn. Điều này giúp linh hoạt hơn trong việc xác định model tham chiếu mà các model này chỉ được xác định sau này.

Trình trang trí @api.model cần được sử dụng vì nó hoạt động ở cấp model.

Dữ liệu của trường Reference lưu trữ trong cơ sở dữ liệu có dạng model,id, ví dụ như res.users,1. Sau khi được sử dụng và đi qua ORM, dữ liệu này sẽ được xử lý và thể hiện thành bản ghi của model được tham chiếu đến (recordset).

Mặc dù tính năng này có vẻ linh hoạt, nhưng nó đi kèm với chi phí thực thi đáng kể. Hiển thị các trường reference cho một số lượng lớn các bản ghi (ví dụ: trong tree view) có thể tạo ra các tải cơ sở dữ liệu nặng vì mỗi giá trị phải được tra cứu trong một truy vấn riêng biệt. Nó cũng không thể tận dụng tính toàn vẹn tham chiếu của cơ sở dữ liệu, không giống như các trường quan hệ thông thường.

Thêm các tính năng vào Model bằng cách sử dụng kế thừa

Theo Odoo, Odoo cung cấp ba loại kế thừa:

  • Kế thừa Class (mở rộng).

  • Kế thừa Prototype (nguyên mẫu).

  • Kế thừa Delegation (ủy thác).

Trong phần này, chúng ta sẽ sẽ vào Kế thừa Class. Nó được sử dụng để thêm các trường hoặc các phương thức mới vào các model hiện có.

Chuẩn bị

Chúng ta sẽ sử dụng module viin_education từ phần trước.

Các bước thực hiện

Chúng ta sẽ mở rộng model res.partner có sẵn tại file models/res_partner.py.

  1. Đầu tiên, chúng ta sẽ kế thừa model res.partner:

    from odoo import fields, models, api
    
    class ResPartner(models.Model):
       _inherit = 'res.partner'
    
  2. Thêm một trường vào model:

    # ...
    is_teacher = fields.Boolean(string='Is Teacher')
    
  3. Thêm một phương thức mới vào model:

    def _get_all_teachers(self):
      return self.search([('is_teacher', '=', True)])
    

Cuối cùng, nâng cấp module để áp dụng thay đổi này.

Cơ chế hoạt động

Khi một lớp model được xác định bởi thuộc tính _inherit, nó sẽ thêm các sửa đổi vào model kế thừa, thay vì thay thế nó. Điều này có nghĩa là các trường được định nghĩa mới trong lớp kế thừa sẽ được thêm hoặc thay đổi trong lớp cha. Trong cơ sở dữ liệu, ORM sẽ thêm một cột vào bảng cơ sở dữ liệu của model.

Nếu trường thêm mới trong lớp kế thừa và trường này đã tồn tại trong lớp cha, thì chỉ những thuộc tính được khai báo trong lớp kế thừa mới được sửa đổi. Còn những thuộc tính khác trong lớp cha sẽ giữ nguyên.

Với ví dụ trên, trong trường is_teacher và phương thức _get_all_teachers đã được thêm vào model res.partner.

Các phương thức trong lớp kế thừa sẽ thay thế các phương thức trong lớp cha nếu bạn đặt trùng tên. Do đó, nếu bạn không gọi phương thức cha bằng lệnh gọi super, trong trường hợp đó, phương thức cha sẽ không được thực thi. Vì vậy, khi bạn thêm một logic mới bằng cách kế thừa phương thức thì hãy sử dụng lệnh gọi super để thực thi cả logic của phương thức cha.

Sao chép định nghĩa Model bằng cách sử dụng kế thừa

Chúng ta đã giới thiệu về kế thừa Class (mở rộng) trong phần trước. Trong phần này, chúng ta sẽ tiếp tục về kế thừa Prototype (nguyên mẫu).

Chuẩn bị

Chúng ta sẽ sử dụng module viin_education từ phần trước.

Các bước thực hiện

Kế thừa Prototype (nguyên mẫu) sẽ sử dụng đồng thời 2 thuộc tính là _inherit_name cùng lúc.

  1. Thêm một file model mới models/education_student_copy.py.

    from odoo import fields, models
    
    class EducationStudentCopy(models.Model):
       _name = 'education.student.copy'
       _inherit = 'education.student'
       _description = 'Education Student - Copy'
    
  2. Import file trên vào models/__init__.py.

    # ...
    from . import education_student_copy
    

Cuối cùng, nâng cấp module để áp dụng thay đổi này.

Cơ chế hoạt động

Với việc sử dụng 2 thuộc tính _inherit_name cùng một lúc, một model mới với tên được đặt theo thuộc tính _name được tạo ra và được sao chép từ model cha theo thuộc tính _inherit.

Theo ví dụ của chúng ta, model education.student.copy đã được sao chép từ model education.student. Trong cơ sở dữ liệu, một bảng mới của model education.student.copy được tạo ra với dữ liệu riêng của nó, nó hoàn toàn độc lập với model education.student. Bây giờ, khi thêm bất cứ trường hoặc phương thức nào vào model mới này cũng sẽ không làm ảnh hưởng đến model cha.

Vì nó vẫn kế thừa từ model education.student nên bất kỳ sự thay đổi nào đối với nó cũng sẽ làm ảnh hưởng đến các model đã kế thừa đến nó.

Cảnh báo

Kế thừa Prototype không hoạt động nếu bạn sử dụng cùng một tên model trong thuộc tính _name. Nếu bạn sử dụng cùng một tên cho thuộc tính _name, nó sẽ chỉ hoạt động như một kiểu kế thừa Class (mở rộng).

Sử dụng kế thừa Delegation để sao chép các tính năng sang Model khác

Trong một số trường hợp, chúng ta sẽ kế thừa và mở rộng model hiện có hoặc là tạo model mới dựa trên model có sẵn. Điều này có thể thực hiện bằng cách kế thừa prototype (nguyên mẫu), nhưng nó sẽ tạo ra các cấu trúc trùng lặp. Nếu bạn muốn sao chép các định nghĩa của model thành model mới nhưng không muốn sao chép cấu trúc dư liệu, bạn cũng có thể thực hiện bằng cách sử dụng loại kế thừa thứ ba là kế thừa Delegation (ủy thác), nó sẽ dùng thuộc tính _inherits.

Chuẩn bị

Chúng ta sẽ sử dụng module viin_education từ phần trước.

Các bước thực hiện

  1. Chúng ta đã có model education.student.copy, bây giờ, chúng ta sẽ thay đổi nó để model này kế thừa ủy thác đến model res.partner:

    from odoo import fields, models
    
    class EducationStudentCopy(models.Model):
       _name = 'education.student.copy'
       _inherits = {'res.partner': 'partner_id'}
       _description = 'Education Student - Copy'
    
       partner_id = fields.Many2one('res.partner', string='Student', ondelete="restrict", required=True)
    
  2. Tiếp theo, chúng ta sẽ thêm một số trường mới vào model:

    # ...
       date_of_birth = fields.Date(string='Date of Birth')
       age = fields.Integer(string='Age')
    

Cuối cùng, nâng cấp module viin_education để áp dụng thay đổi trên.

Cơ chế hoạt động

Thuộc tính _inherits của model đặt tập hợp các model cha mà chúng ta muốn kế thừa. Trong ví dụ trên, chúng ta chỉ kế thừa đến model đối tác res.partner.

Giá trị của thuộc tính _inherits là kiểu dict bao gồm các cặp key-value. Trong đó, key là các model kế thừa và value là tên trường được sử dụng model để liên kết đến model kế thừa. Trường này sẽ có kiểu là Many2one, tham chiếu đến model sẽ kế thừa và được xác định trong model. Trong ví dụ tren của chúng ta, trường partner_id là trường được sử dụng để liên kết đến model kế thừa res.partner.

Để rõ hơn về cách hoạt động của kiểu kế thừa này, bạn có thể xem điều gì sẽ xảy ra cở cơ sở dữ liệu khi chúng ta tạo một học sinh mới:

  • Một bản ghi mới được tạo trong bảng res_partner.

  • Một bản ghi được tạo trong bảng education_student_copy.

  • Cột partner_id của bảng education_student_copy là id của bản ghi đưcọ tạo trong bảng res_partner.

Điều này có nghĩa, là khi bạn tạo một bản ghi học sinh, nó cũng sẽ tự động tạo ra một bản ghi đối tác, và nó cũng tự động thêm quan hệ vào trường partner_id. Khi duyệt qua các bản ghi đối tác, bạn cũng có thể thấy rằng, tất cả các bản ghi học sinh cũng là đối tác, nhưng có thể chỉ có một số đôi tác là học sinh.

Ngoài ra, đối với việc xóa các bản ghi đối tác, chúng ta đã đặt thuộc tính ondelete="restrict" để đảm bảo rằng sẽ không thể xóa bản ghi đối tác khi có bản ghi học sinh tham chiếu đến. Chúng ta cũng có thể đặt ondelete="cascade" để có thể xóa bản ghi đối tác và đồng thời cũng sẽ xóa bản ghi học sinh nếu bản ghi học sinh có tham chiếu đến.

Quan trọng

Với kiểu kế thừa ủy thác này, nó chỉ hoạt động với các trường, và không liên quan đến các phương thức. Vì vậy, model mới sẽ không thể sử dụng được bất kỳ phương thức nào trong model đã kế thừa.

Còn nữa

Có một lối tắt trong việc kế thừa ủy thác, thay vì đặt thuộc tính _inherits của model, bạn cũng có thể đặt thuộc tính delegate=True cho trường many2one cần tham chiếu. Ưu diểm của cách này là đơn giản hơn:

from odoo import fields, models

class EducationStudentCopy(models.Model):
   _name = 'education.student.copy'
   _description = 'Education Student - Copy'

   partner_id = fields.Many2one('res.partner', string='Student', ondelete="restrict", required=True, delegate=True)

Sử dụng Abstract Models cho các tính năng của Model có thể tái sử dụng

Trong nhiều trường hợp, chúng ta có một tính năng cụ thể và muốn thêm vào nhiều model khác nhau. Nhưng việc lặp đoạn code trong nhiều model khác nhau sẽ không tốt, sẽ tốt hơn nhiều nếu có thể thực hiện nó 1 lần và sử dụng lại nó.

Các Abstract model (model trừu tượng) cho phép chúng ta tạo model chung, triển khai một số tính năng chung mà sau đó, các model thông thường sẽ kế thừa lại nó để tính năng đó trở nên khẳ dụng.

Chuẩn bị

Chúng ta sẽ sử dụng module viin_education từ phần trước.

Các bước thực hiện

  1. Đầu tiên, chúng ta sẽ thêm một model trừu tượng models/base_education.py như sau:

    from odoo import fields, models
    
    class BaseDucation(models.AbstractModel):
       _name = 'base.education'
    
       active = fields.Boolean(string='Active')
    
       def do_active(self):
          for r in self:
             r.active = not r.active
    
  2. Import base_education.py vào file models/__init__.py:

    # ...
    from . import base_education
    
  3. Tiếp theo, chúng ta sẽ chỉnh sửa model education.class kế thừa vào model base.education:

    #...
    class EducationClass(models.Model):
       #...
       _inherit = 'education.class'
       # ...
    

Cuối cùng, nâng cấp module viin_education để áp dụng thay đổi trên.

Cơ chế hoạt động

Model trừ tượng được tạo dựa trên việc class kế thừa từ models.AbstractModel thay vì models.Model thông thường. Nó có đầy đủ các thuộc tính cũng như các phương thức của các model thông thường, nhưng ORM sẽ không tạo bảng cho nhưng model trừ tượng này trong cơ sở dữ liệu. Điều này có nghĩa là model này sẽ không có dữ liệu và chỉ đóng vai trò như một khuôn mẫu cho các tính năng có thể tái sử dụng ở các model thông thường sẽ kế thừa vào nó.

Với ví dụ trên, chúng ta đơn giản chỉ định nghĩa1 model trừ tượng mới với 1 trường active và 1 phương thức để đảo ngược giá trị cho trường này. Sau đó, model education.class hay bất kỳ model nào khác kế thừa vào nó cũng sẽ có sẵn các tính năng của nó.

Với thuộc tính _inherit, giá trị của nó sẽ có 2 dạng. Sử dụng một danh sách các model nếu chúng ta muốn kế thừa nếu kế thừa nhiều model hoặc sử dụng chuỗi văn bản đến 1 model.