mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-24 15:20:52 -05:00
90dde470da
- Normalize line endings from CRLF to LF across all files to match .editorconfig - Standardize quote style from single quotes to double quotes - Normalize whitespace and formatting throughout codebase - Apply consistent code style across 372 files including: * Application code (models, routes, services, utils) * Test files * Configuration files * CI/CD workflows This ensures consistency with the project's .editorconfig settings and improves code maintainability.
246 lines
7.9 KiB
Python
246 lines
7.9 KiB
Python
"""
|
|
Tests for invoice expense functionality
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import datetime, timedelta, date
|
|
from decimal import Decimal
|
|
from app import db
|
|
from app.models import Invoice, InvoiceItem, Expense, User, Project, Client
|
|
from factories import UserFactory, ClientFactory, ProjectFactory, InvoiceFactory, ExpenseFactory
|
|
|
|
|
|
@pytest.fixture
|
|
def test_user(app):
|
|
"""Create a test user"""
|
|
user = UserFactory(role="admin")
|
|
try:
|
|
user.set_password("testpass")
|
|
except Exception:
|
|
pass
|
|
yield user
|
|
db.session.delete(user)
|
|
db.session.commit()
|
|
|
|
|
|
@pytest.fixture
|
|
def test_client(app):
|
|
"""Create a test client"""
|
|
client = ClientFactory(name="Test Client", email="client@example.com")
|
|
yield client
|
|
db.session.delete(client)
|
|
db.session.commit()
|
|
|
|
|
|
@pytest.fixture
|
|
def test_project(app, test_client):
|
|
"""Create a test project"""
|
|
project = ProjectFactory(
|
|
name="Test Project", client_id=test_client.id, billable=True, hourly_rate=Decimal("100.00")
|
|
)
|
|
yield project
|
|
db.session.delete(project)
|
|
db.session.commit()
|
|
|
|
|
|
@pytest.fixture
|
|
def test_invoice(app, test_user, test_project, test_client):
|
|
"""Create a test invoice"""
|
|
invoice = InvoiceFactory(
|
|
project_id=test_project.id,
|
|
client_name=test_client.name,
|
|
client_id=test_client.id,
|
|
due_date=date.today() + timedelta(days=30),
|
|
created_by=test_user.id,
|
|
tax_rate=Decimal("10.00"),
|
|
)
|
|
yield invoice
|
|
db.session.delete(invoice)
|
|
db.session.commit()
|
|
|
|
|
|
@pytest.fixture
|
|
def test_expense(app, test_user, test_project):
|
|
"""Create a test expense"""
|
|
expense = ExpenseFactory(
|
|
user_id=test_user.id,
|
|
project_id=test_project.id,
|
|
title="Travel Expense",
|
|
description="Client meeting travel",
|
|
category="travel",
|
|
amount=Decimal("150.00"),
|
|
tax_amount=Decimal("15.00"),
|
|
expense_date=date.today(),
|
|
billable=True,
|
|
vendor="Taxi Service",
|
|
status="approved",
|
|
)
|
|
yield expense
|
|
db.session.delete(expense)
|
|
db.session.commit()
|
|
|
|
|
|
class TestInvoiceExpenseIntegration:
|
|
"""Test invoice expense integration"""
|
|
|
|
def test_link_expense_to_invoice(self, app, test_invoice, test_expense):
|
|
"""Test linking an expense to an invoice"""
|
|
# Mark expense as invoiced
|
|
test_expense.mark_as_invoiced(test_invoice.id)
|
|
db.session.commit()
|
|
|
|
# Verify the expense is linked
|
|
assert test_expense.invoiced is True
|
|
assert test_expense.invoice_id == test_invoice.id
|
|
assert test_expense.is_invoiced is True
|
|
|
|
# Verify the invoice has the expense
|
|
assert test_expense in test_invoice.expenses.all()
|
|
|
|
def test_unlink_expense_from_invoice(self, app, test_invoice, test_expense):
|
|
"""Test unlinking an expense from an invoice"""
|
|
# Mark expense as invoiced first
|
|
test_expense.mark_as_invoiced(test_invoice.id)
|
|
db.session.commit()
|
|
|
|
# Then unmark it
|
|
test_expense.unmark_as_invoiced()
|
|
db.session.commit()
|
|
|
|
# Verify the expense is unlinked
|
|
assert test_expense.invoiced is False
|
|
assert test_expense.invoice_id is None
|
|
assert test_expense.is_invoiced is False
|
|
|
|
def test_calculate_totals_with_expenses(self, app, test_invoice, test_expense):
|
|
"""Test that invoice totals include expenses"""
|
|
# Add an invoice item
|
|
from factories import InvoiceItemFactory
|
|
|
|
item = InvoiceItemFactory(
|
|
invoice_id=test_invoice.id,
|
|
description="Development Work",
|
|
quantity=Decimal("10.00"),
|
|
unit_price=Decimal("100.00"),
|
|
)
|
|
|
|
# Link expense to invoice
|
|
test_expense.mark_as_invoiced(test_invoice.id)
|
|
db.session.commit()
|
|
|
|
# Calculate totals
|
|
test_invoice.calculate_totals()
|
|
db.session.commit()
|
|
|
|
# Expected: 1000 (item) + 165 (expense with tax) = 1165
|
|
# Then apply 10% tax on subtotal: 1165 * 1.10 = 1281.50
|
|
expected_subtotal = Decimal("1165.00") # 1000 + 165
|
|
expected_tax = Decimal("116.50") # 1165 * 0.10
|
|
expected_total = Decimal("1281.50") # 1165 + 116.50
|
|
|
|
assert test_invoice.subtotal == expected_subtotal
|
|
assert test_invoice.tax_amount == expected_tax
|
|
assert test_invoice.total_amount == expected_total
|
|
|
|
def test_uninvoiced_expenses_query(self, app, test_expense, test_project):
|
|
"""Test querying for uninvoiced expenses"""
|
|
# The expense should be uninvoiced initially
|
|
uninvoiced = Expense.get_uninvoiced_expenses(project_id=test_project.id)
|
|
|
|
assert len(uninvoiced) > 0
|
|
assert test_expense in uninvoiced
|
|
|
|
def test_uninvoiced_expenses_excludes_invoiced(self, app, test_invoice, test_expense, test_project):
|
|
"""Test that invoiced expenses are excluded from uninvoiced query"""
|
|
# Mark expense as invoiced
|
|
test_expense.mark_as_invoiced(test_invoice.id)
|
|
db.session.commit()
|
|
|
|
# Query for uninvoiced expenses
|
|
uninvoiced = Expense.get_uninvoiced_expenses(project_id=test_project.id)
|
|
|
|
# The expense should not be in the list
|
|
assert test_expense not in uninvoiced
|
|
|
|
def test_expense_in_pdf_export(self, app, test_invoice, test_expense):
|
|
"""Test that expenses are included in PDF export (template test)"""
|
|
# Link expense to invoice
|
|
test_expense.mark_as_invoiced(test_invoice.id)
|
|
db.session.commit()
|
|
|
|
# Verify the expense is accessible from the invoice
|
|
expenses = test_invoice.expenses.all()
|
|
assert len(expenses) == 1
|
|
assert expenses[0].id == test_expense.id
|
|
|
|
def test_multiple_expenses_on_invoice(self, app, test_invoice, test_user, test_project):
|
|
"""Test that multiple expenses can be added to an invoice"""
|
|
# Create multiple expenses
|
|
expense1 = ExpenseFactory(
|
|
user_id=test_user.id,
|
|
project_id=test_project.id,
|
|
title="Travel Expense 1",
|
|
category="travel",
|
|
amount=Decimal("100.00"),
|
|
expense_date=date.today(),
|
|
billable=True,
|
|
status="approved",
|
|
)
|
|
expense2 = ExpenseFactory(
|
|
user_id=test_user.id,
|
|
project_id=test_project.id,
|
|
title="Meals Expense",
|
|
category="meals",
|
|
amount=Decimal("50.00"),
|
|
expense_date=date.today(),
|
|
billable=True,
|
|
status="approved",
|
|
)
|
|
|
|
# Link both to invoice
|
|
expense1.mark_as_invoiced(test_invoice.id)
|
|
expense2.mark_as_invoiced(test_invoice.id)
|
|
db.session.commit()
|
|
|
|
# Verify both are linked
|
|
expenses = test_invoice.expenses.all()
|
|
assert len(expenses) == 2
|
|
|
|
# Calculate totals
|
|
test_invoice.calculate_totals()
|
|
db.session.commit()
|
|
|
|
# Expected: 150 (expenses) * 1.10 (tax) = 165
|
|
expected_subtotal = Decimal("150.00")
|
|
expected_tax = Decimal("15.00")
|
|
expected_total = Decimal("165.00")
|
|
|
|
assert test_invoice.subtotal == expected_subtotal
|
|
assert test_invoice.tax_amount == expected_tax
|
|
assert test_invoice.total_amount == expected_total
|
|
|
|
# Cleanup
|
|
db.session.delete(expense1)
|
|
db.session.delete(expense2)
|
|
db.session.commit()
|
|
|
|
|
|
class TestExpenseProperties:
|
|
"""Test expense model properties"""
|
|
|
|
def test_expense_total_amount_includes_tax(self, app, test_expense):
|
|
"""Test that expense total_amount property includes tax"""
|
|
# Expense amount is 150, tax is 15
|
|
assert test_expense.total_amount == Decimal("165.00")
|
|
|
|
def test_expense_is_invoiced_property(self, app, test_invoice, test_expense):
|
|
"""Test the is_invoiced property"""
|
|
# Initially not invoiced
|
|
assert test_expense.is_invoiced is False
|
|
|
|
# After marking as invoiced
|
|
test_expense.mark_as_invoiced(test_invoice.id)
|
|
db.session.commit()
|
|
|
|
assert test_expense.is_invoiced is True
|