Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion pos_config_logo/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ This module is useful for a company that runs stores with different
brandings. To have a logo from the main branch is weird for the customer
of those branded stores.

The 18.0 migration used an absolute ``web.base.url`` image URL for
``pos.config.logo``. That requires a session and can show Odoo's camera
placeholder. The receipt now embeds the logo as a data URI (16.0
behaviour). See ``readme/CONTEXT.md``.

Configuration
=============

Expand All @@ -64,7 +69,7 @@ Usage
=====

When the cashier prints a receipt the point of sale logo will show up in
the receipt.
the receipt (embedded as a data URI).

Bug Tracker
===========
Expand Down
7 changes: 6 additions & 1 deletion pos_config_logo/models/pos_config.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# Copyright 2024 Tecnativa - David Vidal
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import fields, models
from odoo import api, fields, models


class PosConfig(models.Model):
_inherit = "pos.config"

logo = fields.Binary()

@api.model
def _load_pos_data_fields(self, config_id):
fields = list(super()._load_pos_data_fields(config_id) or self._fields)
return list(dict.fromkeys([*fields, "logo"]))
19 changes: 19 additions & 0 deletions pos_config_logo/readme/CONTEXT.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,21 @@
This module is useful for a company that runs stores with different brandings. To have
a logo from the main branch is weird for the customer of those branded stores.

## Receipt image (why it is embedded)

16.0 put the POS logo on the ticket as a data URI after loading a
**relative** ``/web/image?model=pos.config`` URL (same idea as the core
company logo). The 18.0 migration switched to an absolute
``{web.base.url}/web/image?model=pos.config`` URL.

That URL needs a session cookie. Company logos are typically public;
``pos.config.logo`` is not. If the browser host does not match
``web.base.url`` (``localhost`` vs ``127.0.0.1``) or print runs without
cookies, Odoo returns ``placeholder.png`` (camera icon) even when the
alternative logo is set. Changing ``web.base.url`` only fixes one host.

This module therefore embeds the binary already loaded on the POS as
``data:image/…;base64,…``, with a relative ``/web/image`` fallback (never
``_base_url``). That matches 16.0 and current Odoo receipt guidance:
store the logo as a data URL instead of refetching ``/web/image`` when
printing.
1 change: 1 addition & 0 deletions pos_config_logo/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
- [Tecnativa](https://tecnativa.com)
- David Vidal
- Miguel Machado
5 changes: 4 additions & 1 deletion pos_config_logo/readme/USAGE.md
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
When the cashier prints a receipt the point of sale logo will show up in the receipt.
When the cashier prints a receipt the point of sale logo will show up in
the receipt, embedded as a data URI so preview/print does not depend on
``web.base.url``. After changing the logo, close and reopen the POS so
the client reloads ``pos.config``.
39 changes: 39 additions & 0 deletions pos_config_logo/static/src/js/logo_src.esm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
function _guessImageMime(b64) {
if (b64.startsWith("/9j/")) {
return "image/jpeg";
}
if (b64.startsWith("R0lGOD")) {
return "image/gif";
}
if (b64.startsWith("UklGR")) {
return "image/webp";
}
return "image/png";
}

/**
* Build a receipt-safe src for the per-POS logo.
*
* 16.0 loaded ``/web/image?model=pos.config`` (relative) and converted it
* to a data URI via canvas (``config_logo_base64``) so the printed ticket did
* not depend on a later HTTP fetch. The 18.0 migration replaced that with
* ``web.base.url`` + ``/web/image?model=pos.config``, which returns Odoo's
* camera placeholder when the request has no session cookie (localhost vs
* 127.0.0.1, print preview). Company logos stay public; ``pos.config.logo``
* does not. Embed the binary already loaded into the POS, with a relative
* ``/web/image`` fallback (never an absolute ``_base_url``).
*/
export function posConfigLogoSrc(config) {
if (!config?.id || !config.logo) {
return false;
}
const raw = config.logo;
if (typeof raw === "string" && (raw.startsWith("data:") || raw.startsWith("/"))) {
return raw;
}
const b64 = String(raw).replace(/\s/g, "");
if (b64.length > 32) {
return `data:${_guessImageMime(b64)};base64,${b64}`;
}
return `/web/image?model=pos.config&id=${config.id}&field=logo`;
}
6 changes: 4 additions & 2 deletions pos_config_logo/static/src/js/models.esm.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {PosStore} from "@point_of_sale/app/store/pos_store";
import {patch} from "@web/core/utils/patch";
import {posConfigLogoSrc} from "@pos_config_logo/js/logo_src.esm";

patch(PosStore.prototype, {
getReceiptHeaderData() {
const result = super.getReceiptHeaderData(...arguments);
if (this.config.logo) {
result.config_logo = `${this.session._base_url}/web/image?model=pos.config&id=${this.config.id}&field=logo`;
const src = posConfigLogoSrc(this.config);
if (src) {
result.config_logo = src;
}
return result;
},
Expand Down
4 changes: 4 additions & 0 deletions pos_config_logo/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright 2026 Miguel Machado
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).

from . import test_pos_config
13 changes: 13 additions & 0 deletions pos_config_logo/tests/test_pos_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright 2026 Miguel Machado
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).

from odoo.tests import tagged

from odoo.addons.point_of_sale.tests.common import TestPoSCommon


@tagged("post_install", "-at_install")
class TestPosConfigLogo(TestPoSCommon):
def test_load_pos_data_fields_includes_logo(self):
fields = self.env["pos.config"]._load_pos_data_fields(self.basic_config.id)
self.assertIn("logo", fields)
Loading