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", }