Public/Test-LedgerSie.ps1
|
<# .SYNOPSIS Validates a SIE file without importing it. .DESCRIPTION Parses a SIE 4 (or compatible) file and runs structural checks: every #VER block must balance to zero, every #TRANS must reference a #KONTO declared in the same file, and verification numbers within a series must be unique. Returns a result object with IsValid, Errors, Warnings and Summary properties. .PARAMETER Path Path to the SIE file to validate. The file is read as CP437 (PC-8). .EXAMPLE Test-LedgerSie -Path .\export.se Returns a result object. Use the IsValid property to check the outcome. .EXAMPLE $result = Test-LedgerSie -Path .\fromfortnox.se if (-not $result.IsValid) { $result.Errors | ForEach-Object { Write-Warning $_ } } Inspect errors before deciding whether to import. #> function Test-LedgerSie { [OutputType([pscustomobject])] [CmdletBinding()] param ( [Parameter(Mandatory)] [string]$Path ) $errors = New-Object System.Collections.Generic.List[string] $warnings = New-Object System.Collections.Generic.List[string] $text = Read-SieText -Path $Path $records = ConvertFrom-SieText -Text $text $accounts = @{} $verKeys = @{} $dimensions = @{} $objects = @{} $verCount = 0 $transCount = 0 $sieType = $null foreach ($rec in $records) { switch ($rec.Tag) { 'SIETYP' { if ($rec.Fields.Count -ge 1) { $sieType = $rec.Fields[0] } } 'DIM' { if ($rec.Fields.Count -ge 2) { $dimensions[[int]$rec.Fields[0]] = $rec.Fields[1] } } 'OBJEKT' { if ($rec.Fields.Count -ge 3) { $dimNum = [int]$rec.Fields[0] if (-not $dimensions.ContainsKey($dimNum)) { $errors.Add("#OBJEKT references undeclared dimension: $dimNum") } $objects["${dimNum}/$($rec.Fields[1])"] = $rec.Fields[2] } } 'KONTO' { if ($rec.Fields.Count -lt 1) { $errors.Add("#KONTO record missing account number") continue } $accounts[$rec.Fields[0]] = if ($rec.Fields.Count -ge 2) { $rec.Fields[1] } else { '' } } 'VER' { $verCount++ if ($rec.Fields.Count -lt 3) { $errors.Add("#VER record has too few fields: $($rec.Fields -join ' ')") continue } $series = $rec.Fields[0] $verNo = $rec.Fields[1] $key = "$series/$verNo" if ($verKeys.ContainsKey($key)) { $errors.Add("Duplicate verification: #VER $series $verNo") } $verKeys[$key] = $true $sum = [decimal]0 foreach ($t in $rec.Transactions) { $transCount++ if ($t.Fields.Count -lt 2) { $errors.Add("#TRANS in VER $series $verNo has too few fields") continue } $acct = $t.Fields[0] if (-not $accounts.ContainsKey($acct)) { $errors.Add("#TRANS in VER $series $verNo references unknown account: $acct") } # Validate object references if ($t.Objects -and $t.Objects.Count -ge 2) { for ($oi = 0; $oi -lt $t.Objects.Count; $oi += 2) { if ($oi + 1 -lt $t.Objects.Count) { $dimN = [int]$t.Objects[$oi] $objN = $t.Objects[$oi + 1] $objKey = "${dimN}/${objN}" if (-not $objects.ContainsKey($objKey)) { $errors.Add("#TRANS in VER $series $verNo references unknown object: dim=$dimN obj=$objN") } } } } $amountText = $t.Fields[1] -replace ',', '.' $amount = [decimal]0 if (-not [decimal]::TryParse($amountText, [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$amount)) { $errors.Add("#TRANS in VER $series $verNo has invalid amount: $($t.Fields[1])") continue } $sum += $amount } if ($sum -ne 0) { $errors.Add("#VER $series $verNo does not balance. Sum: $sum") } } } } if (-not $sieType) { $warnings.Add("Missing #SIETYP record") } elseif ($sieType -ne '4') { $warnings.Add("SIE type is $sieType; PSLedger is optimised for type 4") } [PSCustomObject]@{ Path = $Path IsValid = ($errors.Count -eq 0) SieType = $sieType Accounts = $accounts.Count Verifications = $verCount Transactions = $transCount Errors = $errors.ToArray() Warnings = $warnings.ToArray() } } |