Public/Convert-LedgerChart.ps1
|
<# .SYNOPSIS Converts accounts from PSLedger's earlier chart templates to standard BAS accounts. .DESCRIPTION Earlier versions of PSLedger shipped chart templates (BAS-Komplett and BAS-Smaforetag) that used some account numbers that do not follow the BAS chart of accounts, for example 3300 'Försäljning tjänster inom EU' instead of 3308 and 4020 'Inköp varor och material EU' instead of 4515. Because the VAT report maps accounts to boxes by their BAS numbers, amounts on these accounts end up in the wrong box of the VAT return. Convert-LedgerChart finds these legacy accounts, recognised by both number and the name the template gave them, and: - adds the matching standard BAS account to the chart of accounts; - writes rules to the journal's vat.txt so that amounts already booked on the legacy account are reported in the right VAT box (existing vat.txt rules are kept and take precedence); - with -Date, books one transfer verification (omföring) that moves the balance of each legacy account at that date to the new account, so the rest of the year is booked on the BAS account. The whole conversion runs under the journal write lock. It is safe to repeat: accounts and VAT rules that already exist are kept, and the balance at -Date already includes an earlier transfer on that date, so an interrupted conversion is completed by running the same command again without moving any balance twice. Posted verifications are never changed and the legacy accounts are not removed or renamed. Accounts whose correct BAS account depends on what was booked on them ('Försäljning tjänster utrikes', 'Försäljning utrikes', 'Inköp varor och material utland') are not converted; a warning explains what to decide. With -Date, the year must be open and the date must be within its range. Source and target accounts must have the same effective VAT box after the planned mapping changes. Conflicting overrides cause an error before any files are changed; resolve the conflict in vat.txt or omit -Date to map accounts only. One object is returned per legacy account found. .PARAMETER JournalPath The path to an existing journal directory. .PARAMETER FiscalYear The fiscal year of the transfer verification. Defaults to the current fiscal year. Only used with -Date. .PARAMETER Date Books a transfer verification on this date that moves the balance of each converted legacy account to its new BAS account. Without -Date no verification is booked. .EXAMPLE Convert-LedgerChart -JournalPath .\Konsultbyrån.ledger -WhatIf Shows which legacy accounts would be converted without changing anything. .EXAMPLE Convert-LedgerChart -JournalPath .\Hantverkarna.ledger -FiscalYear '2025-01_2025-12' -Date '2025-07-01' | Format-Table Account, NewAccount, Box, Transferred Adds the BAS accounts, maps the legacy accounts to the right VAT boxes and moves their balances on 1 July 2025 to the new accounts, e.g. 3300 to 3308 (box 39). #> function Convert-LedgerChart { [OutputType([pscustomobject])] [CmdletBinding(SupportsShouldProcess)] param ( [Parameter()] [string]$JournalPath, [Parameter(ValueFromPipelineByPropertyName)] [Alias('Name')] [string]$FiscalYear, [Parameter()] [datetime]$Date ) process { $JournalPath = Resolve-LedgerJournalPath -JournalPath $JournalPath -SchemaCheck Write $journalLock = $null try { $journalLock = Enter-LedgerJournalLock -JournalPath $JournalPath Assert-LedgerJournalSchema -Path $JournalPath -Write # Box $null means "not in any box"; Ambiguous accounts are only reported. $legacy = @( @{ Account = '2660'; Name = 'Moms omvänd skattskyldighet'; NewAccount = '2614'; NewName = 'Utgående moms omvänd skattskyldighet 25%'; Box = 30 } @{ Account = '3050'; Name = 'Försäljning tjänster momsfri'; NewAccount = '3004'; NewName = 'Försäljning inom Sverige, momsfri'; Box = 42 } @{ Account = '3060'; Name = 'Försäljning tjänster utrikes'; Ambiguous = 'Book EU services on 3308 (box 39) and other services abroad on 3305 (box 40), and map 3060 in vat.txt.' } @{ Account = '3100'; Name = 'Försäljning varor inom EU'; NewAccount = '3108'; NewName = 'Försäljning varor till annat EU-land, momsfri'; Box = 35 } @{ Account = '3200'; Name = 'Försäljning varor utanför EU'; NewAccount = '3105'; NewName = 'Försäljning varor till land utanför EU'; Box = 36 } @{ Account = '3200'; Name = 'Försäljning utrikes'; Ambiguous = 'Use 3105/3108 for goods and 3305/3308 for services (boxes 36/35 and 40/39), and map 3200 in vat.txt.' } @{ Account = '3300'; Name = 'Försäljning tjänster inom EU'; NewAccount = '3308'; NewName = 'Försäljning tjänster till annat EU-land'; Box = 39 } @{ Account = '3400'; Name = 'Försäljning tjänster utanför EU'; NewAccount = '3305'; NewName = 'Försäljning tjänster till land utanför EU'; Box = 40 } @{ Account = '4020'; Name = 'Inköp varor och material EU'; NewAccount = '4515'; NewName = 'Inköp av varor från annat EU-land, 25%'; Box = 20 } @{ Account = '4030'; Name = 'Inköp varor och material utland'; Ambiguous = 'Use 4545 for imported goods (box 50) and 4531 for services from outside the EU (box 22), and map 4030 in vat.txt.' } @{ Account = '4530'; Name = 'Importkostnader'; NewAccount = '5720'; NewName = 'Tull- och speditionskostnader m.m.'; Box = $null } @{ Account = '4531'; Name = 'Tull'; NewAccount = '5720'; NewName = 'Tull- och speditionskostnader m.m.'; Box = $null } @{ Account = '4532'; Name = 'Spedition'; NewAccount = '5720'; NewName = 'Tull- och speditionskostnader m.m.'; Box = $null } ) $accounts = @{} foreach ($a in @(Get-LedgerAccount -JournalPath $JournalPath)) { $accounts[[string]$a.AccountNumber] = $a.AccountName } $found = @($legacy | Where-Object { $accounts.ContainsKey($_.Account) -and $accounts[$_.Account].Trim() -eq $_.Name }) if (-not $found) { return } $existingRules = @(Read-LedgerVatMapping -JournalPath $JournalPath) $defaultRules = @(Get-VatDefaultRule) $balances = @{} if ($PSBoundParameters.ContainsKey('Date')) { $FiscalYear = Resolve-LedgerFiscalYear -FiscalYear $FiscalYear -JournalPath $JournalPath $yearDir = Join-Path $JournalPath $FiscalYear Assert-LedgerEntryDate -YearDir $yearDir -FiscalYear $FiscalYear -Date $Date $openingRows = Read-LedgerOpeningBalance -YearDir $yearDir foreach ($row in $openingRows) { $balances[[string]$row.Account] = [decimal]$balances[[string]$row.Account] + [decimal]$row.Amount } foreach ($entry in @(Get-LedgerEntry -JournalPath $JournalPath -FiscalYear $FiscalYear -ToDate $Date)) { foreach ($row in $entry.Rows) { $balances[[string]$row.Account] = [decimal]$balances[[string]$row.Account] + [decimal]$row.Amount } } } $newRules = [System.Collections.Generic.List[object]]::new() $newAccounts = [ordered]@{} $transferRows = [System.Collections.Generic.List[hashtable]]::new() $results = foreach ($item in $found) { if ($item.Ambiguous) { Write-Warning "Account $($item.Account) '$($item.Name)' was not converted: $($item.Ambiguous)" [PSCustomObject]@{ Account = $item.Account; Name = $item.Name; NewAccount = $null; Box = $null VatRule = $false; Transferred = [decimal]0; Status = 'Ambiguous' } continue } $number = [long]$item.Account $covered = @($existingRules | Where-Object { $number -ge $_.From -and $number -le $_.To }).Count -gt 0 $default = Resolve-VatBoxRule -Account $item.Account -Rules $defaultRules $defaultBox = if ($default) { $default.Box } else { $null } $needsRule = -not $covered -and $defaultBox -ne $item.Box if ($needsRule) { $newRules.Add([PSCustomObject]@{ From = $number; To = $number; Box = $item.Box }) } if (-not $accounts.ContainsKey($item.NewAccount)) { $newAccounts[$item.NewAccount] = $item.NewName } $amount = if ($balances.ContainsKey($item.Account)) { [decimal]$balances[$item.Account] } else { [decimal]0 } if ($amount -ne 0) { $transferRows.Add(@{ Account = $item.Account; Amount = -$amount }) $transferRows.Add(@{ Account = $item.NewAccount; Amount = $amount }) } [PSCustomObject]@{ Account = $item.Account; Name = $item.Name; NewAccount = $item.NewAccount Box = $item.Box; VatRule = $needsRule; Transferred = $amount; Status = 'Converted' } } $effectiveRules = @($existingRules) + @($newRules) + @($defaultRules) foreach ($item in $results | Where-Object Status -eq 'Converted') { $sourceRule = Resolve-VatBoxRule -Account $item.Account -Rules $effectiveRules $sourceBox = if ($sourceRule) { $sourceRule.Box } else { $null } $item.Box = $sourceBox if ($item.Transferred -ne 0) { $targetRule = Resolve-VatBoxRule -Account $item.NewAccount -Rules $effectiveRules $targetBox = if ($targetRule) { $targetRule.Box } else { $null } if ($sourceBox -ne $targetBox) { $sourceLabel = if ($null -eq $sourceBox) { '-' } else { "$sourceBox" } $targetLabel = if ($null -eq $targetBox) { '-' } else { "$targetBox" } throw "Cannot transfer account $($item.Account) (VAT box $sourceLabel) to $($item.NewAccount) (VAT box $targetLabel): conflicting VAT rules. Resolve the conflict in vat.txt or omit -Date." } } } if ($transferRows.Count -gt 0) { Assert-LedgerEntryAmounts -Rows $transferRows.ToArray() } if ($PSCmdlet.ShouldProcess($JournalPath, 'Convert legacy template accounts to BAS')) { foreach ($number in $newAccounts.Keys) { Add-LedgerAccount -JournalPath $JournalPath -AccountNumber $number -AccountName $newAccounts[$number] -Confirm:$false } if ($newRules.Count -gt 0) { Save-LedgerVatMapping -JournalPath $JournalPath -Rules (@($existingRules) + @($newRules)) } if ($transferRows.Count -gt 0) { Add-LedgerEntry -JournalPath $JournalPath -FiscalYear $FiscalYear -Date $Date ` -Description 'Omföring till BAS-konton' -Rows $transferRows.ToArray() -Confirm:$false } } $results } finally { Exit-LedgerJournalLock -Lock $journalLock } } } |