Deeply and durably customize an EverJust.app (Odoo) tenant website from the odoo shell — the layer below the MCP website_* tools. Use when a change needs QWeb view edits, site-wide CSS/JS, new server-side behavior, or model/config writes that the everjust MCP and website builder cannot do (editing views, server actions, custom_code_head, base automations). Covers COW inherit views (website_id=1) that survive addon redeploys, the website.custom_code_head Markup-escaping trap, base_automation for behavior, the "view edits need docker restart to render" rule, and the durable-vs-reverts-on-upgrade matrix. Use when the user says "edit the Odoo view/template", "inject CSS/JS site-wide", "add a server action / automation", "the change keeps getting reverted on deploy", or "customize the everjust/connectdomain website beyond the MCP tools".
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Deeply and durably customize an EverJust.app (Odoo) tenant website from the odoo shell — the layer below the MCP website_* tools. Use when a change needs QWeb view edits, site-wide CSS/JS, new server-side behavior, or model/config writes that the everjust MCP and website builder cannot do (editing views, server actions, custom_code_head, base automations). Covers COW inherit views (website_id=1) that survive addon redeploys, the website.custom_code_head Markup-escaping trap, base_automation for behavior, the "view edits need docker restart to render" rule, and the durable-vs-reverts-on-upgrade matrix. Use when the user says "edit the Odoo view/template", "inject CSS/JS site-wide", "add a server action / automation", "the change keeps getting reverted on deploy", or "customize the everjust/connectdomain website beyond the MCP tools".
Customize an EverJust.app tenant (an Odoo instance) at a level the MCP tools and
website builder do not reach: QWeb views, site-wide CSS/JS, server-side
behavior, and model data — done so it survives addon redeploys and does
not conflict with other agents editing the addon's git checkout.
The everjust MCP surface ([[everjust-agent-mcp]], [[everjust-website]]) is ORM
data operations bounded by a user's role; it explicitly punts on "editing views,
server actions... escalate to a human with UI access." This skill is that
escalation, done from the odoo shell instead of the UI.
When to use
A change requires editing a QWeb view / template (layout, markup, xpath).
You need site-wide CSS or JS on a tenant (website.custom_code_head).
You need server-side behavior on a record event (create/confirm) — a
server action / automation.
A previous edit keeps getting reverted when the addon is redeployed or
upgraded (-u), and you need a durable override.
Multiple agents/sessions are churning the tenant's addon git checkout and you
must change the live site without touching those files.
When NOT to use
Plain content/record edits (leads, posts, events, config) → use the MCP
([[everjust-agent-mcp]]) or the everjust-website family.
Installing/upgrading addons, or edits that genuinely need the web Studio UI.
Any change you cannot render-verify afterward — always verify with
[[cdp-render-verification]] before declaring done.
Access: the odoo shell (critical mechanics)
Reach the shell by SSH to the tenant's box, then exec into the Odoo container:
# password comes from the RUNNING server's compose command line, NOT /etc/odoo/odoo.conf
PW=$(sudo grep -rhoE "POSTGRES_PASSWORD=[A-Za-z0-9]+" <deployment-dir> | grep -v "=odoo$" | head -1 | cut -d= -f2)
cat script.py | sudo docker exec -i <odoo-container> odoo shell -d <db> \
--db_host=db --db_user=<user> --db_password="$PW" --no-http
The shell runs inside the container — host /tmp != container /tmp.
To hand files in (video, images, b64), docker cp them into the container
first, then read from the container path.
Scripts are piped on stdin; end with env.cr.commit() (the shell does not
auto-commit; an exception before commit rolls everything back).
website.browse(1) is typically the tenant's primary website. Confirm the id.
Store the box address / SSH key / DB name in the caller's own notes — never in a
committed skill file.
Module-owned views (website_id=False) revert on -u. Create a website-specific
inherit view (website_id=1) instead: it applies on top for that website and
is not owned by the module, so it survives upgrades and addon redeploys.
Target robustly: hasclass('x'), contains(text(),'...'), contains(normalize-space(.),'...'),
or //a[@href='/x' and hasclass('btn')]. Position: replace / after / before / inside.
A bad xpath makes the page 500 at render, so verify immediately (§Verify).
If a base view already has a website_id=1 COW copy (same key), edit that
copy's arch directly — an inherit that targets the generic key may not reach it.
2. website.custom_code_head — site-wide CSS/JS (+ the Markup trap)
website.custom_code_head injects raw HTML into every page's <head>. It is the
best home for CSS and for JS that contains < / && (which would break XML view
arch). The trap: the field returns a Markup; Markup + strHTML-escapes
the appended string, so your <style>/<script> renders as visible escaped
text on the page.
W = env['website'].browse(1)
h = str(W.custom_code_head or'') # <-- str() FIRST, then concat plain strif'my-block'notin h: # id-guard = idempotent
W.write({'custom_code_head': h + '<style id="my-block">...</style>'})
Guard JS to the right page: if (location.pathname.startsWith('/x')) { ... },
wrap in DOMContentLoaded, and null-check every element (head JS runs before body).
CSS/JS here is live immediately (no restart). It degrades gracefully — if a
selector changes, it no-ops rather than 500-ing (unlike a view xpath).
3. base_automation — durable server-side behavior
For behavior on a record event (e.g., generate a link when a booking's calendar
event is created), use a base.automation. It is DB-owned, so it survives addon
redeploys — unlike editing the addon's Python.
# install the module once if absent:
m = env['ir.module.module'].search([('name','=','base_automation')])
if m.state != 'installed': m.button_immediate_install()
auto = env['base.automation'].create({'name':'X','model_id':<model_id>,'trigger':'on_create','active':True})
env['ir.actions.server'].create({
'name':'X code','model_id':<model_id>,'base_automation_id':auto.id,
'usage':'base_automation','state':'code',
'code': "try:\n ...do work on `records`...\nexcept Exception:\n pass", # NEVER let it raise
})
Link is via base.automation.action_server_ids (One2many) / ir.actions.server.base_automation_id
usage='base_automation' (this build has no delegated state/code on base.automation).
Always wrap the code body in try/except — a raise here breaks the
triggering ORM write (and thus the user flow).
Fires on every write to that model DB-wide; filter fast in code and swallow errors.
4. When view edits need a restart
DB data and custom_code_head changes render immediately. ir.ui.view
arch changes often do not — the workers cache the compiled template and
clear_cache() / registry signals do not reliably bust it. After editing/creating
a view, docker restart <odoo-container> and wait for the site to return 200.
Shell-created records with noxml_id (e.g. an appointment type you created)
✅
COW inherit view (website_id=1)
✅
website.custom_code_head CSS/JS
✅
base.automation + its server action
✅
Module-owned view / mail.template / record (has an xml_id)
❌ reverts on -u — re-apply, or fork via COW
Check ownership: env['ir.model.data'].search([('model','=','<m>'),('res_id','=',<id>)])
→ if it returns an xml_id, the record is module-owned and your edits revert.
Verify (mandatory)
Never ship a visual/behavioral change unverified — a bad xpath 500s the page and
a Markup slip prints code to users. Render-verify with [[cdp-render-verification]]:
check the page returns 200, the intended pixels/computed styles changed, no raw
<style/<script/< leaked into body.innerText, and no mobile overflow.
Related
[[everjust-agent-mcp]] — the MCP data layer this skill sits below.