From 4d876848823bce2c79e629308c92206c64d9893d Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:27:17 +0200 Subject: [PATCH] feat(accounting): VAT registration + reclaim-country settings in the Accounting tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the 'VAT registration & reclaim' section to Settings → Accounting: a 'VAT-registered' toggle (charge output + reclaim input VAT) and a multi-select of countries whose input VAT is reclaimable (default domestic CH/LI). Wires accounting.service + the backend keys added earlier (accounting_vat_registered, accounting_vat_reclaim_countries). i18n en/de. The report VAT-payable math that consumes these is the next slice. --- .../features/settings/tabs/AccountingTab.tsx | 46 ++++++++++++++++++- frontend/src/i18n/locales/de.json | 7 +++ frontend/src/i18n/locales/en.json | 7 +++ frontend/src/services/accounting.service.ts | 9 ++++ 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/frontend/src/features/settings/tabs/AccountingTab.tsx b/frontend/src/features/settings/tabs/AccountingTab.tsx index dfe653e9..21890a50 100644 --- a/frontend/src/features/settings/tabs/AccountingTab.tsx +++ b/frontend/src/features/settings/tabs/AccountingTab.tsx @@ -11,32 +11,41 @@ import { Save } from 'lucide-react'; import { Button, Card, CardContent, Loading } from '../../../components/common'; import { DecimalInput } from '../../../components/common/DecimalInput'; import { accountingService } from '../../../services/accounting.service'; +import { sortedCountryOptions } from '../../../constants/countries'; const labelCls = 'block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1'; const inputCls = 'w-full max-w-xs rounded-md border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 px-3 py-2 text-sm'; export const AccountingTab: React.FC = () => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const qc = useQueryClient(); const { data, isLoading } = useQuery({ queryKey: ['accounting-settings'], queryFn: () => accountingService.getSettings() }); const [kmMajor, setKmMajor] = useState(NaN); const [perDiemMajor, setPerDiemMajor] = useState(NaN); const [requireProof, setRequireProof] = useState(false); + const [vatRegistered, setVatRegistered] = useState(false); + const [reclaimCountries, setReclaimCountries] = useState([]); useEffect(() => { if (data) { setKmMajor(data.accounting_km_rate_minor / 100); setPerDiemMajor(data.accounting_per_diem_rate_minor / 100); setRequireProof(data.accounting_require_proof); + setVatRegistered(data.accounting_vat_registered); + setReclaimCountries(data.accounting_vat_reclaim_countries || []); } }, [data]); + const countries = sortedCountryOptions(i18n.language); + const save = useMutation({ mutationFn: () => accountingService.updateSettings({ accounting_km_rate_minor: Number.isFinite(kmMajor) ? Math.round(kmMajor * 100) : 0, accounting_per_diem_rate_minor: Number.isFinite(perDiemMajor) ? Math.round(perDiemMajor * 100) : 0, accounting_require_proof: requireProof, + accounting_vat_registered: vatRegistered, + accounting_vat_reclaim_countries: reclaimCountries, }), onSuccess: () => { toast.success(t('settings.accounting.savedToast', 'Accounting settings saved.')); qc.invalidateQueries({ queryKey: ['accounting-settings'] }); }, onError: (e: any) => toast.error(e?.response?.data?.error || e.message || 'Failed'), @@ -69,6 +78,41 @@ export const AccountingTab: React.FC = () => {

{t('settings.accounting.disclaimer', 'Rates and VAT/tax treatment are guidance only — verify with your Treuhaender.')}

+ {/* VAT registration & reclaim — drives whether output/input VAT applies + and which countries' input VAT is deductible (cost tax-treatment + + the tax report's VAT-payable). */} + +

+ {t('settings.accounting.vat.title', 'VAT registration & reclaim')} +

+ +
+ + +

+ {t('settings.accounting.vat.reclaimCountriesHint', 'Typically your domestic country (CH / LI). Costs from other countries are treated as non-reclaimable foreign VAT. Cmd/Ctrl-click to multi-select.')} +

+
+
+
diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 63e40d7b..32e1e37a 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -1723,6 +1723,13 @@ "perDiemRate": "Spesenpauschale (CHF / Tag)", "perDiemRateHint": "Standard für Pauschal-Aufwände; pro Eintrag überschreibbar.", "requireProof": "Beleg für jeden Aufwand verlangen", + "vat": { + "title": "MwSt-Registrierung & Vorsteuerabzug", + "registered": "MwSt-pflichtig (Umsatzsteuer berechnen + Vorsteuer abziehen)", + "registeredHint": "Aus = Kleinunternehmen / unter der Schwelle: keine MwSt berechnet, Vorsteuer ist Aufwand (nicht abziehbar).", + "reclaimCountries": "Länder mit abziehbarer Vorsteuer", + "reclaimCountriesHint": "Üblicherweise Ihr Inland (CH / LI). Kosten aus anderen Ländern gelten als nicht abziehbare ausländische MwSt. Cmd/Ctrl-Klick für Mehrfachauswahl." + }, "disclaimer": "Sätze und MwSt-/Steuerbehandlung dienen nur als Orientierung — mit Ihrem Treuhänder prüfen.", "savedToast": "Buchhaltungseinstellungen gespeichert." } diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 7e82eac4..a0b5c623 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -1281,6 +1281,13 @@ "perDiemRate": "Per-diem rate (CHF / day)", "perDiemRateHint": "Default applied to per-diem expenses; overridable per entry.", "requireProof": "Require a proof file on every expense", + "vat": { + "title": "VAT registration & reclaim", + "registered": "VAT-registered (charge output VAT + reclaim input VAT)", + "registeredHint": "Off = small business / under threshold: no VAT charged, input VAT is a cost (not reclaimable).", + "reclaimCountries": "Countries where input VAT is reclaimable", + "reclaimCountriesHint": "Typically your domestic country (CH / LI). Costs from other countries are treated as non-reclaimable foreign VAT. Cmd/Ctrl-click to multi-select." + }, "disclaimer": "Rates and VAT/tax treatment are guidance only — verify with your Treuhänder.", "savedToast": "Accounting settings saved." } diff --git a/frontend/src/services/accounting.service.ts b/frontend/src/services/accounting.service.ts index 1db12437..7e3ae7c6 100644 --- a/frontend/src/services/accounting.service.ts +++ b/frontend/src/services/accounting.service.ts @@ -86,6 +86,12 @@ export interface AccountingSettings { accounting_km_rate_minor: number; accounting_per_diem_rate_minor: number; accounting_require_proof: boolean; + /** VAT-registered: charge output VAT + reclaim input VAT. Off = small + * business / under threshold (no VAT). */ + accounting_vat_registered: boolean; + /** ISO-2 countries whose input VAT can be reclaimed (drives cost + * tax-treatment + the report's VAT-payable). */ + accounting_vat_reclaim_countries: string[]; } export interface CategorizePayload { @@ -177,6 +183,9 @@ export const accountingService = { accounting_km_rate_minor: Number(data.accounting_km_rate_minor) || 0, accounting_per_diem_rate_minor: Number(data.accounting_per_diem_rate_minor) || 0, accounting_require_proof: data.accounting_require_proof === true, + accounting_vat_registered: data.accounting_vat_registered === true, + accounting_vat_reclaim_countries: Array.isArray(data.accounting_vat_reclaim_countries) + ? data.accounting_vat_reclaim_countries : [], }; }, async updateSettings(payload: Partial): Promise<{ updated: string[] }> {