Basic Server-Side Development

Ở phần Application Models chúng ta đã được học cách khai báo và mở rộng các model, sử dụng các trường dữ liệu (fields), trường tính toán (compute fields) cũng như các cách để ràng buộc trường giá trị trong một ứng dụng cụ thể. Phần này tập trung vào việc định nghĩa ra các phương thức (methods), thao tác với tập dữ liệu (recordset) và mở rộng các phương thức thừa kế.

Định nghĩa phương thức và sử dụng API decorators

Phần này chúng ta sẽ học cách định nghĩa ra một phương thức và gọi nó bằng một button trên giao diện người dùng hoặc gọi nó từ một hàm khác.

Chuẩn bị

Chúng ta tiếp tục sử dụng add-on viin_education đã tạo ở Creating Odoo Add-On Modules. Bạn cần thêm trường state vào model education.student, đoạn code sẽ như sau:

viin_education/models/education_student.py
class EducationStudent(models.Model):
    _name = 'education.student'
    # ...
    state = fields.Selection(string='Status', selection=[('new', 'New'),
                                                         ('studying', 'Studying'),
                                                         ('off', 'Off')], default='new')

Tham khảo phần Thêm models trong Creating Odoo Add-On Modules để biết thêm thông tin.

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

Giả sử chúng ta muốn định nghĩa ra một phương thức có thể thay đổi trạng thái của các học sinh được chọn, hãy thêm đoạn code sau vào education.student.

  1. Đầu tiên tạo một helper method để kiểm tra xem đầu vào của trạng thái có hợp lệ hay không.

    viin_education/models/education_student.py
    @api.model
    def is_allowed_state(self, current_state, new_state):
        allowed_states = [('new', 'studying'), ('studying', 'off'), ('off', 'studying'), ('new', 'off')]
        return (current_state, new_state) in allowed_states
    
  1. Bước 2 tạo một phương thức cho phép thay đổi state mới.

    viin_education/models/education_student.py
    def change_student_state(self, state):
        for student in self:
            if student.is_allowed_state(student.state, state):
                student.state = state
            else:
                continue
    
  1. Bước 3 tạo các phương thức để thay đổi sang một trạng thái mới tương ứng.

    viin_education/models/education_student.py
    def change_to_new(self):
        self.change_student_state('new')
    
    def change_to_studying(self):
        self.change_student_state('studying')
    
    def change_to_off(self):
        self.change_student_state('off')
    
  1. Thêm các button và field status như đoạn code dưới đây vào header của form view. Nó sẽ gọi các method ở phía trên từ giao diện người dùng.

    viin_education/views/education_student_views.xml
    <form>
    ...
        <header>
            ...
            <button name="change_to_new" type="object" string="New"/>
            <button name="change_to_studying" type="object" string="Studying"/>
            <button name="change_to_off" type="object" string="Off"/>
            <field name="state" widget="statusbar"/>
            ...
        </header>
    ...
    </form>
    

Nâng cấp module để xem kết quả...

Cơ chế hoạt động

Các phương thức trên đều là các phương thức cơ bản của Python, điều đáng chú ý ở đây là một số phương thức sử dụng decorator đến từ module odoo.api.

Mẹo

API decorator ban đầu được Odoo giới thiệu bản 9.0 để hỗ trợ cả framework cũ và mới. Kể từ Odoo 10.0 API cũ không còn được hỗ trợ nữa, tuy nhiên một số decorators như @api.model vẫn còn được sử dụng.

Khi viết một phương thức mới, nếu không sử dụng bất kỳ một decorator nào có nghĩa là phương thức đó đang được thực thi trên một tập bản ghi (recordset). Điều này có nghĩa là self ở phương thức này là recordset tham chiếu đến số lượng bản ghi tương ứng trên cơ sở dữ liệu (có thể bao gồm recordset rỗng). Do đó phải dùng vòng lặp để làm việc với từng bản ghi cụ thể.

Ngược lại khi dùng @api.model trên một phương thức, thì self ở đây chỉ liên quan đến model và không còn liên quan đến tập bản ghi tương ứng model đó nữa. Nó có khái niệm tương tự với việc sử dụng @classmethod decorator của Python.

