Coding standards said "No APIs deprecated in 17 or 18" as if that were sufficient - it isn't, because it's phrased as a non-regression check against an assumed-correct baseline, and the baseline itself (an 18-era mental model of _sql_constraints and res.groups) was already wrong for 19. Verified against a real odoo:19.0 container while building mc_education_base: _sql_constraints is replaced by per-attribute models.Constraint(sql, message), and res.groups lost category_id in favor of a new res.groups.privilege record referenced via privilege_id. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
295 lines
14 KiB
Markdown
295 lines
14 KiB
Markdown
# CLAUDE.md — Track O: School ERP on Odoo 19 Community
|
||
|
||
Master build spec for Claude Code. Read this fully before writing any code.
|
||
|
||
**Also read, once, before starting:**
|
||
- `../shared/DOMAIN_MODEL.md` — entities and naming. Frozen. Do not deviate.
|
||
- `../shared/DEMO_SCRIPT.md` — the eight scenes. This is the definition of done.
|
||
- `../IMPLEMENTATION_PLAN.md` §2 for workstream order.
|
||
|
||
**Never read or modify anything under `../Development/`.** That is the Frappe track, a separate
|
||
product. Its conventions do not apply here and its code must not be referenced.
|
||
|
||
---
|
||
|
||
## 1. Non-negotiable constraints
|
||
|
||
1. **Odoo 19.0 Community only.** No Enterprise module may appear in any dependency list, ever. If a
|
||
feature seems to need one, it is out of scope or built from scratch — flag it, do not silently
|
||
add the dependency. Verified absent from Community: `hr_payroll`, `account_accountant`,
|
||
`account_reports`, `whatsapp`, `sign`, `documents`, `hr_appraisal`, `stock_barcode`.
|
||
2. **This is Metatroncube IP.** Do not vendor, copy or adapt code from OpenEduCat or any other
|
||
LGPL/AGPL education addon. Models are designed from `../shared/DOMAIN_MODEL.md`, not ported.
|
||
3. **Use stock Odoo where it fits.** `account` for all invoicing, `website_slides` for the entire
|
||
LMS, `fleet` for vehicles, `portal` for external users, `hr` and `hr_holidays` for staff,
|
||
`payment_*` for gateways, `survey` for quizzes. Writing a custom version of any of these is a bug.
|
||
4. **Addon suite, not a monolith.** Each `mc_education_*` module installs independently given its
|
||
declared dependencies, and ports to Odoo 20 independently.
|
||
5. **No Studio.** It is Enterprise. Every view, field and report is code in this repo.
|
||
|
||
---
|
||
|
||
## 2. Environment
|
||
|
||
```
|
||
Odoo 19.0 Community (github.com/odoo/odoo, branch 19.0)
|
||
Python 3.12
|
||
PostgreSQL 16
|
||
Node 20 (for asset bundling)
|
||
```
|
||
|
||
Repo layout:
|
||
|
||
```
|
||
Odoo/
|
||
├── CLAUDE.md ← this file
|
||
├── docker-compose.yml
|
||
├── .env.example
|
||
├── odoo.conf
|
||
├── scripts/
|
||
│ ├── setup.sh one-command bring-up
|
||
│ ├── update.sh rebuild + upgrade modules
|
||
│ ├── backup.sh pg_dump + filestore
|
||
│ └── demo-data.sh load the demo school
|
||
├── addons/
|
||
│ ├── mc_education_base/
|
||
│ ├── mc_education_admission/
|
||
│ ├── mc_education_fees/
|
||
│ ├── mc_education_attendance/
|
||
│ ├── mc_education_timetable/
|
||
│ ├── mc_education_exam/
|
||
│ ├── mc_education_portal/
|
||
│ ├── mc_education_lms/
|
||
│ └── mc_education_theme/
|
||
└── third_party/ OCA addons, pinned by commit SHA
|
||
```
|
||
|
||
`update.sh` **must** call `backup.sh` before any module upgrade, and record the previous image tag
|
||
for rollback. This is a lesson already paid for on the Frappe track — do not repeat it.
|
||
|
||
Pin every third-party addon to a commit SHA, never a branch. A moving branch inside a pinned image
|
||
breaks the build later with no obvious cause.
|
||
|
||
---
|
||
|
||
## 3. Standing security rule
|
||
|
||
> **Never trust an identifier supplied by the client.**
|
||
|
||
Every controller route and every method reachable from the portal that accepts a student, guardian,
|
||
invoice or enrollment identifier must resolve it against `request.env.user` and raise `AccessError`
|
||
for anything the user is not entitled to.
|
||
|
||
Concretely, for this codebase:
|
||
|
||
- **Every portal-reachable model gets a record rule.** No exceptions, including models you think are
|
||
only reached indirectly. Write the rule in the same commit as the model.
|
||
- **`sudo()` requires a comment stating what was checked immediately above it.** An uncommented
|
||
`sudo()` is treated as a defect in review.
|
||
- **Guardian access is by relationship, not by role.** A guardian sees a student because a
|
||
`mc.student.guardian` link exists between them, never because they hold the Guardian group.
|
||
- **Write a test for each rule.** The test asserts that guardian A cannot read student B's invoice,
|
||
attendance or marks. These tests are not optional and run in CI.
|
||
|
||
Scene 5 of the demo script requires showing a refused access attempt live on the call. Build toward
|
||
that being true, not staged.
|
||
|
||
---
|
||
|
||
## 4. Coding standards
|
||
|
||
- Odoo 19 conventions throughout. No APIs deprecated in 17 or 18.
|
||
- **Correction (found building O1, verified against a real `odoo:19.0` container — an 18-era
|
||
mental model gets both of these wrong):**
|
||
- `_sql_constraints = [(name, sql, message), ...]` is gone. Declare each constraint as its own
|
||
class attribute instead: `_my_constraint = models.Constraint(sql, message)`. The rule below
|
||
("prefer a database constraint when the database can express it") still holds — only the
|
||
syntax changed.
|
||
- `res.groups.category_id` is gone. A group now points at a `res.groups.privilege` record via
|
||
`privilege_id`, and the privilege carries `category_id` (still an `ir.module.category`).
|
||
Create one `res.groups.privilege` per module category and point every group at it. See
|
||
`addons/mc_education_base/security/mc_education_security.xml` for the pattern.
|
||
- `_name`, `_description` and `_order` on every model. `_rec_name` where the display field is not
|
||
`name`.
|
||
- Computed fields declare `@api.depends` accurately and are `store=True` only when they need to be
|
||
searched or grouped. A stored compute with wrong depends is a silent data-corruption bug.
|
||
- Constraints: prefer a database constraint (`models.Constraint`, see above) over `@api.constrains`
|
||
when the database can express it. The "one active enrollment per student per year" rule is a
|
||
database constraint.
|
||
- `ondelete` is explicit on every `Many2one`. Think about whether it should be `restrict`
|
||
(financial and academic history) or `cascade` (child lines).
|
||
- `tracking=True` on fields a school will argue about later: enrollment status, fee amounts, marks,
|
||
attendance status.
|
||
- Every user-facing string wrapped for translation. The product ships in English now and will need
|
||
Tamil and French later — retrofitting i18n is miserable.
|
||
- `ir.model.access.csv` in the same commit as the model. Never a follow-up.
|
||
- Demo data in `demo/`, and it must be realistic per `../shared/DEMO_SCRIPT.md` — real-looking Indian
|
||
and Canadian names, plausible amounts, a full term of history.
|
||
|
||
Testing:
|
||
|
||
```bash
|
||
docker compose exec odoo odoo -d school --test-enable --stop-after-init -i mc_education_base
|
||
```
|
||
|
||
Write tests for: money arithmetic, grade computation from scales, enrollment constraints, and every
|
||
access rule. Do not write tests for view layouts.
|
||
|
||
---
|
||
|
||
## 5. Module specifications
|
||
|
||
Build in this order. `mc_education_base` is a gate — get it reviewed before fanning out.
|
||
|
||
### O1 · `mc_education_base`
|
||
|
||
Depends: `base`, `mail`, `contacts`, `hr`
|
||
|
||
| Model | Key fields |
|
||
|-------|-----------|
|
||
| `mc.academic.year` | `name` (`2026-27`), `date_start`, `date_end`, `is_current` |
|
||
| `mc.academic.term` | `name`, `year_id`, `date_start`, `date_end`, `sequence` |
|
||
| `mc.program` | `name`, `code`, `sequence_no` (sorting), `display_label`, `board`, `company_id` |
|
||
| `mc.subject` | `name`, `code`, `program_ids`, `is_elective` |
|
||
| `mc.batch` | `name`, `program_id`, `year_id`, `class_teacher_id`, `capacity`, `room_id` |
|
||
| `mc.room` | `name`, `capacity`, `building`, `type` |
|
||
| `mc.student` | `partner_id`, `admission_no`, `name`, `dob`, `gender`, `admission_date`, `photo`, `status`, `blood_group`, `address_id` |
|
||
| `mc.guardian` | `partner_id`, `name`, `occupation`, `phone`, `email` |
|
||
| `mc.student.guardian` | `student_id`, `guardian_id`, `relationship`, `is_primary` |
|
||
| `mc.teacher` | `employee_id`, `subject_ids`, `max_weekly_periods` |
|
||
| `mc.enrollment` | `student_id`, `program_id`, `batch_id`, `year_id`, `state`, `roll_no`, `date_enrolled` |
|
||
|
||
Requirements:
|
||
|
||
- Exactly one `mc.academic.year` may have `is_current = True`. Enforce it.
|
||
- One Active `mc.enrollment` per student per year — SQL constraint.
|
||
- `mc.program.sequence_no` drives sort order everywhere. Never sort grades by label.
|
||
- `admission_no` uses an `ir.sequence` configurable per company.
|
||
- Security groups: `School Administrator`, `School Staff`, `Teacher`, `Accountant`,
|
||
`Guardian` (portal), `Student` (portal).
|
||
|
||
Acceptance: a student can be created, given a guardian, enrolled in a batch, and the batch roster
|
||
lists them. Constraint tests pass. Access rules exist for all six groups.
|
||
|
||
### O2 · `mc_education_admission`
|
||
|
||
Depends: `mc_education_base`, `website`
|
||
|
||
- `mc.applicant` with a stage pipeline: Applied → Document Verification → Interview → Offered →
|
||
Accepted → Enrolled / Rejected. Use `mail.thread` and stock kanban stages.
|
||
- Public website form via stock `website` form handling. File uploads to `ir.attachment`.
|
||
- **Convert action**: applicant → `mc.student` + `mc.enrollment`, carrying every field and all
|
||
attachments. Zero re-typing. Application number persists on the student.
|
||
- Application number from a configurable `ir.sequence`.
|
||
|
||
Acceptance: Demo Scene 2 runs end to end, public form through to enrolled student.
|
||
|
||
### O3 · `mc_education_fees`
|
||
|
||
Depends: `mc_education_base`, `account`, `payment`
|
||
|
||
- `mc.fee.category` — Tuition, Lab, Library, Transport, Exam
|
||
- `mc.fee.structure` — per program + academic year; lines of category × amount
|
||
- `mc.fee.schedule` — installment plan; term-wise due dates and proportions
|
||
- `mc.fee.concession` — type (Sibling / Merit / Staff / Need-based), percent or fixed, reason,
|
||
approver
|
||
- **Invoices are stock `account.move`**, type `out_invoice`, with `mc_student_id` and
|
||
`mc_enrollment_id` added by `_inherit`. Do not build an invoice model.
|
||
- Concessions appear as negative invoice lines with a reason code. Never a reduced gross.
|
||
- Payment through stock `payment` providers.
|
||
|
||
Acceptance: Demo Scene 3. A structure generates a correct invoice, a sibling concession recalculates
|
||
it, an online payment posts a real journal entry and outstanding drops without a manual refresh.
|
||
|
||
### O4 · `mc_education_attendance`
|
||
|
||
Depends: `mc_education_base`
|
||
|
||
- `mc.attendance` — `student_id`, `date`, `session` (period or Daily), `subject_id`, `state`
|
||
(Present / Absent / Late / On Leave), `marked_by`, `batch_id`
|
||
- Unique constraint on student × date × session. Submitting twice must not duplicate.
|
||
- A bulk marking view: roster for a batch + period, all defaulted Present, exceptions toggled.
|
||
- Mobile-first. This is marked on a phone in a corridor.
|
||
- Teachers may only mark their own batches — record rule, not a UI check.
|
||
|
||
Acceptance: Demo Scene 4. Under three taps per exception. Re-submission is idempotent. A teacher
|
||
cannot open another teacher's batch by editing the URL.
|
||
|
||
### O5 · `mc_education_timetable`
|
||
|
||
Depends: `mc_education_base`
|
||
|
||
- `mc.timetable.slot` — `batch_id`, `weekday`, `period`, `subject_id`, `teacher_id`, `room_id`,
|
||
`year_id`, `term_id`
|
||
- Conflict validation on save: a teacher or a room cannot hold two slots in the same weekday+period.
|
||
- Three read views over one dataset: by batch, by teacher, by student (resolved through enrollment).
|
||
- **Auto-generation is out of scope.** Demo data is configured by hand.
|
||
|
||
Acceptance: Demo Scene 7. Three views, one dataset, readable at phone width.
|
||
|
||
### O6 · `mc_education_exam`
|
||
|
||
Depends: `mc_education_base`
|
||
|
||
- `mc.grading.scale` + `mc.grading.interval` — threshold, letter, point, description.
|
||
**Data rows, never code.** CBSE, ICSE, IB, Cambridge, Ontario and percentage all expressible.
|
||
- `mc.exam` — name, term, batch, subject, max marks, pass marks, date
|
||
- `mc.mark` — student, exam, marks obtained, computed grade
|
||
- Grid mark-entry view: whole class on one screen, tab between fields, keyboard only.
|
||
- QWeb report card: logo, all subjects, marks, grades, attendance summary, remarks, signature block.
|
||
Must be print-clean on A4.
|
||
|
||
Acceptance: Demo Scene 6. Grades compute from the scale. Changing the scale changes the grades with
|
||
no code change. PDF has no clipped columns.
|
||
|
||
### O7 · `mc_education_portal`
|
||
|
||
Depends: `mc_education_fees`, `mc_education_attendance`, `mc_education_timetable`,
|
||
`mc_education_exam`, `portal`
|
||
|
||
- Parent and student portal on stock `portal`. External users cost nothing in Community — this is a
|
||
commercial advantage, use it.
|
||
- **Child switcher** for guardians with more than one child. This is the most convincing single
|
||
feature in the demo; give it real design attention.
|
||
- Views: fees + pay online, attendance with a month calendar, timetable, results, notices.
|
||
- Every controller resolves the student from the authenticated user's guardian links. See §3.
|
||
|
||
Acceptance: Demo Scene 5, including the live refused-access demonstration.
|
||
|
||
### O8 · `mc_education_lms`
|
||
|
||
Depends: `mc_education_base`, `website_slides`
|
||
|
||
Thin glue only. Configure stock `website_slides`; link a channel to a `mc.batch` so enrolled students
|
||
see their courses. **Do not build a custom LMS.** If this module exceeds ~200 lines, something has
|
||
gone wrong.
|
||
|
||
Acceptance: Demo Scene 8.
|
||
|
||
### O9 · `mc_education_theme`
|
||
|
||
Depends: `mc_education_base`
|
||
|
||
- Per-company branding: logo, primary/secondary colour, school name, favicon, report letterhead.
|
||
- Applied to backend, portal, website and every QWeb report.
|
||
- Add OCA `web_responsive` (pinned SHA) — the stock Community backend needs it.
|
||
- **The ERPNext and Odoo brand names must not be visible** anywhere the demo audience will see.
|
||
|
||
Acceptance: the closing beat — swap to the second demo school in under five minutes.
|
||
|
||
---
|
||
|
||
## 6. How to work with me on this
|
||
|
||
- One workstream per branch: `o3-fees`, `o6-exam`.
|
||
- One model, one view, or one controller per task. "Build the fees module" produces shallow work.
|
||
- Quote the demo scene a task serves, so scope stays honest.
|
||
- Always ask for the `ir.model.access.csv` and record rules in the same change as the model.
|
||
- Ask for tests on money, grades, enrollment constraints and access rules. Not on view layouts.
|
||
- I will review every access rule and every migration by hand. Surface them, do not bury them in a
|
||
large diff.
|
||
- Commit at every green state.
|
||
|
||
When something in this spec turns out to be wrong — and some of it will be — say so and propose the
|
||
correction rather than working around it silently. Then update this file in the same PR.
|