Bạn gọi template.send_mail(...), không có lỗi gì, nhưng khách bảo chưa nhận được thư. Không phải bug — mặc định send_mail không gửi ngay. Nó điền mẫu, tạo một bản ghi mail.mail ở trạng thái chờ, rồi để một cron gửi theo lô sau đó. Hiểu đúng đường đi này giúp bạn không ngồi debug SMTP khi vấn đề chỉ là "thư đang trong hàng đợi", và biết khi nào cần ép gửi ngay.
Ba cách gửi từ code
Odoo có vài lối gửi email tuỳ ngữ cảnh:

Hình 1: Ba lối gửi. template.send_mail(res_id) — điền mẫu rồi tạo mail.mail; đây là cách phổ biến nhất khi đã có mail.template. record.message_post(...) — ghi vào khung trao đổi (chatter) của bản ghi và gửi email cho partner_ids; cần model kế thừa mail.thread (thẻ có). mail.mail.create(...).send() — tự dựng một thư thô rồi gửi, khi không muốn qua template.
Đo: send_mail đưa vào hàng đợi, chưa gửi
Gọi send_mail cho thẻ VIP-0001 và soi ngay bản ghi thư nó tạo ra:

Hình 2: send_mail(1) tạo một mail.mail ở trạng thái outgoing — tức là đã dựng xong nhưng chưa gửi, đang chờ cron "Gửi email" quét hàng đợi. Chủ đề đã điền đúng, và người nhận nằm ở recipient_ids (partner an.nguyen@example.vn) chứ không phải trường email_to thô — vì Odoo khớp email với một res.partner. Khi tôi ép force_send=True, nó thử gửi ngay và ra exception với lỗi SMTP 535 (máy chủ này chưa cấu hình gửi thư). Đo xong tôi xoá các thư test để không phình bảng.
Hàng đợi vs gửi ngay
Đường đi của một email quyết định bởi force_send:

Hình 3: force_send=False (mặc định) dừng thư ở trạng thái outgoing; cron hệ thống "Gửi email" (một ir.cron có sẵn) quét hàng đợi và gửi theo lô. Ưu điểm: request của người dùng không bị chặn chờ SMTP, gửi hỏng thì cron thử lại. force_send=True gửi thẳng trong request — nhanh, nhưng nếu SMTP chậm/treo thì người dùng chờ theo, và lỗi nổ ngay tại đó (exception).
Quy tắc thực dụng: gần như luôn để force_send=False. Chỉ ép gửi ngay khi thực sự cần thư đến tức thì (ví dụ mã OTP, xác nhận đặt chỗ) và bạn chấp nhận rủi ro SMTP làm chậm thao tác. Việc gửi hàng loạt (nhắc hạn cả nghìn thẻ) thì phải qua hàng đợi, nếu không một lần gọi sẽ treo cả tiến trình.
Vài lưu ý
send_mailtrả về id củamail.mail, không phải kết quả gửi. Muốn biết đã gửi chưa, đọcstatecủa bản ghi đó (outgoing/sent/exception).message_postkhác gửi email thuần: nó ghi vào chatter của bản ghi (lưu vết trao đổi) và mới gửi email cho follower/partner_ids. Dùng khi muốn cả lưu vết và thông báo.- Chưa cấu hình SMTP thì mọi thứ dừng ở
outgoing/exception— thư vẫn được dựng đúng (đọcbody_htmlđể kiểm), chỉ là không ra khỏi hệ thống. Đừng nhầm "code chạy xong" với "khách đã nhận". - Dọn
mail.mailtest. Khi thử nghiệm, mỗi lần gọi tạo một bản ghi; nhớunlinkđể bảng không phình.auto_delete=Truetrên template giúp tự xoá sau khi gửi thành công.
Ba ý mang về
send_mail(res_id)KHÔNG gửi ngay — nó điền mẫu, tạomail.mailstateoutgoingchờ cron gửi theo lô. Trả về id thư; đọcstatemới biết đã gửi.force_send=Truegửi thẳng trong request (cần SMTP; lỗi →exception) — chỉ dùng khi cần tức thì, còn gửi hàng loạt phải qua hàng đợi.- Người nhận đi qua
recipient_ids(partner) chứ không phảiemail_tothô;message_postvừa ghi chatter vừa báo follower; nhớ dọnmail.mailtest.
Email là cách nhắc ra ngoài. Nhưng để nhắc nội bộ — giao việc "gọi khách gia hạn" cho một nhân viên với hạn chót — Odoo có công cụ khác. Phần sau dựng lên hoạt động (activity) từ code: giao việc, đặt hạn, và nó hiện ở đâu trong giao diện.