Bước 1 chúng ta đã tạo phương thức is_allowed_state(). Mục đích của phương thức này để kiểm tra xem trạng thái muốn thay đổi có cho phép hay không, như ở ví dụ này chúng ta không muốn thay đổi trạng thái của student từ off sang new. Đó là lý do mà chúng ta không thêm điều kiện ('off', 'new') vào hàm. Như bạn thấy hàm này không quan tâm đến recordset liên quan đến model vậy có thể sử dụng @api.model trong trường hợp này. Lúc này cho dù bạn có 10 bản ghi student, lúc thực thi self trong hàm cũng chỉ là recordset student rỗng.

bước 2, phương thức change_student_state() có nhiệm vụ thay đổi trạng thái của student. Khi hàm này được gọi, nó sẽ thay đổi trạng thái của student với tham số state đã cho nếu trạng thái hợp lệ. Để ý xem, chúng ta đang sử dụng vòng lặp vì self ở đây có thể là nhiều bản ghi.

Bước tiếp theo chúng ta tạo các hàm để chuyển sang các trạng thái tương ứng.

Bước cuối cùng thêm các button vào form view. Khi click vào button này, Odoo sẽ gọi các phương thức trong Python ứng với giá trị của thuộc tính name, xem Backend Views để biết thêm chi tiết. Chúng ta cũng thêm trường state với widget statusbar để hiển thị trạng thái của student trên form view. Ví dụ khi người dùng click vào button có name="change_to_studying" từ giao diện, phương thức change_to_studying()bước 3 sẽ được gọi.

Thông báo lỗi tới người dùng

Trong quá trình thực thi phương thức, đôi khi cần phải hủy bỏ quá trình xử lý vì hành động do người dùng thực hiện không hợp lệ hoặc thỏa mãn điều kiện để xảy ra lỗi. Phần này sẽ hướng dẫn cho bạn cách quản lý những trường hợp đó bằng cách hiển thị ra những thông báo lỗi hữu ích.

Chuẩn bị

Tiếp tục phần trước, giả sử instance của bạn đã sẵn sàng và cài sẵn module viin_education.

Thực hiện thế nào

Chúng ta sẽ thay đổi phương thức change_student_state để hiển thị lỗi cho người dùng khi họ thay đổi trạng thái student sang một trạng thái không cho phép. Thực hiện như các bước sau đây:

  1. Thêm các import sau đây vào đầu nội dung education_student.py

    viin_education/models/education_student.py
    from odoo import _
    from odoo.exceptions import UserError
    
  2. Thay đổi phương thức change_student_state như sau:

    viin_education/models/education_student.py
    def change_student_state(self, state):
        for student in self:
            if student.is_valid_state(student.state, state):
                student.state = state
            else:
                raise UserError(_("Changing student status from %s to %s is not allowed.") % (student.state, state))
    

Cơ chế hoạt động

Khi người dùng thay đổi trạng thái của bản ghi student có state không được cho phép, cụ thể là student có trạng thái off sang new, trên giao diện phía client sẽ hiển thị một popup như sau:

Sử dụng raise UserError

Mọi ngoại lệ (exception) không được định nghĩa trong odoo.exceptions sẽ được xử lý như là internal server error (HTTP Status 500) với stack strace. UserError sẽ hiển thị thông báo lỗi trên giao diện người dùng. Việc sử dụng raise UserError để đảm bảo rằng thông điệp cảnh báo hiển thị tới người dùng một cách thân thiện nhất.

Như ví dụ trên nội dung của của UserError đang sử dụng function _() được định nghĩa trong odoo. Function này dùng để đánh dấu rằng đoạn văn bản này có thể dịch. Vui lòng đọc Internationalization để biết thêm chi tiết.

Quan trọng

Khi sử dụng function _():
- Đúng: _("Changing student status from %s to %s is not allowed.") % (student.state, state)
- Sai: _("Changing student status from %s to %s is not allowed." % (student.state, state))

Ngoài ra còn một số lớp ngoại lệ được định nghĩa trong odoo.exceptions

  • ValidationError: Ngoại lệ thường được dùng khi xử lý Python constraint. Vui lòng xem Application Models phần Thêm ràng buộc trong model để biết thêm chi tiết.

  • AccessError: Lỗi này thường được raise tự động khi người dùng cố gắng truy cập thứ gì đó không được phép. Bạn cũng có thể sử dụng nó một cách thủ công trong đoạn code của mình.

  • RedirectWarning: Hiển thị popup gồm thông báo lỗi và nút để chuyển đến một action tùy ý.

  • Warning: Loại bỏ từ phiên bản 9.0 và thay thế bằng UserError.

Tạo instance của một model bất kỳ

