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

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)