# Product requirements — odoo_modules

Evidence date: 2026-10-08.

Source: `main` at `df1bfde` in `git@bitbucket.org:overdrive/odoo_modules.git`.

Provenance labels: **Documented** (in-repo docs), **Implemented** (code or config), **Inferred**, **Planned** (explicitly deferred in docs), **Unknown / Requires Confirmation**.

This document records behaviour the repository shows. It does not decide what the product should become.

## Product purpose

Silicon Overdrive runs an Odoo 17 database (named `odoo` by the production bootstrap) and this repository supplies the custom modules on that database.

The repository serves six jobs:

1. Run an internal and customer-facing helpdesk for four departments.
2. Give employees a company social hub (feed, kudos, announcements, colleagues).
3. Give each internal user a private ChatGPT-style assistant backed by OpenAI.
4. Extend timesheets with billable tracking, targets, reminders, and pushes to EIT and Convergence.
5. Give each employee a weekly planning dashboard on top of those timesheets.
6. Sync shared IMAP mailboxes into Odoo and attach the mail to contacts and CRM leads.

Provenance: **Implemented**. Items 1–3 are also **Documented** in `docs/`.

## Users and roles

| User | Where defined | What they can do |
|---|---|---|
| Internal user (`base.group_user`) | Odoo standard | Implied membership of My Company User and AI Assistant User. Helpdesk install hook also adds them to Helpdesk Agent |
| Helpdesk User | `so_helpdesk.group_helpdesk_user` | Read all tickets. Menu access |
| Helpdesk Agent | `so_helpdesk.group_helpdesk_agent` | Create and edit tickets for teams they belong to or lead. Implies User |
| Helpdesk Manager | `so_helpdesk.group_helpdesk_manager` | All tickets and configuration. Root and admin users are members in data |
| Portal user | `base.group_portal` | Own tickets (commercial partner child rule) at `/my/helpdesk` |
| Public website visitor | `auth='public'` on `/helpdesk/submit` | Submit a ticket without logging in |
| My Company User | `my_company.group_my_company_user` | Use the hub. Granted to every internal user |
| My Company Manager | `my_company.group_my_company_manager` | Manager menu (post administration) |
| AI User | `jb_ai_assistant.group_ai_user` | Own conversations only. Granted to every internal user |
| AI Administrator | `jb_ai_assistant.group_ai_admin` | All conversations, models, settings. Admin user is a member in data |
| Timesheet User | `jb_timesheet.group_timesheet_user` | Own analytic lines and own weekly plans |
| Timesheet Manager | `jb_timesheet.group_timesheet_manager` | All lines, all tasks, all weekly plans, office target report |
| Timesheet Invoice Manager | `jb_timesheet.group_timesheet_invoice` | Group comment: mark invoiced and push to EIT. Does not imply Timesheet Manager |
| Mail Inbox User | `jb_mail_inbox.group_mail_inbox_user` | Read and update all synced messages. Cannot create or delete them |
| Mail Inbox Manager | `jb_mail_inbox.group_mail_inbox_manager` | Configure IMAP servers and full message access |

Provenance: **Implemented** in each module's `security/*.xml` and `so_helpdesk/hooks.py`.

Whether every internal user should be a Helpdesk Agent is a product decision already encoded in the post-init hook and in `work_scripts/so_helpdesk/grant_access.sh` (called from production startup). Provenance: **Implemented**. A separate written policy was not found.

## Major workflows

### Helpdesk ticket

