Từ đầu loạt website tới giờ ta luôn đưa dữ liệu ra cho khách xem. Chiều ngược lại — cho khách gửi dữ liệu vào — mới là chỗ dễ vấp: một form công khai đặt trên Internet phải chống giả mạo request, phải cho phép một "Public user" không có quyền tạo bản ghi thực sự tạo được bản ghi, phải kiểm dữ liệu và chặn bot. Bài này dựng thật một form "Đăng ký thẻ thành viên", điền và gửi ngay trên Odoo 19, xác nhận bản ghi được tạo — và chỉ ra những chỗ chết người nếu làm sai.

Hai route: GET để hiện form, POST để nhận

Một form web luôn cần một cặp route: một GET render trang chứa <form>, một POST nhận dữ liệu khi khách bấm gửi.

Ảnh chụp mã nền tối hai phần. Phần Python: route http quan_ca_phe dang_ky type http auth public website True, hàm form trả về request render dang_ky_form. Route http quan_ca_phe dang_ky gui type http auth public website True methods POST, hàm submit nhận post, kiểm honeypot email_phu nếu có thì redirect trang cảm ơn, lấy ho_ten và so_dt strip, nếu thiếu thì redirect kèm error, rồi vì public user không có quyền create nên gọi request env quan the thanh vien sudo create với name ho_ten hang_the bac ghi_chu_noi_bo chứa số điện thoại, cuối cùng redirect trang cảm ơn. Phần XML form: thẻ form action quan_ca_phe dang_ky gui method post, input hidden csrf_token t-att-value request csrf_token thiếu là lỗi 400, input email_phu ẩn honeypot, input ho_ten required, input so_dt required, button submit Đăng ký

Hình 1: Cặp route và form. GET (form) render template; POST (submit, methods=['POST']) nhận **post, kiểm tra, rồi create bản ghi. Trong template, dòng csrf_token là bắt buộc — mục sau giải thích. Sau khi tạo xong, redirect sang trang cảm ơn (mẫu PRG: Post-Redirect-Get, tránh gửi lại khi bấm F5).

Điền và gửi thật

Mở form, điền "Trần Thị Mai" và số điện thoại, bấm Đăng ký:

Ảnh chụp form đăng ký thẻ thành viên thật trên Odoo 19. Trên cùng tiêu đề Đăng ký thẻ thành viên. Có ô nhập Họ và tên, ô nhập Số điện thoại, và nút Đăng ký 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. Trang mượn header footer từ website layout

Hình 2: Form thật tại /quan_ca_phe/dang_ky — hai ô nhập và nút "Đăng ký", nằm trong khung website (t-call="website.layout").

Bấm gửi, server nhận POST, tạo bản ghi, rồi chuyển sang trang cảm ơn:

Ảnh chụp trang cảm ơn thật sau khi gửi form trên Odoo 19. Tiêu đề lớn màu xanh lá Đăng ký thành công. Dòng chữ Cảm ơn bạn đã đăng ký thẻ thành viên Quán Cà Phê Việt. Một nút viền tím Đăng ký thêm. Dưới cùng là footer website

Hình 3: Sau khi POST thành công (server trả 303 redirect), khách thấy trang "Đăng ký thành công!". Kiểm tra CSDL: một bản ghi quan.the.thanh.vien mới đúng như đã nhập — name = "Trần Thị Mai", ghi_chu_noi_bo = "SĐT: 0912345678 (đăng ký online)", hạng "bac". Dữ liệu khách gửi đã vào hệ thống thật.

csrf_token: thiếu là 400 ngay

Đây là chỗ vấp phổ biến nhất. Với route type='http', Odoo mặc định bật bảo vệ CSRF: mọi request thay đổi (POST) phải kèm một csrf_token hợp lệ, nếu không server trả 400 "Session expired (invalid CSRF token)". Nhìn lõi http.py là rõ cơ chế:

# Trong HttpDispatcher: kiểm CSRF cho method không an toàn
if method not in SAFE_HTTP_METHODS and endpoint.routing.get('csrf', True):
    token = request.params.pop('csrf_token', None)
    if not request.validate_csrf(token):
        raise werkzeug.exceptions.BadRequest('Session expired (invalid CSRF token)')

