bemade-addons/bemade_sports_clinic/models/sports_event.py

421 lines
14 KiB
Python

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)