1. A ticket is created by an agent, by the public form `GET/POST /helpdesk/submit`, by the portal, or by IMAP import.
2. `ticket_ref` is assigned from sequence `so.helpdesk.ticket` with prefix `SO-HD/%(year)s/` and padding 5 (example shape `SO-HD/2026/00001`).
3. If no stage is set, the first stage of the team (lowest sequence) is applied.
4. SLA status rows are generated. The customer partner is subscribed as a follower when a partner is set.
5. Agents move the ticket through team stages. A stage must belong to the ticket's team.
6. Moving to a stage with `is_close` sets `close_date` and emails a 1–3 star rating request when the customer has an email and has not already been asked.
7. A public reply whose subject contains `SO-HD/YYYY/#####` is appended to that ticket. A reply after close is also logged as a note that the customer wrote after closure.
8. Agent comments that are not internal notes email the customer until the ticket is closed. The subject is prefixed with `[ticket_ref]` when missing.
9. Escalate sets `is_escalated`, records who escalated and when, assigns `escalated_to` to the team leader (or the first member), and raises priority to `3` when it is not already `3`.

Provenance: **Implemented** in `addons/so_helpdesk/models/helpdesk_ticket.py`, `helpdesk_mail_server.py`, `controllers/portal.py`, and `data/helpdesk_sequence.xml`. The department list and the "no stock moves" statement are **Documented** in `docs/so_helpdesk.md`.

### My Company hub

1. An internal user opens the My Company app (OWL client action).
2. The hub shows applications, a feed, and colleagues with birthdays.
3. The user creates a post, announcement, or kudos. Kudos requires a recipient.
4. Tagged partners, the kudos recipient, and members of targeted departments are notified. The author is excluded.
5. Comments are mail messages on the post.
6. Optional: the user, or a company setting, sets the hub as the Odoo home action.
7. A weekly cron posts a note to internal users who have not authored a post in seven days, unless `my_company.engagement_reminders` is not `True`.

Provenance: **Implemented** in `addons/my_company/`. Layout and the deferred roadmap are **Documented** in `docs/my_company.md`.

### AI assistant

1. An administrator sets the OpenAI API key, base URL, temperature, max tokens, and optional system prompt under Settings.
2. A user opens AI Assistant, picks a configured model (seeded models: GPT-4o default, GPT-4o Mini), and sends a message.
3. The module posts to `{base_url}/chat/completions` with the system content (settings prompt or a default identity prompt, plus active card rails, plus session memory) and the conversation messages.
4. The user sees only their own conversations. An AI Administrator sees all of them.
5. Each user can rename the bot. Empty name falls back to **Udi**.

Provenance: **Implemented** in `addons/jb_ai_assistant/models/ai_conversation.py` and `security/security.xml`. Features and the v2 roadmap are **Documented** in `docs/jb_ai_assistant.md`.

### Timesheet and weekly plan

1. A timesheet user logs time on `account.analytic.line` (standard HR timesheet, extended by `jb_timesheet`).
2. On create, and on write of time fields while `convergence_time_id` is empty, the line is POSTed to the Convergence URL when that parameter is set. Failure is logged. The save still succeeds.
3. An invoice manager can mark lines invoiced (sets `is_invoiced` and `invoiced_date`) and can push lines that have no `eit_billing_id` to the EIT URL. Lines that already have an EIT id are skipped. A missing EIT URL raises an error. Partial HTTP failures raise after the successful lines have been written.
4. A daily cron checks open tasks (`allocated_hours` > 0 and stage not folded) and sends one overrun email at the highest newly crossed threshold among 50%, 75%, and 100%.
5. A weekly cron (next call calculated toward Monday 08:00 UTC) emails employees with `include_in_office_target` who have a user and no analytic lines in the Monday–Sunday week, and increments `remind_count`. A daily cron resets `remind_count` on the last calendar day of the month.
6. Time Sheet V2 opens a weekly plan for the employee. Week start must be a Monday. One plan per employee per week. To-do lines carry a client, optional project, planned hours, and a done flag. Logged hours are computed from analytic lines in that week.

Provenance: **Implemented**. No module-level product doc was found for the timesheet modules (screenshots exist under `docs/timesheet_v2_*.png`).

### Mail inbox

