feat(accounting): invoices force-enable the Accounting master
Invoice VAT config (codes + label) and the hourly rate now live under
Settings → Accounting, so an install with Invoices must have Accounting
available.
- applyDependencyRules (backend adminFeatureFlags.js + frontend
FeatureFlagsContext.tsx): bills on → accounting on, before the
accounting→children rule so the sub-features keep their own state.
- Migration 133 corrects existing installs: set the STORED accounting=true
where bills is on. requireFeatureFlag('accounting') reads the raw row, so
without this an upgraded install (invoices on, accounting off) would show
the tab but 403 its endpoints. Idempotent; only flips on; no down.
- Features tab: the Accounting card shows locked-on (disabled + hint) while
Invoices is enabled.
Also includes the i18n keys (en/de) for the VAT/financial settings move.
This commit is contained in:
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Migration 133: invoices (Bills) force-enable the Accounting master.
|
||||||
|
*
|
||||||
|
* Invoice VAT config (codes + label) and the default hourly rate now live under
|
||||||
|
* Settings → Accounting, so an install with Bills enabled must have Accounting
|
||||||
|
* available. `applyDependencyRules` enforces this on every flag READ/WRITE, but
|
||||||
|
* the `requireFeatureFlag('accounting')` middleware reads the STORED row
|
||||||
|
* directly — so existing installs that already have `bills=true, accounting=false`
|
||||||
|
* would show the Accounting tab yet 403 its endpoints. This one-time correction
|
||||||
|
* brings the stored value in line (forward fix, not a compensation: it encodes a
|
||||||
|
* new dependency rule, it doesn't patch a buggy earlier migration).
|
||||||
|
*
|
||||||
|
* Idempotent: only flips accounting ON where Bills is on; never turns it off.
|
||||||
|
*/
|
||||||
|
function isOn(row) {
|
||||||
|
return !!(row && (row.value === true || row.value === 1 || row.value === '1'));
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.up = async function (knex) {
|
||||||
|
if (!(await knex.schema.hasTable('feature_flags'))) return;
|
||||||
|
const bills = await knex('feature_flags').where({ key: 'bills' }).first();
|
||||||
|
if (!isOn(bills)) return;
|
||||||
|
|
||||||
|
const accounting = await knex('feature_flags').where({ key: 'accounting' }).first();
|
||||||
|
if (!accounting) {
|
||||||
|
await knex('feature_flags').insert({ key: 'accounting', value: true });
|
||||||
|
} else if (!isOn(accounting)) {
|
||||||
|
await knex('feature_flags').where({ key: 'accounting' }).update({ value: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// No down — we can't know whether Accounting was independently wanted, and
|
||||||
|
// turning it back off could hide a section the admin now relies on.
|
||||||
|
exports.down = async function () {};
|
||||||
@@ -127,6 +127,11 @@ function applyDependencyRules(flags) {
|
|||||||
// Sub-features can't outlive their parents.
|
// Sub-features can't outlive their parents.
|
||||||
if (out.quotes === false) out.bills = false;
|
if (out.quotes === false) out.bills = false;
|
||||||
if (out.calendar === false) out.calendarBooking = false;
|
if (out.calendar === false) out.calendarBooking = false;
|
||||||
|
// Invoices (Bills) force-enable the Accounting master: invoice VAT config
|
||||||
|
// (codes + label) and the hourly rate live under Settings → Accounting, so
|
||||||
|
// an install with invoices must have Accounting available. Runs BEFORE the
|
||||||
|
// accounting→children rule so the sub-features keep their own stored state.
|
||||||
|
if (out.bills === true) out.accounting = true;
|
||||||
// Accounting is a top-level MASTER; its sub-features can't outlive it.
|
// Accounting is a top-level MASTER; its sub-features can't outlive it.
|
||||||
// Tax export is now independent of Bills — it relocated permanently
|
// Tax export is now independent of Bills — it relocated permanently
|
||||||
// into the Accounting section (its own master gate).
|
// into the Accounting section (its own master gate).
|
||||||
|
|||||||
@@ -91,6 +91,10 @@ function applyDependencyRules(flags: FeatureFlags): FeatureFlags {
|
|||||||
out.galleries = true; // foundation — always on
|
out.galleries = true; // foundation — always on
|
||||||
if (out.quotes === false) out.bills = false; // bills depend on quotes
|
if (out.quotes === false) out.bills = false; // bills depend on quotes
|
||||||
if (out.calendar === false) out.calendarBooking = false; // booking depends on calendar
|
if (out.calendar === false) out.calendarBooking = false; // booking depends on calendar
|
||||||
|
// Invoices (Bills) force-enable the Accounting master — invoice VAT config +
|
||||||
|
// hourly rate live under Settings → Accounting. Before the accounting→children
|
||||||
|
// rule so sub-features keep their own state.
|
||||||
|
if (out.bills === true) out.accounting = true;
|
||||||
// Accounting sub-features require the Accounting master. Tax export is
|
// Accounting sub-features require the Accounting master. Tax export is
|
||||||
// independent of Bills now — it relocated permanently into Accounting.
|
// independent of Bills now — it relocated permanently into Accounting.
|
||||||
if (out.accounting === false) {
|
if (out.accounting === false) {
|
||||||
|
|||||||
@@ -324,6 +324,13 @@ export const FeaturesTab: React.FC = () => {
|
|||||||
sidebarLabel={t('settings.features.accounting.sidebar', 'Accounting')}
|
sidebarLabel={t('settings.features.accounting.sidebar', 'Accounting')}
|
||||||
enabled={staged.accounting}
|
enabled={staged.accounting}
|
||||||
onToggle={(next) => setFlag('accounting', next)}
|
onToggle={(next) => setFlag('accounting', next)}
|
||||||
|
// Invoices force-enable Accounting (invoice VAT settings live here),
|
||||||
|
// so the master can't be turned off while Bills is on.
|
||||||
|
disabled={staged.bills}
|
||||||
|
lockedReason={staged.bills ? t(
|
||||||
|
'settings.features.accounting.requiredByBills',
|
||||||
|
'On automatically because Invoices is enabled — invoice VAT settings live in the Accounting section.',
|
||||||
|
) : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FeatureCard
|
<FeatureCard
|
||||||
|
|||||||
@@ -1646,7 +1646,8 @@
|
|||||||
"accounting": {
|
"accounting": {
|
||||||
"title": "Buchhaltung",
|
"title": "Buchhaltung",
|
||||||
"description": "Ein eigener Buchhaltungsbereich, getrennt vom CRM. Hier aktivieren und dann die Unterfunktionen unten einschalten (Steuerexport, Eingangsrechnungen). MwSt-/Steuerbehandlung dient nur als Orientierung — vor dem Verlassen darauf mit Ihrem Treuhänder prüfen.",
|
"description": "Ein eigener Buchhaltungsbereich, getrennt vom CRM. Hier aktivieren und dann die Unterfunktionen unten einschalten (Steuerexport, Eingangsrechnungen). MwSt-/Steuerbehandlung dient nur als Orientierung — vor dem Verlassen darauf mit Ihrem Treuhänder prüfen.",
|
||||||
"sidebar": "Buchhaltung"
|
"sidebar": "Buchhaltung",
|
||||||
|
"requiredByBills": "Automatisch an, weil Rechnungen aktiviert ist — die MwSt-Einstellungen der Rechnungen liegen im Buchhaltungsbereich."
|
||||||
},
|
},
|
||||||
"incomingInvoices": {
|
"incomingInvoices": {
|
||||||
"title": "Eingangsrechnungen",
|
"title": "Eingangsrechnungen",
|
||||||
@@ -1731,7 +1732,16 @@
|
|||||||
"reclaimCountriesHint": "Üblicherweise Ihr Inland (CH / LI). Kosten aus anderen Ländern gelten als nicht abziehbare ausländische MwSt. Cmd/Ctrl-Klick für Mehrfachauswahl."
|
"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.",
|
"disclaimer": "Sätze und MwSt-/Steuerbehandlung dienen nur als Orientierung — mit Ihrem Treuhänder prüfen.",
|
||||||
"savedToast": "Buchhaltungseinstellungen gespeichert."
|
"savedToast": "Buchhaltungseinstellungen gespeichert.",
|
||||||
|
"profileFields": {
|
||||||
|
"title": "MwSt-Bezeichnung & Stundensatz",
|
||||||
|
"vatLabel": "MwSt-Bezeichnung (z. B. MwSt., VAT)",
|
||||||
|
"vatLabelHint": "Wird als Bezeichnung der MwSt-Zeile auf Rechnungs-/Angebots-PDFs gedruckt. Leer lassen, um die Standardbezeichnung der Dokumentsprache zu verwenden.",
|
||||||
|
"hourlyRate": "Standard-Stundensatz",
|
||||||
|
"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.",
|
||||||
|
"savedToast": "Gespeichert."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"branding": {
|
"branding": {
|
||||||
@@ -3937,7 +3947,8 @@
|
|||||||
"vat": {
|
"vat": {
|
||||||
"addTitle": "MWST-Code hinzufügen", "editTitle": "MWST-Code bearbeiten",
|
"addTitle": "MWST-Code hinzufügen", "editTitle": "MWST-Code bearbeiten",
|
||||||
"code": "Code", "name": "Name", "rate": "Satz %", "direction": "Richtung",
|
"code": "Code", "name": "Name", "rate": "Satz %", "direction": "Richtung",
|
||||||
"account": "MWST-Konto", "noAccount": "— keines —", "confirmDelete": "Diesen MWST-Code löschen?"
|
"account": "MWST-Konto", "noAccount": "— keines —", "confirmDelete": "Diesen MWST-Code löschen?",
|
||||||
|
"legacyRate": "{{rate}}% (nicht konfiguriert)"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"title": "Treuhänder-Export",
|
"title": "Treuhänder-Export",
|
||||||
@@ -4309,6 +4320,7 @@
|
|||||||
},
|
},
|
||||||
"businessProfile": {
|
"businessProfile": {
|
||||||
"savedToast": "Geschäftsprofil gespeichert.",
|
"savedToast": "Geschäftsprofil gespeichert.",
|
||||||
|
"movedToAccounting": "MwSt-Satz, MwSt-Bezeichnung und Standard-Stundensatz befinden sich jetzt unter Einstellungen → Buchhaltung.",
|
||||||
"title": "Geschäftsprofil",
|
"title": "Geschäftsprofil",
|
||||||
"subtitle": "Briefkopf, Kontaktdaten und Standardwerte für Angebote und Rechnungen.",
|
"subtitle": "Briefkopf, Kontaktdaten und Standardwerte für Angebote und Rechnungen.",
|
||||||
"businessHours": {
|
"businessHours": {
|
||||||
|
|||||||
@@ -1204,7 +1204,8 @@
|
|||||||
"accounting": {
|
"accounting": {
|
||||||
"title": "Accounting",
|
"title": "Accounting",
|
||||||
"description": "A dedicated Accounting area, separate from CRM. Turn this on, then enable the sub-features below (Tax export, Incoming invoices). VAT / tax treatment is guidance only — verify with your Treuhänder before relying on it.",
|
"description": "A dedicated Accounting area, separate from CRM. Turn this on, then enable the sub-features below (Tax export, Incoming invoices). VAT / tax treatment is guidance only — verify with your Treuhänder before relying on it.",
|
||||||
"sidebar": "Accounting"
|
"sidebar": "Accounting",
|
||||||
|
"requiredByBills": "On automatically because Invoices is enabled — invoice VAT settings live in the Accounting section."
|
||||||
},
|
},
|
||||||
"incomingInvoices": {
|
"incomingInvoices": {
|
||||||
"title": "Incoming invoices",
|
"title": "Incoming invoices",
|
||||||
@@ -1289,7 +1290,16 @@
|
|||||||
"reclaimCountriesHint": "Typically your domestic country (CH / LI). Costs from other countries are treated as non-reclaimable foreign VAT. Cmd/Ctrl-click to multi-select."
|
"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.",
|
"disclaimer": "Rates and VAT/tax treatment are guidance only — verify with your Treuhänder.",
|
||||||
"savedToast": "Accounting settings saved."
|
"savedToast": "Accounting settings saved.",
|
||||||
|
"profileFields": {
|
||||||
|
"title": "VAT label & hourly rate",
|
||||||
|
"vatLabel": "VAT label (e.g. MwSt., VAT)",
|
||||||
|
"vatLabelHint": "Printed as the VAT-line label on invoice / quote PDFs. Leave blank to use the document language default.",
|
||||||
|
"hourlyRate": "Default hourly rate",
|
||||||
|
"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.",
|
||||||
|
"savedToast": "Saved."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"analytics": {
|
"analytics": {
|
||||||
@@ -3937,7 +3947,8 @@
|
|||||||
"vat": {
|
"vat": {
|
||||||
"addTitle": "Add VAT code", "editTitle": "Edit VAT code",
|
"addTitle": "Add VAT code", "editTitle": "Edit VAT code",
|
||||||
"code": "Code", "name": "Name", "rate": "Rate %", "direction": "Direction",
|
"code": "Code", "name": "Name", "rate": "Rate %", "direction": "Direction",
|
||||||
"account": "VAT account", "noAccount": "— none —", "confirmDelete": "Delete this VAT code?"
|
"account": "VAT account", "noAccount": "— none —", "confirmDelete": "Delete this VAT code?",
|
||||||
|
"legacyRate": "{{rate}}% (not configured)"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"title": "Treuhänder export",
|
"title": "Treuhänder export",
|
||||||
@@ -4307,6 +4318,7 @@
|
|||||||
},
|
},
|
||||||
"businessProfile": {
|
"businessProfile": {
|
||||||
"savedToast": "Business profile saved.",
|
"savedToast": "Business profile saved.",
|
||||||
|
"movedToAccounting": "The VAT rate, VAT label and default hourly rate now live under Settings → Accounting.",
|
||||||
"title": "Business profile",
|
"title": "Business profile",
|
||||||
"subtitle": "Issuer block shown on every quote and invoice PDF.",
|
"subtitle": "Issuer block shown on every quote and invoice PDF.",
|
||||||
"businessHours": {
|
"businessHours": {
|
||||||
|
|||||||
Reference in New Issue
Block a user