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:

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.

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ọiformatIntegerđể 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ớiparseMonetary,formatFloatđi vớiparseFloat— 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ề
- 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.
import { formatInteger } from "@web/views/fields/formatters"vàimport { parseInteger } from "@web/views/fields/parsers"(cùng họformatFloat/formatMonetary/formatPercentagevàparseFloat/parseMonetary). Format theo locale —1234567→"1.234.567"; parse ngược lại đúng locale.- Parser luôn có thể ném lỗi vì input do người gõ (
parseInteger("abc")ném ngay). Bọctry/catchtrongonChange, 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.