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>
80 lines
3.0 KiB
Python
80 lines
3.0 KiB
Python
from dateutil.relativedelta import relativedelta
|
|
|
|
from odoo import fields
|
|
from odoo.tests.common import TransactionCase, tagged
|
|
|
|
|
|
@tagged('post_install', '-at_install')
|
|
class TestSchool(TransactionCase):
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.term = self.env['community.school.term'].create({
|
|
'name': 'Fall 2030',
|
|
'start_date': '2030-09-01',
|
|
'end_date': '2030-12-15',
|
|
})
|
|
self.level = self.env['community.school.level'].create({
|
|
'name': 'Beginner',
|
|
'code': 'BEG',
|
|
'min_age': 5,
|
|
'max_age': 8,
|
|
})
|
|
self.klass = self.env['community.school.class'].create({
|
|
'level_id': self.level.id,
|
|
'term_id': self.term.id,
|
|
'max_students': 2,
|
|
})
|
|
self.parent = self.env['res.partner'].create({'name': 'Parent One'})
|
|
self.child = self.env['res.partner'].create({'name': 'Child One'})
|
|
self.student = self.env['community.school.student'].create({
|
|
'partner_id': self.child.id,
|
|
'parent_partner_id': self.parent.id,
|
|
'date_of_birth': '2023-01-01',
|
|
})
|
|
|
|
def test_class_auto_creates_slide_channel(self):
|
|
self.assertTrue(self.klass.slide_channel_id, "Creating a class should auto-create a slide channel")
|
|
|
|
def test_class_name_computed(self):
|
|
self.assertEqual(self.klass.name, f"{self.level.name} - {self.term.name}")
|
|
|
|
def test_enrollment_updates_enrolled_count(self):
|
|
self.assertEqual(self.klass.enrolled_count, 0)
|
|
self.env['community.school.enrollment'].create({
|
|
'student_id': self.student.id,
|
|
'class_id': self.klass.id,
|
|
})
|
|
self.klass.invalidate_recordset()
|
|
self.assertEqual(self.klass.enrolled_count, 1)
|
|
|
|
def test_withdrawn_enrollment_does_not_count(self):
|
|
enrollment = self.env['community.school.enrollment'].create({
|
|
'student_id': self.student.id,
|
|
'class_id': self.klass.id,
|
|
})
|
|
enrollment.state = 'withdrawn'
|
|
self.klass.invalidate_recordset()
|
|
self.assertEqual(self.klass.enrolled_count, 0)
|
|
|
|
def test_attendance_rate_computed(self):
|
|
enrollment = self.env['community.school.enrollment'].create({
|
|
'student_id': self.student.id,
|
|
'class_id': self.klass.id,
|
|
})
|
|
self.env['community.school.attendance'].create({
|
|
'enrollment_id': enrollment.id, 'date': '2030-09-08', 'state': 'present',
|
|
})
|
|
self.env['community.school.attendance'].create({
|
|
'enrollment_id': enrollment.id, 'date': '2030-09-15', 'state': 'absent',
|
|
})
|
|
self.env['community.school.attendance'].create({
|
|
'enrollment_id': enrollment.id, 'date': '2030-09-22', 'state': 'present',
|
|
})
|
|
self.assertAlmostEqual(enrollment.attendance_rate, (2 / 3) * 100)
|
|
|
|
def test_age_computed(self):
|
|
dob = fields.Date.today() - relativedelta(years=5, days=1)
|
|
self.student.date_of_birth = dob
|
|
self.assertEqual(self.student.age, 5)
|