Token do request.csrf_token() sinh: một HMAC-SHA1 của session id + hạn dùng, ký bằng database.secret; validate_csrf so lại bằng consteq (so sánh thời-gian-hằng chống dò). Vì token gắn với session, form phải render token trong cùng phiên mà khách sẽ gửi — nên luôn đặt <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/> trong form. (Khi thật cần một endpoint không session, ví dụ webhook máy ngoài, mới đặt csrf=False — và tự lo chống giả mạo cách khác.)

sudo(): Public user không có quyền create

Điểm chết người thứ hai: route auth='public' chạy bằng "Public user" — user này gần như không có quyền gì, chắc chắn không được create trên model của bạn. Gọi thẳng request.env['quan.the.thanh.vien'].create(...) sẽ ném AccessError. Vì vậy phải sudo():

request.env['quan.the.thanh.vien'].sudo().create({...})

Nhưng sudo() là con dao hai lưỡi: nó bỏ mọi kiểm quyền, nên bạn tự chịu trách nhiệm. Nguyên tắc: sudo() chỉ cho đúng thao tác cần (tạo một bản ghi từ dữ liệu đã kiểm), kiểm dữ liệu trước khi tạo, và không trả lại cho khách bất cứ dữ liệu nhạy cảm nào mà sudo() vô tình mở ra.

Validate và chống bot

Form công khai là mồi ngon cho bot. Hai lớp phòng thủ tối thiểu:

  • Kiểm dữ liệu trước khi tạo. Thiếu họ tên hay số điện thoại thì redirect lại form kèm cờ lỗi, không tạo bản ghi rác. Đừng tin đầu vào — kể cả khi HTML có required, kẻ gửi POST thẳng bỏ qua được.
  • Honeypot. Thêm một ô ẩn (display:none) tên vô hại; người thật không thấy nên để rỗng, bot điền tự động vào mọi ô. Nếu ô đó có giá trị, coi như bot và bỏ qua lặng lẽ. Rẻ và hiệu quả với bot đơn giản. (Lưu ý: đừng đặt tên ô ẩn là website — Odoo dùng tên đó cho routing, sẽ nhiễu; tôi đặt email_phu.)

Với form nhiều hơn, có thể thêm giới hạn tần suất theo IP hoặc captcha — nhưng honeypot + validate là mức nền cho mọi form.

Tự viết hay dùng Website Form builder

Odoo còn có sẵn snippet "Form" trong trình dựng web: kéo thả để tạo form ghi vào một model, không cần code. Vậy khi nào tự viết controller?

  • Dùng builder khi form đơn giản (liên hệ, đăng ký nhận tin) và muốn người quản trị tự sửa field bằng chuột.
  • Tự viết controller khi cần logic: kiểm tra phức tạp, tạo nhiều bản ghi liên quan, gọi API ngoài, xử lý sau khi lưu (gửi email, tạo mã), hay luồng nhiều bước. Controller cho bạn toàn quyền.

Ba ý mang về

  1. Form web cần cặp route GET (render form) + POST (methods=['POST'], tạo bản ghi), kết thúc bằng redirect sang trang cảm ơn (PRG chống gửi lại khi F5).
  2. csrf_token là bắt buộc với POST type='http' — <input name="csrf_token" t-att-value="request.csrf_token()"/>; thiếu là 400. Và sudo() để Public user tạo được bản ghi, nhưng chỉ dùng có kiểm soát.
  3. Luôn validate + chống bot: kiểm dữ liệu server-side (đừng tin required), thêm honeypot (đừng đặt tên ô ẩn là website); form phức tạp thì tự viết controller, form đơn giản dùng snippet Form của builder.

Form của ta mới nhận chữ. Nhiều lúc khách cần gửi cả tệp — ảnh CMND, file đính kèm. Phần sau làm controller nhận file upload — đọc file từ request, kiểm loại và kích thước, và lưu an toàn thành attachment trong Odoo.