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.
jb_timesheet, jb_timesheet_v2, jb_mail_inbox) that extend
timesheets, weekly planning, and shared-mail IMAP routing into CRM.
Repository layout
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
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
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
- Docker Desktop (Windows) or Docker Engine + Docker Compose
- Ports 8069 (Odoo), 5432 (PostgreSQL) available
- Git clone of this repository
First-time startup
cd C:\wamp64\www\odoo_modules docker compose up -d
Wait for both containers to be healthy, then open:
Create a database (first visit)
- On the database manager screen, set Master Password (matches
admin_passwdinconfig/odoo.conf). - Database name: e.g.
testing(used by seed scripts). - Email / password: your admin user credentials.
- Country, language, demo data: as needed.
Install custom modules
Via UI: Apps → Update Apps List → remove "Apps" filter → search and install in order:
jb_timesheet(JB Timesheets)jb_timesheet_v2(Time Sheet V2) — depends on jb_timesheetjb_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
| Action | Command |
|---|---|
| Start stack | docker compose up -d |
| Stop stack | docker compose down |
| View Odoo logs | docker logs -f odoo_app |
| Restart Odoo only | docker compose restart odoo |
| Odoo shell | docker exec -it odoo_app odoo shell -d testing --no-http |
Configuration reference
| Setting | Value | Where |
|---|---|---|
| Odoo HTTP port | 8069 | docker-compose.yml, odoo.conf |
| Longpolling | 8072 | docker-compose.yml |
| Addons path | /mnt/extra-addons + core addons | config/odoo.conf |
| DB host | db (Docker service name) | odoo.conf + compose env |
| Dev mode | reload,qweb,werkzeug,xml | odoo.conf — auto-reload Python/XML in dev |
| Workers | 0 (single-process dev) | odoo.conf |
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
- Clone repo on server; set strong passwords in
docker-compose.ymlandodoo.conf. docker compose up -d- Create production database via UI or
-i base. - Install modules:
odoo -d prod -i jb_timesheet,jb_timesheet_v2,jb_mail_inbox --stop-after-init - Configure
ir.config_parameterkeys for EIT/Convergence APIs (Timesheets). - Configure IMAP mail servers under Mail Inbox → Configuration.
- Back up
data/postgresanddata/odoovolumes 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
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
| Parameter | Value | Notes |
|---|---|---|
db_host | db | Docker Compose service name, resolved via the internal Docker DNS — not localhost |
db_port | 5432 | Standard PostgreSQL port, only exposed to the host for external tools (pgAdmin, DBeaver) |
db_user / db_password | odoo / odoo | Dev-only credentials — must be rotated for staging/production |
db_name | False | No default DB pinned — Odoo shows the database selector / manager screen |
PGDATA | /var/lib/postgresql/data/pgdata | Mapped 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
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
- Cursor (
cr) — one psycopg2 cursor per request/cron job/shell session; all ORM calls in that scope share one open transaction. - Environment (
env) — bundlescr,uid(acting user, e.g.SUPERUSER_ID), andcontext. Every model method receives records bound to an environment. - Commit boundaries — a normal HTTP request commits automatically on success and rolls back on unhandled exception. In scripts /
odoo shell, you must callcr.commit()explicitly (seework_scripts/). - Savepoints — nested
with cr.savepoint():blocks let a sub-operation fail and roll back without aborting the outer transaction (used internally by@api.model_create_multi, wizards, etc.).
# 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
- Dev credentials in
odoo.conf/docker-compose.ymlare committed in plaintext — acceptable for local dev only. - For staging/production, move secrets to a
.envfile (git-ignored) referenced by Compose, or Docker secrets, and never commit real passwords. db_passwordin a hashedadmin_passwd(pbkdf2-sha512) only protects the database manager screen, not the Postgres connection itself.- Set
list_db = Falsein production to hide the database selector/manager UI from the public internet.
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
| File | Purpose |
|---|---|
__manifest__.py | Name, version, dependencies, data files, assets, hooks |
__init__.py | Imports Python subpackages (models, wizard, etc.) |
models/*.py | Extends Odoo models via _inherit or new _name |
views/*.xml | Forms, 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
What happens on install
- Odoo creates new database tables/columns from Python
fields.*definitions. - XML/CSV in
data[]loads records (security groups, views, cron, parameters). post_init_hookruns once after install — used injb_timesheet_v2to recompute stored fields on existing timesheet lines.
What happens on upgrade (-u module_name)
- Odoo compares model definitions and alters tables (add columns, change types where safe).
- Re-loads XML data files (respecting
noupdate="1"on existing records). - Bumps module version in
ir_module_module. - 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:
| Script | Purpose |
|---|---|
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 lines —
time_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 parameters —
jb_timesheet.eit_api_url,jb_timesheet.convergence_api_url(+ tokens)
Security groups
group_timesheet_user— own timesheetsgroup_timesheet_manager— all timesheets + office reportsgroup_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 model —
timesheet.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 styling —
static/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 messages —
mail.sync.messagestores 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"
}
addons/jb_timesheet, addons/jb_timesheet_v2, and addons/jb_mail_inbox.Notable constraints
| Table | Constraint | Why |
|---|---|---|
timesheet_v2_week_plan | UNIQUE(employee_id, week_start) | One plan per employee per Monday-start week |
timesheet_v2_week_plan | week_start must be a Monday (Python constraint) | Keeps weekly boundaries consistent for reporting |
mail_sync_message | UNIQUE(server_id, uid) | Prevents re-importing the same IMAP message |
mail_sync_message | UNIQUE(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.
-
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
-
Write
__manifest__.py— declaredependson the modules whose models/views you extend, list every XML/CSV file underdatain load order (security before views, views before menus), and register any SCSS/JS underassets. -
Define models in
models/*.py. Use_inheritto extend an existing model (e.g.account.analytic.line) or_namefor a brand-new table. Keep files under ~250 lines; split by concern (per workspace convention). -
Add security —
ir.model.access.csv(one row per model/group with read/write/create/unlink permissions) and, if data must be restricted per-user,ir.rulerecord rules insecurity/*.xml. -
Build views — form/list/search views,
ir.actions.act_window, andmenuitementries. Reference security groups on menus withgroups="module.group_xxx". -
Add data files as needed —
ir.cronfor scheduled jobs,ir.config_parameterfor settings,mail.templatefor emails, demo data guarded bynoupdate="1"where records should not be re-loaded on upgrade. -
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 -
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). -
Iterate: after every model/view/security change, run
odoo -d testing -u jb_new_module --stop-after-initto apply schema/data changes, then restart ifdev_modeauto-reload does not pick up the change. -
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.
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
Secondary
Typography
- Font: Poppins (Google Fonts) — already used in this guide and recommended for Odoo SCSS overrides
- Headings: Poppins Bold, uppercase, 30–40px
- Sub-headings: Poppins Light, uppercase, 20–24px
- Body: Sentence case, line-height 1.2, 8–12px minimum in print; scale up for screen UI
- Language: American English; professional, clear, technical tone
Applying CI to Odoo modules
Odoo backend styling is done via SCSS bundled in __manifest__.py → web.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:
- Global SCSS variables and Poppins font import
- Login page / app menu color overrides
- Module icon templates using official logo assets
Other modules (jb_timesheet_v2, jb_mail_inbox) would depend on it for consistent branding.
Next Steps & Build-On Checklist
- Create
jb_brandmodule with Silicon Overdrive SCSS variables and Poppins - Update
timesheet_v2.scssto use CI palette instead of generic hex values - Add module icons under
static/description/icon.pngusing official logo - Configure EIT/Convergence API parameters in Settings for billing integration
- Set up IMAP mail servers in Mail Inbox for production mailboxes
- Add
pre_init_hook/ migration scripts if complex data transforms are needed on upgrade - Document production secrets management (env vars instead of committed
odoo.confpasswords)
Quick reference card
| I want to… | Do this |
|---|---|
| Start Odoo locally | docker compose up -d → localhost:8069 |
| Install all custom modules | Apps UI or odoo -d testing -i jb_timesheet,jb_timesheet_v2,jb_mail_inbox --stop-after-init |
| Apply code changes to DB schema/views | odoo -d testing -u <module> --stop-after-init |
| Load demo timesheet data | Run work_scripts/seed_timesheet_v2/*.py in container |
| Add a new module | Follow Steps to Build a New One |
| Style with company CI | SCSS in static/src/scss/, register in manifest assets |