← Back to Blog
Development by Featured

Fonts in Odoo, Part 2: The OCA Modules (and Why They All Stop at 17.0)

Fonts in Odoo, Part 2: The OCA Modules (and Why They All Stop at 17.0)

Part 1 mapped where fonts live in Odoo: backend, PDF reports, website. Before you write a module for any of them, check whether OCA already did. Three modules exist, one of them hiding in a localization repo. None of them run on 19.0.

You finished part 1. You know the three layers, you have the snippets, and now you are staring at a client request: “let the website editor upload our brand font without a developer.” Before you write that module, check whether OCA already wrote it.

This article is that check. Three OCA modules touch fonts, and one of them is filed somewhere you would never search. This piece covers what each one does and where it hooks into the layer map from part 1. Since we are looking at this from Odoo 18/19, it also covers which ones actually run on the version you are deploying today. None of them do.

Verified against the OCA GitHub repositories on 2026-07-30. Version numbers and PR states change; the mechanisms do not.

The map, scored

Part 1 organized Odoo’s font handling into three layers: backend, PDF reports, website. Score each layer against what OCA actually ships.

Layer OCA coverage Module
Backend UI font None, and core has no UI either none, write it yourself
PDF report font family No config module, core owns it, but see the script packs and the warning below core, plus l10n_th_fonts for Thai
PDF report font size Partial, stalled at 17.0 web_font_size_report_layout
Website font upload Yes, stalled at 17.0 website_local_font

Two of those rows are worth pausing on, because “no module” means something different in each.

Backend font family genuinely has nothing: no OCA module, and no configuration screen in core either. It is code on every version, exactly as part 1 described.

Report font family has no OCA module for the opposite reason. Core already covers the common case. res.company carries a font field (res_company.py:89) offering Lato, Roboto, Open Sans, Montserrat, Oswald, Raleway, Tajawal and Fira Mono, and all eight are bundled locally under web/static/fonts/, so nothing calls out to Google at render time. The single consumer is web.styles_company_report, which emits font-family onto the per-company layout class:

<t t-set="font" t-value="company.font or 'Lato'" />
...
.o_company_<t t-esc='company.id'/>_layout {
    font-family: <t t-esc="font" />;

Pick one of those eight in Settings → Document Layout and you are done, no module required. Part 1’s @font-face plus web.report_assets_common recipe is what you need when the client’s brand font is not one of the eight, which for corporate typefaces is most of the time.

Where OCA does ship report fonts: script packs

There is one OCA module in this layer, and it does not live where you would look for it. l10n_th_fonts sits in OCA/l10n-thailand, filed under category Report, and it is part 1’s recipe packaged as an addon. Sixteen TTF files, one stylesheet of @font-face declarations, and a manifest that drops them into exactly the bundle part 1 named:

"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");
}

The eight core fonts do not cover Thai, so Thai reports need this before they render correctly at all. It exists on every branch from 13.0 through 17.0, with no 18.0 port. No other OCA localization repo ships an equivalent: Vietnamese, Chinese, Japanese, Indian and Taiwanese localizations have nothing font-related, which means every other non-Latin script is still a DIY job on the part 1 pattern.

The exception nobody documents

That picker only reaches reports built on core’s layouts. Some OCA report modules define their own layout and hardcode a font, which means the setting silently does not apply to them.

account_financial_report is the one you are most likely to hit, since it powers General Ledger, Trial Balance, Aged Partner Balance and Open Items. Its layout never calls web.external_layout or web.styles_company_report. It wraps report content in its own class and links its own stylesheet:

<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"/>

and that stylesheet ends with:

.o_account_financial_reports_page {
    ...
    font-family: Helvetica, Arial;
}

So a client who sets Document Layout to Roboto gets Roboto on invoices and Helvetica on the General Ledger, with no setting anywhere explaining the difference. stock_inventory_valuation_report (OCA/stock-logistics-reporting) carries the identical rule, copy-pasted down to the selector shape.

This is also where fonts and locale collide. Helvetica and Arial cover Latin text but not Vietnamese diacritics or CJK glyphs in every wkhtmltopdf build, and part 1’s warning applies with full force: the renderer substitutes silently, so the report looks fine to whoever built it and drops marks for the client who reads it. Overriding it means targeting that module’s class in your own report stylesheet, not touching the Document Layout setting.

