mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-02-10 22:28:43 -06:00
Implement a complete expense tracking feature that allows users to record, manage, approve, and track business expenses with full integration into existing project management and invoicing systems. Features: - Create and manage expenses with detailed information (amount, category, vendor, receipts, tax tracking) - Multi-currency support (EUR, USD, GBP, CHF) - Approval workflow with admin oversight (pending → approved → rejected) - Reimbursement tracking and status management - Billable expense flagging for client invoicing - Receipt file upload and attachment - Project and client association with auto-client selection - Tag-based organization and advanced filtering - CSV export functionality - Analytics dashboard with category breakdowns - API endpoints for programmatic access Database Changes: - Add expenses table with comprehensive schema - Create Alembic migration (029_add_expenses_table.py) - Add composite indexes for query performance - Implement proper foreign key constraints and cascading Routes & Templates: - Add expenses blueprint with 14 endpoints (CRUD, approval, export, API) - Create 4 responsive templates (list, form, view, dashboard) - Implement advanced filtering (status, category, project, client, date range) - Add permission-based access control (user vs admin) - Integrate receipt file upload handling User Experience: - Add "Expenses" to Insights navigation menu - Auto-populate client when project is selected - Provide visual feedback for auto-selections - Display summary statistics and analytics - Implement pagination and search functionality Testing & Documentation: - Add 40+ comprehensive tests covering models, methods, and workflows - Create complete user documentation (docs/EXPENSE_TRACKING.md) - Add API documentation and examples - Include troubleshooting guide and best practices Integration: - Link expenses to projects for cost tracking - Associate with clients for billing purposes - Connect billable expenses to invoicing system - Add PostHog event tracking for analytics - Implement structured logging for audit trail Security: - Role-based access control (users see only their expenses) - Admin-only approval and reimbursement actions - CSRF protection and file upload validation - Proper permission checks on all operations This implementation follows existing codebase patterns and includes full test coverage, documentation, and database migrations per project standards.
717 lines
24 KiB
Python
717 lines
24 KiB
Python
"""
|
|
Comprehensive tests for Expense model and related functionality.
|
|
|
|
This module tests:
|
|
- Expense model creation and validation
|
|
- Relationships with User, Project, Client, and Invoice models
|
|
- Query methods (get_expenses, get_total_expenses, etc.)
|
|
- Approval and reimbursement workflows
|
|
- Data integrity and constraints
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import date, datetime, timedelta
|
|
from decimal import Decimal
|
|
from app import create_app, db
|
|
from app.models import User, Project, Client, Invoice, Expense
|
|
|
|
|
|
@pytest.fixture
|
|
def app():
|
|
"""Create and configure a test application instance."""
|
|
app = create_app({
|
|
'TESTING': True,
|
|
'SQLALCHEMY_DATABASE_URI': 'sqlite:///:memory:',
|
|
'WTF_CSRF_ENABLED': False
|
|
})
|
|
|
|
with app.app_context():
|
|
db.create_all()
|
|
yield app
|
|
db.session.remove()
|
|
db.drop_all()
|
|
|
|
|
|
@pytest.fixture
|
|
def client_fixture(app):
|
|
"""Create a test Flask client."""
|
|
return app.test_client()
|
|
|
|
|
|
@pytest.fixture
|
|
def test_user(app):
|
|
"""Create a test user."""
|
|
with app.app_context():
|
|
user = User(username='testuser', role='user')
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
return user.id
|
|
|
|
|
|
@pytest.fixture
|
|
def test_admin(app):
|
|
"""Create a test admin user."""
|
|
with app.app_context():
|
|
admin = User(username='admin', role='admin')
|
|
db.session.add(admin)
|
|
db.session.commit()
|
|
return admin.id
|
|
|
|
|
|
@pytest.fixture
|
|
def test_client(app):
|
|
"""Create a test client."""
|
|
with app.app_context():
|
|
client = Client(name='Test Client', description='A test client')
|
|
db.session.add(client)
|
|
db.session.commit()
|
|
return client.id
|
|
|
|
|
|
@pytest.fixture
|
|
def test_project(app, test_client):
|
|
"""Create a test project."""
|
|
with app.app_context():
|
|
project = Project(
|
|
name='Test Project',
|
|
client_id=test_client,
|
|
description='A test project',
|
|
billable=True,
|
|
hourly_rate=Decimal('100.00')
|
|
)
|
|
db.session.add(project)
|
|
db.session.commit()
|
|
return project.id
|
|
|
|
|
|
@pytest.fixture
|
|
def test_invoice(app, test_client, test_project, test_user):
|
|
"""Create a test invoice."""
|
|
with app.app_context():
|
|
client = db.session.get(Client, test_client)
|
|
invoice = Invoice(
|
|
invoice_number='INV-TEST-001',
|
|
project_id=test_project,
|
|
client_name=client.name,
|
|
due_date=date.today() + timedelta(days=30),
|
|
created_by=test_user,
|
|
client_id=test_client,
|
|
issue_date=date.today(),
|
|
status='draft'
|
|
)
|
|
db.session.add(invoice)
|
|
db.session.commit()
|
|
return invoice.id
|
|
|
|
|
|
# Model Tests
|
|
|
|
class TestExpenseModel:
|
|
"""Test Expense model creation, validation, and basic operations."""
|
|
|
|
def test_create_expense(self, app, test_user):
|
|
"""Test creating a basic expense."""
|
|
with app.app_context():
|
|
expense = Expense(
|
|
user_id=test_user,
|
|
title='Travel Expense',
|
|
category='travel',
|
|
amount=Decimal('150.00'),
|
|
expense_date=date.today()
|
|
)
|
|
db.session.add(expense)
|
|
db.session.commit()
|
|
|
|
assert expense.id is not None
|
|
assert expense.title == 'Travel Expense'
|
|
assert expense.category == 'travel'
|
|
assert expense.amount == Decimal('150.00')
|
|
assert expense.currency_code == 'EUR'
|
|
assert expense.status == 'pending'
|
|
assert expense.billable is False
|
|
assert expense.reimbursable is True
|
|
|
|
def test_create_expense_with_all_fields(self, app, test_user, test_project, test_client):
|
|
"""Test creating an expense with all optional fields."""
|
|
with app.app_context():
|
|
expense = Expense(
|
|
user_id=test_user,
|
|
title='Conference Travel',
|
|
category='travel',
|
|
amount=Decimal('500.00'),
|
|
expense_date=date.today(),
|
|
description='Flight and hotel for conference',
|
|
project_id=test_project,
|
|
client_id=test_client,
|
|
currency_code='USD',
|
|
tax_amount=Decimal('50.00'),
|
|
payment_method='credit_card',
|
|
payment_date=date.today(),
|
|
vendor='Airline Inc',
|
|
receipt_number='REC-2024-001',
|
|
notes='Business class flight',
|
|
tags='conference,travel,urgent',
|
|
billable=True,
|
|
reimbursable=True
|
|
)
|
|
db.session.add(expense)
|
|
db.session.commit()
|
|
|
|
assert expense.description == 'Flight and hotel for conference'
|
|
assert expense.project_id == test_project
|
|
assert expense.client_id == test_client
|
|
assert expense.currency_code == 'USD'
|
|
assert expense.tax_amount == Decimal('50.00')
|
|
assert expense.vendor == 'Airline Inc'
|
|
assert expense.billable is True
|
|
|
|
def test_expense_str_representation(self, app, test_user):
|
|
"""Test __repr__ method."""
|
|
with app.app_context():
|
|
expense = Expense(
|
|
user_id=test_user,
|
|
title='Office Supplies',
|
|
category='supplies',
|
|
amount=Decimal('75.50'),
|
|
expense_date=date.today()
|
|
)
|
|
db.session.add(expense)
|
|
db.session.commit()
|
|
|
|
assert 'Office Supplies' in str(expense)
|
|
assert 'EUR' in str(expense)
|
|
|
|
def test_expense_timestamps(self, app, test_user):
|
|
"""Test automatic timestamp creation."""
|
|
with app.app_context():
|
|
expense = Expense(
|
|
user_id=test_user,
|
|
title='Test Expense',
|
|
category='other',
|
|
amount=Decimal('10.00'),
|
|
expense_date=date.today()
|
|
)
|
|
db.session.add(expense)
|
|
db.session.commit()
|
|
|
|
assert expense.created_at is not None
|
|
assert expense.updated_at is not None
|
|
assert isinstance(expense.created_at, datetime)
|
|
assert isinstance(expense.updated_at, datetime)
|
|
|
|
|
|
class TestExpenseProperties:
|
|
"""Test Expense computed properties."""
|
|
|
|
def test_total_amount_property(self, app, test_user):
|
|
"""Test total_amount property including tax."""
|
|
with app.app_context():
|
|
expense = Expense(
|
|
user_id=test_user,
|
|
title='Test Expense',
|
|
category='travel',
|
|
amount=Decimal('100.00'),
|
|
tax_amount=Decimal('10.00'),
|
|
expense_date=date.today()
|
|
)
|
|
db.session.add(expense)
|
|
db.session.commit()
|
|
|
|
assert expense.total_amount == Decimal('110.00')
|
|
|
|
def test_tag_list_property(self, app, test_user):
|
|
"""Test tag_list property parsing."""
|
|
with app.app_context():
|
|
expense = Expense(
|
|
user_id=test_user,
|
|
title='Test Expense',
|
|
category='travel',
|
|
amount=Decimal('100.00'),
|
|
expense_date=date.today(),
|
|
tags='urgent, client-meeting, conference'
|
|
)
|
|
db.session.add(expense)
|
|
db.session.commit()
|
|
|
|
tags = expense.tag_list
|
|
assert len(tags) == 3
|
|
assert 'urgent' in tags
|
|
assert 'client-meeting' in tags
|
|
assert 'conference' in tags
|
|
|
|
def test_is_approved_property(self, app, test_user, test_admin):
|
|
"""Test is_approved property."""
|
|
with app.app_context():
|
|
expense = Expense(
|
|
user_id=test_user,
|
|
title='Test Expense',
|
|
category='travel',
|
|
amount=Decimal('100.00'),
|
|
expense_date=date.today()
|
|
)
|
|
db.session.add(expense)
|
|
db.session.commit()
|
|
|
|
# Initially not approved
|
|
assert expense.is_approved is False
|
|
|
|
# Approve
|
|
expense.approve(test_admin)
|
|
db.session.commit()
|
|
|
|
assert expense.is_approved is True
|
|
|
|
def test_is_reimbursed_property(self, app, test_user):
|
|
"""Test is_reimbursed property."""
|
|
with app.app_context():
|
|
expense = Expense(
|
|
user_id=test_user,
|
|
title='Test Expense',
|
|
category='travel',
|
|
amount=Decimal('100.00'),
|
|
expense_date=date.today()
|
|
)
|
|
db.session.add(expense)
|
|
db.session.commit()
|
|
|
|
assert expense.is_reimbursed is False
|
|
|
|
expense.mark_as_reimbursed()
|
|
db.session.commit()
|
|
|
|
assert expense.is_reimbursed is True
|
|
|
|
|
|
class TestExpenseRelationships:
|
|
"""Test Expense relationships with other models."""
|
|
|
|
def test_user_relationship(self, app, test_user):
|
|
"""Test relationship with User model."""
|
|
with app.app_context():
|
|
expense = Expense(
|
|
user_id=test_user,
|
|
title='Test Expense',
|
|
category='travel',
|
|
amount=Decimal('100.00'),
|
|
expense_date=date.today()
|
|
)
|
|
db.session.add(expense)
|
|
db.session.commit()
|
|
|
|
expense = db.session.get(Expense, expense.id)
|
|
user = db.session.get(User, test_user)
|
|
|
|
assert expense.user is not None
|
|
assert expense.user.id == test_user
|
|
assert expense in user.expenses.all()
|
|
|
|
def test_project_relationship(self, app, test_user, test_project):
|
|
"""Test relationship with Project model."""
|
|
with app.app_context():
|
|
expense = Expense(
|
|
user_id=test_user,
|
|
title='Test Expense',
|
|
category='travel',
|
|
amount=Decimal('100.00'),
|
|
expense_date=date.today(),
|
|
project_id=test_project
|
|
)
|
|
db.session.add(expense)
|
|
db.session.commit()
|
|
|
|
expense = db.session.get(Expense, expense.id)
|
|
project = db.session.get(Project, test_project)
|
|
|
|
assert expense.project is not None
|
|
assert expense.project.id == test_project
|
|
assert expense in project.expenses.all()
|
|
|
|
def test_client_relationship(self, app, test_user, test_client):
|
|
"""Test relationship with Client model."""
|
|
with app.app_context():
|
|
expense = Expense(
|
|
user_id=test_user,
|
|
title='Test Expense',
|
|
category='travel',
|
|
amount=Decimal('100.00'),
|
|
expense_date=date.today(),
|
|
client_id=test_client
|
|
)
|
|
db.session.add(expense)
|
|
db.session.commit()
|
|
|
|
expense = db.session.get(Expense, expense.id)
|
|
client = db.session.get(Client, test_client)
|
|
|
|
assert expense.client is not None
|
|
assert expense.client.id == test_client
|
|
assert expense in client.expenses.all()
|
|
|
|
|
|
class TestExpenseMethods:
|
|
"""Test Expense instance and class methods."""
|
|
|
|
def test_approve_method(self, app, test_user, test_admin):
|
|
"""Test approving an expense."""
|
|
with app.app_context():
|
|
expense = Expense(
|
|
user_id=test_user,
|
|
title='Test Expense',
|
|
category='travel',
|
|
amount=Decimal('100.00'),
|
|
expense_date=date.today()
|
|
)
|
|
db.session.add(expense)
|
|
db.session.commit()
|
|
|
|
expense.approve(test_admin, notes='Approved for reimbursement')
|
|
db.session.commit()
|
|
|
|
assert expense.status == 'approved'
|
|
assert expense.approved_by == test_admin
|
|
assert expense.approved_at is not None
|
|
|
|
def test_reject_method(self, app, test_user, test_admin):
|
|
"""Test rejecting an expense."""
|
|
with app.app_context():
|
|
expense = Expense(
|
|
user_id=test_user,
|
|
title='Test Expense',
|
|
category='travel',
|
|
amount=Decimal('100.00'),
|
|
expense_date=date.today()
|
|
)
|
|
db.session.add(expense)
|
|
db.session.commit()
|
|
|
|
expense.reject(test_admin, 'Receipt not provided')
|
|
db.session.commit()
|
|
|
|
assert expense.status == 'rejected'
|
|
assert expense.approved_by == test_admin
|
|
assert expense.rejection_reason == 'Receipt not provided'
|
|
|
|
def test_mark_as_reimbursed(self, app, test_user, test_admin):
|
|
"""Test marking expense as reimbursed."""
|
|
with app.app_context():
|
|
expense = Expense(
|
|
user_id=test_user,
|
|
title='Test Expense',
|
|
category='travel',
|
|
amount=Decimal('100.00'),
|
|
expense_date=date.today()
|
|
)
|
|
db.session.add(expense)
|
|
db.session.commit()
|
|
|
|
# Approve first
|
|
expense.approve(test_admin)
|
|
db.session.commit()
|
|
|
|
# Mark as reimbursed
|
|
expense.mark_as_reimbursed()
|
|
db.session.commit()
|
|
|
|
assert expense.reimbursed is True
|
|
assert expense.reimbursed_at is not None
|
|
assert expense.status == 'reimbursed'
|
|
|
|
def test_mark_as_invoiced(self, app, test_user, test_invoice):
|
|
"""Test marking expense as invoiced."""
|
|
with app.app_context():
|
|
expense = Expense(
|
|
user_id=test_user,
|
|
title='Test Expense',
|
|
category='travel',
|
|
amount=Decimal('100.00'),
|
|
expense_date=date.today(),
|
|
billable=True
|
|
)
|
|
db.session.add(expense)
|
|
db.session.commit()
|
|
|
|
expense.mark_as_invoiced(test_invoice)
|
|
db.session.commit()
|
|
|
|
assert expense.invoiced is True
|
|
assert expense.invoice_id == test_invoice
|
|
|
|
def test_to_dict(self, app, test_user):
|
|
"""Test converting expense to dictionary."""
|
|
with app.app_context():
|
|
expense = Expense(
|
|
user_id=test_user,
|
|
title='Test Expense',
|
|
category='travel',
|
|
amount=Decimal('100.00'),
|
|
tax_amount=Decimal('10.00'),
|
|
expense_date=date.today(),
|
|
description='Test description'
|
|
)
|
|
db.session.add(expense)
|
|
db.session.commit()
|
|
|
|
expense = db.session.get(Expense, expense.id)
|
|
expense_dict = expense.to_dict()
|
|
|
|
assert expense_dict['id'] == expense.id
|
|
assert expense_dict['user_id'] == test_user
|
|
assert expense_dict['title'] == 'Test Expense'
|
|
assert expense_dict['category'] == 'travel'
|
|
assert expense_dict['amount'] == 100.00
|
|
assert expense_dict['tax_amount'] == 10.00
|
|
assert expense_dict['total_amount'] == 110.00
|
|
assert 'created_at' in expense_dict
|
|
|
|
|
|
class TestExpenseQueries:
|
|
"""Test Expense query class methods."""
|
|
|
|
def test_get_expenses(self, app, test_user):
|
|
"""Test retrieving expenses."""
|
|
with app.app_context():
|
|
expenses = [
|
|
Expense(
|
|
user_id=test_user,
|
|
title=f'Expense {i}',
|
|
category='travel',
|
|
amount=Decimal(f'{100 + i * 10}.00'),
|
|
expense_date=date.today() - timedelta(days=i)
|
|
)
|
|
for i in range(5)
|
|
]
|
|
db.session.add_all(expenses)
|
|
db.session.commit()
|
|
|
|
retrieved = Expense.get_expenses(user_id=test_user)
|
|
assert len(retrieved) == 5
|
|
|
|
# Should be ordered by expense_date desc
|
|
assert retrieved[0].title == 'Expense 0'
|
|
|
|
def test_get_expenses_by_status(self, app, test_user, test_admin):
|
|
"""Test filtering expenses by status."""
|
|
with app.app_context():
|
|
# Create expenses with different statuses
|
|
exp1 = Expense(
|
|
user_id=test_user,
|
|
title='Pending Expense',
|
|
category='travel',
|
|
amount=Decimal('100.00'),
|
|
expense_date=date.today()
|
|
)
|
|
exp2 = Expense(
|
|
user_id=test_user,
|
|
title='Approved Expense',
|
|
category='travel',
|
|
amount=Decimal('200.00'),
|
|
expense_date=date.today()
|
|
)
|
|
db.session.add_all([exp1, exp2])
|
|
db.session.commit()
|
|
|
|
exp2.approve(test_admin)
|
|
db.session.commit()
|
|
|
|
pending = Expense.get_expenses(user_id=test_user, status='pending')
|
|
assert len(pending) == 1
|
|
assert pending[0].title == 'Pending Expense'
|
|
|
|
approved = Expense.get_expenses(user_id=test_user, status='approved')
|
|
assert len(approved) == 1
|
|
assert approved[0].title == 'Approved Expense'
|
|
|
|
def test_get_total_expenses(self, app, test_user):
|
|
"""Test calculating total expenses."""
|
|
with app.app_context():
|
|
amounts = [Decimal('100.00'), Decimal('250.50'), Decimal('75.25')]
|
|
taxes = [Decimal('10.00'), Decimal('25.00'), Decimal('7.50')]
|
|
|
|
expenses = [
|
|
Expense(
|
|
user_id=test_user,
|
|
title=f'Expense {i}',
|
|
category='travel',
|
|
amount=amount,
|
|
tax_amount=tax,
|
|
expense_date=date.today()
|
|
)
|
|
for i, (amount, tax) in enumerate(zip(amounts, taxes))
|
|
]
|
|
db.session.add_all(expenses)
|
|
db.session.commit()
|
|
|
|
total = Expense.get_total_expenses(user_id=test_user, include_tax=True)
|
|
expected = sum(amounts) + sum(taxes)
|
|
assert abs(total - float(expected)) < 0.01
|
|
|
|
def test_get_expenses_by_category(self, app, test_user):
|
|
"""Test grouping expenses by category."""
|
|
with app.app_context():
|
|
categories = ['travel', 'travel', 'meals', 'supplies', 'meals']
|
|
amounts = [Decimal('100.00'), Decimal('150.00'), Decimal('50.00'),
|
|
Decimal('75.00'), Decimal('60.00')]
|
|
|
|
expenses = [
|
|
Expense(
|
|
user_id=test_user,
|
|
title=f'Expense {i}',
|
|
category=category,
|
|
amount=amount,
|
|
expense_date=date.today()
|
|
)
|
|
for i, (category, amount) in enumerate(zip(categories, amounts))
|
|
]
|
|
db.session.add_all(expenses)
|
|
db.session.commit()
|
|
|
|
by_category = Expense.get_expenses_by_category(user_id=test_user)
|
|
|
|
assert len(by_category) == 3
|
|
|
|
travel = next(c for c in by_category if c['category'] == 'travel')
|
|
assert travel['count'] == 2
|
|
assert abs(travel['total_amount'] - 250.00) < 0.01
|
|
|
|
def test_get_pending_approvals(self, app, test_user):
|
|
"""Test retrieving pending expenses."""
|
|
with app.app_context():
|
|
exp1 = Expense(
|
|
user_id=test_user,
|
|
title='Pending 1',
|
|
category='travel',
|
|
amount=Decimal('100.00'),
|
|
expense_date=date.today(),
|
|
status='pending'
|
|
)
|
|
exp2 = Expense(
|
|
user_id=test_user,
|
|
title='Pending 2',
|
|
category='travel',
|
|
amount=Decimal('200.00'),
|
|
expense_date=date.today(),
|
|
status='pending'
|
|
)
|
|
db.session.add_all([exp1, exp2])
|
|
db.session.commit()
|
|
|
|
pending = Expense.get_pending_approvals(user_id=test_user)
|
|
assert len(pending) == 2
|
|
|
|
def test_get_uninvoiced_expenses(self, app, test_user, test_admin, test_project):
|
|
"""Test retrieving uninvoiced billable expenses."""
|
|
with app.app_context():
|
|
exp1 = Expense(
|
|
user_id=test_user,
|
|
title='Billable Expense',
|
|
category='travel',
|
|
amount=Decimal('100.00'),
|
|
expense_date=date.today(),
|
|
billable=True,
|
|
project_id=test_project
|
|
)
|
|
exp2 = Expense(
|
|
user_id=test_user,
|
|
title='Non-billable Expense',
|
|
category='travel',
|
|
amount=Decimal('200.00'),
|
|
expense_date=date.today(),
|
|
billable=False,
|
|
project_id=test_project
|
|
)
|
|
db.session.add_all([exp1, exp2])
|
|
db.session.commit()
|
|
|
|
# Approve both
|
|
exp1.approve(test_admin)
|
|
exp2.approve(test_admin)
|
|
db.session.commit()
|
|
|
|
uninvoiced = Expense.get_uninvoiced_expenses(project_id=test_project)
|
|
assert len(uninvoiced) == 1
|
|
assert uninvoiced[0].title == 'Billable Expense'
|
|
|
|
|
|
class TestExpenseConstraints:
|
|
"""Test database constraints and data integrity."""
|
|
|
|
def test_cannot_create_expense_without_user(self, app):
|
|
"""Test that user_id is required."""
|
|
with app.app_context():
|
|
expense = Expense(
|
|
user_id=None,
|
|
title='Test Expense',
|
|
category='travel',
|
|
amount=Decimal('100.00'),
|
|
expense_date=date.today()
|
|
)
|
|
db.session.add(expense)
|
|
|
|
with pytest.raises(Exception):
|
|
db.session.commit()
|
|
|
|
db.session.rollback()
|
|
|
|
|
|
# Smoke Tests
|
|
|
|
class TestExpenseSmokeTests:
|
|
"""Basic smoke tests to ensure Expense functionality works."""
|
|
|
|
def test_expense_creation_smoke(self, app, test_user):
|
|
"""Smoke test: Can we create an expense?"""
|
|
with app.app_context():
|
|
expense = Expense(
|
|
user_id=test_user,
|
|
title='Smoke Test Expense',
|
|
category='travel',
|
|
amount=Decimal('99.99'),
|
|
expense_date=date.today()
|
|
)
|
|
db.session.add(expense)
|
|
db.session.commit()
|
|
|
|
assert expense.id is not None
|
|
|
|
def test_expense_query_smoke(self, app, test_user):
|
|
"""Smoke test: Can we query expenses?"""
|
|
with app.app_context():
|
|
expense = Expense(
|
|
user_id=test_user,
|
|
title='Query Smoke Test',
|
|
category='travel',
|
|
amount=Decimal('200.00'),
|
|
expense_date=date.today()
|
|
)
|
|
db.session.add(expense)
|
|
db.session.commit()
|
|
|
|
expenses = Expense.query.filter_by(user_id=test_user).all()
|
|
assert len(expenses) > 0
|
|
|
|
def test_expense_workflow_smoke(self, app, test_user, test_admin):
|
|
"""Smoke test: Does the full approval workflow work?"""
|
|
with app.app_context():
|
|
# Create expense
|
|
expense = Expense(
|
|
user_id=test_user,
|
|
title='Workflow Test',
|
|
category='travel',
|
|
amount=Decimal('500.00'),
|
|
expense_date=date.today(),
|
|
reimbursable=True
|
|
)
|
|
db.session.add(expense)
|
|
db.session.commit()
|
|
|
|
# Approve
|
|
expense.approve(test_admin)
|
|
db.session.commit()
|
|
assert expense.status == 'approved'
|
|
|
|
# Reimburse
|
|
expense.mark_as_reimbursed()
|
|
db.session.commit()
|
|
assert expense.status == 'reimbursed'
|
|
|