Khi sử dụng Odoo, nếu bạn muốn làm việc trên các model khác nhau, không chỉ đơn giản là tạo instance trực tiếp từ class của model đó mà bạn cần lấy một recordset cho model đó mới có thể sử dụng được. Phần này chúng ta sẽ tìm hiểu cách để lấy một recordset rỗng của bất kỳ một model nào trong phương thức của một model.

Chuẩn bị

Tiếp tục phần trước, giả sử instance của bạn đã sẵn sàng và cài sẵn module viin_education. Chúng ta sẽ viết một phương thức đơn giản trong education.class để lấy ra tất cả học sinh. Để làm điều này, chúng ta cần lấy một recordset rỗng của education.student. Chắc chắn rằng bạn đã thêm model education.class và access rights cho model đó. Thêm đoạn code dưới đây, nếu đã có bạn có thể bỏ qua...

Thêm trường class_id vào model education.student

viin_education/models/education_student.py
class EducationStudent(models.Model):
    _name = 'education.student'
    # ...
    class_id = fields.Many2one('education.class', string='Class', ondelete="restrict")

Thêm trường student_ids vào model education.class

viin_education/models/education_class.py
class EducationClass(models.Model):
    _name = 'education.class'
    # ...
    student_ids = fields.One2many('education.student', 'class_id', string='Students')

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

  1. Trong class EducationClass thêm phương thức sau:

    viin_education/models/education_class.py
    class EducationClass(models.Model):
        # ...
        def get_all_students(self):
            # Khởi tạo đối tượng education.student (đây là một recordset rỗng của model education.student)
            student = self.env['education.student']
            all_students = student.search([])
            print("All Students: ", all_students)
    
  2. Thêm button sau trong form view lớp học

    viin_education/views/education_class_views.xml
    ...
        <button name="get_all_students" type="object" string="Log All Students" />
    ...
    

Sau khi nâng cấp module, bạn sẽ thấy nút Log All Students trên form view lớp học. Click vào nút này để xem dữ liệu recordset của student trong server log. Kết quả ở ví dụ và của bạn có thể khác nhau tùy vào số lượng bản ghi student mà bạn tạo...

...
All Students:  education.student(1, 2, 3, 4)
...

Cơ chế hoạt động

Khi khởi động, Odoo sẽ load tất cả các module và kết hợp các lớp khác nhau từ Model. Các lớp này được lưu trữ trong Odoo registry, được lập chỉ mục theo name. Thuộc tính env của bất kỳ recordset nào được truy cập dưới dạng self.env, là một instance của lớp Environment được định nghĩa trong module odoo.api

Lớp Environment đóng vai trò cốt lõi trong Odoo, nó cung cấp:

- self.env['model_name'] để lấy recordset rỗng của model model_name (hay còn gọi là tạo đối tượng).
- self.env.cr là con trỏ (cursor) của cơ sở dữ liệu để truy vấn Raw SQL.
- self.env.user tham chiếu tới user hiện tại đang thực hiện.
- self.env.context hoặc self._context là một dictionary chứa các dữ liệu môi trường.

Tạo mới các bản ghi

Phần này chúng ta sẽ tìm hiểu cách để tạo mới các bản ghi, đây là điều quan trọng khi viết các phương thức logic nghiệp vụ.

Chuẩn bị

Chúng ta sẽ tiếp tục sử dụng module viin_education, thêm trường student_ids vào model education.class, class EducationClass sẽ trở thành như sau:

viin_education/models/education_class.py
class EducationClass(models.Model):
    _name = 'education.class'
    _description = 'Education Class'

    # ...
    student_ids = fields.One2many('education.student', 'class_id', string='Students')

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

  1. Thêm phương thức create_classes vào trong education.class

    def create_classes(self):
        # Giá trị để tạo bản ghi student 01
        student_01 = {
            'name': 'Student 01',
        }
        # Giá trị để tạo bản ghi student 02
        student_02 = {
            'name': 'Student 02'
        }
        # Giá trị để tạo bản ghi lớp học
        class_value = {
            'name': 'Class 01',
            # Đồng thời tạo mới 2 học sinh
            'student_ids': [
                (0, 0, student_01),
                (0, 0, student_02)
            ]
        }
        record = self.env['education.class'].create(class_value)
    
  2. Thêm button Create Classes vào form lớp học:

    viin_education/views/education_class_views.xml
    ...
        <button name="create_classes" type="object" string="Create Classes" />
    ...
    

Cơ chế hoạt động

