Ba module OCA đụng tới font trong Odoo, một cái được xếp theo quốc gia chứ không theo tính năng, và không cái nào chạy trên Odoo 18 hay 19. Mỗi module làm gì và móc vào đâu.
Bạn đã đọc xong phần 1. Bạn nắm ba tầng, có sẵn các đoạn code, và giờ đang nhìn vào một yêu cầu của khách hàng: “cho người biên tập website tự tải font thương hiệu lên mà không cần lập trình viên.” Trước khi viết module đó, hãy kiểm tra xem OCA đã viết sẵn chưa.
Bài viết này chính là lần kiểm tra đó. Có ba module OCA đụng tới font, và một trong số đó được xếp ở nơi bạn sẽ chẳng bao giờ nghĩ tới mà tìm. Bài này nói mỗi module làm gì và nó móc vào đâu trên bản đồ ba tầng ở phần 1. Vì chúng ta đang nhìn từ Odoo 18/19, bài cũng nói module nào thật sự chạy được trên phiên bản bạn triển khai hôm nay. Không module nào chạy cả.
Đã kiểm chứng trên các repository GitHub của OCA ngày 30/07/2026. Số hiệu phiên bản và trạng thái PR sẽ thay đổi; cơ chế thì không.
Bản đồ, có chấm điểm
Phần 1 chia cách Odoo xử lý font thành ba tầng: backend, báo cáo PDF, website. Hãy chấm điểm từng tầng theo những gì OCA thật sự cung cấp.
| Tầng | Mức độ OCA phủ | Module |
|---|---|---|
| Font giao diện backend | Không có, và bản lõi cũng không có giao diện nào | không có, tự viết |
| Họ font báo cáo PDF | Không có module cấu hình, bản lõi tự lo, nhưng xem phần gói chữ viết và cảnh báo bên dưới | bản lõi, cộng l10n_th_fonts cho tiếng Thái |
| Cỡ chữ báo cáo PDF | Một phần, đứng lại ở 17.0 | web_font_size_report_layout |
| Tải font lên cho website | Có, đứng lại ở 17.0 | website_local_font |
Hai dòng trong bảng đáng dừng lại, vì “không có module” ở mỗi dòng mang một nghĩa khác nhau.
Họ font backend thì đúng là chẳng có gì: không module OCA, mà bản lõi cũng không có màn hình cấu hình nào. Trên mọi phiên bản, đó là việc phải viết code, đúng như phần 1 đã mô tả.
Họ font báo cáo không có module OCA vì lý do ngược lại. Bản lõi đã lo trường hợp phổ biến. res.company có sẵn một trường font (res_company.py:89) với các lựa chọn Lato, Roboto, Open Sans, Montserrat, Oswald, Raleway, Tajawal và Fira Mono, cả tám đều được đóng gói cục bộ dưới web/static/fonts/, nên lúc render không có lời gọi nào ra Google. Nơi duy nhất dùng trường này là web.styles_company_report, template phát ra font-family lên class layout riêng của từng công ty:
<t t-set="font" t-value="company.font or 'Lato'" />
...
.o_company_<t t-esc='company.id'/>_layout {
font-family: <t t-esc="font" />;
Chọn một trong tám font đó ở Settings → Document Layout là xong, không cần module nào. Công thức @font-face cộng web.report_assets_common ở phần 1 là thứ bạn cần khi font thương hiệu của khách không nằm trong tám font đó, mà với các bộ chữ doanh nghiệp thì phần lớn là vậy.
Chỗ OCA thật sự có font cho báo cáo: các gói chữ viết
Có đúng một module OCA ở tầng này, và nó không nằm ở nơi bạn sẽ tìm. l10n_th_fonts nằm trong OCA/l10n-thailand, xếp vào nhóm Report, và nó chính là công thức ở phần 1 được đóng gói thành một addon. Mười sáu file TTF, một stylesheet gồm các khai báo @font-face, và một manifest thả chúng vào đúng cái bundle mà phần 1 đã nêu tên:
"assets": {
"web.report_assets_common": [
"l10n_th_fonts/static/src/scss/fonts_style.scss",
],
},
@font-face {
font-family: "THSarabunNew";
src: url("../../fonts/THSarabunNew.ttf") format("truetype");
}
Tám font của bản lõi không phủ tiếng Thái, nên báo cáo tiếng Thái cần module này thì mới render đúng được. Module có trên mọi nhánh từ 13.0 đến 17.0, chưa có bản port 18.0. Không repo localization nào khác của OCA có thứ tương đương: các localization Việt Nam, Trung Quốc, Nhật Bản, Ấn Độ và Đài Loan đều không có gì liên quan tới font, nghĩa là mọi hệ chữ không phải Latin còn lại vẫn phải tự làm theo cách của phần 1.
Ngoại lệ không ai ghi lại
Bộ chọn đó chỉ với tới được các báo cáo dựng trên layout của bản lõi. Một số module báo cáo của OCA tự định nghĩa layout riêng và gán cứng font, nghĩa là thiết lập kia âm thầm không có tác dụng với chúng.
account_financial_report là module bạn dễ gặp nhất, vì nó tạo ra Sổ cái, Bảng cân đối thử, Bảng tuổi nợ và Công nợ chưa tất toán. Layout của nó không bao giờ gọi web.external_layout hay web.styles_company_report. Nó bọc nội dung báo cáo trong class riêng và nạp stylesheet riêng:
<template id="account_financial_report.internal_layout">
<div class="article o_account_financial_reports_page">
<link href="/account_financial_report/static/src/css/report.css" rel="stylesheet"/>
và stylesheet đó kết thúc bằng:
.o_account_financial_reports_page {
...
font-family: Helvetica, Arial;
}
Vậy là một khách hàng đặt Document Layout thành Roboto sẽ thấy Roboto trên hóa đơn và Helvetica trên Sổ cái, mà chẳng có thiết lập nào giải thích sự khác biệt. stock_inventory_valuation_report (OCA/stock-logistics-reporting) mang đúng quy tắc y hệt, chép nguyên cả hình dạng selector.
Đây cũng là chỗ font va vào ngôn ngữ. Helvetica và Arial phủ được chữ Latin nhưng không phủ dấu tiếng Việt hay glyph CJK trên mọi bản build wkhtmltopdf, và cảnh báo ở phần 1 đúng đến từng chữ: bộ render thay font một cách âm thầm, nên báo cáo trông vẫn ổn với người dựng ra nó và rụng dấu với khách hàng là người đọc nó. Muốn ghi đè thì phải nhắm vào class của module đó trong stylesheet báo cáo của bạn, chứ không phải đụng vào thiết lập Document Layout.
Cũng nên biết rằng phiên bản gắt nhất của chuyện này từng là một lỗi. Ở Odoo 11, chính stylesheet đó được nạp vào web.assets_backend với selector trần body, table, td, span, div, nên cài một module báo cáo kế toán là đổi luôn font của cả web client và làm hỏng vị trí con trỏ trong trình sửa XML view (OCA/account-financial-reporting#398). Lỗi được sửa bằng cách thu hẹp selector về class riêng của module, và đó là lý do quy tắc này còn tồn tại tới nay dưới một hình hài trông có vẻ vô hại.
Website: website_local_font
Đây đúng là khoảng trống phần 1 đã chỉ ra: trình tùy biến website của Odoo chỉ đưa ra Google Fonts. website_local_font (OCA/website) thêm một tùy chọn “Add a Local Font” ngay cạnh đó, để người biên tập tải thẳng lên một file otf/ttf/woff/woff2.
Phần cài đặt nhỏ hơn nhiều so với vẻ ngoài của tính năng. ir_attachment.py thêm đúng một phương thức:
def add_local_font(self, font_name, extension, file_data):
font_attachment = self.create({
"name": f"local-font-{font_name}",
"type": "binary",
"datas": file_data,
"mimetype": "font/" + extension,
"public": True,
})
...
file_string = f"@font-face {{ \n font-family: {font_name}; \nsrc:{src}; \n}}"
font_css_attachment = self.create({
"datas": base64.b64encode(file_string.encode("utf-8")),
"mimetype": "text/css",
"public": True,
})
font_attachment.original_id = font_css_attachment.id
return font_css_attachment.id
Tải font lên, module sẽ ghi thêm một ir.attachment thứ hai chứa một quy tắc @font-face dựng tay dưới dạng văn bản CSS, rồi nối hai bản ghi qua original_id. Đó chính xác là cơ chế phần 1 đã mô tả cho việc Odoo tự tải Google Fonts về: một ir.attachment công khai đóng vai một file trên đĩa. Module này chỉ mở thêm cho người biên tập một cánh cửa thứ hai vào cùng căn phòng. Một phần ghi đè nhỏ của web_editor.assets (assets.py) lo việc xóa các attachment đó khi font bị gỡ và định dạng lại danh sách font mà phía JS gửi sang thành cú pháp map SCSS mà $o-theme-font-configs cần.
Phiên bản: 15.0 và 17.0. Bản migration 16.0, OCA/website#1051, mở từ lần phát hành đó tới nay và chưa từng được merge. Không có gì cho 18.0 hay 19.0. Nếu bạn đang ở một trong hai phiên bản này thì không có giao diện tải font lên. Hãy quay về cách mở rộng map font của theme ở phần 1, hoặc dùng bộ chọn Google Fonts có sẵn nếu việc tải file lên cục bộ không phải yêu cầu bắt buộc.
Báo cáo PDF: web_font_size_report_layout
web_font_size_report_layout (OCA/web) trả lời một câu hỏi hẹp hơn cái tên của nó: không phải “font nào”, mà “cỡ bao nhiêu”. Một khi đã biết bản lõi tự lo phần chọn họ font, phạm vi này trở nên hợp lý. Module lấp đúng cái thiết lập mà bản lõi bỏ sót, và nó làm vậy bằng cách chép y nguyên khuôn mẫu của bản lõi, thêm một trường thứ hai nằm cạnh font trên res.company:
report_font_size = fields.Selection(
selection=[("9", "9 pt"), ("10", "10 pt"), ("11", "11 pt"),
("12", "12 pt"), ("13", "13 pt"), ("14", "14 pt")],
default="11",
)
trường này hiện ra trên wizard Document Layout để ai cũng đổi được mà không đụng tới code. Cách thực thi thô một cách có chủ đích: một XML inherit của web.styles_company_report và của mọi template web.external_layout_* có sẵn (standard, boxed, bold, striped) chèn vào một khối <style> nội tuyến:
<t t-set="fs" t-value="(company and company.report_font_size) or '11'" />
<style>
body, .o_report_layout, .page, .o_report_layout *, .page *,
table, td, th, p, li, span, div { font-size: <t t-esc="fs" />pt !important; }
</style>
!important trên một danh sách selector rộng thì không đẹp, nhưng chính nhờ vậy module chạy y hệt nhau trên mọi layout Odoo cung cấp, mà không cần vá riêng từng template.
Module cũng dùng lại cơ chế vô hiệu hóa cache của bản lõi thay vì tự nghĩ ra cái mới, bằng cách thêm report_font_size vào đúng tập trường vốn đã kích hoạt _update_asset_style() cho font và các màu của layout. Phần họ font vẫn là việc của bản lõi: chọn một trong tám font dựng sẵn qua bộ chọn, hoặc dùng @font-face của riêng bạn theo phần 1 cho mọi trường hợp khác.
Phiên bản: 16.0 và 17.0. Bản port 18.0, OCA/web#3389, đang mở và chưa merge tính tới lúc viết bài. Không có gì cho 19.0.
Tóm lại
- Họ font backend: OCA không có gì, bản lõi không có màn hình nào. Cách ghi đè SCSS ở phần 1 là lối đi duy nhất, trên mọi phiên bản.
- Họ font báo cáo: hãy kiểm tra bản lõi trước. Bộ chọn Document Layout phủ tám font đóng gói sẵn mà không cần module nào. Chỉ dùng
@font-facecủa phần 1 khi font thương hiệu không nằm trong số đó, và nhớ rằng bộ chọn không với tới các báo cáo tự mang layout riêng, trước hết làaccount_financial_report. - Chữ viết không phải Latin trong báo cáo:
l10n_th_fontsgiải quyết tiếng Thái trên 13.0 đến 17.0. Mọi hệ chữ khác, kể cả tiếng Việt và CJK, không có module OCA nào và phải dùng cách của phần 1. - Cỡ chữ báo cáo:
web_font_size_report_layoutphủ 16.0 và 17.0; 18.0 còn là một PR đang mở. - Tải font lên cho website:
website_local_fontchỉ phủ 15.0 và 17.0. Trên 18.0/19.0, hãy mở rộng map font của theme bằng tay.
Odoo 17.0 là phiên bản mà tất cả các module này cùng tồn tại. Qua mốc đó, độ phủ mỏng đi rất nhanh, và hai bản migration đang đứng im nằm chờ trên GitHub một người nào đó hoàn thiện. Cũng đáng nói là l10n_th_fonts dễ bị bỏ sót đến mức nào: tìm “font” trong tên các repository của OCA sẽ không bao giờ ra nó, vì module hữu ích lại được xếp theo một quốc gia chứ không theo tính năng. Nếu khách hàng của bạn đang ở 18 hay 19 và cần đúng thứ mà website_local_font hay web_font_size_report_layout từng cho họ miễn phí, bạn đang phải chọn giữa việc hồi sinh một PR đang đứng im và viết hai mươi dòng mà phần 1 đã đưa sẵn.
Một nhóm bài viết này cố ý để ra ngoài: font biểu tượng. OCA có duy trì module cho chúng và chúng cũng được đóng gói dưới dạng file font, nên trông như thể là dòng thứ tư trong bảng ở trên. Không phải vậy. Một bộ biểu tượng và một bộ chữ chẳng có gì chung ngoài định dạng file, và khác biệt đó hóa ra quan trọng đủ để có hẳn một bài riêng.
Việc tùy biến họ font ở cấp ứng dụng, vượt ra ngoài phạm vi OCA, là nơi Odoo Apps Store trở nên thú vị. Phần 3 dạo qua chỗ đó.