Skip to content

[20.0][MIG] web_favicon - #3732

Open
kh-badep wants to merge 55 commits into
OCA:20.0from
BADEP:20.0-mig-web_favicon
Open

kh-badep wants to merge 55 commits into
OCA:20.0from
BADEP:20.0-mig-web_favicon

Conversation

@kh-badep

@kh-badep kh-badep commented Oct 18, 2026 •

Copy link
Copy Markdown

Migration

Migrate web_favicon to Odoo 20.0. The module provides company-specific favicon customization for backend and portal pages.

Testing

  • Automated: all four web_favicon test methods passed; Odoo reported 0 failures and 0 errors across 6 tests on a fresh test database.
  • Manual UI: updated the company favicon through the developer-mode company form, saved, navigated away, and reopened the company. The uploaded image persisted, and the page favicon link resolved to /web/image/res.company/1/favicon?unique=a054748.

Migration and first testing pass: Claude Opus 5.5.
Manual testing and PR preparation: Me + Copilot Auto.

hbrunn and others added 30 commits October 10, 2026 21:23
Currently translated at 100,0% (9 of 9 strings)

Translation: web-11.0/web-11.0-web_favicon
Translate-URL: https://translation.odoo-community.org/projects/web-11-0/web-11-0-web_favicon/pt_BR/
Updated by Update PO files to match POT (msgmerge) hook in Weblate.
Currently translated at 22.2% (2 of 9 strings)

Translation: web-12.0/web-12.0-web_favicon
Translate-URL: https://translation.odoo-community.org/projects/web-12-0/web-12-0-web_favicon/pt/
Currently translated at 100.0% (9 of 9 strings)

Translation: web-12.0/web-12.0-web_favicon
Translate-URL: https://translation.odoo-community.org/projects/web-12-0/web-12-0-web_favicon/zh_CN/
Currently translated at 100.0% (9 of 9 strings)

Translation: web-12.0/web-12.0-web_favicon
Translate-URL: https://translation.odoo-community.org/projects/web-12-0/web-12-0-web_favicon/pt_BR/
Currently translated at 100.0% (9 of 9 strings)

Translation: web-12.0/web-12.0-web_favicon
Translate-URL: https://translation.odoo-community.org/projects/web-12-0/web-12-0-web_favicon/ca/
Currently translated at 100.0% (9 of 9 strings)

Translation: web-12.0/web-12.0-web_favicon
Translate-URL: https://translation.odoo-community.org/projects/web-12-0/web-12-0-web_favicon/es/
Currently translated at 100.0% (9 of 9 strings)

Translation: web-12.0/web-12.0-web_favicon
Translate-URL: https://translation.odoo-community.org/projects/web-12-0/web-12-0-web_favicon/tr/
Currently translated at 88.8% (8 of 9 strings)

Translation: web-12.0/web-12.0-web_favicon
Translate-URL: https://translation.odoo-community.org/projects/web-12-0/web-12-0-web_favicon/de/
@kh-badep
kh-badep marked this pull request as draft October 18, 2026 21:07
@kh-badep
kh-badep force-pushed the 20.0-mig-web_favicon branch from f68dca7 to 7826064 Compare October 18, 2026 21:19
@legalsylvain

Copy link
Copy Markdown
Contributor

#3728 (comment)

@pedrobaeza

Copy link
Copy Markdown
Member

@legalsylvain I think they can be legit. For example, this one seems OK in a code review. I will reopen some of them and review them. They are not against our AI policy, as they are being reviewed after the AI pass and assuming the responsibility.

@pedrobaeza pedrobaeza reopened this Oct 19, 2026
@legalsylvain

Copy link
Copy Markdown
Contributor

Hi @pedrobaeza. I don't know ! The current AI Policy https://github.com/OCA/.github/blob/master/AI_POLICY.md#policy says "Accounts exhibiting bot-like behaviour - automated mass pull request creation or reviews - will be permanently banned, whether they belong to a human or not."

