Advanced Server-Side Development Techniques

Trong Basic Server-Side Development bạn đã biết cách viết phương thức cho một model, class, cách mở rộng các phương thức từ các model kế thừa và cách làm việc với recordsets. Trong phần này chúng ta sẽ đề cập đến các chủ đề nâng cao hơn, chẳng hạn như làm việc với env của recordsets, gọi một phương thức khi bấm vào nút và làm việc với phương thức onchange. Các phương thức trong phần này sẽ giúp bạn quản lý các vấn đề, nghiệp vụ phức tạp hơn. Bạn sẽ học cách tạo các ứng dụng có tính tương tác cao hơn.

Trong phần này, chúng ta sẽ đề cập đến các nội dung sau:

Thay đổi người dùng để thực hiện một hành động

Khi viết code logic nghiệp vụ, bạn có thể phải thực hiện một số hành động với một bối cảnh bảo mật khác nhau. Trường hợp điển hình là thực hiện một hành động với quyền của superuser, bỏ qua kiểm tra bảo mật. Yêu cầu như vậy nảy sinh khi các yêu cầu nghiệp vụ bắt buộc hoạt động trên các bản ghi mà người dùng không có quyền truy cập.

Phần dưới đây sẽ cho bạn thấy làm thế nào người dùng bình thường có thể sửa đổi số di động của học sinh bằng cách sử dụng sudo().

Chuẩn bị

Chúng ta sẽ làm việc với bản ghi của model education.student. Theo thiết lập quyền truy cập của module viin_education thì phải thuộc nhóm quyền viin_education_group_user mới có thể sửa được thông tin học sinh. Tuy nhiên chúng ta có thể cung cấp điểm truy cập để thay đổi số di động của học sinh khi mà người dùng không thuộc nhóm quyền viin_education_group_user.

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

Để cho người dùng bình thường có thể sửa đổi số điện thoại của một học sinh, bạn cần thực hiện các bước sau:

  1. Kế thừa mở rộng model education.student:

    class EducationStudent(models.Model):
        _inherit = 'education.student'
    
  2. Thêm một phương thức update_mobile_number():

    def update_mobile_number(self, new_number):
    
  3. Trong phương thức này phải đảm bảo hành động chỉ thực hiện trên một bản ghi duy nhất:

    self.ensure_one()
    
  4. Sửa đổi người dùng của environment:

    self = self.sudo()
    
  5. Thay số di động mới cho học sinh:

    self.mobile = new_number
    

Cơ chế hoạt động

Trong bước 4, chúng ta gọi self.sudo(). Phương thức này trả về một recordset mới với environment mới mà người dùng có quyền superuser. Khi recordset được gọi với sudo(), environment sẽ sửa đổi thuộc tính của environment thành su với trạng thái superuser. Bạn có thể truy cập trạng thái của nó qua recordset.env.su. Tất cả các phương thức gọi qua recordset sudo này được thực hiện bằng quyền superuser. Để hiểu rõ hơn về điều này, bạn hãy bỏ dòng self = self.sudo() trong code rồi gọi đến phương thức update_mobile_number từ người dùng không thuộc nhóm quyền viin_education_group_user. Sẽ có cảnh báo lỗi Access Error trả về. Chỉ cần sử dụng sudo() sẽ bỏ qua tất cả các quy tắc bảo mật.

Nếu bạn cần chỉ định một người dùng cụ thể bạn có thể truyền vào recordset bản ghi người dùng đó hoặc id của người dùng đó trong cơ sở dữ liệu như sau:

education_group_user = self.env.ref('viin_education.viin_education_group_user')
student_a = self.env['education.student'].with_user(education_group_user).search([('name', 'ilike', 'Nguyễn Thị Đào')])

Đoạn code trên sẽ cho phép bạn tìm kiếm học sinh có tên Nguyễn Thị Đào với người dùng không thuộc nhóm quyền viin_education_group_user.

Mở rộng

Việc sử dụng sudo(), bạn có thể bỏ qua quyền truy cập và quy tắc bản ghi bảo mật. Đôi khi bạn có thể truy cập vào nhiều bản ghi được tách biệt, chẳng hạn như bản ghi từ các công ty khác nhau trong môi trường đa công ty. Sử dụng sudo() recordset bỏ qua tất cả quy tắc bảo mật của Odoo.

Nếu bạn không cẩn thận, các bản ghi được tìm kiếm trong environment này có thể được liên kết với bất kỳ công ty nào có trong cơ sở dữ liệu, điều đó có nghĩa là bạn có thể bị lộ thông tin cho người dùng. Tệ hơn, bạn có thể vô tình làm hỏng cơ sở dữ liệu khi liên kết các bản ghi thuộc về các công ty khác nhau.

Tip

Khi sử dụng sudo(), luôn kiểm tra kỹ để đảm bảo rằng các lần gọi phương thức search() không dựa vào các quy tắc bản ghi tiêu chuẩn để lọc kết quả.

Xem thêm

Tham khảo các tài liệu này để biết thêm thông tin:

Gọi một phương thức có ngữ cảnh đã sửa đổi

Context là một phần trong environment của recordset. Nó được sử dụng để truyền các thông tin bổ sung như múi giờ và ngôn ngữ của người dùng từ giao diện người dùng. Bạn cũng có thể sử dụng context để truyền các tham số cụ thể trong các hành động. Một số phương thức trong các addon của Odoo tiêu chuẩn sử dụng context để điều chỉnh logic nghiệp vụ dựa trên các giá trị context này. Đôi khi cần phải sửa đổi context trên một recordset để có được kết quả mong muốn khi gọi một phương thức hoặc giá trị mong muốn cho một trường được tính toán.

