Widget huy hiệu ở bài trước chỉ hiển thị con số thô — vì điểm là số nguyên nhỏ. Nhưng đời thật phức tạp hơn: 1234567 mà đập thẳng ra màn hình thì khó đọc, phải thành 1.234.567. Và khi widget cho nhập, người dùng gõ 1.234.567 (có dấu phân tách) thì bạn phải hiểu ngược lại thành số 1234567 — còn gõ abc thì phải chặn. Hai chiều biến đổi đó là việc của formatter và parser.

Hai chiều của một field

Một field có hai "mặt": giá trị thô (số 1234567 trong record) và chuỗi hiển thị ("1.234.567" trên màn hình). Formatter và parser là cặp hàm nối hai mặt đó:

  • Formatter: giá trị thô → chuỗi hiển thị (khi render).
  • Parser: chuỗi người gõ → giá trị thô (khi đọc input), và ném lỗi nếu chuỗi không hợp lệ.

Odoo có sẵn cả bộ cho từng kiểu field, import thẳng mà dùng:

Ảnh chụp đoạn mã nền tối minh hoạ một field widget số nguyên có phân tách nghìn, import formatInteger từ web views fields formatters và parseInteger từ web views fields parsers, lớp SoNguyenField kế thừa Component với static props trải standardFieldProps, một getter hienThi dùng formatInteger biến giá trị record thành chuỗi theo locale ví dụ 1234567 thành một chấm 234 chấm 567, và một method onChange dùng khối try gọi parseInteger biến chuỗi người gõ thành số rồi record update, còn khối catch bắt lỗi khi nhập rác thì giữ giá trị cũ và hiện thông báo lỗi màu đỏ

Hình 1: Một field widget cho nhập, dùng cả hai. Getter hienThi gọi formatInteger để render (số → chuỗi có dấu nghìn). Method onChange gọi parseInteger trong try/catch: gõ đúng thì record.update ghi số vào record; gõ sai thì parser ném lỗi, ta bắt lại, giữ giá trị cũ và báo cho người dùng. Không có try/catch này thì một cú gõ nhầm làm hỏng cả form.

Gọi thật để thấy chúng làm gì

Mấy hàm này không trừu tượng — mình gọi thẳng chúng trong web client đang chạy (bắt bằng Playwright, không bịa kết quả). Cơ sở dữ liệu blog19 dùng locale kiểu châu Âu: dấu chấm ngăn nghìn, dấu phẩy ngăn thập phân.

Ảnh chụp kết quả thật khi gọi các hàm format và parse trong web client Odoo. Phần formatter số thành chuỗi hiển thị gồm formatInteger của 1234567 ra chuỗi một chấm 234 chấm 567, formatFloat của 1234567 phẩy 89 ra một chấm 234 chấm 567 phẩy 89, formatPercentage của 0 chấm 1523 ra 15 phẩy 23 phần trăm. Phần parser chuỗi thành số gồm parseInteger của chuỗi một chấm 234 chấm 567 ra số 1234567, parseFloat của chuỗi một chấm 234 chấm 567 phẩy 89 ra số 1234567 chấm 89. Phần cuối parser ném lỗi khi chuỗi không hợp lệ gồm parseInteger của abc và của 2 nhân 3 đều ném lỗi is not a correct number

Hình 2: Kết quả thật. Formatter theo locale: formatInteger(1234567) → "1.234.567", formatPercentage(0.1523) → "15,23%" (0.1523 là tỉ lệ, nhân 100 và thêm ký hiệu %). Parser đọc ngược đúng locale: parseInteger("1.234.567") → 1234567. Và điểm quan trọng nhất: parser ném lỗi với "abc" hay "2*3" — đó chính là cơ chế chặn nhập sai của form.

Vì sao tách làm hai hàm

Có thể hỏi: sao không viết thẳng logic vào widget? Vì cùng một quy tắc định dạng phải áp nhất quán khắp mọi nơi hiển thị field đó — form, list, kanban, report. Gom vào một cặp hàm dùng chung thì đổi locale (dấu nghìn, số chữ số thập phân) một chỗ là đổi hết, không phải sửa từng widget.

Ba điểm cần nhớ khi tự viết widget cho nhập:

  • Formatter theo locale, không tự nối chuỗi. 1234567 ở locale này là 1.234.567, ở locale khác là 1,234,567. Gọi formatInteger để nó lo, đừng tự chèn dấu.
  • Parser luôn có thể ném lỗi — vì input là do con người gõ. Bắt buộc bọc try/catch. Không bắt thì lỗi rơi ra ngoài và làm hỏng luồng lưu.
  • Format và parse phải là cặp đối xứng. Cái hiện ra bằng formatter nào thì đọc lại bằng parser tương ứng. formatMonetary đi với parseMonetary, formatFloat đi với parseFloat — lệch cặp là dữ liệu vào ra không khớp.

Có cả registry cho chúng

Ngoài import thẳng, Odoo còn có category "formatters" và "parsers" trong registry (đúng cơ chế bài registry), tra theo tên kiểu field. Nhờ vậy widget chung như IntegerField của lõi tự chọn đúng formatter theo kiểu mà không cần import cứng. Khi bạn thêm một kiểu hiển thị mới, đăng ký formatter/parser vào đó để phần còn lại của hệ thống dùng lại được.

Ba ý mang về

  1. Formatter (giá trị → chuỗi, theo locale) và parser (chuỗi → giá trị, ném lỗi khi sai) là hai chiều biến đổi của một field. Field widget cho nhập cần cả hai: formatter khi render, parser khi đọc input.
  2. import { formatInteger } from "@web/views/fields/formatters" và import { parseInteger } from "@web/views/fields/parsers" (cùng họ formatFloat/formatMonetary/formatPercentage và parseFloat/parseMonetary). Format theo locale — 1234567 → "1.234.567"; parse ngược lại đúng locale.
  3. Parser luôn có thể ném lỗi vì input do người gõ (parseInteger("abc") ném ngay). Bọc try/catch trong onChange, giữ giá trị cũ và báo lỗi — đó là cách form chặn nhập sai.

Ta đã tuỳ biến một field trong một view có sẵn. Nhưng nếu muốn một loại view hoàn toàn mới — không phải list/form/kanban mà là cách trình bày riêng của bạn? Phần sau bắt tay viết một view JS tuỳ biến từ đầu và đăng ký nó vào hệ thống view của Odoo.