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:

Ảnh chụp đoạn mã Python nền tối hai phần. Phần trên import base64 rồi env ir.attachment create với name hop-dong-VIP-0001.txt, datas là base64 b64encode của noi_dung_bytes chú thích nội dung base64, res_model quan.the.thanh.vien chú thích gắn vào model nào, res_id the id chú thích bản ghi nào hiện ở chatter, mimetype text plain. Phần dưới đọc lại noi_dung bằng base64 b64decode att datas, gửi kèm trong tin chatter bằng the message_post body Đính kèm hợp đồng attachment_ids danh sách tuple 4 att id, và tạo đính kèm là link ngoài với type url url caphe-viet.vn the VIP-0001 res_model res_id

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:

Ảnh chụp khung trao đổi thẻ VIP-0001 của Odoo, biểu tượng kẹp giấy ở góc phải hiện số 1, một khối hoạt động đã lên kế hoạch phía trên, rồi mục Tập tin với một thẻ tệp biểu tượng văn bản tên hop-dong-VIP-0001.txt và nút Đính kèm tệp bên dướ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ỏ:

Ảnh chụp phiên odoo shell nền tối, in att mimetype file_size type trả về tuple text plain 69 binary, in att checksum trả về chuỗi sha1 64451b35 và chú thích sha1 nội dung, in att store_fname trả về 64 gạch chéo 64451b35 chú thích đường dẫn trong filestore, rồi lệnh ls filestore blog19 64 cho thấy file thật 69 byte trên đĩa với chú thích file thật trên đĩa 69 byte, và chú thích DB bảng ir_attachment giữ name mimetype checksum store_fname không giữ bytes, trùng nội dung thì cùng checksum nên dùng chung một file khử trùng lặp

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_dump CSDL 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 Binary thường cũng đẩy xuống attachment (attachment=True mặc định), nhưng nếu đặt attachment=False thì 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.attachment kế 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ề

  1. 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ằng base64.b64decode(att.datas), gửi kèm qua message_post(attachment_ids=...).
  2. 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ý.
  3. 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.