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>
33 lines
1.2 KiB
Python
33 lines
1.2 KiB
Python
from dateutil.relativedelta import relativedelta
|
|
|
|
from odoo import api, fields, models
|
|
|
|
|
|
class CommunitySchoolStudent(models.Model):
|
|
_name = 'community.school.student'
|
|
_description = 'School Student'
|
|
_order = 'id desc'
|
|
|
|
partner_id = fields.Many2one('res.partner', string='Student', required=True)
|
|
parent_partner_id = fields.Many2one('res.partner', string='Parent/Guardian', required=True)
|
|
date_of_birth = fields.Date()
|
|
age = fields.Integer(compute='_compute_age')
|
|
proficiency = fields.Selection(
|
|
[('beginner', 'Beginner'), ('intermediate', 'Intermediate'), ('advanced', 'Advanced')],
|
|
default='beginner',
|
|
)
|
|
grade_ref = fields.Char(string='Grade', help="Free-text grade reference, e.g. 'Grade 3'.")
|
|
health_notes = fields.Text()
|
|
emergency_contact_name = fields.Char()
|
|
emergency_contact_phone = fields.Char()
|
|
enrollment_ids = fields.One2many('community.school.enrollment', 'student_id')
|
|
|
|
@api.depends('date_of_birth')
|
|
def _compute_age(self):
|
|
today = fields.Date.context_today(self)
|
|
for student in self:
|
|
if student.date_of_birth:
|
|
student.age = relativedelta(today, student.date_of_birth).years
|
|
else:
|
|
student.age = 0
|