Phần này sẽ hướng dẫn cách thay đổi hành vi của một phương thức dựa trên các giá trị trong environmental context.

Chuẩn bị

Phần này chúng ta sẽ làm ví dụ với module viin_education_admission. Khi chúng ta bấm nút Hủy trên phiếu Đăng ký nhập học của học sinh sẽ lưu trữ thông tin học sinh đó trên hệ thống.

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

  1. Trong model education.application sửa action_cancel như sau:

    def action_cancel(self):
        for r in self:
            r.stutent_id.with_context(test_archive=True).test_archive_student()
        self.write({'state': 'cancelled'})
    
  2. Kế thừa model education.student bổ sung phương thức:

    from odoo import models
    
    class EducationStudent(models.Model):
        _inherit = 'education.student'
    
        def test_archive_student(self):
            self.ensure_one()
            if self.env.context.get('test_archive', False):
                self.active = False
    
  3. Vào một phiếu Đăng ký nhập học có gắn tới học sinh, bấm hủy và kiểm tra học sinh trong phiếu đó đã bị lưu trữ.

Cơ chế hoạt động

Ở bước 1 chúng ta gọi r.stutent_id.with_context() với một tham số truyền vào. Điều này trả về một phiên bản mới của bản ghi student_id với context được cập nhật. Ở đây, chúng ta thêm một khóa trong context, nếu muốn bạn cũng có thể truyền vào nhiều khoá khác.

Ở bước 2 chúng ta kiểm tra context của self: if self.env.context.get('test_archive', False), tức là nếu trong context có khóa test_archive thì sẽ chạy dòng code self.active = False.

Đây là một ví dụ đơn giản về sửa đổi context, bạn cũng có thể sử dụng nó ở bất kỳ đâu trong ORM. Bạn hãy hiểu đơn giản là truyền thêm tham số cho context và ở chỗ nào đó sẽ lấy tham số trong context vừa truyền để sử dụng.

Mở rộng

Chúng ta cũng có thể truyền một dict vào with_context(). Trong bước 1 nếu truyền dict vào context sẽ ghi đè dict hiện tại, mất những thông tin khác của context. Tùy từng mục đích của người dùng có cần đè context hay không để viết code cho hợp lý. Code trong bước 1 cũng có thể viết như sau để tránh ghi đè context:

def action_cancel(self):
    for r in self:
        context = self.env.context.copy()
        context.update({'test_archive': True})
        r.stutent_id.with_context(context).test_archive_student()
    self.write({'state': 'cancelled'})

Xem thêm

Tham khảo các phần sau để biết thêm thông tin về các context trong Odoo:

Thực thi các truy vấn SQL thuần

Hầu hết thời gian bạn có thể thực hiện các hoạt động bạn muốn bằng cách sử dụng ORM. Ví dụ như bạn sử dụng phương thức search() để tìm kiếm các bản ghi. Tuy nhiên đôi khi bạn cần nhiều hơn nữa, hoặc bạn không thể diễn đạt những gì bạn muốn bằng cách sử dụng domain (cho một số thao tác phức tạp, nếu không muốn nói là hoàn toàn không thể), hoặc truy vấn của bạn yêu cầu phải gọi nhiều lần phương thức search dẫn tới không hiệu quả.

Mục này cho phép bạn sử dụng các truy vấn SQL thuần để tính toán thông tin số điện thoại không định dạng trên bản ghi học sinh.

Kỹ thuật phát triển server-side sử dụng compute

Chuẩn bị

Phần này chúng ta sẽ sử dụng model education.student làm ví dụ. Bình thường có thể mỗi người sẽ nhập số điện thoại theo cách của họ. Ví dụ có người nhập 0912 345 678, có người nhập 0912.345.678, … Nếu muốn tìm kiếm học sinh theo số điện thoại thì không thể search() được đúng người, hoặc khi search() miền tìm kiếm sẽ khá khó khăn.

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

Giờ trên model education.student chúng ta sẽ bổ sung một trường phone_no_format để tính toán ra một trường điện thoại không định dạng trên học sinh. Để khi tìm kiếm học sinh theo số điện thoại sẽ cho kết quả đúng.

  1. Kế thừa model education.student bổ sung trường phone_no_format là trường compute:

    from odoo import models, fields, api
    
    class EducationStudent(models.Model):
        _inherit = 'education.student'
    
        phone_no_format = fields.Char(string='Phone No Format', compute='_compute_phone_mobile_no_format')
    
        def _compute_phone_no_format(self):
            _query = """
                SELECT es.id,
                    regexp_replace(coalesce(res.phone, ''), '[^0-9]+','','g') AS phone_no_format
                FROM education_student AS es
                    LEFT JOIN res_partner AS res ON res.id = es.partner_id
                WHERE es.id in %s
                """, (tuple(self.ids),)
            self.env.cr.execute(_query)
            student_data = self.env.cr.dictfetchall()
            mapped_data = dict([
                (row['id'], {'phone_no_format': row['phone_no_format']}) for row in student_data])
            for r in self:
                r.phone_no_format = mapped_data.get(r.id, {}).get('phone_no_format', False)
    
  2. Đưa trường phone_no_format lên form view để kiểm tra kết quả:

    <record id="education_student_view_form" model="ir.ui.view">
        <field name="name">education.student.inherit</field>
        <field name="model">education.student</field>
        <field name="inherit_id" ref="viin_education.education_student_view_form" />
        <field name="arch" type="xml">
            <field name="phone" position="after">
                <field name="phone_no_format"/>
            </field>
        </field>
    </record>
    

Cơ chế hoạt động

