feat(accounting): scope the tax-report export to income-only or cost-only

Adds a Complete / Income only / Cost only selector to the readable PDF + CSV
export (the on-screen report stays complete). Income-only emits just the
outgoing rows + the income summary line (+ the per-rate breakdown in the PDF);
cost-only emits the incoming-invoice + expense rows + the cost line and drops
the income-by-rate breakdown. Useful in Liechtenstein where, under the income
threshold, a flat 20% Gewinnungskosten deduction is sometimes better than actual
costs — handing the Treuhänder just the income (or just the cost) basis is
cleaner.

Backend: renderTaxReportPdf/Csv take a `scope` param (all|income|cost) that
filters report.ledger by row.type + the summary lines; the /pdf + /csv routes
accept & validate `?scope=`; filenames get an income_/cost_ tag. Frontend:
scope <select> beside the export buttons, threaded through buildQueryString.
i18n en/de. The 20% calculation itself is intentionally NOT in-app (applied by
the Treuhänder) per the scoping decision.
This commit is contained in:
Luca
2026-06-16 18:51:11 +02:00
parent d6da89f48a
commit 9f3b28684f
6 changed files with 71 additions and 14 deletions
+4
View File
@@ -3953,6 +3953,10 @@
"export": {
"reportTitle": "Bericht",
"reportHint": "Lesbare Liste — für Ihre Unterlagen.",
"scopeLabel": "Export-Umfang",
"scopeAll": "Vollständig",
"scopeIncome": "Nur Einnahmen",
"scopeCost": "Nur Kosten",
"journalTitle": "Buchungsjournal"
},
"exportCsv": "CSV exportieren",
+4
View File
@@ -3953,6 +3953,10 @@
"export": {
"reportTitle": "Report",
"reportHint": "Readable list — for your own records.",
"scopeLabel": "Export scope",
"scopeAll": "Complete",
"scopeIncome": "Income only",
"scopeCost": "Cost only",
"journalTitle": "Accounting journal"
},
"exportCsv": "Export CSV",
@@ -102,6 +102,8 @@ export const TaxReportPage: React.FC = () => {
const [to, setTo] = useState(initialPeriod.to);
const [currency, setCurrency] = useState<string>('CHF');
const [isExporting, setIsExporting] = useState<'pdf' | 'csv' | 'ledger' | null>(null);
// Export-only scope (income/cost split). The on-screen report stays complete.
const [exportScope, setExportScope] = useState<'all' | 'income' | 'cost'>('all');
// Treuhänder (collective-journal) export — same period/currency as the
// report; target tool picks the import format (generic / Banana / bexio).
const [ledgerFormat, setLedgerFormat] = useState<ExportFormat>('generic');
@@ -132,9 +134,10 @@ export const TaxReportPage: React.FC = () => {
const handleExport = async (format: 'pdf' | 'csv') => {
setIsExporting(format);
try {
const exportParams = { ...params, scope: exportScope };
const { url, filename } = format === 'pdf'
? await taxReportService.downloadPdfUrl(params)
: await taxReportService.downloadCsvUrl(params);
? await taxReportService.downloadPdfUrl(exportParams)
: await taxReportService.downloadCsvUrl(exportParams);
triggerBrowserDownload(url, filename);
} catch (err) {
toast.error(t('taxReport.exportFailed', 'Export failed. Please try again.'));
@@ -313,6 +316,17 @@ export const TaxReportPage: React.FC = () => {
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<select
value={exportScope}
onChange={(e) => setExportScope(e.target.value as 'all' | 'income' | 'cost')}
disabled={exportsDisabled}
aria-label={t('taxReport.export.scopeLabel', 'Export scope')}
className={`${selectClassName} min-w-[150px]`}
>
<option value="all">{t('taxReport.export.scopeAll', 'Complete')}</option>
<option value="income">{t('taxReport.export.scopeIncome', 'Income only')}</option>
<option value="cost">{t('taxReport.export.scopeCost', 'Cost only')}</option>
</select>
<Button
className="min-w-[150px]"
variant="outline"
+8 -2
View File
@@ -142,6 +142,9 @@ export interface TaxReportParams {
to: string;
currency: string;
locale?: string;
/** Export-only scope: 'all' (default) | 'income' | 'cost'. Ignored by the
* on-screen report; the PDF/CSV exports filter the ledger + summary. */
scope?: 'all' | 'income' | 'cost';
}
function buildQueryString(params: TaxReportParams): string {
@@ -151,6 +154,7 @@ function buildQueryString(params: TaxReportParams): string {
currency: params.currency,
});
if (params.locale) usp.set('locale', params.locale);
if (params.scope && params.scope !== 'all') usp.set('scope', params.scope);
return usp.toString();
}
@@ -172,7 +176,8 @@ export const taxReportService = {
responseType: 'blob',
});
const url = URL.createObjectURL(res.data);
const filename = `tax_report_${params.from}_to_${params.to}_${params.currency}.pdf`;
const scopeTag = params.scope && params.scope !== 'all' ? `${params.scope}_` : '';
const filename = `tax_report_${scopeTag}${params.from}_to_${params.to}_${params.currency}.pdf`;
return { url, filename };
},
@@ -181,7 +186,8 @@ export const taxReportService = {
responseType: 'blob',
});
const url = URL.createObjectURL(res.data);
const filename = `tax_report_${params.from}_to_${params.to}_${params.currency}.csv`;
const scopeTag = params.scope && params.scope !== 'all' ? `${params.scope}_` : '';
const filename = `tax_report_${scopeTag}${params.from}_to_${params.to}_${params.currency}.csv`;
return { url, filename };
},
};