Adds the multi-step website registration at /school/register (parent ->
student -> class), carrying state across steps in the request session
(auth='public' - a family with no account yet can register). Finalizing
creates/finds the parent partner by email, creates the child partner +
student, and creates the enrollment as 'enrolled' or 'waitlist' depending
on whether the chosen class.fee_product_id/max_students still has room -
generating a draft fee invoice when the class has a fee (a new
class.fee/fee_product_id, following the same auto-created-product pattern
as community_membership's tiers).
LMS glue: enrollment create/write now auto-enrols the student's partner
into the class's slide.channel via _action_add_members() when state
becomes 'enrolled', and deactivates the slide.channel.partner membership
on withdrawal. An hourly cron (_cron_promote_waitlist) fills freed seats
from the waitlist in enrollment-date order and emails the parent.
Adds scripts/migrate_classroom.py: a standalone, dependency-free (stdlib
only) JSON-RPC script that reads a title,url CSV and creates slide.slide
records in a target channel - YouTube links become published video slides
(source_type='external' lets Odoo's own compute fields resolve youtube_id
automatically), everything else becomes an unpublished document slide
flagged for manual re-upload/review, since the script can't read Google
Drive content itself. Idempotent by (channel_id, title); --dry-run and
--force supported.
Verified against a live Odoo 19 + Postgres 16 container: 14/14 automated
tests pass, plus full manual live runs - walked all three registration
steps over real HTTP (with CSRF tokens) and confirmed the resulting
enrollment and draft invoice; ran the migration script twice against a
real channel and confirmed the second run skipped both already-created
slides, with the YouTube slide's youtube_id correctly auto-derived.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Session 3-A - core models: community.school.term, .level (admin-defined,
so the same module fits a Grade 1-12 school or a Beginner-Advanced
language school with no code change), .class (auto-creates a linked
slide.channel for LMS glue), .student, .enrollment, and .attendance, plus
is_teacher on res.partner. Class enrolled_count and enrollment
attendance_rate are stored/non-stored computes driven by the
enrollment/attendance one2many chains.
Session 3-B - teacher attendance + at-risk reporting: a portal page at
/school/attendance (auth='user', scoped to classes where teacher_id
matches the logged-in user's partner - works whether the teacher is an
internal or portal user) with a roster and batch save, built as a v19
Interaction (same pattern as event_qr_ticketing's check-in page). Marking
a student absent queues a configurable notice to the parent partner.
Enrollment.is_at_risk flags students below a configurable attendance
threshold (School settings page, same <app>/<block>/<setting> pattern as
community_membership), plus an admin pivot attendance report and an
At-Risk Students list.
Verified against a live Odoo 19 + Postgres 16 container: 14/14 automated
tests pass (class/slide-channel creation, enrolled_count tracking incl.
withdrawal, attendance_rate and at-risk computation, unique
enrollment+date constraint, absence email queuing), plus a full manual
live run - created a term/level/class/student/enrollment via JSON-RPC,
loaded the actual /school/attendance page as the teacher, POSTed a batch
save marking the student absent, and confirmed both the attendance record
and the queued "Absence notice" mail.mail record.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>