diff --git a/modules/tax_management/index.py b/modules/tax_management/index.py index 1ebc363..8313303 100644 --- a/modules/tax_management/index.py +++ b/modules/tax_management/index.py @@ -4,11 +4,38 @@ Handles tax forms (Modelo 349, 303, 130, 390, 100) and Social Security payments. """ +import csv +import io + from module_manager import BaseModule -from flask import Blueprint, render_template, request, redirect, url_for, flash +from flask import Blueprint, render_template, request, redirect, url_for, flash, Response from datetime import datetime +def _csv_safe(value): + """Neutralize spreadsheet formula injection in user-entered text.""" + if value and value[0] in ('=', '+', '-', '@', '\t', '\r'): + return "'" + value + return value + + +def build_ss_payments_csv(payments): + """Render SS payments to CSV text (Date, Description, Amount) with a total row.""" + buf = io.StringIO() + writer = csv.writer(buf) + writer.writerow(['Date', 'Description', 'Amount (EUR)']) + total = 0.0 + for p in payments: + writer.writerow([ + p.payment_date.isoformat(), + _csv_safe(p.description or ''), + f'{p.amount:.2f}', + ]) + total += p.amount + writer.writerow(['', 'Total', f'{total:.2f}']) + return buf.getvalue() + + class TaxManagementModule(BaseModule): """Tax forms and Social Security payment management""" @@ -133,6 +160,11 @@ def ss_payment_edit(id): def ss_payment_delete(id): return self._delete_ss_payment(id) + @bp.route('/ss-payments/export') + @login_required + def ss_payment_export(): + return self._export_ss_payments() + app.register_blueprint(bp) # --- REST API (served under /api/v1/m/tax_management/... ) --- @@ -352,6 +384,28 @@ def _edit_ss_payment(self, id): flash('Error processing form data. Please check your input.', 'danger') return redirect(url_for('tax_management.tax_forms_index')) + def _export_ss_payments(self): + """Export SS payments as a CSV download, optionally filtered by ?year=YYYY""" + year = request.args.get('year', type=int) + query = self.SSPayment.query + if year: + query = query.filter( + self._db.extract('year', self.SSPayment.payment_date) == year + ) + payments = query.order_by(self.SSPayment.payment_date).all() + + csv_text = build_ss_payments_csv(payments) + filename = f'ss_payments_{year}.csv' if year else 'ss_payments.csv' + self.core.log_activity( + 'ss_payments_exported', 'tax', + f'{len(payments)} payments' + (f', year {year}' if year else '') + ) + return Response( + csv_text, + mimetype='text/csv; charset=utf-8', + headers={'Content-Disposition': f'attachment; filename="{filename}"'} + ) + def _delete_ss_payment(self, id): """Delete a Social Security payment""" try: diff --git a/modules/tax_management/templates/tax_forms.html b/modules/tax_management/templates/tax_forms.html index cc6d9b5..7037cf2 100644 --- a/modules/tax_management/templates/tax_forms.html +++ b/modules/tax_management/templates/tax_forms.html @@ -158,8 +158,12 @@