metatroncubeswdev e05448b939 feat(community_school): multi-step registration, waitlist, LMS glue (Session 3-C)
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>
2026-08-17 21:42:51 -04:00

44 lines
2.0 KiB
XML

<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="mail_template_absence_notice" model="mail.template">
<field name="name">School: Absence Notice</field>
<field name="model_id" ref="model_community_school_attendance"/>
<field name="subject">Absence notice: {{ object.enrollment_id.student_id.partner_id.name }}</field>
<field name="partner_to">{{ object.enrollment_id.student_id.parent_partner_id.id }}</field>
<field name="auto_delete" eval="True"/>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px; font-size: 13px;">
<p>Dear <t t-out="object.enrollment_id.student_id.parent_partner_id.name or ''">Parent</t>,</p>
<p>
This is to let you know that
<t t-out="object.enrollment_id.student_id.partner_id.name or ''"/>
was marked absent from
<t t-out="object.class_id.name or ''"/>
on <t t-out="format_date(object.date)"/>.
</p>
<p t-if="object.notes">Notes: <t t-out="object.notes"/></p>
</div>
</field>
</record>
<record id="mail_template_seat_available" model="mail.template">
<field name="name">School: Waitlist Seat Available</field>
<field name="model_id" ref="model_community_school_enrollment"/>
<field name="subject">A seat opened up in {{ object.class_id.name }}</field>
<field name="partner_to">{{ object.student_id.parent_partner_id.id }}</field>
<field name="auto_delete" eval="True"/>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px; font-size: 13px;">
<p>Dear <t t-out="object.student_id.parent_partner_id.name or ''">Parent</t>,</p>
<p>
Good news - a seat has opened up in
<t t-out="object.class_id.name or ''"/>
and <t t-out="object.student_id.partner_id.name or ''"/> has been moved from the
waitlist to enrolled.
</p>
</div>
</field>
</record>
</data>
</odoo>