(proposed by @etobella here : OCA/.github#5)

This github account :

  • created 30 PR in a week-end against OCA org.
  • is flagged as " First-time contributor" against most of OCA repo.
  • In the 11 October 2026, it opened 689 PRs. (Ref : https://github.com/kh-badep)

Maybe it's a matter of interpretation, but for me 689 PR per day == "totally unsupervised spam".

@pedrobaeza

Copy link
Copy Markdown
Member

Yeah, of course that's something unsupervised, but it's not inside OCA. About OCA PRs, a couple of them that I have checked are not the typical AI slop, so this may worth to check.

@kh-badep

kh-badep commented Oct 19, 2026 •

Copy link
Copy Markdown
Author

@legalsylvain As I mentioned I did a first pass to migrate all OCA repos against my own account. Then I spent all my weekend reviewing modules one by one and opening a PR after manual testing and adjustment on each. That's why I left some of them as draft until I have time next weekend. This one for example was left as draft since it's failing some checks and needs updating the test cases.

I stopped contributing to OCA long ago since I know how things can be slow over here, understandably or not. e-g. OCA/social#425. I wasn't using this account either for the last 4 years since I tend to use separate accounts for each company I work for.

I'll stop creating PRs as requested. I'll be maintaining the forks instead for the time being.

@pedrobaeza

Copy link
Copy Markdown
Member

Khalid, the main thing is to not bring slop, but your contributions are welcome as the rest. You can also complement them reviewing other PRs for speeding up the merge process.

@kh-badep

Copy link
Copy Markdown
Author

I've been using a small diff script to keep an eye on #lines changes, then checking if non-zero. Usually it's the tests suites missing, others like OCA/account-analytic#988 can be explained by Odoo dropping the stock.scrap models.

The only time when I'm not checking the code thoroughly is when the diff is 0 (excluding indentation and formatting) or missing test cases. In all cases I'm functionally testing each module on a fresh database before submitting a PR.

@cristina-hidalgo-tecnativa cristina-hidalgo-tecnativa left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_get_favicon calls int() on the cids cookie, so cids=abc raises ValueError and breaks /web/login, the error page and /favicon.ico. Using self.env.company (already validated by Odoo) removes the parsing, and bin_size=True avoids loading the file on every render. The template should also keep core's x_icon first, which makes the website_id branch redundant.

Image Image Image Image

TT64891

Comment thread web_favicon/models/res_company.py Resolved
Comment thread web_favicon/models/res_company.py Resolved
Comment thread web_favicon/views/templates.xml Resolved
@oca-cla-bot

oca-cla-bot Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

The following commit authors need to sign the Contributor License Agreement:

CLA not signed

@kh-badep
kh-badep marked this pull request as ready for review October 28, 2026 18:35
@etobella

Copy link
Copy Markdown
Member

@remi-filament @tarteo @hbrunn @rov-adhoc can you check the CLA and resign with this emails 🙏

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_get_favicon no longer reads the cids cookie and uses self.env.company instead, so the tests must set the current company with with_company() instead of mocking the cookie.

test_04 still expects the first company of the cookie and fails ('16' not found in '/web/image/res.company/1/favicon...').

test_01 only passes by chance, and MockRequest is no longer needed. Asserting on /res.company/<id>/favicon is also stricter than assertIn(str(id), url), which any 1 in the URL satisfies.

Comment thread web_favicon/tests/test_web_favicon.py Resolved
Comment thread web_favicon/tests/test_web_favicon.py Resolved
Comment thread web_favicon/tests/test_web_favicon.py Resolved

@les-adhoc les-adhoc left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this PR (head 22167e6) on a local 20.0 database and compared it with 19.0, with real HTTP requests and the module tests (with and without website installed).

The main point is that the backend favicon no longer follows the active company (see the comment on _get_favicon). Details and suggestions inline.


import odoo
from odoo import api, fields, models, tools
from odoo.tools.binary import BinaryBytes

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed by the suggestion on _get_favicon below.

Suggested change
from odoo.tools.binary import BinaryBytes
from odoo.http import request
from odoo.tools.binary import BinaryBytes

Comment thread web_favicon/models/res_company.py Outdated
Comment on lines +69 to +72
"""Returns a local url that points to the favicon of the current company."""
company = self.env.company.sudo()
# bin_size avoids reading the whole file just to know if it exists
if not company.with_context(bin_size=True).favicon:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The favicon no longer follows the active company (regression from 19.0).

When Odoo renders an HTML page (/odoo, /web/login), it does not load the cids cookie into allowed_company_ids, so self.env.company is always the user's main company (odoo/orm/environments.py, company). Core reads the cookie by hand when it needs the active company in an HTML route (e.g. _get_active_company in addons/hr_attendance/controllers/main.py).

Tested with an admin user, main company 1, allowed companies 1 and B:

Request 19.0 20.0 (this PR)
/odoo, cids=B /web/image/res.company/B/favicon /web/image/res.company/1/favicon
/odoo, cids=B-1 /web/image/res.company/B/favicon /web/image/res.company/1/favicon

The int() problem is real (cids=abc gives a 500 in 19.0), but we think the fix is to read the cookie safely, not to stop reading it. We tested this suggestion on a local 20.0 database: cids=B and B-1 give company B, 1-B gives company 1, and abc or a company the user is not allowed in falls back to self.env.company.

Also, the bin_size context is not used anymore in 20.0 (no reference in odoo/), and reading company.favicon is already lazy, so the with_context and its comment can go.

Suggested change
"""Returns a local url that points to the favicon of the current company."""
company = self.env.company.sudo()
# bin_size avoids reading the whole file just to know if it exists
if not company.with_context(bin_size=True).favicon:
"""Returns a local url that points to the favicon of the active company."""
company = self.env.company
cids = request and request.cookies.get("cids")
first = cids.split("-")[0] if cids else ""
if first.isdigit() and int(first) in self.env.user._get_company_ids():
company = self.env["res.company"].browse(int(first))
company = company.sudo()
if not company.favicon:

Comment thread web_favicon/models/res_company.py Outdated
favicon = fields.Binary(
string="Company Favicon",
help="This field holds the image used to display favicon for a given company.",
default=lambda self: self._get_default_favicon(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: on install, all existing companies get the same favicon.

In 20.0, Binary.update_db fills existing records with the field default when the field has one (update_db_binary_attachment in odoo/orm/fields_binary.py), using one value for all of them. So all existing companies (except the main one, set by the XML) get the same favicon with the same color bar. In 19.0 they stayed empty. The create override already sets a favicon on new companies, so removing default= gives back the 19.0 behavior.

Suggested change
default=lambda self: self._get_default_favicon(),

Comment thread web_favicon/tests/test_web_favicon.py Outdated
Comment on lines +51 to +76
def test_03_website_favicon(self):
"""Test if favicon URL is correctly returned when website_id is in context."""
if self.env["ir.module.module"].search(
[("name", "=", "website"), ("state", "=", "installed")]
):
company = self.env["res.company"].create(
{
"name": "Test Company with Website",
}
)
website = self.env["website"].create(
{
"name": "Test Website",
"domain": "www.test.com",
"company_id": company.id,
}
)
website.favicon = website._default_favicon()
favicon_url = company.with_context(website_id=website.id)._get_favicon()
expected_favicon_url = website.image_url(website, "favicon")
self.assertEqual(
favicon_url,
expected_favicon_url,
"The favicon URL should match the expected value.",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_03 only runs when website is installed, which is not the case in CI. With website installed it fails, because the website_id branch was removed from _get_favicon. Since x_icon now goes first in the template, core covers the website case (website.layout sets x_icon), so this test can be removed.

Suggested change
def test_03_website_favicon(self):
"""Test if favicon URL is correctly returned when website_id is in context."""
if self.env["ir.module.module"].search(
[("name", "=", "website"), ("state", "=", "installed")]
):
company = self.env["res.company"].create(
{
"name": "Test Company with Website",
}
)
website = self.env["website"].create(
{
"name": "Test Website",
"domain": "www.test.com",
"company_id": company.id,
}
)
website.favicon = website._default_favicon()
favicon_url = company.with_context(website_id=website.id)._get_favicon()
expected_favicon_url = website.image_url(website, "favicon")
self.assertEqual(
favicon_url,
expected_favicon_url,
"The favicon URL should match the expected value.",
)

Comment thread web_favicon/tests/test_web_favicon.py Outdated
Comment on lines +77 to +104
def test_04_favicon_multiple_companies(self):
"""Test _get_favicon with multiple companies in cids cookie."""
Company = self.env["res.company"]

company_1 = Company.create(
{"name": "Company 1", "favicon": Company._get_default_favicon()}
)
company_2 = Company.create(
{"name": "Company 2", "favicon": Company._get_default_favicon()}
)
company_3 = Company.create(
{"name": "Company 3", "favicon": Company._get_default_favicon()}
)

with MockRequest(self.env) as mock_request:
mock_request.cookies = {
"cids": f"{company_1.id}-{company_2.id}-{company_3.id}"
}
favicon_url = Company._get_favicon()

self.assertTrue(
favicon_url, "Favicon URL should be generated for multiple companies."
)
self.assertIn(
str(company_1.id),
favicon_url,
"Favicon URL should correspond to the first company in the cids cookie.",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A test based on with_company() would pass even without the fix on _get_favicon, because a real request does not go through that path. MockRequest accepts cookies= in 20.0, so the test can keep checking the cookie.

With the _get_favicon suggestion applied, these tests pass on a local 20.0 database, with and without website installed. With the current code, test_04 fails.

Suggested change
def test_04_favicon_multiple_companies(self):
"""Test _get_favicon with multiple companies in cids cookie."""
Company = self.env["res.company"]
company_1 = Company.create(
{"name": "Company 1", "favicon": Company._get_default_favicon()}
)
company_2 = Company.create(
{"name": "Company 2", "favicon": Company._get_default_favicon()}
)
company_3 = Company.create(
{"name": "Company 3", "favicon": Company._get_default_favicon()}
)
with MockRequest(self.env) as mock_request:
mock_request.cookies = {
"cids": f"{company_1.id}-{company_2.id}-{company_3.id}"
}
favicon_url = Company._get_favicon()
self.assertTrue(
favicon_url, "Favicon URL should be generated for multiple companies."
)
self.assertIn(
str(company_1.id),
favicon_url,
"Favicon URL should correspond to the first company in the cids cookie.",
)
def test_04_favicon_active_company(self):
"""The favicon URL follows the first company of the cids cookie."""
Company = self.env["res.company"]
company_1 = Company.create({"name": "Company 1"})
company_2 = Company.create({"name": "Company 2"})
cookie = f"{company_2.id}-{company_1.id}"
with MockRequest(self.env, cookies={"cids": cookie}):
favicon_url = Company._get_favicon()
self.assertIn(f"/res.company/{company_2.id}/favicon", favicon_url)
def test_05_favicon_invalid_cookie(self):
"""An invalid or not allowed company falls back to the current one."""
Company = self.env["res.company"]
for cookie in ("abc", "999999"):
with MockRequest(self.env, cookies={"cids": cookie}):
favicon_url = Company._get_favicon()
self.assertIn(f"/res.company/{self.env.company.id}/favicon", favicon_url)

@kh-badep
kh-badep force-pushed the 20.0-mig-web_favicon branch from 22167e6 to 8f82cbf Compare October 31, 2026 16:16
Co-authored-by: Luciano Esperlazza <257575630+les-adhoc@users.noreply.github.com>
Assisted-by: Claude Opus 5.5
@kh-badep
kh-badep force-pushed the 20.0-mig-web_favicon branch from 8f82cbf to 8868906 Compare October 31, 2026 16:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.