mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-07 12:10:04 -06:00
fix: resolve Excel export 500 error caused by incorrect project.client access
- Fixed AttributeError when accessing project.client.name in Excel export - Project.client is a string property returning the client name, not an object - Changed all incorrect .name accesses to use the string property directly - Added unit tests for Excel export functionality to prevent regression Fixes bug where exporting time entries to Excel resulted in 500 server error with message: 'str' object has no attribute 'name' Files changed: - app/utils/excel_export.py: Fixed time entries export client column - app/routes/reports.py: Fixed project report export client field - app/templates/projects/view.html: Fixed project view template - tests/test_excel_export.py: Added comprehensive Excel export tests
This commit is contained in:
@@ -744,7 +744,7 @@ def export_project_excel():
|
||||
if project.id not in projects_map:
|
||||
projects_map[project.id] = {
|
||||
'name': project.name,
|
||||
'client': project.client.name if project.client else '',
|
||||
'client': project.client if project.client else '',
|
||||
'total_hours': 0,
|
||||
'billable_hours': 0,
|
||||
'hourly_rate': float(project.hourly_rate) if project.hourly_rate else 0,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold tracking-wide uppercase bg-gray-200 text-gray-800 dark:bg-gray-700 dark:text-gray-200" title="{{ _('Project Code') }}">{{ project.code_display }}</span>
|
||||
{% endif %}
|
||||
</h1>
|
||||
<p class="text-text-muted-light dark:text-text-muted-dark">{{ project.client.name }}</p>
|
||||
<p class="text-text-muted-light dark:text-text-muted-dark">{{ project.client }}</p>
|
||||
</div>
|
||||
{% if current_user.is_admin or has_any_permission(['edit_projects', 'archive_projects']) %}
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
|
||||
@@ -54,7 +54,7 @@ def create_time_entries_excel(entries, filename_prefix='timetracker_export'):
|
||||
entry.id,
|
||||
entry.user.display_name if entry.user else 'Unknown',
|
||||
entry.project.name if entry.project else 'N/A',
|
||||
entry.project.client.name if (entry.project and entry.project.client) else 'N/A',
|
||||
entry.project.client if (entry.project and entry.project.client) else 'N/A',
|
||||
entry.task.name if entry.task else 'N/A',
|
||||
entry.start_time.isoformat() if entry.start_time else '',
|
||||
entry.end_time.isoformat() if entry.end_time else '',
|
||||
|
||||
144
tests/test_excel_export.py
Normal file
144
tests/test_excel_export.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""
|
||||
Tests for Excel export functionality
|
||||
"""
|
||||
import pytest
|
||||
from datetime import datetime, timedelta
|
||||
from app.models import TimeEntry, Task
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.routes
|
||||
def test_create_time_entries_excel_with_client(app, user, project, test_client):
|
||||
"""Test that Excel export handles project.client correctly as a string property"""
|
||||
from app.utils.excel_export import create_time_entries_excel
|
||||
|
||||
# Create a time entry with project that has a client
|
||||
start_time = datetime.utcnow() - timedelta(hours=2)
|
||||
end_time = datetime.utcnow()
|
||||
|
||||
entry = TimeEntry(
|
||||
user_id=user.id,
|
||||
project_id=project.id,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
notes='Test entry for Excel export',
|
||||
tags='test,export',
|
||||
source='manual',
|
||||
billable=True
|
||||
)
|
||||
|
||||
# Calculate duration manually since we're not going through the full commit cycle
|
||||
entry.duration_seconds = (end_time - start_time).total_seconds()
|
||||
|
||||
# Test that project.client is a string property, not an object
|
||||
assert hasattr(project, 'client')
|
||||
assert isinstance(project.client, str)
|
||||
assert project.client == test_client.name
|
||||
|
||||
# Test Excel export function
|
||||
output, filename = create_time_entries_excel([entry])
|
||||
|
||||
# Verify the output was created successfully
|
||||
assert output is not None
|
||||
assert filename is not None
|
||||
assert filename.endswith('.xlsx')
|
||||
|
||||
# Verify the file content can be read
|
||||
output.seek(0)
|
||||
content = output.read()
|
||||
assert len(content) > 0
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.routes
|
||||
def test_create_time_entries_excel_with_task(app, user, project, task):
|
||||
"""Test that Excel export handles entries with tasks correctly"""
|
||||
from app.utils.excel_export import create_time_entries_excel
|
||||
|
||||
# Create a time entry with a task
|
||||
start_time = datetime.utcnow() - timedelta(hours=1)
|
||||
end_time = datetime.utcnow()
|
||||
|
||||
entry = TimeEntry(
|
||||
user_id=user.id,
|
||||
project_id=project.id,
|
||||
task_id=task.id,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
notes='Test entry with task',
|
||||
billable=True
|
||||
)
|
||||
|
||||
# Calculate duration
|
||||
entry.duration_seconds = (end_time - start_time).total_seconds()
|
||||
|
||||
# Test Excel export function
|
||||
output, filename = create_time_entries_excel([entry])
|
||||
|
||||
# Verify the output was created successfully
|
||||
assert output is not None
|
||||
assert filename is not None
|
||||
|
||||
# Verify the file content can be read
|
||||
output.seek(0)
|
||||
content = output.read()
|
||||
assert len(content) > 0
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.routes
|
||||
def test_create_time_entries_excel_multiple_entries(app, multiple_time_entries):
|
||||
"""Test Excel export with multiple time entries"""
|
||||
from app.utils.excel_export import create_time_entries_excel
|
||||
|
||||
# Test Excel export function with multiple entries
|
||||
output, filename = create_time_entries_excel(multiple_time_entries)
|
||||
|
||||
# Verify the output was created successfully
|
||||
assert output is not None
|
||||
assert filename is not None
|
||||
|
||||
# Verify the file content can be read
|
||||
output.seek(0)
|
||||
content = output.read()
|
||||
assert len(content) > 0
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.routes
|
||||
def test_project_report_excel_export(app, time_entry):
|
||||
"""Test project report Excel export with project.client"""
|
||||
from app.utils.excel_export import create_project_report_excel
|
||||
|
||||
# Create project data structure
|
||||
project = time_entry.project
|
||||
projects_data = [{
|
||||
'name': project.name,
|
||||
'client': project.client, # Should be a string
|
||||
'total_hours': 8.0,
|
||||
'billable_hours': 7.5,
|
||||
'hourly_rate': 75.00,
|
||||
'billable_amount': 562.50,
|
||||
'total_costs': 0,
|
||||
'total_value': 562.50
|
||||
}]
|
||||
|
||||
# Test that project.client is a string
|
||||
assert isinstance(project.client, str)
|
||||
|
||||
# Test Excel export function
|
||||
output, filename = create_project_report_excel(
|
||||
projects_data,
|
||||
start_date='2024-01-01',
|
||||
end_date='2024-12-31'
|
||||
)
|
||||
|
||||
# Verify the output was created successfully
|
||||
assert output is not None
|
||||
assert filename is not None
|
||||
|
||||
# Verify the file content can be read
|
||||
output.seek(0)
|
||||
content = output.read()
|
||||
assert len(content) > 0
|
||||
|
||||
Reference in New Issue
Block a user