249 lines
9.8 KiB
Python
249 lines
9.8 KiB
Python
# pyrefly: ignore [missing-import]
|
|
from odoo import api, fields, models
|
|
# pyrefly: ignore [missing-import]
|
|
from odoo.exceptions import ValidationError
|
|
|
|
|
|
class EducationHostelMember(models.Model):
|
|
"""Student residential allocation — the core membership/allocation record.
|
|
|
|
Design decision: We use a SINGLE model (education.hostel.member) to serve
|
|
as both the current membership record AND the historical record.
|
|
Status tracks the lifecycle (pending → active → exited/transferred/cancelled).
|
|
This avoids duplicate logic between 'member' and 'allocation' models.
|
|
"""
|
|
_name = 'education.hostel.member'
|
|
_description = 'Hostel Member (Allocation)'
|
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
|
_order = 'allocation_date desc, id desc'
|
|
_rec_name = 'name'
|
|
|
|
name = fields.Char(
|
|
string='Reference',
|
|
required=True,
|
|
copy=False,
|
|
readonly=True,
|
|
default=lambda self: self.env['ir.sequence'].next_by_code('education.hostel.member'),
|
|
index=True,
|
|
)
|
|
student_id = fields.Many2one(
|
|
'education.student',
|
|
string='Student',
|
|
required=True,
|
|
ondelete='restrict',
|
|
index=True,
|
|
tracking=True,
|
|
)
|
|
hostel_id = fields.Many2one(
|
|
'education.hostel',
|
|
string='Hostel',
|
|
required=True,
|
|
ondelete='restrict',
|
|
index=True,
|
|
tracking=True,
|
|
)
|
|
room_id = fields.Many2one(
|
|
'education.hostel.room',
|
|
string='Room',
|
|
required=True,
|
|
ondelete='restrict',
|
|
index=True,
|
|
tracking=True,
|
|
)
|
|
bed_id = fields.Many2one(
|
|
'education.hostel.bed',
|
|
string='Bed',
|
|
required=True,
|
|
ondelete='restrict',
|
|
index=True,
|
|
tracking=True,
|
|
)
|
|
allocation_date = fields.Date(
|
|
string='Allocation Date',
|
|
required=True,
|
|
default=fields.Date.context_today,
|
|
tracking=True,
|
|
)
|
|
expected_exit_date = fields.Date(string='Expected Exit Date', tracking=True)
|
|
actual_exit_date = fields.Date(string='Actual Exit Date', tracking=True)
|
|
status = fields.Selection([
|
|
('pending', 'Pending'),
|
|
('active', 'Active'),
|
|
('transferred', 'Transferred'),
|
|
('exited', 'Exited'),
|
|
('cancelled', 'Cancelled'),
|
|
], string='Status', default='pending', required=True, tracking=True, index=True)
|
|
|
|
guardian_name = fields.Char(string='Guardian Name')
|
|
guardian_phone = fields.Char(string='Guardian Phone')
|
|
emergency_contact = fields.Char(string='Emergency Contact')
|
|
notes = fields.Text(string='Notes')
|
|
|
|
allocated_by = fields.Many2one(
|
|
'res.users',
|
|
string='Allocated By',
|
|
default=lambda self: self.env.user,
|
|
readonly=True,
|
|
)
|
|
company_id = fields.Many2one(
|
|
'res.company',
|
|
string='Company',
|
|
required=True,
|
|
default=lambda self: self.env.company,
|
|
index=True,
|
|
)
|
|
|
|
# Convenience relational
|
|
student_gender = fields.Selection(
|
|
related='student_id.gender', store=False, string='Student Gender'
|
|
)
|
|
|
|
# ── Constraints ──────────────────────────────────────────────────────────
|
|
|
|
@api.constrains('student_id', 'status')
|
|
def _check_unique_active_allocation(self):
|
|
"""A student cannot have more than one active allocation."""
|
|
for rec in self:
|
|
if rec.status == 'active':
|
|
duplicate = self.search([
|
|
('student_id', '=', rec.student_id.id),
|
|
('status', '=', 'active'),
|
|
('id', '!=', rec.id),
|
|
])
|
|
if duplicate:
|
|
raise ValidationError(
|
|
f"Student '{rec.student_id.full_name}' already has an active hostel allocation "
|
|
f"({duplicate[0].name}). A student cannot be allocated to multiple hostels simultaneously."
|
|
)
|
|
|
|
@api.constrains('bed_id', 'status')
|
|
def _check_unique_bed_occupant(self):
|
|
"""A bed cannot have more than one active occupant."""
|
|
for rec in self:
|
|
if rec.status == 'active':
|
|
duplicate = self.search([
|
|
('bed_id', '=', rec.bed_id.id),
|
|
('status', '=', 'active'),
|
|
('id', '!=', rec.id),
|
|
])
|
|
if duplicate:
|
|
raise ValidationError(
|
|
f"Bed '{rec.bed_id.code}' is already occupied by student "
|
|
f"'{duplicate[0].student_id.full_name}'. Please select a different bed."
|
|
)
|
|
|
|
@api.constrains('room_id', 'hostel_id')
|
|
def _check_room_hostel_consistency(self):
|
|
for rec in self:
|
|
if rec.room_id and rec.room_id.hostel_id != rec.hostel_id:
|
|
raise ValidationError(
|
|
'The selected room does not belong to the selected hostel.'
|
|
)
|
|
|
|
@api.constrains('bed_id', 'room_id')
|
|
def _check_bed_room_consistency(self):
|
|
for rec in self:
|
|
if rec.bed_id and rec.bed_id.room_id != rec.room_id:
|
|
raise ValidationError(
|
|
'The selected bed does not belong to the selected room.'
|
|
)
|
|
|
|
@api.constrains('student_id', 'room_id', 'status')
|
|
def _check_gender_restriction(self):
|
|
"""Enforce gender restriction based on room.gender setting."""
|
|
for rec in self:
|
|
if rec.status in ('pending', 'active') and rec.room_id and rec.student_id:
|
|
room_gender = rec.room_id.gender
|
|
student_gender = rec.student_id.gender
|
|
if room_gender == 'male' and student_gender not in ('male', False):
|
|
raise ValidationError(
|
|
f"Room '{rec.room_id.room_number}' is restricted to male students."
|
|
)
|
|
if room_gender == 'female' and student_gender not in ('female', False):
|
|
raise ValidationError(
|
|
f"Room '{rec.room_id.room_number}' is restricted to female students."
|
|
)
|
|
|
|
@api.constrains('room_id', 'status')
|
|
def _check_room_capacity(self):
|
|
"""Active allocations for a room must not exceed room.capacity."""
|
|
for rec in self:
|
|
if rec.status == 'active' and rec.room_id:
|
|
active_count = self.search_count([
|
|
('room_id', '=', rec.room_id.id),
|
|
('status', '=', 'active'),
|
|
('id', '!=', rec.id),
|
|
])
|
|
if active_count >= rec.room_id.capacity:
|
|
raise ValidationError(
|
|
f"Room '{rec.room_id.room_number}' has reached its maximum capacity "
|
|
f"of {rec.room_id.capacity} occupants."
|
|
)
|
|
|
|
# ── Workflow Actions ──────────────────────────────────────────────────────
|
|
|
|
def action_activate(self):
|
|
"""Activate the allocation and mark the bed as occupied."""
|
|
for rec in self:
|
|
if rec.status != 'pending':
|
|
raise ValidationError("Only pending allocations can be activated.")
|
|
rec.status = 'active'
|
|
rec.bed_id.sudo().write({'status': 'occupied'})
|
|
rec.message_post(body=f"Allocation activated. Bed {rec.bed_id.code} marked as occupied.")
|
|
|
|
def action_exit(self):
|
|
"""Mark student as exited and free the bed."""
|
|
for rec in self:
|
|
if rec.status != 'active':
|
|
raise ValidationError("Only active allocations can be exited.")
|
|
rec.actual_exit_date = fields.Date.today()
|
|
rec.status = 'exited'
|
|
rec.bed_id.sudo().write({'status': 'available'})
|
|
rec.message_post(body=f"Student exited on {rec.actual_exit_date}. Bed {rec.bed_id.code} freed.")
|
|
|
|
def action_cancel(self):
|
|
"""Cancel the allocation and free the bed if it was active."""
|
|
for rec in self:
|
|
if rec.status in ('exited', 'cancelled'):
|
|
raise ValidationError("This allocation is already closed.")
|
|
if rec.status == 'active':
|
|
rec.bed_id.sudo().write({'status': 'available'})
|
|
rec.status = 'cancelled'
|
|
rec.message_post(body="Allocation cancelled.")
|
|
|
|
def action_transfer(self, new_hostel_id, new_room_id, new_bed_id):
|
|
"""Transfer student to a different room/bed. Creates new member record."""
|
|
self.ensure_one()
|
|
if self.status != 'active':
|
|
raise ValidationError("Only active allocations can be transferred.")
|
|
|
|
# Close current allocation
|
|
self.actual_exit_date = fields.Date.today()
|
|
self.status = 'transferred'
|
|
self.bed_id.sudo().write({'status': 'available'})
|
|
|
|
# Create new allocation
|
|
new_member = self.create({
|
|
'student_id': self.student_id.id,
|
|
'hostel_id': new_hostel_id,
|
|
'room_id': new_room_id,
|
|
'bed_id': new_bed_id,
|
|
'allocation_date': fields.Date.today(),
|
|
'status': 'pending',
|
|
'guardian_name': self.guardian_name,
|
|
'guardian_phone': self.guardian_phone,
|
|
'emergency_contact': self.emergency_contact,
|
|
'company_id': self.company_id.id,
|
|
})
|
|
self.message_post(body=f"Student transferred. New allocation: {new_member.name}")
|
|
return new_member
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
for vals in vals_list:
|
|
if not vals.get('name') or vals.get('name') == '/':
|
|
vals['name'] = (
|
|
self.env['ir.sequence'].next_by_code('education.hostel.member') or '/'
|
|
)
|
|
return super().create(vals_list)
|