Để tạo một bản ghi mới cho một model, chúng ta có thể gọi phương thức create(values) ở bất kỳ recorset nào. Phương thức này trả về 1 recordset mới, với các trường giá trị cụ thể trong values dictionary. Trong dictionary này, key là tên trường, value là giá trị tương ứng trường đó. Tùy thuộc vào từng loại trường bạn cần phải chuyển đổi type tương ứng cho các giá trị đó.

- Trường Text: giá trị là chuỗi Python.
- Trường FloatInteger: giá trị là Python float hoặc integer.
- Trường Boolean: giá trị là Python bool hoặc integer.
- Trường Date: giá trị là Python datetime.date object.
- Trường Datetime: giá trị là Python datetime.datetime object.
- Trường Binary: giá trị là chuỗi mã hóa Base64. Module base64 trong thư viện tiêu chuẩn của Python cung cấp phương thức encodebytes(bytestring) cho phép mã hóa một chuỗi trong Base64.
- Trường Many2one: là một số kiểu integer - ID của bản ghi quan hệ lưu trong cơ sở dữ liệu.
- Trường One2ManyMany2many: giá trị là một cú pháp đặc biệt, sử dụng các command sau:

Lệnh | Chức năng

(0, 0, values)

Tạo mới bản ghi có quan hệ với bản ghi chính với values (dictionary) đã cho

(6, 0, ids)

Thay thế toàn bộ danh sách các bản ghi quan hệ hiện tại bằng danh sách mới đã cho

Khi click vào button Create Classes ở phía trên, 3 bản ghi sẽ được tạo. Lúc này vào giao diện list view lớp học sẽ xuất hiện 1 bản ghi có tên Class 01, và 2 students lần lượt là Student 01, Student 02 trong list view Student.

Mở rộng

Hàm create còn hỗ trợ bạn tạo cùng lúc nhiều bản ghi. Để tạo bạn cần truyền list dictionary vào phương thức create. Cùng xem đoạn code sau:

...
class_01 = {
    'name': 'Class 01'
}
class_02 = {
    'name': 'Class 02'
}
record = self.env['education.class'].create([class_01, class_02])

Cập nhật giá trị của bản ghi

Logic nghiệp vụ thường yêu cầu chúng ta cập nhật các bản ghi bằng cách thay đổi giá trị của một số lĩnh vực. Phần này chỉ cho bạn cách thay đổi các trường trên bản ghi cụ thể.

Chuẩn bị

Chúng ta sẽ tiếp tục sử dụng module viin_education, sử dụng model education.class để làm ví dụ...

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

  1. Thêm phương thức change_class_name vào trong model education.class

    viin_education/models/education_class.py
    def change_class_name(self):
        self.ensure_one()
        self.name = 'Class 12A1'
    
  2. Thêm button Create Classes vào form lớp học để tạo bản ghi:

    viin_education/views/education_class_views.xml
    ...
        <button name="change_class_name" type="object" string="Change Name" />
    ...
    
  3. Khởi động lại instance và upgrade module viin_education. Click vào nút Change Name, dữ liệu lớp học sẽ thay đổi.

Cơ chế hoạt động

Phương thức này bắt đầu bằng việc kiểm tra xem self có đúng một bản ghi hay không thông qua phương thức ensure_one. Nó sẽ raise exception nếu không thỏa mãn điều kiện và quá trình xử lý bị hủy bỏ. Điều này là cần thiết vì chúng tôi không muốn thay đổi tên của nhiều bản ghi. Nếu bạn muốn update nhiều bản ghi hãy xóa dòng self.ensure_one(). Cuối cùng là việc cập nhật tên của lớp học bằng cách thay đổi giá trị của thuộc tính đại diện cho trường name của bản ghi lớp học.

Có 3 cách để cập nhật giá trị của bản ghi:

  1. Gán trực tiếp giá trị cho thuộc tính đại diện trường của bản ghi.

  2. Sử dụng hàm write với tham số là một dictionary, đoạn code trên sẽ như sau:

    viin_education/models/education_class.py
    def change_class_name(self):
        self.ensure_one()
        self.write({
            'name': 'Class 12A1'
        })
    

    Cảnh báo

    Hàm này có thể việc với nhiều records, chỉ hoạt động với các bản ghi thật sự tồn tại trong cơ sở dữ liệu. Vui lòng đọc Advanced Server-Side Development Techniques để biết thêm chi tiết.

  3. Sử dụng hàm update với tham số là một dictionary

    Hàm này có thể việc với nhiều records dùng trong các trường hợp đặc biệt, thường được sử dụng với các bản ghi ảo (pseudo-records). Vui lòng đọc Advanced Server-Side Development Techniques để biết thêm chi tiết.

