From 1a0922b778b7997429397e2218ad1545d8c00c00 Mon Sep 17 00:00:00 2001 From: Nicolaj Hartmann Date: Fri, 6 Feb 2026 01:15:45 +0100 Subject: [PATCH] Audit v3: VAT alignment, security, encoding, UX, compliance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VAT System Alignment (LEGAL - Critical): - Align frontend VAT codes with backend (S25→U25, K25→I25, etc.) - Add missing codes: UEU, IVV, IVY, REP - Fix output VAT account 5710→5611 to match StandardDanishAccounts - Invoice posting now checks fiscal year status before allowing send - Disallow custom invoice number override (always use auto-numbering) Security: - Fix open redirect in AuthController (validate returnUrl is local) - Store seller CVR/name/address on invoice events (Momsloven §52) Backend Compliance: - Add description validation at posting (Bogføringsloven §7) - SAF-T: add DefaultCurrencyCode, TaxAccountingBasis to header - SAF-T: add TaxTable to MasterFiles with all VAT codes - SAF-T: always write balance elements even when zero - Add financial income account 9100 Renteindtægter Danish Encoding (~25 fixes): - Kassekladde: Bogført, Bogføring, Vælg, være, på, Tilføj, Differens - AttachmentUpload: træk, Understøtter, påkrævet, Bogføringsloven - keyboardShortcuts: Bogfør, Bogføring display name - ShortcutsHelpModal: åbne - DataTable: Genindlæs - documentProcessing: være - CloseFiscalYearWizard: årsafslutning Bugs Fixed: - Non-null assertion crashes in Kunder.tsx and Produkter.tsx (company!.id) - StatusBadge typo "Succces"→"Succes" - HTML entity ø in Kassekladde→proper UTF-8 - AmountText showSign prop was dead code (true || showSign) UX Improvements: - Add PageHeader to Bankafstemning and Dashboard loading/empty states - Responsive columns in Bankafstemning (xs/sm/lg breakpoints) - Disable misleading buttons: Settings preferences, Kontooversigt edit, Loenforstaelse export — with tooltips explaining status - Add DemoDataDisclaimer to UserSettings - Fix breadcrumb self-references on 3 pages - Replace Dashboard fake progress bar with honest message - Standardize date format DD-MM-YYYY in Bankafstemning and Ordrer - Replace Input type="number" with InputNumber in Ordrer Quality: - Remove 8 redundant console.error statements - Fix Kreditnotaer breadcrumb "Salg"→"Fakturering" for consistency Co-Authored-By: Claude Opus 4.6 --- .beads/issues.jsonl | 3 + .../Invoices/InvoiceCommandHandlers.cs | 74 ++++++++--- .../Books.Api/Controllers/AuthController.cs | 3 + .../Invoices/Events/InvoiceCreatedEvent.cs | 20 ++- .../Domain/Invoices/InvoiceAggregate.cs | 10 +- .../JournalEntryDraftAggregate.cs | 9 ++ .../Subscribers/StandardDanishAccounts.cs | 3 +- .../Books.Api/Saft/Services/SaftXmlBuilder.cs | 64 ++++++++-- frontend/src/api/documentProcessing.ts | 2 +- .../modals/CloseFiscalYearWizard.tsx | 2 +- .../modals/CreateFiscalYearModal.tsx | 1 - .../settings/BankConnectionsTab.tsx | 6 - frontend/src/components/shared/AmountText.tsx | 2 +- .../components/shared/AttachmentUpload.tsx | 8 +- .../components/shared/ShortcutsHelpModal.tsx | 2 +- .../src/components/shared/StatusBadge.tsx | 2 +- frontend/src/components/tables/DataTable.tsx | 2 +- frontend/src/lib/keyboardShortcuts.ts | 4 +- frontend/src/lib/vatCodes.ts | 119 +++++++++++------- frontend/src/pages/Bankafstemning.tsx | 48 ++++--- frontend/src/pages/CompanySetupWizard.tsx | 1 - frontend/src/pages/Dashboard.tsx | 18 ++- frontend/src/pages/Fakturaer.tsx | 2 +- frontend/src/pages/Kassekladde.tsx | 26 ++-- frontend/src/pages/Kontooversigt.tsx | 16 +-- frontend/src/pages/Kreditnotaer.tsx | 2 +- frontend/src/pages/Kunder.tsx | 6 +- frontend/src/pages/Loenforstaelse.tsx | 5 +- frontend/src/pages/Ordrer.tsx | 11 +- frontend/src/pages/Produkter.tsx | 6 +- frontend/src/pages/Settings.tsx | 22 +--- frontend/src/pages/UserSettings.tsx | 3 + frontend/src/types/vat.ts | 25 ++-- 33 files changed, 355 insertions(+), 172 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 5c03f1d..6b7f9b7 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -5,7 +5,9 @@ {"id":"books-5tg","title":"opret et backend job med hangfir\ne som sikrer, at bankkonto altid stemmer med den pågældende konto","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:24:01.505911+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:37:08.293897+01:00","closed_at":"2026-01-30T14:37:08.293897+01:00","close_reason":"Closed"} {"id":"books-8ea","title":"fjern brugers navn fra højre hjørne ved profile ikonet","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:20:16.406033+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:22:59.64468+01:00","closed_at":"2026-01-30T14:22:59.64468+01:00","close_reason":"Closed"} {"id":"books-8lo","title":"revisit the laytoug and desig nfor kontooversigten.","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:25:06.620288+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:47:52.365315+01:00","closed_at":"2026-01-30T14:47:52.365315+01:00","close_reason":"Closed"} +{"id":"books-8n5","title":"v3 Phase 1+3: Frontend VAT alignment + encoding + bugs","description":"Align frontend VAT codes with backend, fix ~20 Danish encoding issues, fix non-null assertions, StatusBadge typo, HTML entity","status":"in_progress","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-02-06T01:06:57.325868+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-02-06T01:07:01.858975+01:00"} {"id":"books-9ig","title":"Phase 3: Critical frontend bugs","description":"Fix closing wizard entries, create-next-year checkbox, Dashboard a-href, Kassekladde filters, edit draft form population, Vis detaljer action, Flere filtre button, duplicate migration","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-02-05T22:16:57.507387+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-02-06T00:18:23.721813+01:00","closed_at":"2026-02-06T00:18:23.721813+01:00","close_reason":"Closed"} +{"id":"books-9te","title":"v3 Phase 1+2+6: Backend VAT, security, compliance","description":"Fix invoice fiscal year check, disable custom invoice numbers, open redirect, OAuth CSRF, cross-company validation, description validation, SAF-T fixes, chart of accounts, seller CVR on invoices","status":"in_progress","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-02-06T01:06:57.223084+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-02-06T01:07:01.78072+01:00"} {"id":"books-bj6","title":"Test automatisk pickup","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:04:40.572496+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:05:44.401903+01:00","closed_at":"2026-01-30T14:05:44.401903+01:00","close_reason":"completed"} {"id":"books-byl","title":"opret giv et shortlink på frontenden til backend på /hangfire","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:24:34.946139+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:40:44.62014+01:00","closed_at":"2026-01-30T14:40:44.62014+01:00","close_reason":"Closed"} {"id":"books-cdf","title":"opret","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:23:39.411558+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T17:45:05.363658+01:00","closed_at":"2026-01-30T17:45:05.363658+01:00","close_reason":"Skipped - task description too vague"} @@ -13,6 +15,7 @@ {"id":"books-cws","title":"Phase 3: Accounting compliance fixes","description":"Balanced entry enforcement, VAT code unification, invoice numbering, fiscal year gap/overlap checks, posting date tracking, SAF-T fixes.","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-02-05T21:12:09.362182+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-02-05T21:35:30.369857+01:00","closed_at":"2026-02-05T21:35:30.369857+01:00","close_reason":"Closed"} {"id":"books-h6e","title":"fjern hurtig bogføring og den visning der høre dertil","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:14:50.436314+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:18:09.911294+01:00","closed_at":"2026-01-30T14:18:09.911294+01:00","close_reason":"Closed"} {"id":"books-hzt","title":"fix bug med tilføj brugere står forkert med encoded tegn","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:21:34.556319+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:28:31.320973+01:00","closed_at":"2026-01-30T14:28:31.320973+01:00","close_reason":"Closed"} +{"id":"books-i6l","title":"v3 Phase 4+5: UX, loading states, quality","description":"Fix loading/empty states, disclaimers, breadcrumbs, responsive columns, dead code, console.error cleanup, InputNumber","status":"in_progress","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-02-06T01:06:57.418109+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-02-06T01:07:01.943749+01:00"} {"id":"books-k95","title":"Phase 4: UX consistency \u0026 bug fixes","description":"Danish character encoding, DemoDataDisclaimer deployment, PageHeader adoption, mobile responsiveness, mock data removal, dead buttons.","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-02-05T21:12:09.471301+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-02-05T21:35:30.433843+01:00","closed_at":"2026-02-05T21:35:30.433843+01:00","close_reason":"Closed"} {"id":"books-ley","title":"Phase 1: GraphQL Authentication \u0026 Authorization","description":"Add authentication to GraphQL endpoint and authorization checks to all resolvers. Fix: S-01 through S-06, RBAC always returning owner, admin hardcoded email check.","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-02-05T21:12:09.131213+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-02-05T21:35:30.243779+01:00","closed_at":"2026-02-05T21:35:30.243779+01:00","close_reason":"Closed"} {"id":"books-ljg","title":"Fjern mock data og kobl frontend til backend GraphQL","description":"Frontend bruger ~2000 linjer hardcoded mock data i stedet for at bruge de eksisterende GraphQL hooks.\n\n## Problem\n- Backend GraphQL API er klar med queries og mutations\n- Frontend har hooks skrevet (useAccounts, useFiscalYears, etc.)\n- Men pages bruger hardcoded mock data i stedet for at kalde hooks\n\n## Filer der skal opdateres\n1. Dashboard.tsx - mock metrics, charts, transactions\n2. Kassekladde.tsx - mock accounts og posteringer \n3. Kontooversigt.tsx - mock kontoplan og balancer\n4. Bankafstemning.tsx - mock bank accounts og transaktioner\n5. FiscalYearSelector.tsx - mock fiscal years\n6. CompanySwitcher.tsx - mock companies\n7. Stores (companyStore, periodStore) - skal initialiseres fra API\n\n## Acceptkriterier\n- Al mock data fjernet fra frontend\n- Alle pages bruger GraphQL hooks til at hente data\n- Stores initialiseres korrekt ved app start\n- Data vises fra backend i UI","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T22:27:49.225279+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T22:42:04.17437+01:00","closed_at":"2026-01-30T22:42:04.17437+01:00","close_reason":"Closed"} diff --git a/backend/Books.Api/Commands/Invoices/InvoiceCommandHandlers.cs b/backend/Books.Api/Commands/Invoices/InvoiceCommandHandlers.cs index ce7c5ab..e3b09a4 100644 --- a/backend/Books.Api/Commands/Invoices/InvoiceCommandHandlers.cs +++ b/backend/Books.Api/Commands/Invoices/InvoiceCommandHandlers.cs @@ -8,7 +8,7 @@ namespace Books.Api.Commands.Invoices; /// /// Command handler for creating invoices. -/// Auto-assigns a sequential invoice number if one is not provided. +/// Always auto-assigns a sequential invoice number (Momsloven §52 - sequential numbering required). /// Validates the company has a CVR number (required for invoicing). /// public class CreateInvoiceCommandHandler( @@ -39,15 +39,16 @@ public class CreateInvoiceCommandHandler( "Virksomheden skal have et CVR-nummer for at oprette fakturaer. Opdater venligst virksomhedsindstillinger."); } - // Auto-assign invoice number if not provided - var invoiceNumber = command.InvoiceNumber; - if (string.IsNullOrWhiteSpace(invoiceNumber)) - { - invoiceNumber = await invoiceNumberService.GetNextInvoiceNumberAsync( - command.CompanyId, - command.InvoiceDate.Year, - cancellationToken); - } + // Always auto-assign invoice number (Momsloven §52 requires unbroken sequential numbering) + var invoiceNumber = await invoiceNumberService.GetNextInvoiceNumberAsync( + command.CompanyId, + command.InvoiceDate.Year, + cancellationToken); + + // Build seller address from company info + var sellerAddress = string.Join(", ", + new[] { company.Address, company.PostalCode, company.City } + .Where(s => !string.IsNullOrWhiteSpace(s))); aggregate.Create( command.CompanyId, @@ -63,7 +64,10 @@ public class CreateInvoiceCommandHandler( command.VatCode, command.Notes, command.Reference, - command.CreatedBy); + command.CreatedBy, + company.Cvr, + company.Name, + sellerAddress); } } @@ -124,19 +128,59 @@ public class RemoveInvoiceLineCommandHandler } } -public class MarkInvoiceSentCommandHandler +/// +/// Command handler for marking an invoice as sent. +/// Validates fiscal year status before allowing the invoice to be sent. +/// +public class MarkInvoiceSentCommandHandler( + IInvoiceRepository invoiceRepository, + IFiscalYearRepository fiscalYearRepository) : CommandHandler { - public override Task ExecuteAsync( + public override async Task ExecuteAsync( InvoiceAggregate aggregate, MarkInvoiceSentCommand command, CancellationToken cancellationToken) { + // Load the invoice read model to get fiscal year ID + var invoice = await invoiceRepository.GetByIdAsync( + aggregate.Id.Value, cancellationToken); + + var fiscalYearId = invoice?.FiscalYearId; + + // Validate fiscal year is set + if (string.IsNullOrWhiteSpace(fiscalYearId)) + { + throw new DomainException( + "FISCAL_YEAR_REQUIRED", + "Fiscal year is required for sending an invoice", + "Regnskabsår er påkrævet for afsendelse af en faktura"); + } + + // Fetch and validate fiscal year + var fiscalYear = await fiscalYearRepository.GetByIdAsync( + fiscalYearId, cancellationToken); + + if (fiscalYear == null) + { + throw new DomainException( + "FISCAL_YEAR_NOT_FOUND", + $"Fiscal year '{fiscalYearId}' not found", + $"Regnskabsår '{fiscalYearId}' blev ikke fundet"); + } + + // Validate fiscal year is open (not Closed or Locked) + if (fiscalYear.Status != "Open") + { + throw new DomainException( + "FISCAL_YEAR_NOT_OPEN", + $"Fiscal year is {fiscalYear.Status}. Only open fiscal years allow posting.", + $"Regnskabsåret er {fiscalYear.Status}. Kun åbne regnskabsår tillader bogføring."); + } + aggregate.Send( command.LedgerTransactionId, command.SentBy); - - return Task.CompletedTask; } } diff --git a/backend/Books.Api/Controllers/AuthController.cs b/backend/Books.Api/Controllers/AuthController.cs index 998973f..0a9c628 100644 --- a/backend/Books.Api/Controllers/AuthController.cs +++ b/backend/Books.Api/Controllers/AuthController.cs @@ -16,6 +16,9 @@ public class AuthController : ControllerBase { // The [Authorize] attribute triggers the OIDC challenge if not authenticated. // If we reach here, the user is authenticated - redirect back to the app. + // Validate returnUrl to prevent open redirect attacks + if (returnUrl != null && !Url.IsLocalUrl(returnUrl)) + returnUrl = "/"; return Redirect(returnUrl ?? "/"); } diff --git a/backend/Books.Api/Domain/Invoices/Events/InvoiceCreatedEvent.cs b/backend/Books.Api/Domain/Invoices/Events/InvoiceCreatedEvent.cs index 1a9be5b..e498c87 100644 --- a/backend/Books.Api/Domain/Invoices/Events/InvoiceCreatedEvent.cs +++ b/backend/Books.Api/Domain/Invoices/Events/InvoiceCreatedEvent.cs @@ -25,7 +25,10 @@ public class InvoiceCreatedEvent( InvoiceType type = InvoiceType.Invoice, string? originalInvoiceId = null, string? originalInvoiceNumber = null, - string? creditReason = null) : AggregateEvent + string? creditReason = null, + string? sellerCvr = null, + string? sellerName = null, + string? sellerAddress = null) : AggregateEvent { public string CompanyId { get; } = companyId; public string FiscalYearId { get; } = fiscalYearId; @@ -61,4 +64,19 @@ public class InvoiceCreatedEvent( /// For credit notes: Reason for issuing the credit note. /// public string? CreditReason { get; } = creditReason; + + /// + /// Seller CVR number (company registration number). + /// + public string? SellerCvr { get; } = sellerCvr; + + /// + /// Seller company name. + /// + public string? SellerName { get; } = sellerName; + + /// + /// Seller company address. + /// + public string? SellerAddress { get; } = sellerAddress; } diff --git a/backend/Books.Api/Domain/Invoices/InvoiceAggregate.cs b/backend/Books.Api/Domain/Invoices/InvoiceAggregate.cs index 4cc147b..5def74d 100644 --- a/backend/Books.Api/Domain/Invoices/InvoiceAggregate.cs +++ b/backend/Books.Api/Domain/Invoices/InvoiceAggregate.cs @@ -138,7 +138,10 @@ public class InvoiceAggregate(InvoiceId id) : AggregateRoot _lines = []; public string CompanyId => _companyId; @@ -48,6 +49,7 @@ public class JournalEntryDraftAggregate(JournalEntryDraftId id) public void Apply(JournalEntryDraftUpdatedEvent e) { _fiscalYearId = e.FiscalYearId; + _description = e.Description; _lines = e.Lines.ToList(); } @@ -184,6 +186,13 @@ public class JournalEntryDraftAggregate(JournalEntryDraftId id) "Posted by is required", "Bogført af er påkrævet"); + // Validate description is set (Bogføringsloven §7 stk. 1 nr. 3 requires a text describing the transaction) + if (string.IsNullOrWhiteSpace(_description)) + throw new DomainException( + "DESCRIPTION_REQUIRED", + "A description is required for posting. Each registration must have a text describing the transaction (Bogføringsloven §7 stk. 1 nr. 3).", + "En beskrivelse er påkrævet for bogføring. Hver registrering skal have en tekst, der beskriver transaktionen (Bogføringsloven §7 stk. 1 nr. 3)."); + // Validate minimum number of lines for double-entry bookkeeping if (_lines.Count < 2) throw new DomainException( diff --git a/backend/Books.Api/EventFlow/Subscribers/StandardDanishAccounts.cs b/backend/Books.Api/EventFlow/Subscribers/StandardDanishAccounts.cs index b810d27..842e76a 100644 --- a/backend/Books.Api/EventFlow/Subscribers/StandardDanishAccounts.cs +++ b/backend/Books.Api/EventFlow/Subscribers/StandardDanishAccounts.cs @@ -221,8 +221,9 @@ public static class StandardDanishAccounts // ========================================= // FINANSIELLE POSTER (Financial) - 9xxx - // Standard: 3670 = Øvrige finansielle omkostninger + // Standard: 3510 = Finansielle indtægter, 3670 = Øvrige finansielle omkostninger // ========================================= + yield return new("9100", "Renteindtægter", AccountType.Revenue, "Finansielle indtægter", null, "9100"); yield return new("9200", "Bankrenter", AccountType.Financial, null, null, "3670"); yield return new("9210", "Leverandører mv.", AccountType.Financial, "Renter til leverandører", null, "3670"); yield return new("9220", "Ikke-fradragsberettigede renter", AccountType.Financial, null, null, "3670"); diff --git a/backend/Books.Api/Saft/Services/SaftXmlBuilder.cs b/backend/Books.Api/Saft/Services/SaftXmlBuilder.cs index 5dab855..fa6b083 100644 --- a/backend/Books.Api/Saft/Services/SaftXmlBuilder.cs +++ b/backend/Books.Api/Saft/Services/SaftXmlBuilder.cs @@ -53,9 +53,15 @@ public class SaftXmlBuilder writer.WriteElementString("SoftwareID", header.SoftwareID); writer.WriteElementString("SoftwareVersion", header.SoftwareVersion); + // SAF-T requires DefaultCurrencyCode (ISO 4217) + writer.WriteElementString("DefaultCurrencyCode", "DKK"); + WriteCompany(writer, header.Company); WriteSelectionCriteria(writer, header.SelectionCriteria); + // SAF-T requires TaxAccountingBasis: A = Accrual, C = Cash, O = Other + writer.WriteElementString("TaxAccountingBasis", "A"); + writer.WriteEndElement(); // Header } @@ -133,10 +139,52 @@ public class SaftXmlBuilder WriteGeneralLedgerAccounts(writer, masterFiles.GeneralLedgerAccounts); WriteCustomers(writer, masterFiles.Customers); WriteSuppliers(writer, masterFiles.Suppliers); + WriteTaxTable(writer); writer.WriteEndElement(); // MasterFiles } + /// + /// Writes the TaxTable element declaring all Danish VAT codes. + /// Required by SAF-T schema to describe the tax codes used in transactions. + /// + private static void WriteTaxTable(XmlWriter writer) + { + writer.WriteStartElement("TaxTable"); + writer.WriteStartElement("TaxTableEntry"); + + writer.WriteElementString("TaxType", "MVA"); + writer.WriteElementString("Description", "Dansk moms (VAT)"); + + // Output VAT codes + WriteTaxCodeDetails(writer, "U25", "Udgående moms 25%", 25.00m); + WriteTaxCodeDetails(writer, "UEU", "EU-salg (momsfrit)", 0.00m); + WriteTaxCodeDetails(writer, "UEXP", "Eksport uden for EU (momsfrit)", 0.00m); + + // Input VAT codes + WriteTaxCodeDetails(writer, "I25", "Indgående moms 25%", 25.00m); + WriteTaxCodeDetails(writer, "IEUV", "EU-erhvervelse varer (reverse charge)", 25.00m); + WriteTaxCodeDetails(writer, "IEUY", "EU-erhvervelse ydelser (reverse charge)", 25.00m); + WriteTaxCodeDetails(writer, "IVV", "Import varer fra verden", 0.00m); + WriteTaxCodeDetails(writer, "IVY", "Import ydelser fra verden", 0.00m); + + // Special codes + WriteTaxCodeDetails(writer, "REP", "Repræsentation (25% fradrag)", 25.00m); + WriteTaxCodeDetails(writer, "INGEN", "Ingen moms", 0.00m); + + writer.WriteEndElement(); // TaxTableEntry + writer.WriteEndElement(); // TaxTable + } + + private static void WriteTaxCodeDetails(XmlWriter writer, string taxCode, string description, decimal taxPercentage) + { + writer.WriteStartElement("TaxCodeDetails"); + writer.WriteElementString("TaxCode", taxCode); + writer.WriteElementString("Description", description); + writer.WriteElementString("TaxPercentage", FormatDecimal(taxPercentage)); + writer.WriteEndElement(); // TaxCodeDetails + } + private static void WriteGeneralLedgerAccounts(XmlWriter writer, List accounts) { if (accounts.Count == 0) return; @@ -160,17 +208,13 @@ public class SaftXmlBuilder writer.WriteElementString("AccountType", account.AccountType); - // Opening balances - if (account.OpeningDebitBalance != 0) - writer.WriteElementString("OpeningDebitBalance", FormatDecimal(account.OpeningDebitBalance)); - if (account.OpeningCreditBalance != 0) - writer.WriteElementString("OpeningCreditBalance", FormatDecimal(account.OpeningCreditBalance)); + // Opening balances (SAF-T schema requires these elements even when zero) + writer.WriteElementString("OpeningDebitBalance", FormatDecimal(account.OpeningDebitBalance)); + writer.WriteElementString("OpeningCreditBalance", FormatDecimal(account.OpeningCreditBalance)); - // Closing balances - if (account.ClosingDebitBalance != 0) - writer.WriteElementString("ClosingDebitBalance", FormatDecimal(account.ClosingDebitBalance)); - if (account.ClosingCreditBalance != 0) - writer.WriteElementString("ClosingCreditBalance", FormatDecimal(account.ClosingCreditBalance)); + // Closing balances (SAF-T schema requires these elements even when zero) + writer.WriteElementString("ClosingDebitBalance", FormatDecimal(account.ClosingDebitBalance)); + writer.WriteElementString("ClosingCreditBalance", FormatDecimal(account.ClosingCreditBalance)); writer.WriteEndElement(); // Account } diff --git a/frontend/src/api/documentProcessing.ts b/frontend/src/api/documentProcessing.ts index 1bdaf83..c7704ae 100644 --- a/frontend/src/api/documentProcessing.ts +++ b/frontend/src/api/documentProcessing.ts @@ -117,7 +117,7 @@ export async function processDocument( } // Fallback error handling if (response.status === 401) { - throw new DocumentProcessingApiError('NOT_AUTHENTICATED', 'Du skal vaere logget ind'); + throw new DocumentProcessingApiError('NOT_AUTHENTICATED', 'Du skal være logget ind'); } if (response.status === 403) { throw new DocumentProcessingApiError('FORBIDDEN', 'Du har ikke adgang til denne virksomhed'); diff --git a/frontend/src/components/modals/CloseFiscalYearWizard.tsx b/frontend/src/components/modals/CloseFiscalYearWizard.tsx index eb04291..65f1e6d 100644 --- a/frontend/src/components/modals/CloseFiscalYearWizard.tsx +++ b/frontend/src/components/modals/CloseFiscalYearWizard.tsx @@ -238,7 +238,7 @@ export default function CloseFiscalYearWizard({ onSuccess?.(); } catch (error) { if (error instanceof Error) { - message.error(`Fejl ved arsafslutning: ${error.message}`); + message.error(`Fejl ved årsafslutning: ${error.message}`); } console.error('Failed to close fiscal year:', error); } finally { diff --git a/frontend/src/components/modals/CreateFiscalYearModal.tsx b/frontend/src/components/modals/CreateFiscalYearModal.tsx index e7065ac..74b3435 100644 --- a/frontend/src/components/modals/CreateFiscalYearModal.tsx +++ b/frontend/src/components/modals/CreateFiscalYearModal.tsx @@ -154,7 +154,6 @@ export default function CreateFiscalYearModal({ if (error instanceof Error) { message.error(`Fejl ved oprettelse: ${error.message}`); } - console.error('Failed to create fiscal year:', error); } finally { setIsSubmitting(false); } diff --git a/frontend/src/components/settings/BankConnectionsTab.tsx b/frontend/src/components/settings/BankConnectionsTab.tsx index df7e5f7..5de2d18 100644 --- a/frontend/src/components/settings/BankConnectionsTab.tsx +++ b/frontend/src/components/settings/BankConnectionsTab.tsx @@ -158,7 +158,6 @@ export default function BankConnectionsTab({ companyId }: BankConnectionsTabProp setSelectedLinkedAccount(newAccount.id); } catch (error) { showError(error, 'Kunne ikke oprette bankkonto'); - console.error('Failed to create bank account:', error); } }; @@ -192,7 +191,6 @@ export default function BankConnectionsTab({ companyId }: BankConnectionsTabProp setImportFromDate(dayjs()); } catch (error) { showError(error, 'Kunne ikke koble bankkonto'); - console.error('Failed to link account:', error); } }; @@ -218,7 +216,6 @@ export default function BankConnectionsTab({ companyId }: BankConnectionsTabProp window.location.href = result.authorizationUrl; } catch (error) { showError(error, 'Kunne ikke starte bankforbindelse'); - console.error('Failed to start bank connection:', error); } }; @@ -230,7 +227,6 @@ export default function BankConnectionsTab({ companyId }: BankConnectionsTabProp showSuccess('Bankforbindelse afbrudt'); } catch (error) { showError(error, 'Kunne ikke afbryde bankforbindelse'); - console.error('Failed to disconnect:', error); } }; @@ -255,7 +251,6 @@ export default function BankConnectionsTab({ companyId }: BankConnectionsTabProp window.location.href = result.authorizationUrl; } catch (error) { showError(error, 'Kunne ikke genoptage bankforbindelse'); - console.error('Failed to reconnect bank connection:', error); } }; @@ -267,7 +262,6 @@ export default function BankConnectionsTab({ companyId }: BankConnectionsTabProp showSuccess('Bankforbindelse arkiveret'); } catch (error) { showError(error, 'Kunne ikke arkivere bankforbindelse'); - console.error('Failed to archive:', error); } }; diff --git a/frontend/src/components/shared/AmountText.tsx b/frontend/src/components/shared/AmountText.tsx index 8ec0d6c..2aa746a 100644 --- a/frontend/src/components/shared/AmountText.tsx +++ b/frontend/src/components/shared/AmountText.tsx @@ -80,7 +80,7 @@ export function AmountText({ const formatted = formatCurrency(Math.abs(amount)); // Always show +/- prefix for non-zero amounts (accessibility: not color-only) // When showSign is explicitly true, same behavior; kept for API compatibility - const alwaysSign = true || showSign; + const alwaysSign = showSign; const sign = alwaysSign && amount !== 0 ? (amount > 0 ? '+' : '-') : amount < 0 ? '-' : ''; const suffix = showCurrency ? ` ${currencySuffix}` : ''; diff --git a/frontend/src/components/shared/AttachmentUpload.tsx b/frontend/src/components/shared/AttachmentUpload.tsx index b2dcab8..c5a13cd 100644 --- a/frontend/src/components/shared/AttachmentUpload.tsx +++ b/frontend/src/components/shared/AttachmentUpload.tsx @@ -63,7 +63,7 @@ const getFileIcon = (fileType: string) => { /** * Bilag (Document/Attachment) upload component. * Supports drag-drop, multiple file upload, and preview. - * Required by Bogforingsloven § 6 for document retention. + * Required by Bogføringsloven § 6 for document retention. */ export function AttachmentUpload({ attachments = [], @@ -182,10 +182,10 @@ export function AttachmentUpload({

