2026-09-28 11:03:02 +05:30

231 lines
8.7 KiB
Python

from odoo import api, fields, models
from odoo.exceptions import ValidationError
class DanceSession(models.Model):
_name = "dance.session"
_description = "Dance Class Session"
_order = "date desc, start_time desc, id desc"
name = fields.Char(compute="_compute_name", store=True)
batch_id = fields.Many2one("dance.batch", required=True, ondelete="restrict")
date = fields.Date(required=True, default=fields.Date.context_today)
start_time = fields.Float()
end_time = fields.Float()
room = fields.Char()
instructor_id = fields.Many2one("hr.employee", string="Instructor")
substitute_instructor_id = fields.Many2one(
"hr.employee",
string="Substitute Instructor",
)
is_cancelled = fields.Boolean(string="Cancelled")
cancellation_reason = fields.Text()
makeup_for_id = fields.Many2one(
"dance.session",
string="Makeup For",
domain="[('status', '=', 'cancelled')]",
ondelete="set null",
)
makeup_session_ids = fields.One2many(
"dance.session",
"makeup_for_id",
string="Makeup Sessions",
)
status = fields.Selection(
[
("scheduled", "Scheduled"),
("completed", "Completed"),
("cancelled", "Cancelled"),
],
default="scheduled",
required=True,
)
notes = fields.Text()
attendance_ids = fields.One2many(
"dance.attendance",
"session_id",
string="Attendance",
)
attendance_count = fields.Integer(compute="_compute_attendance_counts")
present_count = fields.Integer(compute="_compute_attendance_counts")
absent_count = fields.Integer(compute="_compute_attendance_counts")
late_count = fields.Integer(compute="_compute_attendance_counts")
@api.depends("batch_id", "date", "start_time")
def _compute_name(self):
for session in self:
if session.batch_id and session.date:
session.name = "%s - %s" % (session.batch_id.name, session.date)
else:
session.name = "New Session"
@api.depends("attendance_ids.status")
def _compute_attendance_counts(self):
for session in self:
session.attendance_count = len(session.attendance_ids)
session.present_count = len(
session.attendance_ids.filtered(lambda line: line.status == "present")
)
session.absent_count = len(
session.attendance_ids.filtered(lambda line: line.status == "absent")
)
session.late_count = len(
session.attendance_ids.filtered(lambda line: line.status == "late")
)
@api.onchange("batch_id")
def _onchange_batch_id(self):
for session in self:
if session.batch_id:
session.instructor_id = session.batch_id.instructor_id
session.start_time = session.batch_id.start_time
session.end_time = session.batch_id.end_time
session.room = session.batch_id.room
@api.constrains("batch_id", "date")
def _check_date_inside_batch(self):
for session in self:
batch = session.batch_id
if not batch or not session.date:
continue
if batch.start_date and session.date < batch.start_date:
raise ValidationError("Session date cannot be before batch start date.")
if batch.end_date and session.date > batch.end_date:
raise ValidationError("Session date cannot be after batch end date.")
@api.constrains("start_time", "end_time")
def _check_times(self):
for session in self:
if session.end_time and session.start_time and session.end_time <= session.start_time:
raise ValidationError("End time must be after start time.")
@api.constrains("date", "start_time", "end_time", "room", "instructor_id", "substitute_instructor_id", "status")
def _check_room_and_instructor_conflicts(self):
for session in self:
if (
session.status == "cancelled"
or not session.date
or not session.start_time
or not session.end_time
):
continue
overlap_domain = [
("id", "!=", session.id),
("date", "=", session.date),
("status", "!=", "cancelled"),
("start_time", "<", session.end_time),
("end_time", ">", session.start_time),
]
if session.room:
room_conflict = self.search(overlap_domain + [("room", "=", session.room)], limit=1)
if room_conflict:
raise ValidationError(
"Room conflict: %s is already booked for %s from %.2f to %.2f."
% (
session.room,
room_conflict.name,
room_conflict.start_time,
room_conflict.end_time,
)
)
teaching_instructor = session.substitute_instructor_id or session.instructor_id
if teaching_instructor:
instructor_conflict = self.search(
overlap_domain
+ [
"|",
("instructor_id", "=", teaching_instructor.id),
("substitute_instructor_id", "=", teaching_instructor.id),
],
limit=1,
)
if instructor_conflict:
raise ValidationError(
"Instructor conflict: %s already has %s from %.2f to %.2f."
% (
teaching_instructor.name,
instructor_conflict.name,
instructor_conflict.start_time,
instructor_conflict.end_time,
)
)
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get("batch_id"):
batch = self.env["dance.batch"].browse(vals["batch_id"])
vals.setdefault("instructor_id", batch.instructor_id.id)
vals.setdefault("start_time", batch.start_time)
vals.setdefault("end_time", batch.end_time)
vals.setdefault("room", batch.room)
return super().create(vals_list)
def write(self, vals):
if vals.get("is_cancelled"):
vals = dict(vals, status="cancelled")
elif vals.get("status") == "cancelled":
vals = dict(vals, is_cancelled=True)
return super().write(vals)
def action_load_students(self):
for session in self:
active_enrollments = self.env["dance.enrollment"].search(
[
("batch_id", "=", session.batch_id.id),
("status", "=", "active"),
]
)
existing_student_ids = set(session.attendance_ids.mapped("student_id").ids)
attendance_vals = []
for enrollment in active_enrollments:
if enrollment.student_id.id in existing_student_ids:
continue
attendance_vals.append(
{
"session_id": session.id,
"student_id": enrollment.student_id.id,
"status": "present",
}
)
if attendance_vals:
self.env["dance.attendance"].create(attendance_vals)
return True
def action_mark_all_present(self):
for session in self:
if not session.attendance_ids:
session.action_load_students()
session.attendance_ids.write({"status": "present"})
return True
def action_complete(self):
self.write({"status": "completed"})
return True
def action_cancel(self):
self.write({"status": "cancelled", "is_cancelled": True})
return True
def action_create_makeup_session(self):
self.ensure_one()
makeup = self.copy(
{
"date": fields.Date.context_today(self),
"status": "scheduled",
"is_cancelled": False,
"cancellation_reason": False,
"makeup_for_id": self.id,
"attendance_ids": False,
}
)
return {
"name": "Makeup Session",
"type": "ir.actions.act_window",
"res_model": "dance.session",
"res_id": makeup.id,
"view_mode": "form",
}