# 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. - **Correction (found building O3):** `res.users.groups_id` is gone too — the field is now `group_ids`. Same `[(6, 0, [group_ids...])]` Many2many-write syntax otherwise. Matters for every test that creates a user in a specific group (see `addons/mc_education_fees/tests/test_access.py`), and will matter again for O7's portal users. - `_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.