From 91e2c1705aba5ed8e540255694e63d6fef49ed25 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg Date: Sat, 21 Feb 2026 14:57:15 +0100 Subject: [PATCH] feat: per-line VAT, invoice document types, ledger-based VAT declaration, bank reconciliation, and pagination MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-line VAT rates: - Add generatePerRateLines() to group invoice items by vat_rate with separate revenue + VAT lines per rate group (invoice-entries.ts) - Add getAvailableVatRates() and getVatTreatmentForRate() (vat-rules.ts) - PDF template shows per-line VAT column and per-rate totals for mixed-rate invoices - Invoice create/review UI supports per-line rate selection - Types: add vat_rate/vat_amount to InvoiceItem, vat_rate to CreateInvoiceItemInput Invoice document types (proforma, delivery note): - Add InvoiceDocumentType, document_type and converted_from_id to Invoice type - PDF hides prices for delivery notes, adds proforma notice - Email templates support all document types - mark-paid skips journal entries for non-invoice document types - Migration 031: invoice_document_type Accounting method support: - Add AccountingMethod type (accrual/cash) - Migration 032: add_accounting_method column to company_settings VAT declaration rewrite: - Rewrite to read directly from general ledger (26xx/3xxx account lines) instead of aggregating invoices/transactions/receipts - ACCOUNT_RUTA mapping drives momsdeklaration boxes from GL balances Bank reconciliation: - Transaction ingest now pre-fetches unlinked GL lines and attempts auto-reconciliation during import - Add transaction.reconciled event type - Add ReconciliationMethod type and reconciliation_method on Transaction - Migration 030: bank_reconciliation - New reconciliation engine, API routes, and BankReconciliationView component Pagination (fetchAllRows): - New lib/supabase/fetch-all.ts overcomes PostgREST 1000-row limit - Adopted in all report generators, SIE/SRU export, account list APIs Fiscal period validation: - New validate-period-duration.ts enforces max 18 months per BFL 3 kap. - Applied in period-service.ts and fiscal-periods API Account mapper simplification: - Remove Levenshtein/fuzzy matching, use exact account number match only Swedbank parser improvements: - Support abbreviated headers (Clnr, Bokfdag, Radnr) - Use Referens column as counterparty Chart of accounts management: - Add DELETE endpoint with system account and usage protection - PUT uses partial updates - New AccountCombobox, AddAccountDialog, EditAccountDialog, ChartOfAccountsManager Tax deadline corrections: - Rewrite inkomstdeklaration_ab using Skatteverket lookup table - Rewrite arsredovisning deadline to 7 months after FY end per ÅRL 8:3 Onboarding first fiscal year: - Add first fiscal year toggle with date pickers and 18-month validation UI terminology: - Change "okategoriserad/kategorisera" to "obokförd/bokföra" throughout Report column fix: - Fix start_date/end_date to period_start/period_end in report queries Supplier invoice input: - CreateSupplierInvoiceItemInput uses amount field (legacy quantity/unit_price kept) Misc: - SIE import uses upsert for idempotent account creation - account-descriptions.ts falls back to BAS reference data - Add invoice_default_notes to CompanySettings - Update CLAUDE.md to reflect current project state Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 69 +- app/(dashboard)/bookkeeping/page.tsx | 4 +- app/(dashboard)/import/page.tsx | 22 +- app/(dashboard)/invoices/[id]/page.tsx | 150 +- app/(dashboard)/invoices/new/page.tsx | 209 +- app/(dashboard)/invoices/page.tsx | 28 +- app/(dashboard)/reports/page.tsx | 12 +- app/(dashboard)/settings/page.tsx | 52 +- .../supplier-invoices/new/page.tsx | 148 +- app/(dashboard)/transactions/page.tsx | 37 +- app/(onboarding)/onboarding/page.tsx | 92 +- .../bookkeeping/accounts/[number]/route.ts | 72 +- .../bookkeeping/accounts/activate/route.ts | 84 + .../bookkeeping/accounts/reference/route.ts | 62 + app/api/bookkeeping/accounts/route.ts | 41 +- app/api/bookkeeping/fiscal-periods/route.ts | 7 + app/api/import/sie/create-accounts/route.ts | 43 +- app/api/import/sie/execute/route.ts | 20 +- app/api/import/sie/parse/route.ts | 20 +- app/api/invoices/[id]/convert/route.ts | 137 ++ app/api/invoices/[id]/mark-paid/route.ts | 44 +- app/api/invoices/[id]/mark-sent/route.ts | 10 +- app/api/invoices/[id]/send/route.ts | 28 +- app/api/invoices/preview-pdf/route.ts | 134 ++ app/api/invoices/route.ts | 107 +- app/api/reconciliation/bank/link/route.ts | 33 + app/api/reconciliation/bank/run/route.ts | 45 + app/api/reconciliation/bank/status/route.ts | 20 + app/api/reconciliation/bank/unlink/route.ts | 30 + .../bank/unmatched-entries/route.ts | 20 + app/api/reports/vat-declaration/route.ts | 14 +- app/api/settings/route.ts | 10 + .../supplier-invoices/[id]/credit/route.ts | 46 +- app/api/supplier-invoices/route.ts | 13 +- .../transactions/[id]/match-invoice/route.ts | 140 +- components/bookkeeping/AccountCombobox.tsx | 218 ++ components/bookkeeping/AddAccountDialog.tsx | 225 ++ .../bookkeeping/ChartOfAccountsManager.tsx | 564 +++++ components/bookkeeping/DocumentUploadZone.tsx | 11 +- components/bookkeeping/EditAccountDialog.tsx | 139 ++ components/bookkeeping/JournalEntryForm.tsx | 28 +- components/dashboard/DashboardContent.tsx | 8 +- components/import/AccountMappingStep.tsx | 23 +- components/import/BankFileConfirmStep.tsx | 6 +- components/import/BankFilePreviewStep.tsx | 17 + components/import/BankFileResultStep.tsx | 8 +- components/import/ImportResultStep.tsx | 8 +- components/invoices/InvoiceReviewContent.tsx | 44 +- .../onboarding/Step3TaxRegistration.tsx | 442 +++- components/reports/BankReconciliationView.tsx | 598 +++++ .../SupplierInvoiceReviewContent.tsx | 188 +- .../transactions/BatchCategorySelector.tsx | 7 +- .../transactions/SwipeCategorizationView.tsx | 10 +- components/ui/confirmation-dialog.tsx | 3 + components/ui/empty-state.tsx | 2 +- .../Skärmavbild 2026-01-29 kl. 12.41.50.png | Bin 0 -> 83663 bytes .../Skärmavbild 2026-01-29 kl. 12.42.17.png | Bin 0 -> 86424 bytes .../Skärmavbild 2026-01-29 kl. 12.42.24.png | Bin 0 -> 85220 bytes extensions/ne-bilaga/ne-engine.ts | 14 +- .../components/ReceiptDashboard.tsx | 2 +- .../receipt-ocr/lib/receipt-categorizer.ts | 2 +- extensions/sru-export/sru-engine.ts | 50 +- .../__tests__/invoice-entries.test.ts | 355 ++- .../validate-period-duration.test.ts | 80 + lib/bookkeeping/account-descriptions.ts | 50 +- lib/bookkeeping/bas-reference.ts | 2038 +++++++++++++++++ lib/bookkeeping/invoice-entries.ts | 217 +- lib/bookkeeping/mapping-engine.ts | 2 +- lib/bookkeeping/validate-period-duration.ts | 48 + lib/core/bookkeeping/period-service.ts | 7 + lib/email/invoice-templates.ts | 29 +- lib/events/types.ts | 2 + lib/import/__tests__/account-mapper.test.ts | 351 +++ lib/import/__tests__/sie-import.test.ts | 223 ++ lib/import/__tests__/sie-parser.test.ts | 345 +++ lib/import/account-mapper.ts | 202 +- lib/import/bank-file/formats/swedbank.ts | 96 +- lib/import/sie-import.ts | 1 + lib/invoice/pdf-template.tsx | 129 +- lib/invoice/vat-rules.ts | 53 + .../__tests__/bank-reconciliation.test.ts | 515 +++++ lib/reconciliation/bank-reconciliation.ts | 548 +++++ lib/reports/__tests__/general-ledger.test.ts | 14 +- .../__tests__/journal-register.test.ts | 12 +- .../__tests__/monthly-breakdown.test.ts | 8 +- lib/reports/__tests__/sie-export.test.ts | 2 +- lib/reports/__tests__/trial-balance.test.ts | 2 +- lib/reports/__tests__/vat-declaration.test.ts | 324 ++- lib/reports/general-ledger.ts | 24 +- lib/reports/journal-register.ts | 20 +- lib/reports/monthly-breakdown.ts | 6 +- lib/reports/sie-export.ts | 16 +- lib/reports/trial-balance.ts | 14 +- lib/reports/vat-declaration.ts | 520 +---- lib/supabase/fetch-all.ts | 39 + lib/tax/__tests__/deadline-config.test.ts | 156 ++ lib/tax/deadline-config.ts | 110 +- lib/tax/expense-warnings.ts | 2 +- lib/transactions/__tests__/ingest.test.ts | 126 + lib/transactions/ingest.ts | 36 + scripts/clear-user-data.sql | 115 + .../20240101000030_bank_reconciliation.sql | 70 + .../20240101000031_invoice_document_type.sql | 19 + .../20240101000032_add_accounting_method.sql | 5 + tests/helpers.ts | 1 + types/index.ts | 39 +- 106 files changed, 9950 insertions(+), 1641 deletions(-) create mode 100644 app/api/bookkeeping/accounts/activate/route.ts create mode 100644 app/api/bookkeeping/accounts/reference/route.ts create mode 100644 app/api/invoices/[id]/convert/route.ts create mode 100644 app/api/invoices/preview-pdf/route.ts create mode 100644 app/api/reconciliation/bank/link/route.ts create mode 100644 app/api/reconciliation/bank/run/route.ts create mode 100644 app/api/reconciliation/bank/status/route.ts create mode 100644 app/api/reconciliation/bank/unlink/route.ts create mode 100644 app/api/reconciliation/bank/unmatched-entries/route.ts create mode 100644 components/bookkeeping/AccountCombobox.tsx create mode 100644 components/bookkeeping/AddAccountDialog.tsx create mode 100644 components/bookkeeping/ChartOfAccountsManager.tsx create mode 100644 components/bookkeeping/EditAccountDialog.tsx create mode 100644 components/reports/BankReconciliationView.tsx create mode 100644 dev_docs/bokio/Skärmavbild 2026-01-29 kl. 12.41.50.png create mode 100644 dev_docs/bokio/Skärmavbild 2026-01-29 kl. 12.42.17.png create mode 100644 dev_docs/bokio/Skärmavbild 2026-01-29 kl. 12.42.24.png create mode 100644 lib/bookkeeping/__tests__/validate-period-duration.test.ts create mode 100644 lib/bookkeeping/bas-reference.ts create mode 100644 lib/bookkeeping/validate-period-duration.ts create mode 100644 lib/import/__tests__/account-mapper.test.ts create mode 100644 lib/import/__tests__/sie-import.test.ts create mode 100644 lib/import/__tests__/sie-parser.test.ts create mode 100644 lib/reconciliation/__tests__/bank-reconciliation.test.ts create mode 100644 lib/reconciliation/bank-reconciliation.ts create mode 100644 lib/supabase/fetch-all.ts create mode 100644 lib/tax/__tests__/deadline-config.test.ts create mode 100644 scripts/clear-user-data.sql create mode 100644 supabase/migrations/20240101000030_bank_reconciliation.sql create mode 100644 supabase/migrations/20240101000031_invoice_document_type.sql create mode 100644 supabase/migrations/20240101000032_add_accounting_method.sql diff --git a/CLAUDE.md b/CLAUDE.md index ca8f6687..9222472a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,7 +40,13 @@ app/ components/ ui/ shadcn/ui primitives (button, card, dialog, table, etc.) - [feature]/ Feature-organized components (banking, invoices, suppliers, etc.) + bookkeeping/ Chart of accounts manager, account combobox, add/edit dialogs + calendar/ Calendar views, deadline cards, payment summary, todo widgets + chat/ ChatWidget, ChatPanel, ChatInput, ChatMessage + dashboard/ DashboardContent, DashboardNav, FSkattWarningCard + reports/ Report views (including BankReconciliationView) + settings/ CalendarFeedSettings + [feature]/ Feature-organized components (invoices, suppliers, import, etc.) extensions/ First-party extension implementations ai-categorization/ AI-powered transaction categorization @@ -55,12 +61,15 @@ extensions/ First-party extension implementations lib/ bookkeeping/ Core journal entry engine and all entry generators engine.ts Draft/commit workflow, balance validation, voucher numbering - invoice-entries.ts Sales invoice journal entries + invoice-entries.ts Sales invoice journal entries (supports per-line VAT rates) transaction-entries.ts Bank transaction journal entries supplier-invoice-entries.ts Purchase invoice journal entries category-mapping.ts Category-to-BAS-account mapping mapping-engine.ts Rule-based auto-categorization (MCC codes, merchant patterns) vat-entries.ts VAT line generation + bas-reference.ts BAS account catalog (~180 accounts with metadata, SRU codes) + account-descriptions.ts Human-readable account name lookup + validate-period-duration.ts Fiscal period duration validation (BFL 3 kap.) core/ bookkeeping/ Period service, storno reversal, year-end closing documents/ Document archive (upload, versioning, SHA-256 integrity) @@ -74,23 +83,28 @@ lib/ events/ Event bus (bus.ts, types.ts) extensions/ Extension registry, loader, types import/ SIE and bank file parser - invoice/ VAT rules for invoicing - invoices/ Invoice business logic helpers + bank-file/ Bank file parser with format modules + formats/ camt053, generic-csv, handelsbanken, nordea, seb, swedbank + invoice/ VAT rules, invoice matching (vat-rules.ts, invoice-matching.ts) + invoices/ Invoice business logic (reminder-processor) + reconciliation/ Bank reconciliation engine (4-pass matching algorithm) reports/ Financial reports (trial-balance, income-statement, balance-sheet, vat-declaration, sie-export, supplier-ledger, supplier-reconciliation, general-ledger, journal-register, - ar-ledger, ar-reconciliation) - supabase/ Client setup (client.ts = browser, server.ts = server/admin) + ar-ledger, ar-reconciliation, monthly-breakdown) + supabase/ Client setup (client.ts = browser, server.ts = server/admin, + fetch-all.ts = pagination helper for large queries) tax/ Tax calculations, deadlines, Swedish holidays transactions/ Transaction processing helpers init.ts Extension loader (idempotent, called by API routes) utils.ts Shared utility functions -types/index.ts Canonical type definitions (110+ types, single source of truth) +types/index.ts Canonical type definitions (120+ types, single source of truth) types/chat.ts Chat-specific type definitions tests/helpers.ts Mock factories and fixture builders supabase/migrations/ SQL migration files +scripts/ Utility scripts (clear-user-data.sql) dev_docs/ Extensive project documentation (PRD, architecture, BAS guide, etc.) ``` @@ -118,10 +132,10 @@ The bookkeeping engine (`lib/bookkeeping/engine.ts`) is the most critical system | Function | File | Purpose | |----------|------|---------| -| `createInvoiceJournalEntry()` | `invoice-entries.ts` | Debit 1510, Credit 30xx + 26xx VAT | +| `createInvoiceJournalEntry()` | `invoice-entries.ts` | Debit 1510, Credit 30xx + 26xx VAT (per-line VAT rates) | | `createInvoicePaymentJournalEntry()` | `invoice-entries.ts` | Debit 1930, Credit 1510 | -| `createCreditNoteJournalEntry()` | `invoice-entries.ts` | Reverses original invoice entry | -| `createInvoiceCashEntry()` | `invoice-entries.ts` | Cash method: revenue + VAT at payment | +| `createCreditNoteJournalEntry()` | `invoice-entries.ts` | Reverses original invoice entry (per-rate lines) | +| `createInvoiceCashEntry()` | `invoice-entries.ts` | Cash method: revenue + VAT at payment (per-rate) | | `createTransactionJournalEntry()` | `transaction-entries.ts` | Maps bank transactions via MappingResult | | `createSupplierInvoiceRegistrationEntry()` | `supplier-invoice-entries.ts` | Debit expense + 2641, Credit 2440 | | `createSupplierInvoicePaymentEntry()` | `supplier-invoice-entries.ts` | Debit 2440, Credit 1930 | @@ -147,6 +161,23 @@ The bookkeeping engine (`lib/bookkeeping/engine.ts`) is the most critical system `standard_25`, `reduced_12`, `reduced_6`, `reverse_charge`, `export`, `exempt` +### Per-Line VAT + +Invoice items support individual `vat_rate` values, enabling mixed-rate invoices. The helper `generatePerRateLines()` in `invoice-entries.ts` groups items by VAT rate and creates separate revenue + VAT account lines per rate group. Available rates depend on customer type — use `getAvailableVatRates(customerType, vatNumberValidated)` from `lib/invoice/vat-rules.ts`. + +### Bank Reconciliation + +The reconciliation engine (`lib/reconciliation/bank-reconciliation.ts`) matches bank transactions to journal entry lines on account 1930 using a 4-pass algorithm: + +| Pass | Method | Confidence | Match Criteria | +|------|--------|------------|----------------| +| 1 | `auto_exact` | 0.95 | Exact amount + exact date | +| 2 | `auto_reference` | 0.90 | Exact amount + reference/description match | +| 3 | `auto_date_range` | 0.85 | Exact amount + date within ±3 days | +| 4 | `auto_fuzzy` | 0.75 | Fuzzy amount (±0.01) + exact date | + +Manual linking (`manual` method) is also supported. Only SEK transactions are reconciled. Greedy assignment prevents double-matching. + --- ## Accounting Guard Rails @@ -251,6 +282,7 @@ All defined in `lib/events/types.ts`: | `receipt.extracted` | `{ receipt, documentId, confidence, userId }` | | `receipt.matched` | `{ receipt, transaction, confidence, autoMatched, userId }` | | `receipt.confirmed` | `{ receipt, businessTotal, privateTotal, userId }` | +| `transaction.reconciled` | `{ transaction, journalEntryId, method, userId }` | | `audit.security_event` | `{ event, userId }` | ### Event Bus Behavior @@ -305,10 +337,17 @@ mockResult({ data: makeTransaction(), error: null }) ### Reference Tests - `lib/bookkeeping/__tests__/engine.test.ts` — Balance validation +- `lib/bookkeeping/__tests__/invoice-entries.test.ts` — Per-line VAT, mixed-rate invoices, credit notes - `lib/core/bookkeeping/__tests__/storno-service.test.ts` — Complex mock queues - `lib/core/documents/__tests__/document-service.test.ts` — Storage mocking - `lib/events/__tests__/bus.test.ts` — Event bus behavior - `lib/extensions/__tests__/registry.test.ts` — Extension registration +- `lib/import/__tests__/sie-parser.test.ts` — SIE file parsing +- `lib/import/bank-file/__tests__/parser.test.ts` — Bank file format parsing +- `lib/reconciliation/__tests__/bank-reconciliation.test.ts` — Reconciliation matching algorithm +- `lib/reports/__tests__/vat-declaration.test.ts` — VAT declaration report +- `lib/tax/__tests__/deadline-config.test.ts` — Tax deadline configuration +- `lib/transactions/__tests__/ingest.test.ts` — Transaction ingestion and dedup --- @@ -316,11 +355,11 @@ mockResult({ data: makeTransaction(), error: null }) ### Location -`supabase/migrations/` — currently 28 files numbered `20240101000001` through `20240101000028`. +`supabase/migrations/` — currently 32 files numbered `20240101000001` through `20240101000032`. ### Naming Convention -`YYYYMMDD00NNNN_descriptive_name.sql` — next migration: `20240101000029_*.sql` +`YYYYMMDD00NNNN_descriptive_name.sql` — next migration: `20240101000033_*.sql` ### Migration Rules @@ -354,6 +393,12 @@ mockResult({ data: makeTransaction(), error: null }) - `set_committed_at` — Auto-sets timestamp on draft-to-posted transition - `calculate_retention_expiry` — Auto-sets `retention_expires_at = period_end + 7 years` +### Recent Migrations + +- **Migration 030 (`bank_reconciliation`)** — Adds `reconciliation_method` column to `transactions` (CHECK constraint for method types), indexes for unmatched transaction lookup, and RPC `get_unlinked_1930_lines()` for finding unreconciled GL lines. +- **Migration 031 (`invoice_document_type`)** — Adds `document_type` column to `invoices` (CHECK: invoice/proforma/delivery_note, default 'invoice') and `converted_from_id` FK for tracking proforma-to-invoice conversions. +- **Migration 032 (`add_accounting_method`)** — Adds `accounting_method` column to `company_settings` (CHECK: accrual/cash, default 'accrual') to support kontantmetoden vs faktureringsmetoden. + --- ## Type System diff --git a/app/(dashboard)/bookkeeping/page.tsx b/app/(dashboard)/bookkeeping/page.tsx index fa16d229..0c5c7254 100644 --- a/app/(dashboard)/bookkeeping/page.tsx +++ b/app/(dashboard)/bookkeeping/page.tsx @@ -6,7 +6,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' import { Button } from '@/components/ui/button' import JournalEntryList from '@/components/bookkeeping/JournalEntryList' import JournalEntryForm from '@/components/bookkeeping/JournalEntryForm' -import ChartOfAccounts from '@/components/bookkeeping/ChartOfAccounts' +import ChartOfAccountsManager from '@/components/bookkeeping/ChartOfAccountsManager' import { Lock } from 'lucide-react' export default function BookkeepingPage() { @@ -45,7 +45,7 @@ export default function BookkeepingPage() { - + diff --git a/app/(dashboard)/import/page.tsx b/app/(dashboard)/import/page.tsx index a0719c2e..83ee858d 100644 --- a/app/(dashboard)/import/page.tsx +++ b/app/(dashboard)/import/page.tsx @@ -133,8 +133,10 @@ function BankFileImportWizard() { description: `${txCount} transaktioner hittades`, }) } else if (data.data.parse_result.format === 'generic_csv' || !data.data.detected_format) { - // Unrecognized format — show upload step with error setBankError('Kunde inte identifiera bankformatet. Välj bank manuellt eller använd "Annan CSV".') + } else { + // Format detected but no transactions parsed — parser couldn't extract rows + setBankError('Filen kunde läsas men inga transaktioner hittades. Kontrollera att filen innehåller transaktionsdata och inte bara rubriker.') } } catch (err) { setBankError(err instanceof Error ? err.message : 'Kunde inte läsa filen') @@ -293,8 +295,6 @@ function BankFileImportWizard() { // SIE Import Wizard (unchanged, extracted into component) // ============================================================ -const SIE_STEPS: ImportWizardStep[] = ['upload', 'preview', 'mapping', 'review', 'result'] - const SIE_STEP_LABELS: Record = { upload: 'Ladda upp', preview: 'Förhandsgranskning', @@ -320,8 +320,14 @@ function SIEImportWizard() { const [_sieAccounts, setSieAccounts] = useState<{ number: string; name: string }[]>([]) const [isCreatingAccounts, setIsCreatingAccounts] = useState(false) - const currentStepIndex = SIE_STEPS.indexOf(step) - const progress = ((currentStepIndex + 1) / SIE_STEPS.length) * 100 + // Skip the mapping step when all accounts are already mapped + const hasUnmapped = mappings.some((m) => !m.targetAccount) + const sieSteps: ImportWizardStep[] = hasUnmapped + ? ['upload', 'preview', 'mapping', 'review', 'result'] + : ['upload', 'preview', 'review', 'result'] + + const currentStepIndex = sieSteps.indexOf(step) + const progress = ((currentStepIndex + 1) / sieSteps.length) * 100 const handleFileSelect = useCallback(async (selectedFile: File) => { setFile(selectedFile) @@ -490,7 +496,7 @@ function SIEImportWizard() { }, [file, mappings, toast]) const goToStep = (targetStep: ImportWizardStep) => { setStep(targetStep); setError(null) } - const goBack = () => { const i = SIE_STEPS.indexOf(step); if (i > 0) setStep(SIE_STEPS[i - 1]) } + const goBack = () => { const i = sieSteps.indexOf(step); if (i > 0) setStep(sieSteps[i - 1]) } const handleNewImport = () => { setStep('upload'); setFile(null); setParsed(null); setMappings([]) @@ -504,7 +510,7 @@ function SIEImportWizard() {
- {SIE_STEPS.map((s, i) => ( + {sieSteps.map((s, i) => ( {SIE_STEP_LABELS[s]} @@ -519,7 +525,7 @@ function SIEImportWizard() { {step === 'preview' && preview && ( goToStep('mapping')} onBack={goBack} /> + onContinue={() => goToStep(hasUnmapped ? 'mapping' : 'review')} onBack={goBack} /> )} {step === 'mapping' && ( = { draft: { label: 'Utkast', variant: 'secondary', icon: FileText }, @@ -63,6 +63,8 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st const [reminders, setReminders] = useState([]) const [creditNote, setCreditNote] = useState(null) const [originalInvoice, setOriginalInvoice] = useState(null) + const [convertedFromInvoice, setConvertedFromInvoice] = useState(null) + const [isConverting, setIsConverting] = useState(false) const [isLoading, setIsLoading] = useState(true) const [isUpdating, setIsUpdating] = useState(false) const [isDownloading, setIsDownloading] = useState(false) @@ -139,6 +141,19 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st } } + // If this invoice was converted from a proforma, fetch it + if (data.converted_from_id) { + const { data: convertedData } = await supabase + .from('invoices') + .select('id, invoice_number') + .eq('id', data.converted_from_id) + .single() + + if (convertedData) { + setConvertedFromInvoice(convertedData as Invoice) + } + } + setIsLoading(false) } @@ -235,6 +250,38 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st setIsSendingEmail(false) } + async function convertToInvoice() { + if (!invoice) return + setIsConverting(true) + + try { + const response = await fetch(`/api/invoices/${invoice.id}/convert`, { + method: 'POST', + }) + + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Kunde inte konvertera proformafakturan') + } + + toast({ + title: 'Konverterad till faktura', + description: `Faktura ${data.data.invoice_number} har skapats`, + }) + + router.push(`/invoices/${data.data.id}`) + } catch (error) { + toast({ + title: 'Fel', + description: error instanceof Error ? error.message : 'Kunde inte konvertera', + variant: 'destructive', + }) + } + + setIsConverting(false) + } + async function downloadPDF() { if (!invoice) return @@ -288,6 +335,11 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st const StatusIcon = status.icon const customer = invoice.customer const customerHasEmail = !!customer.email + const docType = ((invoice as Invoice & { document_type?: InvoiceDocumentType }).document_type || 'invoice') as InvoiceDocumentType + const isProforma = docType === 'proforma' + const isDeliveryNote = docType === 'delivery_note' + const isRealInvoice = docType === 'invoice' + const docLabel = isProforma ? 'Proformafaktura' : isDeliveryNote ? 'Följesedel' : 'Faktura' return (
@@ -300,6 +352,12 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st

{invoice.invoice_number}

+ {isProforma && ( + Proforma + )} + {isDeliveryNote && ( + Följesedel + )} {status.label} @@ -314,7 +372,17 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st {/* Actions */}
- {invoice.status === 'draft' && ( + {isProforma && invoice.status !== 'cancelled' && ( + + )} + {invoice.status === 'draft' && !isDeliveryNote && ( customerHasEmail ? ( ) )} - {(invoice.status === 'sent' || invoice.status === 'overdue') && ( + {isDeliveryNote && invoice.status === 'draft' && ( + + )} + {(invoice.status === 'sent' || invoice.status === 'overdue') && isRealInvoice && ( + + + + )} + {/* Status actions */} {invoice.status !== 'cancelled' && invoice.status !== 'credited' && !invoice.credited_invoice_id && ( @@ -663,9 +760,34 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st Åtgärder - {invoice.status === 'draft' && ( + {isProforma && ( <> - {customerHasEmail ? ( + + + + )} + {!isProforma && invoice.status === 'draft' && ( + <> + {!isDeliveryNote && customerHasEmail ? ( )} - {(invoice.status === 'sent' || invoice.status === 'overdue') && ( + {(invoice.status === 'sent' || invoice.status === 'overdue') && isRealInvoice && ( <>
-

Ny faktura

-

Skapa en ny faktura

+

+ {watchDocumentType === 'proforma' ? 'Ny proformafaktura' : watchDocumentType === 'delivery_note' ? 'Ny följesedel' : 'Ny faktura'} +

+

+ {watchDocumentType === 'proforma' ? 'Skapa en proformafaktura (ingen bokföring)' : watchDocumentType === 'delivery_note' ? 'Skapa en följesedel (utan priser)' : 'Skapa en ny faktura'} +

@@ -300,7 +391,7 @@ export default function NewInvoicePage() {
{fields.map((field, index) => (
-
+
)}
-
+
+
+ + ( + + )} + /> +
+ } > ({ + ...item, + vat_rate: item.vat_rate ?? (vatRules?.rate || 25), + }))} subtotal={subtotal} vatRate={vatRules.rate} vatAmount={vatAmount} diff --git a/app/(dashboard)/invoices/page.tsx b/app/(dashboard)/invoices/page.tsx index 630187f6..f10a5a15 100644 --- a/app/(dashboard)/invoices/page.tsx +++ b/app/(dashboard)/invoices/page.tsx @@ -11,7 +11,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { PageHeader } from '@/components/ui/page-header' import { useToast } from '@/components/ui/use-toast' import { formatCurrency, formatDate } from '@/lib/utils' -import { Plus, Search, Receipt, FileText, Send, CheckCircle, Clock, XCircle, ReceiptText, AlertTriangle } from 'lucide-react' +import { Plus, Search, Receipt, FileText, Send, CheckCircle, Clock, XCircle, ReceiptText, AlertTriangle, FileQuestion, Truck } from 'lucide-react' import { EmptyInvoices } from '@/components/ui/empty-state' import type { Invoice, InvoiceStatus } from '@/types' @@ -82,11 +82,14 @@ export default function InvoicesPage() { (invoice.customer as { name: string })?.name?.toLowerCase().includes(searchTerm.toLowerCase()) const isCreditNote = !!invoice.credited_invoice_id + const docType = (invoice as Invoice & { document_type?: string }).document_type || 'invoice' const matchesTab = activeTab === 'all' || - (activeTab === 'unpaid' && ['sent', 'overdue'].includes(invoice.status) && !isCreditNote) || + (activeTab === 'unpaid' && ['sent', 'overdue'].includes(invoice.status) && !isCreditNote && docType === 'invoice') || (activeTab === 'credit' && isCreditNote) || - invoice.status === activeTab + (activeTab === 'proforma' && docType === 'proforma') || + (activeTab === 'delivery_note' && docType === 'delivery_note') || + (activeTab !== 'proforma' && activeTab !== 'delivery_note' && invoice.status === activeTab) return matchesSearch && matchesTab }) @@ -181,6 +184,8 @@ export default function InvoicesPage() { Obetalda Betalda Utkast + Proforma + Följesedel Kredit @@ -232,9 +237,12 @@ export default function InvoicesPage() { {filteredInvoices.map((invoice) => { const status = statusConfig[invoice.status] const isCreditNote = !!invoice.credited_invoice_id - const StatusIcon = isCreditNote ? ReceiptText : status.icon + const docType = (invoice as Invoice & { document_type?: string }).document_type || 'invoice' + const isProforma = docType === 'proforma' + const isDeliveryNote = docType === 'delivery_note' + const StatusIcon = isCreditNote ? ReceiptText : isProforma ? FileQuestion : isDeliveryNote ? Truck : status.icon const relativeTime = invoice.due_date ? getRelativeTimeLabel(invoice.due_date, invoice.status) : null - const borderClass = isCreditNote ? 'border-l-4 border-l-destructive/50' : `border-l-4 ${status.borderColor}` + const borderClass = isCreditNote ? 'border-l-4 border-l-destructive/50' : isProforma ? 'border-l-4 border-l-blue-400' : isDeliveryNote ? 'border-l-4 border-l-emerald-400' : `border-l-4 ${status.borderColor}` return ( @@ -253,6 +261,16 @@ export default function InvoicesPage() { Kredit )} + {isProforma && ( + + Proforma + + )} + {isDeliveryNote && ( + + Följesedel + + )} {status.label} diff --git a/app/(dashboard)/reports/page.tsx b/app/(dashboard)/reports/page.tsx index 14120498..825f0d8e 100644 --- a/app/(dashboard)/reports/page.tsx +++ b/app/(dashboard)/reports/page.tsx @@ -6,10 +6,11 @@ import { Button } from '@/components/ui/button' import { Label } from '@/components/ui/label' import { Badge } from '@/components/ui/badge' import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' -import { Download, FileText, FileDown, TrendingUp, Scale, AlertCircle, Receipt, Briefcase, Building2, BookOpen, List, Users, ChevronDown, ChevronRight } from 'lucide-react' +import { Download, FileText, FileDown, TrendingUp, Scale, AlertCircle, Receipt, Briefcase, Building2, BookOpen, List, Users, ChevronDown, ChevronRight, ArrowLeftRight } from 'lucide-react' import { AccountNumber } from '@/components/ui/account-number' import { NEDeclarationView } from '@/extensions/ne-bilaga/NEDeclarationView' import { SRUExportView } from '@/extensions/sru-export/SRUExportView' +import { BankReconciliationView } from '@/components/reports/BankReconciliationView' import { TrialBalanceChart } from '@/components/reports/TrialBalanceChart' import { VatCompositionChart } from '@/components/reports/VatCompositionChart' import { IncomeExpenseChart } from '@/components/reports/IncomeExpenseChart' @@ -82,7 +83,7 @@ export default function ReportsPage() { > {periods.map((p) => ( ))} @@ -146,6 +147,10 @@ export default function ReportsPage() { Lev.reskontra + + + Bankavstämning +
@@ -182,6 +187,9 @@ export default function ReportsPage() { + + + ) : ( diff --git a/app/(dashboard)/settings/page.tsx b/app/(dashboard)/settings/page.tsx index 579379db..0068f4a4 100644 --- a/app/(dashboard)/settings/page.tsx +++ b/app/(dashboard)/settings/page.tsx @@ -6,6 +6,7 @@ import { createClient } from '@/lib/supabase/client' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' +import { Textarea } from '@/components/ui/textarea' import { Label } from '@/components/ui/label' import { Badge } from '@/components/ui/badge' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' @@ -134,6 +135,7 @@ export default function SettingsPage() { next_invoice_number: parseInt(formData.get('next_invoice_number') as string) || 1, invoice_default_days: parseInt(formData.get('invoice_default_days') as string) || 30, accounting_method: formData.get('accounting_method') as string || 'accrual', + invoice_default_notes: (formData.get('invoice_default_notes') as string) || null, } try { @@ -457,37 +459,35 @@ export default function SettingsPage() {
- {settings?.entity_type === 'aktiebolag' ? ( - <> - -
- - - Obligatorisk för aktiebolag - -
- - ) : ( - - )} +

{settings?.entity_type === 'aktiebolag' - ? 'Aktiebolag måste använda faktureringsmetoden enligt BFL.' + ? 'Aktiebolag med omsättning över 3 MSEK måste använda faktureringsmetoden enligt BFL. Mindre aktiebolag kan välja kontantmetoden.' : 'Kontantmetoden är tillgänglig för enskild firma med omsättning under 3 MSEK.'}

+ +
+ +