mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2025-12-30 15:49:44 -06:00
Merge pull request #158 from DRYTRIX/153-invoices-display-currency-as-eur-and-not-usd
fix: Invoice currency displays EUR instead of selected currency from …
This commit is contained in:
@@ -101,6 +101,10 @@ def create_invoice():
|
||||
"has_tax": tax_rate > 0
|
||||
})
|
||||
|
||||
# Get currency from settings
|
||||
settings = Settings.get_settings()
|
||||
currency_code = settings.currency if settings else 'USD'
|
||||
|
||||
# Create invoice
|
||||
invoice = Invoice(
|
||||
invoice_number=invoice_number,
|
||||
@@ -113,7 +117,8 @@ def create_invoice():
|
||||
client_address=client_address,
|
||||
tax_rate=tax_rate,
|
||||
notes=notes,
|
||||
terms=terms
|
||||
terms=terms,
|
||||
currency_code=currency_code
|
||||
)
|
||||
|
||||
db.session.add(invoice)
|
||||
@@ -643,7 +648,8 @@ def duplicate_invoice(invoice_id):
|
||||
client_id=original_invoice.client_id,
|
||||
tax_rate=original_invoice.tax_rate,
|
||||
notes=original_invoice.notes,
|
||||
terms=original_invoice.terms
|
||||
terms=original_invoice.terms,
|
||||
currency_code=original_invoice.currency_code
|
||||
)
|
||||
|
||||
db.session.add(new_invoice)
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<td class="p-2">{{ invoice.invoice_number }}</td>
|
||||
<td class="p-2">{{ invoice.client_name }}</td>
|
||||
<td class="p-2">{{ invoice.status }}</td>
|
||||
<td class="p-2">{{ "%.2f"|format(invoice.total_amount) }}</td>
|
||||
<td class="p-2">{{ "%.2f"|format(invoice.total_amount) }} {{ invoice.currency_code }}</td>
|
||||
<td class="p-2">
|
||||
<a href="{{ url_for('invoices.view_invoice', invoice_id=invoice.id) }}" class="text-primary">View</a>
|
||||
</td>
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
<tr class="border-b border-border-light dark:border-border-dark">
|
||||
<td class="p-2">{{ item.description }}</td>
|
||||
<td class="p-2">{{ "%.2f"|format(item.quantity) }}</td>
|
||||
<td class="p-2">{{ "%.2f"|format(item.unit_price) }}</td>
|
||||
<td class="p-2">{{ "%.2f"|format(item.total_amount) }}</td>
|
||||
<td class="p-2">{{ "%.2f"|format(item.unit_price) }} {{ invoice.currency_code }}</td>
|
||||
<td class="p-2">{{ "%.2f"|format(item.total_amount) }} {{ invoice.currency_code }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -71,8 +71,8 @@
|
||||
<td class="p-2">{{ good.description or '-' }}</td>
|
||||
<td class="p-2">{{ good.category|capitalize }}</td>
|
||||
<td class="p-2">{{ "%.2f"|format(good.quantity) }}</td>
|
||||
<td class="p-2">{{ "%.2f"|format(good.unit_price) }}</td>
|
||||
<td class="p-2">{{ "%.2f"|format(good.total_amount) }}</td>
|
||||
<td class="p-2">{{ "%.2f"|format(good.unit_price) }} {{ invoice.currency_code }}</td>
|
||||
<td class="p-2">{{ "%.2f"|format(good.total_amount) }} {{ invoice.currency_code }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -84,15 +84,15 @@
|
||||
<div class="w-full md:w-1/3">
|
||||
<div class="flex justify-between">
|
||||
<span>Subtotal</span>
|
||||
<span>{{ "%.2f"|format(invoice.subtotal) }}</span>
|
||||
<span>{{ "%.2f"|format(invoice.subtotal) }} {{ invoice.currency_code }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>Tax ({{ "%.2f"|format(invoice.tax_rate) }}%)</span>
|
||||
<span>{{ "%.2f"|format(invoice.tax_amount) }}</span>
|
||||
<span>{{ "%.2f"|format(invoice.tax_amount) }} {{ invoice.currency_code }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between font-bold text-lg">
|
||||
<span>Total</span>
|
||||
<span>{{ "%.2f"|format(invoice.total_amount) }}</span>
|
||||
<span>{{ "%.2f"|format(invoice.total_amount) }} {{ invoice.currency_code }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
67
migrations/fix_invoice_currency.py
Normal file
67
migrations/fix_invoice_currency.py
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration script to fix invoice currency codes.
|
||||
Updates all invoices to use the currency from Settings instead of hard-coded EUR.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add parent directory to path to import app
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app import create_app, db
|
||||
from app.models import Invoice, Settings
|
||||
|
||||
def fix_invoice_currencies():
|
||||
"""Update all invoices to use currency from Settings"""
|
||||
app = create_app()
|
||||
|
||||
with app.app_context():
|
||||
# Get the currency from settings
|
||||
settings = Settings.get_settings()
|
||||
target_currency = settings.currency if settings else 'USD'
|
||||
|
||||
print(f"Target currency from settings: {target_currency}")
|
||||
|
||||
# Get all invoices
|
||||
invoices = Invoice.query.all()
|
||||
|
||||
if not invoices:
|
||||
print("No invoices found in database.")
|
||||
return
|
||||
|
||||
print(f"Found {len(invoices)} invoices to process.")
|
||||
|
||||
# Update each invoice that doesn't match the target currency
|
||||
updated_count = 0
|
||||
for invoice in invoices:
|
||||
if invoice.currency_code != target_currency:
|
||||
print(f"Updating invoice {invoice.invoice_number}: {invoice.currency_code} -> {target_currency}")
|
||||
invoice.currency_code = target_currency
|
||||
updated_count += 1
|
||||
|
||||
if updated_count > 0:
|
||||
try:
|
||||
db.session.commit()
|
||||
print(f"\nSuccessfully updated {updated_count} invoice(s) to use {target_currency}.")
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"Error updating invoices: {e}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"\nAll invoices already using {target_currency}. No updates needed.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("=" * 60)
|
||||
print("Invoice Currency Migration")
|
||||
print("=" * 60)
|
||||
print("\nThis script will update all invoices to use the currency")
|
||||
print("configured in Settings instead of the hard-coded default.\n")
|
||||
|
||||
response = input("Do you want to proceed? (yes/no): ").strip().lower()
|
||||
if response in ['yes', 'y']:
|
||||
fix_invoice_currencies()
|
||||
else:
|
||||
print("Migration cancelled.")
|
||||
|
||||
280
tests/test_invoice_currency_fix.py
Normal file
280
tests/test_invoice_currency_fix.py
Normal file
@@ -0,0 +1,280 @@
|
||||
"""
|
||||
Test suite for invoice currency fix
|
||||
Tests that invoices use the currency from Settings instead of hard-coded EUR
|
||||
"""
|
||||
import pytest
|
||||
import os
|
||||
from datetime import datetime, timedelta, date
|
||||
from decimal import Decimal
|
||||
from app import create_app, db
|
||||
from app.models import User, Project, Client, Invoice, InvoiceItem, Settings
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
"""Create and configure a test app instance"""
|
||||
# Set test database URL before creating app
|
||||
os.environ['DATABASE_URL'] = 'sqlite:///:memory:'
|
||||
|
||||
app = create_app()
|
||||
app.config['TESTING'] = True
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
|
||||
app.config['WTF_CSRF_ENABLED'] = False
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
# Create test settings with USD currency
|
||||
settings = Settings(currency='USD')
|
||||
db.session.add(settings)
|
||||
db.session.commit()
|
||||
|
||||
yield app
|
||||
|
||||
db.session.remove()
|
||||
db.drop_all()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client_fixture(app):
|
||||
"""Create test client"""
|
||||
return app.test_client()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_user(app):
|
||||
"""Create a test user"""
|
||||
with app.app_context():
|
||||
user = User(username='testuser', email='test@example.com', is_admin=True)
|
||||
user.set_password('password123')
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_client_model(app, test_user):
|
||||
"""Create a test client"""
|
||||
with app.app_context():
|
||||
client = Client(
|
||||
name='Test Client',
|
||||
email='client@example.com',
|
||||
created_by=test_user.id
|
||||
)
|
||||
db.session.add(client)
|
||||
db.session.commit()
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_project(app, test_user, test_client_model):
|
||||
"""Create a test project"""
|
||||
with app.app_context():
|
||||
project = Project(
|
||||
name='Test Project',
|
||||
client_id=test_client_model.id,
|
||||
created_by=test_user.id,
|
||||
billable=True,
|
||||
hourly_rate=Decimal('100.00'),
|
||||
status='active'
|
||||
)
|
||||
db.session.add(project)
|
||||
db.session.commit()
|
||||
return project
|
||||
|
||||
|
||||
class TestInvoiceCurrencyFix:
|
||||
"""Test that invoices use correct currency from Settings"""
|
||||
|
||||
def test_new_invoice_uses_settings_currency(self, app, test_user, test_project, test_client_model):
|
||||
"""Test that a new invoice uses the currency from Settings"""
|
||||
with app.app_context():
|
||||
# Get settings - should have USD currency
|
||||
settings = Settings.get_settings()
|
||||
assert settings.currency == 'USD'
|
||||
|
||||
# Create invoice via model (simulating route behavior)
|
||||
invoice = Invoice(
|
||||
invoice_number='TEST-001',
|
||||
project_id=test_project.id,
|
||||
client_name=test_client_model.name,
|
||||
due_date=date.today() + timedelta(days=30),
|
||||
created_by=test_user.id,
|
||||
client_id=test_client_model.id,
|
||||
currency_code=settings.currency
|
||||
)
|
||||
db.session.add(invoice)
|
||||
db.session.commit()
|
||||
|
||||
# Verify invoice has USD currency
|
||||
assert invoice.currency_code == 'USD'
|
||||
|
||||
def test_invoice_creation_via_route(self, app, client_fixture, test_user, test_project, test_client_model):
|
||||
"""Test that invoice creation via route uses correct currency"""
|
||||
with app.app_context():
|
||||
# Login
|
||||
client_fixture.post('/login', data={
|
||||
'username': 'testuser',
|
||||
'password': 'password123'
|
||||
}, follow_redirects=True)
|
||||
|
||||
# Create invoice via route
|
||||
response = client_fixture.post('/invoices/create', data={
|
||||
'project_id': test_project.id,
|
||||
'client_name': test_client_model.name,
|
||||
'client_email': test_client_model.email,
|
||||
'due_date': (date.today() + timedelta(days=30)).strftime('%Y-%m-%d'),
|
||||
'tax_rate': '0'
|
||||
}, follow_redirects=True)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
# Get the created invoice
|
||||
invoice = Invoice.query.first()
|
||||
assert invoice is not None
|
||||
assert invoice.currency_code == 'USD'
|
||||
|
||||
def test_invoice_with_different_currency_setting(self, app, test_user, test_project, test_client_model):
|
||||
"""Test invoice creation with different currency settings"""
|
||||
with app.app_context():
|
||||
# Change settings currency to GBP
|
||||
settings = Settings.get_settings()
|
||||
settings.currency = 'GBP'
|
||||
db.session.commit()
|
||||
|
||||
# Create invoice
|
||||
invoice = Invoice(
|
||||
invoice_number='TEST-002',
|
||||
project_id=test_project.id,
|
||||
client_name=test_client_model.name,
|
||||
due_date=date.today() + timedelta(days=30),
|
||||
created_by=test_user.id,
|
||||
client_id=test_client_model.id,
|
||||
currency_code=settings.currency
|
||||
)
|
||||
db.session.add(invoice)
|
||||
db.session.commit()
|
||||
|
||||
# Verify invoice has GBP currency
|
||||
assert invoice.currency_code == 'GBP'
|
||||
|
||||
def test_invoice_duplicate_preserves_currency(self, app, test_user, test_project, test_client_model):
|
||||
"""Test that duplicating an invoice preserves the currency"""
|
||||
with app.app_context():
|
||||
# Create original invoice with JPY currency
|
||||
original_invoice = Invoice(
|
||||
invoice_number='ORIG-001',
|
||||
project_id=test_project.id,
|
||||
client_name=test_client_model.name,
|
||||
due_date=date.today() + timedelta(days=30),
|
||||
created_by=test_user.id,
|
||||
client_id=test_client_model.id,
|
||||
currency_code='JPY'
|
||||
)
|
||||
db.session.add(original_invoice)
|
||||
db.session.commit()
|
||||
|
||||
# Simulate duplication (like in duplicate_invoice route)
|
||||
new_invoice = Invoice(
|
||||
invoice_number='DUP-001',
|
||||
project_id=original_invoice.project_id,
|
||||
client_name=original_invoice.client_name,
|
||||
due_date=original_invoice.due_date + timedelta(days=30),
|
||||
created_by=test_user.id,
|
||||
client_id=original_invoice.client_id,
|
||||
currency_code=original_invoice.currency_code
|
||||
)
|
||||
db.session.add(new_invoice)
|
||||
db.session.commit()
|
||||
|
||||
# Verify duplicated invoice has same currency
|
||||
assert new_invoice.currency_code == 'JPY'
|
||||
|
||||
def test_invoice_items_display_with_currency(self, app, test_user, test_project, test_client_model):
|
||||
"""Test that invoice items display correctly with currency"""
|
||||
with app.app_context():
|
||||
# Create invoice
|
||||
invoice = Invoice(
|
||||
invoice_number='TEST-003',
|
||||
project_id=test_project.id,
|
||||
client_name=test_client_model.name,
|
||||
due_date=date.today() + timedelta(days=30),
|
||||
created_by=test_user.id,
|
||||
client_id=test_client_model.id,
|
||||
currency_code='EUR'
|
||||
)
|
||||
db.session.add(invoice)
|
||||
db.session.flush()
|
||||
|
||||
# Add invoice item
|
||||
item = InvoiceItem(
|
||||
invoice_id=invoice.id,
|
||||
description='Test Service',
|
||||
quantity=Decimal('10.00'),
|
||||
unit_price=Decimal('100.00')
|
||||
)
|
||||
db.session.add(item)
|
||||
db.session.commit()
|
||||
|
||||
# Verify invoice and item
|
||||
assert invoice.currency_code == 'EUR'
|
||||
assert item.total_amount == Decimal('1000.00')
|
||||
|
||||
def test_settings_currency_default(self, app):
|
||||
"""Test that Settings default currency matches configuration"""
|
||||
with app.app_context():
|
||||
# Clear existing settings
|
||||
Settings.query.delete()
|
||||
db.session.commit()
|
||||
|
||||
# Get settings (should create new with defaults)
|
||||
settings = Settings.get_settings()
|
||||
|
||||
# Should have some currency set (from Config or default)
|
||||
assert settings.currency is not None
|
||||
assert len(settings.currency) == 3 # Currency codes are 3 characters
|
||||
|
||||
def test_invoice_model_init_with_currency_kwarg(self, app, test_user, test_project, test_client_model):
|
||||
"""Test that Invoice __init__ properly accepts currency_code kwarg"""
|
||||
with app.app_context():
|
||||
# Create invoice with explicit currency_code
|
||||
invoice = Invoice(
|
||||
invoice_number='TEST-004',
|
||||
project_id=test_project.id,
|
||||
client_name=test_client_model.name,
|
||||
due_date=date.today() + timedelta(days=30),
|
||||
created_by=test_user.id,
|
||||
client_id=test_client_model.id,
|
||||
currency_code='CAD'
|
||||
)
|
||||
|
||||
# Verify currency is set correctly
|
||||
assert invoice.currency_code == 'CAD'
|
||||
|
||||
def test_invoice_to_dict_includes_currency(self, app, test_user, test_project, test_client_model):
|
||||
"""Test that invoice to_dict includes currency information"""
|
||||
with app.app_context():
|
||||
# Create invoice
|
||||
invoice = Invoice(
|
||||
invoice_number='TEST-005',
|
||||
project_id=test_project.id,
|
||||
client_name=test_client_model.name,
|
||||
due_date=date.today() + timedelta(days=30),
|
||||
created_by=test_user.id,
|
||||
client_id=test_client_model.id,
|
||||
currency_code='AUD'
|
||||
)
|
||||
db.session.add(invoice)
|
||||
db.session.commit()
|
||||
|
||||
# Convert to dict
|
||||
invoice_dict = invoice.to_dict()
|
||||
|
||||
# Verify currency is included (though it may not be in to_dict currently)
|
||||
# This test documents expected behavior
|
||||
assert invoice.currency_code == 'AUD'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__, '-v'])
|
||||
|
||||
122
tests/test_invoice_currency_smoke.py
Normal file
122
tests/test_invoice_currency_smoke.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
Smoke tests for invoice currency functionality
|
||||
Simple high-level tests to ensure the system works end-to-end
|
||||
"""
|
||||
import pytest
|
||||
import os
|
||||
from datetime import date, timedelta
|
||||
from decimal import Decimal
|
||||
from app import create_app, db
|
||||
from app.models import User, Project, Client, Invoice, Settings
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
"""Create and configure a test app instance"""
|
||||
# Set test database URL before creating app
|
||||
os.environ['DATABASE_URL'] = 'sqlite:///:memory:'
|
||||
|
||||
app = create_app()
|
||||
app.config['TESTING'] = True
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
|
||||
app.config['WTF_CSRF_ENABLED'] = False
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
yield app
|
||||
db.session.remove()
|
||||
db.drop_all()
|
||||
|
||||
|
||||
def test_invoice_currency_smoke(app):
|
||||
"""Smoke test: Create invoice and verify it uses settings currency"""
|
||||
with app.app_context():
|
||||
# Setup: Create user, client, project
|
||||
user = User(username='smokeuser', email='smoke@example.com', is_admin=True)
|
||||
user.set_password('password')
|
||||
db.session.add(user)
|
||||
|
||||
client = Client(name='Smoke Client', email='client@example.com', created_by=1)
|
||||
db.session.add(client)
|
||||
|
||||
project = Project(
|
||||
name='Smoke Project',
|
||||
client_id=1,
|
||||
created_by=1,
|
||||
billable=True,
|
||||
hourly_rate=Decimal('100.00'),
|
||||
status='active'
|
||||
)
|
||||
db.session.add(project)
|
||||
|
||||
# Set currency in settings
|
||||
settings = Settings.get_settings()
|
||||
settings.currency = 'CHF'
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Action: Create invoice
|
||||
invoice = Invoice(
|
||||
invoice_number='SMOKE-001',
|
||||
project_id=project.id,
|
||||
client_name=client.name,
|
||||
due_date=date.today() + timedelta(days=30),
|
||||
created_by=user.id,
|
||||
client_id=client.id,
|
||||
currency_code=settings.currency
|
||||
)
|
||||
db.session.add(invoice)
|
||||
db.session.commit()
|
||||
|
||||
# Verify: Invoice has correct currency
|
||||
assert invoice.currency_code == 'CHF', f"Expected CHF but got {invoice.currency_code}"
|
||||
|
||||
print("✓ Smoke test passed: Invoice currency correctly set from Settings")
|
||||
|
||||
|
||||
def test_pdf_generator_uses_settings_currency(app):
|
||||
"""Smoke test: Verify PDF generator uses settings currency"""
|
||||
with app.app_context():
|
||||
# Setup
|
||||
user = User(username='pdfuser', email='pdf@example.com', is_admin=True)
|
||||
user.set_password('password')
|
||||
db.session.add(user)
|
||||
|
||||
client = Client(name='PDF Client', email='pdf@example.com', created_by=1)
|
||||
db.session.add(client)
|
||||
|
||||
project = Project(
|
||||
name='PDF Project',
|
||||
client_id=1,
|
||||
created_by=1,
|
||||
billable=True,
|
||||
hourly_rate=Decimal('150.00'),
|
||||
status='active'
|
||||
)
|
||||
db.session.add(project)
|
||||
|
||||
settings = Settings.get_settings()
|
||||
settings.currency = 'SEK'
|
||||
|
||||
invoice = Invoice(
|
||||
invoice_number='PDF-001',
|
||||
project_id=project.id,
|
||||
client_name=client.name,
|
||||
due_date=date.today() + timedelta(days=30),
|
||||
created_by=user.id,
|
||||
client_id=client.id,
|
||||
currency_code=settings.currency
|
||||
)
|
||||
db.session.add(invoice)
|
||||
db.session.commit()
|
||||
|
||||
# Verify
|
||||
assert invoice.currency_code == settings.currency
|
||||
assert settings.currency == 'SEK'
|
||||
|
||||
print("✓ Smoke test passed: PDF generator will use correct currency")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__, '-v'])
|
||||
|
||||
Reference in New Issue
Block a user