1. A manager configures an IMAP server (host, port, user, password, folder, SSL).
2. A cron every 5 minutes, or a manual fetch, imports unseen messages.
3. Dedup is by `(server, IMAP UID)` and by `(server, Message-ID)`.
4. Participant addresses (From, To, Cc, Bcc), excluding the mailbox itself, are matched to contacts. A match receives one internal log note. Repeat routing to the same contact is skipped.
5. When `crm_route_enabled` is true (default), the same mail is matched to active CRM leads by email or linked partner. When nothing matches and `crm_create_lead_if_unmatched` is true (default), a lead is created from the external sender.
6. Users can mark messages new, read, or archived. Users cannot create or delete message records. Managers can.

Provenance: **Implemented** in `addons/jb_mail_inbox/`. No product doc under `docs/` was found for this module.

## Functional requirements

### FR-001 — Four helpdesk teams

Requirement: On install, four teams exist: Technical Support, Software Services, Cloud & AWS Services, Products & Returns. Each has stages New, In Progress, Waiting on Customer (folded), and Solved (`is_close`, folded).

Who: data file on module install (`noupdate="1"`).

Evidence: `addons/so_helpdesk/data/helpdesk_team_data.xml`.

Implementation: Implemented.

Confidence: High.

Note: `docs/so_helpdesk.md` names the third stage "Waiting". The data record name is "Waiting on Customer".

### FR-002 — Ticket types

Requirement: Seeded types are General Support, Product Return / RMA, Warranty Claim, Software Bug Report, Software Feature Request, Software License / Access, AWS Provisioning Request, AWS / Cloud Incident, and Consulting Engagement. Each type is linked to one or more teams. Some types set a default priority (return and bug and AWS provisioning = 2, cloud incident = 3). Changing the type on a ticket copies `default_priority` onto the ticket.

Evidence: `data/helpdesk_ticket_type_data.xml`, `helpdesk_ticket.py` `_onchange_ticket_type_id`.

Implementation: Implemented. The type list is also Documented.

Confidence: High.

### FR-003 — Default SLA policies

Requirement: Three SLA policies are seeded: Technical Support first response 4 hours to In Progress at priority 3; Cloud & AWS urgent resolution 8 hours to Solved at priority 3; Products & Returns processing 2 days to Solved.

Evidence: `helpdesk_team_data.xml`.

Implementation: Implemented.

Confidence: High.

An hourly cron calls `so.helpdesk.sla.status._cron_check_sla_failures()`. Mail fetch cron runs every 5 minutes.

### FR-004 — Ticket reference and stage constraint

Requirement: New tickets receive `SO-HD/{year}/{five digits}` unless `ticket_ref` is already set. A stage that belongs to a different team is rejected.

Evidence: `helpdesk_sequence.xml`, `helpdesk_ticket.py` `_check_stage_team`.

Implementation: Implemented.

Confidence: High.

### FR-005 — Rating

Requirement: Closing a ticket emails the customer a link `/helpdesk/rate/{id}/{access_token}`. Score must be 1, 2, or 3. A second rating is rejected. The score is posted on the ticket as an internal note.

Trigger: write of `stage_id` to a closing stage, when `rating` is empty and `rating_requested` is false, and `partner_email` is set.

Evidence: `helpdesk_ticket.py` `submit_rating`, `controllers/rating.py`, `data/helpdesk_mail_cron.xml`.

Implementation: Implemented. Documented as 1–3 stars in `docs/so_helpdesk.md`.

Confidence: High.

### FR-006 — Public ticket submission

Requirement: `/helpdesk/submit` (public, CSRF on) accepts subject, team, optional type, email, phone, description, and priority (default `1`). It creates a ticket with sudo. Logged-in non-public users are stored as `partner_id`. Public users are not linked to a partner; the typed email is stored.

Evidence: `controllers/portal.py`.

Implementation: Implemented.

Confidence: High.

Validation of empty fields is on the QWeb form (`required`), not in the controller. A non-browser POST can create a ticket with empty subject. Provenance: **Inferred** from the controller using `post.get` without checks.

