Trong một form Odoo, có ô bạn gõ vào chỉ ra chữ trơn, có ô lại hiện nguyên một thanh công cụ soạn thảo — đậm, nghiêng, gạch đầu dòng, chèn link, chèn ảnh. Cùng là "một ô nhập văn bản", vì sao khác nhau đến vậy? Câu trả lời không nằm ở view mà nằm ở kiểu field trong model. Ô có định dạng là fields.Html; ô chữ trơn là fields.Text hoặc fields.Char. Bài này mổ xẻ fields.Html trên Odoo 19: nó lưu gì, sanitize làm gì, và widget='html' cùng các options tùy biến trình soạn thảo ra sao — tất cả qua field ghi_chu_html thật của module demo quan_ca_phe.

Ba kiểu field, ba cách lưu văn bản

Odoo có ba field cho văn bản, khác nhau ở cái được lưu vào cột:

  • fields.Char — một dòng, chuỗi thô. Cột varchar. Dùng cho tên, mã, tiêu đề ngắn.
  • fields.Text — nhiều dòng, vẫn là chuỗi thô. Cột text. Xuống dòng được, nhưng không có định dạng: gõ **đậm** thì lưu đúng bốn ký tự sao và hai chữ, không in đậm gì cả.
  • fields.Html — lưu HTML có cấu trúc: <h3>, <b>, <ul><li>, <a href>... Cột cũng là text, nhưng nội dung là markup, và Odoo hiểu nó là HTML nên render ra định dạng.

Trong quan_ca_phe, model quan.the.thanh.vien có sẵn một field Html để ghi chú khách:

Ảnh chụp đoạn mã Python và XML nền tối. Phần models khai ghi_chu_html bằng fields.Html tên Ghi chú với sanitize bằng True kèm chú thích Html tự làm sạch chống XSS và so sánh với Text chuỗi thô nhiều dòng, Char một dòng. Phần view có ba cách khai field ghi_chu_html: không khai widget thì mặc định dùng html_editor đầy đủ, khai widget html kèm options style-inline true cho email, và khai widget html kèm options height 200 để thu gọn. Dòng cuối ghi Html field lưu thẻ h3 b ul còn Text Char chỉ lưu chuỗi thô

Hình 1: Khai báo fields.Html trong model và ba cách dùng nó ở view. Điểm mấu chốt: field Html mặc định đã nhúng trình soạn thảo html_editor — không cần khai widget gì cũng có đủ thanh công cụ. Khai widget="html" tường minh chỉ cần khi muốn truyền options để tùy biến (nội tuyến CSS cho email, hoặc giới hạn chiều cao/số nút).

Định nghĩa thật trong quan_ca_phe/models/the_thanh_vien.py gọn đúng một dòng:

ghi_chu_html = fields.Html('Ghi chú', sanitize=True)

Và trong form view, nó nằm ở một trang notebook, khai trần không kèm widget:

<page string="Ghi chú">
    <field name="ghi_chu_html"/>
</page>

sanitize: hàng rào chống XSS ngay tại field

Vì field Html lưu markup do người dùng nhập, nó là một cửa ngõ XSS kinh điển: ai đó dán <script>...</script> hay một thuộc tính onerror= vào ghi chú, lần sau form mở ra là mã chạy trong trình duyệt người khác. sanitize=True (mặc định của fields.Html) bật một bộ lọc ngay lúc ghi: Odoo bóc các thẻ và thuộc tính nguy hiểm trước khi lưu vào CSDL. Nó không phải lớp trang trí — nó là biện pháp an toàn ở tầng ORM, chạy cho mọi lối ghi kể cả ghi qua RPC hay import, không chỉ khi gõ qua giao diện.

Vài tham số con của sanitize cho bạn siết thêm:

# Chỉ giữ định dạng cơ bản, bỏ style/class/id
mo_ta = fields.Html('Mô tả', sanitize=True,
                    sanitize_attributes=True,
                    sanitize_style=True,
                    strip_style=True)

Ngược lại, một số field nội bộ mà bạn chủ động muốn giữ nguyên HTML thô (ví dụ template email do lập trình viên soạn) mới đặt sanitize=False — và khi đó bạn tự chịu trách nhiệm về nội dung. Với dữ liệu người dùng nhập, luôn để sanitize=True.

Trình soạn thảo thật: html_editor của Odoo 19

Mở form một thẻ thành viên, sang tab "Ghi chú", field ghi_chu_html không hiện một ô <textarea> mà hiện nguyên một vùng soạn thảo có định dạng. Đây là ảnh thật, sau khi tôi đặt cho thẻ VIP-0003 một ghi chú có tiêu đề, chữ đậm, chữ nghiêng và danh sách gạch đầu dòng:

Ảnh chụp form Odoo 19 tab Ghi chú đang mở. Trong vùng soạn thảo hiện tiêu đề lớn Ghi chú khách VIP, một đoạn văn Khách thân thiết in đậm phẩy thích cà phê sữa đá in nghiêng phẩy Ưu đãi hai chấm, và một danh sách gạch đầu dòng gồm Giảm 10 phần trăm mỗi ly và Tặng bánh sinh nhật. Phía trên là các tab Lịch sử điểm, Ghi chú đang chọn, Ưu đãi, Bố cục demo, Separator demo

Hình 2: field ghi_chu_html render bằng html_editor của Odoo 19. Nội dung không phải chữ trơn: tiêu đề (<h3>), đậm (<b>), nghiêng (<i>) và danh sách (<ul><li>) đều là HTML thật lưu trong cột. Cũng nội dung này, nếu để trong field Text, sẽ hiện ra nguyên các thẻ <h3>, <b>... dưới dạng chữ.

Odoo 19 đã viết lại bộ soạn thảo thành html_editor (thay cho wysiwyg cũ dựa trên summernote). Thanh công cụ không cố định ở trên đầu mà là một thanh nổi, xuất hiện ngay cạnh chỗ bạn bôi đen. Bôi đen chữ "thân thiết" (đang in đậm), thanh nổi hiện lên:

Ảnh chụp thanh công cụ nổi của trình soạn thảo Odoo 19. Từ trái sang phải có ô chọn kiểu ghi Đoạn văn, ô cỡ chữ ghi số 14, nút B in đậm đang được tô sáng, nút I in nghiêng, nút U gạch chân, nút A đổi màu chữ, biểu tượng chèn liên kết, và một nút ba chấm dọc để mở thêm lựa chọn

Hình 3: Thanh công cụ nổi của html_editor. Nút B đang được tô sáng vì đoạn đang chọn in đậm — thanh phản ánh đúng định dạng tại con trỏ. Có kiểu đoạn ("Đoạn văn" — heading/paragraph), cỡ chữ (14), đậm/nghiêng/gạch chân, đổi màu, chèn link và nút "ba chấm" mở thêm. Đây là bằng chứng field Html không chỉ lưu HTML mà còn cho người dùng tạo HTML một cách trực quan.

Tùy biến bằng options

Khai widget="html" tường minh mở đường cho options. Vài cái hay dùng:

<!-- Nội tuyến CSS: cần khi HTML sẽ đi vào email -->
<field name="ghi_chu_html" widget="html" options="{'style-inline': true}"/>

<!-- Giới hạn chiều cao vùng soạn thảo -->
<field name="ghi_chu_html" widget="html" options="{'height': 200}"/>

<!-- Cho phép chèn 'snippet'/khối dựng sẵn như trình dựng web -->
<field name="ghi_chu_html" widget="html" options="{'snippets': 'web_editor.snippets'}"/>

style-inline đáng chú ý: email không đọc được <style> hay class ngoài, mọi định dạng phải nằm ngay trong thuộc tính style của từng thẻ. Bật option này, Odoo tự "ép" CSS vào nội tuyến khi lưu — đó chính là lý do field body của mẫu email trong Odoo dùng nó.

Đọc field Html ra trang web: t-field

HTML đã lưu, muốn hiện ra trang công khai (website/portal) thì dùng t-field trong QWeb — nó xuất thẳng HTML, không escape:

<div t-field="the.ghi_chu_html"/>

Khác hẳn t-esc/t-out với chuỗi thô (sẽ escape < thành &lt;). Vì t-field tin nội dung là HTML hợp lệ, đây lại là một lý do nữa để sanitize=True đứng gác từ đầu: cái gì đã qua cửa sanitize lúc ghi thì lúc phun ra t-field mới yên tâm.

Ba ý mang về

  1. fields.Html lưu HTML có cấu trúc (<h3>, <b>, <ul>...), khác Text/Char chỉ lưu chuỗi thô — và nó mặc định nhúng trình soạn thảo html_editor, không cần khai widget cũng có đủ thanh công cụ.
  2. sanitize=True là hàng rào XSS ở tầng ORM, chạy lúc ghi cho mọi lối vào; giữ mặc định này với mọi dữ liệu người dùng nhập, chỉ tắt cho HTML nội bộ do lập trình viên kiểm soát.
  3. widget="html" + options để tùy biến: style-inline khi HTML đi vào email, height để giới hạn khung, snippets để mở khối dựng sẵn. Đọc ra web bằng t-field (xuất thẳng HTML), không t-esc.

Odoo 19 đổi mạnh phần front-end, và phần dịch thuật cũng không ngoại lệ. Phần sau mổ xẻ dịch chuỗi trong JavaScript với hàm _t — nó lấy bản dịch từ đâu, chạy lúc nào, và vì sao gọi _t sai chỗ thì chuỗi không bao giờ được dịch.