Branch
[FIX] viin_brand_crm: depend on viin_brand_digest to fix install-order crash Installing modules in some environments could abort partway through with: ParseError: .../viin_brand_crm/data/digest_data.xml:5 ValueError: Invalid field 'active' on model 'digest.tip' digest_data.xml writes digest.tip.active, but that field is defined by the sibling module viin_brand_digest, not by viin_brand_crm or by crm. Both modules are auto_install: True with no dependency edge between them, so Odoo's loader has no ordering guarantee and can process viin_brand_crm before viin_brand_digest has added the field - observed on a real build aborting at module 72 of 126. Add viin_brand_digest to viin_brand_crm's depends so the loader is forced to install it first. This does not widen who gets auto-installed: core crm already depends on digest, so whenever crm is installed, digest is installed too, which means viin_brand_digest (depends ['digest'], auto_install) was already an install candidate in exactly the same situations. The new edge only constrains order, which is precisely what was missing. digest_data.xml itself is untouched - the data is correct; it was only running too early.
[IMP] viin_brand: own the module-icon and favicon branding mechanism Moved from to_base (tvtmaaddons), where it ran for every database served by a process that had viin_brand on its addons path and forced to_base to be loaded server wide. The functions, hooks, models (company favicon, settings-page icon RPC, menu web_icon rewrite, session marker), data, views, assets and the QUnit mock server come over unchanged apart from their imports; the uninstall hook now actually restores the core functions it replaced. viin_brand is the module to load server wide from now on (--load=base,web,viin_brand); it warns when it is not.
[FIX] web_responsive: dismiss the apps menu when a search result is chosen Searching the apps menu and choosing a result appeared to do nothing: the menus filtered, but clicking one or pressing Enter left the user where they were. Navigation was in fact succeeding. The controller changed and the URL moved to the target, with no JS error and every RPC returning 200 - but the full-screen overlay stayed mounted over the destination and, being position:fixed with pointer-events:auto, swallowed clicks on the view underneath. The overlay must be dismissed explicitly. When the apps menu became an overlay-service render independent of the action stack, dismiss() was added before selectMenu() on the app-icon path but not on the search paths, and core's selectMenu() only mutates the action stack. Three call sites needed it, not one: the canonical search bar (whose handler the Enter key also reaches, so click and Enter are one path), the fuse variant that inherits it, and the command-palette variant, which fails through a different mechanism because openMainPalette was passed no onClose. The command-palette path dismisses only when a menu was actually selected. onClose fires on every palette close, Escape included, so dismissing unconditionally would tear the apps menu down when a user merely backs out of the palette - trading one surprise for another. It also registers that callback once per open palette session: core's command service ignores onClose on every call after the first while its palette is open, so re-passing it per keystroke orphaned a patch layer on the session-lifetime menu service each time. The app-tile route, which every core tour uses to reach an app, was already correct and is untouched. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] viin_brand_common: withdraw the square-corners brand decision The product owner withdrew the "square corners are part of the Viindoo brand identity" decision. This module forced Bootstrap's global $border-radius to 0 in bootstrap_overridden.scss; drop that override so Odoo's own product-wide border-radius default applies again across buttons, inputs, cards, badges, dropdowns and modals. No sibling radius variable ($border-radius-sm/lg/xl/xxl) is touched - those are independent Bootstrap literals this module never overrode. viin_brand_square_corners_tour and its Python HttpCase driver (test_real_backend_buttons_keep_the_brand_square_corners) existed solely to guard the withdrawn contract, so both are retired together with it. Every other assertion in the same tour file and test class (brand primary colour, theme secondary colour, base font-size, statusbar outline/fill, discuss sidebar, settings tab) is untouched. Verified: before this change the retired assertion measured .btn border-top-left-radius = 0px (passing, as designed). After this change the full --test-tags /viin_brand_common suite is green (0 failed, 0 error, 0 skipped, 20/20), and a fresh computed-style probe measures .btn border-top-left-radius = 4px - Odoo core's own $o-border-radius default - confirming the product-wide change took effect. Bump manifest version 0.3.4 -> 0.3.5. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] web_responsive: stop leaking a patch layer per command-palette keystroke A prior fix on this branch (05b40f6) made the command-palette search variant dismiss the apps-menu overlay only when a menu was actually selected, using a temporary patch() on the menu service to detect the selection. That patch was re-applied on every keystroke, but core's command service silently drops the onClose callback on every call after the first while its palette stays open - so only the first keystroke's patch layer was ever unwound, and each keystroke after that permanently leaked one more wrapper layer onto the menu service for the rest of the browser session. Track whether an open-palette session is already being watched and only patch the menu service + register the dismiss callback once per session; later keystrokes in the same session now only update the palette's search value. Adds a Hoot test covering the character-by-character search-typing path (the one the prior tests bypassed) and trims two test comments per the repo's comment convention. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] web_responsive: restore mobile breadcrumb visibility On a mobile-width screen the navbar breadcrumb could be squeezed to zero width, so a user could not tell which record they were on, and mobile tours hung at the first breadcrumb step until the watchdog fired. The apps-menu control was rendering twice on small screens: the `t-else` branch was empty while the grid button sat outside it, so mobile paid for two controls doing one job. Move the grid button inside `t-else` so only the wide-screen layout renders it, matching the template's own one-or-the-other intent and freeing width back to the breadcrumb. The breadcrumb itself had no minimum width and was the only shrinkable element in the navbar, so every systray icon a module adds is paid for out of the breadcrumb, down to zero. Give it a floor on small viewports and let the systray shrink/scroll instead, so this whole class of defect - not just this one instance - becomes impossible. TOGGLE_HOME_MENU_BUTTON in the navbar dereferenced a ref that no longer renders on mobile; guard it so it is a no-op when the button is absent, reproducing the previous mobile behaviour exactly. Tests: assert the small-screen navbar exposes exactly one apps control, plus the wide-screen counterpart. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] web_responsive: pin primary-variables bundle order CPython randomizes string hashing per process (PYTHONHASHSEED), so a Python set literal used as an assets bundle value iterates in a different order every time the Odoo server process starts. The "web._assets_primary_variables" bundle was declared as a set of two SCSS paths, making its file load order nondeterministic across deployments and restarts. Switch the bundle from a set literal to a list literal, keeping the exact same two files in the exact same order they were already written in: form_variable.scss first, then primary_variable.scss. Verified safe to keep this order: form_variable.scss defines only $o-form-renderer-max-width and $o-form-view-sheet-max-width, while primary_variable.scss defines only $app-menu-text-color, $app-menu-background-color, $app-menu-item-highlight, $app-menu-box-shadow and $app-menu-box-shadow-highlight, all with !default. The two files' variable namespaces are fully disjoint in both directions, so there was no dependency-driven "correct" order to choose - this order is now a deliberate, deterministic choice rather than an accident of hash seeding. Protected by the repo-wide regression guard added in the immediately preceding commit (viin_brand/tests/test_manifest_assets_no_set_literals.py), which was measured RED against exactly this bundle before this fix, and is now confirmed GREEN together with this fix on a real Odoo 18.0 instance (db odoo_18_0_standard_viindoo_18_t_cbeabf2d): odoo.tests.result: 0 failed, 0 error(s) of 6 tests. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] viin_brand_onboarding: drop own SCSS from core's JS unit-test bundles viin_brand_onboarding was the one brand module the earlier four-module sweep missed. Its onboarding.scss reads five brand variables that only viin_brand_common defines - $brand-primary-light/-dark/-darker and $brand-secondary-light/-dark - and that web.assets_unit_tests_setup and web.tests_assets no longer carry now that viin_brand_common's own sweep removed them. So this leak did not merely pollute core's JS unit tests with Viindoo design tokens, it aborted the compile of the whole bundle with "Undefined variable". Remove this module's own stylesheet from those two TEST-ONLY bundles; web.assets_backend (the real webclient) is untouched and still serves the stylesheet with all 18 brand gradient declarations byte-identical. New tests guard both directions. Measured: before 7 failed of 21 tests (14 "Undefined variable" occurrences), after 1 failed of 21 (0 "Undefined variable"). The one remaining failure is unrelated and pre-existing: a sibling guard test that needs point_of_sale or hr_attendance installed to verify anything, and correctly refuses to pass vacuously when neither is. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] viin_brand_common: guard overrides out of public bundles Lock in the separation the previous commit established, so it cannot quietly rot. This module's overrides of core files belong to the backend webclient and must stay out of the point-of-sale terminal, the public mail page, the embedded livechat widget and the attendance kiosk. The guard asserts that in both directions: it also asserts the emoji-picker override is still deliberately shared with the public mail page and the livechat embed, so a future cleanup that mistakes those two intentional declarations for part of the leak gets caught instead of silently stripping brand coverage. It further asserts every override still reaches the webclient, so nobody can turn the suite green by deleting the overrides outright. Verified: 22 tests pass on a live database, and the four brand-appearance tours each still complete all three steps. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] web_responsive: guard responsive layout now unit tests carry no brand CSS The previous commits on this branch stripped web_responsive's own SCSS back out of core's Hoot/QUnit test bundles, which fixed core tests that were measuring this module's layout instead of core's own defaults - but also removed the only place a regression that silently drops this module's SCSS from the real webclient would have been noticed. Add test_brand_css_out_of_test_bundles.py (2 tests): assert web_responsive.scss is absent from web.assets_unit_tests_setup and web.tests_assets (what the prior commit fixed) while still present in web.assets_web, the bundle the real webclient is served from - so a future change cannot silently strip the responsive layout from production while the test bundles stay clean. The module marker is matched on the full SCSS-compiler comment path, not a bare module-name substring, since this database also carries viin_customizer_web_responsive whose own stylesheet legitimately sits in the test bundle and would otherwise report a false leak. Verified live on a real instance: baseline 0 failed of 18, an inversion that comments out this module's own ('remove', ...) manifest entry takes down this guard specifically, and a restore-verify pass returns to 0 failed with all tours succeeding.
[FIX] to_backend_theme: drop own SCSS from core's Hoot/QUnit bundles Both web.assets_unit_tests_setup (core's Hoot test-setup bundle) and web.tests_assets (the legacy QUnit bundle) carry ('include', 'web.assets_backend') and ('include', 'web.assets_backend_lazy'), declared in Odoo core's own addons/web/__manifest__.py. That pulled to_backend_theme's own backend-theme SCSS straight into core's own unit-test pages, so several of core's own tests were measuring to_backend_theme's styling instead of core's own defaults. Add explicit ('remove', <path>) entries for all 7 of this module's own SCSS files, spanning both source bundles (web.assets_backend / web.assets_backend_lazy), to both TEST-ONLY bundle keys in this module's own manifest. web.assets_backend and web.assets_backend_lazy themselves (the real webclient) are untouched - AssetPaths.remove() operates on the accumulated per-bundle path list, not on the source bundle that contributed it. Confirmed both statically (zero diff on either production bundle key) and live (the two test bundles now resolve zero of this module's SCSS paths while the production bundles still resolve all of them, unchanged, and both test bundles still compile clean with zero errors). Manifest-only change - this module has no test-page CSS shim to remove.
[FIX] web_responsive: fix apps-menu toggle dead-end and flash bug The apps-menu toggle button (button.o_grid_apps_menu__button) could go permanently dead. AppsMenuAction is registered under the "menu" client-action tag, so it can be mounted directly via doAction("menu"), bypassing the apps_menu service's own openMenu() (the overlay path) entirely. When that happens, the service's isOpening flag still turns true (AppsMenuScreen's mount effect fires unconditionally) but removeOverlay stays null, so the next click's closeMenu() finds nothing to remove and does nothing - the button stays dead until the action is dismissed some other way. A code comment claimed this path was "unreachable, no live path to test" - that was false: erponline-enterprise18/viin_customizer_web_responsive (auto_install: True) reaches it via its own isolated action service, and this module's own test suite already drives doAction("menu") through the real action service elsewhere. closeMenu() now falls back to the real "action" service: when the "menu" action is the current controller and something is underneath it (breadcrumbs already exclude "menu"-tagged controllers, so a non-empty array means a real prior screen), it restores that screen. With nothing underneath (a bare doAction("menu"), e.g. the boot fallback) the button stays a no-op by design - there is nothing to reveal. Separately, the previous commit's anti-flash fix (66a6a7d65d, passing `open` down as a prop so the app grid renders in the same cycle the screen mounts) was silently discarded for any upgraded user. AppsMenu.setup() called useState() a second time whenever session.apps_menu.is_redirect_home is true, recomputing `open` from the router's menu_id and overwriting the prop entirely. migrations/18.0.1.0.8/post-migration.py backfills is_redirect_home=True for every upgraded user with no action_id - the dominant real configuration, not an edge case - so any such user landing on a non-root menu (menu_id != 0) got the one-frame empty-overlay flash back. Collapsed to a single useState() call: the prop wins whenever the caller passes it; the is_redirect_home fallback only applies when AppsMenu is mounted without that prop. Also removed a dead `browser.localStorage.setItem("redirect_menuId", "")` write (and its now-unused import) - its only reader, onMenuClick, was deleted by 66a6a7d65d; no other reader exists anywhere across branding18, tvtmaaddons18, or erponline-enterprise18. Updated a stale test comment describing the retired bus-round-trip open mechanism, and dropped a stray blank line. Bumped the module version. Two RED-before-green Hoot tests were added protecting each fix, both confirmed failing on the pre-fix code and passing after, live on Hoot desktop preset: - "clicking the navbar apps button dismisses the menu action too, not only the overlay" - "the is_redirect_home branch never overrides the open prop AppsMenuScreen always passes" Before: filter=@web_responsive/apps_menu - 17 tests, 15 passed / 2 failed (both new tests red on the exact predicted assertions). After: same filter - 17/17 passed. Full @web_responsive tag - 17/17 passed. Also manually verified the ordinary overlay open/close flow live in the browser - no regression.
[FIX] web_responsive: reach the apps menu the user actually opens e2e5205 split the apps-menu screen into two sibling classes - AppsMenuAction (still registered under the "menu" actions tag, the class third-party modules patch by name) and AppsMenuOverlay (what appsMenuService.toggleMenu() actually mounts for the user, via the overlay service). Because the overlay was a SIBLING of AppsMenuAction rather than a descendant, a module that does patch(AppsMenuAction.prototype, {...}) and separately extends the shared "web_responsive.AppsMenuAction" template by name - the real, shipped shape in erponline-enterprise/viin_customizer_web_responsive - never reached the class that renders for the user. Runbot build 224030 failed 71 of 83 tests with "TypeError: ctx.getAppCreatorItem is not a function at AppsMenuOverlay.slot2": every tour that opens the apps menu died. Make AppsMenuOverlay extend AppsMenuAction instead of the shared AppsMenuScreen base, so a prototype patch() applied to AppsMenuAction - by this module or any third party, in either load order - is inherited by the overlay too. Corrected what plain inheritance would otherwise get wrong on the new subclass: static props (an overlay's shape, not the action-service props it would otherwise inherit), coversNothing (the overlay has no env.config to read breadcrumbs from, so it falls back to the base class's own resolution), and left static target/displayName inherited-but-inert with a comment so a future reader does not "clean them up" and reopen this. Documented at the class declarations that AppsMenuAction is both the "menu" action- tag registrant and the patch target other modules reach for by name, so the next module that legally patches this class does not silently break again. This repairs a regression we shipped, already merged into 18.0 (origin/18.0 @ 2cfee94 contains e2e5205). The suite was NOT executed here: this fix was produced under a static-verification-only constraint (no instance, no odoo-bin, no database, no test run). RED was argued from source - traced against e2e5205's diff and the real external patcher/template-inheritor - and the fix confirmed by static reasoning over the JS prototype chain (AppsMenuOverlay.prototype's [[Prototype]] is a live reference to AppsMenuAction.prototype, so a patch() mutation to the latter is visible to overlay instances). Syntax-checked with node --input-type=module --check; no reachable project ESLint config to run further here - that gate runs later at the run's own pre-PR tail.
[FIX] viin_brand_common: stop prepending brand to every tab title Every real page title got "Viindoo - " prepended to it, because WebClient.setup() injected a PERMANENT titleParts entry (zopenerp: "Viindoo") whenever session.viin_brand is set. Core only ever composes the literal "Odoo" as the EMPTY-CASE FALLBACK - Object.values(titleParts).join(" - ") || "Odoo" in web/static/src/core/browser/title_service.js - it never appears beside a real title. website's SEO dialog rendered "Viindoo - Hello, world!" instead of "Hello, world!", failing core's own test_website_event_pages_seo tour (runbot 223907, target G1 of run tour-reenable-14456). Remove the permanent setParts() injection - webclient.js had no other purpose, so it is deleted - and instead patch core's "title" service itself (new static/src/core/browser/title_service.js) so ONLY the empty-case fallback is rebranded: core computes the title exactly as it always does, and only when core's own getParts() shows titleParts is empty does the patch replace the trailing "Odoo" with "Viindoo". A real title composed by core is never touched. Still gated on the server-stamped session.viin_brand marker (introduced by f35f41c) so core's own QUnit/Hoot suites, whose mock sessions lack the marker, keep asserting the plain "Odoo" title. NOT verified against a live Hoot/QUnit run: this environment has no spare RAM to build an instance (a foreign run owns the only live one). RED-today / green-after was argued by reading title_service.js's composition and hand-tracing titleParts for both scenarios in the companion test, never executed.
[FIX] web_responsive: drop the statusbar DropdownItem hotkey override DropdownItem no longer accepts a hotkey prop on 18.0, so grafting hotkey='shift+a' onto every statusbar dropdown item crashed the whole statusbar with an OwlError on small screens (where the buttons collapse into that dropdown). The override was pointless anyway — it assigned the same hotkey to every item.
[FWD] forward-port 17.0 -> 18.0 (c676d5a..2fe943f, 8 commits) Single merge of origin/17.0 into 18.0. Every source commit enters the DAG with its own SHA intact, so the merge-base advances to 17.0's tip and none of these conflicts is ever re-resolved on a future run. Ported: 6aef9e0 8069ccc 221946b 4c0d67c f35f41c 0d6646d 33a11ec 2fe943f FOUR THINGS WOULD HAVE MERGED GREEN AND BEEN WRONG AT 18.0. Each is invisible in the diff, so they are named here. 1. 221946b's guard is deliberately NOT taken. Source gates _loadDefaultApp on registry.category("actions").contains("apps_menu"). At 18.0 the action registers under the tag "menu", so that condition is permanently false and would have silently disabled the apps-menu redirect for every user. The target already reaches the same no-throw outcome via its session marker. 2. 4c0d67c's isCheck does not exist at 18.0. The tour engine was rewritten; StepSchema declares no isCheck and validates through Owl validate(). `run` is optional-typed, so an explicit undefined is not the same as absent - the key is deleted, not set. 3. The AppsMenu tests are re-implemented on Hoot, not ported. 18.0 replaced QUnit and all three of this module's suites imported a helper 18.0 core does not ship - they were already dead on this branch. 18.0 also passes a BARE STRING to doAction("menu"), so a literal port of `step(`do-action:${action.tag}`)` reads .tag off a string, records "do-action:undefined", and passes while asserting nothing. The new tests step on the argument itself. 4. One CSS reset was not enough. 17.0 had one test page; 18.0 runs two. Both resets are declared under web.tests_assets AND web.assets_unit_tests. EVIDENCE. A/B on this tree - identical suite, brand absent from the addons path vs brand installed with the resets absent - returned 0 failures against 28, all pixel-geometry (11x column_widths, 4x daterange, 1x datetime, draggable, scroller, nested_sortable, 7x html_editor, 1x mail). With the resets in place that drops to 6, all in @html_editor/*. The 17.0 runbot ids and counts were deliberately not carried into the code - 18.0's suite has different names and numbers, and quoting the old ones would be fabricated evidence. Also verified on a live 18.0 build rooted on this tree: 10/10 new Hoot tests pass, 26/26 Python tests pass across the four modules, and WebSuite.test_check_suite now passes - it was erroring on 18.0 before this work, which is what 2fe943f fixes. TWO CHANGES BEYOND THE EIGHT COMMITS, both deliberate: - viin_brand_website's x_icon QWeb override is removed and replaced by a data-level default on website.favicon. The override was a position="replace" that, because locate_node resolves an xpath to the first document-order match, destroyed website's own favicon producer - so a customer who configured a favicon could never serve it. Removing it alone would have left new websites unbranded, so the brand image is now the field's default instead: branded out of the box, and the customer's own value survives once set. - Three local fixes were needed to make the ported tests actually run at 18.0: defineMailModels() for mail's mock models, componentEnv for env.config on a direct mount, and /web/offline -> /odoo/offline after core's route rename. KNOWN GAPS, not hidden: the design doc's test_asset_isolation.py leak-guard was not written; 6 @html_editor failures remain (brand-caused, pre-existing on 18.0, outside what the 17.0 commits addressed); test_qunit_desktop fails on viin_web_gantt, a module in a different repo; the colorpicker tour was adapted statically and not executed; cluster-wide acceptance was not run.
[IMP] viin_brand_website_sale: drop unverifiable "#1" claim from promo The eCommerce "Powered by" footer stated "The #1 Open Source eCommerce Software" (vi_VN: "... Mã nguồn mở số 1"), an unverifiable superiority ranking. Vietnam's advertising law forbids "#1"/"best" style claims without supporting evidence and penalises them heavily, so remove the ranking wording from both the template source and the Vietnamese translation. - views/templates.xml: drop the "#1" from the promo label and the standalone "The" text node so the label reads "Open Source eCommerce Software"; add a comment at the label noting the VN advertising-law constraint. - i18n/vi_VN.po and .pot: realign msgid to the new source string and drop the now-orphaned "The" term; msgstr "... Mã nguồn mở số 1" -> "... Mã nguồn mở". - bump module version 0.1 -> 0.2 so existing databases pick up the updated view and translation on upgrade.
[FIX] viin_brand_mail: restore the test bot identity on update, not only on install data/res_partner_data.xml renames base.partner_root to ViindooBot and is not noupdate, so the rebranding is re-applied on EVERY module update. The guard that puts core's OdooBot identity back for test runs lived in post_init_hook, which odoo/modules/loading.py:244 calls only when the module's state is 'to install' (:178). The two were asymmetric - branding on every load, restore on the first one only - so any -u left the partner branded and every core test hardcoding odoobot@example.com failed. google_calendar alone contributed 12 of them, since the superuser is the implicit event organiser in its Odoo-to-Google payload comparisons. The restore now lives on res.partner and is driven from the same data file that applies the branding, so both are on one trigger; a function tag runs on update as well as install (odoo/tools/convert.py:279-281 skips it only under noupdate). post_init_hook is kept as a one-line delegation so the logic has a single home.
[FIX] viin_brand_common: restore core's QUnit leak-guard on web.layout viin_brand_common anchored its web.layout brand title/favicon defaults on //title and //link[@type='image/x-icon']. Core's WebSuite._check_only_call (web/tests/test_js.py:104) rewrites web.layout's whole arch_db to a stub containing neither node, and that write revalidates every extension of web.layout. The recombination raised a ValidationError, leaving core's QUnit leak-guard - the guard that stops a stray QUnit.only() from silently disabling the whole JS suite - dead on every Viindoo 17 database. Replace the two position="attributes" xpaths with a single <xpath expr="//head/meta[last()]" position="after"> that sets both values as `or` fallbacks. {head, meta[@charset]} is exactly the node set present in both the real arch and core's stub, and meta[last()] is the latest reachable point in it, so the defaults bind after every upstream producer - notably website.layout's per-page title, its seo_object assignment and the OpenGraph block - and before <title>. An anchor placed before <head> instead lands outside it and pre-empts those producers. Bump the manifest version 0.3.1 -> 0.3.2: Viindoo SaaS auto-upgrade selects modules to -u by diffing the on-disk manifest version against the customer database's ir_module_module.latest_version, and a view change only reaches an existing database through -u. Without the bump the fix would reach fresh installs only. Add regression coverage: viin_brand_common/tests/test_web_layout.py (4 methods) locks the stub survival and the brand defaults; viin_brand_website gains tests/test_website_title_not_preempted.py (3 methods) owning the cross-module contract that the brand default never pre-empts a website title or its OpenGraph meta. Both suites were proven RED - the first against 33a11ec, the second against acc5474. Close https://github.com/Viindoo/branding/issues/654 Signed-off-by: David Tran <davidtran.hp@gmail.com>
[FIX] viin_brand_common: anchor web.layout xpaths on //head, not //title/link Core's WebSuite.test_check_suite (web/tests/test_js.py::_check_only_call) replaces web.layout's entire arch_db with a minimal stand-in arch that has a <head> but no <title>/<link>, to validate no stray QUnit.only()/ QUnit.debug() was left in the JS test suite. Writing that stand-in fires ir.ui.view's arch_db constraint, which recombines every view inheriting web.layout - viin_brand_common's two xpaths (anchored on //title and //link[@type='image/x-icon']) cannot locate their anchor against the stand-in, raising ValidationError and leaving core's QUnit-guard dead on every Viindoo 17 database. Replace both element-anchored xpaths with a single xpath anchored on //head (which the stand-in does provide), inserting two <t t-set> nodes as previous siblings of <head> that set the `title`/`x_icon` QWeb variables to their Viindoo defaults via an `or` fallback chain (`title or 'Viindoo'`, `x_icon or '/viin_brand/static/img/favicon.ico'`). Core's own <title t-esc="title or 'Odoo'"/> and favicon <link t-att-href="x_icon or '/web/static/img/favicon.ico'"/> then pick up those pre-set values unchanged, so exactly one <title> renders and an explicit caller-supplied title/x_icon still wins. Bump version 0.3.1 -> 0.3.2 so existing databases reload arch_db on upgrade. Add tests/test_web_layout.py (wired into tests/__init__.py) with 4 HttpCase regression tests covering: survival of core's exact stub write (RED before this fix), default title/favicon still brand correctly with exactly one <title>, an explicit title still wins, and an explicit x_icon still wins. All 4 assert on rendered HTTP output, never the arch string. Close https://github.com/Viindoo/branding/issues/654
[IMP] viin_brand_website_sale: drop unverifiable "#1" claim from promo The eCommerce "Powered by" footer stated "The #1 Open Source eCommerce Software" (vi_VN: "... mã nguồn mở số 1"), an unverifiable superiority ranking. Vietnam's advertising law forbids "#1"/"best" style claims without supporting evidence and penalises them heavily, so remove the ranking wording from both the template source and the Vietnamese translation. - views/templates.xml: promo label "The #1 Open Source eCommerce Software" -> "Open Source eCommerce Software"; add a comment at the label noting the VN advertising-law constraint. - i18n/vi_VN.po and .pot: realign msgid to the new source string; msgstr "... mã nguồn mở số 1" -> "... mã nguồn mở". - bump module version 0.1 -> 0.2 so existing databases pick up the updated view and translation on upgrade.
[IMP] viin_brand_website_sale: drop unverifiable "#1" claim from promo The eCommerce "Powered by" footer stated "The #1 Open Source eCommerce Software" (vi_VN: "... mã nguồn mở số 1"), an unverifiable superiority ranking. Vietnam's advertising law forbids "#1"/"best" style claims without supporting evidence and penalises them heavily, so remove the ranking wording from both the template source and the Vietnamese translation. - views/templates.xml: promo label "The #1 Open Source eCommerce Software" -> "Open Source eCommerce Software"; document the inherited replace with a comment. - i18n/vi_VN.po and .pot: realign msgid to the new source string; msgstr "... mã nguồn mở số 1" -> "... mã nguồn mở". - bump module version 0.1 -> 0.2 so existing databases pick up the updated view and translation on upgrade.
[FIX] web_responsive: core checkbox metrics inside QUnit pages The 'big checkboxes' restyle (+10px margin-right, 1.5em box) widens the list record-selector column, shifting core pixel-width assertions by exactly those pixels (runbot 223235/396399: ListView 'column widths should depend on the content when there is data' 41->51 and 'editable list: resize column headers' 52->51). Load a web.tests_assets-only reset that pins the test page back to core metrics; the real webclient keeps big checkboxes.
[FIX] viin_brand_common: apply brand palette/title only in genuine sessions; core metrics in QUnit Three brand client tweaks broke core QUnit suites: the unconditional colors.getColor override failed 30 GraphView tests asserting the default palette; the 'Viindoo' window-title part failed 3 ActionManager title tests; and the 15px base font shifted pixel-geometry assertions (Draggable confinement, Scroller offsets, tour anchors) by fractions of a pixel (runbot 223219/396221 & 223235/396399). session_info now stamps a viin_brand marker: real sessions keep the brand palette and title, mock test sessions (marker absent) fall through to core behavior. A web.tests_assets-only CSS reset pins the test page back to the 14px core base font - the real webclient is untouched. Also fix the missing '#' on the last palette color (was an invalid CSS color).
[FIX] viin_brand_website: end the colorpicker tour override with a pure check step The override dropped the final RGBA assertion entirely, leaving the last step action-less -- web_tour warns "ignoring action (auto) of last step" on every run. Relax the step to a pure check (isCheck on .o_rgba_div) instead of dropping it: the tour still ends on a verification, only the hard-coded rgb assertion the brand palette invalidates is gone. Verify: /website:TestUi.test_32_website_background_colorpicker 0 failed, 0 "ignoring action" warnings (clean CE and v17_full).
[FIX] viin_brand_common: keep statusbar arrows compact so 4 stages fit on mobile The brand raises $o-font-size-base to 15px (core default is 14px). That extra width on the status-flow arrows is enough that a 4-stage statusbar no longer fits one row on small screens: adjustVisibleItems detects the wrap and folds one arrow into the overflow dropdown. Core's mobile web StatusBarField QUnit tests assert every stage renders as an inline arrow (4 visible, a single "..." toggle, one disabled dropdown item), so they failed 3/... under the branded assets while passing on clean CE. Root-caused on v17_full by instrumenting core areItemsWrapping(): branded arrows measured 65px wide / 34.5px tall vs clean CE's 50-61px / 33px, tipping 4 arrows over the container width. Scope the arrows to $o-font-size-base-small (13px) - the framework's compact control size - so they match core's geometry and all four stages stay inline. Only the arrow chrome shrinks; stage labels remain legible. Verified on v17_full: web MobileWebSuite.test_mobile_js 0 failed (the three StatusBarField "small devices" / "extra small screens" / "clickable ... on mobile" cases now pass).
[FIX] web_responsive: restore disabled binding on chatter Log Note button The web_responsive.Chatter template replaces the Log Note button but dropped its t-att-disabled binding (the sibling Send message button kept it). This left Log Note always enabled regardless of posting permission, breaking Odoo core's project_chatter_log_disabled tour (project/tests/test_project_ui.py), which logs in as a limited Project User and asserts the button is disabled. Restore the same disabled expression used by the Send message button.
[FIX] viin_brand_pos: guard company service in POS Navbar favicon patch The POS Navbar patch read this.env.services.company.currentCompany.id unconditionally in setup() to set the branded favicon. The company service is not registered in the bare point_of_sale QUnit test env, so this.env.services.company was undefined and .currentCompany threw "Cannot read properties of undefined (reading 'currentCompany')" in Navbar.setup -> the whole POS Chrome mount failed, failing point_of_sale:WebSuite.test_pos_js ("mount the Chrome"). Optional-chain the service and bail out when there is no current company (no branded favicon to set). Real POS is unaffected. Verified: point_of_sale:WebSuite.test_pos_js passes (0 failed) on a focused point_of_sale+viin_brand_pos install with the guard.
[FIX] viin_brand_website: adapt website_background_colorpicker tour to branded palette viin_brand_common recolours the frontend theme palette (primary #7f4282), so the core website_background_colorpicker tour no longer holds: its custom-colour step targets a hard-coded swatch background-color:#65435C (= rgb(101,67,92)) that the recoloured palette never produces, and the following RGBA check asserts that exact rgb. That breaks website: TestUi.test_32_website_background_colorpicker. Re-register the tour (force) in web.assets_tests with the custom-colour step adapted to pick whatever the first custom swatch is, and drop only the hard-coded custom-colour rgb assertion. The gradient steps (a fixed preset) and their RGBA check are kept, so the tour still exercises the colorpicker.
[FIX] viin_brand: repoint hr_contract link to the canonical 17.0 store URL to_base overrides a core module's manifest 'website' from this branding dict at load time (_get_brand_module_website -> _load_manifest_plus), so this value is what a Viindoo deployment actually displays for the core hr_contract module - it is shipped, not inert. The hr_contract entry was wrong twice over: it used the legacy /apps/app/ prefix (now only a permanent 301 shim), and it pinned version 15.0 inside the 17.0 branch, so 17.0 users were sent to a four-series-old store page. viin_hr_contract is published on the store at 17.0, so the canonical target resolves directly. The remaining entries are deliberately left alone: they map core Odoo modules to Viindoo marketing intro pages, which is the correct destination - a core Odoo module has no Viindoo store page under its own name. hr_contract is the only store link in this dict, and the only one that was stale.
[FIX] web_responsive: don't break core component tests when apps_menu is absent The NavBar and WebClient patches called useService("apps_menu") and the menu searchbar read session.apps_menu.search_type. Isolated core component tests (NavBar, BurgerMenu, mail mobile) do not start apps_menu nor set session.apps_menu, so these threw as soon as web_responsive is installed ("Service apps_menu is not available", "Cannot read properties of undefined (reading 'search_type')"). Read apps_menu from env.services (optional), fall back to super._loadDefaultApp(), and use session.apps_menu?.search_type.
[FIX] viin_brand_mail: fix chatter contrast, harden dark edited marker DARK: the mail chatter embedded in the Odoo 19 spreadsheet editor's comments side panel was unreadable. Odoo keeps the spreadsheet a light island inside a dark app (its dark-bundle-only o_spreadsheet_extended.dark.scss forces .bg-white back to white and pins .btn to a dark label), but core repaints only the button label, not the fill, leaving half of every pair in the wrong colour scheme. Finish that light island for the embedded chatter instead of darkening it, scoped entirely under .o-spreadsheet. Measured live at 1440px in dark mode, before -> after: Log note 1.19 -> 7.92, Activity 1.19 -> 7.92, Send message 2.17 -> 8.24, author name 2.63 -> 10.31, timestamp 2.59 -> 6.11, message body -> 9.25. The same two message nodes the island fix above touches - the empty-message placeholder (3.37:1 worst) and the (edited) marker (4.04:1 worst) - were unreadable everywhere OUTSIDE that island too, because core dims both a second time on top of a tier already tuned to clear AA once. Fix them product-wide instead of leaving the island as the one readable spot: every form-view chatter, the Discuss thread canvas and every chat window, on the panel and on all three message-bubble tints. With that rule in place the island's own placeholder correction became a pure duplicate of it and was dropped. The island's (edited) rule is kept and hardened with its own colour, because the product-wide fix lands that marker on the dark metadata tier, which measures 2.34-2.59:1 on the island's light bubbles - too dark for that ground. LIGHT: the mail message muted tier renders below AA wherever it lands - 4.06:1 on a message bubble, 4.20:1 on the chatter panel - and two nodes are dimmed further by opacity utilities to 2.7:1 and 2.4:1. The shared $text-muted token is deliberately left untouched: it also paints off/disabled affordances that WCAG exempts, and repointing it would change roughly 380 nodes across 205 templates. Re-point the tier for mail message surfaces only. After: 5.48-6.11 across the panel and all bubble variants. Dark stays provably unaffected, since the light rules compile to zero bytes in the dark bundle. Also drop a rule that went dead once the comments panel lost its background utility - it was this module's only reference to another repo's class. A comment on the read-conversation muted tier said its --secondary-color token "does not flip" in dark - true only while $o-main-color-muted and $body-secondary-color were two independent Sass values. dark_palette.scss has since aliased the former onto the latter, so both names now resolve to the one tier this rule already reads: the rule was right, the explanation had gone stale. Reworded to name the tier through its own alias chain instead of restating a divergence that no longer exists. Guard every case above with tests that assert computed WCAG ratios, never colour values. DARK EDITED MARKER, HARDENED: the product-wide dark rule just added above (.o-mail-Message-edited .opacity-50 in mail_dark.scss) read the bare Bootstrap Sass global $text-muted, which has no !default guard, so any module sharing the web.assets_web_dark compile can reassign it from under this one. Core's hr_skills does exactly that. hr_skills/__manifest__.py:53 wildcard-globs 'hr_skills/static/src/scss/*.scss' into web.assets_backend, separately from the intended web.report_assets_pdf entry on line 68; that glob also catches report_employee_cv.scss, whose line 3 hard-assigns $text-muted: #3b4757 with no !default; and the bundle chain web.assets_web_dark -> web.assets_web -> web.assets_backend carries that value in. So whenever hr_skills is installed alongside the branding modules (always true on Runbot, which installs the whole repo) the marker compiled to the wrong colour and failed WCAG AA contrast (~1.3-2.0:1, needs >= 4.5:1). A one-variable install-topology experiment proved it: same commit, same suite - the three branding modules installed alone compiled $text-muted to #8EA5A8 and passed all 147 tests; adding hr_skills to the install set, nothing else changed, compiled it to #3b4757 and failed 12 subtests. That asymmetry is exactly why the branch stayed green on a local, branding-only run while Runbot - which always installs the whole repo - was red on the same code. Re-point the rule to var(--secondary-color) - the CSS custom property Bootstrap emits from $body-secondary-color (a different Sass variable, hard-assigned in dark_palette.scss:76 with no !default, which hr_skills' write never touches) - matching an existing pattern already used twice in this file (:56 and :245) for the analogous chatter-timestamp / notification-item cases. Updated the neighbouring comment to match, since it previously explained the value in terms of $text-muted. Added a regression guard, test_dark_edited_marker_stays_readable_when_another_module_hijacks_text_muted, that splices hr_skills' exact clobbering literal into the real web.assets_web_dark bundle source and recompiles through Odoo's own AssetsBundle.compile_css / ScssStylesheetAsset.compile (libsass), proving the rule survives a hijacked $text-muted regardless of which modules happen to be installed on whatever DB runs this module's own suite (no cr.commit(), no real hr_skills install needed). Verified RED before (12 genuine assertion failures on a live instance with hr_skills installed) and GREEN after (0 failed, 0 errors across 187 tests in viin_backend_theme + viin_brand_web + viin_brand_mail, same instance, hr_skills genuinely installed). Also fixes a flake8 E303 (too many blank lines) left behind while extending the compiled-CSS test suite above. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] viin_brand_mail: fix chatter contrast in light and dark schemes DARK: the mail chatter embedded in the Odoo 19 spreadsheet editor's comments side panel was unreadable. Odoo keeps the spreadsheet a light island inside a dark app (its dark-bundle-only o_spreadsheet_extended.dark.scss forces .bg-white back to white and pins .btn to a dark label), but core repaints only the button label, not the fill, leaving half of every pair in the wrong colour scheme. Finish that light island for the embedded chatter instead of darkening it, scoped entirely under .o-spreadsheet. Measured live at 1440px in dark mode, before -> after: Log note 1.19 -> 7.92, Activity 1.19 -> 7.92, Send message 2.17 -> 8.24, author name 2.63 -> 10.31, timestamp 2.59 -> 6.11, message body -> 9.25. The same two message nodes the island fix above touches - the empty-message placeholder (3.37:1 worst) and the (edited) marker (4.04:1 worst) - were unreadable everywhere OUTSIDE that island too, because core dims both a second time on top of a tier already tuned to clear AA once. Fix them product-wide instead of leaving the island as the one readable spot: every form-view chatter, the Discuss thread canvas and every chat window, on the panel and on all three message-bubble tints. With that rule in place the island's own placeholder correction became a pure duplicate of it and was dropped. The island's (edited) rule is kept and hardened with its own colour, because the product-wide fix lands that marker on the dark metadata tier, which measures 2.34-2.59:1 on the island's light bubbles - too dark for that ground. LIGHT: the mail message muted tier renders below AA wherever it lands - 4.06:1 on a message bubble, 4.20:1 on the chatter panel - and two nodes are dimmed further by opacity utilities to 2.7:1 and 2.4:1. The shared $text-muted token is deliberately left untouched: it also paints off/disabled affordances that WCAG exempts, and repointing it would change roughly 380 nodes across 205 templates. Re-point the tier for mail message surfaces only. After: 5.48-6.11 across the panel and all bubble variants. Dark stays provably unaffected, since the light rules compile to zero bytes in the dark bundle. Also drop a rule that went dead once the comments panel lost its background utility - it was this module's only reference to another repo's class. A comment on the read-conversation muted tier said its --secondary-color token "does not flip" in dark - true only while $o-main-color-muted and $body-secondary-color were two independent Sass values. dark_palette.scss has since aliased the former onto the latter, so both names now resolve to the one tier this rule already reads: the rule was right, the explanation had gone stale. Reworded to name the tier through its own alias chain instead of restating a divergence that no longer exists. Guard every case above with tests that assert computed WCAG ratios, never colour values. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] viin_backend_theme: restyle core without rewriting it A theme may change how Odoo looks. This one had begun changing what Odoo says, what it does, and how it describes itself to assistive technology - and runbot batch 223955 was where that came due. The theme case-transformed two surfaces it does not own: core's command-palette category block and the list view's column headers. Rendered text is what Odoo's own automated checks, tour assertions, screen readers and a user's copy-paste all read back, so a purely cosmetic rule turned 102 core checks red at once ("Command#" read back as "COMMAND#"). Core already presents the palette's category label in capitals itself, so ours added nothing. It was also a defect users could see: the class it targeted is the block holding every command row, not the label, so its muted colour, 11px size, weight and letter-spacing were inherited by every row in the palette. Aiming the rule at the label alone restores the rows to core's typography. Odoo ends every font stack it ships with two safety nets: a small Noto subset for characters a system font lacks or renders badly, and the four emoji families. This theme replaced Odoo's stacks with its own and dropped both nets from headings and from body text, so an emoji or an uncommon script in ordinary backend text - a customer name, a chatter message, a product label - could land on a system with nothing able to draw it and render as an empty box. Both stacks are whole again and now end identically. The brand heading font was also reaching into the rich-text editor, where an outgoing email body inlines whatever font it is shown in, handing the recipient a Viindoo face their mail client does not have and which is not even vendored yet. Email bodies go back to the portable system stack; the backend keeps Montserrat. Clicking the navbar apps icon threw away whatever the user was working on. The icon opened the home menu as a full page, so an open record, a half filled form or a kanban the user had just filtered was torn down and had to be rebuilt on the way back. Odoo's own contract for that icon is that it shows the app list without navigating, and a large part of Odoo's test suite is written on that assumption: 57 test files click it, some to pick an app, others merely in passing before carrying on with the screen they were already on. The latter could not work here at all, and the former could not either, because our tiles were not the kind of element those tests look for. The icon now shows the same full-screen home menu over the untouched view. Pressing it again puts it away and the user is back where they were - instantly, because nothing was ever destroyed - and so does clicking the page behind it or pressing Escape, which the full-page version offered no way to do. The design is unchanged: one icon, one home menu, and the landing page shown at sign-in is untouched. App tiles are now real links, as they are in stock Odoo, so they can be opened in a new tab, copied, or announced sensibly. They had carried an explicit role telling a screen reader each tile is an item in a list, never that it is something the user can follow; because an explicit role replaces an element's own rather than adding to it, the tiles were also absent from links navigation, one of the main ways people using a screen reader move through a page. That role is gone, and the grid's own list role went with it, since a list is required to contain list items and keeping one alone would have left an empty, malformed list rather than a fix. Wrapping each tile to keep both was rejected: the tiles are laid out directly by the grid and are the elements the drag-to-reorder handles, so a wrapper breaks the layout and the reordering unless hidden by a CSS trick with its own record of removing elements from the accessibility tree. Nothing on screen changes, and keyboard navigation and drag-to-reorder are unaffected. Guards now fail the build if this theme case-transforms anything it did not author, if either font stack loses its fallback, or if a tile stops being announced as a link; the font guard reads the value the browser is actually handed rather than the source. One existing check was inverted rather than weakened: it asserted the previous view was gone after clicking the apps icon, which is the behaviour being fixed, and now asserts the view survives. Owner decisions D1, D2 and D3, 2026-08-17. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] viin_brand_base_setup, viin_brand_web: split the catalogue to follow the artefacts A stale #: reference silently DROPS the translation - the loader builds ir_model_data keys straight out of the occurrence string, finds no row, and skips the entry with no error and nothing red in any test suite. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] viin_brand_website: brand the websites a field default cannot reach Removing the x_icon QWeb override was right - it resolved to website's own producer and destroyed it, so a configured favicon could never be served. But that override was also, incidentally, what made unconfigured websites look branded: it forced the brand path at render time for every website regardless of the stored field. With it gone the stored value renders, and for two populations that value is still Odoo's icon - website.default_website, which core creates while loading the website module, before this module's field extension exists, and any website predating this module's install. A field default fires only at record creation, so it reaches neither. So on every existing 19.0 database this branch, as it stood, silently swapped the Viindoo favicon for Odoo's. Reported by Codex on PR #664. Corrected once, from two entry points that share one implementation: post_init_hook for a fresh install, migrations/0.1.2 for a database where the module is already installed. Both rewrite ONLY records byte-identical to core's own default - those are provably nobody's choice - and leave every configured favicon exactly as it is, which is the whole point of having removed the override. Verified on clean databases: a fresh install and an upgrade both end with the default website branded, while a website whose owner set its own favicon keeps it byte-for-byte. UntouchedFaviconsGetBrandedTest locks both halves - doing nothing fails one assertion, overwriting everything fails the other.
[FWD] forward-port 18.0 -> 19.0 (0dafec5..467ac60, 9 commits) Absorbs every commit that landed on 18.0 since the last forward-port, so the merge-base advances and none of them is reconsidered next run. Three carry real behaviour to 19.0: * viin_brand_website no longer replaces website.layout's x_icon producer. That xpath resolved to core's own producer and destroyed it, so a customer who configured their own favicon could never serve it. Branding now comes from a field default in models/website.py: a new website starts branded, and the customer's value wins the moment they set one. Locked by tests/test_website_favicon_not_preempted.py. * viin_backend_theme guards _loadDefaultApp before diverting to the home menu, so an environment with a cleaned action registry falls back to the stock default app instead of throwing during boot. * viin_brand_website_sale drops the unverifiable "#1" ranking claim from the eCommerce promo, in the template and both translation catalogs. The rest are absorbed without adapt because 19.0 already moved past them: web_responsive is gone entirely, replaced by viin_backend_theme; QUnit is replaced by Hoot; the web.layout title/favicon fallback was deliberately re-homed onto that template's callers and must not be restored. One auto-merge carried a symbol that no longer exists here: the window-title branding arrived guarded by session.viin_brand, a marker stamped by an ir.http.session_info override 19.0 does not have, which would have left the title permanently unbranded with no error. Reverted to the 19.0 version. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] viin_backend_theme: realign radius invariant to owner decision D5 viin_brand_common's brand_variables.scss restores a pre-19 square-corners token ($o-border-radius: 0) in the previous commit; this file's cluster-wide "zero radius overrides anywhere" invariant predates that restore and read it as the exact regression it exists to catch. Owner decision D5 keeps the restore, so the guard is updated to treat that single base-rung declaration as a pinned, SSOT-read exception (-sm/-lg remain unchanged and must still equal core's own scale exactly) instead of a violation. Signed-off-by: David Tran <david.tran@tvtmarine.com>