### FR-007 — Portal ticket list

Requirement: Portal home counts tickets for `partner_id` of the current user when the user is in the portal group. `/my/helpdesk` lists those tickets, 20 per page. The detail route redirects to `/my` when the ticket partner is not the current user's partner.

Evidence: `controllers/portal.py`.

Implementation: Implemented.

Confidence: High.

The record rule for portal users is `partner_id child_of commercial_partner_id`, which is wider than the controller's exact `partner_id` match. Provenance: **Implemented** in both places. Which one wins for a contact of a commercial partner is **Requires Confirmation** (controller checks equality before render; the record rule may still allow read of sibling contacts).

### FR-008 — Helpdesk access on install

Requirement: After install, every active internal user is added to Helpdesk Agent. Production startup repeats a grant script when the module is already installed.

Evidence: `hooks.py`, `scripts/odoo-start.sh`.

Implementation: Implemented.

Confidence: High.

### FR-009 — Company feed

Requirement: Posts have type post, announcement, or kudos. Title, type, author, and company are required. Kudos without a recipient is rejected. Pinned posts sort first. Creating or changing tags, departments, or type notifies those people.

Evidence: `addons/my_company/models/mycompany_post.py`.

Implementation: Implemented.

Confidence: High.

`docs/my_company.md` lists "@mention autocomplete in composer" as not in v1. The model has tagged partners and notification. Autocomplete in the composer was not verified in the OWL widget beyond the documented roadmap. Provenance: tagging is **Implemented**; autocomplete is **Planned** per the doc.

### FR-010 — Hub as home

Requirement: A user can set or clear My Company as their home action. A setting can assign the hub as home for active internal users who have no home action, and can rename the root menu and hub action to `my_company.hub_name` (default "My Company").

Evidence: `res_users.py`, `res_config_settings.py`.

Implementation: Implemented. Documented in `docs/my_company.md`.

Confidence: High.

### FR-011 — Engagement reminder

Requirement: Weekly cron. If parameter `my_company.engagement_reminders` is the string `True` (the code default when the parameter is missing is also `True`), each active internal user with no post in the last 7 days gets a note on their partner.

Evidence: `mycompany_post.py` `_cron_engagement_reminder`, `data/ir_cron.xml`.

Implementation: Implemented.

Confidence: High.

### FR-012 — Private AI chat

Requirement: Conversations and messages are limited to `user_id` for AI Users. AI Administrators have an unrestricted rule. A call without an API key raises a user error telling the user to ask an administrator. Empty OpenAI content, timeout, HTTP errors, and connection errors raise user errors. Timeout is 60 seconds.

Evidence: `security/security.xml`, `ai_conversation.py`.

Implementation: Implemented. Documented.

Confidence: High.

Default parameters: base URL `https://api.openai.com/v1`, temperature `0.7`, max tokens `2048`, empty system prompt and empty API key.

### FR-013 — Timesheet ownership

Requirement: Timesheet Users read, write, create, and delete analytic lines where `user_id` is themselves, and tasks they are assigned to or created. They cannot delete tasks. Timesheet Managers have an unrestricted rule on lines and tasks.

Evidence: `addons/jb_timesheet/security/jb_timesheet_security.xml`.

Implementation: Implemented.

Confidence: High.

These rules are group rules on models that standard Odoo also rules. Combined behaviour with `hr_timesheet` record rules is **Requires Confirmation**.

### FR-014 — Convergence push

Requirement: When `jb_timesheet.convergence_api_url` is non-empty, a timesheet line without `convergence_time_id` is POSTed as JSON (date, duration, description, employee EIT id, project EIT id, task name, time in, time out). Optional bearer token. Response id is taken from `id`, `convergence_id`, or `time_id`. HTTP failure is logged and does not roll back the timesheet.

Evidence: `account_analytic_line.py`.

Implementation: Implemented.

