2026-09-25 18:00:58 +05:30

274 lines
12 KiB
Python

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