Bạn đã dùng widget="progressbar", widget="badge", widget="priority" trong view mà không nghĩ nhiều. Mỗi cái tên đó là một field widget: một component OWL quyết định một field hiển thị ra sao và cho nhập thế nào. Odoo có sẵn vài chục cái. Nhưng khi không cái nào vừa ý, bạn viết được widget của riêng mình — và bài trước về registry đã cho ta đúng công cụ để cắm nó vào. Bài này viết một widget thật: hiển thị điểm thẻ thành một huy hiệu đổi màu theo mốc.

Field widget là gì

Trong một view, <field name="diem"/> không tự biết vẽ gì. Web client nhìn thuộc tính widget= (hoặc kiểu field, nếu không khai) để chọn một component đảm nhiệm việc render field đó. Component ấy nhận vào một bộ props chuẩn — quan trọng nhất là record (bản ghi đang mở) và name (tên field) — rồi tự quyết định vẽ input, badge, thanh tiến độ, hay bất cứ thứ gì.

Widget tự viết gồm ba mảnh quen thuộc, đúng như mọi component OWL: một class, một template, và một dòng đăng ký vào registry — nhưng lần này vào category "fields":

Ảnh chụp hai đoạn mã nền tối. Đoạn trên là file diem_badge js import standardFieldProps từ web views fields standard_field_props, định nghĩa lớp DiemBadgeField kế thừa Component với template quan_ca_phe DiemBadgeField và static props trải standardFieldProps gồm id name readonly record, một getter diem đọc giá trị từ props record data theo props name, một getter lop chọn lớp màu text-bg-warning khi điểm từ 150, text-bg-success khi từ 100, text-bg-secondary khi lớn hơn 0 và text-bg-light khi bằng 0, rồi đăng ký registry category fields add diem_badge với object gồm component DiemBadgeField supportedTypes integer và displayName Huy hiệu điểm. Đoạn dưới là dòng view form gắn field diem với thuộc tính widget diem_badge

Hình 1: Widget (trên) và cách gắn vào form (dưới). static props = { ...standardFieldProps } khai đúng bộ props mà field widget nào cũng nhận: id, name, readonly, record. Đọc giá trị field bằng this.props.record.data[this.props.name]. Đăng ký vào "fields" với supportedTypes cho biết widget dùng được cho kiểu field nào (integer ở đây).

Ba điểm cốt lõi

1. standardFieldProps. Mọi field widget đều nhận cùng một bộ props do web client truyền vào. Thay vì tự khai lại, import standardFieldProps rồi trải (...) vào static props. Thiếu bước này thì OWL kiểm props sẽ than phiền, hoặc bạn tự gõ lại và lệch với lõi.

2. Đọc giá trị qua record. Widget không nhận thẳng giá trị — nó nhận cả record và tên field, rồi tự lấy: this.props.record.data[this.props.name]. Cách gián tiếp này có lý do: cùng một widget dùng được cho bất kỳ field integer nào, không dính cứng vào tên diem.

3. Đăng ký vào category "fields" với value là { component, supportedTypes, displayName }. supportedTypes là danh sách kiểu field mà widget phục vụ — gắn widget="diem_badge" lên một field không thuộc supportedTypes sẽ bị cảnh báo. displayName là tên hiện trong trình chọn widget của Studio.

Gắn vào form và xem kết quả

Trong form thẻ, mình đổi <field name="diem" widget="progressbar"/> thành <field name="diem" widget="diem_badge"/>, khai hai file JS/XML vào bundle web.assets_backend của manifest, rồi nâng cấp. Mở một thẻ 150 điểm:

Ảnh chụp thật form thẻ thành viên Odoo 19, nhóm bên trái Thông tin thẻ với Chủ thẻ Ghi chú nội bộ và Ngày cấp 17 tháng 9, nhóm bên phải Điểm và số dư trong đó dòng Điểm tích luỹ hiển thị một huy hiệu bo tròn màu vàng cam với chữ 150 điểm chấm giữa Kim cương, tiếp theo là Hạng thẻ Vàng, Số dư 0 đồng đô la, Tiền tệ USD và Ưu đãi áp dụng

Hình 2: Field diem giờ hiển thị bằng widget tự viết: huy hiệu "150 điểm · Kim cương" nền vàng cam. Màu (text-bg-warning) và nhãn ("Kim cương") do getter lop và nhan trong component tính từ chính giá trị 150 — vượt mốc 150 nên vào bậc cao nhất. Đổi thẻ khác điểm thấp hơn, badge tự sang màu xanh/xám và đổi nhãn.

Toàn bộ logic hiển thị nằm trong component: web client chỉ đưa record vào, component đọc điểm và tự quyết vẽ gì. Đây là widget chỉ đọc — nó trình bày giá trị chứ chưa cho sửa.

Widget đọc và widget ghi

Widget vừa viết chỉ hiển thị. Một widget cho phép nhập/sửa thì cần thêm một bước: khi người dùng thay đổi, gọi this.props.record.update({ [this.props.name]: giá_trị_mới }) để ghi ngược vào record (rồi Odoo lo phần lưu). Widget chỉ đọc như huy hiệu này bỏ qua bước đó — hợp lý, vì điểm là thứ hệ thống tính, không cho gõ tay.

Một lưu ý thực dụng: đừng lạm dụng widget tự viết. Phần lớn nhu cầu đã có widget sẵn (badge, progressbar, priority, handle...) hoặc giải quyết được bằng decoration-* trong view. Chỉ viết widget mới khi cách hiển thị/tương tác thật sự không có sẵn — mỗi widget tự viết là một mẩu code phải bảo trì qua các bản Odoo.

Ba ý mang về

  1. Field widget là component OWL quyết định một field hiển thị/nhập thế nào. Tự viết gồm ba mảnh: class (với static props = { ...standardFieldProps }), template, và đăng ký vào category "fields" với { component, supportedTypes, displayName }.
  2. Widget không nhận thẳng giá trị — nó nhận record và name, đọc bằng this.props.record.data[this.props.name] (và ghi bằng record.update({[name]: val}) nếu cho sửa). Nhờ gián tiếp mà một widget dùng được cho mọi field cùng kiểu.
  3. Gắn vào view bằng widget="tên", và supportedTypes giới hạn widget cho đúng kiểu field. Chỉ viết widget mới khi widget sẵn có và decoration-* không đủ.

Widget huy hiệu này hiển thị con số thô. Nhưng nếu field là tiền tệ, phần trăm, hay ngày, bạn cần định dạng khi hiện và phân tích ngược khi người dùng gõ vào. Phần sau đi vào formatter và parser — cặp hàm biến giá trị thô thành chuỗi đẹp và ngược lại trong field widget.