Phần trước khách gửi chữ; giờ khách gửi file — ảnh CMND, ảnh thẻ, tài liệu đính kèm. File upload có một cái bẫy khiến nhiều người mới bối rối: gọi post.get('anh') ra None, tưởng form hỏng. Thật ra file không nằm trong request.params — nó ở một chỗ riêng. Bài này chỉ đúng chỗ đó, cách đọc file, kiểm an toàn trước khi lưu, và lưu thành ir.attachment — qua một form tải ảnh dựng thật, upload một PNG và xác nhận trên Odoo 19.
File ở đâu: request.httprequest.files
Với form thường, đầu vào text đi vào **post (tức request.params). Nhưng file thì khác: nó được multipart-encode và werkzeug tách ra một chỗ riêng — request.httprequest.files. Đó là một dict, key là name của <input type="file">, value là một đối tượng FileStorage của werkzeug.

Hình 1: Lấy file bằng request.httprequest.files.get('anh') — trả về một FileStorage có .filename, .mimetype, .content_type và .read() (đọc ra bytes). Form phải khai enctype="multipart/form-data"; thiếu nó thì trình duyệt gửi tên file thay vì nội dung, và files sẽ rỗng.
Kiểm loại và kích thước TRƯỚC khi lưu
Đây là phần quan trọng nhất về an toàn. Một endpoint nhận file công khai mà không kiểm là mời rắc rối: người ta tải lên file thực thi, file khổng lồ làm nghẽn ổ đĩa, hay loại file bạn không xử lý được. Luôn kiểm trước khi tạo bản ghi:
- Loại (mimetype). Chỉ chấp nhận danh sách trắng (
{'image/png', 'image/jpeg', 'image/webp'}), không phải danh sách đen. Lưu ý:tep.mimetypedo trình duyệt khai, kẻ xấu đổi được — với yêu cầu cao, còn phải kiểm byte đầu (magic number) để xác nhận đúng định dạng. - Kích thước.
len(du_lieu)sau khiread(); chặn ở một ngưỡng (ở đây 2 MB). Odoo cũng có giới hạn multipart tổng thể, nhưng kiểm ở tầng ứng dụng cho bạn thông báo lỗi tử tế thay vì một trang 413 trống.
Sai một bước ở đây là lỗ hổng, không chỉ là trải nghiệm xấu.
Lưu thành ir.attachment
Cách chuẩn để cất một file trong Odoo là ir.attachment — model quản lý mọi tệp đính kèm (ảnh, PDF, file). Đọc lõi thấy các trường chính:
# ir_attachment.py
name = fields.Char('Name', required=True)
raw = fields.Binary("File Content (raw)") # nhận bytes thô
datas = fields.Binary('File Content (base64)') # nhận base64
mimetype = fields.Char('Mime Type')
res_model = fields.Char('Resource Model') # gắn với model nào
res_id = fields.Many2oneReference('Resource ID') # id bản ghi cụ thể
file_size = fields.Integer('File Size', readonly=True)
Điểm tiện của Odoo 19: gán raw nhận thẳng bytes — không cần tự base64.b64encode; nếu bạn đã có base64 thì gán vào datas. file_size được tính tự động. Vì route auth='public', phải sudo() để tạo (Public user không có quyền). res_model/res_id cho phép gắn attachment vào một bản ghi cụ thể (ví dụ một thẻ thành viên).
Upload thật một tấm ảnh
Mở form, form hiện ô chọn tệp:

Hình 2: Form tải ảnh tại /quan_ca_phe/tai_anh — một <input type="file"> với accept="image/*" và nút "Tải lên", trong khung website.
Chọn một tấm PNG và tải lên, controller đọc file, kiểm, tạo attachment, rồi hiện trang xác nhận:

Hình 3: Sau khi upload, trang xác nhận hiện tên, kích thước và loại đọc thẳng từ attachment vừa tạo: anh_the_demo.png, 195112 byte, image/png. Kiểm CSDL xác nhận một bản ghi ir.attachment mới đúng các thông số đó, gắn res_model = 'quan.the.thanh.vien'. File khách gửi đã được lưu an toàn.
Binary field hay ir.attachment
Có hai chỗ để cất file trong Odoo:
- Field
Binarytrên model của bạn (anh = fields.Binary()): file gắn liền một bản ghi, một field. Đơn giản khi mỗi bản ghi có đúng một ảnh (ví dụ ảnh đại diện). Thực ra bên dưới Odoo vẫn lưu nội dung quair.attachment, nhưng bạn thao tác qua field. ir.attachmenttrực tiếp: linh hoạt hơn — nhiều file cho một bản ghi, hoặc file không thuộc bản ghi nào, quản lý tập trung, gắnres_model/res_idtuỳ ý. Hợp cho "danh sách tệp đính kèm".
Nhận nhiều file, hay file rời rạc → ir.attachment. Một ảnh cố định cho mỗi bản ghi → field Binary.
Vài lưu ý
- Đừng quên
enctype="multipart/form-data"— thiếu làfilesrỗng, mà không có lỗi rõ ràng. .read()trả vềbytes, đọc một lần; nếu cần đọc lại phảitep.seek(0).- Kiểm trước, tạo sau. Tạo attachment rồi mới kiểm là đã ghi rác vào hệ thống.
sudo()có kiểm soát. Public tạo attachment được nhờsudo(), nhưng đừng để nó tạo bừa — luôn giới hạn loại/cỡ và cân nhắc gắnres_modelđể dễ dọn.
Ba ý mang về
- File upload nằm ở
request.httprequest.files, không phảirequest.params— mỗi phần tử là mộtFileStorage(werkzeug) có.filename,.mimetype,.read(); form phảienctype="multipart/form-data". - Luôn kiểm loại và kích thước trước khi lưu (danh sách trắng mimetype, ngưỡng
len(bytes)); mimetype trình duyệt khai có thể giả, việc quan trọng thì kiểm cả magic number. - Lưu thành
ir.attachment: gánraw(bytes) hoặcdatas(base64), đặtmimetype,res_model/res_idđể gắn bản ghi;sudo()cho public nhưng có kiểm soát. Một ảnh cố định thì dùng fieldBinary.
Ta vừa nhận file vào. Phần sau làm chiều ngược lại: controller trả file cho tải về — đọc một attachment ra, đặt đúng Content-Type và Content-Disposition để trình duyệt tải xuống với tên đẹp, và trả file lớn hiệu quả.