Tương tự như create, khi cập nhật các trường quan hệ One2many và Many2many cũng có các command tương ứng:

Lệnh

Chức năng

(0, 0, values)

Tạo mới bản ghi có quan hệ với bản ghi chính với values (dictionary) đã cho.

(1, id, values)

Cập nhật bản ghi có id = id trong danh sách bản ghi quan hệ với values (dictionary) đã cho.

(2, id, 0)

Loại bỏ bản ghi có id = id trong danh sách bản ghi quan hệ và xóa khỏi cơ cở dữ liệu.

(3, id, 0)

Loại bỏ bản ghi có id = id trong danh sách bản ghi quan hệ nhưng không xóa khỏi cơ sở dữ liệu.

(4, id, 0)

Thêm bản ghi có id = id vào danh sách các bản ghi quan hệ.

(5, 0, 0)

Loại bỏ toàn bộ bản ghi trong danh sách bản ghi quan hệ. Tương ứng với (3, id, 0) cho mỗi bản ghi cụ thể trong danh sách đó.

(6, 0, ids)

Thay thế toàn bộ danh sách bản ghi quan hệ hiện tại bằng danh sách mới đã cho.

Tìm kiếm bản ghi

Tìm kiếm bản ghi cũng là một hoạt động phổ biến và vô cùng quan trọng trong các phương thức logic nghiệp vụ.

Chuẩn bị

Tiếp tục sử dụng module viin_education, chúng tôi sẽ viết phương thức mới có tên find_student trong model education.student cho phép tìm kiếm các học sinh có tên chứa "John" hoặc thuộc lớp có tên là "12A1"

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

  1. Thêm phương thức find_student:

    viin_education/models/education_student.py
    def find_student(self):
        ...
    
  1. Thêm search domain vào phương thức find_student:

    viin_education/models/education_student.py
    def find_student(self):
        # miền lọc theo điều kiện: tên có chứa từ John hoặc lớp tên là 12A1
        domain = ['|', ('name', 'ilike', 'John'), ('class_id.name', '=', '12A1')]
    
  1. Gọi phương thức search với domain phía trên, nó sẽ trả về recordset

    viin_education/models/education_student.py
    def find_student(self):
        domain = ['|', ('name', 'ilike', 'John'), ('class_id.name', '=', '12A1')]
        students = self.search(domain)
    

Cơ chế hoạt động

Bước 1 định nghĩa method.

Bước 2 tạo biến domain chứa miền tìm kiếm. Để có giải thích đầy đủ về cú pháp miền tìm kiếm vui lòng xem Backend Views.

Bước 3 gọi phương thức search() với domain, do cùng model nên chỉ cần self.search() là đủ. Nó trả về một tập bản ghi chứa tất cả các bản ghi khớp với miền điều kiện. Ở ví dụ này chúng tôi chỉ sử dụng search với domain, tuy nhiên không chỉ có vậy nó còn hỗ trợ khá nhiều tham số khác nữa:

- offset: Được sử dụng để bỏ qua N bản ghi đầu tiên phù hợp với truy vấn, thường được sử dụng cùng với limit để chia nhỏ các bản ghi thành từng phần (mặc định: 0). Ví dụ: phân trang..
- limit: Số lượng bản ghi tối đa để trả về (mặc định: None)
- order: Sắp xếp các bản ghi được trả về. Mặc định sử dụng thuộc tính _order của model.
- count: True nếu bạn muốn kết quả trả về là số lượng bản ghi (mặc định: False)

Ghi chú

Sử dụng search_count(domain) thay vì search(domain, count=True) để thể hiện nội dung một cách rõ ràng hơn do cả 2 đều cho cùng một kết quả.

Đôi khi bạn muốn tìm kiếm từ 1 model khác, để làm điều này bạn cần lấy recordset model đó (vui lòng đọc Tạo instance của một model bất kỳ). Ví dụ chúng tôi muốn tìm kiếm các bản ghi trên model hr.employee

def find_employees(self):
    # tạo đối tượng sale order
    Employee = self.env['hr.employee']
    # tìm kiếm nhân viên là nữ và có họ Nguyễn
    domain = ['&', ('gender', '=', 'female'), ('name', 'ilike' 'Nguyễn%')]
    # gọi phương thức search
    employees = Employee.search(domain)

Ghi chú

