import re from datetime import date # pyrefly: ignore [missing-import] from odoo import api, fields, models # pyrefly: ignore [missing-import] from odoo.exceptions import ValidationError class EducationStudent(models.Model): _name = 'education.student' _description = 'Student' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'admission_number desc' _rec_name = 'full_name' active = fields.Boolean(default=True, tracking=True) image = fields.Binary(string='Photo', attachment=True) image_1920 = fields.Image(string='Photo 1920', related='image', store=False) admission_number = fields.Char( string='Admission Number', required=True, copy=False, readonly=True, default=lambda self: self.env['ir.sequence'].next_by_code('education.student.admission'), index=True, ) student_id = fields.Char( string='Student ID', required=True, index=True, copy=False, default=lambda self: self.env['ir.sequence'].next_by_code('education.student.id'), ) name = fields.Char(string='Student Name', compute='_compute_full_name', store=True, index=True) first_name = fields.Char(string='First Name', required=True) last_name = fields.Char(string='Last Name', required=True) middle_name = fields.Char(string='Middle Name') full_name = fields.Char(string='Full Name', compute='_compute_full_name', store=True, readonly=True, index=True) email = fields.Char(string='Email', index=True) phone = fields.Char(string='Phone', index=True) mobile = fields.Char(string='Mobile', index=True) gender = fields.Selection([ ('male', 'Male'), ('female', 'Female'), ('other', 'Other'), ], string='Gender') date_of_birth = fields.Date(string='Date of Birth') age = fields.Integer(string='Age', compute='_compute_age', store=False) place_of_birth = fields.Char(string='Place of Birth') nationality = fields.Char(string='Nationality') blood_group = fields.Selection([ ('a+', 'A+'), ('a-', 'A-'), ('b+', 'B+'), ('b-', 'B-'), ('ab+', 'AB+'), ('ab-', 'AB-'), ('o+', 'O+'), ('o-', 'O-'), ], string='Blood Group') marital_status = fields.Selection([ ('single', 'Single'), ('married', 'Married'), ('divorced', 'Divorced'), ('widowed', 'Widowed'), ], string='Marital Status') religion = fields.Char(string='Religion') identification_number = fields.Char(string='Identification Number') emergency_contact = fields.Char(string='Emergency Contact Name') emergency_phone = fields.Char(string='Emergency Phone') # Academic Fields campus_id = fields.Many2one('education.campus', string='Campus', required=True, index=True) department_id = fields.Many2one('education.department', string='Department', required=True, index=True) program_id = fields.Many2one('education.program', string='Program', required=True, index=True) level_id = fields.Many2one('education.academic.level', string='Academic Level', required=True, index=True) academic_year_id = fields.Many2one('education.academic.year', string='Academic Year', required=True, index=True) academic_term_id = fields.Many2one('education.academic.term', string='Academic Term', required=True, index=True) batch_id = fields.Many2one('education.batch', string='Batch', required=True, index=True) semester_id = fields.Many2one('education.semester', string='Semester', required=True, index=True) section_id = fields.Many2one('education.section', string='Section', required=True, index=True) roll_number = fields.Char(string='Roll Number', index=True, copy=False) registration_number = fields.Char(string='Registration Number', index=True, copy=False) admission_type = fields.Selection([ ('regular', 'Regular'), ('lateral', 'Lateral'), ('transfer', 'Transfer'), ], string='Admission Type', default='regular', required=True) current_semester_id = fields.Many2one('education.semester', string='Current Semester', index=True) company_id = fields.Many2one( 'res.company', string='Company', required=True, default=lambda self: self.env.company, index=True, ) # Guardian Information father_name = fields.Char(string="Father's Name") father_occupation = fields.Char(string="Father's Occupation") father_phone = fields.Char(string="Father's Phone") mother_name = fields.Char(string="Mother's Name") mother_occupation = fields.Char(string="Mother's Occupation") mother_phone = fields.Char(string="Mother's Phone") guardian_name = fields.Char(string='Guardian Name') guardian_relation = fields.Char(string='Guardian Relationship') guardian_phone = fields.Char(string='Guardian Phone') guardian_mobile = fields.Char(string='Guardian Mobile') guardian_email = fields.Char(string='Guardian Email') guardian_address = fields.Text(string='Guardian Address') guardian_occupation = fields.Char(string='Guardian Occupation') # Contact & Address street = fields.Char(string='Street') street2 = fields.Char(string='Street 2') city = fields.Char(string='City') state_id = fields.Many2one('res.country.state', string='State') zip = fields.Char(string='ZIP') country_id = fields.Many2one('res.country', string='Country') # Documents Storage doc_passport = fields.Binary(string='Passport') doc_passport_name = fields.Char(string='Passport Filename') doc_aadhaar = fields.Binary(string='Aadhaar') doc_aadhaar_name = fields.Char(string='Aadhaar Filename') doc_birth_cert = fields.Binary(string='Birth Certificate') doc_birth_cert_name = fields.Char(string='Birth Certificate Filename') doc_transfer_cert = fields.Binary(string='Transfer Certificate') doc_transfer_cert_name = fields.Char(string='Transfer Certificate Filename') doc_community_cert = fields.Binary(string='Community Certificate') doc_community_cert_name = fields.Char(string='Community Certificate Filename') doc_mark_sheets = fields.Binary(string='Mark Sheets') doc_mark_sheets_name = fields.Char(string='Mark Sheets Filename') status = fields.Selection([ ('draft', 'Draft'), ('admitted', 'Admitted'), ('active', 'Active'), ('on_leave', 'On Leave'), ('graduated', 'Graduated'), ('withdrawn', 'Withdrawn'), ('cancelled', 'Cancelled'), ], string='Status', default='draft', required=True, tracking=True, index=True) admission_date = fields.Date(string='Admission Date', default=fields.Date.context_today) enrollment_date = fields.Date(string='Enrollment Date') graduation_date = fields.Date(string='Graduation Date') leave_date = fields.Date(string='Leave Date') notes = fields.Text(string='Notes') _sql_constraints = [ ('uniq_student_id_company', 'unique(student_id, company_id)', 'Student ID must be unique per company.'), ('uniq_admission_number_company', 'unique(admission_number, company_id)', 'Admission number must be unique per company.'), ('uniq_student_email_company', 'unique(email, company_id)', 'Student email must be unique per company.'), ('uniq_roll_number_company', 'unique(roll_number, batch_id, company_id)', 'Roll number must be unique per batch/company.'), ('uniq_registration_number_company', 'unique(registration_number, company_id)', 'Registration number must be unique per company.'), ] @api.depends('first_name', 'middle_name', 'last_name') def _compute_full_name(self): for rec in self: names = [n for n in [rec.first_name, rec.middle_name, rec.last_name] if n] f_name = ' '.join(names) if names else '' rec.full_name = f_name rec.name = f_name @api.depends('date_of_birth') def _compute_age(self): for rec in self: if rec.date_of_birth: today = date.today() dob = rec.date_of_birth rec.age = today.year - dob.year - ((today.month, today.day) < (dob.month, dob.day)) else: rec.age = 0 @api.constrains('date_of_birth') def _check_date_of_birth(self): for rec in self: if rec.date_of_birth and rec.date_of_birth > fields.Date.today(): raise ValidationError('Date of birth cannot be in the future.') @api.constrains('email') def _check_email_format(self): for rec in self: if rec.email: if not re.match(r"^[^@\s]+@[^@\s]+\.[^@\s]+$", rec.email): raise ValidationError('Please enter a valid email address.') @api.constrains('mobile') def _check_mobile_format(self): for rec in self: if rec.mobile: if not re.match(r"^\+?[0-9\s\-()]+$", rec.mobile): raise ValidationError('Please enter a valid mobile number.') @api.constrains('admission_date', 'graduation_date') def _check_dates(self): for rec in self: if rec.admission_date and rec.graduation_date and rec.graduation_date < rec.admission_date: raise ValidationError('Graduation date cannot be before the admission date.') @api.constrains('campus_id', 'department_id', 'program_id', 'level_id', 'academic_year_id', 'academic_term_id') def _check_academic_link(self): for rec in self: if rec.department_id.campus_id and rec.campus_id and rec.department_id.campus_id != rec.campus_id: raise ValidationError('Department must belong to the selected campus.') if rec.program_id.department_id and rec.department_id and rec.program_id.department_id != rec.department_id: raise ValidationError('Program must belong to the selected department.') if rec.program_id.level_id and rec.level_id and rec.program_id.level_id != rec.level_id: raise ValidationError('Program level must match the selected academic level.') if rec.academic_term_id.academic_year_id and rec.academic_year_id and rec.academic_term_id.academic_year_id != rec.academic_year_id: raise ValidationError('Academic term must belong to the selected academic year.') @api.constrains('student_id') def _check_student_id(self): for rec in self: if rec.student_id and not rec.student_id.strip(): raise ValidationError('Student ID cannot be blank.') def action_draft(self): self.write({'status': 'draft', 'leave_date': False}) def action_admit(self): self.write({'status': 'admitted', 'admission_date': fields.Date.today()}) def action_activate(self): self.write({'status': 'active', 'enrollment_date': fields.Date.today()}) # If roll number is not assigned yet, support auto-generation from sequence for rec in self: if not rec.roll_number: rec.roll_number = self.env['ir.sequence'].next_by_code('education.student.roll') or '/' def action_on_leave(self): self.write({'status': 'on_leave', 'leave_date': fields.Date.today()}) def action_graduate(self): self.write({'status': 'graduated', 'graduation_date': fields.Date.today()}) def action_withdraw(self): self.write({'status': 'withdrawn', 'leave_date': fields.Date.today()}) def action_cancel(self): self.write({'status': 'cancelled', 'leave_date': fields.Date.today()}) @api.model_create_multi def create(self, vals_list): for vals in vals_list: if not vals.get('admission_number') or vals.get('admission_number') == '/': vals['admission_number'] = self.env['ir.sequence'].next_by_code('education.student.admission') or '/' if not vals.get('student_id') or vals.get('student_id') == '/': vals['student_id'] = self.env['ir.sequence'].next_by_code('education.student.id') or '/' if vals.get('student_id'): vals['student_id'] = vals['student_id'].strip().upper() return super().create(vals_list) def write(self, vals): if 'student_id' in vals and vals.get('student_id'): vals['student_id'] = vals['student_id'].strip().upper() return super().write(vals) @api.model def default_get(self, fields_list): defaults = super().default_get(fields_list) if 'company_id' not in defaults: defaults['company_id'] = self.env.company.id return defaults