from odoo import api, fields, models from odoo.exceptions import ValidationError class SportsEvent(models.Model): _name = 'sports.event' _description = 'Sports Event' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'date_start desc, name' _rec_name = 'name' # Portal access group definition - only authorized portal users _portal_groups = 'base.group_user,bemade_sports_clinic.group_portal_treatment_professional,bemade_sports_clinic.group_portal_team_coach' # ======================================== # CORE EVENT FIELDS (Portal Accessible) # ======================================== name = fields.Char( string='Event Name', required=True, tracking=True, groups=_portal_groups, help='Name of the sports event' ) description = fields.Html( string='Description', groups=_portal_groups, help='Detailed description of the event' ) # Event timing date_start = fields.Datetime( string='Event Start Time', required=True, tracking=True, index=True, groups=_portal_groups, default=fields.Datetime.now, help='When the event starts' ) date_end = fields.Datetime( string='Event End Time', required=True, tracking=True, index=True, groups=_portal_groups, help='When the event ends' ) # Therapist coverage timing (independent fields that sync with task) therapist_start = fields.Datetime( string='Therapist Start Time', tracking=True, index=True, groups=_portal_groups, help='When therapist coverage begins (may be before event start for preparation)' ) therapist_end = fields.Datetime( string='Therapist End Time', tracking=True, index=True, groups=_portal_groups, help='When therapist coverage ends (may be after event end for cleanup)' ) # Event details venue_id = fields.Many2one( 'res.partner', string='Venue', domain=[('is_venue', '=', True)], groups=_portal_groups, help='Venue/location where the event takes place' ) event_type = fields.Selection([ ('game', 'Game'), ('practice', 'Practice'), ('training', 'Training'), ('meeting', 'Team Meeting'), ('other', 'Other') ], string='Event Type', default='game', groups=_portal_groups, tracking=True) # Status and priority state = fields.Selection([ ('draft', 'Draft'), ('confirmed', 'Confirmed'), ('in_progress', 'In Progress'), ('completed', 'Completed'), ('cancelled', 'Cancelled') ], string='Status', default='confirmed', tracking=True, groups=_portal_groups) # ======================================== # RELATIONSHIPS (Portal Accessible) # ======================================== team_id = fields.Many2one( 'sports.team', string='Team', required=True, tracking=True, groups=_portal_groups, help='The sports team this event is for' ) # Staff assignments assigned_staff_ids = fields.Many2many( 'res.users', 'sports_event_staff_rel', 'event_id', 'user_id', string='Assigned Staff', groups=_portal_groups, help='Treatment professionals assigned to this event' ) # ======================================== # TASK INTEGRATION (Internal Management) # ======================================== task_id = fields.Many2one( 'project.task', string='Management Task', ondelete='set null', groups='base.group_user', help='Internal project task for managing this event' ) project_id = fields.Many2one( 'project.project', string='Project', groups='base.group_user', help='Project this event belongs to' ) # ======================================== # COMPUTED FIELDS # ======================================== partner_id = fields.Many2one( 'res.partner', string='Organization', compute='_compute_partner_id', store=True, groups=_portal_groups, help='Parent organization (computed from team)' ) duration = fields.Float( string='Event Duration (Hours)', compute='_compute_duration', store=True, groups=_portal_groups, help='Event duration in hours' ) therapist_duration = fields.Float( string='Therapist Coverage Duration (Hours)', compute='_compute_therapist_duration', store=True, groups=_portal_groups, help='Therapist coverage duration in hours' ) is_today = fields.Boolean( string='Is Today', compute='_compute_is_today', help='Whether the event is happening today' ) is_upcoming = fields.Boolean( string='Is Upcoming', compute='_compute_is_upcoming', help='Whether the event is in the future' ) # ======================================== # COMPUTED METHODS # ======================================== @api.depends('team_id', 'team_id.parent_id') def _compute_partner_id(self): """Compute partner/organization from team""" for event in self: event.partner_id = event.team_id.parent_id if event.team_id else False def action_recompute_partner_ids(self): """Recompute partner_id for all events (for fixing organization filter)""" all_events = self.search([]) all_events._compute_partner_id() return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'message': f'Recomputed partner_id for {len(all_events)} events', 'type': 'success', } } @api.depends('date_start', 'date_end') def _compute_duration(self): """Calculate event duration in hours""" for event in self: if event.date_start and event.date_end: delta = event.date_end - event.date_start event.duration = delta.total_seconds() / 3600.0 else: event.duration = 0.0 @api.depends('therapist_start', 'therapist_end') def _compute_therapist_duration(self): """Calculate therapist coverage duration in hours""" for event in self: if event.therapist_start and event.therapist_end: delta = event.therapist_end - event.therapist_start event.therapist_duration = delta.total_seconds() / 3600.0 else: event.therapist_duration = 0.0 @api.depends('date_start') def _compute_is_today(self): """Check if event is today""" from datetime import date today = date.today() for event in self: if event.date_start: event.is_today = event.date_start.date() == today else: event.is_today = False @api.depends('date_start') def _compute_is_upcoming(self): """Check if event is in the future""" from datetime import datetime now = datetime.now() for event in self: if event.date_start: event.is_upcoming = event.date_start > now else: event.is_upcoming = False # ======================================== # ONCHANGE METHODS # ======================================== @api.onchange('date_start') def _onchange_date_start(self): """When event start changes: - therapist_start = 30 minutes prior - date_end = 2 hours after - therapist_end = date_end """ if self.date_start: from datetime import timedelta self.therapist_start = self.date_start - timedelta(minutes=30) self.date_end = self.date_start + timedelta(hours=2) self.therapist_end = self.date_end @api.onchange('date_end') def _onchange_date_end(self): """When event end changes: - therapist_end = max(therapist_end, date_end) """ if self.date_end: # If therapist_end not set or earlier than date_end, align to date_end if not self.therapist_end or self.therapist_end < self.date_end: self.therapist_end = self.date_end # ======================================== # VALIDATION # ======================================== @api.constrains('date_start', 'date_end') def _check_event_dates(self): """Validate that end date is after start date""" for event in self: if event.date_start and event.date_end: if event.date_end <= event.date_start: raise ValidationError("Event end time must be after start time.") @api.constrains('therapist_start', 'therapist_end') def _check_therapist_dates(self): """Validate that therapist end time is after start time""" for event in self: if event.therapist_start and event.therapist_end: if event.therapist_end <= event.therapist_start: raise ValidationError("Therapist end time must be after start time.") # ======================================== # TASK INTEGRATION METHODS # ======================================== def create_management_task(self): """Create a project task for internal management of this event""" self.ensure_one() if self.task_id: return self.task_id # Find or create a project for the team project = self._get_or_create_team_project() task_vals = { 'name': f"Event: {self.name}", 'description': self.description or '', 'project_id': project.id, 'date_deadline': self.date_end, 'user_ids': [(6, 0, self.assigned_staff_ids.ids)], 'partner_id': self.partner_id.id if self.partner_id else False, } task = self.env['project.task'].create(task_vals) self.task_id = task.id self.project_id = project.id return task def _get_or_create_team_project(self): """Get or create a project for the organization (one project per partner for billing)""" if self.project_id: return self.project_id # Get the organization (partner) from the team organization = self.team_id.parent_id if self.team_id else False if not organization: raise ValidationError("Team must have a parent organization to create events.") # Look for existing organization project (one project per partner) project = self.env['project.project'].search([ ('partner_id', '=', organization.id), ], limit=1) if not project: # Create new project for the organization project = self.env['project.project'].create({ 'name': f"{organization.name} - Sports Events", 'partner_id': organization.id, 'privacy_visibility': 'portal', 'description': f"Event management for {organization.name} sports teams" }) return project # ======================================== # PORTAL ACCESS METHODS # ======================================== def check_portal_access(self, user=None): """Check if user has portal access to this event""" if not user: user = self.env.user # Internal users have full access if user.has_group('base.group_user'): return True # Portal treatment professionals need team access if user.has_group('bemade_sports_clinic.group_portal_treatment_professional'): partner = user.partner_id team_staff_rels = partner.team_staff_rel_ids authorized_teams = team_staff_rels.mapped('team_id') return self.team_id in authorized_teams return False # ======================================== # CRUD OVERRIDES # ======================================== @api.model_create_multi def create(self, vals_list): """Override create to handle task integration (batch-optimized for Odoo 18)""" events = super().create(vals_list) # Auto-create management tasks for events that need them # IMPORTANT: Only internal users (base.group_user) may auto-create tasks here. # Portal users will have tasks created via a controller-side sudo() after access checks. is_internal = self.env.user.has_group('base.group_user') if is_internal: for event, vals in zip(events, vals_list): if vals.get('auto_create_task', True): event.create_management_task() return events def write(self, vals): """Override write to sync with task""" result = super().write(vals) # Sync changes to linked task (only for users with task access) user = self.env.user has_task_access = user.has_group('base.group_user') if has_task_access: for event in self: if event.task_id: event._sync_to_task() return result def _sync_to_task(self): """Sync event changes to linked task""" if not self.task_id: return task_vals = { 'name': f"Event: {self.name}", 'description': self.description or '', 'date_deadline': self.date_end, 'user_ids': [(6, 0, self.assigned_staff_ids.ids)], } # Sync therapist coverage times to task start/end times if self.therapist_start: task_vals['date_start'] = self.therapist_start if self.therapist_end: task_vals['date_end'] = self.therapist_end self.task_id.write(task_vals)