domain ['&', ('gender', '=', 'female'), ('name', 'ilike' 'Nguyễn%')][('gender', '=', 'female'), ('name', 'ilike' 'Nguyễn%')] đều cho kết quả giống nhau vì mặc định '&' được tự động thêm nếu không chỉ định.

Trước đó chúng tôi có nói rằng search() trả về tất cả các bản ghi khớp với điều kiện. Trong một số trường hợp điều này không thực sự hoàn toàn đúng, ví dụ:

- Người dùng chỉ nhận được các bản ghi nếu họ có quyền đọc
- Model có dùng trường active kiểu Boolean nếu muốn lấy tất cả bản ghi thì phải dùng context active_test=False vì mặc định search chỉ trả về các bản ghi có active=True
self.env['model_name'].with_context(active_test=False).search(domain)

Kết hợp các recordset

Chuẩn bị

Phần này bạn cần có hai hoặc nhiều bản ghi cùng một model.

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

Ví dụ với các recordset trên model education.student

  1. education.student(1,) + education.student(2,) = education.student(1, 2)

  2. education.student(1, 2) | education.student(2, 3) = education.student(1, 2, 3)

  3. education.student(1, 2) & education.student(2, 3) = education.student(2, )

  4. education.student(1, 2) - education.student(2, ) = education.student(1, )

  5. education.student(1, 2) >= education.student(1,) => True (Tương tự với > và ngược lại với <=<)

  6. education.student(1, 2) <= education.student(4,) => False (Tương tự với < và ngược lại với >=>)

Cơ chế hoạt động

1. Kết hợp 2 recordset thành 1 tập recordset.
2. Hợp 2 recordset lại và loại bỏ trùng lặp.
3. Giao 2 recorset => giữ lại recordset chung là education.student(2, ).
4. Loại bỏ giá trị recordset vế phải trong recordset vế trái đó là education.student(2, ).
5. Kiểm tra xem recordset vế trái có chứa vế recordset vế phải hay không.
6. Tương tự.

Xem thêm

Odoo ORM Basic phần Toán tử trên Recordset

Lọc recordset

Trong một số trường hợp bạn muốn lọc ra các recordset đã có sẵn với một số tiêu chí mà mình mong muốn. Bạn có thể dùng vòng lặp để duyệt thủ công từng recordset, tuy nhiên Odoo đã cung cấp phương thức filtered() để thực hiện việc này một cách nhanh chóng hơn.

Chuẩn bị

Hãy chắc chắn rằng instance của bạn đang chạy và có model education.class như đã định nghĩa trong Tạo instance của một model bất kỳ.

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

Ví dụ trong phần này là lọc lớp học có 5 học sinh trở lên. Bạn cần thực hiện các bước sau đây:

  1. Định nghĩa phương thức classes_has_student với đầu vào là tập bản ghi lớp học

    @api.model
    def classes_has_student(self, all_classes):
        ...
    
  2. Gọi phương thức filtered với tham số là 1 callback

    @api.model
    def classes_has_student(self, all_classes):
        return all_classes.filtered(lambda c: len(c.student) >= 5)
    

Bạn có thể kiểm tra kết quả bằng cách print ra server log ...

Cơ chế hoạt động

filtered(callback) trả về recordset (có thể rỗng). Nó duyệt tất cả các bản ghi qua vòng lặp để kiểm tra callback đã cho return True hay False, nếu True sẽ được thêm vào tập recordset trả về.

Ví dụ trên chúng tôi sử dụng hàm lambda, tuy nhiên bạn cũng có thể sử dụng một hàm riêng biệt như sau:

@api.model
def classes_has_student(self, all_classes):
    def has_student(_class):
        return len(_class.student) > 5
    return all_classes.filtered(has_student)

Hơn nữa bạn cũng có thể truyền tham số cho filtered với một field. Ví dụ bạn chỉ cần lọc ra các lớp có học sinh:

@api.model
def classes_has_student(self, all_classes):
    return all_classes.filtered('student_ids')

Odoo còn cung cấp phương thức filtered_domain(domain) để lọc recordset qua domain.

Ghi chú

filtered chỉ làm việc với dữ liệu có sẳn của recordset trên bộ nhớ, không truy cập cơ sở dữ liệu để lấy dữ liệu. Do đó bạn có thể lọc bản ghi thông qua các trường không tồn tại trong cơ sở dữ liệu...

Xem thêm

Odoo ORM Basic phần Filter

Sử dụng mapped

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

