Public/Export-LedgerVatDeclaration.ps1

<#
.SYNOPSIS
Exports a VAT declaration (momsdeklaration) as a Skatteverket eSKD file.

.DESCRIPTION
Builds the VAT report for a period (via Get-LedgerVatReport) and writes it as an
electronic tax declaration file (eSKD) that can be uploaded in Skatteverket's
e-service for VAT (Lämna momsdeklaration via fil).

The file is XML with an <eSKDUpload Version="6.0"> root, the company organisation
number and a <Moms> section holding the declaration period and one element per
declaration box (ruta). Amounts are reported in whole kronor (rounded). Boxes
05 (ForsMomsEjAnnan), 10 (MomsUtgHog), 11 (MomsUtgMedel), 12 (MomsUtgLag),
48 (MomsIngAvdr) and 49 (MomsBetala) are always written; the other boxes of the
eSKD 6.0 format (06–08, 20–24, 30–32, 35–42, 50 and 60–62) are written when the
report has an amount for them. See Get-LedgerVatReport for the account mapping
and the optional vat.txt override.

Box 49 (MomsBetala) is computed from the rounded box values as
(10 + 11 + 12 + 30 + 31 + 32 + 60 + 61 + 62) - 48, matching how Skatteverket
sums the declaration. A positive value is VAT to pay; a negative value is VAT to
reclaim.

The file is written with ISO-8859-1 encoding, as required by the eSKD format.

.PARAMETER JournalPath
The path to an existing journal directory.

.PARAMETER FiscalYear
The fiscal year identifier (e.g. '2024-01_2024-12').

.PARAMETER FromDate
Start of the VAT reporting period (inclusive).

.PARAMETER ToDate
End of the VAT reporting period (inclusive).

.PARAMETER Path
Destination path for the eSKD file. Conventionally uses the .xml extension.

.PARAMETER Period
The declaration period as YYYYMM (e.g. '202403'). Defaults to the month of
-ToDate.

.PARAMETER Force
Overwrite the destination file if it already exists.

.EXAMPLE
Export-LedgerVatDeclaration -JournalPath .\MinFirma.ledger -FiscalYear '2024-01_2024-12' -FromDate '2024-01-01' -ToDate '2024-03-31' -Path .\moms-2024-q1.xml

Exports the Q1 2024 VAT declaration as an eSKD file.

.EXAMPLE
Export-LedgerVatDeclaration -JournalPath .\Konsult.ledger -FiscalYear '2024-01_2024-12' -FromDate '2024-06-01' -ToDate '2024-06-30' -Period '202406' -Path C:\Moms\konsult-2024-06.xml -Force

Exports the June 2024 declaration with an explicit period, overwriting any
existing file.
#>

function Export-LedgerVatDeclaration {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter()]
        [string]$JournalPath,

        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias('Name')]
        [string]$FiscalYear,

        [Parameter(Mandatory)]
        [datetime]$FromDate,

        [Parameter(Mandatory)]
        [datetime]$ToDate,

        [Parameter(Mandatory)]
        [string]$Path,

        [Parameter()]
        [ValidatePattern('^\d{6}$')]
        [string]$Period,

        [switch]$Force
    )
    process {
        $JournalPath = Resolve-LedgerJournalPath -JournalPath $JournalPath
        $FiscalYear = Resolve-LedgerFiscalYear -FiscalYear $FiscalYear -JournalPath $JournalPath

        if ($ToDate -lt $FromDate) {
            throw "ToDate ($($ToDate.ToString('yyyy-MM-dd'))) must not be earlier than FromDate ($($FromDate.ToString('yyyy-MM-dd')))."
        }

        $DestPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
        if ((Test-Path -LiteralPath $DestPath) -and -not $Force) {
            throw "Destination file already exists: $DestPath. Use -Force to overwrite."
        }

        $journal = Get-LedgerJournal -Path $JournalPath
        $orgNr = $journal.OrgNumber
        if (-not $orgNr) {
            throw "Journal has no OrgNumber; set one with Set-LedgerJournal before exporting a VAT declaration."
        }

        if (-not $Period) {
            $Period = $ToDate.ToString('yyyyMM')
        }

        $report = @(Get-LedgerVatReport -JournalPath $JournalPath -FiscalYear $FiscalYear `
                -FromDate $FromDate -ToDate $ToDate)

        # Round every box to whole kronor (Skatteverket declarations use integers).
        $boxAmount = @{}
        foreach ($box in $report) {
            $boxAmount[[int]$box.Box] = [long][Math]::Round([decimal]$box.Amount, 0, [System.MidpointRounding]::AwayFromZero)
        }

        # Box 49 is derived from the rounded output/input boxes so it always
        # reconciles with the individual rows the way Skatteverket sums them.
        $definitions = @(Get-VatBoxDefinition)
        $outputVat = 0L
        foreach ($def in $definitions | Where-Object Kind -eq 'OutputVat') {
            if ($boxAmount.ContainsKey($def.Box)) { $outputVat += $boxAmount[$def.Box] }
        }
        $inputVat = if ($boxAmount.ContainsKey(48)) { $boxAmount[48] } else { 0L }
        $boxAmount[49] = $outputVat - $inputVat

        $sb = New-Object System.Text.StringBuilder
        $nl = "`r`n"
        $append = {
            param($line)
            [void]$sb.Append($line)
            [void]$sb.Append($nl)
        }

        & $append '<?xml version="1.0" encoding="ISO-8859-1"?>'
        & $append '<eSKDUpload Version="6.0">'
        & $append " <OrgNr>$orgNr</OrgNr>"
        & $append ' <Moms>'
        & $append " <Period>$Period</Period>"
        $boxes = [ordered]@{}
        foreach ($def in $definitions) {
            if (-not $boxAmount.ContainsKey($def.Box)) { continue }
            & $append " <$($def.Tag)>$($boxAmount[$def.Box])</$($def.Tag)>"
            $boxes['{0:D2}' -f $def.Box] = $boxAmount[$def.Box]
        }
        & $append ' </Moms>'
        & $append '</eSKDUpload>'

        if (-not $PSCmdlet.ShouldProcess($DestPath, "Export VAT declaration for $Period")) {
            return
        }

        $encoding = [System.Text.Encoding]::GetEncoding('ISO-8859-1')
        [System.IO.File]::WriteAllText($DestPath, $sb.ToString(), $encoding)

        [PSCustomObject]@{
            Path      = $DestPath
            OrgNumber = $orgNr
            Period    = $Period
            Boxes     = [PSCustomObject]$boxes
        }
    }
}