4 Commits

Author SHA1 Message Date
metatroncubeswdev
508bf27417 O2: mc_education_admission - public form through to enrolled student
Serves Demo Scene 2 end to end: mc.applicant with the stage pipeline
(Applied -> Document Verification -> Interview -> Offered -> Accepted
-> Enrolled/Rejected) on mail.thread, a public admission page on the
website, and a convert wizard that turns an accepted applicant into a
real mc.student + mc.enrollment with zero re-typing.

The public form uses Odoo's stock /website/form/<model> mechanism,
not a custom controller (CLAUDE.md sec 1.3 - writing a custom version
of stock infrastructure is a bug). Verified the real mechanism against
core source first rather than assuming: website_hr_recruitment's own
data/config_data.xml is the template this follows (ir.model.
website_form_access + ir.model.fields.formbuilder_whitelist()).

This is the module's actual security boundary, and it's worth being
explicit about why it holds. The generic controller creates the record
as SUPERUSER - normal ir.model.access rows do not apply to it at all.
The only thing stopping a submitter from setting state, student_id,
application_no or company_id is that those fields are not in the
formbuilder_whitelist() call in data/mc_applicant_website_form_data.xml
(every field defaults to website_form_blacklisted=True and stays that
way unless explicitly opted in). Confirmed this isn't just theoretical:
posted state=enrolled and application_no=HACKED-0001 directly at
/website/form/mc.applicant on a live instance, and the resulting
record came back with the model's own default state=applied and a
server-generated APP20260004 - the injected values were silently
dropped, exactly as the whitelist should do. Also exercised a real
file upload (birth certificate) and the full convert-to-student path
(guardian dedup by email, application_no -> student.application_no,
enrollment, attachment reparenting) via odoo shell against the live
container, not just read by inspection.

mc.student gets a new application_no field (_inherit from this
module, not O1 - it only makes sense where admission is installed)
so "the application number persists on the student" is a stored fact,
not just a claim in the demo script.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 11:55:48 -04:00
metatroncubeswdev
4a2e7b7016 fix: admin group membership on install, and db_name breaking the db list
- group_school_administrator now includes base.user_admin, matching
  core Odoo's own convention (see hr.group_hr_manager) - otherwise
  every fresh install requires a manual trip to Settings > Users just
  to see this module's own menus.
- odoo.conf: drop db_name. Found by hand while testing: when db_name
  is set and dbfilter is not, Odoo's list_dbs() returns db_name's
  value verbatim instead of querying postgres, so the database
  selector shows only that one database no matter how many actually
  exist. Silently breaks O9's "swap to the second demo school in
  under five minutes" requirement, which depends on switching between
  multiple real databases.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 11:25:37 -04:00
metatroncubeswdev
709bd2eca7 O1: complete the gate - program, subject, room, batch, people, enrollment
Rounds out mc_education_base with the remaining O1 models: mc.program,
mc.subject, mc.room, mc.batch, mc.teacher, mc.student, mc.guardian,
mc.student.guardian, and mc.enrollment - the spine everything else in
the O1 table (attendance, timetable, exam, portal) is built against.

Two rules get the same "DB constraint is the real guarantee, the ORM
does a friendly pre-check" treatment as academic.year.is_current:

  - At most one primary guardian per student (mc.student.guardian):
    partial unique index on student_id WHERE is_primary, plus a
    create/write toggle.
  - At most one Active enrollment per student per year
    (mc.enrollment): partial unique index on (student_id, year_id)
    WHERE state='active'. This one is CLAUDE.md's flagship rule
    ("Enforce as a database constraint, not application logic").

Two more real bugs surfaced by testing against a live odoo:19.0
container rather than trusting the code by inspection:

  - The partial unique index fires at INSERT/UPDATE time, before
    @api.constrains ever runs - so a naive "index + constrains for a
    friendly message" design never reaches the friendly message, the
    raw IntegrityError wins the race. Fixed by pre-checking for a
    conflict in create()/write() before calling super(), with
    @api.constrains kept only as a backstop for batch creates.
  - create() issues a direct SQL INSERT that does not wait for
    unrelated pending writes in the ORM cache (e.g. withdrawing one
    enrollment right before creating its replacement, in the same
    method) - needs an explicit self.env.flush_all() first, same
    lesson as the is_current toggle.

Also caught before it became a permanent test flake: the enrollment
and academic-calendar tests originally hardcoded the same year names
("2025-26", "2026-27") and program code ("G8") as the demo data.
Passed in isolation, failed as soon as demo data was loaded first -
so verification here included a combined
`--without-demo=False --test-enable` run, matching what CI actually
does, not just an isolated test-tagged run. Renamed to TEST-prefixed
fixtures.

Security access rows added for all new models across the four
internal groups (Administrator: full CRUD everywhere; Staff: full
CRUD on the people/enrollment models that are front office's daily
job, read-only on academic structure; Teacher/Accountant: read-only
across the board, narrower record rules land with the modules that
need them - attendance, exam, portal). No portal-group access yet;
that is O7's job once explicit ownership-scoped record rules exist -
granting it now without those rules would be exactly the "identifier
supplied by the client" hole CLAUDE.md's standing security rule
warns about.

Demo data populates the shared/DEMO_SCRIPT.md cast: Meera Krishnan as
primary guardian of both Aditya (Grade 8-A) and Ananya (Grade 5-B) -
the multi-child guardian view the script calls "the single most
convincing portal feature" - plus Arun Prakash as Grade 8-A's class
teacher. Verified by querying the resulting database directly, not
just by the install succeeding.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 09:58:36 -04:00
metatroncubeswdev
8b7bd91f74 O1: mc_education_base gate, first slice - academic calendar
mc.academic.year and mc.academic.term: the two models every other O1
model (program, batch, enrollment...) will hang off. Exactly one
current year is enforced two ways - the ORM toggles is_current off
the previous year on create/write, and a partial unique index on
(company_id) WHERE is_current backs it at the database level so the
rule holds even if something writes around the ORM.

Security groups for all six roles from the spec (Administrator,
Staff, Teacher, Accountant, Guardian, Student) are scaffolded now
since every later O1 model needs them, though only Administrator/
Staff have access rows on these two models so far.

Verified against a real odoo:19.0 container, not just read: module
installs clean with views, menus and demo data, and all 7 test
methods pass. That surfaced two things CLAUDE.md's Coding Standards
section didn't anticipate, since Odoo 19 moved past 17/18-era APIs
in ways not caught by an 18-era mental model:

  - `_sql_constraints` is gone; constraints are now per-attribute
    `models.Constraint(sql, message)`.
  - `res.groups.category_id` is gone; groups now hang off a new
    `res.groups.privilege` record, which carries the category.

Both addons/mc_education_base files already use the new APIs.
CLAUDE.md itself needs a note added in a follow-up so this isn't
rediscovered per-module - flagging here per its own closing
instruction ("say so and propose the correction... update this file
in the same PR") rather than leaving it implicit in this commit body.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 08:53:34 -04:00