Để lấy tên của học sinh từ một tập bản ghi lớp học bạn cần thực hiện các bước sau:

  1. Định nghĩa phương thức có tên là get_student_names():

    @api.model
    def get_student_names(self, classes):
        ...
    
  2. Gọi phương thức mapped() để lấy danh sách tên của học sinh.

    @api.model
    def get_student_names(self, classes):
        return classes.mapped('student_ids.name')
    

Cơ chế hoạt động

Chúng tôi gọi tới hàm mapped(path) để duyệt qua các giá trị name của từng recordset, path là một chuỗi bao gồm tên của trường phân cách bởi dấu chấm. mapped() có 3 tùy chọn:

- Nếu field cuối trong path là một trường không quan hệ thì kết quả trả về là một danh sách giá trị của trường đó theo thứ tự của recordset. Ví dụ:
# return list student's name ['Student 01', 'Student 02', ...]
class_records.student_ids.mapped('name')

# hoặc như này đều được
class_records.mapped('student_ids.name')

# return list class name. Ví dụ: ['Class 01', Class 02']
class_records.mapped('name')
- Nếu path là trường quan hệ thì kết quả trả về là một recordset của quan hệ đó.
# return recordset of student. ví dụ: education.student(1, 2) ...
class_records.mapped('student_ids')

# return recordset of school. ví dụ: education.school(1, )
class_records.mapped('student_ids.school_id')
- Bạn cũng có thể truyền tham số với một callback. ví dụ:
# return list class name. Ví dụ: ['Class 01', Class 02']
class_record.mapped(lambda r: r.name)

Mẹo

Trong trường hợp chỉ 1 bản ghi thì không nhất thiết phải dùng mapped. Ví dụ: class_record.name. Tuy nhiên nếu có 2 bản ghi trở lên mà không sử dụng mapped sẽ gây lỗi expected singleton. Ví dụ class_records.name.

Ghi chú

Tương tự như filtered, mapped cũng chỉ hoạt động trên dữ liệu có sẵn của recordset trên bộ nhớ, không truy cập cơ sở dữ liệu.

Xem thêm

Sắp xếp các recordset

Sắp xếp các recordset là điều cần thiết, đôi khi bạn cần sắp xếp lại các recordset đã có theo một thứ tự cụ thể. Nó cũng hữu ích trong việc sắp xếp lại recordset nếu bạn sử dụng các toán tử để kết hợp các recordset với nhau, điều này có thể làm mất thứ tự của chúng.

Phần này sẽ hướng dẫn cách sử dụng phương thức sorted để sắp xếp lại các recordset đang tồn tại. Chúng tôi sẽ sắp xếp các học sinh theo ngày sinh.

Chuẩn bị

Thêm field dob vào model education.student, nếu đã có bạn có thể bỏ qua bước này.

dob = fields.Date(string='Date of birth')

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

Bạn cần thêm đoạn code sau để sắp xếp học sinh theo ngày sinh

@api.model
def sort_students_by_dob(self, students):
    # sắp xếp tăng dần theo dob
    return students.sorted(key='dob')

Cơ chế hoạt động

Bên trong sorted tìm nạp đến dữ liêu các trường đã truyền qua đối số key. Sau đó sắp xếp lại bằng việc sử dụng Python built-in method sorted rồi trả về tập recordset mới đã được sắp xếp.

sorted còn hỗ trợ 1 tham số tùy chọn reverse (mặc định = False) giúp bạn có muốn đảo ngược thứ tự bản ghi hay không. Trường hợp muốn đảo ngược chỉ cần truyền reverse=True như sau:

@api.model
def sort_students_by_dob(self, students):
    # sắp xếp tăng dần theo dob
    return students.sorted(key='dob', reverse=True)

