# Business rules — odoo_modules

Evidence date: 2026-10-08. Source: `main` at `df1bfde`.

Rules below are taken from code, XML data, or in-repo docs. Each rule is marked **Documented**, **Implemented**, or **Inferred**. Nothing here is a new policy.

Where the code and a doc disagree, both are stated.

## Helpdesk

### Teams and catalogue

1. Four teams are delivered with the module: Technical Support, Software Services, Cloud & AWS Services, Products & Returns. **Implemented** (`noupdate="1"`). **Documented** in `docs/so_helpdesk.md`. Later edits in the database are not overwritten by module update.
2. Each team is delivered with the stage sequence New → In Progress → Waiting on Customer → Solved. Waiting on Customer and Solved are folded. Only Solved is a closing stage. **Implemented**. The doc says "Waiting", not "Waiting on Customer".
3. Ticket types are restricted to teams by `team_ids` in data (for example Product Return / RMA only on Products & Returns; Consulting Engagement on Software Services and Cloud & AWS). **Implemented** as default data. Whether the form domain enforces that restriction on every create path was not fully verified. Public submit posts `ticket_type_id` without a server-side team check. **Inferred** gap: the controller does not reject a type that is not linked to the chosen team.
4. Selecting a ticket type copies that type's default priority onto the ticket (onchange). **Implemented**.
5. Product returns do not create stock moves or delivery orders. **Documented**. **Implemented** by absence of a stock dependency and by storing only `product_id`.

### Identity, stage, and closure

6. A ticket reference is `SO-HD/{year}/{five-digit number}` when the create values do not already set `ticket_ref`. **Implemented**.
7. A ticket stage must belong to the ticket's team. **Implemented** (`ValidationError`).
8. If create does not set a stage, the team's lowest-sequence stage is used. **Implemented**.
9. Entering a closing stage sets `close_date` to now. Leaving a closing stage clears `close_date` via `setdefault` only when the write values did not already include `close_date`. **Implemented**.
10. The first time a ticket is closed, if it has no rating yet and a customer email, a rating email is sent and `rating_requested` is set. **Implemented**.
11. A rating may be submitted once. Allowed scores are the strings `1`, `2`, and `3`. **Implemented**. **Documented**.
12. A customer email that arrives after the ticket is closed is still posted on the thread, and an extra note records that the customer replied after closure. **Implemented**. The doc says agent Send message emails the customer until the ticket is closed, which matches `message_post` skipping customer subscribe behaviour when `is_closed` is true. It does not say the inbound reply is dropped.

### Assignment and escalation

13. Setting `user_id` sets `assign_date` if that date was not already in the write values. **Implemented**.
14. Escalate marks the ticket escalated, stores the current user and the time, sets the target to the team leader or else the first member, and sets priority to `3` unless it is already `3`. **Implemented**. **Documented** as one-click escalate to the team lead.
15. If the team has no leader and no members, `escalated_to` is false and the note says the ticket was escalated to "team lead". **Implemented**.

### SLA

16. Seeded policies: Technical Support first response 4 hours (priority 3, target stage In Progress); Cloud & AWS urgent resolution 8 hours (priority 3, target Solved); returns processing 2 days (target Solved, no priority filter in the data record). **Implemented**.
17. SLA rows are generated on create and again when team, priority, or ticket type changes. Reaching the policy's target stage marks the SLA reached. An hourly cron checks failures. **Implemented**.
18. Priority values used in data are Odoo selection keys (`2`, `3`), not labels. **Inferred** from `default_priority` and SLA `priority` fields. The selection labels were not copied into this document.

### Mail

19. Helpdesk mailbox fetch runs every 5 minutes. **Implemented**. **Documented**.
20. An inbound subject matching `SO-HD/` + four digits + `/` + digits is treated as an existing ticket. **Implemented** (`TICKET_REF_RE`).
21. Outbound subjects that do not already contain the ticket reference are rewritten to `[{ticket_ref}] {subject}`. **Implemented**.
22. Reply-to uses the mailbox from-name and from-address when the ticket has `mail_server_id`. **Implemented**.
23. Internal notes (`mail.mt_note`) do not add the customer as a follower. Other comments do, until the ticket is closed. **Implemented**. **Documented**.

