Khách gửi hợp đồng scan, CMND, PDF hoá đơn — bạn muốn gắn thẳng vào thẻ thành viên của họ để lần sau mở ra là thấy. Trong Odoo, mọi tệp đính kèm đều là bản ghi ir.attachment, và nó gắn được vào bất kỳ bản ghi nào qua hai trường res_model + res_id. Nhưng có một điều nhiều người tưởng nhầm: nội dung tệp không nằm trong cơ sở dữ liệu. Hiểu chỗ tệp thật sự sống ở đâu giúp bạn sao lưu đúng và không làm phình DB.
Gắn file vào một bản ghi
Tạo ir.attachment với nội dung base64 và trỏ nó vào bản ghi đích:

Hình 1: ir.attachment.create với datas (nội dung base64), res_model + res_id (bản ghi được gắn vào). Nhờ hai trường này, tệp lập tức hiện trong khung trao đổi của bản ghi. Đọc lại nội dung bằng base64.b64decode(att.datas). Muốn gửi kèm trong một tin thì truyền attachment_ids cho message_post. Và với type='url', đính kèm chỉ là một liên kết ngoài — không lưu bytes, chỉ trỏ tới URL.
Trên giao diện, tệp hiện ngay dưới khung trao đổi:

Hình 2: Tệp hop-dong-VIP-0001.txt gắn vào thẻ hiện trong mục Tập tin của chatter, và biểu tượng kẹp giấy đếm "1". Người dùng bấm vào là tải/xem được; nút "Đính kèm tệp" cho họ thêm file bằng tay. Toàn bộ chỉ nhờ res_model='quan.the.thanh.vien' + res_id trỏ đúng thẻ.
Bytes ở filestore, không ở CSDL
Đây là điểm cốt lõi. Nội dung tệp không nằm trong bảng của PostgreSQL — nó là một file thật trên đĩa, trong thư mục filestore. CSDL chỉ giữ metadata và một con trỏ:

Hình 3: att.store_fname là 64/64451b35... — đường dẫn tệp trong filestore (data_dir/filestore/<db>/), chia thư mục theo 2 ký tự đầu của checksum (sha1 nội dung). Chạy ls đúng đường dẫn đó thấy file thật 69 byte trên đĩa. Bảng ir_attachment chỉ giữ name, mimetype, file_size, checksum, store_fname — không giữ bytes. Hệ quả hay của thiết kế này: hai tệp trùng nội dung có cùng checksum nên dùng chung một file vật lý (khử trùng lặp tự động).
Vài lưu ý khi làm việc với đính kèm
- Sao lưu phải gồm cả filestore. Chỉ
pg_dumpCSDL là mất hết tệp — bytes nằm ởdata_dir/filestore. Sao lưu đầy đủ = dump DB và copy filestore. - Đừng nhồi file lớn vào field Binary của model. Field
Binarythường cũng đẩy xuống attachment (attachment=Truemặc định), nhưng nếu đặtattachment=Falsethì bytes vào thẳng cột DB — làm DB phình và chậm. Với tệp lớn, luôn để nó đi qua filestore. type='url'cho link ngoài — khi tài liệu đã ở nơi khác (Google Drive, CDN), đừng tải về; tạo attachment kiểu url trỏ tới đó, nhẹ và luôn mới.- Quyền xem đính kèm theo bản ghi gốc.
ir.attachmentkế thừa quyền từres_model/res_id— ai không đọc được thẻ thì cũng không tải được tệp gắn vào thẻ đó. Đừng dựa vào "URL bí mật" để bảo mật.
Ba ý mang về
ir.attachment.create({'datas', 'res_model', 'res_id', ...})gắn tệp vào bất kỳ bản ghi nào — hiện ngay ở khung trao đổi; đọc lại bằngbase64.b64decode(att.datas), gửi kèm quamessage_post(attachment_ids=...).- Bytes nằm ở filestore trên đĩa, không trong CSDL: DB giữ
store_fname+checksum(sha1) làm con trỏ; tệp trùng nội dung dùng chung một file vật lý. - Sao lưu phải kèm filestore; tệp lớn để đi qua attachment (đừng nhồi cột DB);
type='url'cho link ngoài; quyền đọc tệp theo bản ghi gốc.
Đến đây ta đã đi hết mảng dữ liệu và tự động hoá phía server. Phần tiếp theo bước sang giao diện phía trình duyệt — nơi Odoo 19 dùng framework OWL. Phần sau dựng component OWL đầu tiên: cấu trúc một component, template, và cách đăng ký nó vào Odoo.