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

> Three OCA modules touch fonts in Odoo, one is filed under a country rather than a feature, and none of them runs on Odoo 18 or 19. What each does and where it hooks in.

**Date:** 2026-07-30
**Source:** <https://trobz.com/insights/oca-font-modules-in-odoo/>

---



You finished [part 1]({{< surl >}}/insights/fonts-in-odoo). 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`](https://github.com/odoo/odoo/blob/19.0/odoo/addons/base/models/res_company.py)) 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:

```xml
<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](https://github.com/OCA/l10n-thailand/tree/16.0/l10n_th_fonts), 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:

```python
"assets": {
    "web.report_assets_common": [
        "l10n_th_fonts/static/src/scss/fonts_style.scss",
    ],
},
```

```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:

```xml
<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:

```css
.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](https://github.com/OCA/account-financial-reporting/issues/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`](https://github.com/OCA/website/tree/17.0/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:

```python
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](https://github.com/OCA/website/pull/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`](https://github.com/OCA/web/tree/17.0/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`:

```python
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:

```xml
<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](https://github.com/OCA/web/pull/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]({{< surl >}}/insights/font-awesome-in-odoo).

Font family customization at the app level, beyond what OCA covers, is where the Odoo Apps Store gets interesting. [Part 3]({{< surl >}}/insights/apps-store-font-modules-in-odoo) tours it.