Trong hàm _compute_phone_no_format() chúng ta khai báo một câu truy vấn SQL để lấy idphone của bảng education_student. Do trường phone được khai báo trên bảng res_partner, chúng ta phải JOIN đến bảng res_partner để lấy giá trị trường phone. Chúng ta sử dụng regexp_replace(coalesce(res.phone, ''), '[^0-9]+','','g') để xóa đi những ký tự không phải số.

Chúng ta gọi phương thức execute() trên con trỏ cơ sở dữ liệu được lưu trữ trong self.env.cr để sẽ gửi truy vấn đến PostgreSQL và thực thi nó.

Sử dụng phương thức dictfetchall() của con trỏ để truy xuất danh sách các hàng được chọn bởi truy vấn, và đưa dữ liệu trả về thành dạng list các dict, mỗi dict chứa các khóa id, phone_no_format và giá trị của chúng.

Cuối cùng ta sẽ map dữ liệu theo từng id của học sinh và giá trị của phone_no_format.

Note

Nếu bạn đang thực hiện truy vấn UPDATE, bạn cần phải xóa bộ nhớ cache theo cách thủ công. Vì bộ nhớ cache của Odoo ORM không biết về những thay đổi bạn đã thực hiện với truy vấn UPDATE. Để xóa bộ nhớ cache bạn có thể sử dụng self.invalidate_cache().

Mở rộng

Đối tượng trong self.env.cr là một lớp bao quanh con trỏ psycopg2. Dưới đây sẽ là những phương thức thường được sử dụng với cr:

  • execute(query, params): Phương thức này thực thi truy vấn SQL với các tham số được đánh dấu %s trong truy vấn được thay thế bằng các giá trị trong tham số, là một tuple.

    Warning

    Không bao giờ được truyền trực tiếp tham số, luôn sử dụng các tùy chọn định dạng như %s, bởi vì nếu bạn sử dụng kĩ thuật như nối chuỗi nó có thể làm cho mã dễ bị SQL Injection.

  • fetchone(): Phương thức này trả về một hàng từ cơ sở dữ liệu, được bọc trong một tuple (ngay cả khi chỉ có một cột được chọn bởi truy vấn).

  • fetchall(): Phương thức này trả về tất cả các hàng từ cơ sở dữ liệu dưới dạng list các tuple.

  • dictfetchall(): Trả về tất cả các hàng từ cơ sở dữ liệu dưới dạng list các dict với key, value là tên cột và giá trị.

