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ứconchange
. 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 moduleviin_education
thì phải thuộc nhóm quyềnviin_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ềnviin_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:
Kế thừa mở rộng model
education.student
:class EducationStudent(models.Model): _inherit = 'education.student'Thêm một phương thức
update_mobile_number()
:def update_mobile_number(self, new_number):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()Sửa đổi người dùng của environment:
self = self.sudo()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ớisudo()
, environment sẽ sửa đổi thuộc tính của environment thànhsu
với trạng thái superuser. Bạn có thể truy cập trạng thái của nó quarecordset.env.su
. Tất cả các phương thức gọi quarecordset 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òngself = self.sudo()
trong code rồi gọi đến phương thứcupdate_mobile_number
từ người dùng không thuộc nhóm quyềnviin_education_group_user
. Sẽ có cảnh báo lỗiAccess Error
trả về. Chỉ cần sử dụngsudo()
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ụngsudo()
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ứcsearch()
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:
Nếu bạn muốn tìm hiểu về
environment
hãy tham khảo Basic Server-Side Development.Để biết thêm thông tin về quản lý quyền truy cập hãy tham khảo Security Access.
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¶
Trong model
education.application
sửaaction_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'})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 = FalseVà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óatest_archive
thì sẽ chạy dòng codeself.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:
Cách lấy recordset rỗng trong model ở Basic Server-Side Development.
Cách truyền tham số cho form, action ở Backend Views.
Tìm kiếm bản ghi ở Basic Server-Side Development.
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ứcsearch
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.
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 khisearch()
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ườngphone_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.
Kế thừa model
education.student
bổ sung trườngphone_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)Đư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ấyid
vàphone
của bảngeducation_student
. Do trườngphone
được khai báo trên bảngres_partner
, chúng ta phảiJOIN
đến bảngres_partner
để lấy giá trị trườngphone
. Chúng ta sử dụngregexp_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ữ trongself.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óaid
,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ủaphone_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ấnUPDATE
. Để xóa bộ nhớ cache bạn có thể sử dụngself.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ớicr
:
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
,UNIQUE
vàFOREIGN 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ứccreate()
và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¶
Để quản lý quyền truy cập hãy tham khảo Security Access.
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 đó.
Các bước thực hiện¶
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]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>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'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>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ảTransientModel
vàModel
đề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ênTransientModel
mà tham chiếu đến model bình thường. Việc này sẽ thêm một cột trêncomodel
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ườngOne2many
nếucomodel
trongOne2many
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 đếneducation_student_dropout_wizard_action
. Trongeducation_student_dropout_wizard_action
chúng ta có thiết lậptarget=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 tinstudent_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ấmXác nhận
, hàmaction_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à modeleducation.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ứcaction_confirm
, chúng ta cần truyền các khóaactive_model/active_id/active_ids/active_domain
vào context để sử dụng. Tham khảo cách truyền context ở Gọ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à modeleducation.student
vàactive_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ứcaction_confirm
của modeleducation.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ủair.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 resultKĩ 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¶
Tham khảo Backend Views để hiểu thêm về cách tạo view, action cho wizard.
Tham khảo Basic Server-Side Development để xem cách kết hợp các recordset.
Đị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¶
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
- Điền thông tin Nhóm lớp học cho Lớp. Truy cập menu Cấu hình/Lớp:
Trên giao diện học sinh, chúng ta chọn thông tin Lớp cho học sinh:
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ườngclass_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ứconchange
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ứconchange
,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ủaodoo.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ứconchange
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áponchange
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óatitle
vàmessage
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ứconchange
đượ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ườngOne2many
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ứconchange
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ứconchange
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àmonchange
để đ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 modeleducation.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¶
Kế thừa model
education.admission
sửa code vào hàmaction_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()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:
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ệnonchange
. Khi ta gán dữ liệu trên Form ảo này thì sẽ kích hoạt các hàmonchange
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 modeleducation.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ứcsave()
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ạtonchange
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 vionchange
. Trong phần này chúng ta sẽ sử dụng phương thứccompute
để 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ụngcompute
để đ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¶
Trên model
education.student
khai báo lại trườngclass_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ứconchange
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ínhcompute
và sử dụngreadonly=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 đặtreadonly=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¶
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)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()))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>Vào Menu Báo cáo chương trình tuyển sinh để xem 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à đặtreadonly=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 sauauto_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ặcrelated
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ụngrelated
.
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 moduleviin_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¶
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')Đư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>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"/>Vào giao diện Thiết lập kiểm tra thông tin:
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ềnviin_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ỡ moduleviin_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óadigest.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 quapost_init_hook
.
Các bước thực hiện¶
Trong file
__manifest__.py
khai báo hook bằng khóapost_init_hook
:'post_init_hook': 'add_school_hook',Trong file
__init__.py
thêm phương thứcadd_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ứcadd_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ớicr
vàregistry
.Ở 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 haihook
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ớipost_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.