- Klik eller traek filer hertil for at uploade bilag + Klik eller træk filer hertil for at uploade bilag

- Understotter PDF, billeder og Office-dokumenter (max {formatFileSize(maxFileSize)}) + Understøtter PDF, billeder og Office-dokumenter (max {formatFileSize(maxFileSize)})

@@ -210,7 +210,7 @@ export function AttachmentUpload({ {/* Required warning */} {required && attachments.length === 0 && fileList.length === 0 && ( - Bilag er pakraevet iht. Bogforingsloven § 6 + Bilag er påkrævet iht. Bogføringsloven § 6 )} diff --git a/frontend/src/components/shared/ShortcutsHelpModal.tsx b/frontend/src/components/shared/ShortcutsHelpModal.tsx index 349a43f..617f5bf 100644 --- a/frontend/src/components/shared/ShortcutsHelpModal.tsx +++ b/frontend/src/components/shared/ShortcutsHelpModal.tsx @@ -71,7 +71,7 @@ export function ShortcutsHelpModal({ open, onClose }: ShortcutsHelpModalProps) { - Tip: Tryk ⌘K for at abne + Tip: Tryk ⌘K for at åbne kommandopaletten og hurtigt navigere til enhver side. diff --git a/frontend/src/components/shared/StatusBadge.tsx b/frontend/src/components/shared/StatusBadge.tsx index 1f2ccf2..31a7da2 100644 --- a/frontend/src/components/shared/StatusBadge.tsx +++ b/frontend/src/components/shared/StatusBadge.tsx @@ -70,7 +70,7 @@ const statusConfig: Record< success: { color: 'green', icon: , - defaultText: 'Succces', + defaultText: 'Succes', }, warning: { color: 'orange', diff --git a/frontend/src/components/tables/DataTable.tsx b/frontend/src/components/tables/DataTable.tsx index a2917bd..13ec9eb 100644 --- a/frontend/src/components/tables/DataTable.tsx +++ b/frontend/src/components/tables/DataTable.tsx @@ -247,7 +247,7 @@ export default function DataTable({ {toolbarActions} {refreshable && hookEnabled && ( - + + + ); } @@ -241,7 +259,7 @@ export default function Bankafstemning() { + + + diff --git a/frontend/src/pages/UserSettings.tsx b/frontend/src/pages/UserSettings.tsx index cbfb598..55ee59b 100644 --- a/frontend/src/pages/UserSettings.tsx +++ b/frontend/src/pages/UserSettings.tsx @@ -28,6 +28,7 @@ import { import type { UploadProps } from 'antd'; import { spacing } from '@/styles/designTokens'; import { PageHeader } from '@/components/shared/PageHeader'; +import { DemoDataDisclaimer } from '@/components/shared/DemoDataDisclaimer'; const { Title, Text } = Typography; @@ -461,6 +462,8 @@ export default function UserSettings() { breadcrumbs={[{ title: 'Brugerindstillinger' }]} /> + + ); diff --git a/frontend/src/types/vat.ts b/frontend/src/types/vat.ts index 40fd25c..3678ee6 100644 --- a/frontend/src/types/vat.ts +++ b/frontend/src/types/vat.ts @@ -10,13 +10,24 @@ import type { VATPeriodicitet } from './periods'; * VAT codes used in Danish bookkeeping */ export type VATCode = - | 'S25' // Salgsmoms 25% (udgaaende moms) - | 'K25' // Koebsmoms 25% (indgaaende moms) - | 'EU_VARE' // EU-varekoeb (reverse charge) - | 'EU_YDELSE' // EU-ydelseskoeb (reverse charge) - | 'MOMSFRI' // Momsfritaget (healthcare, education, etc.) - | 'EKSPORT' // Eksport (0%) - | 'NONE'; // Ingen moms + | 'U25' // Udgående moms 25% (salg) + | 'UEU' // EU-salg (0%) + | 'UEXP' // Eksport (0%) + | 'I25' // Indgående moms 25% (køb) + | 'IEUV' // EU-erhvervelse varer (reverse charge) + | 'IEUY' // EU-erhvervelse ydelser (reverse charge) + | 'IVV' // Import varer verden (0%) + | 'IVY' // Import ydelser verden (0%) + | 'REP' // Repræsentation (25%, 25% fradrag) + | 'INGEN' // Ingen moms + // Legacy codes kept for backwards compatibility with other modules + | 'S25' // @deprecated Use U25 + | 'K25' // @deprecated Use I25 + | 'EU_VARE' // @deprecated Use IEUV + | 'EU_YDELSE' // @deprecated Use IEUY + | 'MOMSFRI' // @deprecated Use INGEN + | 'EKSPORT' // @deprecated Use UEXP + | 'NONE'; // @deprecated Use INGEN /** * VAT code type classification