feat(accounting): split Incoming invoices vs Expenses - flags, schema, settings (stage 1)
Foundation for separating external supplier invoices from internal expenses, per design review. This stage is additive + buildable; the service/route/UI data rework follows in stage 2. - Migration 126: incoming invoices own their payable on inbound_documents (supplier_paid/at/method/ref + disposition + tax_treatment + booking event_id + category_id + re-bill markup/linkage); expenses gain kind (amount/mileage/ per_diem) + quantity + snapshotted rate_minor. Additive, hasColumn-guarded. - Migration 127: seed `expenses` feature flag (default off) + accounting app_settings (accounting_km_rate_minor=70, accounting_per_diem_rate_minor=0, accounting_require_proof=false). - Backend: `expenses` added to feature-flag known/defaults/dependency (forced off when the accounting master is off); new PUT /admin/settings/accounting (read via the generic GET /:type). - Frontend: `expenses` flag (type + context + dependency); Features tab gets an Expenses sub-card; the Expenses sub-nav + route now gate on `expenses` (not incomingInvoices); AccountingIndex prefers inbox -> expenses -> tax. - i18n: settings.features.expenses.* (EN + DE). Verified: node -c; migration 124->126->127 harness (new columns, flag, settings + idempotency); en/de JSON valid; npm run build green.
This commit is contained in:
@@ -272,6 +272,8 @@ function App() {
|
||||
<Route path="accounting" element={<AccountingLayout />}>
|
||||
<Route element={<RequireFeature flag="incomingInvoices" />}>
|
||||
<Route path="inbox" element={<AccountingInboxPage />} />
|
||||
</Route>
|
||||
<Route element={<RequireFeature flag="expenses" />}>
|
||||
<Route path="expenses" element={<ExpensesLedgerPage />} />
|
||||
</Route>
|
||||
<Route element={<RequireFeature flag="taxReport" />}>
|
||||
|
||||
@@ -41,7 +41,7 @@ export const AccountingLayout: React.FC = () => {
|
||||
to: '/admin/accounting/expenses',
|
||||
label: t('accounting.subnav.expenses', 'Expenses'),
|
||||
icon: Wallet,
|
||||
featureFlag: 'incomingInvoices',
|
||||
featureFlag: 'expenses',
|
||||
},
|
||||
{
|
||||
key: 'tax-report',
|
||||
@@ -159,6 +159,7 @@ export const AccountingLayout: React.FC = () => {
|
||||
export const AccountingIndex: React.FC = () => {
|
||||
const { flags } = useFeatureFlags();
|
||||
if (flags.incomingInvoices) return <Navigate to="/admin/accounting/inbox" replace />;
|
||||
if (flags.expenses) return <Navigate to="/admin/accounting/expenses" replace />;
|
||||
if (flags.taxReport) return <Navigate to="/admin/accounting/tax-report" replace />;
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -48,9 +48,12 @@ export const DEFAULT_FLAGS: FeatureFlags = {
|
||||
// Accounting (migration 122). Top-level MASTER for the Accounting
|
||||
// section (separate from CRM). Sub-features below require it.
|
||||
accounting: false,
|
||||
// Incoming invoices (migration 124) — supplier-invoice capture +
|
||||
// expenses + re-bill. Accounting sub-feature; requires `accounting`.
|
||||
// Incoming invoices (migration 124) — external supplier-invoice capture +
|
||||
// re-bill. Accounting sub-feature; requires `accounting`.
|
||||
incomingInvoices: false,
|
||||
// Expenses (migration 127) — internal expenses (mileage / per-diem / cash).
|
||||
// Separate Accounting sub-feature; requires `accounting`.
|
||||
expenses: false,
|
||||
};
|
||||
|
||||
export const FEATURE_FLAGS_QUERY_KEY = ['feature-flags'] as const;
|
||||
@@ -87,6 +90,7 @@ function applyDependencyRules(flags: FeatureFlags): FeatureFlags {
|
||||
if (out.accounting === false) {
|
||||
out.taxReport = false;
|
||||
out.incomingInvoices = false;
|
||||
out.expenses = false;
|
||||
}
|
||||
// Clients parent flag is DERIVED from its children. Admins don't
|
||||
// toggle it directly — enabling any CRM-area sub-feature
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
Calculator,
|
||||
Landmark,
|
||||
ScanLine,
|
||||
Wallet,
|
||||
} from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Card } from '../../../components/common';
|
||||
@@ -331,6 +332,25 @@ export const FeaturesTab: React.FC = () => {
|
||||
'Enable Accounting first — Incoming invoices live in the Accounting section.',
|
||||
) : undefined}
|
||||
/>
|
||||
|
||||
<FeatureCard
|
||||
icon={Wallet}
|
||||
title={t('settings.features.expenses.title', 'Expenses')}
|
||||
description={t(
|
||||
'settings.features.expenses.description',
|
||||
'Internal expenses (mileage, per-diem, cash) booked to an event or the company, with optional proof. Separate from incoming supplier invoices. Configure km / per-diem rates and the proof requirement in the Accounting settings tab.',
|
||||
)}
|
||||
status="new"
|
||||
statusLabel={statusLabel('new')}
|
||||
sidebarLabel={t('settings.features.expenses.sidebar', 'Expenses')}
|
||||
enabled={staged.expenses}
|
||||
onToggle={(next) => setFlag('expenses', next)}
|
||||
disabled={!staged.accounting}
|
||||
lockedReason={!staged.accounting ? t(
|
||||
'settings.features.expenses.requiresAccounting',
|
||||
'Enable Accounting first — Expenses live in the Accounting section.',
|
||||
) : undefined}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* Insights & Access */}
|
||||
|
||||
@@ -1650,6 +1650,12 @@
|
||||
"sidebar": "Eingang",
|
||||
"requiresAccounting": "Bitte zuerst Buchhaltung aktivieren — Eingangsrechnungen liegen im Buchhaltungsbereich."
|
||||
},
|
||||
"expenses": {
|
||||
"title": "Aufwände",
|
||||
"description": "Interne Aufwände (Kilometer, Spesenpauschale, Barbelege), gebucht auf ein Event oder die Firma, mit optionalem Beleg. Getrennt von Eingangsrechnungen.",
|
||||
"sidebar": "Aufwände",
|
||||
"requiresAccounting": "Bitte zuerst Buchhaltung aktivieren — Aufwände liegen im Buchhaltungsbereich."
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Statistiken",
|
||||
"description": "Speichernutzung, Galerie-Aufrufe, Download-Zahlen und Statistiken pro Veranstaltung."
|
||||
|
||||
@@ -1208,6 +1208,12 @@
|
||||
"sidebar": "Incoming",
|
||||
"requiresAccounting": "Enable Accounting first — Incoming invoices live in the Accounting section."
|
||||
},
|
||||
"expenses": {
|
||||
"title": "Expenses",
|
||||
"description": "Internal expenses (mileage, per-diem, cash) booked to an event or the company, with optional proof. Separate from incoming supplier invoices.",
|
||||
"sidebar": "Expenses",
|
||||
"requiresAccounting": "Enable Accounting first — Expenses live in the Accounting section."
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analytics",
|
||||
"description": "Storage usage, gallery views, download counts, and per-event stats."
|
||||
|
||||
@@ -46,9 +46,12 @@ export type FeatureKey =
|
||||
// section (separate from CRM). Its sub-features (tax export, incoming
|
||||
// invoices) require it. Strictly opt-in.
|
||||
| 'accounting'
|
||||
// Incoming invoices (migration 124) — supplier-invoice capture +
|
||||
// expenses + re-bill. Accounting sub-feature; requires `accounting`.
|
||||
| 'incomingInvoices';
|
||||
// Incoming invoices (migration 124) — external supplier-invoice capture +
|
||||
// re-bill. Accounting sub-feature; requires `accounting`.
|
||||
| 'incomingInvoices'
|
||||
// Expenses (migration 127) — internal expenses (mileage / per-diem / cash).
|
||||
// Separate Accounting sub-feature; requires `accounting`.
|
||||
| 'expenses';
|
||||
|
||||
export type FeatureFlags = Record<FeatureKey, boolean>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user