feat: Update bemade_sports_clinic to v18.0.2.0.0 with comprehensive activity management
- Added integrated mail.activity system for task management - Implemented portal access to activities for treatment professionals - Added activity creation, completion, and reassignment functionality - Enhanced player management with Canadian address validation - Improved injury tracking with parental consent and document attachments - Implemented layered security architecture (ACL + Record Rules + Controller filtering) - Added French Canadian (fr_CA) localization support - Enhanced portal UI with activity counts and navigation - Implemented RPC security protection with buddy method pattern - Added comprehensive demo data and integration features - Updated manifest to reflect all new capabilities and improvements
This commit is contained in:
parent
0c0d047f2e
commit
d2286d136e
37 changed files with 3716 additions and 157 deletions
200
bemade_sports_clinic/PROJECT_TASK_PORTAL_SECURITY.md
Normal file
200
bemade_sports_clinic/PROJECT_TASK_PORTAL_SECURITY.md
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
# Project Task Portal Security Architecture
|
||||
|
||||
## Overview
|
||||
This document outlines the comprehensive security implementation for portal treatment professionals accessing project.task objects (events) in the bemade_sports_clinic module.
|
||||
|
||||
## Security Principles Applied
|
||||
|
||||
### 1. **Explicit Authorization Only**
|
||||
- **NO** blanket access based on `privacy_visibility = 'portal'`
|
||||
- **YES** explicit authorization through follower relationships or team membership
|
||||
- **Principle**: Users must be explicitly granted access, not implicitly through project settings
|
||||
|
||||
### 2. **Multi-Layer Security Architecture**
|
||||
1. **ACL Level**: Model-level CRUD permissions
|
||||
2. **Record Rule Level**: Record-level filtering based on explicit relationships
|
||||
3. **Field Level**: Field-level group access controls
|
||||
4. **Controller Level**: Additional business logic validation
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Access Control Lists (ACLs)
|
||||
**File**: `security/ir.model.access.csv`
|
||||
|
||||
```csv
|
||||
# Portal Treatment Professionals get read/write/create access to project tasks
|
||||
access_project_task_portal_tp,Portal TP Access for Project Tasks,project.model_project_task,bemade_sports_clinic.group_portal_treatment_professional,1,1,1,0
|
||||
|
||||
# Portal Treatment Professionals get read/write access to projects (for follower management)
|
||||
access_project_project_portal_tp,Portal TP Access for Projects,project.model_project_project,bemade_sports_clinic.group_portal_treatment_professional,1,1,0,0
|
||||
|
||||
# Read-only access to supporting models
|
||||
access_project_task_type_portal_tp,Portal TP Access for Task Types,project.model_project_task_type,bemade_sports_clinic.group_portal_treatment_professional,1,0,0,0
|
||||
access_project_tags_portal_tp,Portal TP Access for Project Tags,project.model_project_tags,bemade_sports_clinic.group_portal_treatment_professional,1,0,0,0
|
||||
access_project_milestone_portal_tp,Portal TP Access for Project Milestones,project.model_project_milestone,bemade_sports_clinic.group_portal_treatment_professional,1,0,0,0
|
||||
```
|
||||
|
||||
### Record Rules
|
||||
**File**: `security/project_task_portal_rules.xml`
|
||||
|
||||
#### Project Task Access Rule
|
||||
```xml
|
||||
<field name="domain_force">[
|
||||
'|', '|', '|',
|
||||
# Tasks assigned to the user
|
||||
('user_ids', 'in', [user.id]),
|
||||
# Tasks where user is a follower
|
||||
('message_partner_ids', 'in', [user.partner_id.id]),
|
||||
# Tasks from projects where user is a follower
|
||||
('project_id.message_partner_ids', 'in', [user.partner_id.id]),
|
||||
# Tasks from projects where user's teams are partners
|
||||
('project_id.partner_id', 'in', user.partner_id.team_staff_rel_ids.mapped('team_id.id') or [0])
|
||||
]
|
||||
```
|
||||
|
||||
#### Project Access Rule
|
||||
```xml
|
||||
<field name="domain_force">[
|
||||
'|',
|
||||
# Projects where user is explicitly a follower
|
||||
('message_partner_ids', 'in', [user.partner_id.id]),
|
||||
# Projects where user's teams are partners
|
||||
('partner_id', 'in', user.partner_id.team_staff_rel_ids.mapped('team_id.id') or [0])
|
||||
]
|
||||
```
|
||||
|
||||
### Field-Level Security
|
||||
**Files**: `models/project_task.py` and `models/project_project.py`
|
||||
|
||||
All critical fields are overridden with explicit group access for authorized sports clinic users only:
|
||||
```python
|
||||
# SECURE: Only authorized sports clinic portal users, not all portal users
|
||||
_portal_groups = 'base.group_user,bemade_sports_clinic.group_portal_treatment_professional,bemade_sports_clinic.group_portal_team_coach'
|
||||
|
||||
# Core fields
|
||||
name = fields.Char(groups=_portal_groups)
|
||||
description = fields.Html(groups=_portal_groups)
|
||||
user_ids = fields.Many2many(groups=_portal_groups)
|
||||
project_id = fields.Many2one(groups=_portal_groups)
|
||||
# ... and many more
|
||||
```
|
||||
|
||||
### Controller Security
|
||||
**File**: `controllers/events_portal.py`
|
||||
|
||||
#### Secure Domain Construction
|
||||
```python
|
||||
def _prepare_events_domain(self, view_type='all'):
|
||||
# Base domain: tasks from projects where user has explicit authorization
|
||||
base_domain = [
|
||||
'|', '|',
|
||||
# Tasks from projects where user's teams are partners
|
||||
('project_id.partner_id', 'in', team_ids or [0]),
|
||||
# Tasks from projects where user is a follower
|
||||
('project_id.message_partner_ids', 'in', [partner.id]),
|
||||
# Tasks where user is directly assigned or following
|
||||
'|',
|
||||
('user_ids', 'in', [user.id]),
|
||||
('message_partner_ids', 'in', [partner.id])
|
||||
]
|
||||
```
|
||||
|
||||
#### Project Filtering
|
||||
```python
|
||||
def _get_available_projects(self):
|
||||
# Only show projects where user has explicit authorization
|
||||
domain = [
|
||||
'|',
|
||||
# Projects where user's teams are partners
|
||||
('partner_id', 'in', team_ids or [0]),
|
||||
# Projects where user is explicitly a follower
|
||||
('message_partner_ids', 'in', [partner.id])
|
||||
]
|
||||
```
|
||||
|
||||
## Security Validation
|
||||
|
||||
### Model-Level Access Checking
|
||||
```python
|
||||
def check_portal_task_access(self):
|
||||
# 1. Check user is assigned to task
|
||||
# 2. Check user is follower of task
|
||||
# 3. Check user is follower of project
|
||||
# 4. Check user's teams are partners of project
|
||||
# NO blanket portal visibility check
|
||||
```
|
||||
|
||||
### Project Configuration
|
||||
```python
|
||||
def ensure_portal_access_for_treatment_professionals(self):
|
||||
# Only add treatment professionals who are staff on the team
|
||||
# NO blanket addition of all portal users
|
||||
```
|
||||
|
||||
## Critical Security Fixes Applied
|
||||
|
||||
### ❌ **BEFORE (Vulnerable)**
|
||||
```python
|
||||
# SECURITY FLAW: Any portal user could access any portal-visible project
|
||||
domain = [
|
||||
'|', '|',
|
||||
('partner_id', 'in', team_ids),
|
||||
('message_partner_ids', 'in', [partner.id]),
|
||||
('privacy_visibility', '=', 'portal') # ← VULNERABILITY
|
||||
]
|
||||
```
|
||||
|
||||
### ✅ **AFTER (Secure)**
|
||||
```python
|
||||
# SECURE: Only explicitly authorized users can access projects
|
||||
domain = [
|
||||
'|',
|
||||
('partner_id', 'in', team_ids or [0]), # Team relationship
|
||||
('message_partner_ids', 'in', [partner.id]) # Explicit follower
|
||||
]
|
||||
```
|
||||
|
||||
## Testing and Validation
|
||||
|
||||
### Security Test Utility
|
||||
**File**: `models/project_task_security_test.py`
|
||||
- Admin UI for testing field access
|
||||
- Validates all security layers
|
||||
- Tests unauthorized access scenarios
|
||||
|
||||
### Comprehensive Test Suite
|
||||
**File**: `tests/test_project_task_portal_security.py`
|
||||
- 10 test cases covering all security aspects
|
||||
- Validates proper access isolation
|
||||
- Tests record rule enforcement
|
||||
|
||||
## Best Practices Established
|
||||
|
||||
1. **Explicit Authorization Required**: Never grant access based solely on project privacy settings
|
||||
2. **Follower-Based Security**: Use message_partner_ids for explicit access control
|
||||
3. **Team-Based Authorization**: Link project access to sports team relationships
|
||||
4. **Layered Security**: Multiple security layers working together
|
||||
5. **Principle of Least Privilege**: Grant minimum necessary permissions
|
||||
|
||||
## Common Security Anti-Patterns Avoided
|
||||
|
||||
1. **❌ Portal Visibility OR Condition**: `('privacy_visibility', '=', 'portal')` as standalone OR
|
||||
2. **❌ Blanket Group Access**: Adding all portal users as followers
|
||||
3. **❌ Overly Broad Domains**: Using `[(1, '=', 1)]` or similar catch-all domains
|
||||
4. **❌ Missing Fallbacks**: Not using `or [0]` for empty list protection
|
||||
|
||||
## Deployment Checklist
|
||||
|
||||
- [ ] Update module to install new security files
|
||||
- [ ] Run security tests to validate implementation
|
||||
- [ ] Verify portal users can only access authorized projects/tasks
|
||||
- [ ] Test that unauthorized users are properly denied access
|
||||
- [ ] Validate field-level access works correctly in portal UI
|
||||
|
||||
## Maintenance Guidelines
|
||||
|
||||
1. **Always use explicit authorization** when creating new access rules
|
||||
2. **Test security boundaries** whenever adding new portal functionality
|
||||
3. **Document security decisions** for future developers
|
||||
4. **Regular security audits** of domain logic and record rules
|
||||
5. **Follow the established patterns** for consistent security implementation
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
from . import models
|
||||
from . import controllers
|
||||
from . import wizards
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@
|
|||
#
|
||||
{
|
||||
'name': 'Sports Clinic Management',
|
||||
'version': '18.0.1.9.0',
|
||||
'summary': 'Manage the patients of a sports medicine clinic.',
|
||||
'version': '18.0.2.0.0',
|
||||
'summary': 'Comprehensive sports medicine clinic management with portal access and activity tracking.',
|
||||
'description': """
|
||||
Sports Clinic Management System
|
||||
=============================
|
||||
|
||||
A comprehensive solution for managing sports medicine clinics, focusing on player health,
|
||||
injury tracking, and team collaboration.
|
||||
injury tracking, team collaboration, and integrated activity management.
|
||||
|
||||
Key Features:
|
||||
------------
|
||||
|
|
@ -34,50 +34,78 @@
|
|||
- Internal clinic staff with full patient record access
|
||||
- Treatment professionals with medical record access
|
||||
- Portal access for field therapists and team coaches
|
||||
- Automated group assignment based on team roles
|
||||
|
||||
2. Player Management:
|
||||
- Track player details and contact information
|
||||
- Track player details and contact information with address management
|
||||
- Monitor team memberships and playing status
|
||||
- Record and track injuries and treatment history
|
||||
- Track match and practice availability
|
||||
- Emergency contacts management with mobile numbers
|
||||
- Canadian address validation (provinces/territories)
|
||||
|
||||
3. Injury Tracking:
|
||||
- Comprehensive injury recording and documentation
|
||||
- Treatment professional assignment
|
||||
- Treatment professional assignment and parental consent tracking
|
||||
- Progress tracking and resolution monitoring
|
||||
- Internal and external notes for different audiences
|
||||
- Document attachment support with portal access
|
||||
- Injury status workflow (Unverified → Active → Resolved)
|
||||
|
||||
4. Team Management:
|
||||
- Organize players into teams
|
||||
- Assign coaching and medical staff
|
||||
- Organize players into teams with staff assignments
|
||||
- Assign coaching and medical staff with automatic portal access
|
||||
- Team-specific player status tracking
|
||||
- Player removal workflow with approval process
|
||||
- Treatment notes management across team members
|
||||
|
||||
5. Portal Access:
|
||||
5. Activity Management (NEW):
|
||||
- Integrated mail.activity system for task management
|
||||
- Portal access to activities for treatment professionals
|
||||
- Activity creation, completion, and reassignment
|
||||
- Team-based activity filtering and access control
|
||||
- Activity counts and navigation throughout portal
|
||||
- Activity detail views with attachment support
|
||||
|
||||
6. Portal Access:
|
||||
- Coaches can view their teams and player status
|
||||
- Field therapists can access and update medical records
|
||||
- Injury reporting directly through the portal
|
||||
- Coaches can request player removal with reason
|
||||
- Treatment professionals can approve/process removal requests
|
||||
- Visual indicators for pending removal requests
|
||||
- Emergency contacts management for treatment professionals
|
||||
- Treatment notes management for players with or without injuries
|
||||
- Comprehensive activity management interface
|
||||
- Player removal requests with reason tracking
|
||||
- Emergency contacts and address management
|
||||
- Document upload and download capabilities
|
||||
- Messages and attachments portal access
|
||||
|
||||
6. Security and Privacy:
|
||||
- Granular permission system
|
||||
7. Security and Privacy:
|
||||
- Layered security architecture (ACL + Record Rules + Controller filtering)
|
||||
- Team-based access control throughout the system
|
||||
- Field-level security for sensitive information
|
||||
- Audit trails for all changes
|
||||
- GDPR and Quebec Law 25 compliance features
|
||||
- Configurable data retention policies
|
||||
- RPC security protection with buddy method pattern
|
||||
|
||||
7. Data Protection:
|
||||
8. Data Protection:
|
||||
- Scheduled data anonymization
|
||||
- Configurable retention periods
|
||||
- Audit logging of all data handling
|
||||
- Manual anonymization wizard
|
||||
|
||||
This module is designed to facilitate communication between medical professionals,
|
||||
coaching staff, and administrative personnel while maintaining appropriate access
|
||||
controls and data privacy.
|
||||
9. Localization:
|
||||
- Full French Canadian (fr_CA) translation support
|
||||
- Canadian-specific address and province handling
|
||||
- Localized date and number formatting
|
||||
|
||||
10. Integration Features:
|
||||
- Mail system integration for notifications
|
||||
- Project task integration for event management
|
||||
- Task-to-event conversion wizard
|
||||
- Comprehensive demo data for testing
|
||||
|
||||
This module provides a complete sports medicine clinic management solution with
|
||||
robust portal access, activity tracking, and team collaboration features while
|
||||
maintaining strict security and data privacy controls.
|
||||
""",
|
||||
"category": "Services/Medical",
|
||||
"author": "Bemade Inc.",
|
||||
|
|
@ -88,6 +116,7 @@
|
|||
"portal",
|
||||
"contacts",
|
||||
"phone_validation", # For phone number formatting in patient contacts
|
||||
"project", # Required for project.task (Events) functionality
|
||||
],
|
||||
"external_dependencies": {
|
||||
"python": [
|
||||
|
|
@ -102,9 +131,12 @@
|
|||
"security/sports_clinic_rules.xml",
|
||||
"security/sports_clinic_portal_rules.xml",
|
||||
"security/mail_activity_portal_rules.xml",
|
||||
"security/project_task_portal_rules.xml",
|
||||
"security/sports_event_rules.xml",
|
||||
"security/partner_access.xml",
|
||||
"data/sports_clinic_data.xml",
|
||||
"data/admin_access_data.xml",
|
||||
# "data/project_portal_demo_data.xml", # Temporarily disabled for clean upgrade
|
||||
"data/cron_actions.xml",
|
||||
"views/sports_team_views.xml",
|
||||
"views/sports_clinic_menus.xml",
|
||||
|
|
@ -115,12 +147,19 @@
|
|||
"views/player_management_portal_templates.xml",
|
||||
"views/injury_management_portal_templates.xml",
|
||||
"views/task_management_portal_templates.xml",
|
||||
"views/events_portal_templates.xml",
|
||||
"views/project_task_views.xml",
|
||||
"views/project_task_security_test_views.xml",
|
||||
"views/sports_event_views.xml",
|
||||
"views/portal_activity_detail_template.xml",
|
||||
"views/portal_messages_template.xml",
|
||||
"views/portal_attachments_template.xml",
|
||||
"views/portal_event_detail_template.xml",
|
||||
"views/portal_event_edit_template.xml",
|
||||
"views/treatment_note_views.xml",
|
||||
"views/res_partner_views.xml",
|
||||
"views/res_users_views.xml",
|
||||
"views/task_to_event_wizard_views.xml",
|
||||
],
|
||||
"demo": ["data/demo/sports_clinic_demo_data.xml"],
|
||||
"installable": True,
|
||||
|
|
|
|||
|
|
@ -4,3 +4,4 @@ from . import patient_injury_portal
|
|||
from . import team_management_portal
|
||||
from . import player_management_portal
|
||||
from . import task_management_portal
|
||||
from . import events_portal
|
||||
|
|
|
|||
437
bemade_sports_clinic/controllers/events_portal.py
Normal file
437
bemade_sports_clinic/controllers/events_portal.py
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
from odoo.addons.portal.controllers.portal import CustomerPortal, pager
|
||||
from odoo import http, _, fields
|
||||
from odoo.exceptions import UserError, AccessError
|
||||
from datetime import datetime, timedelta
|
||||
from .access_control_mixin import AccessControlMixin
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EventsPortal(CustomerPortal, AccessControlMixin):
|
||||
|
||||
def _prepare_home_portal_values(self, counters):
|
||||
"""Add events count to portal home"""
|
||||
rtn = super()._prepare_home_portal_values(counters)
|
||||
if 'events_count' in counters:
|
||||
events_domain = self._prepare_events_domain()
|
||||
rtn['events_count'] = http.request.env['sports.event'].search_count(events_domain)
|
||||
return rtn
|
||||
|
||||
def _prepare_events_domain(self, view_type='all'):
|
||||
"""Prepare domain for sports events based on user access"""
|
||||
user = http.request.env.user
|
||||
partner = user.partner_id
|
||||
|
||||
# Check if user is therapist (can see all events) or coach (only their teams)
|
||||
is_therapist = user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or \
|
||||
user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
|
||||
is_coach = user.has_group('bemade_sports_clinic.group_portal_team_coach')
|
||||
|
||||
if is_therapist:
|
||||
# Therapists can see all events
|
||||
base_domain = []
|
||||
elif is_coach:
|
||||
# Coaches can only see events for teams they are staff on
|
||||
team_staff_rels = partner.team_staff_rel_ids
|
||||
team_ids = team_staff_rels.mapped('team_id.id')
|
||||
base_domain = [('team_id', 'in', team_ids or [0])]
|
||||
else:
|
||||
# No access for other users
|
||||
base_domain = [('id', '=', 0)] # No results
|
||||
|
||||
# Add view-specific filters
|
||||
if view_type == 'my':
|
||||
# My Events: assigned to current user
|
||||
base_domain.append(('assigned_staff_ids', 'in', [user.id]))
|
||||
elif view_type == 'unassigned':
|
||||
# Unassigned Events: no assigned staff
|
||||
base_domain.append(('assigned_staff_ids', '=', False))
|
||||
# 'all' view uses base domain only
|
||||
|
||||
return base_domain
|
||||
|
||||
def _get_treatment_professionals(self):
|
||||
"""Get all treatment professionals from team staff with relevant roles"""
|
||||
# Search for users who are on team staff with therapist, head therapist, or doctor roles
|
||||
team_staff_users = http.request.env['sports.team.staff'].search([
|
||||
('role', 'in', ['therapist', 'head_therapist', 'doctor', 'treatment_professional'])
|
||||
]).mapped('partner_id.user_ids')
|
||||
|
||||
# Filter active users and sort by name
|
||||
active_users = team_staff_users.filtered(lambda u: u.active)
|
||||
|
||||
_logger.info(f"Found {len(active_users)} treatment professionals from team staff: {[u.name for u in active_users]}")
|
||||
return active_users.sorted('name')
|
||||
|
||||
def _get_accessible_teams(self):
|
||||
"""Get teams accessible to current user"""
|
||||
user = http.request.env.user
|
||||
partner = user.partner_id
|
||||
|
||||
# Check if user is therapist (can see all teams) or coach (only their teams)
|
||||
is_therapist = user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or \
|
||||
user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
|
||||
|
||||
if is_therapist:
|
||||
# Therapists can see all teams
|
||||
teams = http.request.env['sports.team'].search([])
|
||||
else:
|
||||
# Coaches can only see teams they are staff on
|
||||
team_staff_rels = partner.team_staff_rel_ids
|
||||
team_ids = team_staff_rels.mapped('team_id.id')
|
||||
teams = http.request.env['sports.team'].browse(team_ids)
|
||||
|
||||
return teams.sorted('name')
|
||||
|
||||
def _get_organizations(self):
|
||||
"""Get organizations (parent partners of teams)"""
|
||||
teams = self._get_accessible_teams()
|
||||
organizations = teams.mapped('parent_id').filtered(lambda p: p)
|
||||
return organizations.sorted('name')
|
||||
|
||||
@http.route(['/my/events', '/my/events/page/<int:page>'], type='http', auth='user', website=True)
|
||||
def view_events(self, page=1, view_type='all', team_id=None, organization_id=None, assigned_user_id=None,
|
||||
date_from=None, date_to=None, sortby=None, search=None, **kw):
|
||||
"""Main events view with filtering and pagination"""
|
||||
|
||||
# Check access
|
||||
user = http.request.env.user
|
||||
is_therapist = user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or \
|
||||
user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
|
||||
is_coach = user.has_group('bemade_sports_clinic.group_portal_team_coach')
|
||||
|
||||
if not (is_therapist or is_coach or user.has_group('base.group_system')):
|
||||
raise AccessError(_("You don't have access to events."))
|
||||
|
||||
# Prepare base domain
|
||||
domain = self._prepare_events_domain(view_type)
|
||||
|
||||
# Apply additional filters
|
||||
if team_id:
|
||||
domain.append(('team_id', '=', int(team_id)))
|
||||
|
||||
if organization_id:
|
||||
org_id = int(organization_id)
|
||||
domain.append(('partner_id', '=', org_id))
|
||||
# Debug: Log organization filter
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
_logger.info(f"Organization filter applied: partner_id = {org_id}")
|
||||
|
||||
if assigned_user_id:
|
||||
domain.append(('assigned_staff_ids', 'in', [int(assigned_user_id)]))
|
||||
|
||||
if date_from:
|
||||
try:
|
||||
date_from_dt = fields.Datetime.from_string(date_from)
|
||||
domain.append(('date_start', '>=', date_from_dt))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if date_to:
|
||||
try:
|
||||
date_to_dt = fields.Datetime.from_string(date_to)
|
||||
domain.append(('date_start', '<=', date_to_dt))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if search:
|
||||
domain.extend([
|
||||
'|', '|', '|',
|
||||
('name', 'ilike', search),
|
||||
('description', 'ilike', search),
|
||||
('team_id.name', 'ilike', search),
|
||||
('venue_id.name', 'ilike', search)
|
||||
])
|
||||
|
||||
# Sorting options - default to date ascending as requested
|
||||
sort_options = {
|
||||
'date': 'date_start asc',
|
||||
'date_desc': 'date_start desc',
|
||||
'name': 'name',
|
||||
'team': 'team_id',
|
||||
'assigned': 'assigned_staff_ids',
|
||||
}
|
||||
order = sort_options.get(sortby, 'date_start asc') # Default ascending by date
|
||||
|
||||
# Debug: Log final domain and count
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
_logger.info(f"Final events domain: {domain}")
|
||||
|
||||
# Count and pagination
|
||||
event_count = http.request.env['sports.event'].search_count(domain)
|
||||
_logger.info(f"Event count with domain: {event_count}")
|
||||
pager_values = pager(
|
||||
url='/my/events',
|
||||
url_args={'view_type': view_type, 'team_id': team_id, 'organization_id': organization_id,
|
||||
'assigned_user_id': assigned_user_id, 'date_from': date_from,
|
||||
'date_to': date_to, 'sortby': sortby, 'search': search},
|
||||
total=event_count,
|
||||
page=page,
|
||||
step=self._items_per_page,
|
||||
)
|
||||
|
||||
# Get events
|
||||
events = http.request.env['sports.event'].search(
|
||||
domain,
|
||||
order=order,
|
||||
limit=self._items_per_page,
|
||||
offset=pager_values['offset']
|
||||
)
|
||||
|
||||
# Get filter options
|
||||
teams = self._get_accessible_teams()
|
||||
organizations = self._get_organizations()
|
||||
treatment_professionals = self._get_treatment_professionals()
|
||||
|
||||
# Debug: Log filter options
|
||||
_logger.info(f"Available organizations: {[(org.id, org.name) for org in organizations]}")
|
||||
_logger.info(f"Received organization_id parameter: {organization_id}")
|
||||
|
||||
# Debug: Check sample events and their partner_id values
|
||||
sample_events = http.request.env['sports.event'].search([], limit=5)
|
||||
for event in sample_events:
|
||||
_logger.info(f"Event '{event.name}': team={event.team_id.name if event.team_id else None}, partner_id={event.partner_id.name if event.partner_id else None}")
|
||||
|
||||
# Check if user can edit events (only therapists)
|
||||
can_edit = is_therapist
|
||||
|
||||
values = {
|
||||
'events': events,
|
||||
'page_name': 'events',
|
||||
'pager': pager_values,
|
||||
'default_url': '/my/events',
|
||||
'view_type': view_type,
|
||||
'team_id': int(team_id) if team_id else None,
|
||||
'organization_id': int(organization_id) if organization_id else None,
|
||||
'assigned_user_id': int(assigned_user_id) if assigned_user_id else None,
|
||||
'date_from': date_from,
|
||||
'date_to': date_to,
|
||||
'sortby': sortby,
|
||||
'search': search,
|
||||
'teams': teams,
|
||||
'organizations': organizations,
|
||||
'treatment_professionals': treatment_professionals,
|
||||
'can_edit': can_edit,
|
||||
}
|
||||
|
||||
return http.request.render('bemade_sports_clinic.portal_events_list', values)
|
||||
|
||||
@http.route(['/my/event/<int:event_id>'], type='http', auth='user', website=True)
|
||||
def view_event_detail(self, event_id, **kw):
|
||||
"""View individual event detail"""
|
||||
|
||||
# Check access
|
||||
user = http.request.env.user
|
||||
is_therapist = user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or \
|
||||
user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
|
||||
is_coach = user.has_group('bemade_sports_clinic.group_portal_team_coach')
|
||||
|
||||
if not (is_therapist or is_coach or user.has_group('base.group_system')):
|
||||
raise AccessError(_("You don't have access to events."))
|
||||
|
||||
# Get the event
|
||||
event = http.request.env['sports.event'].browse(event_id)
|
||||
if not event.exists():
|
||||
return http.request.not_found()
|
||||
|
||||
# Check team access for coaches
|
||||
if is_coach and not is_therapist:
|
||||
partner = user.partner_id
|
||||
team_staff_rels = partner.team_staff_rel_ids
|
||||
accessible_team_ids = team_staff_rels.mapped('team_id.id')
|
||||
if event.team_id.id not in accessible_team_ids:
|
||||
raise AccessError(_("You don't have access to this event."))
|
||||
|
||||
# Check if user can edit (only therapists)
|
||||
can_edit = is_therapist
|
||||
|
||||
values = {
|
||||
'event': event,
|
||||
'page_name': 'event_detail',
|
||||
'can_edit': can_edit,
|
||||
}
|
||||
|
||||
return http.request.render('bemade_sports_clinic.portal_event_detail', values)
|
||||
|
||||
@http.route(['/my/event/<int:event_id>/edit'], type='http', auth='user', website=True)
|
||||
def edit_event_form(self, event_id, **kw):
|
||||
"""Edit event form - only accessible to therapists"""
|
||||
|
||||
# Check access - only therapists can edit
|
||||
user = http.request.env.user
|
||||
is_therapist = user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or \
|
||||
user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
|
||||
|
||||
if not (is_therapist or user.has_group('base.group_system')):
|
||||
raise AccessError(_("You don't have permission to edit events."))
|
||||
|
||||
# Get the event
|
||||
event = http.request.env['sports.event'].browse(event_id)
|
||||
if not event.exists():
|
||||
return http.request.not_found()
|
||||
|
||||
# Get filter options for form
|
||||
teams = http.request.env['sports.team'].search([])
|
||||
treatment_professionals = self._get_treatment_professionals()
|
||||
venues = http.request.env['res.partner'].search([('is_venue', '=', True)])
|
||||
|
||||
values = {
|
||||
'event': event,
|
||||
'teams': teams,
|
||||
'treatment_professionals': treatment_professionals,
|
||||
'venues': venues,
|
||||
'page_name': 'event_edit',
|
||||
}
|
||||
|
||||
return http.request.render('bemade_sports_clinic.portal_event_edit', values)
|
||||
|
||||
@http.route(['/my/event/<int:event_id>/save'], type='http', auth='user', website=True, methods=['POST'], csrf=False)
|
||||
def save_event(self, event_id, **post):
|
||||
"""Save event changes - only accessible to therapists"""
|
||||
|
||||
# Debug: Log all POST data
|
||||
_logger.info(f"Full POST data received: {dict(post)}")
|
||||
_logger.info(f"POST keys: {list(post.keys())}")
|
||||
|
||||
# Check access - only therapists can edit
|
||||
user = http.request.env.user
|
||||
is_therapist = user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or \
|
||||
user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
|
||||
|
||||
if not (is_therapist or user.has_group('base.group_system')):
|
||||
raise AccessError(_("You don't have permission to edit events."))
|
||||
|
||||
# Get the event
|
||||
event = http.request.env['sports.event'].browse(event_id)
|
||||
if not event.exists():
|
||||
return http.request.not_found()
|
||||
|
||||
try:
|
||||
# Update event fields
|
||||
update_vals = {}
|
||||
|
||||
if 'name' in post:
|
||||
update_vals['name'] = post['name']
|
||||
if 'description' in post:
|
||||
update_vals['description'] = post['description']
|
||||
if 'team_id' in post and post['team_id']:
|
||||
update_vals['team_id'] = int(post['team_id'])
|
||||
if 'venue_id' in post and post['venue_id']:
|
||||
update_vals['venue_id'] = int(post['venue_id'])
|
||||
if 'event_type' in post:
|
||||
update_vals['event_type'] = post['event_type']
|
||||
if 'state' in post:
|
||||
update_vals['state'] = post['state']
|
||||
if 'date_start' in post and post['date_start']:
|
||||
# Parse datetime from HTML datetime-local format (ISO format)
|
||||
date_str = post['date_start']
|
||||
try:
|
||||
if 'T' in date_str:
|
||||
# ISO format: 2025-05-26T12:15
|
||||
update_vals['date_start'] = datetime.strptime(date_str, '%Y-%m-%dT%H:%M')
|
||||
else:
|
||||
# Standard format: 2025-05-26 12:15
|
||||
update_vals['date_start'] = datetime.strptime(date_str, '%Y-%m-%d %H:%M')
|
||||
except ValueError as ve:
|
||||
# Try alternative formats
|
||||
try:
|
||||
# Try with seconds
|
||||
if 'T' in date_str:
|
||||
update_vals['date_start'] = datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S')
|
||||
else:
|
||||
update_vals['date_start'] = datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S')
|
||||
except ValueError:
|
||||
# Last fallback: let Odoo handle it
|
||||
update_vals['date_start'] = date_str
|
||||
|
||||
if 'date_end' in post and post['date_end']:
|
||||
date_str = post['date_end']
|
||||
try:
|
||||
if 'T' in date_str:
|
||||
update_vals['date_end'] = datetime.strptime(date_str, '%Y-%m-%dT%H:%M')
|
||||
else:
|
||||
update_vals['date_end'] = datetime.strptime(date_str, '%Y-%m-%d %H:%M')
|
||||
except ValueError:
|
||||
try:
|
||||
if 'T' in date_str:
|
||||
update_vals['date_end'] = datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S')
|
||||
else:
|
||||
update_vals['date_end'] = datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S')
|
||||
except ValueError:
|
||||
update_vals['date_end'] = date_str
|
||||
|
||||
if 'therapist_start' in post and post['therapist_start']:
|
||||
date_str = post['therapist_start']
|
||||
try:
|
||||
if 'T' in date_str:
|
||||
update_vals['therapist_start'] = datetime.strptime(date_str, '%Y-%m-%dT%H:%M')
|
||||
else:
|
||||
update_vals['therapist_start'] = datetime.strptime(date_str, '%Y-%m-%d %H:%M')
|
||||
except ValueError:
|
||||
try:
|
||||
if 'T' in date_str:
|
||||
update_vals['therapist_start'] = datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S')
|
||||
else:
|
||||
update_vals['therapist_start'] = datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S')
|
||||
except ValueError:
|
||||
update_vals['therapist_start'] = date_str
|
||||
|
||||
if 'therapist_end' in post and post['therapist_end']:
|
||||
date_str = post['therapist_end']
|
||||
try:
|
||||
if 'T' in date_str:
|
||||
update_vals['therapist_end'] = datetime.strptime(date_str, '%Y-%m-%dT%H:%M')
|
||||
else:
|
||||
update_vals['therapist_end'] = datetime.strptime(date_str, '%Y-%m-%d %H:%M')
|
||||
except ValueError:
|
||||
try:
|
||||
if 'T' in date_str:
|
||||
update_vals['therapist_end'] = datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S')
|
||||
else:
|
||||
update_vals['therapist_end'] = datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S')
|
||||
except ValueError:
|
||||
update_vals['therapist_end'] = date_str
|
||||
|
||||
# Handle assigned staff (many2many)
|
||||
# Check for both array-style and regular parameter names
|
||||
staff_param = None
|
||||
if 'assigned_staff_ids' in post:
|
||||
staff_param = post['assigned_staff_ids']
|
||||
param_name = 'assigned_staff_ids'
|
||||
elif 'assigned_staff_ids[]' in post:
|
||||
staff_param = post['assigned_staff_ids[]']
|
||||
param_name = 'assigned_staff_ids[]'
|
||||
|
||||
if staff_param is not None:
|
||||
_logger.info(f"Raw {param_name} from form: {staff_param} (type: {type(staff_param)})")
|
||||
staff_ids = []
|
||||
if isinstance(staff_param, list):
|
||||
# Handle list of values
|
||||
staff_ids = [int(x) for x in staff_param if x]
|
||||
elif staff_param:
|
||||
# Handle single value or comma-separated string
|
||||
if ',' in str(staff_param):
|
||||
# Comma-separated values from JavaScript workaround
|
||||
staff_ids = [int(x.strip()) for x in str(staff_param).split(',') if x.strip()]
|
||||
else:
|
||||
# Single value
|
||||
staff_ids = [int(staff_param)]
|
||||
_logger.info(f"Processed staff_ids: {staff_ids}")
|
||||
update_vals['assigned_staff_ids'] = [(6, 0, staff_ids)]
|
||||
else:
|
||||
_logger.info("No assigned_staff_ids parameter in post data")
|
||||
# If no staff selected, clear the field
|
||||
update_vals['assigned_staff_ids'] = [(6, 0, [])]
|
||||
|
||||
# Update the event
|
||||
event.write(update_vals)
|
||||
|
||||
return http.request.redirect(f'/my/event/{event_id}?success=1')
|
||||
|
||||
except Exception as e:
|
||||
# Clean error message to prevent redirect issues with newlines
|
||||
error_msg = str(e).replace('\n', ' ').replace('\r', ' ')
|
||||
return http.request.redirect(f'/my/event/{event_id}/edit?error={error_msg}')
|
||||
|
|
@ -38,11 +38,25 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
|||
is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||
user = request.env.user
|
||||
|
||||
# Get treatment professionals for the multi-select field (if treatment professional)
|
||||
treatment_professionals = []
|
||||
parental_consent_options = None
|
||||
if is_treatment_prof:
|
||||
# Include both portal and internal treatment professionals
|
||||
portal_tp_group = request.env.ref('bemade_sports_clinic.group_portal_treatment_professional')
|
||||
internal_tp_group = request.env.ref('bemade_sports_clinic.group_sports_clinic_treatment_professional')
|
||||
treatment_professionals = request.env['res.users'].search([
|
||||
('groups_id', 'in', [portal_tp_group.id, internal_tp_group.id])
|
||||
])
|
||||
parental_consent_options = request.env['sports.patient.injury']._fields['parental_consent'].selection
|
||||
|
||||
values = {
|
||||
'patient': patient,
|
||||
'return_url': return_url,
|
||||
'page_name': 'report_injury',
|
||||
'is_treatment_prof': is_treatment_prof, # Pass flag to template for conditional display
|
||||
'treatment_professionals': treatment_professionals,
|
||||
'parental_consent_options': parental_consent_options,
|
||||
}
|
||||
|
||||
return request.render('bemade_sports_clinic.portal_create_injury', values)
|
||||
|
|
@ -76,11 +90,18 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
|||
vals = {
|
||||
'patient_id': patient.id,
|
||||
'diagnosis': post.get('diagnosis', ''),
|
||||
'injury_date': post.get('injury_date'),
|
||||
'external_notes': post.get('external_notes', ''),
|
||||
'stage': 'active' if is_treatment_prof else 'unverified',
|
||||
}
|
||||
|
||||
# Handle injury date and injury_date_na checkbox
|
||||
if post.get('injury_date_na'):
|
||||
vals['injury_date_na'] = True
|
||||
vals['injury_date'] = False # Clear injury_date if N/A is checked
|
||||
elif post.get('injury_date'):
|
||||
vals['injury_date'] = post.get('injury_date')
|
||||
vals['injury_date_na'] = False
|
||||
|
||||
# Only add team_id if we have a single team for the patient
|
||||
if team_id:
|
||||
vals['team_id'] = int(team_id)
|
||||
|
|
@ -92,6 +113,10 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
|||
if post.get('predicted_resolution_date'):
|
||||
vals['predicted_resolution_date'] = post.get('predicted_resolution_date')
|
||||
|
||||
# Handle internal notes for treatment professionals
|
||||
if is_treatment_prof and post.get('internal_notes'):
|
||||
vals['internal_notes'] = post.get('internal_notes')
|
||||
|
||||
# Create the injury record - portal users now have create permission
|
||||
injury = request.env['sports.patient.injury'].create(vals)
|
||||
|
||||
|
|
@ -103,12 +128,26 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
|||
|
||||
# Assign treatment professionals based on user role
|
||||
|
||||
# If user is a treatment professional, add them to the treatment professionals
|
||||
# Only check group membership, not computed field
|
||||
# Handle treatment professional assignments
|
||||
treatment_prof_ids = []
|
||||
|
||||
# If user is a treatment professional, add them by default
|
||||
if is_treatment_prof:
|
||||
# Add current user as treatment professional
|
||||
treatment_prof_ids.append(user.id)
|
||||
|
||||
# Also add any additional treatment professionals selected in the form (checkbox-based)
|
||||
selected_tp_ids = request.httprequest.form.getlist('treatment_professional_ids[]')
|
||||
if selected_tp_ids:
|
||||
# Convert to integers and add to list (avoiding duplicates)
|
||||
for tp_id in selected_tp_ids:
|
||||
tp_id_int = int(tp_id)
|
||||
if tp_id_int not in treatment_prof_ids:
|
||||
treatment_prof_ids.append(tp_id_int)
|
||||
|
||||
# Assign treatment professionals if any were identified
|
||||
if treatment_prof_ids:
|
||||
injury.write({
|
||||
'treatment_professional_ids': [(4, user.id)]
|
||||
'treatment_professional_ids': [(6, 0, treatment_prof_ids)]
|
||||
})
|
||||
else:
|
||||
# User is not a treatment professional
|
||||
|
|
@ -180,6 +219,21 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
|||
if not treatment_pros_assigned:
|
||||
_logger.warning("No valid therapists found to assign to the injury")
|
||||
|
||||
# Handle treatment note creation if provided by treatment professional
|
||||
if is_treatment_prof and post.get('treatment_note'):
|
||||
treatment_note_text = post.get('treatment_note').strip()
|
||||
if treatment_note_text:
|
||||
try:
|
||||
# Create treatment note using the injury's _add_treatment_note method
|
||||
injury._add_treatment_note(
|
||||
patient=injury.patient_id,
|
||||
note=treatment_note_text,
|
||||
user=request.env.user
|
||||
)
|
||||
_logger.info(f"Treatment note added to injury {injury.id} by user {request.env.user.id}")
|
||||
except Exception as e:
|
||||
_logger.error(f"Failed to create treatment note for injury {injury.id}: {str(e)}")
|
||||
|
||||
# Trigger recomputation of patient status based on the injury
|
||||
patient._compute_is_injured()
|
||||
patient._compute_stage()
|
||||
|
|
@ -220,9 +274,11 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
|||
stage_selection = request.env['sports.patient.injury']._fields['stage'].selection
|
||||
stages = [(k, v) for k, v in stage_selection]
|
||||
|
||||
# Get treatment professionals for the multi-select field
|
||||
# Get treatment professionals for the multi-select field (both portal and internal)
|
||||
portal_tp_group = request.env.ref('bemade_sports_clinic.group_portal_treatment_professional')
|
||||
internal_tp_group = request.env.ref('bemade_sports_clinic.group_sports_clinic_treatment_professional')
|
||||
treatment_professionals = request.env['res.users'].search([
|
||||
('groups_id', 'in', request.env.ref('bemade_sports_clinic.group_portal_treatment_professional').id)
|
||||
('groups_id', 'in', [portal_tp_group.id, internal_tp_group.id])
|
||||
])
|
||||
|
||||
# Get parental consent options if treatment professional
|
||||
|
|
@ -293,18 +349,15 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
|||
if post.get('resolution_date'):
|
||||
vals['resolution_date'] = post.get('resolution_date')
|
||||
|
||||
# Handle treatment professionals (multi-select)
|
||||
if post.get('treatment_professional_ids'):
|
||||
# Convert to list if it's a single value
|
||||
prof_ids = post.get('treatment_professional_ids')
|
||||
if isinstance(prof_ids, str):
|
||||
prof_ids = [prof_ids]
|
||||
elif not isinstance(prof_ids, list):
|
||||
prof_ids = [prof_ids]
|
||||
|
||||
# Handle treatment professionals (checkbox-based multi-select)
|
||||
selected_tp_ids = request.httprequest.form.getlist('treatment_professional_ids[]')
|
||||
if selected_tp_ids:
|
||||
# Convert to integers and set using Odoo's many2many syntax
|
||||
prof_ids = [int(pid) for pid in prof_ids if pid]
|
||||
prof_ids = [int(pid) for pid in selected_tp_ids if pid]
|
||||
vals['treatment_professional_ids'] = [(6, 0, prof_ids)]
|
||||
else:
|
||||
# If no checkboxes are selected, clear the treatment professionals
|
||||
vals['treatment_professional_ids'] = [(6, 0, [])]
|
||||
|
||||
# Fields only treatment professionals can update
|
||||
if is_treatment_prof:
|
||||
|
|
@ -639,3 +692,41 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
|||
except Exception as e:
|
||||
_logger.error(f"Error verifying injury: {e}")
|
||||
return request.redirect('/my')
|
||||
|
||||
@http.route(['/my/injury/delete'], type='http', auth='user', website=True, methods=['POST'])
|
||||
def delete_injury(self, **post):
|
||||
"""Delete an injury record (only for treatment professionals)"""
|
||||
injury_id = post.get('injury_id')
|
||||
return_url = post.get('return_url', '/my/players')
|
||||
|
||||
if not injury_id:
|
||||
return request.redirect(return_url)
|
||||
|
||||
try:
|
||||
injury = self._check_access_to_injury(injury_id)
|
||||
except UserError as e:
|
||||
return request.render('http_routing.http_error', {
|
||||
'status_code': 403,
|
||||
'status_message': 'Forbidden',
|
||||
'error_message': str(e)
|
||||
})
|
||||
|
||||
# Check if user is a treatment professional (only they can delete injuries)
|
||||
is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||
if not is_treatment_prof:
|
||||
return request.redirect(f'{return_url}?error=permission_denied')
|
||||
|
||||
# Get patient info before deletion for redirect
|
||||
patient_id = injury.patient_id.id
|
||||
|
||||
try:
|
||||
# Delete the injury record (this will cascade delete related records)
|
||||
injury.sudo().unlink()
|
||||
_logger.info(f"Injury {injury_id} deleted by user {request.env.user.id}")
|
||||
|
||||
# Redirect back to player page with success message
|
||||
return request.redirect(f'/my/player?player_id={patient_id}&success=injury_deleted')
|
||||
|
||||
except Exception as e:
|
||||
_logger.error(f"Error deleting injury {injury_id}: {str(e)}")
|
||||
return request.redirect(f'{return_url}?error=delete_failed')
|
||||
|
|
|
|||
|
|
@ -9,11 +9,14 @@ class TeamStaffPortal(CustomerPortal):
|
|||
teams_domain = self._prepare_teams_domain()
|
||||
players_domain = self._prepare_players_domain(teams_domain)
|
||||
activities_domain = self._prepare_activities_domain()
|
||||
events_domain = self._prepare_events_domain()
|
||||
rtn['teams_count'] = http.request.env['sports.team'].search_count(teams_domain)
|
||||
rtn['players_count'] = http.request.env['sports.patient'].search_count(
|
||||
players_domain)
|
||||
rtn['activities_count'] = http.request.env['mail.activity'].search_count(
|
||||
activities_domain)
|
||||
rtn['events_count'] = http.request.env['sports.event'].search_count(
|
||||
events_domain)
|
||||
return rtn
|
||||
|
||||
@classmethod
|
||||
|
|
@ -55,6 +58,29 @@ class TeamStaffPortal(CustomerPortal):
|
|||
('res_id', 'in', team_staff_rels.mapped('team_id.id') or [0])
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def _prepare_events_domain(cls):
|
||||
"""Prepare domain for sports events based on user access"""
|
||||
user = http.request.env.user
|
||||
partner = user.partner_id
|
||||
|
||||
# Check if user is therapist (can see all events) or coach (only their teams)
|
||||
is_therapist = user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or \
|
||||
user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
|
||||
is_coach = user.has_group('bemade_sports_clinic.group_portal_team_coach')
|
||||
|
||||
if is_therapist:
|
||||
# Therapists can see all events
|
||||
return []
|
||||
elif is_coach:
|
||||
# Coaches can only see events for teams they are staff on
|
||||
team_staff_rels = partner.team_staff_rel_ids
|
||||
team_ids = team_staff_rels.mapped('team_id.id')
|
||||
return [('team_id', 'in', team_ids or [0])]
|
||||
else:
|
||||
# No access for other users
|
||||
return [('id', '=', 0)] # No results
|
||||
|
||||
@http.route(route=['/my/teams', '/my/teams/page/<int:page>'], type='http', auth='user', website=True)
|
||||
def view_teams(self, page=0, **kw):
|
||||
""" Display the list of teams that a portal user has access to """
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- Sports Clinic Events Project -->
|
||||
<record id="project_sports_clinic_events" model="project.project">
|
||||
<field name="name">Sports Clinic Events</field>
|
||||
<field name="description">Default project for sports clinic events and activities</field>
|
||||
<field name="is_sports_clinic_project">True</field>
|
||||
<field name="privacy_visibility">portal</field>
|
||||
<field name="partner_id" ref="team_carabins"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
</record>
|
||||
|
||||
<!-- Team-Specific Projects -->
|
||||
<record id="project_team_carabins" model="project.project">
|
||||
<field name="name">Carabins Team Events</field>
|
||||
<field name="description">Events and activities for Carabins team</field>
|
||||
<field name="is_sports_clinic_project">True</field>
|
||||
<field name="privacy_visibility">portal</field>
|
||||
<field name="partner_id" ref="team_carabins"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
</record>
|
||||
|
||||
<!-- Sample Project Tasks (Events) -->
|
||||
<record id="task_team_meeting_carabins" model="project.task">
|
||||
<field name="name">Team Meeting - Injury Prevention</field>
|
||||
<field name="description">Monthly team meeting to discuss injury prevention strategies</field>
|
||||
<field name="project_id" ref="project_team_carabins"/>
|
||||
<field name="user_ids" eval="[(4, ref('base.user_admin'))]"/>
|
||||
<field name="partner_id" ref="team_carabins"/>
|
||||
<field name="date_deadline" eval="(DateTime.now() + timedelta(days=7)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="priority">1</field>
|
||||
</record>
|
||||
|
||||
<record id="task_medical_checkup" model="project.task">
|
||||
<field name="name">Pre-Season Medical Checkups</field>
|
||||
<field name="description">Conduct medical checkups for all team members before season start</field>
|
||||
<field name="project_id" ref="project_team_carabins"/>
|
||||
<field name="user_ids" eval="[(4, ref('base.user_admin'))]"/>
|
||||
<field name="partner_id" ref="team_carabins"/>
|
||||
<field name="date_deadline" eval="(DateTime.now() + timedelta(days=14)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="priority">2</field>
|
||||
</record>
|
||||
|
||||
<!-- Project Followers - Add treatment professional as follower -->
|
||||
<record id="project_follower_carabins_therapist" model="mail.followers">
|
||||
<field name="res_model">project.project</field>
|
||||
<field name="res_id" ref="project_team_carabins"/>
|
||||
<field name="partner_id" ref="partner_therapist_team_carabins"/>
|
||||
</record>
|
||||
|
||||
<record id="project_follower_sports_clinic_therapist" model="mail.followers">
|
||||
<field name="res_model">project.project</field>
|
||||
<field name="res_id" ref="project_sports_clinic_events"/>
|
||||
<field name="partner_id" ref="partner_therapist_team_carabins"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -3,7 +3,11 @@ from . import patient
|
|||
from . import patient_injury
|
||||
from . import patient_contact
|
||||
from . import res_partner
|
||||
from . import sports_team
|
||||
from . import res_users
|
||||
from . import sports_team
|
||||
from . import treatment_note
|
||||
from . import injury_document
|
||||
from . import project_task
|
||||
from . import project_task_security_test
|
||||
from . import project_project
|
||||
from . import sports_event
|
||||
|
|
|
|||
|
|
@ -62,10 +62,7 @@ class PatientInjury(models.Model):
|
|||
column1="patient_injury_id",
|
||||
column2="treatment_pro_id",
|
||||
string="Treatment Professionals",
|
||||
domain=lambda self: [('groups_id', 'in', [
|
||||
self.env.ref('bemade_sports_clinic.group_sports_clinic_treatment_professional').id,
|
||||
self.env.ref('bemade_sports_clinic.group_portal_treatment_professional').id
|
||||
])],
|
||||
|
||||
tracking=True,
|
||||
)
|
||||
predicted_resolution_date = fields.Date(tracking=True)
|
||||
|
|
|
|||
96
bemade_sports_clinic/models/project_project.py
Normal file
96
bemade_sports_clinic/models/project_project.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class ProjectProject(models.Model):
|
||||
_inherit = 'project.project'
|
||||
|
||||
# Portal access group definition for reuse - only authorized sports clinic users
|
||||
_portal_groups = 'base.group_user,bemade_sports_clinic.group_portal_treatment_professional,bemade_sports_clinic.group_portal_team_coach'
|
||||
|
||||
# Override core fields to grant portal access
|
||||
name = fields.Char(groups=_portal_groups)
|
||||
description = fields.Html(groups=_portal_groups)
|
||||
partner_id = fields.Many2one(groups=_portal_groups)
|
||||
user_id = fields.Many2one(groups=_portal_groups)
|
||||
privacy_visibility = fields.Selection(groups=_portal_groups)
|
||||
is_favorite = fields.Boolean(groups=_portal_groups)
|
||||
color = fields.Integer(groups=_portal_groups)
|
||||
stage_id = fields.Many2one(groups=_portal_groups)
|
||||
|
||||
# Sports clinic specific fields
|
||||
is_sports_clinic_project = fields.Boolean(
|
||||
string='Sports Clinic Project',
|
||||
default=False,
|
||||
help='Mark this project as related to sports clinic activities',
|
||||
groups=_portal_groups
|
||||
)
|
||||
|
||||
related_team_ids = fields.Many2many(
|
||||
'sports.team',
|
||||
string='Related Teams',
|
||||
help='Teams associated with this project',
|
||||
groups=_portal_groups
|
||||
)
|
||||
|
||||
@api.model
|
||||
def create_sports_clinic_project(self, name, team_id=None, description=None):
|
||||
"""Create a project specifically for sports clinic events"""
|
||||
vals = {
|
||||
'name': name,
|
||||
'is_sports_clinic_project': True,
|
||||
'privacy_visibility': 'portal', # Allow portal access
|
||||
'description': description or f'Project for sports clinic events: {name}',
|
||||
}
|
||||
|
||||
if team_id:
|
||||
vals['partner_id'] = team_id # Set team as project partner
|
||||
|
||||
project = self.create(vals)
|
||||
|
||||
# Only add authorized treatment professionals as followers
|
||||
if team_id:
|
||||
project.ensure_portal_access_for_treatment_professionals()
|
||||
|
||||
return project
|
||||
|
||||
def ensure_portal_access_for_treatment_professionals(self):
|
||||
"""Ensure authorized treatment professionals have access to this project"""
|
||||
self.ensure_one()
|
||||
|
||||
# Only add treatment professionals who have team relationships with this project
|
||||
if self.partner_id: # Project must have a partner (team)
|
||||
# Find treatment professionals who are staff on this team
|
||||
team_staff = self.env['sports.team.staff'].search([
|
||||
('team_id', '=', self.partner_id.id),
|
||||
('role', 'in', ['therapist', 'head_therapist'])
|
||||
])
|
||||
|
||||
authorized_partners = team_staff.mapped('partner_id')
|
||||
current_followers = self.message_partner_ids
|
||||
new_followers = authorized_partners - current_followers
|
||||
|
||||
if new_followers:
|
||||
self.message_subscribe(partner_ids=new_followers.ids)
|
||||
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def get_or_create_default_sports_project(self):
|
||||
"""Get or create a default project for sports clinic events"""
|
||||
default_project = self.search([
|
||||
('is_sports_clinic_project', '=', True),
|
||||
('name', '=', 'Sports Clinic Events')
|
||||
], limit=1)
|
||||
|
||||
if not default_project:
|
||||
default_project = self.create_sports_clinic_project(
|
||||
name='Sports Clinic Events',
|
||||
description='Default project for sports clinic events and activities'
|
||||
)
|
||||
|
||||
# Ensure portal access is configured
|
||||
default_project.ensure_portal_access_for_treatment_professionals()
|
||||
|
||||
return default_project
|
||||
77
bemade_sports_clinic/models/project_task.py
Normal file
77
bemade_sports_clinic/models/project_task.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
from odoo import api, fields, models
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
|
||||
class ProjectTask(models.Model):
|
||||
_inherit = 'project.task'
|
||||
|
||||
# Portal access group definition for reuse - only authorized sports clinic users
|
||||
_portal_groups = 'base.group_user,bemade_sports_clinic.group_portal_treatment_professional,bemade_sports_clinic.group_portal_team_coach'
|
||||
|
||||
# Override core fields to grant portal access
|
||||
name = fields.Char(groups=_portal_groups)
|
||||
description = fields.Html(groups=_portal_groups)
|
||||
user_ids = fields.Many2many(groups=_portal_groups)
|
||||
project_id = fields.Many2one(groups=_portal_groups)
|
||||
stage_id = fields.Many2one(groups=_portal_groups)
|
||||
tag_ids = fields.Many2many(groups=_portal_groups)
|
||||
partner_id = fields.Many2one(groups=_portal_groups)
|
||||
date_deadline = fields.Datetime(groups=_portal_groups)
|
||||
priority = fields.Selection(groups=_portal_groups)
|
||||
sequence = fields.Integer(groups=_portal_groups)
|
||||
state = fields.Selection(groups=_portal_groups)
|
||||
is_closed = fields.Boolean(groups=_portal_groups)
|
||||
task_properties = fields.Properties(groups=_portal_groups)
|
||||
|
||||
date_start = fields.Datetime(
|
||||
string='Starting Date',
|
||||
groups=_portal_groups
|
||||
)
|
||||
|
||||
date_end = fields.Datetime(
|
||||
string='Ending Date',
|
||||
groups=_portal_groups
|
||||
)
|
||||
|
||||
|
||||
|
||||
@api.model
|
||||
def check_portal_access_rights(self, operation, raise_exception=True):
|
||||
"""Check if portal treatment professional has access to perform operation"""
|
||||
if self.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional'):
|
||||
allowed_operations = ['read', 'write', 'create']
|
||||
if operation in allowed_operations:
|
||||
return True
|
||||
|
||||
if raise_exception:
|
||||
raise AccessError(f"Portal treatment professionals cannot perform {operation} operation")
|
||||
return False
|
||||
|
||||
def check_portal_task_access(self):
|
||||
"""Check if current user can access this specific task"""
|
||||
if not self.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional'):
|
||||
return False
|
||||
|
||||
user = self.env.user
|
||||
partner = user.partner_id
|
||||
|
||||
# Check if user is assigned to task
|
||||
if user in self.user_ids:
|
||||
return True
|
||||
|
||||
# Check if user is follower of task
|
||||
if partner in self.message_partner_ids:
|
||||
return True
|
||||
|
||||
# Check if user is follower of project
|
||||
if self.project_id and partner in self.project_id.message_partner_ids:
|
||||
return True
|
||||
|
||||
# Check if user's teams are partners of the project
|
||||
if self.project_id:
|
||||
team_staff_rels = partner.team_staff_rel_ids
|
||||
team_ids = team_staff_rels.mapped('team_id.id')
|
||||
if self.project_id.partner_id.id in team_ids:
|
||||
return True
|
||||
|
||||
return False
|
||||
136
bemade_sports_clinic/models/project_task_security_test.py
Normal file
136
bemade_sports_clinic/models/project_task_security_test.py
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import AccessError
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProjectTaskSecurityTest(models.TransientModel):
|
||||
_name = 'project.task.security.test'
|
||||
_description = 'Project Task Security Testing Utility'
|
||||
|
||||
task_id = fields.Many2one('project.task', string='Task to Test', required=True)
|
||||
user_id = fields.Many2one('res.users', string='User to Test', required=True)
|
||||
test_results = fields.Text(string='Test Results', readonly=True)
|
||||
|
||||
def action_test_field_access(self):
|
||||
"""Test field access for the specified user and task"""
|
||||
self.ensure_one()
|
||||
|
||||
# List of critical fields to test
|
||||
fields_to_test = [
|
||||
'name', 'description', 'user_ids', 'project_id', 'stage_id',
|
||||
'tag_ids', 'partner_id', 'date_deadline', 'priority', 'sequence',
|
||||
'state', 'is_closed', 'date_start', 'date_end',
|
||||
'date_event_start', 'date_event_end'
|
||||
]
|
||||
|
||||
results = []
|
||||
results.append(f"Testing field access for user: {self.user_id.name}")
|
||||
results.append(f"Testing task: {self.task_id.name}")
|
||||
results.append(f"User groups: {', '.join(self.user_id.groups_id.mapped('name'))}")
|
||||
results.append("=" * 50)
|
||||
|
||||
# Test as the specified user
|
||||
task_as_user = self.task_id.sudo(self.user_id)
|
||||
|
||||
for field_name in fields_to_test:
|
||||
try:
|
||||
# Attempt to read the field
|
||||
field_value = getattr(task_as_user, field_name)
|
||||
results.append(f"✅ {field_name}: ACCESSIBLE")
|
||||
|
||||
# Test field groups if defined
|
||||
field_obj = self.env['project.task']._fields.get(field_name)
|
||||
if field_obj and hasattr(field_obj, 'groups') and field_obj.groups:
|
||||
results.append(f" Groups: {field_obj.groups}")
|
||||
|
||||
except AccessError as e:
|
||||
results.append(f"❌ {field_name}: ACCESS DENIED - {str(e)}")
|
||||
except Exception as e:
|
||||
results.append(f"⚠️ {field_name}: ERROR - {str(e)}")
|
||||
|
||||
results.append("=" * 50)
|
||||
|
||||
# Test model-level access
|
||||
try:
|
||||
task_as_user.check_access_rights('read')
|
||||
results.append("✅ Model READ access: GRANTED")
|
||||
except AccessError:
|
||||
results.append("❌ Model READ access: DENIED")
|
||||
|
||||
try:
|
||||
task_as_user.check_access_rights('write')
|
||||
results.append("✅ Model WRITE access: GRANTED")
|
||||
except AccessError:
|
||||
results.append("❌ Model WRITE access: DENIED")
|
||||
|
||||
try:
|
||||
task_as_user.check_access_rights('create')
|
||||
results.append("✅ Model CREATE access: GRANTED")
|
||||
except AccessError:
|
||||
results.append("❌ Model CREATE access: DENIED")
|
||||
|
||||
# Test record-level access
|
||||
try:
|
||||
task_as_user.check_access_rule('read')
|
||||
results.append("✅ Record READ access: GRANTED")
|
||||
except AccessError:
|
||||
results.append("❌ Record READ access: DENIED")
|
||||
|
||||
try:
|
||||
task_as_user.check_access_rule('write')
|
||||
results.append("✅ Record WRITE access: GRANTED")
|
||||
except AccessError:
|
||||
results.append("❌ Record WRITE access: DENIED")
|
||||
|
||||
# Test portal-specific access method
|
||||
if hasattr(task_as_user, 'check_portal_task_access'):
|
||||
try:
|
||||
portal_access = task_as_user.check_portal_task_access()
|
||||
results.append(f"✅ Portal task access: {'GRANTED' if portal_access else 'DENIED'}")
|
||||
except Exception as e:
|
||||
results.append(f"⚠️ Portal task access: ERROR - {str(e)}")
|
||||
|
||||
self.test_results = '\n'.join(results)
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'project.task.security.test',
|
||||
'res_id': self.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
@api.model
|
||||
def quick_test_portal_access(self, task_id=None, user_login=None):
|
||||
"""Quick test method for debugging - can be called from shell"""
|
||||
if not task_id:
|
||||
# Find any task
|
||||
task = self.env['project.task'].search([], limit=1)
|
||||
if not task:
|
||||
return "No tasks found to test"
|
||||
task_id = task.id
|
||||
|
||||
if not user_login:
|
||||
# Find a portal treatment professional
|
||||
portal_group = self.env.ref('bemade_sports_clinic.group_portal_treatment_professional')
|
||||
portal_users = portal_group.users
|
||||
if not portal_users:
|
||||
return "No portal treatment professionals found"
|
||||
user_login = portal_users[0].login
|
||||
|
||||
user = self.env['res.users'].search([('login', '=', user_login)], limit=1)
|
||||
if not user:
|
||||
return f"User {user_login} not found"
|
||||
|
||||
# Create test record and run test
|
||||
test_record = self.create({
|
||||
'task_id': task_id,
|
||||
'user_id': user.id
|
||||
})
|
||||
|
||||
test_record.action_test_field_access()
|
||||
return test_record.test_results
|
||||
|
|
@ -26,6 +26,13 @@ class Partner(models.Model):
|
|||
patient_ids = fields.One2many(
|
||||
comodel_name="sports.patient", inverse_name="partner_id"
|
||||
)
|
||||
|
||||
# Boolean field to identify venues
|
||||
is_venue = fields.Boolean(
|
||||
string='Is Venue',
|
||||
default=False,
|
||||
help='Check this box if this contact represents a venue/location'
|
||||
)
|
||||
|
||||
def write(self, vals):
|
||||
if (
|
||||
|
|
|
|||
407
bemade_sports_clinic/models/sports_event.py
Normal file
407
bemade_sports_clinic/models/sports_event.py
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
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,
|
||||
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='draft', 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', 'date_end')
|
||||
def _onchange_event_dates(self):
|
||||
"""Set default therapist times when event dates change"""
|
||||
if self.date_start and self.date_end:
|
||||
# Only set therapist times if they are currently blank
|
||||
if not self.therapist_start:
|
||||
# Set therapist start to 30 minutes before event start
|
||||
from datetime import timedelta
|
||||
self.therapist_start = self.date_start - timedelta(minutes=30)
|
||||
|
||||
if not self.therapist_end:
|
||||
# Set therapist end to event end time
|
||||
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
|
||||
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)
|
||||
|
|
@ -4,7 +4,10 @@ access_patient_treatment_pro,Treatment Professional Access for Patients,model_sp
|
|||
access_patient_admin,Admin Access for Patients,model_sports_patient,group_sports_clinic_admin,1,1,1,1
|
||||
access_patient_portal,Portal Access for Patients,model_sports_patient,base.group_portal,1,1,1,0
|
||||
access_patient_portal_tp,Portal Treatment Professional Access for Patients,model_sports_patient,bemade_sports_clinic.group_portal_treatment_professional,1,1,1,0
|
||||
access_patient_portal_coach,Portal Coach Access for Patients,model_sports_patient,bemade_sports_clinic.group_portal_team_coach,1,1,1,0
|
||||
access_treatment_note_portal_coach,Portal Coach Access for Treatment Notes,model_sports_treatment_note,bemade_sports_clinic.group_portal_team_coach,1,0,0,0
|
||||
access_sports_event_user,User Access for Sports Events,model_sports_event,base.group_user,1,1,1,1
|
||||
access_sports_event_portal_treatment_professional,Portal Treatment Professional Access for Sports Events,model_sports_event,bemade_sports_clinic.group_portal_treatment_professional,1,1,1,0
|
||||
access_sports_event_portal_team_coach,Portal Team Coach Access for Sports Events,model_sports_event,bemade_sports_clinic.group_portal_team_coach,1,0,0,0
|
||||
access_patient_contact_user,User Access for Patient Contacts,model_sports_patient_contact,group_sports_clinic_user,1,1,1,1
|
||||
access_patient_contact_portal_tp,Portal TP Access for Patient Contacts,model_sports_patient_contact,bemade_sports_clinic.group_portal_treatment_professional,1,1,1,0
|
||||
access_injury_treatment_pro,Treatment Professional Access for Injuries,model_sports_patient_injury,group_sports_clinic_treatment_professional,1,1,1,1
|
||||
|
|
@ -49,7 +52,16 @@ access_res_users_portal_coach,Portal Coach Access for Users,base.model_res_users
|
|||
access_res_partner_portal_tp,Portal TP Access for Partners,base.model_res_partner,bemade_sports_clinic.group_portal_treatment_professional,1,1,1,0
|
||||
access_res_partner_portal_coach,Portal Coach Access for Partners,base.model_res_partner,bemade_sports_clinic.group_portal_team_coach,1,1,1,0
|
||||
access_mail_followers_portal_tp,Portal TP Access for Followers,mail.model_mail_followers,bemade_sports_clinic.group_portal_treatment_professional,1,1,1,0
|
||||
access_mail_followers_portal_coach,Portal Coach Access for Followers,mail.model_mail_followers,bemade_sports_clinic.group_portal_team_coach,1,1,1,0
|
||||
access_bus_bus_portal_tp,Portal TP Access for Bus Messages,bus.model_bus_bus,bemade_sports_clinic.group_portal_treatment_professional,1,1,1,0
|
||||
access_mail_followers_portal_tp,Portal TP Access for Mail Followers,mail.model_mail_followers,bemade_sports_clinic.group_portal_treatment_professional,1,1,1,0
|
||||
access_bus_bus_portal_tp,Portal TP Access for Bus,bus.model_bus_bus,bemade_sports_clinic.group_portal_treatment_professional,1,1,1,0
|
||||
access_project_task_portal_tp,Portal TP Access for Project Tasks,project.model_project_task,bemade_sports_clinic.group_portal_treatment_professional,1,1,1,0
|
||||
access_project_task_portal_coach,Portal Coach Access for Project Tasks,project.model_project_task,bemade_sports_clinic.group_portal_team_coach,1,1,1,0
|
||||
access_task_to_event_wizard_user,User Access for Task to Event Wizard,model_task_to_event_wizard,base.group_user,1,1,1,1
|
||||
access_task_to_event_wizard_portal_tp,Portal TP Access for Task to Event Wizard,model_task_to_event_wizard,bemade_sports_clinic.group_portal_treatment_professional,1,1,1,1
|
||||
access_task_to_event_wizard_portal_coach,Portal Coach Access for Task to Event Wizard,model_task_to_event_wizard,bemade_sports_clinic.group_portal_team_coach,1,1,1,1
|
||||
access_project_project_portal_tp,Portal TP Access for Projects,project.model_project_project,bemade_sports_clinic.group_portal_treatment_professional,1,1,0,0
|
||||
access_project_task_type_portal_tp,Portal TP Access for Task Types,project.model_project_task_type,bemade_sports_clinic.group_portal_treatment_professional,1,0,0,0
|
||||
access_project_tags_portal_tp,Portal TP Access for Project Tags,project.model_project_tags,bemade_sports_clinic.group_portal_treatment_professional,1,0,0,0
|
||||
access_project_milestone_portal_tp,Portal TP Access for Project Milestones,project.model_project_milestone,bemade_sports_clinic.group_portal_treatment_professional,1,0,0,0
|
||||
access_snailmail_letter_portal_tp,Portal TP Access for Snailmail Letters,snailmail.model_snailmail_letter,bemade_sports_clinic.group_portal_treatment_professional,1,0,0,0
|
||||
access_snailmail_letter_portal_coach,Portal Coach Access for Snailmail Letters,snailmail.model_snailmail_letter,bemade_sports_clinic.group_portal_team_coach,1,0,0,0
|
||||
|
|
|
|||
|
84
bemade_sports_clinic/security/project_task_portal_rules.xml
Normal file
84
bemade_sports_clinic/security/project_task_portal_rules.xml
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- Project Task Access Rules for Portal Treatment Professionals -->
|
||||
<record model="ir.rule" id="project_task_portal_tp_rule">
|
||||
<field name="name">Project Task: Portal Treatment Professional Access</field>
|
||||
<field name="model_id" ref="project.model_project_task"/>
|
||||
<field name="domain_force">[
|
||||
'&',
|
||||
# SECURITY: Only tasks from projects with portal visibility
|
||||
('project_id.privacy_visibility', '=', 'portal'),
|
||||
'|', '|', '|',
|
||||
# Tasks assigned to the user
|
||||
('user_ids', 'in', [user.id]),
|
||||
# Tasks where user is a follower
|
||||
('message_partner_ids', 'in', [user.partner_id.id]),
|
||||
# Tasks from projects where user is a follower
|
||||
('project_id.message_partner_ids', 'in', [user.partner_id.id]),
|
||||
# Tasks from projects where user's teams are partners
|
||||
('project_id.partner_id', 'in', user.partner_id.team_staff_rel_ids.mapped('team_id.id') or [0])
|
||||
]</field>
|
||||
<field name="groups" eval="[(4, ref('bemade_sports_clinic.group_portal_treatment_professional'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Project Access Rules for Portal Treatment Professionals -->
|
||||
<record model="ir.rule" id="project_project_portal_tp_rule">
|
||||
<field name="name">Project: Portal Treatment Professional Access</field>
|
||||
<field name="model_id" ref="project.model_project_project"/>
|
||||
<field name="domain_force">[
|
||||
'&',
|
||||
# SECURITY: Only projects with portal visibility
|
||||
('privacy_visibility', '=', 'portal'),
|
||||
'|',
|
||||
# Projects where user is explicitly a follower
|
||||
('message_partner_ids', 'in', [user.partner_id.id]),
|
||||
# Projects where user's teams are partners
|
||||
('partner_id', 'in', user.partner_id.team_staff_rel_ids.mapped('team_id.id') or [0])
|
||||
]</field>
|
||||
<field name="groups" eval="[(4, ref('bemade_sports_clinic.group_portal_treatment_professional'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Project Task Stage Access for Portal Treatment Professionals -->
|
||||
<record model="ir.rule" id="project_task_stage_portal_tp_rule">
|
||||
<field name="name">Project Task Stage: Portal Treatment Professional Access</field>
|
||||
<field name="model_id" ref="project.model_project_task_type"/>
|
||||
<field name="domain_force">[
|
||||
'|', '|',
|
||||
# Stages from projects where user is a follower
|
||||
('project_ids.message_partner_ids', 'in', [user.partner_id.id]),
|
||||
# Stages from projects where user's teams are partners
|
||||
('project_ids.partner_id', 'in', user.partner_id.team_staff_rel_ids.mapped('team_id.id') or [0]),
|
||||
# Global stages (no project restriction)
|
||||
('project_ids', '=', False)
|
||||
]</field>
|
||||
<field name="groups" eval="[(4, ref('bemade_sports_clinic.group_portal_treatment_professional'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Project Tags Access for Portal Treatment Professionals -->
|
||||
<record model="ir.rule" id="project_tags_portal_tp_rule">
|
||||
<field name="name">Project Tags: Portal Treatment Professional Access</field>
|
||||
<field name="model_id" ref="project.model_project_tags"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('bemade_sports_clinic.group_portal_treatment_professional'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -61,6 +61,18 @@
|
|||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
</record>
|
||||
|
||||
<!-- Portal treatment professionals can access all teams -->
|
||||
<record id="portal_treatment_professional_team_access" model="ir.rule">
|
||||
<field name="name">Portal Treatment Professional Access to Teams</field>
|
||||
<field name="model_id" ref="model_sports_team"/>
|
||||
<field name="groups" eval="[(6, 0, [ref('bemade_sports_clinic.group_portal_treatment_professional')])]"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
</record>
|
||||
|
||||
<!-- Portal users can access injury documents for their team's players -->
|
||||
<record id="portal_injury_document_access" model="ir.rule">
|
||||
<field name="name">Portal Access to Injury Documents</field>
|
||||
|
|
|
|||
46
bemade_sports_clinic/security/sports_event_rules.xml
Normal file
46
bemade_sports_clinic/security/sports_event_rules.xml
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<!-- <data noupdate="1"> -->
|
||||
|
||||
<!-- Sports Event Access Rules -->
|
||||
|
||||
<!-- Internal Users: Full access to all events -->
|
||||
<record id="sports_event_rule_user" model="ir.rule">
|
||||
<field name="name">Sports Event: Internal Users</field>
|
||||
<field name="model_id" ref="model_sports_event"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- Portal Treatment Professionals: Full access to all events -->
|
||||
<record id="sports_event_rule_portal_treatment_professional" model="ir.rule">
|
||||
<field name="name">Sports Event: Portal Treatment Professionals</field>
|
||||
<field name="model_id" ref="model_sports_event"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('bemade_sports_clinic.group_portal_treatment_professional'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Portal Team Coaches: Team-based read-only access -->
|
||||
<record id="sports_event_rule_portal_team_coach" model="ir.rule">
|
||||
<field name="name">Sports Event: Portal Team Coaches (Read-Only)</field>
|
||||
<field name="model_id" ref="model_sports_event"/>
|
||||
<field name="domain_force">[
|
||||
('team_id', 'in', user.partner_id.team_staff_rel_ids.mapped('team_id.id') or [0])
|
||||
]</field>
|
||||
<field name="groups" eval="[(4, ref('bemade_sports_clinic.group_portal_team_coach'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- </data> -->
|
||||
</odoo>
|
||||
|
|
@ -6,3 +6,4 @@ from . import test_treatment_professional_consistency
|
|||
from . import test_player_removal
|
||||
from . import test_mail_activity_portal_access
|
||||
from . import test_mail_activity_portal_integration
|
||||
from . import test_project_task_portal_security
|
||||
|
|
|
|||
180
bemade_sports_clinic/tests/test_project_task_portal_security.py
Normal file
180
bemade_sports_clinic/tests/test_project_task_portal_security.py
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
class TestProjectTaskPortalSecurity(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
# Create test users
|
||||
self.portal_tp_user = self.env['res.users'].create({
|
||||
'name': 'Portal Treatment Professional',
|
||||
'login': 'portal_tp_test',
|
||||
'email': 'portal_tp@test.com',
|
||||
'groups_id': [(4, self.env.ref('bemade_sports_clinic.group_portal_treatment_professional').id)]
|
||||
})
|
||||
|
||||
self.regular_portal_user = self.env['res.users'].create({
|
||||
'name': 'Regular Portal User',
|
||||
'login': 'portal_regular_test',
|
||||
'email': 'portal_regular@test.com',
|
||||
'groups_id': [(4, self.env.ref('base.group_portal').id)]
|
||||
})
|
||||
|
||||
# Create test project with portal access
|
||||
self.test_project = self.env['project.project'].create({
|
||||
'name': 'Test Sports Project',
|
||||
'privacy_visibility': 'portal',
|
||||
'is_sports_clinic_project': True,
|
||||
})
|
||||
|
||||
# Add portal TP as follower
|
||||
self.test_project.message_subscribe(partner_ids=[self.portal_tp_user.partner_id.id])
|
||||
|
||||
# Create test task
|
||||
self.test_task = self.env['project.task'].create({
|
||||
'name': 'Test Event Task',
|
||||
'project_id': self.test_project.id,
|
||||
'user_ids': [(4, self.portal_tp_user.id)],
|
||||
'date_event_start': datetime.now() + timedelta(days=1),
|
||||
'date_event_end': datetime.now() + timedelta(days=1, hours=2),
|
||||
})
|
||||
|
||||
def test_01_portal_tp_can_read_task_fields(self):
|
||||
"""Test that portal treatment professionals can read all required task fields"""
|
||||
task_as_portal_tp = self.test_task.sudo(self.portal_tp_user)
|
||||
|
||||
# Test core fields
|
||||
self.assertTrue(task_as_portal_tp.name)
|
||||
self.assertTrue(task_as_portal_tp.project_id)
|
||||
self.assertTrue(task_as_portal_tp.user_ids)
|
||||
|
||||
# Test custom event fields
|
||||
self.assertTrue(task_as_portal_tp.date_event_start)
|
||||
self.assertTrue(task_as_portal_tp.date_event_end)
|
||||
|
||||
# Test overridden fields
|
||||
self.assertIsNotNone(task_as_portal_tp.date_start)
|
||||
self.assertIsNotNone(task_as_portal_tp.date_end)
|
||||
|
||||
def test_02_portal_tp_can_write_task_fields(self):
|
||||
"""Test that portal treatment professionals can write to task fields"""
|
||||
task_as_portal_tp = self.test_task.sudo(self.portal_tp_user)
|
||||
|
||||
# Test writing to various fields
|
||||
task_as_portal_tp.write({
|
||||
'name': 'Updated Event Name',
|
||||
'description': 'Updated description',
|
||||
'priority': '1',
|
||||
'date_event_start': datetime.now() + timedelta(days=2),
|
||||
'date_event_end': datetime.now() + timedelta(days=2, hours=3),
|
||||
})
|
||||
|
||||
self.assertEqual(task_as_portal_tp.name, 'Updated Event Name')
|
||||
self.assertEqual(task_as_portal_tp.priority, '1')
|
||||
|
||||
def test_03_portal_tp_can_create_tasks(self):
|
||||
"""Test that portal treatment professionals can create tasks"""
|
||||
task_as_portal_tp = self.env['project.task'].sudo(self.portal_tp_user)
|
||||
|
||||
new_task = task_as_portal_tp.create({
|
||||
'name': 'New Portal Created Task',
|
||||
'project_id': self.test_project.id,
|
||||
'date_event_start': datetime.now() + timedelta(days=3),
|
||||
'date_event_end': datetime.now() + timedelta(days=3, hours=1),
|
||||
})
|
||||
|
||||
self.assertTrue(new_task.exists())
|
||||
self.assertEqual(new_task.name, 'New Portal Created Task')
|
||||
|
||||
def test_04_regular_portal_user_cannot_access_tasks(self):
|
||||
"""Test that regular portal users cannot access project tasks"""
|
||||
task_as_regular_portal = self.test_task.sudo(self.regular_portal_user)
|
||||
|
||||
# Should not be able to read task fields
|
||||
with self.assertRaises(AccessError):
|
||||
_ = task_as_regular_portal.name
|
||||
|
||||
def test_05_portal_task_access_method(self):
|
||||
"""Test the custom portal task access checking method"""
|
||||
# Test with portal TP user (should have access)
|
||||
task_as_portal_tp = self.test_task.sudo(self.portal_tp_user)
|
||||
self.assertTrue(task_as_portal_tp.check_portal_task_access())
|
||||
|
||||
# Test with regular portal user (should not have access)
|
||||
task_as_regular_portal = self.test_task.sudo(self.regular_portal_user)
|
||||
self.assertFalse(task_as_regular_portal.check_portal_task_access())
|
||||
|
||||
def test_06_project_access_for_portal_tp(self):
|
||||
"""Test that portal treatment professionals can access projects"""
|
||||
project_as_portal_tp = self.test_project.sudo(self.portal_tp_user)
|
||||
|
||||
# Should be able to read project fields
|
||||
self.assertTrue(project_as_portal_tp.name)
|
||||
self.assertTrue(project_as_portal_tp.privacy_visibility)
|
||||
self.assertTrue(project_as_portal_tp.is_sports_clinic_project)
|
||||
|
||||
def test_07_record_rule_filtering(self):
|
||||
"""Test that record rules properly filter accessible tasks"""
|
||||
# Create a task that portal TP should NOT have access to
|
||||
other_project = self.env['project.project'].create({
|
||||
'name': 'Private Project',
|
||||
'privacy_visibility': 'employees', # Not portal accessible
|
||||
})
|
||||
|
||||
private_task = self.env['project.task'].create({
|
||||
'name': 'Private Task',
|
||||
'project_id': other_project.id,
|
||||
})
|
||||
|
||||
# Portal TP should not be able to access this task
|
||||
task_as_portal_tp = private_task.sudo(self.portal_tp_user)
|
||||
self.assertFalse(task_as_portal_tp.check_portal_task_access())
|
||||
|
||||
def test_08_field_groups_configuration(self):
|
||||
"""Test that field groups are properly configured"""
|
||||
task_model = self.env['project.task']
|
||||
|
||||
# Check that critical fields have portal groups
|
||||
critical_fields = ['name', 'description', 'user_ids', 'project_id', 'date_event_start', 'date_event_end']
|
||||
|
||||
for field_name in critical_fields:
|
||||
field_obj = task_model._fields.get(field_name)
|
||||
self.assertIsNotNone(field_obj, f"Field {field_name} not found")
|
||||
|
||||
if hasattr(field_obj, 'groups') and field_obj.groups:
|
||||
self.assertIn('bemade_sports_clinic.group_portal_treatment_professional',
|
||||
field_obj.groups,
|
||||
f"Field {field_name} missing portal TP group access")
|
||||
|
||||
def test_09_project_creation_helper(self):
|
||||
"""Test the project creation helper method"""
|
||||
# Test creating a sports clinic project
|
||||
new_project = self.env['project.project'].create_sports_clinic_project(
|
||||
name='Test Helper Project',
|
||||
description='Created via helper method'
|
||||
)
|
||||
|
||||
self.assertTrue(new_project.exists())
|
||||
self.assertTrue(new_project.is_sports_clinic_project)
|
||||
self.assertEqual(new_project.privacy_visibility, 'portal')
|
||||
|
||||
# Check that treatment professionals are followers
|
||||
portal_group = self.env.ref('bemade_sports_clinic.group_portal_treatment_professional')
|
||||
tp_partners = portal_group.users.mapped('partner_id')
|
||||
|
||||
for partner in tp_partners:
|
||||
self.assertIn(partner, new_project.message_partner_ids)
|
||||
|
||||
def test_10_default_project_creation(self):
|
||||
"""Test the default project creation method"""
|
||||
default_project = self.env['project.project'].get_or_create_default_sports_project()
|
||||
|
||||
self.assertTrue(default_project.exists())
|
||||
self.assertEqual(default_project.name, 'Sports Clinic Events')
|
||||
self.assertTrue(default_project.is_sports_clinic_project)
|
||||
self.assertEqual(default_project.privacy_visibility, 'portal')
|
||||
287
bemade_sports_clinic/views/events_portal_templates.xml
Normal file
287
bemade_sports_clinic/views/events_portal_templates.xml
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Events List Template -->
|
||||
<template id="portal_events_list" name="Portal Events List">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-set="breadcrumbs_searchbar" t-value="True"/>
|
||||
<t t-call="portal.portal_searchbar">
|
||||
<t t-set="title">Events</t>
|
||||
</t>
|
||||
|
||||
<div class="container-fluid">
|
||||
<!-- Breadcrumbs -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="/my">Home</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Events</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<!-- View Type Navigation -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<ul class="nav nav-pills">
|
||||
<li class="nav-item">
|
||||
<a t-attf-class="nav-link #{'active' if view_type == 'all' else ''}"
|
||||
t-attf-href="/my/events?view_type=all#{project_id and '&project_id=%s' % project_id or ''}#{assigned_user_id and '&assigned_user_id=%s' % assigned_user_id or ''}#{date_from and '&date_from=%s' % date_from or ''}#{date_to and '&date_to=%s' % date_to or ''}">
|
||||
All Events
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a t-attf-class="nav-link #{'active' if view_type == 'my' else ''}"
|
||||
t-attf-href="/my/events?view_type=my#{project_id and '&project_id=%s' % project_id or ''}#{assigned_user_id and '&assigned_user_id=%s' % assigned_user_id or ''}#{date_from and '&date_from=%s' % date_from or ''}#{date_to and '&date_to=%s' % date_to or ''}">
|
||||
My Events
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a t-attf-class="nav-link #{'active' if view_type == 'unassigned' else ''}"
|
||||
t-attf-href="/my/events?view_type=unassigned#{project_id and '&project_id=%s' % project_id or ''}#{assigned_user_id and '&assigned_user_id=%s' % assigned_user_id or ''}#{date_from and '&date_from=%s' % date_from or ''}#{date_to and '&date_to=%s' % date_to or ''}">
|
||||
Unassigned Events
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
<a href="/my/events?view_type=all" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fa fa-times"/> Clear Filters
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<form method="get" class="o_portal_search_panel" action="/my/events">
|
||||
<input type="hidden" name="view_type" t-att-value="view_type"/>
|
||||
|
||||
<!-- First Row: Main Filters -->
|
||||
<div class="row g-2 mb-2">
|
||||
<!-- Organization Filter -->
|
||||
<div class="col-md-4">
|
||||
<select name="organization_id" class="form-select" onchange="this.form.submit();">
|
||||
<option value="">All Organizations</option>
|
||||
<t t-foreach="organizations" t-as="organization">
|
||||
<option t-att-value="organization.id" t-att-selected="'selected' if organization_id and organization_id == organization.id else None">
|
||||
<t t-esc="organization.name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Team Filter -->
|
||||
<div class="col-md-4">
|
||||
<select name="team_id" class="form-select" onchange="this.form.submit();">
|
||||
<option value="">All Teams</option>
|
||||
<t t-foreach="teams" t-as="team">
|
||||
<option t-att-value="team.id" t-att-selected="team_id == team.id">
|
||||
<t t-esc="team.name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Assigned Professional Filter -->
|
||||
<div class="col-md-4">
|
||||
<select name="assigned_user_id" class="form-select" onchange="this.form.submit();">
|
||||
<option value="">All Assigned Professionals</option>
|
||||
<t t-foreach="treatment_professionals" t-as="user">
|
||||
<option t-att-value="user.id" t-att-selected="assigned_user_id == user.id">
|
||||
<t t-esc="user.name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Second Row: Date Filters -->
|
||||
<div class="row g-2">
|
||||
<!-- Date From Filter -->
|
||||
<div class="col-md-6">
|
||||
<label for="date_from" class="form-label text-muted small">From Date</label>
|
||||
<input type="date" id="date_from" name="date_from" class="form-control"
|
||||
t-att-value="date_from" onchange="this.form.submit();"/>
|
||||
</div>
|
||||
|
||||
<!-- Date To Filter -->
|
||||
<div class="col-md-6">
|
||||
<label for="date_to" class="form-label text-muted small">To Date</label>
|
||||
<input type="date" id="date_to" name="date_to" class="form-control"
|
||||
t-att-value="date_to" onchange="this.form.submit();"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Record Count Display -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span class="text-muted">
|
||||
<t t-if="events">
|
||||
Showing <strong t-esc="len(events)"/> events
|
||||
<t t-if="pager and pager.get('page_count', 1) > 1">
|
||||
(Page <t t-esc="pager.get('page', {}).get('num', 1)"/> of <t t-esc="pager.get('page_count', 1)"/>)
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="not events">
|
||||
<strong>0</strong> events found
|
||||
</t>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<t t-if="view_type == 'all'">
|
||||
<span class="badge bg-secondary">All Events</span>
|
||||
</t>
|
||||
<t t-if="view_type == 'my'">
|
||||
<span class="badge bg-primary">My Events</span>
|
||||
</t>
|
||||
<t t-if="view_type == 'unassigned'">
|
||||
<span class="badge bg-warning">Unassigned Events</span>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Events Table -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<t t-if="not events">
|
||||
<div class="alert alert-info text-center">
|
||||
<h4>No events found</h4>
|
||||
<p>There are no events matching your criteria.</p>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-if="events">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>
|
||||
<a t-attf-href="/my/events?#{request.httprequest.query_string.decode()}&sortby=name">
|
||||
Event Name
|
||||
<i t-if="sortby == 'name'" class="fa fa-sort-alpha-asc"/>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a t-attf-href="/my/events?#{request.httprequest.query_string.decode()}&sortby=date">
|
||||
Date
|
||||
<i t-if="sortby == 'date'" class="fa fa-sort-numeric-asc"/>
|
||||
<i t-if="sortby == 'date_desc'" class="fa fa-sort-numeric-desc"/>
|
||||
</a>
|
||||
</th>
|
||||
<th>Type</th>
|
||||
<th>
|
||||
<a t-attf-href="/my/events?#{request.httprequest.query_string.decode()}&sortby=team">
|
||||
Team
|
||||
<i t-if="sortby == 'team'" class="fa fa-sort-alpha-asc"/>
|
||||
</a>
|
||||
</th>
|
||||
<th>Venue</th>
|
||||
<th>Assigned Staff</th>
|
||||
<th t-if="can_edit">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="events" t-as="event">
|
||||
<tr class="clickable-row" style="cursor: pointer;"
|
||||
t-att-onclick="'window.location.href=\'%s\'' % ('/my/event/%s' % event.id)">
|
||||
<!-- Name -->
|
||||
<td>
|
||||
<strong t-esc="event.name"/>
|
||||
<t t-if="event.description">
|
||||
<br/><small class="text-muted" t-esc="event.description[:100] + '...' if len(event.description) > 100 else event.description"/>
|
||||
</t>
|
||||
</td>
|
||||
|
||||
<!-- Date -->
|
||||
<td>
|
||||
<t t-if="event.therapist_start">
|
||||
<span t-esc="event.therapist_start" t-options="{'widget': 'date', 'format': 'yyyy-MM-dd'}"/>
|
||||
<br/><span t-esc="event.therapist_start" t-options="{'widget': 'datetime', 'format': 'HH:mm'}"/>
|
||||
<t t-if="event.date_start">
|
||||
<br/><small class="text-muted">(<span t-esc="event.date_start" t-options="{'widget': 'datetime', 'format': 'HH:mm'}"/>)</small>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="not event.therapist_start and event.date_start">
|
||||
<span t-esc="event.date_start" t-options="{'widget': 'date', 'format': 'yyyy-MM-dd'}"/>
|
||||
<br/><span t-esc="event.date_start" t-options="{'widget': 'datetime', 'format': 'HH:mm'}"/>
|
||||
</t>
|
||||
<t t-if="not event.therapist_start and not event.date_start">
|
||||
<span class="text-muted">No date</span>
|
||||
</t>
|
||||
</td>
|
||||
|
||||
<!-- Type -->
|
||||
<td>
|
||||
<span class="badge bg-info">
|
||||
<t t-esc="dict(event._fields['event_type'].selection).get(event.event_type, event.event_type)"/>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Team -->
|
||||
<td>
|
||||
<t t-if="event.team_id">
|
||||
<i class="fa fa-users"/> <span t-esc="event.team_id.name"/>
|
||||
</t>
|
||||
<t t-if="not event.team_id">
|
||||
<span class="text-muted">No Team</span>
|
||||
</t>
|
||||
</td>
|
||||
|
||||
<!-- Venue -->
|
||||
<td>
|
||||
<t t-if="event.venue_id">
|
||||
<i class="fa fa-map-marker"/> <span t-esc="event.venue_id.name"/>
|
||||
</t>
|
||||
<t t-if="not event.venue_id">
|
||||
<span class="text-muted">TBD</span>
|
||||
</t>
|
||||
</td>
|
||||
|
||||
<!-- Assigned Staff -->
|
||||
<td>
|
||||
<t t-if="event.assigned_staff_ids">
|
||||
<t t-foreach="event.assigned_staff_ids" t-as="staff">
|
||||
<span class="badge bg-primary me-1" t-esc="staff.name"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="not event.assigned_staff_ids">
|
||||
<span class="text-muted">Unassigned</span>
|
||||
</t>
|
||||
</td>
|
||||
|
||||
<!-- Actions -->
|
||||
<td t-if="can_edit">
|
||||
<a t-attf-href="/my/event/#{event.id}" class="btn btn-sm btn-outline-primary me-1">
|
||||
<i class="fa fa-eye"/> View
|
||||
</a>
|
||||
<a t-attf-href="/my/event/#{event.id}/edit" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fa fa-edit"/> Edit
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<t t-call="portal.pager"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
<h4 class="mb-0">Injury Details</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Basic injury information -->
|
||||
<!-- Line 1: injury date, N/A checkbox, predicted return date -->
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="form-group">
|
||||
|
|
@ -73,10 +73,17 @@
|
|||
<small class="text-muted">Check N/A if exact date is unknown</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-if="is_treatment_prof" class="col-lg-6">
|
||||
<div class="form-group">
|
||||
<label for="predicted_resolution_date">Predicted Resolution Date</label>
|
||||
<input type="date" name="predicted_resolution_date" id="predicted_resolution_date" class="form-control"
|
||||
t-att-value="injury.predicted_resolution_date"/>
|
||||
<small class="text-muted">Expected recovery timeline</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Diagnosis -->
|
||||
<!-- Line 2: diagnosis -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-lg-12">
|
||||
<div class="form-group">
|
||||
|
|
@ -87,29 +94,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Treatment dates and status -->
|
||||
<div t-if="is_treatment_prof" class="row mt-3">
|
||||
<div class="col-lg-6">
|
||||
<div class="form-group">
|
||||
<label for="predicted_resolution_date">Predicted Resolution Date</label>
|
||||
<input type="date" name="predicted_resolution_date" id="predicted_resolution_date" class="form-control"
|
||||
t-att-value="injury.predicted_resolution_date"/>
|
||||
<small class="text-muted">Expected recovery timeline</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="form-group">
|
||||
<label for="resolution_date">Resolution Date</label>
|
||||
<input type="date" name="resolution_date" id="resolution_date" class="form-control"
|
||||
t-att-value="injury.resolution_date"/>
|
||||
<small class="text-muted">Actual recovery date (when resolved)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status and treatment professionals -->
|
||||
<!-- Line 3: Status, resolution date (only for treatment professionals) -->
|
||||
<div t-if="is_treatment_prof" class="row mt-3">
|
||||
<div class="col-lg-6">
|
||||
<div class="form-group">
|
||||
|
|
@ -125,22 +110,38 @@
|
|||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="form-group">
|
||||
<label for="treatment_professional_ids">Treatment Professionals</label>
|
||||
<select name="treatment_professional_ids" id="treatment_professional_ids" class="form-control" multiple="multiple">
|
||||
<t t-foreach="treatment_professionals" t-as="tp">
|
||||
<option t-att-value="tp.id" t-att-selected="tp.id in injury.treatment_professional_ids.ids">
|
||||
<t t-esc="tp.name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
<small class="text-muted">Hold Ctrl/Cmd to select multiple professionals</small>
|
||||
<label for="resolution_date">Resolution Date</label>
|
||||
<input type="date" name="resolution_date" id="resolution_date" class="form-control"
|
||||
t-att-value="injury.resolution_date"/>
|
||||
<small class="text-muted">Actual recovery date (when resolved)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parental consent field (only visible to treatment professionals) -->
|
||||
<div t-if="is_treatment_prof and parental_consent_options" class="row mt-3">
|
||||
<!-- Line 4: Treatment professionals, parental consent -->
|
||||
<div t-if="is_treatment_prof" class="row mt-3">
|
||||
<div class="col-lg-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Treatment Professionals</label>
|
||||
<div class="border rounded p-3" style="max-height: 200px; overflow-y: auto;">
|
||||
<t t-foreach="treatment_professionals" t-as="tp">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
t-att-id="'tp_%s' % tp.id"
|
||||
name="treatment_professional_ids[]"
|
||||
t-att-value="tp.id"
|
||||
t-att-checked="tp.id in injury.treatment_professional_ids.ids"/>
|
||||
<label class="form-check-label" t-att-for="'tp_%s' % tp.id">
|
||||
<span t-esc="tp.name"/>
|
||||
<small class="text-muted d-block" t-esc="tp.email"/>
|
||||
</label>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<small class="text-muted">Select multiple professionals</small>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="parental_consent_options" class="col-lg-6">
|
||||
<div class="form-group">
|
||||
<label for="parental_consent">Parental Consent</label>
|
||||
<select name="parental_consent" id="parental_consent" class="form-control">
|
||||
|
|
@ -154,7 +155,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Internal notes (only visible to treatment professionals) -->
|
||||
<!-- Line 5: Internal notes -->
|
||||
<div t-if="is_treatment_prof" class="row mt-3">
|
||||
<div class="col-lg-12">
|
||||
<div class="form-group">
|
||||
|
|
@ -222,6 +223,12 @@
|
|||
<a t-att-href="'/my/injury/documents?injury_id=%s' % injury.id" class="btn bg-o-color-2 text-white">
|
||||
<i class="fa fa-file-medical"></i> Documents
|
||||
</a>
|
||||
<!-- Delete injury button (only for treatment professionals) -->
|
||||
<button t-if="is_treatment_prof" type="button" class="btn btn-danger"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteInjuryModal"
|
||||
t-att-onclick="'setDeleteInjuryId(%s)' % injury.id">
|
||||
<i class="fa fa-trash"></i> Delete Injury
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -230,6 +237,50 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Injury Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteInjuryModal" tabindex="-1" aria-labelledby="deleteInjuryModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteInjuryModalLabel">Delete Injury</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
<strong>Warning:</strong> This action cannot be undone.
|
||||
</div>
|
||||
<p>Are you sure you want to permanently delete this injury record?</p>
|
||||
<p><strong>This will also delete:</strong></p>
|
||||
<ul>
|
||||
<li>All treatment notes associated with this injury</li>
|
||||
<li>All documents and attachments</li>
|
||||
<li>All activity history</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn bg-o-color-2 text-white" data-bs-dismiss="modal">Cancel</button>
|
||||
<form method="post" action="/my/injury/delete" style="display: inline;">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<input type="hidden" name="injury_id" id="deleteInjuryId"/>
|
||||
<input type="hidden" name="return_url" t-att-value="return_url"/>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="fa fa-trash"></i> Yes, Delete Injury
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript for modal functionality -->
|
||||
<script>
|
||||
function setDeleteInjuryId(injuryId) {
|
||||
document.getElementById('deleteInjuryId').value = injuryId;
|
||||
console.log('Delete modal opened for injury:', injuryId);
|
||||
}
|
||||
</script>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
222
bemade_sports_clinic/views/portal_event_detail_template.xml
Normal file
222
bemade_sports_clinic/views/portal_event_detail_template.xml
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Event Detail Template -->
|
||||
<template id="portal_event_detail" name="Portal Event Detail">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-set="breadcrumbs_searchbar" t-value="True"/>
|
||||
|
||||
<div class="container-fluid">
|
||||
<!-- Breadcrumbs -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="/my">Home</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="/my/events">Events</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">
|
||||
<t t-esc="event.name"/>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h2 t-esc="event.name"/>
|
||||
<p class="text-muted mb-0">
|
||||
<i class="fa fa-users"/> <span t-esc="event.team_id.name"/>
|
||||
<t t-if="event.venue_id">
|
||||
| <i class="fa fa-map-marker"/> <span t-esc="event.venue_id.name"/>
|
||||
</t>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/my/events" class="btn btn-outline-secondary me-2">
|
||||
<i class="fa fa-arrow-left"/> Back to Events
|
||||
</a>
|
||||
<t t-if="can_edit">
|
||||
<a t-attf-href="/my/event/#{event.id}/edit" class="btn btn-primary">
|
||||
<i class="fa fa-edit"/> Edit Event
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success/Error Messages -->
|
||||
<t t-if="request.params.get('success')">
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="fa fa-check-circle"/> Event updated successfully!
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Event Details -->
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fa fa-info-circle"/> Event Details
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4">Type:</dt>
|
||||
<dd class="col-sm-8">
|
||||
<span class="badge bg-info">
|
||||
<t t-esc="dict(event._fields['event_type'].selection).get(event.event_type, event.event_type)"/>
|
||||
</span>
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-4">Status:</dt>
|
||||
<dd class="col-sm-8">
|
||||
<span t-attf-class="badge #{event.state == 'completed' and 'bg-success' or event.state == 'cancelled' and 'bg-danger' or event.state == 'in_progress' and 'bg-warning' or 'bg-secondary'}">
|
||||
<t t-esc="dict(event._fields['state'].selection).get(event.state, event.state)"/>
|
||||
</span>
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-4">Team:</dt>
|
||||
<dd class="col-sm-8">
|
||||
<i class="fa fa-users"/> <span t-esc="event.team_id.name"/>
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-4">Venue:</dt>
|
||||
<dd class="col-sm-8">
|
||||
<t t-if="event.venue_id">
|
||||
<i class="fa fa-map-marker"/> <span t-esc="event.venue_id.name"/>
|
||||
</t>
|
||||
<t t-if="not event.venue_id">
|
||||
<span class="text-muted">To be determined</span>
|
||||
</t>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-5">Event Start:</dt>
|
||||
<dd class="col-sm-7">
|
||||
<t t-if="event.date_start">
|
||||
<i class="fa fa-calendar"/> <span t-esc="event.date_start" t-options="{'widget': 'datetime'}"/>
|
||||
</t>
|
||||
<t t-if="not event.date_start">
|
||||
<span class="text-muted">Not scheduled</span>
|
||||
</t>
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-5">Event End:</dt>
|
||||
<dd class="col-sm-7">
|
||||
<t t-if="event.date_end">
|
||||
<i class="fa fa-calendar"/> <span t-esc="event.date_end" t-options="{'widget': 'datetime'}"/>
|
||||
</t>
|
||||
<t t-if="not event.date_end">
|
||||
<span class="text-muted">Not scheduled</span>
|
||||
</t>
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-5">Therapist Start:</dt>
|
||||
<dd class="col-sm-7">
|
||||
<t t-if="event.therapist_start">
|
||||
<i class="fa fa-clock-o"/> <span t-esc="event.therapist_start" t-options="{'widget': 'datetime'}"/>
|
||||
</t>
|
||||
<t t-if="not event.therapist_start">
|
||||
<span class="text-muted">Same as event</span>
|
||||
</t>
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-5">Therapist End:</dt>
|
||||
<dd class="col-sm-7">
|
||||
<t t-if="event.therapist_end">
|
||||
<i class="fa fa-clock-o"/> <span t-esc="event.therapist_end" t-options="{'widget': 'datetime'}"/>
|
||||
</t>
|
||||
<t t-if="not event.therapist_end">
|
||||
<span class="text-muted">Same as event</span>
|
||||
</t>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<t t-if="event.description">
|
||||
<hr/>
|
||||
<h6>Description:</h6>
|
||||
<div t-field="event.description" class="oe_no_empty"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<!-- Assigned Staff -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fa fa-user-md"/> Assigned Staff
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<t t-if="event.assigned_staff_ids">
|
||||
<t t-foreach="event.assigned_staff_ids" t-as="staff">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<div class="avatar-sm bg-primary rounded-circle d-flex align-items-center justify-content-center me-2">
|
||||
<i class="fa fa-user text-white"/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-medium" t-esc="staff.name"/>
|
||||
<small class="text-muted" t-esc="staff.email"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="not event.assigned_staff_ids">
|
||||
<p class="text-muted mb-0">
|
||||
<i class="fa fa-info-circle"/> No staff assigned yet
|
||||
</p>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Organization -->
|
||||
<t t-if="event.partner_id">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fa fa-building"/> Organization
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="avatar-sm bg-secondary rounded-circle d-flex align-items-center justify-content-center me-2">
|
||||
<i class="fa fa-building text-white"/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-medium" t-esc="event.partner_id.name"/>
|
||||
<t t-if="event.partner_id.email">
|
||||
<small class="text-muted d-block" t-esc="event.partner_id.email"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
310
bemade_sports_clinic/views/portal_event_edit_template.xml
Normal file
310
bemade_sports_clinic/views/portal_event_edit_template.xml
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Event Edit Template -->
|
||||
<template id="portal_event_edit" name="Portal Event Edit">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-set="breadcrumbs_searchbar" t-value="True"/>
|
||||
|
||||
<div class="container-fluid">
|
||||
<!-- Breadcrumbs -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="/my">Home</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="/my/events">Events</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a t-attf-href="/my/event/#{event.id}">
|
||||
<t t-esc="event.name"/>
|
||||
</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Edit</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h2>Edit Event</h2>
|
||||
<p class="text-muted mb-0">
|
||||
<i class="fa fa-edit"/> Editing: <span t-esc="event.name"/>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<a t-attf-href="/my/event/#{event.id}" class="btn btn-outline-secondary">
|
||||
<i class="fa fa-arrow-left"/> Cancel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Messages -->
|
||||
<t t-if="request.params.get('error')">
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="fa fa-exclamation-circle"/> Error: <span t-esc="request.params.get('error')"/>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Edit Form -->
|
||||
<form t-attf-action="/my/event/#{event.id}/save" method="post" class="needs-validation" novalidate="true">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fa fa-info-circle"/> Event Information
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<!-- Event Name -->
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Event Name *</label>
|
||||
<input type="text" class="form-control" id="name" name="name"
|
||||
t-att-value="event.name" required="true"/>
|
||||
<div class="invalid-feedback">
|
||||
Please provide a valid event name.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Type -->
|
||||
<div class="mb-3">
|
||||
<label for="event_type" class="form-label">Event Type</label>
|
||||
<select class="form-select" id="event_type" name="event_type">
|
||||
<option value="game" t-att-selected="event.event_type == 'game'">Game</option>
|
||||
<option value="practice" t-att-selected="event.event_type == 'practice'">Practice</option>
|
||||
<option value="training" t-att-selected="event.event_type == 'training'">Training</option>
|
||||
<option value="meeting" t-att-selected="event.event_type == 'meeting'">Team Meeting</option>
|
||||
<option value="other" t-att-selected="event.event_type == 'other'">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="mb-3">
|
||||
<label for="state" class="form-label">Status</label>
|
||||
<select class="form-select" id="state" name="state">
|
||||
<option value="draft" t-att-selected="event.state == 'draft'">Draft</option>
|
||||
<option value="confirmed" t-att-selected="event.state == 'confirmed'">Confirmed</option>
|
||||
<option value="in_progress" t-att-selected="event.state == 'in_progress'">In Progress</option>
|
||||
<option value="completed" t-att-selected="event.state == 'completed'">Completed</option>
|
||||
<option value="cancelled" t-att-selected="event.state == 'cancelled'">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<!-- Team -->
|
||||
<div class="mb-3">
|
||||
<label for="team_id" class="form-label">Team *</label>
|
||||
<select class="form-select" id="team_id" name="team_id" required="true">
|
||||
<option value="">Select a team...</option>
|
||||
<t t-foreach="teams" t-as="team">
|
||||
<option t-att-value="team.id" t-att-selected="event.team_id.id == team.id">
|
||||
<t t-esc="team.name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
<div class="invalid-feedback">
|
||||
Please select a team.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Venue -->
|
||||
<div class="mb-3">
|
||||
<label for="venue_id" class="form-label">Venue</label>
|
||||
<select class="form-select" id="venue_id" name="venue_id">
|
||||
<option value="">Select a venue...</option>
|
||||
<t t-foreach="venues" t-as="venue">
|
||||
<option t-att-value="venue.id" t-att-selected="event.venue_id.id == venue.id">
|
||||
<t t-esc="venue.name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="4"
|
||||
t-esc="event.description"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timing Information -->
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fa fa-clock-o"/> Timing Information
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<!-- Event Start -->
|
||||
<div class="mb-3">
|
||||
<label for="date_start" class="form-label">Event Start *</label>
|
||||
<input type="datetime-local" class="form-control" id="date_start" name="date_start"
|
||||
t-att-value="event.date_start and event.date_start.strftime('%Y-%m-%dT%H:%M')" required="true"/>
|
||||
<div class="invalid-feedback">
|
||||
Please provide an event start time.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Therapist Start -->
|
||||
<div class="mb-3">
|
||||
<label for="therapist_start" class="form-label">Therapist Start</label>
|
||||
<input type="datetime-local" class="form-control" id="therapist_start" name="therapist_start"
|
||||
t-att-value="event.therapist_start and event.therapist_start.strftime('%Y-%m-%dT%H:%M')"/>
|
||||
<small class="form-text text-muted">Leave empty to use event start time</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<!-- Event End -->
|
||||
<div class="mb-3">
|
||||
<label for="date_end" class="form-label">Event End *</label>
|
||||
<input type="datetime-local" class="form-control" id="date_end" name="date_end"
|
||||
t-att-value="event.date_end and event.date_end.strftime('%Y-%m-%dT%H:%M')" required="true"/>
|
||||
<div class="invalid-feedback">
|
||||
Please provide an event end time.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Therapist End -->
|
||||
<div class="mb-3">
|
||||
<label for="therapist_end" class="form-label">Therapist End</label>
|
||||
<input type="datetime-local" class="form-control" id="therapist_end" name="therapist_end"
|
||||
t-att-value="event.therapist_end and event.therapist_end.strftime('%Y-%m-%dT%H:%M')"/>
|
||||
<small class="form-text text-muted">Leave empty to use event end time</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<!-- Assigned Staff -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fa fa-user-md"/> Assigned Staff
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Treatment Professionals</label>
|
||||
<t t-foreach="treatment_professionals" t-as="professional">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
t-att-id="'staff_%s' % professional.id"
|
||||
name="assigned_staff_ids[]"
|
||||
t-att-value="professional.id"
|
||||
t-att-checked="professional in event.assigned_staff_ids"
|
||||
data-debug-name="assigned_staff_ids[]"
|
||||
t-att-data-debug-value="professional.id"/>
|
||||
<label class="form-check-label" t-att-for="'staff_%s' % professional.id">
|
||||
<span t-esc="professional.name"/>
|
||||
<small class="text-muted d-block" t-esc="professional.email"/>
|
||||
</label>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-end">
|
||||
<a t-attf-href="/my/event/#{event.id}" class="btn btn-outline-secondary me-2">
|
||||
<i class="fa fa-times"/> Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fa fa-save"/> Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Form Validation Script -->
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
window.addEventListener('load', function() {
|
||||
var forms = document.getElementsByClassName('needs-validation');
|
||||
var validation = Array.prototype.filter.call(forms, function(form) {
|
||||
form.addEventListener('submit', function(event) {
|
||||
if (form.checkValidity() === false) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
form.classList.add('was-validated');
|
||||
|
||||
// Debug: Check all checkboxes on the page
|
||||
var allStaffCheckboxes = form.querySelectorAll('input[name="assigned_staff_ids[]"]');
|
||||
console.log('Total staff checkboxes found:', allStaffCheckboxes.length);
|
||||
|
||||
// Debug: Check which ones are selected
|
||||
var checkedStaff = form.querySelectorAll('input[name="assigned_staff_ids[]"]:checked');
|
||||
console.log('Checked staff checkboxes:', checkedStaff.length);
|
||||
|
||||
// Log the values being submitted for debugging
|
||||
var staffValues = [];
|
||||
checkedStaff.forEach(function(checkbox) {
|
||||
staffValues.push(checkbox.value);
|
||||
console.log('Checkbox checked:', checkbox.id, 'value:', checkbox.value, 'name:', checkbox.name);
|
||||
});
|
||||
console.log('Staff values being submitted:', staffValues);
|
||||
|
||||
// WORKAROUND: Remove all existing staff checkboxes and add single hidden input
|
||||
// with comma-separated values
|
||||
console.log('Applying workaround for staff selection...');
|
||||
|
||||
// Remove the name attribute from all checkboxes to prevent them from being submitted
|
||||
allStaffCheckboxes.forEach(function(checkbox) {
|
||||
checkbox.removeAttribute('name');
|
||||
});
|
||||
|
||||
// Create a single hidden input with comma-separated staff IDs
|
||||
var hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = 'assigned_staff_ids';
|
||||
|
||||
if (checkedStaff.length > 0) {
|
||||
hiddenInput.value = staffValues.join(',');
|
||||
console.log('Added hidden input with comma-separated staff IDs:', hiddenInput.value);
|
||||
} else {
|
||||
hiddenInput.value = '';
|
||||
console.log('Added empty hidden input to clear staff selection');
|
||||
}
|
||||
|
||||
form.appendChild(hiddenInput);
|
||||
}, false);
|
||||
});
|
||||
}, false);
|
||||
})();
|
||||
</script>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<!-- Project Task Security Test Form View -->
|
||||
<record id="view_project_task_security_test_form" model="ir.ui.view">
|
||||
<field name="name">project.task.security.test.form</field>
|
||||
<field name="model">project.task.security.test</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Project Task Security Test">
|
||||
<header>
|
||||
<button name="action_test_field_access"
|
||||
string="Run Field Access Test"
|
||||
type="object"
|
||||
class="btn-primary"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="task_id"/>
|
||||
<field name="user_id"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Test Results">
|
||||
<field name="test_results" widget="text" nolabel="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Project Task Security Test Action -->
|
||||
<record id="action_project_task_security_test" model="ir.actions.act_window">
|
||||
<field name="name">Project Task Security Test</field>
|
||||
<field name="res_model">project.task.security.test</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu Item for Security Testing -->
|
||||
<menuitem id="menu_project_task_security_test"
|
||||
name="Task Security Test"
|
||||
parent="sports_clinic_root"
|
||||
action="action_project_task_security_test"
|
||||
sequence="90"
|
||||
groups="bemade_sports_clinic.group_sports_clinic_admin"/>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
7
bemade_sports_clinic/views/project_task_views.xml
Normal file
7
bemade_sports_clinic/views/project_task_views.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Project Task views cleaned up - event fields moved to sports.event model -->
|
||||
<!-- Keep this file for future project.task customizations if needed -->
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -5,8 +5,17 @@
|
|||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Add is_venue field to the form using simple field position -->
|
||||
<field name="name" position="after">
|
||||
<group col="2">
|
||||
<field name="is_venue" string="Is Venue" help="Check this box if this contact represents a venue/location" widget="boolean_toggle"/>
|
||||
</group>
|
||||
</field>
|
||||
<page name="contact_addresses" position="after">
|
||||
<page name="Teams">
|
||||
<group>
|
||||
<field name="is_venue" string="Is Venue" help="Check this box if this contact represents a venue/location"/>
|
||||
</group>
|
||||
<field invisible="is_company == False" name="owned_team_ids" string="Teams"/>
|
||||
<field invisible="is_company == True" name="teams_served_ids" string="Teams Served">
|
||||
<list editable="bottom" create="False" delete="True">
|
||||
|
|
@ -19,6 +28,7 @@
|
|||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</page></field>
|
||||
</page>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -2,24 +2,92 @@
|
|||
<odoo>
|
||||
<data>
|
||||
<template id="portal_my_home" inherit_id="portal.portal_my_home">
|
||||
<xpath expr="//div[hasclass('o_portal_docs')]" position="inside">
|
||||
<t t-call="portal.portal_docs_entry">
|
||||
<t t-set="title">Teams</t>
|
||||
<t t-set="url">/my/teams</t>
|
||||
<t t-set="placeholder_count">teams_count</t>
|
||||
</t>
|
||||
<t t-call="portal.portal_docs_entry">
|
||||
<t t-set="title">Players</t>
|
||||
<t t-set="url">/my/players</t>
|
||||
<t t-set="placeholder_count">players_count</t>
|
||||
</t>
|
||||
<t t-call="portal.portal_docs_entry">
|
||||
<t t-set="title">Activities</t>
|
||||
<t t-set="url">/my/activities</t>
|
||||
<t t-set="placeholder_count">activities_count</t>
|
||||
</t>
|
||||
<xpath expr="//div[@id='portal_common_category']" position="before">
|
||||
<div class="o_portal_category row g-2 mt-3" id="portal_sports_category">
|
||||
<t t-call="portal.portal_docs_entry">
|
||||
<t t-set="title">Teams</t>
|
||||
<t t-set="url">/my/teams</t>
|
||||
<t t-set="placeholder_count">teams_count</t>
|
||||
</t>
|
||||
<t t-call="portal.portal_docs_entry">
|
||||
<t t-set="title">Players</t>
|
||||
<t t-set="url">/my/players</t>
|
||||
<t t-set="placeholder_count">players_count</t>
|
||||
</t>
|
||||
<t t-call="portal.portal_docs_entry">
|
||||
<t t-set="title">Activities</t>
|
||||
<t t-set="url">/my/activities</t>
|
||||
<t t-set="placeholder_count">activities_count</t>
|
||||
<t t-set="config_card" t-value="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')"/>
|
||||
</t>
|
||||
<t t-call="portal.portal_docs_entry">
|
||||
<t t-set="title">Events</t>
|
||||
<t t-set="url">/my/events</t>
|
||||
<t t-set="placeholder_count">events_count</t>
|
||||
<t t-set="config_card" t-value="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')"/>
|
||||
</t>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- Hide Invoice cards for coaches and treatment professionals -->
|
||||
<template id="portal_my_home_invoice_coach_override" inherit_id="account.portal_my_home_invoice" priority="50">
|
||||
<xpath expr="//div[@id='portal_client_category']" position="attributes">
|
||||
<attribute name="t-if">not request.env.user.has_group('bemade_sports_clinic.group_portal_team_coach') and not request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//div[@id='portal_vendor_category']" position="attributes">
|
||||
<attribute name="t-if">not request.env.user.has_group('bemade_sports_clinic.group_portal_team_coach') and not request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')</attribute>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- Hide Project/Tasks cards for coaches and treatment professionals -->
|
||||
<template id="portal_my_home_project_coach_override" inherit_id="project.portal_my_home" priority="50">
|
||||
<xpath expr="//div[@id='portal_service_category']" position="attributes">
|
||||
<attribute name="t-if">not request.env.user.has_group('bemade_sports_clinic.group_portal_team_coach') and not request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')</attribute>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- Add custom CSS and classes to hide cards for sports clinic users -->
|
||||
<template id="portal_my_home_sports_clinic_styles" inherit_id="portal.portal_my_home" priority="60">
|
||||
<xpath expr="//div[hasclass('o_portal_my_home')]" position="attributes">
|
||||
<attribute name="t-attf-class">o_portal_my_home #{request.env.user.has_group('bemade_sports_clinic.group_portal_team_coach') and 'o_portal_user_coach' or ''} #{request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') and 'o_portal_user_therapist' or ''}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('o_portal_my_home')]" position="before">
|
||||
<style>
|
||||
/* Hide knowledge cards for coaches only (keep visible for therapists) */
|
||||
.o_portal_user_coach .o_portal_docs_entry[href*="/my/knowledge"],
|
||||
.o_portal_user_coach .o_portal_category [href*="/my/knowledge"],
|
||||
.o_portal_user_coach .o_portal_category [href*="knowledge"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide Activities cards for coaches only (keep visible for therapists) */
|
||||
.o_portal_user_coach .o_portal_docs_entry[href*="/my/activities"],
|
||||
.o_portal_user_coach .o_portal_category [href*="/my/activities"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide payment cards for both coaches and therapists */
|
||||
.o_portal_user_coach .o_portal_docs_entry[href*="/my/payment"],
|
||||
.o_portal_user_coach .o_portal_category [href*="/my/payment"],
|
||||
.o_portal_user_therapist .o_portal_docs_entry[href*="/my/payment"],
|
||||
.o_portal_user_therapist .o_portal_category [href*="/my/payment"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide website/ecommerce cards for both coaches and therapists */
|
||||
.o_portal_user_coach .o_portal_docs_entry[href*="/shop"],
|
||||
.o_portal_user_coach .o_portal_category [href*="/shop"],
|
||||
.o_portal_user_therapist .o_portal_docs_entry[href*="/shop"],
|
||||
.o_portal_user_therapist .o_portal_category [href*="/shop"] {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<template id="portal_my_teams">
|
||||
<t t-call="portal.portal_layout">
|
||||
<h1>Your Teams</h1>
|
||||
|
|
@ -30,8 +98,8 @@
|
|||
<th>Parent Organization</th>
|
||||
<th>Total Players</th>
|
||||
<th>Injured</th>
|
||||
<th>Activities</th>
|
||||
<th>Actions</th>
|
||||
<th t-if="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')">Activities</th>
|
||||
<th t-if="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -46,11 +114,11 @@
|
|||
<td><span t-field="team.parent_id"/></td>
|
||||
<td><span t-field="team.player_count"/></td>
|
||||
<td><span t-field="team.injured_count"/></td>
|
||||
<td>
|
||||
<td t-if="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')">
|
||||
<t t-set="team_activity_count" t-value="team.activity_count or 0"/>
|
||||
<span t-esc="team_activity_count"/>
|
||||
</td>
|
||||
<td>
|
||||
<td t-if="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')">
|
||||
<a t-attf-href="/my/activities?model=sports.team&res_id={{ team.id }}"
|
||||
class="btn bg-o-color-1 text-white btn-sm">
|
||||
<i class="fa fa-tasks"></i> Activities
|
||||
|
|
@ -109,7 +177,7 @@
|
|||
</t>
|
||||
</h1>
|
||||
<div>
|
||||
<a t-attf-href="/my/activities?model=sports.team&res_id={{ team.id }}" class="btn bg-o-color-2 text-white me-2">
|
||||
<a t-if="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')" t-attf-href="/my/activities?model=sports.team&res_id={{ team.id }}" class="btn bg-o-color-2 text-white me-2">
|
||||
<i class="fa fa-tasks"></i> Activities
|
||||
</a>
|
||||
<a t-if="is_treatment_prof" t-attf-href="/my/activity/create?model=sports.team&res_id={{ team.id }}" class="btn bg-o-color-2 text-white me-2">
|
||||
|
|
@ -334,9 +402,9 @@
|
|||
<i class="fa fa-edit"></i> Edit Player
|
||||
</a>
|
||||
<a t-att-href="'/my/patient/injury/new?patient_id=%s' % player.id" class="btn bg-o-color-1 text-white me-2">
|
||||
<i class="fa fa-plus"></i> Report Injury
|
||||
<i class="fa fa-plus"></i> <span t-if="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')">Add Injury</span><span t-else="">Report Injury</span>
|
||||
</a>
|
||||
<a t-attf-href="/my/activities?model=sports.patient&res_id={{ player.id }}" class="btn bg-o-color-2 text-white me-2">
|
||||
<a t-if="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')" t-attf-href="/my/activities?model=sports.patient&res_id={{ player.id }}" class="btn bg-o-color-2 text-white me-2">
|
||||
<i class="fa fa-tasks"></i> Activities
|
||||
</a>
|
||||
<a t-if="is_treatment_prof" t-att-href="'/my/activity/create?model=sports.patient&res_id=%s' % player.id" class="btn bg-o-color-2 text-white me-2">
|
||||
|
|
@ -456,7 +524,7 @@
|
|||
<t t-if="not is_treatment_prof">
|
||||
<th>External Notes</th>
|
||||
</t>
|
||||
<th>Activities</th>
|
||||
<th t-if="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')">Activities</th>
|
||||
<t t-if="is_treatment_prof">
|
||||
<th class="text-end">Actions</th>
|
||||
</t>
|
||||
|
|
@ -493,7 +561,7 @@
|
|||
<div t-else="" class="text-muted">No external notes</div>
|
||||
</td>
|
||||
</t>
|
||||
<td>
|
||||
<td t-if="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')">
|
||||
<div class="d-flex align-items-center">
|
||||
<t t-set="injury_activity_count" t-value="injury.activity_count or 0"/>
|
||||
<span t-esc="injury_activity_count" class="me-2"/>
|
||||
|
|
@ -530,7 +598,7 @@
|
|||
<i class="fa fa-ambulance fa-3x mb-3"></i>
|
||||
<p>No injuries recorded</p>
|
||||
<a t-att-href="'/my/patient/injury/new?patient_id=%s' % player.id" class="btn bg-o-color-1 text-white">
|
||||
<i class="fa fa-plus"></i> Report First Injury
|
||||
<i class="fa fa-plus"></i> <span t-if="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')">Add First Injury</span><span t-else="">Report First Injury</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
213
bemade_sports_clinic/views/sports_event_views.xml
Normal file
213
bemade_sports_clinic/views/sports_event_views.xml
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<!-- Sports Event List View -->
|
||||
<record id="sports_event_view_list" model="ir.ui.view">
|
||||
<field name="name">sports.event.list</field>
|
||||
<field name="model">sports.event</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Sports Events" default_order="date_start desc">
|
||||
<field name="name"/>
|
||||
<field name="description" widget="html"/>
|
||||
<field name="event_type"/>
|
||||
<field name="team_id"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="date_start" widget="datetime"/>
|
||||
<field name="date_end" widget="datetime"/>
|
||||
<field name="duration" widget="float_time"/>
|
||||
<field name="therapist_start" widget="datetime"/>
|
||||
<field name="therapist_end" widget="datetime"/>
|
||||
<field name="therapist_duration" widget="float_time"/>
|
||||
<field name="venue_id"/>
|
||||
<field name="assigned_staff_ids" widget="many2many_tags"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Sports Event Form View -->
|
||||
<record id="sports_event_view_form" model="ir.ui.view">
|
||||
<field name="name">sports.event.form</field>
|
||||
<field name="model">sports.event</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Sports Event">
|
||||
<header>
|
||||
<button name="create_management_task" type="object"
|
||||
string="Create Management Task"
|
||||
class="btn-primary"
|
||||
invisible="task_id != False"
|
||||
groups="base.group_user"/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="draft,confirmed,in_progress,completed"
|
||||
clickable="1"/>
|
||||
</header>
|
||||
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="Event Name"/>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<group>
|
||||
<group string="Event Details">
|
||||
<field name="event_type"/>
|
||||
<field name="team_id" required="1"/>
|
||||
<field name="partner_id" readonly="1"/>
|
||||
<field name="venue_id"/>
|
||||
</group>
|
||||
<group string="Event Timing">
|
||||
<field name="date_start" widget="datetime"/>
|
||||
<field name="date_end" widget="datetime"/>
|
||||
<field name="duration" widget="float_time" readonly="1"/>
|
||||
<field name="is_today" invisible="1"/>
|
||||
<field name="is_upcoming" invisible="1"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group string="Therapist Coverage">
|
||||
<group>
|
||||
<field name="therapist_start" widget="datetime"/>
|
||||
<field name="therapist_end" widget="datetime"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="therapist_duration" widget="float_time" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<notebook>
|
||||
<page string="Description">
|
||||
<field name="description" widget="html"/>
|
||||
</page>
|
||||
|
||||
<page string="Staff Assignment">
|
||||
<group>
|
||||
<label for="assigned_staff_ids" string="Assigned Staff"/>
|
||||
<field name="assigned_staff_ids" widget="many2many_checkboxes" nolabel="1">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="email"/>
|
||||
<field name="phone"/>
|
||||
</list>
|
||||
</field>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<page string="Task Integration" groups="base.group_user">
|
||||
<group>
|
||||
<field name="task_id" readonly="1"/>
|
||||
<field name="project_id" readonly="1"/>
|
||||
</group>
|
||||
<div class="alert alert-info" role="alert" invisible="task_id != False">
|
||||
<p><strong>No management task created yet.</strong></p>
|
||||
<p>Click "Create Management Task" to create an internal project task for managing this event.</p>
|
||||
</div>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter reload_on_post="True"/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Sports Event Search View -->
|
||||
<record id="sports_event_view_search" model="ir.ui.view">
|
||||
<field name="name">sports.event.search</field>
|
||||
<field name="model">sports.event</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Sports Events">
|
||||
<field name="name"/>
|
||||
<field name="team_id"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="venue_id"/>
|
||||
<field name="assigned_staff_ids"/>
|
||||
<field name="event_type"/>
|
||||
|
||||
<filter name="today" string="Today"
|
||||
domain="[('date_start', '>=', context_today().strftime('%Y-%m-%d')),
|
||||
('date_start', '<', (context_today() + datetime.timedelta(days=1)).strftime('%Y-%m-%d'))]"/>
|
||||
<filter name="upcoming" string="Upcoming"
|
||||
domain="[('date_start', '>=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter name="this_week" string="This Week"
|
||||
domain="[('date_start', '>=', (context_today() - datetime.timedelta(days=context_today().weekday())).strftime('%Y-%m-%d')),
|
||||
('date_start', '<', (context_today() + datetime.timedelta(days=7-context_today().weekday())).strftime('%Y-%m-%d'))]"/>
|
||||
|
||||
<separator/>
|
||||
<filter name="draft" string="Draft"
|
||||
domain="[('state', '=', 'draft')]"/>
|
||||
<filter name="confirmed" string="Confirmed"
|
||||
domain="[('state', '=', 'confirmed')]"/>
|
||||
<filter name="in_progress" string="In Progress"
|
||||
domain="[('state', '=', 'in_progress')]"/>
|
||||
<filter name="completed" string="Completed"
|
||||
domain="[('state', '=', 'completed')]"/>
|
||||
|
||||
<separator/>
|
||||
|
||||
<group expand="0" string="Group By">
|
||||
<filter name="group_partner" string="Organization"
|
||||
context="{'group_by': 'partner_id'}"/>
|
||||
<filter name="group_team" string="Team"
|
||||
context="{'group_by': 'team_id'}"/>
|
||||
<filter name="group_event_type" string="Event Type"
|
||||
context="{'group_by': 'event_type'}"/>
|
||||
<filter name="group_state" string="Status"
|
||||
context="{'group_by': 'state'}"/>
|
||||
<filter name="group_date" string="Date"
|
||||
context="{'group_by': 'date_start:day'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Sports Event Calendar View -->
|
||||
<record id="sports_event_view_calendar" model="ir.ui.view">
|
||||
<field name="name">sports.event.calendar</field>
|
||||
<field name="model">sports.event</field>
|
||||
<field name="arch" type="xml">
|
||||
<calendar string="Sports Events Calendar"
|
||||
date_start="therapist_start"
|
||||
date_stop="therapist_end"
|
||||
color="team_id"
|
||||
event_open_popup="True">
|
||||
<field name="name"/>
|
||||
<field name="description"/>
|
||||
<field name="team_id"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="venue_id"/>
|
||||
<field name="event_type"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
</calendar>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Sports Event Action -->
|
||||
<record id="sports_event_action" model="ir.actions.act_window">
|
||||
<field name="name">Sports Events</field>
|
||||
<field name="res_model">sports.event</field>
|
||||
<field name="view_mode">calendar,list,form</field>
|
||||
<field name="search_view_id" ref="sports_event_view_search"/>
|
||||
<field name="context">{
|
||||
'search_default_upcoming': 1,
|
||||
}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first sports event!
|
||||
</p>
|
||||
<p>
|
||||
Sports events help you manage games, practices, training sessions, and team meetings.
|
||||
Each event can be linked to a project task for internal management.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu Items -->
|
||||
<menuitem id="sports_event_menu"
|
||||
name="Events"
|
||||
parent="sports_clinic_root"
|
||||
action="sports_event_action"
|
||||
sequence="30"/>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -4,54 +4,190 @@
|
|||
<!-- Template for creating a new injury via the portal -->
|
||||
<template id="portal_create_injury">
|
||||
<t t-call="portal.portal_layout">
|
||||
<div class="container">
|
||||
<div class="container pt-3">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1>Report Injury for <t t-esc="patient.name"/></h1>
|
||||
<form action="/my/patient/injury/create" method="post" class="mt-3">
|
||||
<div class="col-lg-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a t-att-href="'/my/player?player_id=%s' % patient.id">
|
||||
<t t-esc="patient.name"/>
|
||||
</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">
|
||||
<span t-if="is_treatment_prof">Add Injury</span>
|
||||
<span t-else="">Report Injury</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-lg-12">
|
||||
<h2><span t-if="is_treatment_prof">Add Injury for</span><span t-else="">Report Injury for</span> <t t-esc="patient.name"/></h2>
|
||||
|
||||
<form action="/my/patient/injury/create" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<input type="hidden" name="patient_id" t-att-value="patient.id"/>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="injury_date" class="col-md-3 col-form-label">Injury Date</label>
|
||||
<div class="col-md-9">
|
||||
<input type="date" class="form-control" id="injury_date" name="injury_date" required="required"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="diagnosis" class="col-md-3 col-form-label">Diagnosis</label>
|
||||
<div class="col-md-9">
|
||||
<input type="text" class="form-control" id="diagnosis" name="diagnosis" required="required"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="predicted_resolution_date" class="col-md-3 col-form-label">Predicted Resolution Date</label>
|
||||
<div class="col-md-9">
|
||||
<input type="date" class="form-control" id="predicted_resolution_date" name="predicted_resolution_date"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Only show parental consent field for treatment professionals -->
|
||||
<t t-if="is_treatment_prof">
|
||||
<div class="form-group row">
|
||||
<label for="parental_consent" class="col-md-3 col-form-label">Consent for Disclosure to Parent</label>
|
||||
<div class="col-md-9">
|
||||
<select class="form-control" id="parental_consent" name="parental_consent" required="required">
|
||||
<option value="yes">Yes</option>
|
||||
<option value="no">No</option>
|
||||
<option value="na">Not Applicable</option>
|
||||
</select>
|
||||
<input type="hidden" name="return_url" t-att-value="return_url"/>
|
||||
|
||||
<!-- Top Action Buttons -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-lg-12">
|
||||
<div class="d-flex justify-content-between">
|
||||
<a t-att-href="return_url" class="btn bg-o-color-2 text-white">
|
||||
<i class="fa fa-times"></i> Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn bg-o-color-1 text-white">
|
||||
<i class="fa fa-save"></i> <span t-if="is_treatment_prof">Create Injury</span><span t-else="">Submit Report</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<div class="form-group row">
|
||||
<label for="external_notes" class="col-md-3 col-form-label">Notes</label>
|
||||
<div class="col-md-9">
|
||||
<textarea class="form-control" id="external_notes" name="external_notes" rows="5"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Injury Details Section -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h4 class="mb-0">Injury Details</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Basic injury information -->
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="form-group">
|
||||
<label for="injury_date">Date of Injury</label>
|
||||
<div class="d-flex align-items-center">
|
||||
<input type="date" name="injury_date" id="injury_date" class="form-control"
|
||||
required="required"/>
|
||||
<div class="form-check ml-3">
|
||||
<input type="checkbox" name="injury_date_na" id="injury_date_na" class="form-check-input"/>
|
||||
<label for="injury_date_na" class="form-check-label">N/A</label>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted">Check N/A if exact date is unknown</small>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="is_treatment_prof" class="col-lg-6">
|
||||
<div class="form-group">
|
||||
<label for="predicted_resolution_date">Predicted Resolution Date</label>
|
||||
<input type="date" name="predicted_resolution_date" id="predicted_resolution_date" class="form-control"/>
|
||||
<small class="text-muted">Expected recovery timeline</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Diagnosis -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-lg-12">
|
||||
<div class="form-group">
|
||||
<label for="diagnosis">Diagnosis/Description</label>
|
||||
<textarea name="diagnosis" id="diagnosis" class="form-control"
|
||||
rows="3" required="required"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Treatment professionals and parental consent (only for treatment professionals) -->
|
||||
<div t-if="is_treatment_prof" class="row mt-3">
|
||||
<div class="col-lg-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Treatment Professionals</label>
|
||||
<div class="border rounded p-3" style="max-height: 200px; overflow-y: auto;">
|
||||
<t t-foreach="treatment_professionals" t-as="tp">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
t-att-id="'tp_%s' % tp.id"
|
||||
name="treatment_professional_ids[]"
|
||||
t-att-value="tp.id"/>
|
||||
<label class="form-check-label" t-att-for="'tp_%s' % tp.id">
|
||||
<span t-esc="tp.name"/>
|
||||
<small class="text-muted d-block" t-esc="tp.email"/>
|
||||
</label>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<small class="text-muted">Select multiple professionals (you will be added automatically)</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="form-group">
|
||||
<label for="parental_consent">Parental Consent</label>
|
||||
<select name="parental_consent" id="parental_consent" class="form-control" required="required">
|
||||
<t t-foreach="parental_consent_options" t-as="option">
|
||||
<option t-att-value="option[0]">
|
||||
<t t-esc="option[1]"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Internal notes (only visible to treatment professionals) -->
|
||||
<div t-if="is_treatment_prof" class="row mt-3">
|
||||
<div class="col-lg-12">
|
||||
<div class="form-group">
|
||||
<label for="internal_notes">Internal Notes (Medical Staff Only)</label>
|
||||
<textarea name="internal_notes" id="internal_notes" class="form-control"
|
||||
rows="3"></textarea>
|
||||
<small class="text-muted">Notes visible only to treatment professionals</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix mb-3">
|
||||
<button type="submit" class="btn bg-o-color-1 text-white float-right">Submit</button>
|
||||
<a t-if="return_url" t-att-href="return_url" class="btn bg-o-color-2 text-white float-left">Cancel</a>
|
||||
|
||||
<!-- Add Treatment Note Section (only for treatment professionals) -->
|
||||
<div t-if="is_treatment_prof" class="card mb-4">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h4 class="mb-0">Add Treatment Note</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="form-group">
|
||||
<label for="treatment_note">Treatment Note</label>
|
||||
<textarea name="treatment_note" id="treatment_note" class="form-control" rows="4"
|
||||
placeholder="Enter treatment note (will be saved with the current date)"></textarea>
|
||||
<small class="text-muted">Optional: Add an initial treatment note if treatment was provided during injury assessment</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- External Notes Section -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-secondary text-white">
|
||||
<h4 class="mb-0">External Notes</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="form-group">
|
||||
<label for="external_notes">External Notes</label>
|
||||
<textarea name="external_notes" id="external_notes" class="form-control"
|
||||
rows="4"></textarea>
|
||||
<small class="text-muted">Notes visible to all team members</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-lg-12">
|
||||
<div class="float-left">
|
||||
<a t-att-href="return_url" class="btn bg-o-color-2 text-white">Cancel</a>
|
||||
|
||||
<!-- Submit button -->
|
||||
<button type="submit" class="btn bg-o-color-1 text-white">
|
||||
<span t-if="is_treatment_prof">Create Injury</span><span t-else="">Submit Report</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -49,6 +49,17 @@
|
|||
widget="many2many_tags_avatar"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Treatment Notes">
|
||||
<field name="treatment_note_ids">
|
||||
<list string="Treatment Notes" editable="bottom">
|
||||
<field name="date"/>
|
||||
<field name="note"/>
|
||||
<field name="user_id" readonly="1"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter reload_on_post="True"/>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -152,6 +152,17 @@
|
|||
<page string="Injuries">
|
||||
<field name="injury_ids"/>
|
||||
</page>
|
||||
<page string="Treatment Notes">
|
||||
<field name="treatment_note_ids">
|
||||
<list string="Treatment Notes" editable="bottom">
|
||||
<field name="date"/>
|
||||
<field name="injury_id" domain="[('patient_id', '=', parent.id)]"
|
||||
options="{'no_create': True, 'no_open': False}"/>
|
||||
<field name="note"/>
|
||||
<field name="user_id" readonly="1"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Contacts">
|
||||
<group>
|
||||
<group string="Patient Phone & Email">
|
||||
|
|
|
|||
74
bemade_sports_clinic/views/task_to_event_wizard_views.xml
Normal file
74
bemade_sports_clinic/views/task_to_event_wizard_views.xml
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Custom list view for project tasks in wizard -->
|
||||
<record id="view_project_task_list_wizard" model="ir.ui.view">
|
||||
<field name="name">project.task.list.wizard</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="type">list</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="project_id"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
<field name="date_deadline"/>
|
||||
<field name="user_ids" widget="many2many_tags"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Task to Event Conversion Wizard Form -->
|
||||
<record id="view_task_to_event_wizard_form" model="ir.ui.view">
|
||||
<field name="name">task.to.event.wizard.form</field>
|
||||
<field name="model">task.to.event.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Convert Tasks to Sports Events">
|
||||
<group>
|
||||
<group>
|
||||
<field name="task_count" readonly="1"/>
|
||||
<field name="team_id"/>
|
||||
<field name="event_type"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="venue_id"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<notebook>
|
||||
<page string="Selected Tasks" name="tasks">
|
||||
<field name="task_ids" readonly="1" context="{'list_view_ref': 'bemade_sports_clinic.view_project_task_list_wizard'}"/>
|
||||
</page>
|
||||
</notebook>
|
||||
|
||||
<div class="alert alert-info" role="alert">
|
||||
<strong>Conversion Details:</strong>
|
||||
<ul>
|
||||
<li>Each task will be converted to a separate sports event</li>
|
||||
<li>Event dates will be taken from task start/end dates or deadline</li>
|
||||
<li>If no end date exists, it will be set to start date + 2 hours</li>
|
||||
<li>Task assignees will become event assigned staff</li>
|
||||
<li>Original tasks will be linked to the created events</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<button string="Convert to Events" name="action_convert_tasks" type="object" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Wizard Action -->
|
||||
<record id="action_task_to_event_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Convert Tasks to Sports Events</field>
|
||||
<field name="res_model">task.to.event.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="binding_model_id" ref="project.model_project_task"/>
|
||||
<field name="binding_view_types">list</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -85,11 +85,4 @@
|
|||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu Item -->
|
||||
<menuitem id="menu_treatment_notes"
|
||||
name="Treatment Notes"
|
||||
parent="sports_clinic_root"
|
||||
action="action_treatment_notes"
|
||||
sequence="30"/>
|
||||
|
||||
</odoo>
|
||||
|
|
|
|||
1
bemade_sports_clinic/wizards/__init__.py
Normal file
1
bemade_sports_clinic/wizards/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import task_to_event_wizard
|
||||
202
bemade_sports_clinic/wizards/task_to_event_wizard.py
Normal file
202
bemade_sports_clinic/wizards/task_to_event_wizard.py
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class TaskToEventWizard(models.TransientModel):
|
||||
_name = 'task.to.event.wizard'
|
||||
_description = 'Convert Project Tasks to Sports Events'
|
||||
|
||||
task_ids = fields.Many2many(
|
||||
'project.task',
|
||||
string='Selected Tasks',
|
||||
required=True,
|
||||
help='Tasks to convert to sports events'
|
||||
)
|
||||
|
||||
team_id = fields.Many2one(
|
||||
'sports.team',
|
||||
string='Team',
|
||||
required=True,
|
||||
help='Team to assign to all created events'
|
||||
)
|
||||
|
||||
event_type = fields.Selection([
|
||||
('game', 'Game'),
|
||||
('practice', 'Practice'),
|
||||
('training', 'Training'),
|
||||
('meeting', 'Team Meeting'),
|
||||
('other', 'Other')
|
||||
], string='Event Type', default='other', required=True,
|
||||
help='Type of event for all created events')
|
||||
|
||||
venue_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Venue',
|
||||
domain=[('is_venue', '=', True)],
|
||||
help='Default venue for all events (can be changed individually later)'
|
||||
)
|
||||
|
||||
task_count = fields.Integer(
|
||||
string='Number of Tasks',
|
||||
compute='_compute_task_count'
|
||||
)
|
||||
|
||||
@api.depends('task_ids')
|
||||
def _compute_task_count(self):
|
||||
for wizard in self:
|
||||
wizard.task_count = len(wizard.task_ids)
|
||||
|
||||
def _extract_venue_from_description(self, description):
|
||||
"""Extract venue from task description and find or create venue"""
|
||||
import re
|
||||
|
||||
# Look for "Location: " in the description (case insensitive)
|
||||
match = re.search(r'location:\s*(.+?)(?:\n|$)', description, re.IGNORECASE)
|
||||
if not match:
|
||||
return False
|
||||
|
||||
venue_name = match.group(1).strip()
|
||||
if not venue_name:
|
||||
return False
|
||||
|
||||
# Try to find existing venue by name
|
||||
existing_venue = self.env['res.partner'].search([
|
||||
('name', '=', venue_name),
|
||||
('is_venue', '=', True)
|
||||
], limit=1)
|
||||
|
||||
if existing_venue:
|
||||
return existing_venue.id
|
||||
|
||||
# Create new venue if not found
|
||||
new_venue = self.env['res.partner'].create({
|
||||
'name': venue_name,
|
||||
'is_company': True,
|
||||
'is_venue': True,
|
||||
'supplier_rank': 0,
|
||||
'customer_rank': 0,
|
||||
})
|
||||
|
||||
return new_venue.id
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
"""Set default task_ids from context"""
|
||||
res = super().default_get(fields_list)
|
||||
|
||||
# Get selected tasks from context
|
||||
active_ids = self.env.context.get('active_ids', [])
|
||||
if active_ids and 'task_ids' in fields_list:
|
||||
res['task_ids'] = [(6, 0, active_ids)]
|
||||
|
||||
return res
|
||||
|
||||
def action_convert_tasks(self):
|
||||
"""Convert selected tasks to sports events"""
|
||||
if not self.task_ids:
|
||||
raise UserError("No tasks selected for conversion.")
|
||||
|
||||
if not self.team_id:
|
||||
raise UserError("Please select a team for the events.")
|
||||
|
||||
# Ensure we have unique tasks (prevent duplicates)
|
||||
unique_tasks = self.task_ids.filtered(lambda t: t.id)
|
||||
if not unique_tasks:
|
||||
raise UserError("No valid tasks found for conversion.")
|
||||
|
||||
# Check if any tasks have already been converted by this wizard
|
||||
from datetime import timedelta as dt_timedelta
|
||||
existing_events = self.env['sports.event'].search([
|
||||
('task_id', 'in', unique_tasks.ids),
|
||||
('create_date', '>=', fields.Datetime.now() - dt_timedelta(minutes=5)) # Recent conversions
|
||||
])
|
||||
|
||||
if existing_events:
|
||||
task_names = existing_events.mapped('task_id.name')
|
||||
raise UserError(f"Some tasks appear to have been recently converted: {', '.join(task_names)}. "
|
||||
f"Please check existing events before proceeding.")
|
||||
|
||||
created_events = self.env['sports.event']
|
||||
processed_task_ids = set() # Track processed tasks to prevent duplicates
|
||||
|
||||
for task in unique_tasks:
|
||||
# Skip if we've already processed this task (prevent duplicates within same run)
|
||||
if task.id in processed_task_ids:
|
||||
continue
|
||||
processed_task_ids.add(task.id)
|
||||
|
||||
# Validate task has required information
|
||||
if not task.name:
|
||||
raise UserError(f"Task {task.id} has no name and cannot be converted.")
|
||||
|
||||
# Determine event dates from task planned dates
|
||||
date_start = task.planned_date_begin or task.date_deadline
|
||||
date_end = task.date_deadline
|
||||
|
||||
if not date_start:
|
||||
raise UserError(f"Task '{task.name}' has no planned start date or deadline and cannot be converted.")
|
||||
|
||||
# If no end date, set it to start date + 2 hours (reasonable default)
|
||||
if not date_end:
|
||||
from datetime import timedelta
|
||||
date_end = date_start + timedelta(hours=2)
|
||||
|
||||
# Ensure end date is after start date
|
||||
if date_end <= date_start:
|
||||
from datetime import timedelta
|
||||
date_end = date_start + timedelta(hours=2)
|
||||
|
||||
# Extract venue from task description if available
|
||||
venue_id = self.venue_id.id if self.venue_id else False
|
||||
if task.description and not venue_id:
|
||||
venue_id = self._extract_venue_from_description(task.description)
|
||||
|
||||
# Create the sports event
|
||||
event_vals = {
|
||||
'name': task.name,
|
||||
'description': task.description or '',
|
||||
'date_start': date_start,
|
||||
'date_end': date_end,
|
||||
'therapist_start': date_start, # Therapist start = event start
|
||||
'therapist_end': date_end, # Therapist end = event end
|
||||
'team_id': self.team_id.id,
|
||||
'event_type': self.event_type,
|
||||
'venue_id': venue_id,
|
||||
'task_id': task.id, # Link back to original task
|
||||
'project_id': task.project_id.id if task.project_id else False,
|
||||
'state': 'confirmed',
|
||||
}
|
||||
|
||||
# Set assigned staff from task users
|
||||
if task.user_ids:
|
||||
event_vals['assigned_staff_ids'] = [(6, 0, task.user_ids.ids)]
|
||||
|
||||
event = self.env['sports.event'].create(event_vals)
|
||||
created_events |= event
|
||||
|
||||
# Add a note to the task about the conversion (without sending emails)
|
||||
task.with_context(mail_notrack=True, mail_create_nolog=True).message_post(
|
||||
body=f"Task converted to Sports Event: <a href='/web#id={event.id}&model=sports.event'>{event.name}</a>",
|
||||
subject="Converted to Sports Event"
|
||||
)
|
||||
|
||||
# Return action to view created events
|
||||
if len(created_events) == 1:
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Created Sports Event',
|
||||
'res_model': 'sports.event',
|
||||
'res_id': created_events.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Created Sports Events',
|
||||
'res_model': 'sports.event',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('id', 'in', created_events.ids)],
|
||||
'target': 'current',
|
||||
}
|
||||
Loading…
Reference in a new issue