Khi gọi sorted` không sử dụng đối số nào, thuộc tính ``_order trong model sẽ được sủ dụng.

Mở rộng logic nghiệp vụ trong model

Một điều rất hay trong Odoo là chia các tính năng của ứng dụng thành các module khác nhau. Bằng cách đó, bạn có thể chỉ cần bật / tắt các tính năng trong cài đặt hoặc gỡ cài đặt ứng dụng. Khi bạn thêm các tính năng mới vào ứng dụng bất kỳ, việc tùy chỉnh hành vi của các phương thức hiện có của model là rất cần thiết. Đôi khi, bạn cũng muốn thêm các trường mới vào model hiện có. Đây là một rất dễ dàng trong Odoo và một trong những tính năng mạnh mẽ nhất của framework này.

Trong phần này, chúng tôi sẽ hướng dẫn cách mở rộng model, chỉnh sửa phương thức và thêm trường mới.

Chuẩn bị

Giả sử trong model education.class đã tồn tại đoạn code sau:

viin_education/models/education_class.py
def add_student(self):
    self.ensure_one()
    self.write({
        'student_ids': [(0, 0, {
            'name': 'Student'
        })]
    })

Và button trong views

viin_education/views/education_class_views.xml
...
    <button name="add_student" type="object" string="Add Student" />
...

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

Tạo file education_class_size.py trong viin_education/models với đoạn code sau:

from odoo import fields, models,
from odoo.exceptions import UserError


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

    max_student = fields.Integer(string='Max Student', default=20)

    def add_student(self):
        # Kiểm tra max student trước khi thực hiện
        if len(self.student_ids) > self.max_student:
            raise UserError('The number of students has exceeded %s' % self.max_student)
        # Gọi lại nội dung phương thức cha
        super(EducationClass, self).add_student()

Cơ chế hoạt động

Đầu tiên chúng tôi định nghĩa 1 model mở rộng từ education.class, thêm trường max_student và chỉnh sửa phương thức add_student. Ở cuối phương thức có sử dụng từ khóa super, nó giúp thực thi nội dung phương thức của lớp cha mà không cần phải định nghĩa lại. Tùy theo yêu cầu của bài toán mà bạn có thể linh động chỉnh sửa trước hoặc sau khi gọi super. Quá trình thực hiện như sau:

- Bước 1: Kế thừa lại phương thức add_student() của lớp cha education.class.
- Bước 2: Kiểm tra số lượng học sinh trong lớp đã quá số lượng cho phép hay chưa trước khi thực thi. Nếu không thỏa mãn thì thông báo lỗi tới người dùng thông qua raise UserError.
- Bước 3: Thực thi lại nội dung gốc thông của phương thức add_student() qua super().

Mở rộng

Bất kỳ model nào được định nghĩa và kế thừa models.Model đều có phương thức create(), write()unlink(). Tùy theo nhu cầu mà bạn cần mở rộng các phương thức này để thực hiện các logic nghiệp vụ khác nhau trong khi thêm, sửa hoặc xóa bản ghi. Tương tự cách mở rộng phương thức ở ví dụ phía trên, bạn có thể chỉnh sửa lại các phương thức này theo ý muốn của mình. Ví dụ:

@api.model
def create(self, values):
    # thực hiện logic của bạn
    return super(EducationClass, self).create(values)

def write(self, values):
    # thực hiện logic của bạn
    return super(EducationClass, self).write(values)

def unlink(self):
    # thực hiện logic của bạn
    return super(EducationClass, self).unlink()

Nhóm dữ liệu bằng read_group()

Thông thường để tổng hợp dữ liệu theo tiêu chí, chúng ta thường sử dụng group byaggregate function trong SQL. Để nhanh chóng hơn Odoo đã hỗ trợ chúng ta phương thức read_group() giúp thực hiện điều tương tự. Phần này chúng tôi sẽ hướng dẫn cách sử dụng read_group để tổng hợp dữ liệu.

Ví dụ muốn nhóm số lượng học sinh theo từng lớp học, bạn cần thêm đoạn code sau vào model education.student:

@api.model
def group_by_class(self):
    group_result = self.read_group(
        [('state', '=', 'studying')], # domain
        ['class_id'], # field
        ['class_id'] # group by
        )
    return group_result

Để kiểm tra kết quả thực tế, bạn cần thêm một nút vào giao diện người dùng để trigger method này. Sau đó, bạn có thể in kết quả trong server log.

Kêt quả trả về dưới dạng list các dictionary dạng như sau:

[
    # lớp học với class_id = 1 có 6 học sinh
    {'class_id_count': 6, 'class_id': (1, <odoo.tools.func.lazy object at 0x7f92ca08e460>), '__domain': [('class_id', '=', 1)]},
    # lớp học với class_id = 2 có 3 học sinh
    {'class_id_count': 3, 'class_id': (2, <odoo.tools.func.lazy object at 0x7f92ca224dc0>), '__domain': [('class_id', '=', 2)]}
    ...
]

Vui lòng đọc thêm trong Odoo ORM Basic - Các hàm ORM cơ bản.

Mẹo

read_group() nhanh hơn rất nhiều so với việc đọc và xử lý các giá trị từ recordset. Vì vậy, đối với báo cáo hoặc đồ thị, bạn nên sử dụng nó.