Dữ liệu sai làm hỏng cả hệ thống: điểm tích luỹ âm, ngày kết thúc trước ngày bắt đầu, số lượng vượt tồn kho. Bạn không thể tin người dùng luôn nhập đúng — phải có luật kiểm tra chặn dữ liệu sai ngay khi lưu. Odoo cho làm điều đó bằng Python qua @api.constrains. Mở màn chương "ràng buộc & bảo vệ dữ liệu", ta đi vào công cụ này — và tôi sẽ viết một constraint thật rồi cho nó chạy.

Viết một constraint

@api.constrains là decorator đặt lên một method; method đó chạy mỗi khi một trong các trường liệt kê bị create/write. Sai điều kiện thì raise ValidationError:

Cách viết @api.constrains trong Odoo 19 luật kiểm tra hợp lệ bằng Python trong module quan_ca_phe: import models fields api và from odoo.exceptions import ValidationError; class TheThanhVien model quan.the.thanh.vien có diem là fields.Integer Điểm tích luỹ; decorator api.constrains diem nghĩa chạy khi diem đổi lúc create hoặc write; def _check_diem_khong_am self, for the in self luôn duyệt recordset, nếu the.diem nhỏ hơn 0 thì raise ValidationError với thông báo Điểm tích luỹ không được âm kèm tên thẻ và số điểm; đặc điểm là chạy khi create write một trong các trường liệt kê, sai thì raise ValidationError rollback cả giao dịch dữ liệu không lưu, thông báo hiện lên cho người dùng khác lỗi cơ sở dữ liệu thô, dùng cho luật phức tạp nhiều trường tính toán xuyên quan hệ

Hình 1: Constraint thật tôi thêm vào module quan_ca_phe. @api.constrains('diem') bảo Odoo chạy _check_diem_khong_am mỗi khi diem đổi. Bên trong: for the in self (luôn duyệt), rồi kiểm if the.diem < 0 và raise ValidationError(...) với thông báo rõ ràng cho người dùng.

Cho nó chạy thật

Tôi thêm constraint trên, nâng cấp module (-u quan_ca_phe), restart, rồi thử vi phạm. Đây là output thật trên db blog19:

Constraint tự viết chạy thật trên module quan_ca_phe database blog19: đã thêm api.constrains diem nâng cấp module bằng -u và restart; tạo thẻ the bằng T.create name TV-DEMO-029 diem 100 thành công diem 100; một write điểm âm the.diem bằng -50 bị chặn ValidationError Điểm tích luỹ không được âm thẻ TV-DEMO-029 có điểm -50; hai create mới với điểm âm T.create name TV-XAU diem -1 cũng bị chặn ValidationError Điểm tích luỹ không được âm thẻ TV-XAU có điểm -1; sau khi bị chặn dữ liệu không đổi rollback the.diem vẫn là 100 không thành -50; so sánh constraint core kích thật cùng cơ chế partner.parent_id bằng partner.id báo ValidationError You cannot create recursive Partner hierarchies

Hình 2: Output thật. Tạo thẻ với diem=100 — OK. Nhưng the.diem = -50 (write) và create({'diem':-1}) đều bị ValidationError với đúng thông báo tôi viết. Quan trọng: sau khi bị chặn, the.diem vẫn là 100 — constraint sai làm rollback, dữ liệu không lưu. Dưới cùng là một constraint core kích cùng cơ chế: đặt parent_id = chính nó → "You cannot create recursive Partner hierarchies."

Đặc điểm quan trọng

Chạy khi trường liên quan đổi

@api.constrains('diem') chỉ chạy khi diem bị create/write. Liệt kê đủ các trường mà luật phụ thuộc:

@api.constrains('date_start', 'date_end')      # đổi 1 trong 2 → kiểm lại
def _check_ngay(self):
    for rec in self:
        if rec.date_end and rec.date_start and rec.date_end < rec.date_start:
            raise ValidationError("Ngày kết thúc phải sau ngày bắt đầu.")

Luôn for rec in self

Giống computed, method chạy trên recordset — phải duyệt để kiểm từng bản ghi (create theo lô kiểm cả lô một lần).

Sai → rollback cả giao dịch

raise ValidationError làm toàn bộ giao dịch bị hủy, không chỉ trường sai. Nên dữ liệu luôn ở trạng thái nhất quán: hoặc lưu hết hợp lệ, hoặc không lưu gì.

Thông báo cho người dùng

ValidationError hiện lên giao diện dưới dạng hộp thoại dễ hiểu — khác hẳn lỗi CSDL thô. Viết thông báo rõ ràng, có ngữ cảnh (như "thẻ TV-DEMO-029 có điểm -50") để người dùng biết sửa gì.

Khi nào dùng @api.constrains

  • Luật phức tạp: liên quan nhiều trường, có tính toán, xuyên quan hệ (ví dụ "tổng dòng con không vượt hạn mức").
  • Cần thông báo tiếng Việt rõ ràng cho người dùng.
  • Logic cần Python: điều kiện không biểu diễn được bằng SQL đơn thuần.

Còn ràng buộc đơn giản, cấp CSDL (duy nhất, không âm, khác rỗng) thì có công cụ nhẹ và nhanh hơn — _sql_constraints, là bài sau.

Constraint vs onchange

Đừng nhầm với @api.onchange (gợi ý khi đang nhập trên form). Khác biệt cốt lõi:

  • @api.constrains: chạy lúc LƯU (create/write), ở mọi nguồn (giao diện, API, import, shell) — là hàng rào thật, không thể lách.
  • @api.onchange: chỉ chạy trên form đang nhập, để cảnh báo/điền tự động — không chặn được nếu dữ liệu tới từ API/import.

Muốn bảo vệ dữ liệu chắc chắn, luôn dùng @api.constrains (hoặc SQL constraint), không dựa vào onchange.

Nhớ ba ý

  1. @api.constrains('field', ...) chạy method kiểm tra mỗi khi trường đó create/write; sai thì raise ValidationError — đo thật: chặn cả write âm lẫn create âm, dữ liệu giữ nguyên 100.
  2. Luôn for rec in self; ValidationError làm rollback cả giao dịch và hiện thông báo rõ ràng cho người dùng.
  3. Dùng cho luật phức tạp/nhiều trường/cần Python; chạy ở mọi nguồn (khác onchange chỉ trên form) — là hàng rào thật, không lách được.

Constraint bằng Python mạnh nhưng chạy ở tầng ứng dụng. Có những luật đơn giản nên đặt thẳng xuống CSDL cho nhanh và chắc. Phần sau đi vào _sql_constraints — ràng buộc duy nhất/không âm ở tầng PostgreSQL, và khi nào chọn nó thay vì @api.constrains.