""" Export Service — CSV, Excel, and PDF generation for transactions and reports. """ import io import csv from datetime import date from flask import current_app from app.models.transaction import Transaction # ── CSV export ──────────────────────────────────────────────────────────────── def transactions_to_csv(transactions): """Return a CSV string of transactions.""" output = io.StringIO() writer = csv.writer(output) writer.writerow(['Date', 'Type', 'Description', 'Category', 'Account', 'Amount', 'Notes']) for txn in transactions: writer.writerow([ txn.date.strftime('%Y-%m-%d'), txn.transaction_type, txn.description, txn.category.name if txn.category else '', txn.account.name if txn.account else '', float(txn.amount), txn.notes or '', ]) output.seek(0) return output.getvalue() # ── Excel export ────────────────────────────────────────────────────────────── def transactions_to_excel(transactions, period_label='Transactions'): """Return Excel bytes for a list of transactions.""" from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Alignment, Border, Side from openpyxl.utils import get_column_letter wb = Workbook() ws = wb.active ws.title = period_label[:31] # max 31 chars symbol = current_app.config.get('APP_CURRENCY_SYMBOL', '$') # Header style header_fill = PatternFill(start_color='0F172A', end_color='0F172A', fill_type='solid') header_font = Font(color='F1F5F9', bold=True, size=10) thin = Side(style='thin', color='E2E8F0') border = Border(bottom=Side(style='thin', color='E2E8F0')) headers = ['Date', 'Type', 'Description', 'Category', 'Account', f'Amount ({symbol})', 'Notes'] col_widths = [12, 10, 40, 18, 18, 16, 30] for col, (header, width) in enumerate(zip(headers, col_widths), 1): cell = ws.cell(row=1, column=col, value=header) cell.font = header_font cell.fill = header_fill cell.alignment = Alignment(horizontal='left', vertical='center') ws.column_dimensions[get_column_letter(col)].width = width ws.row_dimensions[1].height = 22 # Data rows income_fill = PatternFill(start_color='F0FDF4', end_color='F0FDF4', fill_type='solid') expense_fill = PatternFill(start_color='FFF7F7', end_color='FFF7F7', fill_type='solid') for row_num, txn in enumerate(transactions, 2): fill = income_fill if txn.transaction_type == 'income' else expense_fill data = [ txn.date.strftime('%Y-%m-%d'), txn.transaction_type.title(), txn.description, txn.category.name if txn.category else '', txn.account.name if txn.account else '', float(txn.amount), txn.notes or '', ] for col, value in enumerate(data, 1): cell = ws.cell(row=row_num, column=col, value=value) cell.fill = fill cell.border = border cell.font = Font(size=10) if col == 6: cell.number_format = f'#,##0.00' cell.alignment = Alignment(horizontal='right') # Totals row total_row = len(transactions) + 2 ws.cell(row=total_row, column=5, value='TOTAL').font = Font(bold=True, size=10) total_cell = ws.cell(row=total_row, column=6, value=sum(float(t.amount) for t in transactions)) total_cell.font = Font(bold=True, size=10) total_cell.number_format = f'#,##0.00' total_cell.alignment = Alignment(horizontal='right') output = io.BytesIO() wb.save(output) output.seek(0) return output.getvalue() # ── PDF export ──────────────────────────────────────────────────────────────── def report_to_pdf(html_content): """Convert HTML string to PDF bytes using WeasyPrint.""" try: from weasyprint import HTML, CSS pdf = HTML(string=html_content).write_pdf() return pdf except Exception as e: import logging logging.getLogger(__name__).error(f'[export] PDF generation failed: {e}') return None def build_report_html(report_data, report_type, currency_symbol='$'): """Build a clean HTML string for PDF export.""" today = date.today().strftime('%B %d, %Y') period = report_data.get('period', '') income = report_data.get('income', 0) expense = report_data.get('expense', 0) net = report_data.get('net', 0) savings_rate = report_data.get('savings_rate', 0) def fmt(v): return f"{currency_symbol}{abs(float(v)):,.2f}" # Expense categories table rows cat_rows = '' total_exp = sum(c['total'] for c in report_data.get('expense_categories', [])) for cat in report_data.get('expense_categories', []): pct = round(cat['total'] / total_exp * 100, 1) if total_exp > 0 else 0 cat_rows += f'''
Generated {today}
| Category | Amount | % of Total |
|---|---|---|
| No expenses | ||