### Who may see tickets

24. Helpdesk User: read all tickets, no write/create/unlink on that rule. **Implemented**.
25. Helpdesk Agent: tickets whose team lists them as a member or whose leader is them. **Implemented**. Agents also inherit the User read-all rule, so read is broader than write. **Inferred** from Odoo combining group rules with OR. Confirm in a running database.
26. Helpdesk Manager: all tickets. **Implemented**.
27. Portal: tickets whose partner is a child of the user's commercial partner. **Implemented**. The portal controller only renders a ticket when `partner_id` equals the current user's partner. **Implemented**. The two checks differ. **Requires Confirmation** for contacts under one commercial partner.
28. On install, every active non-portal user is added to Helpdesk Agent. **Implemented**. This is a behaviour rule, not a sentence in the helpdesk doc. The doc says to assign Agent or Manager by hand.

### Public form

29. `/helpdesk/submit` is public and CSRF-protected. **Implemented**.
30. The form marks subject, team, email, and description required. **Implemented** in the template. The controller does not repeat those checks. **Inferred**.

## My Company

31. A kudos post must name a recipient. **Implemented**.
32. Feed order is pinned first, then newest. **Implemented**.
33. Notifications go to tagged partners, the kudos recipient, and users in the selected departments, excluding the author. **Implemented**.
34. Engagement reminders are weekly and only for active internal users with no post in the past 7 days. They run only when `my_company.engagement_reminders` is the string `True`. Missing parameter is treated as on. **Implemented**.
35. Hub display name defaults to "My Company" and is written onto the root menu and the hub action when settings are saved. **Implemented**. **Documented**.
36. "Default startup for new users" also assigns the hub as home for active internal users who currently have no home action. The setting name says new users. The code updates existing users with an empty `action_id`. **Implemented**. **Requires Confirmation** whether that wider write is intended.
37. Every internal user is a My Company User. **Implemented** by extending `base.group_user`.
38. Column widths are stored in the browser. **Documented**. Not re-verified in JavaScript for this document.

Roadmap items in `docs/my_company.md` are not rules of the current system.

## AI assistant

39. Default bot name is Udi when the user preference is empty. **Implemented**. **Documented**.
40. Users see only their own conversations and messages. AI Administrators see all. **Implemented**. **Documented**.
41. Every internal user is an AI User. The admin user is an AI Administrator in module data. **Implemented**.
42. Chat is refused when the API key is empty. **Implemented**.
43. Default sampling is temperature 0.7 and max tokens 2048. Default endpoint is `https://api.openai.com/v1`. **Implemented**.
44. If no system prompt is configured, the system text identifies the bot, the user, and the company and asks for concise, professional help. Active card rails and the conversation memory are appended. **Implemented**.
45. OpenAI calls time out after 60 seconds. **Implemented**.
46. Image generation, streaming, email drafting, and a knowledge base are not current rules. **Documented** as v2 roadmap.

## Timesheets

