Odoo Custom Modules

Deployment, architecture, and developer reference for the odoo_modules repository
Silicon Overdrive • Odoo 17 • Last updated: July 2026 • Open this file in any browser

Project Overview

This repository hosts custom Odoo 17 add-on modules for Silicon Overdrive. Odoo runs in Docker alongside PostgreSQL. Local add-ons live in addons/ and are mounted into the container at /mnt/extra-addons, where Odoo discovers and loads them.

What this repo is: A Docker-based Odoo dev/staging stack + three custom modules (jb_timesheet, jb_timesheet_v2, jb_mail_inbox) that extend timesheets, weekly planning, and shared-mail IMAP routing into CRM.

Repository layout

odoo_modules/ ├── docker-compose.yml # PostgreSQL + Odoo 17 services ├── config/ │ └── odoo.conf # Addons path, DB connection, dev mode ├── addons/ # Custom modules (mounted into container) │ ├── jb_timesheet/ # Base timesheet extensions (v1) │ ├── jb_timesheet_v2/ # Timesheet2-style UI & weekly plans │ └── jb_mail_inbox/ # IMAP inbox sync + CRM routing ├── data/ │ ├── postgres/ # Persistent PostgreSQL data (local) │ └── odoo/ # Odoo filestore & session data (local) ├── work_scripts/ # One-off seed & maintenance scripts │ ├── seed_timesheet_v2/ │ ├── jb_mail_inbox/ │ └── test_mail_inbox_routing/ └── docs/ ├── odoo_modules_guide.html # This document └── Silicon Overdrive - Corporate Identity (CI) Guidelines V1.3.pdf

Architecture

Runtime stack

