Private/Archive.ps1
|
# Archive packages (arkivering): report blocks and checksum helpers used by # Export-LedgerArchive and Test-LedgerArchive. The reports use the shared block # model (see AnnualReportRender.ps1) so each one is written as both text and PDF. # Table text columns are kept short so a row fits an A4 PDF page in 9 pt Courier. $script:LedgerArchiveFormatVersion = 1 $script:LedgerArchiveChecksumFile = 'checksums.txt' $script:LedgerArchiveManifestFile = 'manifest.txt' function Format-LedgerArchiveAmount { [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory)] [decimal]$Amount, [switch]$BlankZero ) if ($BlankZero -and $Amount -eq 0) { return '' } $Amount.ToString('#,##0.00', [cultureinfo]::InvariantCulture).Replace(',', ' ').Replace('.', ',') } function Get-LedgerArchiveShortText { [CmdletBinding()] [OutputType([string])] param ( [AllowEmptyString()] [AllowNull()] [string]$Text, [Parameter(Mandatory)] [int]$MaxLength ) if ($null -eq $Text) { return '' } if ($Text.Length -le $MaxLength) { return $Text } $Text.Substring(0, $MaxLength - 2) + '..' } function New-LedgerArchiveReportHeader { [CmdletBinding()] [OutputType([hashtable[]])] param ( [Parameter(Mandatory)] [string]$Title, [Parameter(Mandatory)] $Context ) $company = if ($Context.OrgNumber) { "$($Context.CompanyName) ($($Context.OrgNumber))" } else { $Context.CompanyName } $blocks = @( @{ Type = 'Title'; Text = $Title } @{ Type = 'Paragraph'; Text = $company } @{ Type = 'Paragraph'; Text = "Räkenskapsår $($Context.StartDate) - $($Context.EndDate)" } ) if ($Context.Preliminary) { $blocks += @{ Type = 'Paragraph'; Text = 'PRELIMINÄR: räkenskapsåret var inte stängt när arkivet skapades.' } } $blocks += @{ Type = 'Paragraph'; Text = "Framtagen $($Context.Created) med PSLedger $($Context.Version)." } $blocks } function Get-LedgerArchiveRowText { [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory)] $Row, [Parameter(Mandatory)] [hashtable]$AccountNames ) $text = [string]$AccountNames[[string]$Row.Account] if ($Row.Objects -and $Row.Objects.Count -gt 0) { $tags = @($Row.Objects.Keys | Sort-Object | ForEach-Object { "$($_):$($Row.Objects[$_])" }) -join ' ' $text = "$text [$tags]" } if ($Row.Comment) { $text = "$text - $($Row.Comment)" } $text.Trim() } function Build-LedgerArchiveJournalBlocks { <# .SYNOPSIS Grundbok: every verification in date order with its rows. #> [CmdletBinding()] [OutputType([hashtable[]])] param ( [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$Entries, [Parameter(Mandatory)] [hashtable]$AccountNames, [Parameter(Mandatory)] $Context ) $rows = [System.Collections.Generic.List[object]]::new() $styles = [System.Collections.Generic.List[string]]::new() $totalDebit = [decimal]0 $totalCredit = [decimal]0 $sorted = $Entries | Sort-Object @{ Expression = { [datetime]$_.Date } }, @{ Expression = { [int]$_.VerificationNumber } } foreach ($e in $sorted) { $rows.Add(@([string]$e.VerificationNumber, ([datetime]$e.Date).ToString('yyyy-MM-dd'), '', (Get-LedgerArchiveShortText $e.Description 40), '', '')) $styles.Add('Section') foreach ($r in $e.Rows) { $amount = [decimal]$r.Amount if ($amount -gt 0) { $totalDebit += $amount } else { $totalCredit -= $amount } $rows.Add(@('', '', [string]$r.Account, (Get-LedgerArchiveShortText (Get-LedgerArchiveRowText -Row $r -AccountNames $AccountNames) 40), (Format-LedgerArchiveAmount ([Math]::Max($amount, 0)) -BlankZero), (Format-LedgerArchiveAmount ([Math]::Max(-$amount, 0)) -BlankZero))) $styles.Add('Normal') } } $rows.Add(@('', '', '', "Summa ($(@($Entries).Count) verifikationer)", (Format-LedgerArchiveAmount $totalDebit), (Format-LedgerArchiveAmount $totalCredit))) $styles.Add('Sum') @(New-LedgerArchiveReportHeader -Title 'Grundbok (verifikationslista)' -Context $Context) + @( @{ Type = 'Table'; Header = @('Ver', 'Datum', 'Konto', 'Text', 'Debet', 'Kredit') Align = @('right', 'left', 'left', 'left', 'right', 'right') Rows = $rows.ToArray(); RowStyles = $styles.ToArray() } ) } function Build-LedgerArchiveGeneralLedgerBlocks { <# .SYNOPSIS Huvudbok: per account, opening balance, every transaction and closing balance. #> [CmdletBinding()] [OutputType([hashtable[]])] param ( [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$Entries, [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$Balances, [Parameter(Mandatory)] $Context ) $byAccount = @{} $sorted = $Entries | Sort-Object @{ Expression = { [datetime]$_.Date } }, @{ Expression = { [int]$_.VerificationNumber } } foreach ($e in $sorted) { foreach ($r in $e.Rows) { $key = [string]$r.Account if (-not $byAccount.ContainsKey($key)) { $byAccount[$key] = [System.Collections.Generic.List[object]]::new() } $byAccount[$key].Add([pscustomobject]@{ Entry = $e; Amount = [decimal]$r.Amount }) } } $rows = [System.Collections.Generic.List[object]]::new() $styles = [System.Collections.Generic.List[string]]::new() foreach ($b in ($Balances | Sort-Object { [string]$_.AccountNumber })) { $account = [string]$b.AccountNumber $balance = [decimal]$b.OpeningBalance $rows.Add(@($account, '', (Get-LedgerArchiveShortText $b.AccountName 30), '', '', '')) $styles.Add('Section') $rows.Add(@('', '', 'Ingående saldo', '', '', (Format-LedgerArchiveAmount $balance))) $styles.Add('Normal') $debit = [decimal]0 $credit = [decimal]0 if ($byAccount.ContainsKey($account)) { foreach ($t in $byAccount[$account]) { $balance += $t.Amount if ($t.Amount -gt 0) { $debit += $t.Amount } else { $credit -= $t.Amount } $rows.Add(@([string]$t.Entry.VerificationNumber, ([datetime]$t.Entry.Date).ToString('yyyy-MM-dd'), (Get-LedgerArchiveShortText $t.Entry.Description 30), (Format-LedgerArchiveAmount ([Math]::Max($t.Amount, 0)) -BlankZero), (Format-LedgerArchiveAmount ([Math]::Max(-$t.Amount, 0)) -BlankZero), (Format-LedgerArchiveAmount $balance))) $styles.Add('Normal') } } $rows.Add(@('', '', 'Utgående saldo', (Format-LedgerArchiveAmount $debit), (Format-LedgerArchiveAmount $credit), (Format-LedgerArchiveAmount $balance))) $styles.Add('Sum') } @(New-LedgerArchiveReportHeader -Title 'Huvudbok' -Context $Context) + @( @{ Type = 'Table'; Header = @('Ver', 'Datum', 'Text', 'Debet', 'Kredit', 'Saldo') Align = @('right', 'left', 'left', 'right', 'right', 'right') Rows = $rows.ToArray(); RowStyles = $styles.ToArray() } ) } function Build-LedgerArchiveTrialBalanceBlocks { [CmdletBinding()] [OutputType([hashtable[]])] param ( [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$Balances, [Parameter(Mandatory)] $Context ) $rows = [System.Collections.Generic.List[object]]::new() foreach ($b in ($Balances | Sort-Object { [string]$_.AccountNumber })) { $rows.Add(@([string]$b.AccountNumber, (Get-LedgerArchiveShortText $b.AccountName 24), (Format-LedgerArchiveAmount ([decimal]$b.OpeningBalance)), (Format-LedgerArchiveAmount ([decimal]$b.Debit)), (Format-LedgerArchiveAmount ([decimal]$b.Credit)), (Format-LedgerArchiveAmount ([decimal]$b.Balance)))) } @(New-LedgerArchiveReportHeader -Title 'Saldobalans' -Context $Context) + @( @{ Type = 'Table'; Header = @('Konto', 'Namn', 'Ingående', 'Debet', 'Kredit', 'Utgående') Align = @('left', 'left', 'right', 'right', 'right', 'right'); Rows = $rows.ToArray() } ) } function Build-LedgerArchiveStatementBlocks { <# .SYNOPSIS Resultaträkning or balansräkning from Get-LedgerIncomeStatement/BalanceSheet lines. #> [CmdletBinding()] [OutputType([hashtable[]])] param ( [Parameter(Mandatory)] [string]$Title, [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$Lines, [Parameter(Mandatory)] $Context, [string[]]$SumGroups = @(), [string[]]$NegateSections = @(), [string]$Note ) $rows = [System.Collections.Generic.List[object]]::new() $styles = [System.Collections.Generic.List[string]]::new() foreach ($l in $Lines) { $amount = [decimal]$l.Amount if ($l.Section -in $NegateSections) { $amount = -$amount } $rows.Add(@([string]$l.Label, (Format-LedgerArchiveAmount $amount))) $styles.Add($(if ($l.Group -in $SumGroups) { 'Sum' } else { 'Normal' })) } $blocks = @(New-LedgerArchiveReportHeader -Title $Title -Context $Context) if ($Note) { $blocks += @{ Type = 'Paragraph'; Text = $Note } } $blocks + @( @{ Type = 'Table'; Header = @('Post', 'Belopp'); Align = @('left', 'right') Rows = $rows.ToArray(); RowStyles = $styles.ToArray() } ) } function Build-LedgerArchiveVatBlocks { [CmdletBinding()] [OutputType([hashtable[]])] param ( [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$Lines, [Parameter(Mandatory)] $Context ) $rows = [System.Collections.Generic.List[object]]::new() foreach ($l in $Lines) { $rows.Add(@([string]$l.Box, (Get-LedgerArchiveShortText $l.Name 50), (Format-LedgerArchiveAmount ([decimal]$l.Amount)))) } @(New-LedgerArchiveReportHeader -Title 'Momsrapport' -Context $Context) + @( @{ Type = 'Paragraph'; Text = 'Hela räkenskapsåret, per ruta i momsdeklarationen.' } @{ Type = 'Table'; Header = @('Ruta', 'Benämning', 'Belopp'); Align = @('right', 'left', 'right'); Rows = $rows.ToArray() } ) } function Write-LedgerArchiveReport { <# .SYNOPSIS Writes report blocks as <BasePath>.txt and <BasePath>.pdf. #> [CmdletBinding()] param ( [Parameter(Mandatory)] [object[]]$Block, [Parameter(Mandatory)] [string]$BasePath ) [System.IO.File]::WriteAllText("$BasePath.txt", (ConvertTo-LedgerReportText -Block $Block), [System.Text.UTF8Encoding]::new($false)) ConvertTo-LedgerReportPdf -Block $Block -Path "$BasePath.pdf" } function Get-LedgerArchiveFileHash { [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory)] [string]$Path ) $stream = [System.IO.File]::OpenRead($Path) try { [Convert]::ToHexString([System.Security.Cryptography.SHA256]::HashData($stream)).ToLowerInvariant() } finally { $stream.Dispose() } } function Get-LedgerArchiveRelativeFile { <# .SYNOPSIS Files under a package root as relative paths with forward slashes, sorted ordinally. #> [CmdletBinding()] [OutputType([string[]])] param ( [Parameter(Mandatory)] [string]$Root ) $rootFull = [System.IO.Path]::GetFullPath($Root).TrimEnd('\', '/') $files = foreach ($f in Get-ChildItem -LiteralPath $rootFull -Recurse -File -Force) { $f.FullName.Substring($rootFull.Length + 1).Replace('\', '/') } [string[]]$sorted = @($files) [Array]::Sort($sorted, [StringComparer]::Ordinal) , $sorted } function Read-LedgerArchiveManifest { [CmdletBinding()] [OutputType([hashtable])] param ( [Parameter(Mandatory)] [string]$Path ) $manifest = @{} foreach ($line in [System.IO.File]::ReadAllLines($Path)) { if (-not $line -or $line.StartsWith(';')) { continue } $parts = $line.Split("`t", 2) if ($parts.Count -eq 2) { $manifest[$parts[0]] = $parts[1] } } $manifest } |