Gửi cùng một email cho trăm khách, nhưng mỗi thư phải xưng đúng tên và ghi đúng số điểm của người đó. Bạn không viết trăm email — bạn viết một mẫu có chỗ trống, để Odoo điền phần động cho từng bản ghi. Đó là mail.template. Bài này khai một mẫu bằng XML trong module, rồi render nó cho một thẻ thật để thấy các chỗ trống được điền — chưa gửi đi đâu cả (việc gửi để phần sau).
Hai loại chỗ trống: {{ }} và t-out
Điểm hay nhầm nhất của mail.template Odoo 19: nó dùng hai cơ chế điền cho hai nhóm trường khác nhau.

Hình 1: Cùng một mẫu, hai cú pháp. subject, email_to, email_from, lang dùng {{ ... }} — inline template kiểu Jinja, hợp cho một dòng ngắn: {{ object.name }}, {{ object.partner_id.email }}. Còn body_html dùng QWeb với t-out — hợp cho HTML nhiều dòng, gọi được format_date, biểu thức Python. object là bản ghi đang render; user là người gửi.
Nhớ quy tắc gọn: một dòng (tiêu đề, địa chỉ) → {{ }}; cả khối HTML (thân thư) → t-out. Trộn ngược (dùng {{ }} trong body_html) vẫn có thể chạy ở vài chỗ nhưng không phải cách chuẩn của Odoo 19 và dễ vỡ.
Render thật để xem, chưa gửi
Muốn biết mẫu ra thế nào, không cần gửi email. Method _render_field điền mẫu cho một bản ghi và trả về chuỗi kết quả:

Hình 2: _render_field('subject', [1]) điền mẫu cho thẻ id 1 (VIP-0001). Tiêu đề ra 'Thẻ VIP-0001 của bạn - còn 150 điểm'; email_to ra đúng email của khách; body_html ra HTML đã điền tên và điểm. Tất cả từ dữ liệu thật của bản ghi, không có SMTP nào bị đụng tới — _render_field chỉ dựng nội dung, hoàn hảo để kiểm mẫu trước khi gửi.
Và đây là thân thư đã render, đặt trong một khung email cho dễ hình dung:

Hình 3: Thân thư body_html sau khi render cho VIP-0001 — chính HTML thật Odoo sinh ra, chỉ được bọc thêm khung để xem. Mọi chỗ trống đã điền: tên khách Nguyễn Văn An, mã thẻ VIP-0001, hạng Bạc, 150 điểm, ngày cấp 09/03/2026. Một mẫu, mỗi thẻ ra một nội dung riêng.
Vài lưu ý khi khai
objectlà bản ghi,userlà người gửi. Trong mọi biểu thức,objecttrỏ tới bản ghi đang render (object.partner_id.name...). Đừng tham chiếu field không tồn tại: model thẻ này không cócompany_id, nên{{ object.company_id.email }}sẽ lỗi khi render — tôi đã vấp và đổi sang{{ user.email }}.body_htmlphải khaitype="html"để Odoo hiểu là HTML/QWeb, không phải văn bản thô.format_date,format_datetimecó sẵn trong ngữ cảnh QWeb của body_html — định dạng ngày theo ngôn ngữ người nhận thay vì in kiểu ISO.auto_delete=Truexoá bản ghimail.mailsau khi gửi xong (đỡ phình bảng); đặtFalsenếu muốn giữ lịch sử thư đã gửi.- Bọc
<data noupdate="1">như các bản ghi cấu hình khác — để nâng cấp module không ghi đè phần người vận hành đã chỉnh nội dung thư.
Ba ý mang về
mail.templatelà email có chỗ trống điền động:subject/email_to/email_from/langdùng{{ }}(inline một dòng);body_htmldùng QWebt-out(khối HTML, gọi đượcformat_datevà biểu thức)._render_field(field, [res_id])điền mẫu mà không gửi — dùng để kiểm nội dung trước;objectlà bản ghi,userlà người gửi.- Cẩn thận field không tồn tại (
company_idở model không có), khaitype="html"cho body, và bọc<data noupdate="1">để giữ chỉnh tay.
Mẫu đã dựng và render đúng. Giờ tới lúc thật sự bấm gửi — tạo mail.mail, đẩy vào hàng đợi, và những gì xảy ra khi chưa cấu hình SMTP. Phần sau đi vào gửi email từ code với send_mail.