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.

Ảnh chụp mã Python nền tối. Hằng LOAI_CHO_PHEP là tập image png jpeg webp, CO_TOI_DA bằng 2 nhân 1024 nhân 1024 tức 2 MB. Route http quan_ca_phe tai_anh gui type http auth public website True methods POST. Hàm gui nhận post, lấy tep bằng request httprequest files get anh kèm chú thích file không ở post params mà ở httprequest files và tep là werkzeug FileStorage. Nếu không có tep hoặc không có filename thì redirect kèm lỗi trong. du_lieu bằng tep read trả về bytes. Kiểm nếu tep mimetype không thuộc LOAI_CHO_PHEP thì redirect lỗi loai, nếu len du_lieu lớn hơn CO_TOI_DA thì redirect lỗi lon. Vì public user nên gọi request env ir attachment sudo create với name tep filename, raw du_lieu bytes thô, mimetype, res_model quan the thanh vien. Cuối cùng render trang cảm ơn với ten co loai. Phần dưới là form action tai_anh gui method post enctype multipart form-data bắt buộc cho file, input hidden csrf_token, input type file name anh accept image, button Tải lên

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.mimetype do 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 khi read(); 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:

Ảnh chụp trang tải ảnh thẻ lên thật trên Odoo 19. Tiêu đề Tải ảnh thẻ lên. Nhãn Chọn ảnh PNG JPEG WEBP tối đa 2 MB. Một ô chọn tệp Choose File No file chosen. Nút Tải lên màu tím. Dưới cùng là footer website với Liên kết hữu ích Về chúng tôi Kết nối với chúng tôi

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:

Ảnh chụp trang cảm ơn thật sau khi tải ảnh lên trên Odoo 19. Header website có Your Logo Trang chủ số điện thoại Tiếng Việt Administrator Liên hệ. Tiêu đề lớn màu xanh lá Tải ảnh thành công. Dòng chữ Đã lưu anh_the_demo.png in đậm gạch ngang 195112 byte loại image png. Một nút viền tím Tải ảnh khác

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 Binary trê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 qua ir.attachment, nhưng bạn thao tác qua field.
  • ir.attachment trự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ắn res_model/res_id tuỳ ý. 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à files rỗ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ải tep.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ắn res_model để dễ dọn.

Ba ý mang về

  1. File upload nằm ở request.httprequest.files, không phải request.params — mỗi phần tử là một FileStorage (werkzeug) có .filename, .mimetype, .read(); form phải enctype="multipart/form-data".
  2. 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.
  3. Lưu thành ir.attachment: gán raw (bytes) hoặc datas (base64), đặt mimetype, 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 field Binary.

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ả.