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.

Ảnh chụp đoạn mã XML nền tối khai record mail.template id mail_the_gia_han, model_id ref model_quan_the_thanh_vien; nhóm subject email_to email_from lang dùng cú pháp ngoặc nhọn kép với object.name object.diem object.partner_id.email user.email object.partner_id.lang kèm chú thích inline template kiểu Jinja cho subject và email; field body_html type html dùng QWeb với thẻ t t-out object.partner_id.name hoặc quý khách, strong t-out object.name, strong t-out object.diem, t-out format_date object.ngay_cap kèm chú thích body_html là QWeb dùng t-out format_date biểu thức Python; dưới cùng chú thích bọc data noupdate 1 để giữ chỉnh tay auto_delete True xoá mail sau khi gửi

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ả:

Ảnh chụp phiên odoo shell nền tối, lấy template t bằng env ref mail_the_gia_han, gọi t _render_field subject cho bản ghi 1 trả về chuỗi Thẻ VIP-0001 của bạn còn 150 điểm với chú thích từ object name và object diem, gọi _render_field email_to trả về an.nguyen@example.vn với chú thích từ object partner_id email, gọi _render_field body_html cắt 80 ký tự đầu trả về đoạn có Chào Nguyễn Văn An và Thẻ VIP-0001 hạng Bạc 150 điểm với chú thích QWeb t-out điền object partner_id name hang_the diem chưa gửi chỉ render

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:

Ảnh xem trước một email trên nền xám, khung thư trắng bo góc, dòng tiêu đề đậm Thẻ VIP-0001 của bạn còn 150 điểm kèm nhãn nhỏ đã render từ template, dòng meta ghi Từ odoobot@example.com và Đến an.nguyen@example.vn, phần thân ghi Chào Nguyễn Văn An phẩy rồi Thẻ thành viên VIP-0001 in đậm hạng Bạc hiện có 150 in đậm điểm tích luỹ, dòng Ngày cấp 09 trên 03 trên 2026, và dòng màu nâu Cảm ơn bạn đã đồng hành cùng Cà Phê Việt

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

  • object là bản ghi, user là người gửi. Trong mọi biểu thức, object trỏ 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_html phải khai type="html" để Odoo hiểu là HTML/QWeb, không phải văn bản thô.
  • format_date, format_datetime có 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=True xoá bản ghi mail.mail sau khi gửi xong (đỡ phình bảng); đặt False nế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ề

  1. mail.template là email có chỗ trống điền động: subject/email_to/email_from/lang dùng {{ }} (inline một dòng); body_html dùng QWeb t-out (khối HTML, gọi được format_date và biểu thức).
  2. _render_field(field, [res_id]) điền mẫu mà không gửi — dùng để kiểm nội dung trước; object là bản ghi, user là người gửi.
  3. Cẩn thận field không tồn tại (company_id ở model không có), khai type="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.