Hãy thận trọng khi sử dụng các truy vấn SQL thuần:

  • Bạn sẽ bỏ qua tất cả các bảo mật của ứng dụng. Khi bạn gọi search([('id', 'in', tuple(ids)]) với bất kỳ danh sách ID nào, hệ thống đã tự lọc ra các bản ghi mà người dùng không có quyền truy cập.

  • Bất kỳ thay đổi nào bạn đang thực hiện đều bỏ qua các ràng buộc được thiết lập bởi các module bổ trợ, ngoại trừ các ràng buộc NOT NULL, UNIQUEFOREIGN KEY, được thực thi ở tầng cơ sở dữ liệu. Sự kiện kích hoạt tính toán lại các trường được tính toán cũng sẽ bị bỏ qua, vì vậy có thể làm hỏng cơ sở dữ liệu.

  • Tránh các truy vấn INSERT/UPDATE, vì việc thông qua truy vấn này sẽ không chạy bất kỳ logic nghiệp vụ nào được viết bằng cách ghi đè các phương thức create()write(). Nó sẽ không cập nhật các trường tính toán được lưu trữ và các ràng buộc của ORM cũng sẽ bị bỏ qua.

Xem thêm

Viết một wizard để hướng dẫn người dùng

Trong Application Models chúng ta đã được tìm hiểu về các tính năng của các model trừu tượng , được giới thiệu ở models.TransientModel. Class này giống với các model bình thường, ngoại trừ việc các bản ghi của TransientModel sẽ được dọn dẹp định kỳ trong cơ sở dữ liệu nên được gọi là model tạm thời. Chúng được sử dụng để tạo wizards hoặc hộp thoại trên giao diện người dùng, thường được sử dụng để thực hiện các hành động trên các bản ghi vĩnh viễn trong cơ sở dữ liệu.

Chuẩn bị

Phần này chúng ta sử dụng module viin_education để tạo ví dụ. Chúng ta sẽ thêm một nút Thôi học trên form view của học sinh. Khi bấm nút Thôi học sẽ hiển thị một hộp thoại yêu cầu điền lý do thôi học lên bản ghi học sinh đó.

Kỹ thuật phát triển server-side sử dụng wizard

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

  1. Kế thừa model education.student bổ sung các thông tin:

    from odoo import fields, models, api
    
    class EducationStudent(models.Model):
        _inherit = 'education.student'
    
        dropout_reason = fields.Text(string='Dropout Reason')
    
        def action_dropout(self):
            return self.env.ref('viin_education.education_student_dropout_wizard_action').read()[0]
    
  2. Trên form view của model education.student bổ sung các thông tin:

    <record id="education_student_view_form" model="ir.ui.view">
        <field name="name">education.student.inherit</field>
        <field name="model">education.student</field>
        <field name="inherit_id" ref="viin_education.education_student_view_form" />
        <field name="arch" type="xml">
            <field name="responsible_id" position="after">
                <field name="dropout_reason"/>
            </field>
            <xpath expr="//header/field[@name='state']" position="before">
                <button name="action_dropout" string="Dropout" type="object" />
            </xpath>
        </field>
    </record>
    
  3. Thêm một wizard mới education.student.dropout.wizard:

    from odoo import fields, models
    
    class EducationStudentDropoutWizard(models.TransientModel):
        _name = 'education.student.dropout.wizard'
        _description = 'Education Student Dropout Wizard'
    
        def _default_student(self):
            active_model = self.env.context.get('active_model')
            active_id = self.env.context.get('active_id')
            return self.env[active_model].browse(active_id)
    
        student_id = fields.Many2one('education.student', string='Student', default=_default_student, required=True)
        dropout_reason = fields.Text(string='Dropout Reason', required=True)
    
        def action_confirm(self):
            self.student_id.dropout_reason = self.dropout_reason
            self.student_id.state = 'off'
    
  4. Tạo form, action cho model education.student.dropout.wizard:

    <record id="education_student_dropout_wizard_action" model="ir.actions.act_window">
        <field name="name">Dropout Reason</field>
        <field name="type">ir.actions.act_window</field>
        <field name="res_model">education.student.dropout.wizard</field>
        <field name="view_mode">form</field>
        <field name="target">new</field>
    </record>
    
    <record id="education_student_dropout_wizard_form" model="ir.ui.view">
        <field name="name">education.student.dropout.wizard.form</field>
        <field name="model">education.student.dropout.wizard</field>
        <field name="arch" type="xml">
            <form string="Dropout Reason">
                <group>
                    <field name="student_id" invisible="1"/>
                    <field name="dropout_reason"/>
                </group>
                <footer>
                    <button name="action_confirm" string="Confirm" type="object" class="oe_highlight"/>
                    <button special="cancel" string="Cancel"/>
                </footer>
            </form>
        </field>
    </record>
    
  5. Phân quyền cho model education.student.dropout.wizard:

    id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
    access_education_student_dropout_wizard,education.student.dropout.wizard,model_education_student_dropout_wizard,viin_education.viin_education_group_user,1,1,1,1
    

Cơ chế hoạt động

Bước 3 định nghĩa một model mới. Nó không khác gì các model khác ngoài phần models.TransientModel thay vì models.Model. Cả TransientModelModel đều chung một lớp base là BaseModel.

Với các bản ghi TransientModel chỉ có các thay đổi sau:

  • Các bản ghi được xóa định kỳ khỏi cơ sở dữ liệu để các chúng không bị phình lên theo thời gian.

  • Bạn không được phép thêm trường One2many trên TransientModel mà tham chiếu đến model bình thường. Việc này sẽ thêm một cột trên comodel liên kết với dữ liệu tạm thời. Trường hợp này nên sử dụng quan hệ Many2many. Tất nhiên, bạn có thể sử dụng trường One2many nếu comodel trong One2many cũng là TransientModel.

Ở bước 1 chúng ta có code của phương thức action_dropout, khi bấm nút Thôi học trên giao diện của học sinh sẽ được gọi đến education_student_dropout_wizard_action. Trong education_student_dropout_wizard_action chúng ta có thiết lập target=new để mở lên hộp thoại trên form view học sinh. Trên form hộp thoại có thông tin student_id được điền mặc định là ID của form học sinh hiện tại nhờ phương thức _default_student() ở bước 3. Khi chúng ta điền Lý do thôi học trên hộp thoại và bấm Xác nhận, hàm action_confirm trong bước 3 sẽ điền Lý do thôi học từ hộp thoại xuống form học sinh hiện tại.

Note

Các phiên bản trước Odoo 14, TransientModel không yêu cầu bất kỳ quy tắc truy cập nào. Bất cứ ai cũng có thể tạo và truy cập các bản ghi do chính họ tạo. Với Odoo 14, TransientModel bắt buộc phải phân quyền truy cập.

Mở rộng

Sử dụng context để tính toán các giá trị mặc định

Wizard mà chúng ta trình bày ở trên yêu cầu người dùng phải điền học sinh lên form hộp thoại. Khi một hành động kích hoạt wizard được thực thi, context được cập nhật với một số giá trị có thể sử dụng:

  • active_model: Tên của model liên quan đến hành động này. Nói chung đây là model được hiển thị trên màn hình. Trên ví dụ thì active_model chính là model education.student.

  • active_id: ID của bản ghi liên quan đến hoạt động này. Trên ví dụ thì active_id chính là ID của bản ghi học sinh hiện tại.

  • active_ids: Nếu nhiều bản ghi được chọn để gọi đến wizard thì đây sẽ là danh sách các ID của các bản ghi đó. Điều này xảy ra khi tích chọn nhiều bản ghi trong list view rồi kích hoạt wizard. Trong form view bạn sẽ nhận được [active_id].

  • active_domain: miền bổ sung khi kích hoạt wizard.

Các giá trị trên có thể được sử dụng để tính toán các giá trị mặc định của model hoặc trong phương thức được gọi bởi nút.

Wizard và tái sử dụng code

Khi sử dụng wizard hãy kiểm tra chắc chắn rằng mã nguồn để có thể sử dụng các khóa active_model/active_id/active_ids/active_domain từ context hay không. Trường hợp đứng từ model khác gọi đến phương thức action_confirm, chúng ta cần truyền các khóa active_model/active_id/active_ids/active_domain vào context để sử dụng. Tham khảo cách truyền contextGọi một phương thức có ngữ cảnh đã sửa đổi.

Như ví dụ trên ta cần phải truyền active_model là model education.studentactive_id là ID của học sinh muốn cho Thôi học vào context khi gọi đến phương thức action_confirm của model education.student.dropout.wizard.

Chuyển hướng người dùng

Ở bước 3 trong hàm action_confirm chúng ta không trả về bất cứ gì. Điều này sẽ khiến hộp thoại trình hướng dẫn bị đóng sau khi hành động được thực hiện. Một khả năng khác là phương thức trả về một dictionary với các trường của ir.action.

Ví dụ sau khi bấm Xác nhận ta muốn trả về form view lớp học của học sinh đã thôi học, ta có thể sửa code vào hàm action_confirm như sau:

def action_confirm(self):
    self.ensure_one()
    self.student_id.dropout_reason = self.dropout_reason
    self.student_id.state = 'off'

    result = self.env.ref('viin_education.education_class_action').read()[0]
    res = self.env.ref('viin_education.education_class_view_form', False)
    result['views'] = [(res and res.id or False, 'form')]
    result['res_id'] = self.student_id.class_id.id
    return result

Kĩ thuật chuyển hướng người dùng có thể được sử dụng để tạo một wizard có các bước được thực hiện lần lượt. Mỗi bước trong wizard có thể sử dụng các giá trị của các bước trước đó bằng cách cung cấp một nút Tiếp theo gọi một phương thức được định nghĩa trên wizard, cập nhật một số trường trên wizard, trả về hành động sẽ hiển thị lại cùng một wizard đã cập nhật và sẵn sàng bước tiếp theo.

Xem thêm

Định nghĩa phương thức onchange

Khi viết logic nghiệp vụ, thường xảy ra trường hợp có một số lĩnh vực liên quan với nhau. Chúng ta đã được biết về ràng buộc giữa các trường trong Application Models. Phần này minh họa một khái niệm hơi khác. Ở đây, các phương thức onchange được gọi khi thay đổi một trường trên giao diện người dùng để cập nhật giá trị các trường khác của bản ghi, thường là trong form view.

Chuẩn bị

Phần này chúng ta sẽ sử dụng module viin_education để thực hiện. Chúng ta mình họa điều này bằng cách thay đổi giá trị của trường Lớp trên giao diện học sinh. khi thay đổi giá trị trường Lớp thì trường Nhóm lớp học cũng sẽ thay đổi theo.

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

  1. Trong model education.student thêm phương thức:

    @api.onchange('class_id')
    def _onchange_class_id(self):
        if self.class_id:
            self.class_group_id = self.class_id.class_group_id
    
  2. Điền thông tin Nhóm lớp học cho Lớp. Truy cập menu Cấu hình/Lớp:
    Kỹ thuật phát triển server-side sử dụng onchange
  3. Trên giao diện học sinh, chúng ta chọn thông tin Lớp cho học sinh:

    Kỹ thuật phát triển server-side sử dụng onchange

Cơ chế hoạt động

Phương thức onchange sử dụng decorator @api.onchange() và truyền vào tên của trường mà khi thay đổi trường đó sẽ kích hoạt lệnh gọi phương thức. Trong trường hợp này chúng ta thấy rằng bất cứ khi nào trường class_id thay đổi trên giao diện người dùng thì phương thức _onchange_class_id() sẽ được gọi.

Trong nội dung của phương thức, chúng ta gán luôn giá trị trường Nhóm lớp học trên bản ghi Lớp sang trường Nhóm lớp học trên bản ghi học sinh hiện tại.

Mở rộng

Việc sử dụng cơ bản của các phương thức onchange là tính toán giá trị mới cho các trường khi thay đổi một số trường khác trên giao diện người dùng. Bên trong nội dung của phương thức, bạn có quyền truy cập vào các trường được hiển thị trong view hiện tại của bản ghi, nhưng không phải là tất cả các trường của model. Điều này do các phương thức onchange có thể được gọi khi bản ghi đang được tạo trong giao diện người dùng trước khi nó được lưu trữ trong cơ sở dữ liệu. Bên trong phương thức onchange, self ở trạng thái đặc biệt, self.id không phải là một số nguyên mà là một instance của odoo.models.NewId. Do đó, bạn không được thực hiện bất kỳ thay đổi nào đối với cơ sở dữ liệu theo phương thức onchange vì cuối cùng người dùng có thể hủy việc tạo, sửa bản ghi. Điều này sẽ không khôi phục bất kỳ thay đổi nào được thực hiện bởi phương pháp onchange trong quá trình chỉnh sửa.

Ngoài ra, phương thức onchange có thể trả về một dict Python. Dict này có thể có các khóa sau:

  • warning: Giá trị phải là một dict khác với các khóa titlemessage chứa tiêu đề và nội dung của hộp thoại cảnh báo, nó sẽ được hiển thị khi phương thức onchange được chạy. Điều này rất hữu ích để thu hút sự chú ý của người dùng đến các điểm không nhất quán hoặc các vấn đề tiềm ẩn.

    @api.onchange('class_id')
    def _onchange_class_id(self):
        if self.class_id:
            self.class_group_id = self.class_id.class_group_id
            return {'warning': {
                'title': _('Warning'),
                'message': _("Please check the information again!")}}
    
  • domain: Giá trị phải là một dict với các cặp khoá và giá trị là tên trường và miền lọc tương ứng. Điều này rất hữu ích khi bạn muốn thay đổi miền lọc của một trường One2many tuỳ thuộc vào giá trị của trường khác.

    @api.onchange('class_id')
    def _onchange_class_id(self):
        if self.class_id:
            self.class_group_id = self.class_id.class_group_id
            return {'domain': {'class_group_id': [('id', '=', self.class_id.class_group_id.id)]}}
    

Gọi phương thức onchange ở server-side

Phương thức onchange có một hạn chế: Nó sẽ không được gọi khi bạn đang thực hiện các thao tác ở sever-side. onchange chỉ được gọi tự động khi các trường phụ thuộc được thay đổi thông qua giao diện người dùng. Tuy nhiên trong một số trường hợp các phương thức onchange này phải được gọi vì chúng cập nhật các trường quan trọng trong bản ghi đã tạo hoặc cập nhật. Tất nhiên bạn có thể tự mình thực hiện các phép tính cần thiết, nhưng điều này không phải lúc nào cũng có thể thực hiện được vì phương thức onchange có thể được thêm vào hoặc sửa đổi bởi một module khác mà bạn không biết.

Mục này sẽ hướng dẫn cách để gọi thủ công các phương thức onchange trước khi tạo bản ghi.

Chuẩn bị

Ở phần định nghĩa các phương thức onchange chúng ta đã sử dụng hàm onchange để điền thông tin Nhóm lớp học khi thay đổi Lớp trên giao diện học sinh. Phần này chúng ta sẽ sử dụng model education.application để làm ví dụ. Ví dụ khi xác nhận phiếu Đăng ký nhập học, sẽ tạo ra một học sinh mới, nếu chúng ta điền Lớp cho học sinh thì thông tin Nhóm lớp học sẽ được tự điền cho học sinh mới.

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

  1. Kế thừa model education.admission sửa code vào hàm action_confirm:

    from odoo import models
    from odoo.tests.common import Form
    
    class EducationApplication(models.Model):
        _inherit = 'education.application'
    
    def action_confirm(self):
        for r in self:
            class_12a1 = self.env.ref('viin_education.demo_education_class_12a1')
            with Form(self.env['education.student']) as student_form:
                student_form.name = r.name
                student_form.class_id = class_12a1
                student = student_form.save()
                r.student_id = student.id
        return super(EducationAdmission, self).action_confirm()
    
  2. Vào form Đăng ký nhập học bấm xác nhận. Ta sẽ thấy một học sinh mới được tạo, thông tin Nhóm lớp học đã được điền tự động:

    Kỹ thuật phát triển server-side sử dụng onchange

Cơ chế hoạt động

Trong hàm action_confirm chúng ta tạo một Form học sinh ảo để xử lý các sự kiện onchange. Khi ta gán dữ liệu trên Form ảo này thì sẽ kích hoạt các hàm onchange liên quan đến trường đó. Như trên ví dụ, ta gán dữ liệu Lớp cho Form ảo, hàm _onchange_class_id của model education.student sẽ được kích hoạt, điền dữ liệu Nhóm lớp học cho Form ảo. Sau đó, chúng ta gọi phương thức save() của Form, phương thức này trả về một bản ghi mới của học sinh.

Phương thức onchange chủ yếu được gọi từ giao diện người dùng. Nhưng trong phần này chúng ta đã biết cách kích hoạt onchange trên server-side. Bằng cách này chúng ta có thể tạo ra bản ghi mà không bỏ qua bất kỳ logic nghiệp vụ nào.

Xem thêm

Tham khảo Basic Server-Side Development để tìm hiểu thêm về cách tạo và cập nhật bản ghi.

Định nghĩa onchange bằng phương thức compute

Chúng ta đã thấy hạn chế của onchange là chỉ được gọi tự động từ giao diện người dùng. Để khắc phục hạn chế này, Odoo 13 đã giới thiệu một cách mới để định nghĩa hành vi onchange. Trong phần này chúng ta sẽ sử dụng phương thức compute để tạo ra hoạt động giống như onchange.

Chuẩn bị

Ở ví dụ trong phần định nghĩa các phương thức onchange chúng ta sử dụng hàm _onchange_class_id để điền dữ liệu cho trường Nhóm lớp học khi điền dữ liệu Lớp. Phần này chúng ta sẽ sử dụng compute để điền dữ liệu cho Nhóm lớp học tương tự như ví dụ trên.

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

  1. Trên model education.student khai báo lại trường class_group_id:

    from odoo import fields, models
    
    class EducationStudent(models.Model)
        _inherit = 'education.student'
    
        class_group_id = fields.Many2one('education.class.group', string='Class Group', ondelete='restrict', compute='_compute_class_group', readonly=False)
    
        @api.depends('class_id')
        def _compute_class_group(self):
            for r in self:
                if r.class_id and r.class_id.class_group_id:
                    r.class_group_id = r.class_id.class_group_id.id
                else:
                    r.class_group_id = False
    

Cơ chế hoạt động

Về mặt chức năng, hàm _compute_class_group hoạt động giống như phương thức onchange thông thường. Hàm _compute_class_group cũng được kích hoạt khi có sự thay đổi của các trường trong depends.

Trên ví dụ chúng ta khai báo lại trường class_group_id có gắn thuộc tính compute và sử dụng readonly=False. Theo mặc định thì các trường tính toán chỉ có thể đọc và không được lưu trữ trong cơ sở dữ liệu, nhưng bằng cách đặt readonly=False thì trường đó có thể chỉnh sửa và sẽ được lưu trữ.

Xem thêm

Tham khảo thêm Application Models để tìm hiểu thêm về các trường tính toán.

Định nghĩa một model dựa trên SQL view

Khi thiết kế một module, mô hình dữ liệu được lập trong các lớp sau đó được ánh xạ tới các bảng cơ sở dữ liệu bằng ORM. Chúng ta áp dụng một số nguyên tắc thiết kế, chẳng hạn như separation of concerns (phân tách phụ thuộc) và data normalization (chuẩn hóa dữ liệu). Tuy nhiên ở các giai đoạn sau của thiết kế module, cần có chỗ tổng hợp dữ liệu từ một số model khác nhau vào một bảng duy nhất, đặc biệt là để làm báo cáo. Để làm điều này, có thể định nghĩa một model chỉ đọc được hỗ trợ bởi PostgreSQL view, thay vì một bảng.

Chuẩn bị

Phần này chúng ta sử dụng module viin_education_admission để tạo báo cáo Chương trình tuyển sinh.

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

  1. Tạo model mới education.admission.report:

    from odoo import fields, models
    from odoo import tools
    
    class EducationAdmission(models.Model):
        _name = 'education.admission.report'
        _description = 'Education Admission Report'
        _auto = False
    
        name = fields.Char(string='Name', readonly=True)
        start_date = fields.Date(string='Start Date', readonly=True)
        end_date = fields.Date(string='End date', readonly=True)
        school_year_id = fields.Many2one('education.school.year', string='School Year', readonly=True)
        state = fields.Selection(string='State', selection=[
            ('draft', 'New'),
            ('confirmed', 'Confirmed'),
            ('inprogress', 'In Progress'),
            ('done', 'Done'),
            ('cancelled', 'Cancelled')], readonly=True)
    
  2. Viết query để lấy dữ liệu cho education.admission.report:

    def _query_education_admission_report(self):
        _query = """
            SELECT ead.id AS id,
                   ead.name AS name,
                   ead.start_date AS start_date,
                   ead.school_year_id AS school_year_id,
                   ead.end_date AS end_date,
                   ead.state AS state
    
            FROM education_application ea
                LEFT JOIN education_admission ead ON ea.admission_id = ead.id
                LEFT JOIN education_school_year esy ON ead.school_year_id = esy.id
    
            WHERE ea.active = True
    
        """
        return _query
    
    def init(self):
        tools.drop_view_if_exists(self.env.cr, self._table)
        self.env.cr.execute("""CREATE or REPLACE VIEW %s as (%s)""" % (self._table, self._query_education_admission_report()))
    
  3. Tạo view cho education.admission.report:

    <record id="education_admission_report_pivot_view" model="ir.ui.view">
         <field name="name">education.admission.report.pivot</field>
         <field name="model">education.admission.report</field>
         <field name="arch" type="xml">
             <pivot string="Education Admission Analysis" disable_linking="True" sample="1">
                 <field name="name" type="row"/>
             </pivot>
         </field>
    </record>
    
    <record id="education_admission_report_search" model="ir.ui.view">
        <field name="name">education.admission.report.search</field>
        <field name="model">education.admission.report</field>
        <field name="arch" type="xml">
            <search string="Education Admission Analysis">
                <field name="name"/>
                <group expand="1" string="Group By">
                    <filter string="Status" name="grp_state" context="{'group_by':'state'}"/>
                </group>
            </search>
        </field>
    </record>
    
  4. Vào Menu Báo cáo chương trình tuyển sinh để xem báo cáo:

    Sử dụng SQL view tạo báo cáo

Cơ chế hoạt động

Thông thường, hệ thống sẽ tạo một bảng mới cho model mà bạn định nghĩa bằng cách sử dụng các trường cho các cột trong bảng. Điều này được thực hiện là do trong class BaseModel có thuộc tính _auto mặc định là True. Trong bước 1 khi khai báo model chúng ta đặt _auto=False để không tự động tạo ra bảng, bảng sẽ do chúng ta tự quản lý. Chúng ta khai báo các trường trong bảng và đặt readonly=True để không thể sửa đổi trên các giao diện, và dữ liệu cũng sẽ không được lưu, vì các chế độ PostgreSQL view là chế độ chỉ đọc.

Chúng ta định nghĩa phương thức init(). Phương thức này thường không làm gì cả, nó được gọi sau auto_init() (chịu trách nhiệm tạo bảng khi _auto=True nhưng không làm gì khác). Và chúng ta sử dụng nó để tạo một dạng SQL view mới (hoặc để cập nhật view hiện có trong trường hợp nâng cấp module). Truy vấn tạo view phải có tên cột khớp với tên trường được khai báo trong model.

Tip

Một sai lầm phổ biến, trong trường hợp bạn quên đổi tên các cột trong SQL view sẽ gây ra thông báo lỗi khi hệ thống không tìm thấy cột. Như ở query trong ví dụ chúng ta đã đổi tên cột giống trường đã khai báo.

Lưu ý rằng chúng ta cũng cần cung cấp một cột integer đặt tên là id chứa các giá trị duy nhất.

Mở rộng

Cũng có thể có một số trường được compute hoặc related trên các model như vậy. Hạn chế duy nhất là các trường không thể được lưu trữ (và do đó không thể sử dụng chúng để nhóm các bản ghi hoặc để tìm kiếm). Nếu cần nhóm hoặc tìm kiếm, bạn hãy thêm trường đó vào view thay vì sử dụng related.

Xem thêm

  • Để tìm hiểu thêm về UI view cho các hành động của người dùng, hãy tham khảo Backend Views.

  • Để hiểu rõ hơn về các quy tắc ghi và kiểm soát truy cập, tham khảo Security Access.

Thêm các tùy chọn trong Thiết lập

Bạn có thể cung cấp các tính năng tùy chọn thông qua menu Thiết lập. Người dùng có thể bật hoặc tắt tùy chọn này bất cứ lúc nào. Trong phần này, chúng ta sẽ tìm hiểu cách tạo các tùy chọn trong Thiết lập.

Chuẩn bị

Phần này chúng ta sẽ sử dụng module viin_education để thực hiện thêm các tùy chọn trong Thiết lập. Chúng ta sẽ thêm nút tích Năm cuối cấp trên Thiết lập, khi tích vào nút này và lưu lại sẽ tự động cài thêm module viin_education_final_year, cho phép đánh dấu học sinh là học sinh cuối cấp.

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

  1. Kế thừa model res.config.settings bổ sung trường Năm cuối cấp:

    from odoo import fields, models
    
    class ResConfigSettings(models.TransientModel):
        _inherit = 'res.config.settings'
    
        module_viin_education_final_year = fields.Boolean(string='Education Final Year')
    
  2. Đưa trường Năm cuối cấp lên view:

    <record id="res_config_settings_view_form" model="ir.ui.view">
        <field name="name">res.config.settings.view.form.inherit.account</field>
        <field name="model">res.config.settings</field>
        <field name="priority" eval="40"/>
        <field name="inherit_id" ref="base.res_config_settings_view_form"/>
        <field name="arch" type="xml">
            <xpath expr="//div[hasclass('settings')]" position="inside">
                <div class="app_settings_block" data-string="Education" string="Education" data-key="viin_education" groups="viin_education.viin_education_group_admin">
                    <h2 id="education_feature">Extra Features</h2>
                    <div class="row mt16 o_settings_container" id="education_feature_col2">
                        <div class="col-12 col-lg-6 o_setting_box" id="education_admission" >
                            <div class="o_setting_left_pane">
                                <field name="module_viin_education_final_year"/>
                            </div>
                            <div class="o_setting_right_pane">
                                <label for="module_viin_education_final_year"/>
                                <div class="text-muted">
                                    Manage education final year
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </xpath>
        </field>
    </record>
    
  3. Thêm menu và action để có thế truy cập Thiết lập từ giao diện Giáo Dục:

    <record id="action_education_config" model="ir.actions.act_window">
        <field name="name">Settings</field>
        <field name="type">ir.actions.act_window</field>
        <field name="res_model">res.config.settings</field>
        <field name="view_id" ref="res_config_settings_view_form"/>
        <field name="view_mode">form</field>
        <field name="target">inline</field>
        <field name="context">{'module' : 'viin_education', 'bin_size': False}</field>
    </record>
    
    <menuitem id="menu_education_config" name="Settings" parent="education_configuration_menu_root"
        sequence="0" action="action_education_config" groups="base.group_system"/>
    
  4. Vào giao diện Thiết lập kiểm tra thông tin:

    Tùy biến thiết lập trong Kỹ thuật phát triển server-side

Cơ chế hoạt động

Model res.config.settings là một TransientModel, ở bước 1 chúng ta đã thêm trường Năm cuối cấp bằng cách kế thừa nó. Nút tích này chỉ có quyền viin_education_group_admin mới được nhìn thấy. Chúng ta đặt tên trường là module_viin_education_final_year, tiền tố module_ được đặt trước tên module cần cài (đây là tính năng của Framework). Khi chúng ta tích chọn hoặc bỏ tích chọn, hệ thống sẽ cài hoặc gỡ module viin_education_final_year.

Ở bước 2 chúng ta sử dụng xpath để đưa trường Năm cuối cấp lên giao diện người dùng. Trong form view chúng ta sẽ thấy giá trị data-key là tên module của chúng ta, điều này chỉ cần thiết khi ta thêm một tab mới trong Thiết lập. Cụ thể là ta thêm tab Giáo dục trong Thiết lập.

Mở rộng

Một cách khác để quản lý Thiết lập là sử dụng các Thông số hệ thống. Dữ liệu thông số hệ thống được lưu trữ trong model ir.config_parameter. Đây là cách bạn tạo các tham số toàn cục trên toàn hệ thống:

digest_emails = fields.Boolean(string='Digest Emails', config_parameter='digest.default_digest_emails')

Thuộc tính config_parameter trong các trường sẽ bảo đảm dữ liệu người dùng được lưu trữ trong Thiết lập >> Kỹ thuật >> Thông số >> Thông số hệ thống. Dữ liệu sẽ được lưu trữ với khóa digest.default_digest_emails.

Các tùy chọn Thiết lập được sử dụng để ứng dụng của chúng ta dễ sử dụng hơn. Người dùng có thể tuỳ ý bật, tắt các tính năng một cách nhanh chóng.

Thực thi init hooks

Trong phần Managing Module Data, bạn đã biết cách thêm, cập nhật và xóa bản ghi khỏi file XML hoặc CSV. Tuy nhiên, đôi khi gặp những nghiệp vụ rất phức tạp và không thể giải quyết bằng file dữ liệu. Trong những trường hợp như vậy, chúng ta sử dụng init hook từ file __manifest__.py để thực hiện các thao tác bạn muốn.

Chuẩn bị

Phần này chúng ta sử dụng module viin_education để thực hiện. Chúng ta sẽ tạo một số dữ liệu về Trường học thông qua post_init_hook.

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

  1. Trong file __manifest__.py khai báo hook bằng khóa post_init_hook:

    'post_init_hook': 'add_school_hook',
    
  2. Trong file __init__.py thêm phương thức add_school_hook():

    from odoo import api, fields, SUPERUSER_ID
    
    def add_school_hook(cr, registry):
        env = api.Environment(cr, SUPERUSER_ID, {})
        data = [{'name': 'Haiphong University', 'code': 'THP'},
                {'name': 'Hanoi University', 'code': 'HANU'}]
        env['education.school'].create(data)
    

Cơ chế hoạt động

Ở bước 1 chúng ta đã khai báo post_init_hook trong __manifest__.py với giá trị là add_school_hook. Điều này có nghĩa là sau khi cài đặt module, hệ thống sẽ tìm kiếm phương thức add_school_hook trong __init__.py. Nếu phương thức được tìm thấy, nó sẽ gọi phương thức với crregistry.

Ở bước 2 chúng ta đã khai báo phương thức add_school_hook(), phương thức này được gọi sau khi module được cài đặt. Chúng ta đã tạo 2 bản ghi Trường học từ phương thức này. Trong các tình huống thực tế, bạn có thể viết logic nghiệp vụ phức tạp tại đây.

Trong ví dụ chúng ta đã biết về post_init_hook, nhưng hệ thống còn hỗ trợ thêm hai hook sau:

  • pre_init_hook: Hook này sẽ được gọi khi bạn bắt đầu cài đặt một module. Nó ngược lại với post_init_hook. Nó sẽ được gọi trước khi cài đặt module hiện tại.

  • uninstall_hook: Hook này sẽ được gọi khi bạn gỡ cài đặt module.