Trên trang "Tài khoản của tôi", khách bấm vào một đơn hàng và thấy ô "Gửi tin nhắn" — họ hỏi một câu, và nhân viên thấy ngay câu đó trong chatter của đơn ở khu quản trị. Hai giao diện khác nhau (portal của khách, backend của nhân viên) nhưng cùng một luồng tin. Đằng sau nó là mail.thread và mail.message — hệ thống trao đổi/theo dõi gắn với mọi bản ghi trong Odoo. Bài này mổ xẻ: làm sao một model có chatter, message_post tạo ra gì, và portal cho khách gửi tin an toàn ra sao — bằng một tin thật đăng lên rồi xem nó hiện trong chatter Odoo 19.

mail.thread: thêm chatter cho model

Chatter không tự có. Một model chỉ có chatter khi kế thừa mail.thread — mixin thêm các trường message_ids (các tin), message_follower_ids (người theo dõi), và những phương thức như message_post.

Ảnh chụp mã Python nền tối. Phần trên model kế thừa mail thread có chatter: class TheThanhVien model, _name quan the thanh vien, _inherit mail thread thêm message_ids follower. Gửi một tin vào chatter server-side: the message_post body Cho em hỏi thẻ VIP còn ưu đãi cuối tuần không ạ, message_type comment tin trao đổi, subtype_xmlid mail mt_comment hiện cho follower, chú thích tạo một mail message gắn model cộng res_id của bản ghi. Phần dưới portal template khung chatter cho khách: t t-call portal message_thread, t t-set object t-value the bản ghi, t t-set token t-value the access_token. Khách gõ tin thì JS gọi controller PortalChatter trong portal_thread py, class PortalChatter kế thừa ThreadController, _get_thread_with_access kiểm quyền theo token hash trước khi cho message_post chống lộ giả mạo. Ghi chú cùng một mail thread hiện ở chatter backend lẫn portal của khách, portal thêm một lớp bảo mật token cộng _document_check_access

Hình 1: _inherit = ['mail.thread'] cho model có chatter. message_post(body, message_type='comment', subtype_xmlid='mail.mt_comment') đăng một tin — tạo một mail.message gắn model + res_id của bản ghi. Phần dưới: template portal t-call="portal.message_thread" dựng khung chatter cho khách, và controller PortalChatter kiểm quyền trước khi cho khách gửi.

message_post: mỗi tin là một mail.message

Gọi message_post trên một thẻ thành viên, đây là bản ghi mail.message thật nó tạo ra:

Ảnh chụp terminal nền tối message_post thật trên Odoo 19 tạo một bản ghi mail message. Lệnh msg bằng the message_post body message_type comment, rồi msg read các trường model res_id message_type subtype_id author_id. Kết quả id 4297, model quan the thanh vien, res_id 47 gắn đúng bản ghi VIP-0003, message_type comment subtype Discussions, author_id OdooBot người gửi khách hoặc nhân viên, body Cho em hỏi thẻ VIP còn ưu đãi cuối tuần không ạ. Ghi chú mọi chatter backend hay portal đều là mail message như thế này

Hình 2: message_post tạo mail.message id 4297 — model='quan.the.thanh.vien', res_id=47 (đúng thẻ VIP-0003), message_type='comment', subtype='Discussions', author_id là người gửi, body là nội dung. Đây là "viên gạch" của mọi chatter, dù hiện ở đâu.

Và tin đó hiện ngay trong chatter của bản ghi (ở đây là khu quản trị):

Ảnh chụp khung chatter thật trên Odoo 19 của một thẻ thành viên. Trên cùng có ba nút Gửi tin, Ghi chú, Hoạt động, cùng biểu tượng tìm kiếm kẹp file và người theo dõi số 0. Dưới là dải Hôm nay và một tin nhắn: avatar OdooBot, tên OdooBot lúc 8 giờ 46, nội dung Cho em hỏi thẻ VIP còn ưu đãi cuối tuần không ạ trong bong bóng tin

Hình 3: Chatter thật hiển thị tin vừa đăng — avatar + tên người gửi, thời gian, nội dung. Nút "Gửi tin" (comment, khách/đối tác thấy), "Ghi chú" (log note, chỉ nội bộ), "Hoạt động" (lịch việc). Cũng mail.message này, nếu bản ghi mở trên portal, khách sẽ thấy trong khung chatter của họ.

Portal chatter: cho khách gửi tin an toàn

Chatter backend là cho nhân viên. Để khách trao đổi ngay trên trang tài khoản của họ, portal dựng một khung chatter riêng bằng template portal.message_thread, truyền vào bản ghi (object) và token truy cập. Khi khách gõ và gửi, JS gọi controller PortalChatter (trong portal_thread.py, kế thừa ThreadController).

Điểm cốt lõi là bảo mật. Khách không đăng nhập đầy đủ như nhân viên; nếu để hở, người ta có thể đổi res_id để đọc/ghi chatter của bản ghi người khác. Vì vậy trước khi cho message_post, controller gọi _get_thread_with_access — kiểm chứng khách có quyền (qua access_token của portal.mixin ở phần 171, hoặc hash/pid). Chỉ khi hợp lệ, tin mới được đăng, và đăng dưới danh nghĩa đúng partner của khách. Đó là cách Odoo cho khách "chat" mà không mở lỗ hổng.

comment vs note: hai loại tin

Một chi tiết hay nhầm khi code chatter:

  • message_type='comment' + subtype='mail.mt_comment' — tin trao đổi, gửi cho follower (email thông báo), khách trên portal thấy được. Dùng cho đối thoại với khách.
  • Log note (subtype='mail.mt_note') — ghi chú nội bộ, chỉ nhân viên thấy, khách không thấy trên portal. Dùng cho ghi chú riêng của team.

Đăng nhầm loại là hoặc làm phiền khách (gửi email ghi chú nội bộ), hoặc để lộ ghi chú nội bộ ra portal. Chọn message_type/subtype đúng theo đối tượng.

Ba ý mang về

  1. Kế thừa mail.thread để model có chatter (message_ids, follower, message_post); mỗi tin là một mail.message gắn model + res_id — cùng dữ liệu hiện ở chatter backend lẫn portal.
  2. Portal cho khách gửi tin qua template portal.message_thread + controller PortalChatter; cốt lõi là _get_thread_with_access kiểm quyền bằng access_token/hash trước khi cho đăng — chống đọc/ghi chatter của người khác.
  3. Phân biệt comment và note: mt_comment là tin trao đổi (khách thấy, gửi thông báo), mt_note là ghi chú nội bộ (chỉ nhân viên) — chọn đúng để không làm phiền khách hay lộ nội bộ.

Hết mảng portal/website, phần sau bước sang một chủ đề lớn của front-end website: Phần sau mổ xẻ cấu trúc cơ bản của một website theme — module theme gồm gì, cách nó khai màu/font/bố cục, và cách Odoo áp theme lên toàn bộ trang.