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ộtvarchar. Dùng cho tên, mã, tiêu đề ngắn.fields.Text— nhiều dòng, vẫn là chuỗi thô. Cộttext. 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:

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:

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:

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 <). 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ề
fields.Htmllưu HTML có cấu trúc (<h3>,<b>,<ul>...), khácText/Charchỉ lưu chuỗi thô — và nó mặc định nhúng trình soạn thảohtml_editor, không cần khaiwidgetcũng có đủ thanh công cụ.sanitize=Truelà 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.widget="html"+optionsđể tùy biến:style-inlinekhi HTML đi vào email,heightđể giới hạn khung,snippetsđể mở khối dựng sẵn. Đọc ra web bằngt-field(xuất thẳng HTML), khôngt-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.