refactor(accounting): consolidate the Accounting tab into two cards + one Save
- Box 1 "Default rates": mileage, daily allowance, hourly rate, require-proof. Hints now make the cost-vs-billing split explicit (daily allowance = expense, hourly = billing fallback). - Box 2 retitled "VAT": registration, reclaim, default invoice VAT code, and the VAT label (moved out of its own card). - Drop the third card (AccountingProfileFields deleted); the two Save buttons become one — it persists both the app_settings and the two business_profile fields (VAT label + hourly rate) together. - Rename "Per-diem" → "Daily allowance" (EN) for clarity; German keeps the established "Spesenpauschale".
This commit is contained in:
@@ -1,82 +0,0 @@
|
|||||||
/**
|
|
||||||
* Business-profile financial fields surfaced on the Accounting tab: the
|
|
||||||
* **VAT label** (printed on invoice/quote PDFs) and the **default hourly rate**
|
|
||||||
* (install-wide fallback for hours logging). The values still live on
|
|
||||||
* `business_profile`; this is a self-contained card with its own save (mirrors
|
|
||||||
* VatCodesManager) so it can't clobber the rest of the business profile, and it
|
|
||||||
* shares the `business-profile` query cache so both pages stay in sync.
|
|
||||||
*/
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
import { Save } from 'lucide-react';
|
|
||||||
import { Button, Card, CardContent, Input, Loading } from '../common';
|
|
||||||
import { DecimalInput } from '../common/DecimalInput';
|
|
||||||
import { businessProfileService } from '../../services/businessProfile.service';
|
|
||||||
|
|
||||||
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 AccountingProfileFields: React.FC = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const qc = useQueryClient();
|
|
||||||
const { data, isLoading } = useQuery({ queryKey: ['business-profile'], queryFn: () => businessProfileService.get() });
|
|
||||||
|
|
||||||
const [vatLabel, setVatLabel] = useState('');
|
|
||||||
const [hourlyMajor, setHourlyMajor] = useState<number>(NaN);
|
|
||||||
const currency = data?.profile?.defaultCurrency || 'CHF';
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data?.profile) {
|
|
||||||
setVatLabel(data.profile.vatLabel || '');
|
|
||||||
setHourlyMajor(data.profile.defaultHourlyRateMinor != null ? data.profile.defaultHourlyRateMinor / 100 : NaN);
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const save = useMutation({
|
|
||||||
mutationFn: () => businessProfileService.update({
|
|
||||||
vatLabel: vatLabel || '',
|
|
||||||
defaultHourlyRateMinor: Number.isFinite(hourlyMajor) ? Math.max(0, Math.round(hourlyMajor * 100)) : null,
|
|
||||||
}),
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success(t('settings.accounting.profileFields.savedToast', 'Saved.'));
|
|
||||||
qc.invalidateQueries({ queryKey: ['business-profile'] });
|
|
||||||
},
|
|
||||||
onError: (e: any) => toast.error(e?.response?.data?.error || e.message || 'Failed'),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isLoading) return <Loading />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card><CardContent className="p-5 space-y-4">
|
|
||||||
<h3 className="text-sm font-semibold uppercase tracking-wider text-neutral-500 dark:text-neutral-400">
|
|
||||||
{t('settings.accounting.profileFields.title', 'VAT label & hourly rate')}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className={labelCls}>{t('settings.accounting.profileFields.vatLabel', 'VAT label (e.g. MwSt., VAT)')}</label>
|
|
||||||
<Input value={vatLabel} onChange={(e) => setVatLabel(e.target.value)} className={inputCls} />
|
|
||||||
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">{t('settings.accounting.profileFields.vatLabelHint', 'Printed as the VAT-line label on invoice / quote PDFs. Leave blank to use the document language default.')}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className={labelCls}>{t('settings.accounting.profileFields.hourlyRate', 'Default hourly rate')}</label>
|
|
||||||
<DecimalInput
|
|
||||||
value={hourlyMajor}
|
|
||||||
fractionDigits={2}
|
|
||||||
onChange={setHourlyMajor}
|
|
||||||
className={inputCls}
|
|
||||||
placeholder={t('settings.accounting.profileFields.hourlyRatePlaceholder', 'e.g. 120.00') as string}
|
|
||||||
/>
|
|
||||||
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">
|
|
||||||
{t('settings.accounting.profileFields.hourlyRateHint', 'Fallback used when a customer has no own rate. In {{currency}}, major units. Leave blank to require a per-customer or per-entry rate.', { currency })}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button onClick={() => save.mutate()} disabled={save.isPending}><Save className="w-4 h-4 mr-2" /> {save.isPending ? t('common.saving', 'Saving…') : t('common.save', 'Save')}</Button>
|
|
||||||
</CardContent></Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AccountingProfileFields;
|
|
||||||
@@ -8,14 +8,14 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { Save } from 'lucide-react';
|
import { Save } from 'lucide-react';
|
||||||
import { Button, Card, CardContent, Loading } from '../../../components/common';
|
import { Button, Card, CardContent, Input, Loading } from '../../../components/common';
|
||||||
import { DecimalInput } from '../../../components/common/DecimalInput';
|
import { DecimalInput } from '../../../components/common/DecimalInput';
|
||||||
import { accountingService } from '../../../services/accounting.service';
|
import { accountingService } from '../../../services/accounting.service';
|
||||||
|
import { businessProfileService } from '../../../services/businessProfile.service';
|
||||||
import { vatCodesService } from '../../../services/vatCodes.service';
|
import { vatCodesService } from '../../../services/vatCodes.service';
|
||||||
import { sortedCountryOptions } from '../../../constants/countries';
|
import { sortedCountryOptions } from '../../../constants/countries';
|
||||||
import { VatCodesManager } from '../../../components/admin/VatCodesManager';
|
import { VatCodesManager } from '../../../components/admin/VatCodesManager';
|
||||||
import { ChartOfAccountsManager } from '../../../components/admin/ChartOfAccountsManager';
|
import { ChartOfAccountsManager } from '../../../components/admin/ChartOfAccountsManager';
|
||||||
import { AccountingProfileFields } from '../../../components/admin/AccountingProfileFields';
|
|
||||||
|
|
||||||
const labelCls = 'block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1';
|
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';
|
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';
|
||||||
@@ -25,13 +25,18 @@ export const AccountingTab: React.FC = () => {
|
|||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const { data, isLoading } = useQuery({ queryKey: ['accounting-settings'], queryFn: () => accountingService.getSettings() });
|
const { data, isLoading } = useQuery({ queryKey: ['accounting-settings'], queryFn: () => accountingService.getSettings() });
|
||||||
const { data: outputVatCodes = [] } = useQuery({ queryKey: ['vat-codes', 'output'], queryFn: () => vatCodesService.listOutput() });
|
const { data: outputVatCodes = [] } = useQuery({ queryKey: ['vat-codes', 'output'], queryFn: () => vatCodesService.listOutput() });
|
||||||
|
// VAT label + default hourly rate live on business_profile, surfaced here so
|
||||||
|
// all financial/VAT config sits in one tab (and one Save).
|
||||||
|
const { data: profileSnap } = useQuery({ queryKey: ['business-profile'], queryFn: () => businessProfileService.get() });
|
||||||
|
|
||||||
const [kmMajor, setKmMajor] = useState<number>(NaN);
|
const [kmMajor, setKmMajor] = useState<number>(NaN);
|
||||||
const [perDiemMajor, setPerDiemMajor] = useState<number>(NaN);
|
const [perDiemMajor, setPerDiemMajor] = useState<number>(NaN);
|
||||||
|
const [hourlyMajor, setHourlyMajor] = useState<number>(NaN);
|
||||||
const [requireProof, setRequireProof] = useState(false);
|
const [requireProof, setRequireProof] = useState(false);
|
||||||
const [vatRegistered, setVatRegistered] = useState(false);
|
const [vatRegistered, setVatRegistered] = useState(false);
|
||||||
const [reclaimCountries, setReclaimCountries] = useState<string[]>([]);
|
const [reclaimCountries, setReclaimCountries] = useState<string[]>([]);
|
||||||
const [defaultOutputVatCode, setDefaultOutputVatCode] = useState('');
|
const [defaultOutputVatCode, setDefaultOutputVatCode] = useState('');
|
||||||
|
const [vatLabel, setVatLabel] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -43,19 +48,38 @@ export const AccountingTab: React.FC = () => {
|
|||||||
setDefaultOutputVatCode(data.accounting_default_output_vat_code || '');
|
setDefaultOutputVatCode(data.accounting_default_output_vat_code || '');
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (profileSnap?.profile) {
|
||||||
|
setVatLabel(profileSnap.profile.vatLabel || '');
|
||||||
|
setHourlyMajor(profileSnap.profile.defaultHourlyRateMinor != null ? profileSnap.profile.defaultHourlyRateMinor / 100 : NaN);
|
||||||
|
}
|
||||||
|
}, [profileSnap]);
|
||||||
|
|
||||||
const countries = sortedCountryOptions(i18n.language);
|
const countries = sortedCountryOptions(i18n.language);
|
||||||
|
const currency = profileSnap?.profile?.defaultCurrency || 'CHF';
|
||||||
|
|
||||||
|
// One Save persists BOTH the app_settings (rates/VAT/proof) and the two
|
||||||
|
// business_profile fields (VAT label + hourly rate).
|
||||||
const save = useMutation({
|
const save = useMutation({
|
||||||
mutationFn: () => accountingService.updateSettings({
|
mutationFn: async () => {
|
||||||
accounting_km_rate_minor: Number.isFinite(kmMajor) ? Math.round(kmMajor * 100) : 0,
|
await accountingService.updateSettings({
|
||||||
accounting_per_diem_rate_minor: Number.isFinite(perDiemMajor) ? Math.round(perDiemMajor * 100) : 0,
|
accounting_km_rate_minor: Number.isFinite(kmMajor) ? Math.round(kmMajor * 100) : 0,
|
||||||
accounting_require_proof: requireProof,
|
accounting_per_diem_rate_minor: Number.isFinite(perDiemMajor) ? Math.round(perDiemMajor * 100) : 0,
|
||||||
accounting_vat_registered: vatRegistered,
|
accounting_require_proof: requireProof,
|
||||||
accounting_vat_reclaim_countries: reclaimCountries,
|
accounting_vat_registered: vatRegistered,
|
||||||
accounting_default_output_vat_code: defaultOutputVatCode,
|
accounting_vat_reclaim_countries: reclaimCountries,
|
||||||
}),
|
accounting_default_output_vat_code: defaultOutputVatCode,
|
||||||
onSuccess: () => { toast.success(t('settings.accounting.savedToast', 'Accounting settings saved.')); qc.invalidateQueries({ queryKey: ['accounting-settings'] }); },
|
});
|
||||||
|
await businessProfileService.update({
|
||||||
|
vatLabel: vatLabel || '',
|
||||||
|
defaultHourlyRateMinor: Number.isFinite(hourlyMajor) ? Math.max(0, Math.round(hourlyMajor * 100)) : null,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t('settings.accounting.savedToast', 'Accounting settings saved.'));
|
||||||
|
qc.invalidateQueries({ queryKey: ['accounting-settings'] });
|
||||||
|
qc.invalidateQueries({ queryKey: ['business-profile'] });
|
||||||
|
},
|
||||||
onError: (e: any) => toast.error(e?.response?.data?.error || e.message || 'Failed'),
|
onError: (e: any) => toast.error(e?.response?.data?.error || e.message || 'Failed'),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,9 +99,14 @@ export const AccountingTab: React.FC = () => {
|
|||||||
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">{t('settings.accounting.kmRateHint', 'Default applied to mileage expenses; overridable per entry.')}</p>
|
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">{t('settings.accounting.kmRateHint', 'Default applied to mileage expenses; overridable per entry.')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className={labelCls}>{t('settings.accounting.perDiemRate', 'Per-diem rate (CHF / day)')}</label>
|
<label className={labelCls}>{t('settings.accounting.perDiemRate', 'Daily allowance (CHF / day)')}</label>
|
||||||
<DecimalInput value={perDiemMajor} onChange={setPerDiemMajor} fractionDigits={2} className={inputCls} />
|
<DecimalInput value={perDiemMajor} onChange={setPerDiemMajor} fractionDigits={2} className={inputCls} />
|
||||||
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">{t('settings.accounting.perDiemRateHint', 'Default applied to per-diem expenses; overridable per entry.')}</p>
|
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">{t('settings.accounting.perDiemRateHint', 'A flat daily allowance booked as an expense (not a client billing rate); overridable per entry.')}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>{t('settings.accounting.profileFields.hourlyRate', 'Default hourly rate')}</label>
|
||||||
|
<DecimalInput value={hourlyMajor} onChange={setHourlyMajor} fractionDigits={2} className={inputCls} placeholder={t('settings.accounting.profileFields.hourlyRatePlaceholder', 'e.g. 120.00') as string} />
|
||||||
|
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">{t('settings.accounting.profileFields.hourlyRateHint', 'Billing fallback used when a customer has no own rate (hours logging). In {{currency}}, major units. Leave blank to require a per-customer or per-entry rate.', { currency })}</p>
|
||||||
</div>
|
</div>
|
||||||
<label className="flex items-center gap-2 text-sm text-neutral-800 dark:text-neutral-200">
|
<label className="flex items-center gap-2 text-sm text-neutral-800 dark:text-neutral-200">
|
||||||
<input type="checkbox" checked={requireProof} onChange={(e) => setRequireProof(e.target.checked)} className="rounded border-neutral-300" />
|
<input type="checkbox" checked={requireProof} onChange={(e) => setRequireProof(e.target.checked)} className="rounded border-neutral-300" />
|
||||||
@@ -91,7 +120,7 @@ export const AccountingTab: React.FC = () => {
|
|||||||
the tax report's VAT-payable). */}
|
the tax report's VAT-payable). */}
|
||||||
<Card><CardContent className="p-5 space-y-4">
|
<Card><CardContent className="p-5 space-y-4">
|
||||||
<h3 className="text-sm font-semibold uppercase tracking-wider text-neutral-500 dark:text-neutral-400">
|
<h3 className="text-sm font-semibold uppercase tracking-wider text-neutral-500 dark:text-neutral-400">
|
||||||
{t('settings.accounting.vat.title', 'VAT registration & reclaim')}
|
{t('settings.accounting.vat.title', 'VAT')}
|
||||||
</h3>
|
</h3>
|
||||||
<label className="flex items-start gap-2 text-sm text-neutral-800 dark:text-neutral-200">
|
<label className="flex items-start gap-2 text-sm text-neutral-800 dark:text-neutral-200">
|
||||||
<input type="checkbox" checked={vatRegistered} onChange={(e) => setVatRegistered(e.target.checked)} className="mt-0.5 rounded border-neutral-300" />
|
<input type="checkbox" checked={vatRegistered} onChange={(e) => setVatRegistered(e.target.checked)} className="mt-0.5 rounded border-neutral-300" />
|
||||||
@@ -127,17 +156,17 @@ export const AccountingTab: React.FC = () => {
|
|||||||
</select>
|
</select>
|
||||||
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">{t('settings.accounting.vat.defaultOutputCodeHint', 'New invoices and quotes start with this VAT code selected. Existing documents are unaffected.')}</p>
|
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">{t('settings.accounting.vat.defaultOutputCodeHint', 'New invoices and quotes start with this VAT code selected. Existing documents are unaffected.')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelCls}>{t('settings.accounting.profileFields.vatLabel', 'VAT label (e.g. MwSt., VAT)')}</label>
|
||||||
|
<Input value={vatLabel} onChange={(e) => setVatLabel(e.target.value)} className={inputCls} />
|
||||||
|
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">{t('settings.accounting.profileFields.vatLabelHint', 'Printed as the VAT-line label on invoice / quote PDFs. Leave blank to use the document language default.')}</p>
|
||||||
|
</div>
|
||||||
</CardContent></Card>
|
</CardContent></Card>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button onClick={() => save.mutate()} disabled={save.isPending}><Save className="w-4 h-4 mr-2" /> {save.isPending ? t('common.saving', 'Saving…') : t('common.save', 'Save')}</Button>
|
<Button onClick={() => save.mutate()} disabled={save.isPending}><Save className="w-4 h-4 mr-2" /> {save.isPending ? t('common.saving', 'Saving…') : t('common.save', 'Save')}</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* VAT label + default hourly rate — moved here from Business profile so
|
|
||||||
all financial/VAT config lives in one place (storage stays on
|
|
||||||
business_profile; this card has its own save). */}
|
|
||||||
<AccountingProfileFields />
|
|
||||||
|
|
||||||
{/* VAT codes + rate→code / treatment→code maps — relocated here from the
|
{/* VAT codes + rate→code / treatment→code maps — relocated here from the
|
||||||
Chart-of-accounts page so all VAT config lives in one place. */}
|
Chart-of-accounts page so all VAT config lives in one place. */}
|
||||||
<VatCodesManager />
|
<VatCodesManager />
|
||||||
|
|||||||
@@ -1718,14 +1718,14 @@
|
|||||||
},
|
},
|
||||||
"accounting": {
|
"accounting": {
|
||||||
"title": "Buchhaltung",
|
"title": "Buchhaltung",
|
||||||
"subtitle": "Standardsätze für interne Aufwände und die Belegpflicht.",
|
"subtitle": "Standardsätze, MwSt-Einstellungen und die Belegpflicht.",
|
||||||
"kmRate": "Kilometersatz (CHF / km)",
|
"kmRate": "Kilometersatz (CHF / km)",
|
||||||
"kmRateHint": "Standard für Kilometer-Aufwände; pro Eintrag überschreibbar.",
|
"kmRateHint": "Standard für Kilometer-Aufwände; pro Eintrag überschreibbar.",
|
||||||
"perDiemRate": "Spesenpauschale (CHF / Tag)",
|
"perDiemRate": "Spesenpauschale (CHF / Tag)",
|
||||||
"perDiemRateHint": "Standard für Pauschal-Aufwände; pro Eintrag überschreibbar.",
|
"perDiemRateHint": "Eine Tagespauschale, die als Aufwand gebucht wird (kein Kunden-Verrechnungssatz); pro Eintrag überschreibbar.",
|
||||||
"requireProof": "Beleg für jeden Aufwand verlangen",
|
"requireProof": "Beleg für jeden Aufwand verlangen",
|
||||||
"vat": {
|
"vat": {
|
||||||
"title": "MwSt-Registrierung & Vorsteuerabzug",
|
"title": "MwSt.",
|
||||||
"registered": "MwSt-pflichtig (Umsatzsteuer berechnen + Vorsteuer abziehen)",
|
"registered": "MwSt-pflichtig (Umsatzsteuer berechnen + Vorsteuer abziehen)",
|
||||||
"registeredHint": "Aus = Kleinunternehmen / unter der Schwelle: keine MwSt berechnet, Vorsteuer ist Aufwand (nicht abziehbar).",
|
"registeredHint": "Aus = Kleinunternehmen / unter der Schwelle: keine MwSt berechnet, Vorsteuer ist Aufwand (nicht abziehbar).",
|
||||||
"reclaimCountries": "Länder mit abziehbarer Vorsteuer",
|
"reclaimCountries": "Länder mit abziehbarer Vorsteuer",
|
||||||
@@ -1742,7 +1742,7 @@
|
|||||||
"vatLabelHint": "Wird als Bezeichnung der MwSt-Zeile auf Rechnungs-/Angebots-PDFs gedruckt. Leer lassen, um die Standardbezeichnung der Dokumentsprache zu verwenden.",
|
"vatLabelHint": "Wird als Bezeichnung der MwSt-Zeile auf Rechnungs-/Angebots-PDFs gedruckt. Leer lassen, um die Standardbezeichnung der Dokumentsprache zu verwenden.",
|
||||||
"hourlyRate": "Standard-Stundensatz",
|
"hourlyRate": "Standard-Stundensatz",
|
||||||
"hourlyRatePlaceholder": "z. B. 120.00",
|
"hourlyRatePlaceholder": "z. B. 120.00",
|
||||||
"hourlyRateHint": "Fallback, wenn ein Kunde keinen eigenen Satz hat. In {{currency}}, in Hauptwährungseinheiten. Leer lassen, um einen Satz pro Kunde oder pro Eintrag zu verlangen.",
|
"hourlyRateHint": "Verrechnungs-Fallback, wenn ein Kunde keinen eigenen Satz hat (Stundenerfassung). In {{currency}}, in Hauptwährungseinheiten. Leer lassen, um einen Satz pro Kunde oder pro Eintrag zu verlangen.",
|
||||||
"savedToast": "Gespeichert."
|
"savedToast": "Gespeichert."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1276,14 +1276,14 @@
|
|||||||
},
|
},
|
||||||
"accounting": {
|
"accounting": {
|
||||||
"title": "Accounting",
|
"title": "Accounting",
|
||||||
"subtitle": "Default rates for internal expenses and the proof requirement.",
|
"subtitle": "Default rates, VAT settings and the proof requirement.",
|
||||||
"kmRate": "Mileage rate (CHF / km)",
|
"kmRate": "Mileage rate (CHF / km)",
|
||||||
"kmRateHint": "Default applied to mileage expenses; overridable per entry.",
|
"kmRateHint": "Default applied to mileage expenses; overridable per entry.",
|
||||||
"perDiemRate": "Per-diem rate (CHF / day)",
|
"perDiemRate": "Daily allowance (CHF / day)",
|
||||||
"perDiemRateHint": "Default applied to per-diem expenses; overridable per entry.",
|
"perDiemRateHint": "A flat daily allowance booked as an expense (not a client billing rate); overridable per entry.",
|
||||||
"requireProof": "Require a proof file on every expense",
|
"requireProof": "Require a proof file on every expense",
|
||||||
"vat": {
|
"vat": {
|
||||||
"title": "VAT registration & reclaim",
|
"title": "VAT",
|
||||||
"registered": "VAT-registered (charge output VAT + reclaim input VAT)",
|
"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).",
|
"registeredHint": "Off = small business / under threshold: no VAT charged, input VAT is a cost (not reclaimable).",
|
||||||
"reclaimCountries": "Countries where input VAT is reclaimable",
|
"reclaimCountries": "Countries where input VAT is reclaimable",
|
||||||
@@ -1300,7 +1300,7 @@
|
|||||||
"vatLabelHint": "Printed as the VAT-line label on invoice / quote PDFs. Leave blank to use the document language default.",
|
"vatLabelHint": "Printed as the VAT-line label on invoice / quote PDFs. Leave blank to use the document language default.",
|
||||||
"hourlyRate": "Default hourly rate",
|
"hourlyRate": "Default hourly rate",
|
||||||
"hourlyRatePlaceholder": "e.g. 120.00",
|
"hourlyRatePlaceholder": "e.g. 120.00",
|
||||||
"hourlyRateHint": "Fallback used when a customer has no own rate. In {{currency}}, major units. Leave blank to require a per-customer or per-entry rate.",
|
"hourlyRateHint": "Billing fallback used when a customer has no own rate (hours logging). In {{currency}}, major units. Leave blank to require a per-customer or per-entry rate.",
|
||||||
"savedToast": "Saved."
|
"savedToast": "Saved."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3639,7 +3639,7 @@
|
|||||||
"expenseKind": {
|
"expenseKind": {
|
||||||
"amount": "Amount",
|
"amount": "Amount",
|
||||||
"mileage": "Mileage (km)",
|
"mileage": "Mileage (km)",
|
||||||
"per_diem": "Per-diem"
|
"per_diem": "Daily allowance"
|
||||||
},
|
},
|
||||||
"category": {
|
"category": {
|
||||||
"infrastructure": "Infrastructure & rent",
|
"infrastructure": "Infrastructure & rent",
|
||||||
|
|||||||
Reference in New Issue
Block a user