47. New analytic lines are billable by default. **Implemented**.
48. `eit_billing_id` and `convergence_time_id` are readonly and not copied. **Implemented**.
49. Convergence push runs only when the URL parameter is set and the line has no Convergence id. Relevant field changes do not push again after an id exists. **Implemented**.
50. Convergence failure does not block saving the timesheet. **Implemented**. **Inferred** product effect: hours can exist in Odoo and be missing in Convergence.
51. EIT push skips lines that already have a billing id. **Implemented**.
52. EIT push cannot start without `jb_timesheet.eit_api_url`. **Implemented**.
53. Client EIT id is the line's partner, otherwise the project's customer. **Implemented**.
54. Mark-invoiced requires at least one line and a date. It does not call EIT. **Implemented**.
55. Timesheet Users only see analytic lines where `user_id` is themselves, and tasks they are assigned to or created. They cannot delete those tasks through this rule. Managers see everything. **Implemented**. Interaction with standard Odoo rules is **Requires Confirmation**.
56. Invoice Manager does not imply Timesheet Manager. **Implemented**. **Inferred**: an invoice manager who is not a timesheet manager is still limited to their own lines by the user rule.
57. `remind_count` is manager-only on the field. **Implemented**.
58. Weekly reminders go to employees with `include_in_office_target` (default true) who have a user and zero analytic lines from Monday to Sunday. **Implemented**.
59. `remind_count` resets for all employees on the last calendar day of the month, not on a billing cycle. **Implemented**.
60. Overrun mail considers tasks with `allocated_hours` > 0 whose stage is not folded. One email per run, at the highest threshold among 100, 75, and 50 whose flag is still false. **Implemented**. **Inferred**: lower thresholds can still fire on a later day after a 100% mail.
61. Office target inclusion defaults to true. Monthly target hours default to 0. **Implemented**. A target of 0 means progress math should be read off the report code before assuming a divide-by-zero. The report Python was not fully quoted here.
62. A project may be marked a template. "Created from template" only offers projects with `is_template`. **Implemented**. No automatic copy method was read. Whether choosing a template clones tasks is **Unknown**.
63. The create-ticket wizard requires a project, a name, and at least one source task. It creates a parent `project.task` with `is_ticket` and one subtask per source task. **Implemented**. It writes `planned_hours`. The progress field reads `allocated_hours`. **Requires Confirmation**.
64. This task-ticket is not an `so.helpdesk.ticket`. **Inferred** from separate models with no shared create path in the files that were read.

## Time Sheet V2

65. Week start must be a Monday. **Implemented**.
66. One weekly plan per employee per week start. **Implemented** (SQL unique).
67. A to-do requires a name. Project choices are limited to the selected client's projects. **Implemented**.
68. Logged hours sum analytic lines for that employee and week. If the to-do has a project, the domain adds that project. The domain always requires `project_id` to be set, so time with no project is not counted. **Implemented**. **Inferred** from the domain in `_compute_actual_hours`.
69. Variance is logged minus planned. **Implemented** (field help).
70. Users see their own plans (employee's user). Managers see all plans. **Implemented**.
71. Team weekly plans and the office target report are manager menus. **Implemented**.

## Mail inbox

72. A message is unique per server and IMAP UID, and unique per server and Message-ID. **Implemented**.
73. The mailbox address is removed from the participant set before contact matching. **Implemented**.
74. A contact is matched on `email_normalized` or case-insensitive `email`. **Implemented**.
75. Each contact and each lead receives at most one log note per synced message. **Implemented**.
76. Notes are internal (`mail.mt_note`), not emails to the customer. **Implemented**.
77. CRM routing defaults on. Creating a lead when unmatched defaults on. **Implemented**.
78. A created lead is type `lead`, name from subject or sender, description capped at 2000 characters of the text body. **Implemented**.
79. Inactive leads are not matched (`active` = True). **Implemented**.
80. Inbox users may read and write messages and may not create or delete them. Managers may. **Implemented**.
81. All inbox users see all messages (shared inbox, not per-user). **Implemented**.

## Production and access

82. Production `web.base.url` is set from `SODOO_WEB_BASE_URL` on startup when the database exists. Compose sets that variable to `https://sodoo.appnotify.co.za`. **Implemented**.
83. An empty production database is loaded once from `odoo_full.zip` if the bootstrap flag is absent. An existing database named `odoo` is left in place. **Implemented**.
84. Helpdesk, My Company upgrade, and AI assistant install run on production start. Timesheet and mail inbox do not. **Implemented**.
85. Local development compose uses database user `odoo` and a fixed password written in `docker-compose.yml` and `config/odoo.conf`. **Implemented**. That password is a local default, not evidence of the production secret.

## Explicit non-rules

- No written approval matrix for who may close a ticket, beyond group write access.
- No rule that a return ticket must have a product. `product_id` is optional. **Implemented**.
- No rule that timesheets must be pushed to EIT before invoice. Mark-invoiced and EIT push are separate actions. **Implemented**.
- No SonarQube quality gate is defined in this repository. **Implemented** by absence.