Worth knowing that the sharpest version of this was a bug. In Odoo 11 the same stylesheet was loaded into web.assets_backend with a bare body, table, td, span, div selector, so installing an accounting report changed the font of the entire web client and broke cursor positioning in the XML view editor (OCA/account-financial-reporting#398). It was fixed by scoping the selector to the module’s own class, which is why the rule survives today in a harmless-looking form.

Website: website_local_font

This is the exact gap part 1 flagged: Odoo’s website customizer only offers Google Fonts. website_local_font (OCA/website) adds an “Add a Local Font” option next to it, so an editor can upload an otf/ttf/woff/woff2 file directly.

The implementation is smaller than the feature suggests. ir_attachment.py adds one method:

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

Upload the font, and the module writes a second ir.attachment containing a hand-built @font-face rule as CSS text, then links the two records through original_id. That is precisely the mechanism part 1 described for Odoo’s own Google Fonts download: a public ir.attachment standing in for a file on disk. This module just gives the editor a second door into the same room. A small override of web_editor.assets (assets.py) handles deleting those attachments when a font is removed and reformats the font list the JS side sends into the SCSS map syntax $o-theme-font-configs expects.

Versions: 15.0 and 17.0. The 16.0 migration, OCA/website#1051, has been open since that release and never merged. Nothing exists for 18.0 or 19.0. If you are on either, the upload UI is not available. Fall back to part 1’s theme-map extension, or the stock Google Fonts picker if a locally-uploaded file is not a hard requirement.

PDF reports: web_font_size_report_layout

web_font_size_report_layout (OCA/web) answers a narrower question than its name suggests: not “what font”, but “what size”. Once you know core already owns the family picker, the scope makes sense. This module fills the one setting core left out, and it does it by copying core’s own pattern exactly, adding a second field next to font on 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",
)

exposed on the Document Layout wizard so anyone can change it without touching code. The delivery mechanism is blunt on purpose: an XML inherit of web.styles_company_report and every stock web.external_layout_* template (standard, boxed, bold, striped) injects an inline <style> block:

<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 on a broad selector list is not elegant, but it is why the module works identically across every layout Odoo ships, without a separate patch per template.

It also reuses core’s cache invalidation rather than inventing its own, adding report_font_size to the same field set that already triggers _update_asset_style() for font and the layout colours. Family stays core’s job: one of the eight built-ins through the picker, or your own @font-face from part 1 for anything else.

Versions: 16.0 and 17.0. An 18.0 port, OCA/web#3389, is open and unmerged as of this writing. Nothing for 19.0.

The takeaway

  • Backend font family: nothing from OCA, no screen in core. Part 1’s SCSS override is the only path, on every version.
  • Report font family: check core first. The Document Layout picker covers eight bundled fonts with no module at all. Only reach for part 1’s @font-face when the brand font is not one of them, and remember the picker does not reach reports that ship their own layout, account_financial_report above all.
  • Non-Latin report scripts: l10n_th_fonts solves Thai on 13.0 to 17.0. Every other script, Vietnamese and CJK included, has no OCA module and needs the part 1 pattern.
  • Report font size: web_font_size_report_layout covers 16.0 and 17.0; 18.0 is an open PR.
  • Website font upload: website_local_font covers 15.0 and 17.0 only. On 18.0/19.0, extend the theme’s font map by hand.

Odoo 17.0 is the version where every one of these modules exists together. Past it, the coverage thins fast, and two stalled migrations are sitting open on GitHub waiting for someone to finish them. It is also worth noting how easy l10n_th_fonts was to miss: searching OCA repository names for “font” never surfaces it, because the useful module was filed under a country rather than a feature. If your client is on 18 or 19 and needs what website_local_font or web_font_size_report_layout used to give them for free, you are choosing between reviving a stalled PR and writing the twenty lines part 1 already gave you.

One category this article deliberately leaves out: icon fonts. OCA maintains modules for them and they ship as font files, which makes them look like a fourth row in the table above. They are not. An icon set and a typeface have nothing in common past the file format, and the distinction turns out to matter enough that it gets its own article.

Font family customization at the app level, beyond what OCA covers, is where the Odoo Apps Store gets interesting. Part 3 tours it.

Ready to get the most out of Odoo?

Whether you are starting a new implementation, upgrading from an older version, or optimizing your current setup — our Odoo-first team is here to help.