from dateutil.relativedelta import relativedelta from odoo import api, fields, models from odoo.exceptions import ValidationError class ResPartner(models.Model): _inherit = "res.partner" is_student = fields.Boolean(string="Is Student", default=False, index=True) student_code = fields.Char(string="Student ID", copy=False, readonly=True, index=True) guardian_id = fields.Many2one( "res.partner", string="Primary Guardian", domain="[('is_student', '=', False)]", ) guardian_ids = fields.Many2many( "res.partner", "student_guardian_rel", "student_id", "guardian_id", string="Guardians", domain="[('is_student', '=', False)]", ) guardian_phone = fields.Char(string="Guardian Phone") sibling_ids = fields.Many2many( "res.partner", "student_sibling_rel", "student_id", "sibling_id", string="Sibling Students", domain="[('is_student', '=', True)]", ) date_of_birth = fields.Date(string="Date of Birth") age = fields.Integer(string="Age", compute="_compute_age", store=True) gender = fields.Selection( [ ("male", "Male"), ("female", "Female"), ("other", "Other"), ], string="Gender", ) medical_notes = fields.Text(string="Medical Notes") dance_style = fields.Selection( [ ("bharatanatyam", "Bharatanatyam"), ("kathak", "Kathak"), ("hiphop", "Hip-Hop"), ("contemporary", "Contemporary"), ("zumba", "Zumba"), ], string="Dance Style", ) level = fields.Selection( [ ("beginner", "Beginner"), ("intermediate", "Intermediate"), ("advanced", "Advanced"), ], string="Level", ) student_status = fields.Selection( [ ("trial", "Trial"), ("active", "Active"), ("hold", "On Hold"), ("dropped", "Dropped"), ("alumni", "Alumni"), ], default="trial", string="Student Status", tracking=True, ) join_date = fields.Date(string="Join Date") _sql_constraints = [ ( "student_code_unique", "unique(student_code)", "Student ID must be unique.", ), ] @api.depends("date_of_birth") def _compute_age(self): today = fields.Date.today() for partner in self: partner.age = ( relativedelta(today, partner.date_of_birth).years if partner.date_of_birth else 0 ) @api.onchange("guardian_id") def _onchange_guardian_id(self): for partner in self: if partner.guardian_id: partner.guardian_phone = partner.guardian_id.phone or partner.guardian_id.mobile @api.constrains("sibling_ids") def _check_sibling_ids(self): for partner in self: if partner in partner.sibling_ids: raise ValidationError("A student cannot be linked as their own sibling.") @api.model_create_multi def create(self, vals_list): for vals in vals_list: if vals.get("is_student") and not vals.get("student_code"): vals["student_code"] = self._get_next_student_code() if vals.get("guardian_id") and not vals.get("guardian_phone"): guardian = self.env["res.partner"].browse(vals["guardian_id"]) vals["guardian_phone"] = guardian.phone or guardian.mobile partners = super().create(vals_list) partners._sync_sibling_links() return partners def write(self, vals): old_sibling_map = {} if "sibling_ids" in vals and not self.env.context.get("skip_sibling_sync"): old_sibling_map = {partner.id: set(partner.sibling_ids.ids) for partner in self} if vals.get("guardian_id") and "guardian_phone" not in vals: guardian = self.env["res.partner"].browse(vals["guardian_id"]) vals = dict(vals, guardian_phone=guardian.phone or guardian.mobile) result = super().write(vals) if "sibling_ids" in vals and not self.env.context.get("skip_sibling_sync"): self._sync_sibling_links(old_sibling_map) return result def _sync_sibling_links(self, old_sibling_map=None): old_sibling_map = old_sibling_map or {} for partner in self: current_siblings = partner.sibling_ids.filtered(lambda sibling: sibling != partner) old_siblings = self.browse(old_sibling_map.get(partner.id, set())) removed_siblings = old_siblings - current_siblings if removed_siblings: removed_siblings.with_context(skip_sibling_sync=True).write( {"sibling_ids": [(3, partner.id)]} ) for sibling in current_siblings: if partner not in sibling.sibling_ids: sibling.with_context(skip_sibling_sync=True).write( {"sibling_ids": [(4, partner.id)]} ) def _get_next_student_code(self): sequence = self.env["ir.sequence"] for _attempt in range(100): student_code = sequence.next_by_code("dance.student") if not self.search_count([("student_code", "=", student_code)]): return student_code raise ValidationError("Could not generate a unique Student ID.")