mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-03 19:00:13 -05:00
b353184a4f
Implement complete Advanced Expense Management feature set with UI templates, database schema fixes, and reorganized navigation structure. Features: - Expense Categories: Full CRUD with budget tracking and visualization - Mileage Tracking: Vehicle mileage entries with approval workflow - Per Diem Management: Daily allowance claims with location-based rates - Receipt OCR: Infrastructure for receipt scanning (utilities ready) Database: - Migration 037: Create expense_categories, mileage, per_diem_rates, per_diems tables - Migration 038: Fix schema column name mismatches (trip_purpose→purpose, etc.) - Add missing columns (description, odometer, rates, reimbursement tracking) - Fix circular foreign key dependencies Templates (11 new files): - expense_categories/: list, form, view - mileage/: list, form, view - per_diem/: list, form, view, rates_list, rate_form Navigation: - Move Mileage and Per Diem to Expenses sub-pages (header buttons) - Move Expense Categories to Admin menu only - Remove expense management items from Finance menu Fixes: - Fix NoneType comparison error in expense categories utilization - Handle None values safely in budget progress bars - Resolve database column name mismatches UI/UX: - Responsive design with Tailwind CSS and dark mode support - Real-time calculations for mileage amounts - Color-coded budget utilization progress bars - Status badges for approval workflow states - Advanced filtering on all list views Default data: - 7 expense categories (Travel, Meals, Accommodation, etc.) - 4 per diem rates (US, GB, DE, FR)
277 lines
7.0 KiB
Python
277 lines
7.0 KiB
Python
"""
|
|
Tests for Mileage model
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import date, datetime
|
|
from decimal import Decimal
|
|
from app import db
|
|
from app.models import Mileage, User, Project, Client, Expense
|
|
|
|
|
|
@pytest.fixture
|
|
def user(client):
|
|
"""Create a test user"""
|
|
user = User(username='testuser', email='test@example.com')
|
|
user.set_password('password123')
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
return user
|
|
|
|
|
|
@pytest.fixture
|
|
def project(client):
|
|
"""Create a test project"""
|
|
client_obj = Client(name='Test Client', company='Test Client')
|
|
db.session.add(client_obj)
|
|
db.session.commit()
|
|
|
|
project = Project(
|
|
name='Test Project',
|
|
client_id=client_obj.id,
|
|
billable=True
|
|
)
|
|
db.session.add(project)
|
|
db.session.commit()
|
|
return project
|
|
|
|
|
|
def test_create_mileage(client, user):
|
|
"""Test creating a mileage entry"""
|
|
mileage = Mileage(
|
|
user_id=user.id,
|
|
trip_date=date.today(),
|
|
purpose='Client meeting',
|
|
start_location='Office',
|
|
end_location='Client Site',
|
|
distance_km=45.5,
|
|
rate_per_km=0.30,
|
|
vehicle_type='car'
|
|
)
|
|
|
|
db.session.add(mileage)
|
|
db.session.commit()
|
|
|
|
assert mileage.id is not None
|
|
assert mileage.purpose == 'Client meeting'
|
|
assert mileage.distance_km == Decimal('45.5')
|
|
assert mileage.rate_per_km == Decimal('0.30')
|
|
assert mileage.calculated_amount == Decimal('13.65')
|
|
assert mileage.status == 'pending'
|
|
|
|
|
|
def test_mileage_round_trip(client, user):
|
|
"""Test mileage calculation for round trip"""
|
|
mileage = Mileage(
|
|
user_id=user.id,
|
|
trip_date=date.today(),
|
|
purpose='Round trip',
|
|
start_location='A',
|
|
end_location='B',
|
|
distance_km=50,
|
|
rate_per_km=0.30,
|
|
is_round_trip=True
|
|
)
|
|
|
|
db.session.add(mileage)
|
|
db.session.commit()
|
|
|
|
# Check that total distance and amount are doubled
|
|
assert mileage.total_distance_km == 100.0
|
|
assert mileage.total_amount == 30.0 # 50 km * 2 * 0.30
|
|
|
|
|
|
def test_mileage_approval(client, user):
|
|
"""Test mileage approval workflow"""
|
|
admin = User(username='admin', email='admin@example.com', role='admin')
|
|
admin.set_password('admin123')
|
|
db.session.add(admin)
|
|
db.session.commit()
|
|
|
|
mileage = Mileage(
|
|
user_id=user.id,
|
|
trip_date=date.today(),
|
|
purpose='Test trip',
|
|
start_location='A',
|
|
end_location='B',
|
|
distance_km=30,
|
|
rate_per_km=0.30
|
|
)
|
|
|
|
db.session.add(mileage)
|
|
db.session.commit()
|
|
|
|
# Approve mileage
|
|
mileage.approve(admin.id, notes='Approved')
|
|
db.session.commit()
|
|
|
|
assert mileage.status == 'approved'
|
|
assert mileage.approved_by == admin.id
|
|
assert mileage.approved_at is not None
|
|
assert 'Approved' in mileage.notes
|
|
|
|
|
|
def test_mileage_rejection(client, user):
|
|
"""Test mileage rejection workflow"""
|
|
admin = User(username='admin', email='admin@example.com', role='admin')
|
|
admin.set_password('admin123')
|
|
db.session.add(admin)
|
|
db.session.commit()
|
|
|
|
mileage = Mileage(
|
|
user_id=user.id,
|
|
trip_date=date.today(),
|
|
purpose='Test trip',
|
|
start_location='A',
|
|
end_location='B',
|
|
distance_km=30,
|
|
rate_per_km=0.30
|
|
)
|
|
|
|
db.session.add(mileage)
|
|
db.session.commit()
|
|
|
|
# Reject mileage
|
|
mileage.reject(admin.id, reason='Missing documentation')
|
|
db.session.commit()
|
|
|
|
assert mileage.status == 'rejected'
|
|
assert mileage.approved_by == admin.id
|
|
assert mileage.rejection_reason == 'Missing documentation'
|
|
|
|
|
|
def test_mileage_create_expense(client, user, project):
|
|
"""Test creating expense from mileage entry"""
|
|
mileage = Mileage(
|
|
user_id=user.id,
|
|
trip_date=date.today(),
|
|
purpose='Client visit',
|
|
start_location='Office',
|
|
end_location='Client',
|
|
distance_km=40,
|
|
rate_per_km=0.30,
|
|
project_id=project.id,
|
|
is_round_trip=True
|
|
)
|
|
|
|
db.session.add(mileage)
|
|
db.session.commit()
|
|
|
|
# Create expense
|
|
expense = mileage.create_expense()
|
|
|
|
assert expense is not None
|
|
assert expense.user_id == user.id
|
|
assert expense.category == 'travel'
|
|
assert expense.amount == mileage.total_amount
|
|
assert expense.project_id == project.id
|
|
assert 'Distance' in expense.description
|
|
|
|
|
|
def test_mileage_to_dict(client, user):
|
|
"""Test converting mileage to dictionary"""
|
|
mileage = Mileage(
|
|
user_id=user.id,
|
|
trip_date=date.today(),
|
|
purpose='Test trip',
|
|
start_location='A',
|
|
end_location='B',
|
|
distance_km=25.5,
|
|
rate_per_km=0.30
|
|
)
|
|
|
|
db.session.add(mileage)
|
|
db.session.commit()
|
|
|
|
data = mileage.to_dict()
|
|
|
|
assert data['id'] == mileage.id
|
|
assert data['user_id'] == user.id
|
|
assert data['purpose'] == 'Test trip'
|
|
assert data['start_location'] == 'A'
|
|
assert data['end_location'] == 'B'
|
|
assert data['distance_km'] == 25.5
|
|
assert data['rate_per_km'] == 0.30
|
|
assert data['calculated_amount'] == 7.65
|
|
assert data['status'] == 'pending'
|
|
|
|
|
|
def test_get_total_distance(client, user):
|
|
"""Test getting total distance traveled"""
|
|
today = date.today()
|
|
|
|
# Create multiple mileage entries
|
|
mileage1 = Mileage(
|
|
user_id=user.id,
|
|
trip_date=today,
|
|
purpose='Trip 1',
|
|
start_location='A',
|
|
end_location='B',
|
|
distance_km=30,
|
|
rate_per_km=0.30,
|
|
status='approved'
|
|
)
|
|
|
|
mileage2 = Mileage(
|
|
user_id=user.id,
|
|
trip_date=today,
|
|
purpose='Trip 2',
|
|
start_location='C',
|
|
end_location='D',
|
|
distance_km=50,
|
|
rate_per_km=0.30,
|
|
status='approved'
|
|
)
|
|
|
|
db.session.add_all([mileage1, mileage2])
|
|
db.session.commit()
|
|
|
|
# Get total distance
|
|
total = Mileage.get_total_distance(user_id=user.id)
|
|
|
|
assert total == 80.0
|
|
|
|
|
|
def test_mileage_default_rates(client):
|
|
"""Test getting default mileage rates"""
|
|
rates = Mileage.get_default_rates()
|
|
|
|
assert 'car' in rates
|
|
assert 'motorcycle' in rates
|
|
assert 'van' in rates
|
|
assert 'truck' in rates
|
|
|
|
assert rates['car']['km'] == 0.30
|
|
assert rates['motorcycle']['km'] == 0.20
|
|
|
|
|
|
def test_mileage_reimbursement(client, user):
|
|
"""Test marking mileage as reimbursed"""
|
|
admin = User(username='admin', email='admin@example.com', role='admin')
|
|
admin.set_password('admin123')
|
|
db.session.add(admin)
|
|
db.session.commit()
|
|
|
|
mileage = Mileage(
|
|
user_id=user.id,
|
|
trip_date=date.today(),
|
|
purpose='Test trip',
|
|
start_location='A',
|
|
end_location='B',
|
|
distance_km=30,
|
|
rate_per_km=0.30,
|
|
status='approved'
|
|
)
|
|
|
|
db.session.add(mileage)
|
|
db.session.commit()
|
|
|
|
# Mark as reimbursed
|
|
mileage.mark_as_reimbursed()
|
|
db.session.commit()
|
|
|
|
assert mileage.status == 'reimbursed'
|
|
assert mileage.reimbursed is True
|
|
assert mileage.reimbursed_at is not None
|
|
|