TNCSC_Odoo/addons/community_school/tests/test_school_registration.py
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

85 lines
3.9 KiB
Python

from odoo.tests.common import TransactionCase, tagged
@tagged('post_install', '-at_install')
class TestSchoolRegistration(TransactionCase):
def setUp(self):
super().setUp()
self.term = self.env['community.school.term'].create({
'name': 'Reg Term', 'start_date': '2030-09-01', 'end_date': '2030-12-15',
})
self.level = self.env['community.school.level'].create({'name': 'Beginner', 'code': 'REG-BEG'})
self.klass = self.env['community.school.class'].create({
'level_id': self.level.id, 'term_id': self.term.id, 'max_students': 1, 'fee': 100.0,
})
def _make_student(self, name):
parent = self.env['res.partner'].create({'name': f'{name} Parent', 'email': f'{name.lower()}@example.com'})
child = self.env['res.partner'].create({'name': name})
return self.env['community.school.student'].create({
'partner_id': child.id, 'parent_partner_id': parent.id,
})
def test_enrollment_creates_invoice_when_class_has_fee(self):
student = self._make_student('Fee Student')
enrollment = self.env['community.school.enrollment'].create({
'student_id': student.id, 'class_id': self.klass.id,
})
invoice = enrollment._create_fee_invoice()
self.assertTrue(invoice)
self.assertEqual(invoice.state, 'draft')
self.assertEqual(enrollment.payment_state, 'invoiced')
def test_full_class_waitlists_second_student(self):
student1 = self._make_student('First Student')
student2 = self._make_student('Second Student')
enrollment1 = self.env['community.school.enrollment'].create({
'student_id': student1.id, 'class_id': self.klass.id,
})
self.assertEqual(enrollment1.state, 'enrolled')
# max_students=1, so a manual second enrollment past capacity should be
# created as waitlist by the controller logic; simulate that here.
state = 'enrolled' if self.klass.enrolled_count < self.klass.max_students else 'waitlist'
enrollment2 = self.env['community.school.enrollment'].create({
'student_id': student2.id, 'class_id': self.klass.id, 'state': state,
})
self.assertEqual(enrollment2.state, 'waitlist')
def test_waitlist_promoted_when_seat_frees(self):
student1 = self._make_student('Withdraw Student')
student2 = self._make_student('Waitlist Student')
enrollment1 = self.env['community.school.enrollment'].create({
'student_id': student1.id, 'class_id': self.klass.id,
})
enrollment2 = self.env['community.school.enrollment'].create({
'student_id': student2.id, 'class_id': self.klass.id, 'state': 'waitlist',
})
enrollment1.write({'state': 'withdrawn'})
self.klass.invalidate_recordset()
self.assertEqual(self.klass.enrolled_count, 0)
mail_count_before = self.env['mail.mail'].search_count([])
self.env['community.school.enrollment']._cron_promote_waitlist()
self.assertEqual(enrollment2.state, 'enrolled')
mail_count_after = self.env['mail.mail'].search_count([])
self.assertGreater(mail_count_after, mail_count_before)
def test_lms_enrol_and_unenrol(self):
student = self._make_student('LMS Student')
enrollment = self.env['community.school.enrollment'].create({
'student_id': student.id, 'class_id': self.klass.id,
})
channel = self.klass.slide_channel_id
membership = self.env['slide.channel.partner'].sudo().search([
('channel_id', '=', channel.id), ('partner_id', '=', student.partner_id.id),
])
self.assertTrue(membership, "Enrolling should add the student to the class's slide channel")
enrollment.write({'state': 'withdrawn'})
membership.invalidate_recordset()
self.assertFalse(membership.active, "Withdrawing should deactivate the LMS membership")