%%{init: {'theme':'base'}}%%
flowchart LR
    classDef host fill:#EDEDED,stroke:#00507A,stroke-width:1px,color:#002A41
    classDef svc fill:#0089D6,stroke:#002A41,stroke-width:1.5px,color:#ffffff
    classDef db fill:#00507A,stroke:#002A41,stroke-width:1.5px,color:#ffffff

    subgraph HOST["🖥️ Developer machine / server"]
        REPO["odoo_modules repo"]:::host
        BROWSER["Browser
localhost:8069"]:::host end subgraph NET["🐳 Docker Compose network"] direction LR ODOO["odoo_app
Odoo 17
ports 8069 · 8071 · 8072"]:::svc DB[("odoo_db
PostgreSQL 15
port 5432")]:::db end REPO -->|"bind mount ./addons"| ODOO REPO -->|"bind mount ./config/odoo.conf"| ODOO REPO -->|"bind mount ./data/odoo"| ODOO REPO -->|"bind mount ./data/postgres"| DB ODOO <-->|"psycopg2 · HOST=db PORT=5432"| DB BROWSER <-->|"HTTP/HTTPS + long-polling"| ODOO
Two Docker services on one Compose network. Odoo resolves the DB by service name db, not by IP.

Module dependency chain

%%{init: {'theme':'base'}}%%
flowchart TB
    classDef core fill:#EDEDED,stroke:#00507A,color:#002A41
    classDef custom fill:#0089D6,stroke:#002A41,stroke-width:1.5px,color:#ffffff

    subgraph CORE["Odoo standard modules"]
        base:::core --> mail:::core
        base:::core --> hr:::core
        hr:::core --> hr_timesheet:::core
        project:::core --> hr_timesheet:::core
        mail:::core --> crm:::core
        contacts:::core --> crm:::core
    end

    jb_timesheet["jb_timesheet
base extensions"]:::custom jb_v2["jb_timesheet_v2
weekly plans + dashboard"]:::custom jb_mail["jb_mail_inbox
IMAP + CRM routing"]:::custom hr_timesheet --> jb_timesheet project --> jb_timesheet jb_timesheet -->|"depends"| jb_v2 crm --> jb_mail mail --> jb_mail
Install order is derived automatically from each module's depends list in __manifest__.py.

Request lifecycle through an add-on

%%{init: {'theme':'base'}}%%
sequenceDiagram
    participant U as 🌐 Browser
    participant O as Odoo HTTP Worker
    participant ORM as ORM / Registry
    participant M as Module Code
(models, security) participant DB as PostgreSQL U->>O: Request (menu, form, list, RPC) O->>ORM: Route action → model + view ORM->>M: Apply _inherit fields, compute methods, record rules M->>DB: SQL via psycopg2 cursor (cr) DB-->>M: Rows M-->>ORM: Recordset ORM-->>O: QWeb / OWL render payload O-->>U: HTML / JSON response Note over O,DB: Transaction commits at end of request;
rolls back on unhandled exception

Deploy & Run

Prerequisites

First-time startup

cd C:\wamp64\www\odoo_modules
docker compose up -d

Wait for both containers to be healthy, then open:

http://localhost:8069

Create a database (first visit)

  1. On the database manager screen, set Master Password (matches admin_passwd in config/odoo.conf).
  2. Database name: e.g. testing (used by seed scripts).
  3. Email / password: your admin user credentials.
  4. Country, language, demo data: as needed.

Install custom modules

Via UI: Apps → Update Apps List → remove "Apps" filter → search and install in order:

  1. jb_timesheet (JB Timesheets)
  2. jb_timesheet_v2 (Time Sheet V2) — depends on jb_timesheet
  3. jb_mail_inbox (JB Mail Inbox) — requires CRM app

Via CLI (non-interactive, good for CI/deploy):

# Install on database "testing"
docker exec odoo_app odoo -d testing -i jb_timesheet,jb_timesheet_v2,jb_mail_inbox --stop-after-init

# Upgrade after code changes (see Migrations section)
docker exec odoo_app odoo -d testing -u jb_timesheet_v2 --stop-after-init

Day-to-day commands

ActionCommand
Start stackdocker compose up -d
Stop stackdocker compose down
View Odoo logsdocker logs -f odoo_app
Restart Odoo onlydocker compose restart odoo
Odoo shelldocker exec -it odoo_app odoo shell -d testing --no-http

Configuration reference

SettingValueWhere
Odoo HTTP port8069docker-compose.yml, odoo.conf
Longpolling8072docker-compose.yml
Addons path/mnt/extra-addons + core addonsconfig/odoo.conf
DB hostdb (Docker service name)odoo.conf + compose env
Dev modereload,qweb,werkzeug,xmlodoo.conf — auto-reload Python/XML in dev
Workers0 (single-process dev)odoo.conf
Production note: Change admin_passwd, database passwords, and set workers > 0 before any production deploy. Use a reverse proxy with TLS and set proxy_mode = True.

Production deployment pattern

  1. Clone repo on server; set strong passwords in docker-compose.yml and odoo.conf.
  2. docker compose up -d
  3. Create production database via UI or -i base.
  4. Install modules: odoo -d prod -i jb_timesheet,jb_timesheet_v2,jb_mail_inbox --stop-after-init
  5. Configure ir.config_parameter keys for EIT/Convergence APIs (Timesheets).
  6. Configure IMAP mail servers under Mail Inbox → Configuration.
  7. Back up data/postgres and data/odoo volumes regularly.

Database Connections — Technical

Odoo is a single Python application (odoo-bin) that talks to PostgreSQL using psycopg2. There is no separate ORM migration tool — the Odoo ORM inspects each model's Python field definitions and creates/alters the matching SQL table on install/upgrade.

How Odoo resolves the database

%%{init: {'theme':'base'}}%%
flowchart LR
    classDef cfg fill:#EDEDED,stroke:#00507A,color:#002A41
    classDef proc fill:#0089D6,stroke:#002A41,color:#ffffff

    ENV["Compose env vars
HOST / PORT / USER / PASSWORD"]:::cfg CONF["odoo.conf
db_host / db_port / db_user / db_password"]:::cfg ODOO["Odoo process boot"]:::proc POOL["psycopg2 connection pool
(db_maxconn, default 64)"]:::proc PG[("PostgreSQL
odoo_db:5432")] ENV --> ODOO CONF --> ODOO ODOO --> POOL POOL -->|"one cursor (cr) per request/thread"| PG
Both the compose environment: block and config/odoo.conf point Odoo at the same db service — odoo.conf wins if both are present.

Connection settings in this repo

ParameterValueNotes
db_hostdbDocker Compose service name, resolved via the internal Docker DNS — not localhost
db_port5432Standard PostgreSQL port, only exposed to the host for external tools (pgAdmin, DBeaver)
db_user / db_passwordodoo / odooDev-only credentials — must be rotated for staging/production
db_nameFalseNo default DB pinned — Odoo shows the database selector / manager screen
PGDATA/var/lib/postgresql/data/pgdataMapped to ./data/postgres on the host for persistence across container restarts

Multi-database model

Each Odoo "database" (e.g. testing, prod) is a separate PostgreSQL database on the same Postgres server/cluster — not a schema. All custom module tables (timesheet_v2_week_plan, mail_sync_message, etc.) are created inside whichever database the module is installed on. Installing a module on testing has no effect on prod.

Filestore vs. database

Binary data (attachments, images) is not stored as PostgreSQL bytea columns by default. Odoo writes files to disk under data/odoo/.local/share/Odoo/filestore/<database_name>/<hash prefix>/<sha1 hash> and stores only the hash reference in the ir_attachment table. Both the Postgres volume and the Odoo filestore volume must be backed up together — restoring one without the other breaks attachment links.

ORM cursor & transaction model

# Pattern used by work_scripts/ (outside the HTTP request cycle)
import odoo
odoo.tools.config.parse_config(['-d', 'testing'])
registry = odoo.registry('testing')

with registry.cursor() as cr:
    env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
    # ... ORM calls against env['model.name'] ...
    # cr.commit() is implicit on clean exit of "with registry.cursor()"

Credential & security notes

How Add-on Modules Load Inside Odoo

Each folder under addons/ is an Odoo module. Odoo loads them in a fixed sequence:

%%{init: {'theme':'base'}}%%
flowchart TD
    classDef step fill:#0089D6,stroke:#002A41,color:#ffffff
    classDef done fill:#00507A,stroke:#002A41,color:#ffffff

    A["1 · Scan addons_path directories"]:::step --> B["2 · Read __manifest__.py"]:::step
    B --> C["3 · Resolve depends[] → install order"]:::step
    C --> D["4 · Import __init__.py → register Python models"]:::step
    D --> E["5 · Load data[] XML/CSV → views, security, cron, demo"]:::step
    E --> F["6 · Register assets{} → SCSS/JS bundles"]:::step
    F --> G["7 · Run post_init_hook (install only)"]:::step
    G --> H["✔ Module state = installed"]:::done
    

Key files per module

FilePurpose
__manifest__.pyName, version, dependencies, data files, assets, hooks
__init__.pyImports Python subpackages (models, wizard, etc.)
models/*.pyExtends Odoo models via _inherit or new _name
views/*.xmlForms, lists, menus, actions, inherited views
security/Groups, record rules, ir.model.access.csv
data/Cron jobs, mail templates, config parameters, demo data
static/src/SCSS, JS, images — bundled into web.assets_backend

Volume mount = live code

docker-compose.yml maps ./addons to /mnt/extra-addons. config/odoo.conf sets:

addons_path = /mnt/extra-addons,/usr/lib/python3/dist-packages/odoo/addons

Edit Python or XML on the host, restart Odoo (or rely on dev_mode = reload,qweb,werkzeug,xml), then upgrade the module if you changed models, security, or data XML.

Model extension pattern

Modules extend Odoo core rather than replacing it:

# addons/jb_timesheet/models/account_analytic_line.py
class AccountAnalyticLine(models.Model):
    _inherit = 'account.analytic.line'   # extends existing timesheet lines
    is_billable = fields.Boolean(...)
    eit_billing_id = fields.Char(...)

Upgrades & Migrations

Important: Odoo does not use separate migration files like CodeIgniter or Django. Schema changes are applied automatically when you upgrade a module. Data backfills use hooks or one-off scripts.

What happens on install

  1. Odoo creates new database tables/columns from Python fields.* definitions.
  2. XML/CSV in data[] loads records (security groups, views, cron, parameters).
  3. post_init_hook runs once after install — used in jb_timesheet_v2 to recompute stored fields on existing timesheet lines.

What happens on upgrade (-u module_name)

  1. Odoo compares model definitions and alters tables (add columns, change types where safe).
  2. Re-loads XML data files (respecting noupdate="1" on existing records).
  3. Bumps module version in ir_module_module.
  4. Does not re-run post_init_hook (that is install-only).
# After changing Python models, views, or security:
docker exec odoo_app odoo -d testing -u jb_timesheet_v2 --stop-after-init
docker compose restart odoo

jb_timesheet_v2 post-init hook

File: addons/jb_timesheet_v2/hooks.py

def post_init_hook(cr, registry):
    """Recompute stored V2 fields on existing timesheet lines."""
    env = api.Environment(cr, SUPERUSER_ID, {})
    lines = env['account.analytic.line'].search([('project_id', '!=', False)])
    if lines:
        lines._compute_amount()
        lines._compute_is_internal()

This is a one-time data migration when V2 is first installed on a database that already had timesheet entries.

One-off maintenance scripts (work_scripts/)

These are not auto-run migrations — run manually inside the container when needed:

ScriptPurpose
work_scripts/seed_timesheet_v2/seed.py Seed demo timesheet lines, employee targets, admin groups
work_scripts/seed_timesheet_v2/seed_week_plans.py Seed current/previous week plan to-dos
work_scripts/jb_mail_inbox/backfill_crm_routing.py Route existing inbox messages to CRM leads
work_scripts/test_mail_inbox_routing/run_tests.py Validate mail-to-CRM routing logic
# Example: run seed inside container
docker exec odoo_app python3 /mnt/extra-addons/../work_scripts/seed_timesheet_v2/seed.py

Custom Modules — Current State

Core jb_timesheet — JB Timesheets

Version: 17.0.1.0.0  |  Depends: base, mail, hr, project, analytic, hr_timesheet

Foundation module extending Odoo timesheets with billing, external API integration, reporting, and scheduled jobs.

Features implemented

  • Extended timesheet linestime_in/time_out, billable flag, invoiced tracking, internal notes, EIT & Convergence API IDs
  • Auto-push to Convergence on create/write when API params configured
  • Employee targets — monthly target hours, office target inclusion, reminder count
  • Project/task extensions — overrun detection with email notification (daily cron)
  • Wizards — mark invoiced, EIT copy, create ticket
  • Reports — timesheet list, office target, my target, project breakdown
  • Scheduled actions — daily overrun check, weekly timesheet reminder (Monday 08:00), monthly remind_count reset
  • Config parametersjb_timesheet.eit_api_url, jb_timesheet.convergence_api_url (+ tokens)

Security groups

  • group_timesheet_user — own timesheets
  • group_timesheet_manager — all timesheets + office reports
  • group_timesheet_invoice — invoicing / EIT push

Menus

JB Timesheets → Timesheets (All) → Reports (list, office/my target, project breakdown)

V2 jb_timesheet_v2 — Time Sheet V2

Version: 17.0.1.2.0  |  Depends: jb_timesheet, hr_timesheet, project  |  Application: yes

Recreates the classic Timesheet2 workflow: personal weekly dashboard, to-do planning, team view, and styled list views.

Features implemented

  • My Dashboard — personal weekly plan with client-linked to-dos and hour estimates; previous-week recap (planned vs logged)
  • My Timesheet — filtered list with client, date, duration display, billable/internal row decorations
  • Team Weekly Plans — manager view of all employees' plans
  • Overview — office and personal target gauges (hours + revenue progress)
  • Week plan modeltimesheet.v2.week.plan + line items per client/project
  • Extended analytic lines — stored client, amount, internal flag, project/item display
  • Demo data — sample partners, projects, tasks for local testing
  • SCSS stylingstatic/src/scss/timesheet_v2.scss (row colors, gauge cards)
  • Reports — reuses jb_timesheet report actions under V2 menu

Menus

Time Sheet V2 → My Dashboard | My Timesheet | Team Weekly Plans | Overview | Reports

Mail jb_mail_inbox — JB Mail Inbox

Version: 17.0.3.0.0  |  Depends: base, mail, contacts, crm  |  Application: yes

Syncs shared IMAP mailboxes into a dedicated inbox and routes messages to contacts and CRM leads.

Features implemented

  • IMAP sync servers — configure host, credentials, folder; batch fetch (50/msg per cron run)
  • Inbox messagesmail.sync.message stores subject, from/to, body text/HTML, attachments
  • Contact routing — matches sender/recipients to res.partner; posts internal log notes on chatter
  • CRM routing — matches or creates crm.lead; links messages via M2M; catch-all lead creation for unmatched mail
  • Partner/lead counters — inbox message count on contact and lead forms
  • Scheduled action — fetch mail every 5 minutes

Menus

Mail Inbox → Inbox | Configuration → Mail Servers

Module install order

%%{init: {'theme':'base'}}%%
flowchart LR
    classDef m fill:#0089D6,stroke:#002A41,color:#ffffff
    Install1["1 · jb_timesheet"]:::m --> Install2["2 · jb_timesheet_v2"]:::m
    Install3["jb_mail_inbox"]:::m
    Install1 -.->|"independent"| Install3
    

Entity Relationship Diagram

Custom models are shown in blue; core Odoo models they extend or reference are shown in grey. Fields marked related/stored denote computed columns physically stored on the table (queryable in SQL, kept in sync by the ORM).

%%{init: {'theme':'base'}}%%
erDiagram
    HR_EMPLOYEE ||--o{ TIMESHEET_V2_WEEK_PLAN : "owns"
    HR_EMPLOYEE ||--o{ ACCOUNT_ANALYTIC_LINE : "logs"
    TIMESHEET_V2_WEEK_PLAN ||--o{ TIMESHEET_V2_WEEK_PLAN_LINE : "contains"
    TIMESHEET_V2_WEEK_PLAN }o--o| TIMESHEET_V2_WEEK_PLAN : "previous_plan_id"
    RES_PARTNER ||--o{ TIMESHEET_V2_WEEK_PLAN_LINE : "client (optional)"
    PROJECT_PROJECT ||--o{ TIMESHEET_V2_WEEK_PLAN_LINE : "planned project"
    RES_PARTNER ||--o{ PROJECT_PROJECT : "client of"
    PROJECT_PROJECT ||--o{ PROJECT_TASK : "has"
    PROJECT_PROJECT }o--o| PROJECT_PROJECT : "template_id"
    PROJECT_PROJECT ||--o{ ACCOUNT_ANALYTIC_LINE : "has"
    PROJECT_TASK ||--o{ ACCOUNT_ANALYTIC_LINE : "has"
    RES_PARTNER ||--o{ ACCOUNT_ANALYTIC_LINE : "related, stored"

    MAIL_SYNC_SERVER ||--o{ MAIL_SYNC_MESSAGE : "fetches into"
    MAIL_SYNC_MESSAGE }o--o{ RES_PARTNER : "matched contacts"
    MAIL_SYNC_MESSAGE }o--o{ CRM_LEAD : "matched leads"
    MAIL_SYNC_MESSAGE }o--o{ IR_ATTACHMENT : "attachments"
    MAIL_SYNC_MESSAGE }o--o{ MAIL_MESSAGE : "posted log notes"

    HR_EMPLOYEE {
        int id PK
        char name
        int user_id FK "res.users"
        float hourly_rate "jb_timesheet"
        float target_hours "jb_timesheet"
        monetary monthly_revenue_target "jb_timesheet_v2"
        boolean include_in_office_target
        int remind_count
        char timesheet2_user_id "legacy import key"
    }

    ACCOUNT_ANALYTIC_LINE {
        int id PK
        int employee_id FK
        int project_id FK
        int task_id FK
        int partner_id FK "related project_id.partner_id, stored"
        float unit_amount "hours"
        float hourly_rate
        monetary amount "computed, stored"
        boolean is_billable
        boolean is_internal "computed, stored"
        boolean is_invoiced
        char eit_billing_id
        char convergence_time_id
        float time_in
        float time_out
    }

    TIMESHEET_V2_WEEK_PLAN {
        int id PK
        int employee_id FK
        date week_start UK "unique with employee_id"
        date week_end "computed, stored"
        float total_planned_hours "computed, stored"
        float total_actual_hours "computed"
        int previous_plan_id FK "self-reference"
    }

    TIMESHEET_V2_WEEK_PLAN_LINE {
        int id PK
        int plan_id FK
        int partner_id FK "client"
        int project_id FK
        char name "to-do text"
        float planned_hours
        boolean is_done
        float actual_hours "computed"
        float variance_hours "computed"
    }

    PROJECT_PROJECT {
        int id PK
        int partner_id FK
        boolean is_template
        int template_id FK "self-reference"
        char eit_project_id
    }

    PROJECT_TASK {
        int id PK
        int project_id FK
        boolean overrun_50_sent
        boolean overrun_75_sent
        boolean overrun_100_sent
        boolean is_ticket
        int ticket_project_id FK
    }

    RES_PARTNER {
        int id PK
        char name
        char email
        char timesheet2_client_id "legacy import key"
        boolean is_timesheet_internal
        int mail_sync_message_count "computed"
    }

    MAIL_SYNC_SERVER {
        int id PK
        char name
        char server "IMAP host"
        int port
        char user
        char password "restricted group"
        int last_uid "dedup watermark"
        char state "draft/ok/error"
        boolean crm_route_enabled
    }

    MAIL_SYNC_MESSAGE {
        int id PK
        int server_id FK
        char message_id UK "RFC Message-ID"
        int uid UK "IMAP UID, unique with server_id"
        char email_from
        char subject
        text body_text
        html body_html
        char state "new/read/archived"
        boolean is_routed "contact routing done"
        boolean is_routed_lead "CRM routing done"
    }

    CRM_LEAD {
        int id PK
        char name
        int partner_id FK
        int mail_sync_message_count "computed"
    }
    
Generated from the model definitions in addons/jb_timesheet, addons/jb_timesheet_v2, and addons/jb_mail_inbox.

Notable constraints

TableConstraintWhy
timesheet_v2_week_planUNIQUE(employee_id, week_start)One plan per employee per Monday-start week
timesheet_v2_week_planweek_start must be a Monday (Python constraint)Keeps weekly boundaries consistent for reporting
mail_sync_messageUNIQUE(server_id, uid)Prevents re-importing the same IMAP message
mail_sync_messageUNIQUE(server_id, message_id)Secondary dedup by RFC Message-ID header

Custom Modules — Steps to Build a New One

Concrete, technical checklist for adding module #4 on top of this stack.

  1. Scaffold the folder under addons/<module_name>/:
    addons/jb_new_module/
    ├── __init__.py
    ├── __manifest__.py
    ├── models/
    │   ├── __init__.py
    │   └── my_model.py
    ├── views/
    │   └── my_model_views.xml
    ├── security/
    │   ├── my_module_security.xml
    │   └── ir.model.access.csv
    ├── data/
    │   └── ir_cron.xml
    └── static/description/icon.png
  2. Write __manifest__.py — declare depends on the modules whose models/views you extend, list every XML/CSV file under data in load order (security before views, views before menus), and register any SCSS/JS under assets.
  3. Define models in models/*.py. Use _inherit to extend an existing model (e.g. account.analytic.line) or _name for a brand-new table. Keep files under ~250 lines; split by concern (per workspace convention).
  4. Add securityir.model.access.csv (one row per model/group with read/write/create/unlink permissions) and, if data must be restricted per-user, ir.rule record rules in security/*.xml.
  5. Build views — form/list/search views, ir.actions.act_window, and menuitem entries. Reference security groups on menus with groups="module.group_xxx".
  6. Add data files as needed — ir.cron for scheduled jobs, ir.config_parameter for settings, mail.template for emails, demo data guarded by noupdate="1" where records should not be re-loaded on upgrade.
  7. Restart Odoo and update the apps list:
    docker compose restart odoo
    docker exec odoo_app odoo -d testing -u base --stop-after-init  # or use Apps → Update Apps List in UI
  8. Install the module:
    docker exec odoo_app odoo -d testing -i jb_new_module --stop-after-init
    or via Apps in the UI (search with the "Apps" filter removed).
  9. Iterate: after every model/view/security change, run odoo -d testing -u jb_new_module --stop-after-init to apply schema/data changes, then restart if dev_mode auto-reload does not pick up the change.
  10. Document it — add the module to the Modules section of this guide, update docs/index.md / task files per workspace conventions, and add its tables to the ERD if it introduces new models or relations.
Tip: use docker exec -it odoo_app odoo shell -d testing --no-http to interactively test ORM calls against your new model before wiring up views.

Work Scripts

Utility scripts live in work_scripts/ per workspace conventions. They run against a named database (default: testing).

Seed timesheet demo data

docker exec odoo_app python3 /mnt/extra-addons/../work_scripts/seed_timesheet_v2/seed.py
docker exec odoo_app python3 /mnt/extra-addons/../work_scripts/seed_timesheet_v2/seed_week_plans.py

Backfill CRM routing for existing mail

docker exec -it odoo_app odoo shell -d testing --no-http
# Then paste contents of work_scripts/jb_mail_inbox/backfill_crm_routing.py

Silicon Overdrive CI — Styling for Modules

Source: docs/Silicon Overdrive - Corporate Identity (CI) Guidelines V1.3.pdf

Brand colors

Primary

#0089D6 — Primary Blue
#EDEDED — Light Grey

Secondary

#31B0F5 — Light Blue
#00507A — Dark Blue
#002A41 — Navy
#FFFFFF — White
#000000 — Black (copy only)
#FF9900 — Orange (AWS assets only)

Typography

Applying CI to Odoo modules

Odoo backend styling is done via SCSS bundled in __manifest__.pyweb.assets_backend.

Current V2 styles (addons/jb_timesheet_v2/static/src/scss/timesheet_v2.scss) use generic greys. Recommended CI alignment:

// Suggested CI variables for future module SCSS
$so-primary: #0089D6;
$so-primary-light: #31B0F5;
$so-primary-dark: #00507A;
$so-navy: #002A41;
$so-grey-bg: #EDEDED;
$so-success-bg: #eef7ee;   // keep or tint with primary
$so-warning-bg: #fff8e6;
$so-danger-bg: #fdeeee;

// Example: gauge card header
.o_timesheet_v2_dashboard .o_timesheet_v2_gauge .card-header {
    font-weight: 600;
    background: $so-grey-bg;
    color: $so-navy;
    border-bottom: 2px solid $so-primary;
}

Shared CI asset module (recommended next step)

Create jb_brand or so_theme as a dependency that loads:

Other modules (jb_timesheet_v2, jb_mail_inbox) would depend on it for consistent branding.

Next Steps & Build-On Checklist

Quick reference card

I want to…Do this
Start Odoo locallydocker compose up -dlocalhost:8069
Install all custom modulesApps UI or odoo -d testing -i jb_timesheet,jb_timesheet_v2,jb_mail_inbox --stop-after-init
Apply code changes to DB schema/viewsodoo -d testing -u <module> --stop-after-init
Load demo timesheet dataRun work_scripts/seed_timesheet_v2/*.py in container
Add a new moduleFollow Steps to Build a New One
Style with company CISCSS in static/src/scss/, register in manifest assets