Confidence: High.

The API contract of the remote system is **Unknown / Requires Confirmation**. The parameter values ship blank.

### FR-015 — EIT push

Requirement: The copy-to-EIT wizard refuses to run when `jb_timesheet.eit_api_url` is blank, and refuses when every selected line already has `eit_billing_id`. Each pending line is POSTed with date, duration, description, employee EIT id, client EIT id (line partner, else project customer), project EIT id, task name, internal notes, and `is_billable`. Billing id is taken from `billing_id`, `id`, or `eit_billing_id`. Errors after a partial success are shown to the user.

Evidence: `wizard/eit_copy_wizard.py`.

Implementation: Implemented.

Confidence: High.

Who may open the wizard in the UI was not fully traced beyond the Invoice Manager group comment. Menu XML for the wizard action was not re-read line by line. Provenance of the group intent: **Implemented** in the group `comment` field.

### FR-016 — Mark invoiced

Requirement: The wizard requires an invoiced date (default today) and at least one line. It sets `is_invoiced` and `invoiced_date`. It does not call an external API.

Evidence: `wizard/mark_invoiced_wizard.py`.

Implementation: Implemented.

Confidence: High.

### FR-017 — Hour overrun mail

Requirement: Daily, for tasks with allocated hours and a non-folded stage, send the 100% template if progress is at least 100 and that flag is clear; else the 75% template; else the 50% template. Each send sets only that threshold's flag.

Evidence: `models/project_task.py`.

Implementation: Implemented.

Confidence: High.

Because the branches are exclusive, a task that jumps to 100% does not send the 50% and 75% mails on the same run. Later runs can still send a lower threshold if its flag is still false. Provenance: **Inferred** from the `if / elif` structure. Whether that order is intended is **Requires Confirmation**.

Progress is `effective_hours / allocated_hours * 100`. The create-ticket wizard writes `planned_hours` on `project.task`. This repository does not define `planned_hours` on the task model. Standard Odoo 17 uses `allocated_hours`. Whether the wizard succeeds is **Unknown / Requires Confirmation**.

### FR-018 — Weekly reminder and remind count

Requirement: Employees with `include_in_office_target` set, a related user, and no timesheet lines from Monday through Sunday of the current week receive the reminder template. `remind_count` increments after the send attempt, including when `send_mail` raises (the error is logged and the counter still moves). If the mail template is missing, the method returns before the loop and nobody is counted. On the last calendar day of the month, every employee's `remind_count` is set to 0. The field is visible only to Timesheet Managers.

Evidence: `models/hr_employee.py`.

Implementation: Implemented.

Confidence: High.

The cron comment says the reminder is Mondays at 08:00. `nextcall` is computed once at module data load toward the next Monday 08:00 UTC. Odoo then repeats every 1 week from that call. Provenance: **Implemented**. Whether the server timezone makes that 08:00 local is **Unknown**.

### FR-019 — Weekly plan

Requirement: A plan's `week_start` must be a Monday. Uniqueness is `(employee_id, week_start)`. Opening a week creates the plan if missing. To-do name is required. Project domain is projects of the selected client. Variance is logged hours minus planned hours. Users see plans for their own employee. Managers see all.

Evidence: `timesheet_v2_week_plan.py`, `security/timesheet_v2_security.xml`.

Implementation: Implemented.

Confidence: High.

### FR-020 — Inbox routing

Requirement: Synced mail is deduplicated per server by UID and by Message-ID. Contact routing posts `mail.mt_note` once per matched partner. CRM routing is on by default and can create a lead when unmatched. Lead description is the text body truncated to 2000 characters. Lead name is the subject, else the sender, else "Inbox Mail".

Evidence: `mail_sync_message.py`, `mail_sync_message_crm.py`, `mail_sync_server.py`.

Implementation: Implemented.

Confidence: High.

A routing exception for one partner is logged and does not stop the others. If no note is posted, the message is not marked routed.

