Audit v3: VAT alignment, security, encoding, UX, compliance
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 <noreply@anthropic.com>
This commit is contained in:
parent
cd5333f07f
commit
1a0922b778
33 changed files with 355 additions and 172 deletions
|
|
@ -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-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-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-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-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-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-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"}
|
{"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-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-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-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-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-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"}
|
{"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"}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ namespace Books.Api.Commands.Invoices;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Command handler for creating 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).
|
/// Validates the company has a CVR number (required for invoicing).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CreateInvoiceCommandHandler(
|
public class CreateInvoiceCommandHandler(
|
||||||
|
|
@ -39,15 +39,16 @@ public class CreateInvoiceCommandHandler(
|
||||||
"Virksomheden skal have et CVR-nummer for at oprette fakturaer. Opdater venligst virksomhedsindstillinger.");
|
"Virksomheden skal have et CVR-nummer for at oprette fakturaer. Opdater venligst virksomhedsindstillinger.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-assign invoice number if not provided
|
// Always auto-assign invoice number (Momsloven §52 requires unbroken sequential numbering)
|
||||||
var invoiceNumber = command.InvoiceNumber;
|
var invoiceNumber = await invoiceNumberService.GetNextInvoiceNumberAsync(
|
||||||
if (string.IsNullOrWhiteSpace(invoiceNumber))
|
|
||||||
{
|
|
||||||
invoiceNumber = await invoiceNumberService.GetNextInvoiceNumberAsync(
|
|
||||||
command.CompanyId,
|
command.CompanyId,
|
||||||
command.InvoiceDate.Year,
|
command.InvoiceDate.Year,
|
||||||
cancellationToken);
|
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(
|
aggregate.Create(
|
||||||
command.CompanyId,
|
command.CompanyId,
|
||||||
|
|
@ -63,7 +64,10 @@ public class CreateInvoiceCommandHandler(
|
||||||
command.VatCode,
|
command.VatCode,
|
||||||
command.Notes,
|
command.Notes,
|
||||||
command.Reference,
|
command.Reference,
|
||||||
command.CreatedBy);
|
command.CreatedBy,
|
||||||
|
company.Cvr,
|
||||||
|
company.Name,
|
||||||
|
sellerAddress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,19 +128,59 @@ public class RemoveInvoiceLineCommandHandler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MarkInvoiceSentCommandHandler
|
/// <summary>
|
||||||
|
/// Command handler for marking an invoice as sent.
|
||||||
|
/// Validates fiscal year status before allowing the invoice to be sent.
|
||||||
|
/// </summary>
|
||||||
|
public class MarkInvoiceSentCommandHandler(
|
||||||
|
IInvoiceRepository invoiceRepository,
|
||||||
|
IFiscalYearRepository fiscalYearRepository)
|
||||||
: CommandHandler<InvoiceAggregate, InvoiceId, MarkInvoiceSentCommand>
|
: CommandHandler<InvoiceAggregate, InvoiceId, MarkInvoiceSentCommand>
|
||||||
{
|
{
|
||||||
public override Task ExecuteAsync(
|
public override async Task ExecuteAsync(
|
||||||
InvoiceAggregate aggregate,
|
InvoiceAggregate aggregate,
|
||||||
MarkInvoiceSentCommand command,
|
MarkInvoiceSentCommand command,
|
||||||
CancellationToken cancellationToken)
|
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(
|
aggregate.Send(
|
||||||
command.LedgerTransactionId,
|
command.LedgerTransactionId,
|
||||||
command.SentBy);
|
command.SentBy);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ public class AuthController : ControllerBase
|
||||||
{
|
{
|
||||||
// The [Authorize] attribute triggers the OIDC challenge if not authenticated.
|
// The [Authorize] attribute triggers the OIDC challenge if not authenticated.
|
||||||
// If we reach here, the user is authenticated - redirect back to the app.
|
// 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 ?? "/");
|
return Redirect(returnUrl ?? "/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,10 @@ public class InvoiceCreatedEvent(
|
||||||
InvoiceType type = InvoiceType.Invoice,
|
InvoiceType type = InvoiceType.Invoice,
|
||||||
string? originalInvoiceId = null,
|
string? originalInvoiceId = null,
|
||||||
string? originalInvoiceNumber = null,
|
string? originalInvoiceNumber = null,
|
||||||
string? creditReason = null) : AggregateEvent<InvoiceAggregate, InvoiceId>
|
string? creditReason = null,
|
||||||
|
string? sellerCvr = null,
|
||||||
|
string? sellerName = null,
|
||||||
|
string? sellerAddress = null) : AggregateEvent<InvoiceAggregate, InvoiceId>
|
||||||
{
|
{
|
||||||
public string CompanyId { get; } = companyId;
|
public string CompanyId { get; } = companyId;
|
||||||
public string FiscalYearId { get; } = fiscalYearId;
|
public string FiscalYearId { get; } = fiscalYearId;
|
||||||
|
|
@ -61,4 +64,19 @@ public class InvoiceCreatedEvent(
|
||||||
/// For credit notes: Reason for issuing the credit note.
|
/// For credit notes: Reason for issuing the credit note.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? CreditReason { get; } = creditReason;
|
public string? CreditReason { get; } = creditReason;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seller CVR number (company registration number).
|
||||||
|
/// </summary>
|
||||||
|
public string? SellerCvr { get; } = sellerCvr;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seller company name.
|
||||||
|
/// </summary>
|
||||||
|
public string? SellerName { get; } = sellerName;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seller company address.
|
||||||
|
/// </summary>
|
||||||
|
public string? SellerAddress { get; } = sellerAddress;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,10 @@ public class InvoiceAggregate(InvoiceId id) : AggregateRoot<InvoiceAggregate, In
|
||||||
string? vatCode,
|
string? vatCode,
|
||||||
string? notes,
|
string? notes,
|
||||||
string? reference,
|
string? reference,
|
||||||
string createdBy)
|
string createdBy,
|
||||||
|
string? sellerCvr = null,
|
||||||
|
string? sellerName = null,
|
||||||
|
string? sellerAddress = null)
|
||||||
{
|
{
|
||||||
if (_isCreated)
|
if (_isCreated)
|
||||||
throw new DomainException("INVOICE_EXISTS", "Invoice already exists", "Faktura eksisterer allerede");
|
throw new DomainException("INVOICE_EXISTS", "Invoice already exists", "Faktura eksisterer allerede");
|
||||||
|
|
@ -166,7 +169,10 @@ public class InvoiceAggregate(InvoiceId id) : AggregateRoot<InvoiceAggregate, In
|
||||||
vatCode,
|
vatCode,
|
||||||
notes,
|
notes,
|
||||||
reference,
|
reference,
|
||||||
createdBy));
|
createdBy,
|
||||||
|
sellerCvr: sellerCvr,
|
||||||
|
sellerName: sellerName,
|
||||||
|
sellerAddress: sellerAddress));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddLine(
|
public void AddLine(
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ public class JournalEntryDraftAggregate(JournalEntryDraftId id)
|
||||||
private string _companyId = string.Empty;
|
private string _companyId = string.Empty;
|
||||||
private string _voucherNumber = string.Empty;
|
private string _voucherNumber = string.Empty;
|
||||||
private string? _fiscalYearId;
|
private string? _fiscalYearId;
|
||||||
|
private string? _description;
|
||||||
private List<DraftLine> _lines = [];
|
private List<DraftLine> _lines = [];
|
||||||
|
|
||||||
public string CompanyId => _companyId;
|
public string CompanyId => _companyId;
|
||||||
|
|
@ -48,6 +49,7 @@ public class JournalEntryDraftAggregate(JournalEntryDraftId id)
|
||||||
public void Apply(JournalEntryDraftUpdatedEvent e)
|
public void Apply(JournalEntryDraftUpdatedEvent e)
|
||||||
{
|
{
|
||||||
_fiscalYearId = e.FiscalYearId;
|
_fiscalYearId = e.FiscalYearId;
|
||||||
|
_description = e.Description;
|
||||||
_lines = e.Lines.ToList();
|
_lines = e.Lines.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,6 +186,13 @@ public class JournalEntryDraftAggregate(JournalEntryDraftId id)
|
||||||
"Posted by is required",
|
"Posted by is required",
|
||||||
"Bogført af er påkrævet");
|
"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
|
// Validate minimum number of lines for double-entry bookkeeping
|
||||||
if (_lines.Count < 2)
|
if (_lines.Count < 2)
|
||||||
throw new DomainException(
|
throw new DomainException(
|
||||||
|
|
|
||||||
|
|
@ -221,8 +221,9 @@ public static class StandardDanishAccounts
|
||||||
|
|
||||||
// =========================================
|
// =========================================
|
||||||
// FINANSIELLE POSTER (Financial) - 9xxx
|
// 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("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("9210", "Leverandører mv.", AccountType.Financial, "Renter til leverandører", null, "3670");
|
||||||
yield return new("9220", "Ikke-fradragsberettigede renter", AccountType.Financial, null, null, "3670");
|
yield return new("9220", "Ikke-fradragsberettigede renter", AccountType.Financial, null, null, "3670");
|
||||||
|
|
|
||||||
|
|
@ -53,9 +53,15 @@ public class SaftXmlBuilder
|
||||||
writer.WriteElementString("SoftwareID", header.SoftwareID);
|
writer.WriteElementString("SoftwareID", header.SoftwareID);
|
||||||
writer.WriteElementString("SoftwareVersion", header.SoftwareVersion);
|
writer.WriteElementString("SoftwareVersion", header.SoftwareVersion);
|
||||||
|
|
||||||
|
// SAF-T requires DefaultCurrencyCode (ISO 4217)
|
||||||
|
writer.WriteElementString("DefaultCurrencyCode", "DKK");
|
||||||
|
|
||||||
WriteCompany(writer, header.Company);
|
WriteCompany(writer, header.Company);
|
||||||
WriteSelectionCriteria(writer, header.SelectionCriteria);
|
WriteSelectionCriteria(writer, header.SelectionCriteria);
|
||||||
|
|
||||||
|
// SAF-T requires TaxAccountingBasis: A = Accrual, C = Cash, O = Other
|
||||||
|
writer.WriteElementString("TaxAccountingBasis", "A");
|
||||||
|
|
||||||
writer.WriteEndElement(); // Header
|
writer.WriteEndElement(); // Header
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,10 +139,52 @@ public class SaftXmlBuilder
|
||||||
WriteGeneralLedgerAccounts(writer, masterFiles.GeneralLedgerAccounts);
|
WriteGeneralLedgerAccounts(writer, masterFiles.GeneralLedgerAccounts);
|
||||||
WriteCustomers(writer, masterFiles.Customers);
|
WriteCustomers(writer, masterFiles.Customers);
|
||||||
WriteSuppliers(writer, masterFiles.Suppliers);
|
WriteSuppliers(writer, masterFiles.Suppliers);
|
||||||
|
WriteTaxTable(writer);
|
||||||
|
|
||||||
writer.WriteEndElement(); // MasterFiles
|
writer.WriteEndElement(); // MasterFiles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes the TaxTable element declaring all Danish VAT codes.
|
||||||
|
/// Required by SAF-T schema to describe the tax codes used in transactions.
|
||||||
|
/// </summary>
|
||||||
|
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<SaftAccount> accounts)
|
private static void WriteGeneralLedgerAccounts(XmlWriter writer, List<SaftAccount> accounts)
|
||||||
{
|
{
|
||||||
if (accounts.Count == 0) return;
|
if (accounts.Count == 0) return;
|
||||||
|
|
@ -160,16 +208,12 @@ public class SaftXmlBuilder
|
||||||
|
|
||||||
writer.WriteElementString("AccountType", account.AccountType);
|
writer.WriteElementString("AccountType", account.AccountType);
|
||||||
|
|
||||||
// Opening balances
|
// Opening balances (SAF-T schema requires these elements even when zero)
|
||||||
if (account.OpeningDebitBalance != 0)
|
|
||||||
writer.WriteElementString("OpeningDebitBalance", FormatDecimal(account.OpeningDebitBalance));
|
writer.WriteElementString("OpeningDebitBalance", FormatDecimal(account.OpeningDebitBalance));
|
||||||
if (account.OpeningCreditBalance != 0)
|
|
||||||
writer.WriteElementString("OpeningCreditBalance", FormatDecimal(account.OpeningCreditBalance));
|
writer.WriteElementString("OpeningCreditBalance", FormatDecimal(account.OpeningCreditBalance));
|
||||||
|
|
||||||
// Closing balances
|
// Closing balances (SAF-T schema requires these elements even when zero)
|
||||||
if (account.ClosingDebitBalance != 0)
|
|
||||||
writer.WriteElementString("ClosingDebitBalance", FormatDecimal(account.ClosingDebitBalance));
|
writer.WriteElementString("ClosingDebitBalance", FormatDecimal(account.ClosingDebitBalance));
|
||||||
if (account.ClosingCreditBalance != 0)
|
|
||||||
writer.WriteElementString("ClosingCreditBalance", FormatDecimal(account.ClosingCreditBalance));
|
writer.WriteElementString("ClosingCreditBalance", FormatDecimal(account.ClosingCreditBalance));
|
||||||
|
|
||||||
writer.WriteEndElement(); // Account
|
writer.WriteEndElement(); // Account
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ export async function processDocument(
|
||||||
}
|
}
|
||||||
// Fallback error handling
|
// Fallback error handling
|
||||||
if (response.status === 401) {
|
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) {
|
if (response.status === 403) {
|
||||||
throw new DocumentProcessingApiError('FORBIDDEN', 'Du har ikke adgang til denne virksomhed');
|
throw new DocumentProcessingApiError('FORBIDDEN', 'Du har ikke adgang til denne virksomhed');
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,7 @@ export default function CloseFiscalYearWizard({
|
||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof 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);
|
console.error('Failed to close fiscal year:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,6 @@ export default function CreateFiscalYearModal({
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
message.error(`Fejl ved oprettelse: ${error.message}`);
|
message.error(`Fejl ved oprettelse: ${error.message}`);
|
||||||
}
|
}
|
||||||
console.error('Failed to create fiscal year:', error);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,6 @@ export default function BankConnectionsTab({ companyId }: BankConnectionsTabProp
|
||||||
setSelectedLinkedAccount(newAccount.id);
|
setSelectedLinkedAccount(newAccount.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error, 'Kunne ikke oprette bankkonto');
|
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());
|
setImportFromDate(dayjs());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error, 'Kunne ikke koble bankkonto');
|
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;
|
window.location.href = result.authorizationUrl;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error, 'Kunne ikke starte bankforbindelse');
|
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');
|
showSuccess('Bankforbindelse afbrudt');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error, 'Kunne ikke afbryde bankforbindelse');
|
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;
|
window.location.href = result.authorizationUrl;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error, 'Kunne ikke genoptage bankforbindelse');
|
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');
|
showSuccess('Bankforbindelse arkiveret');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error, 'Kunne ikke arkivere bankforbindelse');
|
showError(error, 'Kunne ikke arkivere bankforbindelse');
|
||||||
console.error('Failed to archive:', error);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ export function AmountText({
|
||||||
const formatted = formatCurrency(Math.abs(amount));
|
const formatted = formatCurrency(Math.abs(amount));
|
||||||
// Always show +/- prefix for non-zero amounts (accessibility: not color-only)
|
// Always show +/- prefix for non-zero amounts (accessibility: not color-only)
|
||||||
// When showSign is explicitly true, same behavior; kept for API compatibility
|
// 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 sign = alwaysSign && amount !== 0 ? (amount > 0 ? '+' : '-') : amount < 0 ? '-' : '';
|
||||||
const suffix = showCurrency ? ` ${currencySuffix}` : '';
|
const suffix = showCurrency ? ` ${currencySuffix}` : '';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ const getFileIcon = (fileType: string) => {
|
||||||
/**
|
/**
|
||||||
* Bilag (Document/Attachment) upload component.
|
* Bilag (Document/Attachment) upload component.
|
||||||
* Supports drag-drop, multiple file upload, and preview.
|
* 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({
|
export function AttachmentUpload({
|
||||||
attachments = [],
|
attachments = [],
|
||||||
|
|
@ -182,10 +182,10 @@ export function AttachmentUpload({
|
||||||
<UploadOutlined style={{ fontSize: 32, color: '#1890ff' }} />
|
<UploadOutlined style={{ fontSize: 32, color: '#1890ff' }} />
|
||||||
</p>
|
</p>
|
||||||
<p className="ant-upload-text">
|
<p className="ant-upload-text">
|
||||||
Klik eller traek filer hertil for at uploade bilag
|
Klik eller træk filer hertil for at uploade bilag
|
||||||
</p>
|
</p>
|
||||||
<p className="ant-upload-hint">
|
<p className="ant-upload-hint">
|
||||||
Understotter PDF, billeder og Office-dokumenter (max {formatFileSize(maxFileSize)})
|
Understøtter PDF, billeder og Office-dokumenter (max {formatFileSize(maxFileSize)})
|
||||||
</p>
|
</p>
|
||||||
</Upload.Dragger>
|
</Upload.Dragger>
|
||||||
|
|
||||||
|
|
@ -210,7 +210,7 @@ export function AttachmentUpload({
|
||||||
{/* Required warning */}
|
{/* Required warning */}
|
||||||
{required && attachments.length === 0 && fileList.length === 0 && (
|
{required && attachments.length === 0 && fileList.length === 0 && (
|
||||||
<Text type="warning" style={{ display: 'block', marginBottom: spacing.sm }}>
|
<Text type="warning" style={{ display: 'block', marginBottom: spacing.sm }}>
|
||||||
Bilag er pakraevet iht. Bogforingsloven § 6
|
Bilag er påkrævet iht. Bogføringsloven § 6
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ export function ShortcutsHelpModal({ open, onClose }: ShortcutsHelpModalProps) {
|
||||||
<Divider style={{ margin: '16px 0' }} />
|
<Divider style={{ margin: '16px 0' }} />
|
||||||
|
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
Tip: Tryk <Tag style={{ margin: '0 4px' }}>⌘K</Tag> for at abne
|
Tip: Tryk <Tag style={{ margin: '0 4px' }}>⌘K</Tag> for at åbne
|
||||||
kommandopaletten og hurtigt navigere til enhver side.
|
kommandopaletten og hurtigt navigere til enhver side.
|
||||||
</Text>
|
</Text>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ const statusConfig: Record<
|
||||||
success: {
|
success: {
|
||||||
color: 'green',
|
color: 'green',
|
||||||
icon: <CheckCircleOutlined />,
|
icon: <CheckCircleOutlined />,
|
||||||
defaultText: 'Succces',
|
defaultText: 'Succes',
|
||||||
},
|
},
|
||||||
warning: {
|
warning: {
|
||||||
color: 'orange',
|
color: 'orange',
|
||||||
|
|
|
||||||
|
|
@ -247,7 +247,7 @@ export default function DataTable<T extends { id: string }>({
|
||||||
<Space>
|
<Space>
|
||||||
{toolbarActions}
|
{toolbarActions}
|
||||||
{refreshable && hookEnabled && (
|
{refreshable && hookEnabled && (
|
||||||
<Tooltip title="Genindlaes">
|
<Tooltip title="Genindlæs">
|
||||||
<Button
|
<Button
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined />}
|
||||||
onClick={() => refetch()}
|
onClick={() => refetch()}
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@ export const shortcuts: Record<string, ShortcutDefinition> = {
|
||||||
postDraft: {
|
postDraft: {
|
||||||
id: 'postDraft',
|
id: 'postDraft',
|
||||||
keys: 'mod+enter',
|
keys: 'mod+enter',
|
||||||
label: 'Bogfor kladde',
|
label: 'Bogfør kladde',
|
||||||
category: 'bogforing',
|
category: 'bogforing',
|
||||||
scope: 'page',
|
scope: 'page',
|
||||||
},
|
},
|
||||||
|
|
@ -290,7 +290,7 @@ export function formatShortcutForTooltip(shortcutId: string): string | null {
|
||||||
export const categoryNames: Record<ShortcutCategory, string> = {
|
export const categoryNames: Record<ShortcutCategory, string> = {
|
||||||
global: 'Globale',
|
global: 'Globale',
|
||||||
navigation: 'Navigation',
|
navigation: 'Navigation',
|
||||||
bogforing: 'Bogforing',
|
bogforing: 'Bogføring',
|
||||||
faktura: 'Fakturering',
|
faktura: 'Fakturering',
|
||||||
bank: 'Bank',
|
bank: 'Bank',
|
||||||
kunder: 'Kunder',
|
kunder: 'Kunder',
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,9 @@ import type { VATPeriodicitet } from '@/types/periods';
|
||||||
/**
|
/**
|
||||||
* Complete VAT code configuration for Danish bookkeeping
|
* Complete VAT code configuration for Danish bookkeeping
|
||||||
*/
|
*/
|
||||||
export const VAT_CODE_CONFIG: Record<VATCode, VATCodeConfig> = {
|
export const VAT_CODE_CONFIG: Record<string, VATCodeConfig> = {
|
||||||
S25: {
|
U25: {
|
||||||
code: 'S25',
|
code: 'U25',
|
||||||
nameDanish: 'Udgående moms 25%',
|
nameDanish: 'Udgående moms 25%',
|
||||||
nameEnglish: 'Output VAT 25%',
|
nameEnglish: 'Output VAT 25%',
|
||||||
rate: 0.25,
|
rate: 0.25,
|
||||||
|
|
@ -28,8 +28,34 @@ export const VAT_CODE_CONFIG: Record<VATCode, VATCodeConfig> = {
|
||||||
deductible: false,
|
deductible: false,
|
||||||
description: 'Moms på salg af varer og ydelser i Danmark',
|
description: 'Moms på salg af varer og ydelser i Danmark',
|
||||||
},
|
},
|
||||||
K25: {
|
UEU: {
|
||||||
code: 'K25',
|
code: 'UEU',
|
||||||
|
nameDanish: 'EU-salg',
|
||||||
|
nameEnglish: 'EU sales (0%)',
|
||||||
|
rate: 0,
|
||||||
|
type: 'exempt',
|
||||||
|
affectsBoxes: {
|
||||||
|
basisBox: '2',
|
||||||
|
},
|
||||||
|
reverseCharge: false,
|
||||||
|
deductible: false,
|
||||||
|
description: 'Salg af varer og ydelser til andre EU-lande',
|
||||||
|
},
|
||||||
|
UEXP: {
|
||||||
|
code: 'UEXP',
|
||||||
|
nameDanish: 'Eksport',
|
||||||
|
nameEnglish: 'Export (0%)',
|
||||||
|
rate: 0,
|
||||||
|
type: 'exempt',
|
||||||
|
affectsBoxes: {
|
||||||
|
basisBox: '2',
|
||||||
|
},
|
||||||
|
reverseCharge: false,
|
||||||
|
deductible: false,
|
||||||
|
description: 'Eksport til lande uden for EU',
|
||||||
|
},
|
||||||
|
I25: {
|
||||||
|
code: 'I25',
|
||||||
nameDanish: 'Indgående moms 25%',
|
nameDanish: 'Indgående moms 25%',
|
||||||
nameEnglish: 'Input VAT 25%',
|
nameEnglish: 'Input VAT 25%',
|
||||||
rate: 0.25,
|
rate: 0.25,
|
||||||
|
|
@ -41,10 +67,10 @@ export const VAT_CODE_CONFIG: Record<VATCode, VATCodeConfig> = {
|
||||||
deductible: true,
|
deductible: true,
|
||||||
description: 'Fradragsberettiget moms på køb',
|
description: 'Fradragsberettiget moms på køb',
|
||||||
},
|
},
|
||||||
EU_VARE: {
|
IEUV: {
|
||||||
code: 'EU_VARE',
|
code: 'IEUV',
|
||||||
nameDanish: 'EU-varekøb (erhvervelsesmoms)',
|
nameDanish: 'EU-erhvervelse varer',
|
||||||
nameEnglish: 'EU goods purchase (acquisition VAT)',
|
nameEnglish: 'EU goods acquisition (reverse charge)',
|
||||||
rate: 0.25,
|
rate: 0.25,
|
||||||
type: 'reverse_charge',
|
type: 'reverse_charge',
|
||||||
affectsBoxes: {
|
affectsBoxes: {
|
||||||
|
|
@ -52,13 +78,13 @@ export const VAT_CODE_CONFIG: Record<VATCode, VATCodeConfig> = {
|
||||||
basisBox: '3',
|
basisBox: '3',
|
||||||
},
|
},
|
||||||
reverseCharge: true,
|
reverseCharge: true,
|
||||||
deductible: true, // Both output and input VAT
|
deductible: true,
|
||||||
description: 'Køb af varer fra andre EU-lande med omvendt betalingspligt',
|
description: 'Køb af varer fra andre EU-lande med omvendt betalingspligt',
|
||||||
},
|
},
|
||||||
EU_YDELSE: {
|
IEUY: {
|
||||||
code: 'EU_YDELSE',
|
code: 'IEUY',
|
||||||
nameDanish: 'EU-ydelseskøb (omvendt betalingspligt)',
|
nameDanish: 'EU-erhvervelse ydelser',
|
||||||
nameEnglish: 'EU services purchase (reverse charge)',
|
nameEnglish: 'EU services acquisition (reverse charge)',
|
||||||
rate: 0.25,
|
rate: 0.25,
|
||||||
type: 'reverse_charge',
|
type: 'reverse_charge',
|
||||||
affectsBoxes: {
|
affectsBoxes: {
|
||||||
|
|
@ -69,34 +95,43 @@ export const VAT_CODE_CONFIG: Record<VATCode, VATCodeConfig> = {
|
||||||
deductible: true,
|
deductible: true,
|
||||||
description: 'Køb af ydelser fra udlandet med omvendt betalingspligt',
|
description: 'Køb af ydelser fra udlandet med omvendt betalingspligt',
|
||||||
},
|
},
|
||||||
MOMSFRI: {
|
IVV: {
|
||||||
code: 'MOMSFRI',
|
code: 'IVV',
|
||||||
nameDanish: 'Momsfritaget',
|
nameDanish: 'Import varer verden',
|
||||||
nameEnglish: 'VAT exempt',
|
nameEnglish: 'Import goods (world)',
|
||||||
rate: 0,
|
rate: 0,
|
||||||
type: 'exempt',
|
type: 'input',
|
||||||
affectsBoxes: {
|
affectsBoxes: {},
|
||||||
basisBox: '2',
|
|
||||||
},
|
|
||||||
reverseCharge: false,
|
reverseCharge: false,
|
||||||
deductible: false,
|
deductible: false,
|
||||||
description: 'Momsfritaget salg (sundhed, undervisning, mv.)',
|
description: 'Import af varer fra lande uden for EU',
|
||||||
},
|
},
|
||||||
EKSPORT: {
|
IVY: {
|
||||||
code: 'EKSPORT',
|
code: 'IVY',
|
||||||
nameDanish: 'Eksport (0%)',
|
nameDanish: 'Import ydelser verden',
|
||||||
nameEnglish: 'Export (0%)',
|
nameEnglish: 'Import services (world)',
|
||||||
rate: 0,
|
rate: 0,
|
||||||
type: 'exempt',
|
type: 'input',
|
||||||
affectsBoxes: {
|
affectsBoxes: {},
|
||||||
basisBox: '2',
|
|
||||||
},
|
|
||||||
reverseCharge: false,
|
reverseCharge: false,
|
||||||
deductible: false,
|
deductible: false,
|
||||||
description: 'Eksport til lande uden for EU',
|
description: 'Import af ydelser fra lande uden for EU',
|
||||||
},
|
},
|
||||||
NONE: {
|
REP: {
|
||||||
code: 'NONE',
|
code: 'REP',
|
||||||
|
nameDanish: 'Repræsentation',
|
||||||
|
nameEnglish: 'Representation (25%, 25% deductible)',
|
||||||
|
rate: 0.25,
|
||||||
|
type: 'input',
|
||||||
|
affectsBoxes: {
|
||||||
|
vatBox: 'B',
|
||||||
|
},
|
||||||
|
reverseCharge: false,
|
||||||
|
deductible: true,
|
||||||
|
description: 'Repræsentationsudgifter med 25% fradrag',
|
||||||
|
},
|
||||||
|
INGEN: {
|
||||||
|
code: 'INGEN',
|
||||||
nameDanish: 'Ingen moms',
|
nameDanish: 'Ingen moms',
|
||||||
nameEnglish: 'No VAT',
|
nameEnglish: 'No VAT',
|
||||||
rate: 0,
|
rate: 0,
|
||||||
|
|
@ -231,7 +266,7 @@ export const SKAT_VAT_BOXES: Record<VATBoxId | BasisBoxId, SKATVATBox> = {
|
||||||
*/
|
*/
|
||||||
export const VAT_ACCOUNTS = {
|
export const VAT_ACCOUNTS = {
|
||||||
inputVAT: '5610', // Indgående moms (fradrag)
|
inputVAT: '5610', // Indgående moms (fradrag)
|
||||||
outputVAT: '5710', // Udgående moms (skyld)
|
outputVAT: '5611', // Udgående moms (skyld)
|
||||||
euVAT: '5620', // EU-moms (erhvervelsesmoms)
|
euVAT: '5620', // EU-moms (erhvervelsesmoms)
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
@ -243,7 +278,7 @@ export const VAT_ACCOUNTS = {
|
||||||
* Valid VAT code values for validation
|
* Valid VAT code values for validation
|
||||||
*/
|
*/
|
||||||
export const VALID_VAT_CODES: readonly VATCode[] = [
|
export const VALID_VAT_CODES: readonly VATCode[] = [
|
||||||
'S25', 'K25', 'EU_VARE', 'EU_YDELSE', 'MOMSFRI', 'EKSPORT', 'NONE'
|
'U25', 'UEU', 'UEXP', 'I25', 'IEUV', 'IEUY', 'IVV', 'IVY', 'REP', 'INGEN'
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -254,17 +289,17 @@ export function isValidVATCode(code: unknown): code is VATCode {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safely convert a string to VATCode, returns 'NONE' if invalid
|
* Safely convert a string to VATCode, returns 'INGEN' if invalid
|
||||||
*/
|
*/
|
||||||
export function toVATCode(code: unknown): VATCode {
|
export function toVATCode(code: unknown): VATCode {
|
||||||
return isValidVATCode(code) ? code : 'NONE';
|
return isValidVATCode(code) ? code : 'INGEN';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get VAT code configuration
|
* Get VAT code configuration
|
||||||
*/
|
*/
|
||||||
export function getVATCodeConfig(code: VATCode): VATCodeConfig {
|
export function getVATCodeConfig(code: VATCode): VATCodeConfig {
|
||||||
return VAT_CODE_CONFIG[code];
|
return VAT_CODE_CONFIG[code] ?? VAT_CODE_CONFIG['INGEN'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -328,21 +363,21 @@ export function getSKATBox(boxId: VATBoxId | BasisBoxId): SKATVATBox {
|
||||||
* Check if a VAT code is deductible (affects input VAT)
|
* Check if a VAT code is deductible (affects input VAT)
|
||||||
*/
|
*/
|
||||||
export function isVATDeductible(code: VATCode): boolean {
|
export function isVATDeductible(code: VATCode): boolean {
|
||||||
return VAT_CODE_CONFIG[code].deductible;
|
return VAT_CODE_CONFIG[code]?.deductible ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a VAT code is reverse charge
|
* Check if a VAT code is reverse charge
|
||||||
*/
|
*/
|
||||||
export function isReverseCharge(code: VATCode): boolean {
|
export function isReverseCharge(code: VATCode): boolean {
|
||||||
return VAT_CODE_CONFIG[code].reverseCharge;
|
return VAT_CODE_CONFIG[code]?.reverseCharge ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get VAT rate for a code
|
* Get VAT rate for a code
|
||||||
*/
|
*/
|
||||||
export function getVATRate(code: VATCode): number {
|
export function getVATRate(code: VATCode): number {
|
||||||
return VAT_CODE_CONFIG[code].rate;
|
return VAT_CODE_CONFIG[code]?.rate ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import {
|
||||||
BulbOutlined,
|
BulbOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useCompany } from '@/hooks/useCompany';
|
import { useCompany } from '@/hooks/useCompany';
|
||||||
import { useReconciliationStore } from '@/stores/reconciliationStore';
|
import { useReconciliationStore } from '@/stores/reconciliationStore';
|
||||||
import { useCompanyStore } from '@/stores/companyStore';
|
import { useCompanyStore } from '@/stores/companyStore';
|
||||||
|
|
@ -67,6 +68,7 @@ interface MatchSuggestion {
|
||||||
|
|
||||||
export default function Bankafstemning() {
|
export default function Bankafstemning() {
|
||||||
const { company } = useCompany();
|
const { company } = useCompany();
|
||||||
|
const navigate = useNavigate();
|
||||||
const { activeCompany } = useCompanyStore();
|
const { activeCompany } = useCompanyStore();
|
||||||
|
|
||||||
// Fetch data from API
|
// Fetch data from API
|
||||||
|
|
@ -218,10 +220,15 @@ export default function Bankafstemning() {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<PageHeader
|
||||||
|
title="Bankafstemning"
|
||||||
|
subtitle={company?.name}
|
||||||
|
breadcrumbs={[{ title: 'Bank' }, { title: 'Bankafstemning' }]}
|
||||||
|
/>
|
||||||
<Skeleton active paragraph={{ rows: 2 }} />
|
<Skeleton active paragraph={{ rows: 2 }} />
|
||||||
<Row gutter={16} style={{ marginTop: 16 }}>
|
<Row gutter={16} style={{ marginTop: 16 }}>
|
||||||
<Col span={12}><Skeleton active paragraph={{ rows: 8 }} /></Col>
|
<Col xs={24} lg={12}><Skeleton active paragraph={{ rows: 8 }} /></Col>
|
||||||
<Col span={12}><Skeleton active paragraph={{ rows: 8 }} /></Col>
|
<Col xs={24} lg={12}><Skeleton active paragraph={{ rows: 8 }} /></Col>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -230,9 +237,20 @@ export default function Bankafstemning() {
|
||||||
// Empty state - no bank connections
|
// Empty state - no bank connections
|
||||||
if (bankAccounts.length === 0) {
|
if (bankAccounts.length === 0) {
|
||||||
return (
|
return (
|
||||||
|
<div>
|
||||||
|
<PageHeader
|
||||||
|
title="Bankafstemning"
|
||||||
|
subtitle={company?.name}
|
||||||
|
breadcrumbs={[{ title: 'Bank' }, { title: 'Bankafstemning' }]}
|
||||||
|
/>
|
||||||
<Empty
|
<Empty
|
||||||
description="Ingen bankforbindelser. Opret forbindelse til din bank under Indstillinger."
|
description="Ingen bankforbindelser. Opret forbindelse til din bank under Indstillinger."
|
||||||
/>
|
>
|
||||||
|
<Button type="primary" onClick={() => navigate('/indstillinger')}>
|
||||||
|
Gå til indstillinger
|
||||||
|
</Button>
|
||||||
|
</Empty>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -241,7 +259,7 @@ export default function Bankafstemning() {
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Bankafstemning"
|
title="Bankafstemning"
|
||||||
subtitle={company?.name}
|
subtitle={company?.name}
|
||||||
breadcrumbs={[{ title: 'Bank', path: '/bankafstemning' }, { title: 'Bankafstemning' }]}
|
breadcrumbs={[{ title: 'Bank' }, { title: 'Bankafstemning' }]}
|
||||||
extra={
|
extra={
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -283,13 +301,13 @@ export default function Bankafstemning() {
|
||||||
<RangePicker
|
<RangePicker
|
||||||
value={dateRange}
|
value={dateRange}
|
||||||
onChange={(dates) => setDateRange(dates as [dayjs.Dayjs, dayjs.Dayjs])}
|
onChange={(dates) => setDateRange(dates as [dayjs.Dayjs, dayjs.Dayjs])}
|
||||||
format="DD/MM/YYYY"
|
format="DD-MM-YYYY"
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
{/* Summary */}
|
{/* Summary */}
|
||||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||||
<Col span={8}>
|
<Col xs={24} sm={8}>
|
||||||
<Card size="small">
|
<Card size="small">
|
||||||
<Statistic
|
<Statistic
|
||||||
title="Bank (uafstemt)"
|
title="Bank (uafstemt)"
|
||||||
|
|
@ -302,7 +320,7 @@ export default function Bankafstemning() {
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col xs={24} sm={8}>
|
||||||
<Card size="small">
|
<Card size="small">
|
||||||
<Statistic
|
<Statistic
|
||||||
title="Bogføring (uafstemt)"
|
title="Bogføring (uafstemt)"
|
||||||
|
|
@ -315,10 +333,10 @@ export default function Bankafstemning() {
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col xs={24} sm={8}>
|
||||||
<Card size="small">
|
<Card size="small">
|
||||||
<Statistic
|
<Statistic
|
||||||
title="Difference"
|
title="Differens"
|
||||||
value={difference}
|
value={difference}
|
||||||
precision={2}
|
precision={2}
|
||||||
formatter={(value) => formatCurrency(value as number)}
|
formatter={(value) => formatCurrency(value as number)}
|
||||||
|
|
@ -372,7 +390,7 @@ export default function Bankafstemning() {
|
||||||
{/* Side-by-side panels */}
|
{/* Side-by-side panels */}
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
{/* Bank Transactions */}
|
{/* Bank Transactions */}
|
||||||
<Col span={12}>
|
<Col xs={24} lg={12}>
|
||||||
<Card
|
<Card
|
||||||
title={
|
title={
|
||||||
<Space>
|
<Space>
|
||||||
|
|
@ -483,7 +501,7 @@ export default function Bankafstemning() {
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
{/* Ledger Entries */}
|
{/* Ledger Entries */}
|
||||||
<Col span={12}>
|
<Col xs={24} lg={12}>
|
||||||
<Card
|
<Card
|
||||||
title={
|
title={
|
||||||
<Space>
|
<Space>
|
||||||
|
|
@ -496,7 +514,7 @@ export default function Bankafstemning() {
|
||||||
>
|
>
|
||||||
{ledgerEntries.length === 0 ? (
|
{ledgerEntries.length === 0 ? (
|
||||||
<Empty
|
<Empty
|
||||||
description="Ingen uafstemte bogføringsposter (API ikke implementeret endnu)"
|
description="Ingen uafstemte bogføringsposter"
|
||||||
style={{ padding: 24 }}
|
style={{ padding: 24 }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -650,7 +668,7 @@ export default function Bankafstemning() {
|
||||||
label="Dato"
|
label="Dato"
|
||||||
rules={[{ required: true }]}
|
rules={[{ required: true }]}
|
||||||
>
|
>
|
||||||
<DatePicker format="DD/MM/YYYY" style={{ width: '100%' }} />
|
<DatePicker format="DD-MM-YYYY" style={{ width: '100%' }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="description"
|
name="description"
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,6 @@ export default function CompanySetupWizard() {
|
||||||
|
|
||||||
setCurrentStep(4); // Success step
|
setCurrentStep(4); // Success step
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Company creation failed:', error);
|
|
||||||
showError(error, 'Kunne ikke oprette virksomhed');
|
showError(error, 'Kunne ikke oprette virksomhed');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Row, Col, Card, Statistic, Typography, Space, Tag, Progress, Skeleton, Empty } from 'antd';
|
import { Row, Col, Card, Statistic, Typography, Space, Tag, Skeleton, Empty } from 'antd';
|
||||||
import {
|
import {
|
||||||
BankOutlined,
|
BankOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
|
|
@ -154,7 +154,11 @@ export default function Dashboard() {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Skeleton active paragraph={{ rows: 1 }} />
|
<PageHeader
|
||||||
|
title="Dashboard"
|
||||||
|
subtitle={company?.name}
|
||||||
|
breadcrumbs={[{ title: 'Dashboard' }]}
|
||||||
|
/>
|
||||||
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
||||||
<Col xs={24} sm={12} lg={6}>
|
<Col xs={24} sm={12} lg={6}>
|
||||||
<Card size="small"><Skeleton active paragraph={{ rows: 2 }} /></Card>
|
<Card size="small"><Skeleton active paragraph={{ rows: 2 }} /></Card>
|
||||||
|
|
@ -367,14 +371,8 @@ export default function Dashboard() {
|
||||||
value={metrics.unreconciledCount}
|
value={metrics.unreconciledCount}
|
||||||
prefix={<FileTextOutlined />}
|
prefix={<FileTextOutlined />}
|
||||||
/>
|
/>
|
||||||
<Progress
|
<Text type="secondary" style={{ display: 'block', marginTop: 16 }}>
|
||||||
percent={metrics.unreconciledCount === 0 ? 100 : 75}
|
Bankafstemning er ikke tilgængelig endnu
|
||||||
status="active"
|
|
||||||
strokeColor={accountingColors.balance}
|
|
||||||
style={{ marginTop: 16 }}
|
|
||||||
/>
|
|
||||||
<Text type="secondary">
|
|
||||||
{metrics.unreconciledCount === 0 ? '100% afstemt' : 'Bankafstemning ikke implementeret endnu'}
|
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -469,7 +469,7 @@ export default function Fakturaer() {
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Fakturaer"
|
title="Fakturaer"
|
||||||
subtitle={company?.name}
|
subtitle={company?.name}
|
||||||
breadcrumbs={[{ title: 'Fakturering', path: '/fakturaer' }, { title: 'Fakturaer' }]}
|
breadcrumbs={[{ title: 'Fakturering' }, { title: 'Fakturaer' }]}
|
||||||
extra={
|
extra={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreateInvoice}>
|
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreateInvoice}>
|
||||||
Ny fakturakladde
|
Ny fakturakladde
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,7 @@ export default function Kassekladde() {
|
||||||
return <Tag color="red">Annulleret</Tag>;
|
return <Tag color="red">Annulleret</Tag>;
|
||||||
}
|
}
|
||||||
return value ? (
|
return value ? (
|
||||||
<Tag color="green">Bogfort</Tag>
|
<Tag color="green">Bogført</Tag>
|
||||||
) : (
|
) : (
|
||||||
<Tag color="orange">Kladde</Tag>
|
<Tag color="orange">Kladde</Tag>
|
||||||
);
|
);
|
||||||
|
|
@ -301,7 +301,7 @@ export default function Kassekladde() {
|
||||||
case 'void':
|
case 'void':
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: 'Annuller bilag',
|
title: 'Annuller bilag',
|
||||||
content: `Er du sikker pa at du vil annullere bilag ${record.transactionNumber}?`,
|
content: `Er du sikker på at du vil annullere bilag ${record.transactionNumber}?`,
|
||||||
okText: 'Annuller bilag',
|
okText: 'Annuller bilag',
|
||||||
okType: 'danger',
|
okType: 'danger',
|
||||||
cancelText: 'Fortryd',
|
cancelText: 'Fortryd',
|
||||||
|
|
@ -352,7 +352,7 @@ export default function Kassekladde() {
|
||||||
const validation = validateDoubleEntry(lines as TransactionLine[]);
|
const validation = validateDoubleEntry(lines as TransactionLine[]);
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
message.error(
|
message.error(
|
||||||
`Debet (${formatCurrency(validation.totalDebit)}) skal vaere lig kredit (${formatCurrency(validation.totalCredit)})`
|
`Debet (${formatCurrency(validation.totalDebit)}) skal være lig kredit (${formatCurrency(validation.totalCredit)})`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -430,7 +430,7 @@ export default function Kassekladde() {
|
||||||
|
|
||||||
const getStatusLabel = (status: JournalEntryDraftStatus): { label: string; color: string } => {
|
const getStatusLabel = (status: JournalEntryDraftStatus): { label: string; color: string } => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'posted': return { label: 'Bogfort', color: 'green' };
|
case 'posted': return { label: 'Bogført', color: 'green' };
|
||||||
case 'discarded': return { label: 'Annulleret', color: 'red' };
|
case 'discarded': return { label: 'Annulleret', color: 'red' };
|
||||||
case 'draft': return { label: 'Kladde', color: 'orange' };
|
case 'draft': return { label: 'Kladde', color: 'orange' };
|
||||||
case 'pending_review': return { label: 'Afventer gennemgang', color: 'blue' };
|
case 'pending_review': return { label: 'Afventer gennemgang', color: 'blue' };
|
||||||
|
|
@ -447,7 +447,7 @@ export default function Kassekladde() {
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Kassekladde"
|
title="Kassekladde"
|
||||||
subtitle={activeCompany?.name}
|
subtitle={activeCompany?.name}
|
||||||
breadcrumbs={[{ title: 'Bogforing', path: '/kontooversigt' }, { title: 'Kassekladde' }]}
|
breadcrumbs={[{ title: 'Bogføring', path: '/kontooversigt' }, { title: 'Kassekladde' }]}
|
||||||
/>
|
/>
|
||||||
<Skeleton active paragraph={{ rows: 10 }} />
|
<Skeleton active paragraph={{ rows: 10 }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -459,7 +459,7 @@ export default function Kassekladde() {
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Kassekladde"
|
title="Kassekladde"
|
||||||
subtitle={activeCompany?.name}
|
subtitle={activeCompany?.name}
|
||||||
breadcrumbs={[{ title: 'Bogforing', path: '/kontooversigt' }, { title: 'Kassekladde' }]}
|
breadcrumbs={[{ title: 'Bogføring', path: '/kontooversigt' }, { title: 'Kassekladde' }]}
|
||||||
extra={
|
extra={
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|
@ -504,7 +504,7 @@ export default function Kassekladde() {
|
||||||
value={statusFilter}
|
value={statusFilter}
|
||||||
onChange={(value) => setStatusFilter(value ?? null)}
|
onChange={(value) => setStatusFilter(value ?? null)}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'posted', label: 'Bogfort' },
|
{ value: 'posted', label: 'Bogført' },
|
||||||
{ value: 'draft', label: 'Kladde' },
|
{ value: 'draft', label: 'Kladde' },
|
||||||
{ value: 'discarded', label: 'Annulleret' },
|
{ value: 'discarded', label: 'Annulleret' },
|
||||||
]}
|
]}
|
||||||
|
|
@ -569,12 +569,12 @@ export default function Kassekladde() {
|
||||||
</Tag>
|
</Tag>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
{detailDraft.isReconciled && detailDraft.postedAt && (
|
{detailDraft.isReconciled && detailDraft.postedAt && (
|
||||||
<Descriptions.Item label="Bogfort">
|
<Descriptions.Item label="Bogført">
|
||||||
{dayjs(detailDraft.postedAt).format('DD-MM-YYYY HH:mm')}
|
{dayjs(detailDraft.postedAt).format('DD-MM-YYYY HH:mm')}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
)}
|
)}
|
||||||
{detailDraft.postedBy && (
|
{detailDraft.postedBy && (
|
||||||
<Descriptions.Item label="Bogfort af">
|
<Descriptions.Item label="Bogført af">
|
||||||
{detailDraft.postedBy}
|
{detailDraft.postedBy}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
)}
|
)}
|
||||||
|
|
@ -647,7 +647,7 @@ export default function Kassekladde() {
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="date"
|
name="date"
|
||||||
label="Dato"
|
label="Dato"
|
||||||
rules={[{ required: true, message: 'Vaelg dato' }]}
|
rules={[{ required: true, message: 'Vælg dato' }]}
|
||||||
initialValue={dayjs()}
|
initialValue={dayjs()}
|
||||||
>
|
>
|
||||||
<DatePicker format="DD-MM-YYYY" style={{ width: 150 }} />
|
<DatePicker format="DD-MM-YYYY" style={{ width: 150 }} />
|
||||||
|
|
@ -683,7 +683,7 @@ export default function Kassekladde() {
|
||||||
<td style={{ padding: 4 }}>
|
<td style={{ padding: 4 }}>
|
||||||
<Select
|
<Select
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
placeholder="Vaelg konto"
|
placeholder="Vælg konto"
|
||||||
showSearch
|
showSearch
|
||||||
optionFilterProp="label"
|
optionFilterProp="label"
|
||||||
value={line.accountId}
|
value={line.accountId}
|
||||||
|
|
@ -752,7 +752,7 @@ export default function Kassekladde() {
|
||||||
<tr style={{ borderTop: '2px solid #f0f0f0' }}>
|
<tr style={{ borderTop: '2px solid #f0f0f0' }}>
|
||||||
<td style={{ padding: 8 }}>
|
<td style={{ padding: 8 }}>
|
||||||
<Button type="dashed" size="small" onClick={handleAddLine}>
|
<Button type="dashed" size="small" onClick={handleAddLine}>
|
||||||
+ Tilføj linje
|
+ Tilføj linje
|
||||||
</Button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
|
|
@ -775,7 +775,7 @@ export default function Kassekladde() {
|
||||||
</td>
|
</td>
|
||||||
<td colSpan={2} style={{ padding: 8 }}>
|
<td colSpan={2} style={{ padding: 8 }}>
|
||||||
{!balance.valid && (
|
{!balance.valid && (
|
||||||
<Tooltip title={`Difference: ${formatCurrency(balance.difference)}`}>
|
<Tooltip title={`Differens: ${formatCurrency(balance.difference)}`}>
|
||||||
<Tag color="red">Ubalance!</Tag>
|
<Tag color="red">Ubalance!</Tag>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import {
|
||||||
Switch,
|
Switch,
|
||||||
Divider,
|
Divider,
|
||||||
Descriptions,
|
Descriptions,
|
||||||
|
Tooltip,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
|
|
@ -128,13 +129,6 @@ export default function Kontooversigt() {
|
||||||
setIsDrawerOpen(true);
|
setIsDrawerOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditAccount = () => {
|
|
||||||
if (selectedAccount) {
|
|
||||||
form.setFieldsValue(selectedAccount);
|
|
||||||
setIsEditMode(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseDrawer = () => {
|
const handleCloseDrawer = () => {
|
||||||
setIsDrawerOpen(false);
|
setIsDrawerOpen(false);
|
||||||
setIsEditMode(false);
|
setIsEditMode(false);
|
||||||
|
|
@ -367,9 +361,11 @@ export default function Kontooversigt() {
|
||||||
onClose={handleCloseDrawer}
|
onClose={handleCloseDrawer}
|
||||||
extra={
|
extra={
|
||||||
!isEditMode && selectedAccount && (
|
!isEditMode && selectedAccount && (
|
||||||
<Button type="primary" icon={<EditOutlined />} onClick={handleEditAccount}>
|
<Tooltip title="Redigering kommer snart">
|
||||||
|
<Button type="primary" icon={<EditOutlined />} disabled>
|
||||||
Rediger
|
Rediger
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
footer={
|
footer={
|
||||||
|
|
|
||||||
|
|
@ -442,7 +442,7 @@ export default function Kreditnotaer() {
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Kreditnotaer"
|
title="Kreditnotaer"
|
||||||
subtitle={company?.name}
|
subtitle={company?.name}
|
||||||
breadcrumbs={[{ title: 'Salg' }, { title: 'Kreditnotaer' }]}
|
breadcrumbs={[{ title: 'Fakturering' }, { title: 'Kreditnotaer' }]}
|
||||||
extra={
|
extra={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreateCreditNote}>
|
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreateCreditNote}>
|
||||||
Ny kreditnota
|
Ny kreditnota
|
||||||
|
|
|
||||||
|
|
@ -173,8 +173,12 @@ export default function Kunder() {
|
||||||
await updateCustomerMutation.mutateAsync(input);
|
await updateCustomerMutation.mutateAsync(input);
|
||||||
showSuccess('Kunde opdateret');
|
showSuccess('Kunde opdateret');
|
||||||
} else {
|
} else {
|
||||||
|
if (!company) {
|
||||||
|
showError(new Error('Ingen virksomhed valgt'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const input: CreateCustomerInput = {
|
const input: CreateCustomerInput = {
|
||||||
companyId: company!.id,
|
companyId: company.id,
|
||||||
customerType: values.customerType.toUpperCase() as 'BUSINESS' | 'PRIVATE',
|
customerType: values.customerType.toUpperCase() as 'BUSINESS' | 'PRIVATE',
|
||||||
name: values.name,
|
name: values.name,
|
||||||
cvr: values.cvr || undefined,
|
cvr: values.cvr || undefined,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
Button,
|
Button,
|
||||||
Descriptions,
|
Descriptions,
|
||||||
Collapse,
|
Collapse,
|
||||||
|
Tooltip,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
|
|
@ -279,7 +280,9 @@ export default function Loenforstaelse() {
|
||||||
subtitle={company?.name}
|
subtitle={company?.name}
|
||||||
breadcrumbs={[{ title: 'Løn' }, { title: 'Lønforståelse' }]}
|
breadcrumbs={[{ title: 'Løn' }, { title: 'Lønforståelse' }]}
|
||||||
extra={
|
extra={
|
||||||
<Button icon={<DownloadOutlined />}>Eksporter lønsedler</Button>
|
<Tooltip title="Eksport er endnu ikke implementeret">
|
||||||
|
<Button icon={<DownloadOutlined />} disabled>Eksporter lønsedler</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
Modal,
|
Modal,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
|
InputNumber,
|
||||||
Select,
|
Select,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Alert,
|
Alert,
|
||||||
|
|
@ -696,7 +697,7 @@ export default function Ordrer() {
|
||||||
<Tag>{line.vatCode}</Tag>
|
<Tag>{line.vatCode}</Tag>
|
||||||
{line.isInvoiced && line.invoicedAt && (
|
{line.isInvoiced && line.invoicedAt && (
|
||||||
<Tag color="blue">
|
<Tag color="blue">
|
||||||
Faktureret: {dayjs(line.invoicedAt).format('DD/MM/YYYY')}
|
Faktureret: {dayjs(line.invoicedAt).format('DD-MM-YYYY')}
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
|
|
@ -876,7 +877,7 @@ export default function Ordrer() {
|
||||||
label="Antal"
|
label="Antal"
|
||||||
rules={[{ required: true, message: 'Angiv antal' }]}
|
rules={[{ required: true, message: 'Angiv antal' }]}
|
||||||
>
|
>
|
||||||
<Input type="number" min={0} step={1} />
|
<InputNumber min={0} step={1} style={{ width: '100%' }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
|
|
@ -893,10 +894,10 @@ export default function Ordrer() {
|
||||||
label="Enhedspris"
|
label="Enhedspris"
|
||||||
rules={[{ required: true, message: 'Angiv pris' }]}
|
rules={[{ required: true, message: 'Angiv pris' }]}
|
||||||
>
|
>
|
||||||
<Input
|
<InputNumber
|
||||||
type="number"
|
|
||||||
min={0}
|
min={0}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
|
style={{ width: '100%' }}
|
||||||
disabled={addLineMode === 'product' && !!selectedProductId}
|
disabled={addLineMode === 'product' && !!selectedProductId}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
@ -905,7 +906,7 @@ export default function Ordrer() {
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item name="discountPercent" label="Rabat (%)">
|
<Form.Item name="discountPercent" label="Rabat (%)">
|
||||||
<Input type="number" min={0} max={100} step={1} />
|
<InputNumber min={0} max={100} step={1} style={{ width: '100%' }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
|
|
|
||||||
|
|
@ -173,8 +173,12 @@ export default function Produkter() {
|
||||||
await updateProductMutation.mutateAsync(input);
|
await updateProductMutation.mutateAsync(input);
|
||||||
showSuccess('Produkt opdateret');
|
showSuccess('Produkt opdateret');
|
||||||
} else {
|
} else {
|
||||||
|
if (!company) {
|
||||||
|
showError(new Error('Ingen virksomhed valgt'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const input: CreateProductInput = {
|
const input: CreateProductInput = {
|
||||||
companyId: company!.id,
|
companyId: company.id,
|
||||||
productNumber: values.productNumber || undefined,
|
productNumber: values.productNumber || undefined,
|
||||||
name: values.name,
|
name: values.name,
|
||||||
description: values.description || undefined,
|
description: values.description || undefined,
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
Divider,
|
Divider,
|
||||||
message,
|
message,
|
||||||
Space,
|
Space,
|
||||||
|
Tooltip,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
SaveOutlined,
|
SaveOutlined,
|
||||||
|
|
@ -61,19 +62,6 @@ export default function Settings() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSavePreferences = async () => {
|
|
||||||
try {
|
|
||||||
await preferencesForm.validateFields();
|
|
||||||
// TODO: Backend does not yet have a preferences mutation.
|
|
||||||
// Preferences like VAT period, auto-reconcile, etc. need a dedicated backend endpoint.
|
|
||||||
message.info('Præferencer er endnu ikke forbundet til backend');
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
message.error(`Fejl ved gemning: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const tabItems = [
|
const tabItems = [
|
||||||
{
|
{
|
||||||
key: 'company',
|
key: 'company',
|
||||||
|
|
@ -278,9 +266,11 @@ export default function Settings() {
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<div style={{ textAlign: 'right' }}>
|
<div style={{ textAlign: 'right' }}>
|
||||||
<Button type="primary" icon={<SaveOutlined />} onClick={handleSavePreferences}>
|
<Tooltip title="Præferencer er endnu ikke forbundet til backend">
|
||||||
|
<Button type="primary" icon={<SaveOutlined />} disabled>
|
||||||
Gem præferencer
|
Gem præferencer
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import {
|
||||||
import type { UploadProps } from 'antd';
|
import type { UploadProps } from 'antd';
|
||||||
import { spacing } from '@/styles/designTokens';
|
import { spacing } from '@/styles/designTokens';
|
||||||
import { PageHeader } from '@/components/shared/PageHeader';
|
import { PageHeader } from '@/components/shared/PageHeader';
|
||||||
|
import { DemoDataDisclaimer } from '@/components/shared/DemoDataDisclaimer';
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
|
|
@ -461,6 +462,8 @@ export default function UserSettings() {
|
||||||
breadcrumbs={[{ title: 'Brugerindstillinger' }]}
|
breadcrumbs={[{ title: 'Brugerindstillinger' }]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DemoDataDisclaimer message="Brugerindstillinger er endnu ikke forbundet til backend. Ændringer gemmes ikke." />
|
||||||
|
|
||||||
<Tabs items={tabItems} />
|
<Tabs items={tabItems} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,24 @@ import type { VATPeriodicitet } from './periods';
|
||||||
* VAT codes used in Danish bookkeeping
|
* VAT codes used in Danish bookkeeping
|
||||||
*/
|
*/
|
||||||
export type VATCode =
|
export type VATCode =
|
||||||
| 'S25' // Salgsmoms 25% (udgaaende moms)
|
| 'U25' // Udgående moms 25% (salg)
|
||||||
| 'K25' // Koebsmoms 25% (indgaaende moms)
|
| 'UEU' // EU-salg (0%)
|
||||||
| 'EU_VARE' // EU-varekoeb (reverse charge)
|
| 'UEXP' // Eksport (0%)
|
||||||
| 'EU_YDELSE' // EU-ydelseskoeb (reverse charge)
|
| 'I25' // Indgående moms 25% (køb)
|
||||||
| 'MOMSFRI' // Momsfritaget (healthcare, education, etc.)
|
| 'IEUV' // EU-erhvervelse varer (reverse charge)
|
||||||
| 'EKSPORT' // Eksport (0%)
|
| 'IEUY' // EU-erhvervelse ydelser (reverse charge)
|
||||||
| 'NONE'; // Ingen moms
|
| '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
|
* VAT code type classification
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue