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>
85 lines
3.9 KiB
Python
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")
|