### FR-021 — Production bootstrap

Requirement: On container start, if `/var/lib/odoo/.bootstrap_done` is absent and `work_scripts/production_sync/odoo_full.zip` exists: skip when database `odoo` already exists; otherwise `odoo db load` that zip. Then, when the database exists, set `web.base.url` from `SODOO_WEB_BASE_URL`, upgrade My Company, install Helpdesk if not installed (or grant access), and install AI Assistant if not installed.

Evidence: `scripts/odoo-start.sh`, `work_scripts/production_sync/README.md`, `docker-compose.dokploy.yml`.

Implementation: Implemented. The README documents the dump as the seed for `sodoo.appnotify.co.za`.

Confidence: High.

`jb_timesheet`, `jb_timesheet_v2`, and `jb_mail_inbox` are not installed by this script. If they are present in production, they arrived through the loaded database dump or a manual install. Provenance: **Inferred**.

## Integrations

| System | Direction | Trigger | Auth | Failure |
|---|---|---|---|---|
| OpenAI chat completions | Outbound POST | User sends a chat message | Bearer API key in settings | User error; message not stored as a successful assistant reply (call raises) |
| EIT | Outbound POST | User runs copy-to-EIT | Optional bearer `jb_timesheet.eit_api_token` | User error; successful lines in the same run keep their billing id |
| Convergence | Outbound POST | Timesheet create/update without an id | Optional bearer `jb_timesheet.convergence_api_token` | Logged; timesheet remains saved without an id |
| IMAP (helpdesk) | Inbound | Cron 5 minutes or Fetch Now | Username and password on the mailbox | User error on manual test/fetch; cron path was not fully read for swallow-vs-raise |
| IMAP (mail inbox) | Inbound | Cron 5 minutes | Username and password on the server | Same pattern as helpdesk mailbox code |
| Odoo mail | Outbound | Stage template, rating template, overrun, reminder, chatter | Odoo mail stack | Overrun and reminder failures are logged |
| Dokploy / Docker | Runtime | Deploy | `SODOO_DB_PASSWORD`, `SODOO_ADMIN_PASSWORD` | Not described in code beyond shell `set -e` |

EIT and Convergence base URLs are **Unknown** until an administrator sets the system parameters.

## Constraints

- Odoo 17 and Postgres 15, as pinned in both compose files.
- Helpdesk does not create stock moves. **Documented** and consistent with dependencies.
- AI history is personal unless the viewer is an AI Administrator.
- Weekly plans are one per employee per Monday.
- Ratings are a single 1–3 score.
- Local compose publishes Postgres on port 5432 and uses a fixed database password in the file. Production passwords come from the environment and are appended to a runtime config under `/tmp`. Provenance: **Implemented**.

## Planned behaviour (explicitly not built)

From `docs/my_company.md`: mention autocomplete, reactions, tagging Odoo records, embedded Discuss, moderation queue, weekly digest email.

From `docs/jb_ai_assistant.md`: image generation, email drafting helper, knowledge-base tools, streaming responses.

Provenance: **Planned** (documented as roadmap / not in v1). No code for those items was found.

## Unknown / requires confirmation

- Live production URL and whether the 2026-07-08 dump is still how production is seeded.
- Business meaning of EIT and Convergence, and whether the APIs are still in use.
- Whether `planned_hours` on the ticket wizard is a defect against Odoo 17 `allocated_hours`.
- `jb_timesheet_v2` `post_init_hook(cr, registry)` uses the pre-Odoo-17 hook signature. `so_helpdesk` uses `post_init_hook(env)`. Whether V2 install succeeds on Odoo 17 is **Requires Confirmation**.
- Combined record rules between these modules and standard Odoo timesheet, project, and portal rules.
- Whether internal users should all be Helpdesk Agents (the code does this; no separate policy was found).
- `sonar-project.properties` and `nerd.html` listed at task creation are not in `main`.
