274 lines
12 KiB
Python
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
|