Public/Export-LedgerIncomeTaxReturn.ps1
|
<# .SYNOPSIS Exports a fiscal year as a Skatteverket SRU income tax return (INK2) for an aktiebolag. .DESCRIPTION Writes the two files Skatteverket's filöverföringstjänst expects — INFO.SRU (submitter metadata) and BLANKETTER.SRU (the declaration blocks) — into a destination directory. The submission contains three blankett blocks: INK2R Räkenskapsschema — balance sheet and income statement, derived automatically from the trial balance via the official BAS-to-SRU mapping. INK2 Huvudblankett — the fiscal year dates and the surplus/deficit of business activity (överskott/underskott av näringsverksamhet). INK2S Skattemässiga justeringar — årets resultat, the non-deductible booked income tax and the tax adjustments, ending in the surplus/deficit (4.15/4.16). Field codes, rows and printed signs follow Skatteverket's fältnamnstabeller for INK2/INK2R/INK2S and the account ranges follow BAS-kontogruppen's official kopplingstabell. Amounts are written as they appear on the paper form: the row's printed sign carries the direction, so costs, deductions and a loss are positive. A negative amount only occurs where the value goes against the printed sign, e.g. a booked tax income on 3.25 and 4.3a. Income statement rows split into a (+) and a (-) field (3.2, 3.12-3.15, 3.23, 3.24) get the field matching the row's net amount. Amounts are reported in whole kronor (öre truncated per SFL 22:1), the organisation number is written in the 12-digit form and the files use ISO-8859-1 encoding, all as the format requires. Årets resultat (3.26/3.27, and 4.1/4.2 on INK2S) is the signed sum of the truncated income statement rows, and fritt eget kapital (2.28) is the balancing figure of the truncated balance sheet, so both statements tie out to the krona. Fritt eget kapital includes årets resultat whether or not the closing entry (8999/2099) has been booked. A debit balance on a skatteskulder account (25xx) is reported as a receivable (2.21) and a credit balance on the skattekonto (163x) as a skatteskuld (2.49). The surplus/deficit (INK2S 4.15/4.16 and INK2 1.1/1.2) is årets resultat plus the booked income tax (4.3a) plus every INK2S adjustment with its printed sign; the information-only rows 4.17-4.22 do not count. Adjustments that follow from the BAS account itself are derived automatically: 4.3c (7653) non-deductible costs: 6072, 6982, 6992, 7622, 7632 and 8423 (ränta på skattekontot) 4.5c (7754) non-taxable income: 8314 skattefria ränteintäkter 4.3b (7652) net write-downs of shares in other companies (8270-8289), assumed non-deductible; a net reversal goes to 4.5c (7754) The write-down rule assumes kapitalplaceringsaktier and emits a warning whenever it applies. Supply -TaxAdjustment to add further INK2S fields or to replace a derived amount (an entry for the same field wins; give 0 to remove it), or -NoAutomaticAdjustment to derive nothing. An explicit 4.15/4.16 overrides the computed surplus on both INK2S and the INK2 huvudblankett. The returned object's Fields property lists every written field with its row on the form (Box), SRU code, amount and description, so the values can be copied straight onto the paper or e-service form. Run this on a fiscal year either before or after the result has been appropriated into equity (8999/2099); the export is the same in both cases. .PARAMETER JournalPath The path to an existing journal directory. If omitted, uses the current journal set via Set-LedgerCurrentJournal. .PARAMETER FiscalYear The fiscal year identifier (e.g. '2024-01_2024-12'). If omitted, uses the current fiscal year set via Set-LedgerCurrentFiscalYear. .PARAMETER Path Destination directory for the INFO.SRU and BLANKETTER.SRU files. Created if it does not exist. .PARAMETER PostalCode The submitter's postal code (postnummer). Required by INFO.SRU; falls back to the journal metadata field 'PostalCode', and then to the postal code in the metadata field 'Address' (e.g. 'Storgatan 1, 111 22 Stockholm, Sweden'), whose street part is also written to INFO.SRU as #ADRESS. .PARAMETER City The submitter's city (postort). Required by INFO.SRU; falls back to the journal metadata field 'City', and then to the city in the metadata field 'Address'. .PARAMETER ContactPerson Optional contact person written to INFO.SRU; falls back to the journal metadata field 'ContactPerson'. .PARAMETER Email Optional contact e-mail written to INFO.SRU; falls back to the journal metadata field 'Email'. .PARAMETER TaxAdjustment Optional hashtable of additional INK2S fields to whole-krona amount. Each key is either the row on the form, e.g. '4.3b' or '4.6a', or its SRU code, e.g. '7652'. Rows split into a (+) and a (-) field need the suffix: '4.13+' or '4.13-'. Example: @{ '4.6a' = 1200; '4.5c' = 395 }. Each entry replaces any automatically derived amount for the same field (use 0 to remove a derived field). Give amounts as they appear on the form (positive); fields that Skatteverket only accepts as positive throw on a negative amount. Årets resultat (4.1/4.2) always comes from the books and cannot be supplied. .PARAMETER NoAutomaticAdjustment Do not derive any INK2S adjustments from the trial balance; only årets resultat, the booked tax (4.3a) and -TaxAdjustment are used. .PARAMETER ConsultantAssisted Answers the INK2S question whether an uppdragstagare (e.g. a redovisningskonsult) has assisted in preparing the annual report: $true writes Ja (SRU 8040), $false writes Nej (SRU 8041). Omit to leave the question unanswered. .PARAMETER Audited Answers the INK2S question whether the annual report has been audited (varit föremål för revision): $true writes Ja (SRU 8044), $false writes Nej (SRU 8045). Omit to leave the question unanswered. .PARAMETER Force Overwrite existing INFO.SRU / BLANKETTER.SRU files in the destination directory. .EXAMPLE Export-LedgerIncomeTaxReturn -JournalPath .\MinFirma.ledger -FiscalYear '2024-01_2024-12' -Path .\sru -PostalCode '11122' -City 'Stockholm' Writes .\sru\INFO.SRU and .\sru\BLANKETTER.SRU for the 2024 income year. .EXAMPLE Export-LedgerIncomeTaxReturn -JournalPath .\Konsult.ledger -FiscalYear '2024-01_2024-12' -Path C:\Deklaration -TaxAdjustment @{ '4.6a' = 940 } -Force Exports the return, adding a schablonintäkt på periodiseringsfonder (INK2S 4.6a, SRU 7654) of 940 kr to the tax adjustments and overwriting any existing files. .EXAMPLE Export-LedgerIncomeTaxReturn -JournalPath .\AktiehandelNord.ledger -FiscalYear '2024-01_2024-12' -Path .\sru -TaxAdjustment @{ '4.3b' = 0 } Aktiehandel Nord AB trades in shares, so the write-down booked on 8271 concerns lageraktier and is deductible; the automatically derived 4.3b add-back (SRU 7652) is removed. .EXAMPLE (Export-LedgerIncomeTaxReturn -JournalPath .\MinFirma.ledger -FiscalYear '2025-09_2026-08' -Path .\sru -Force).Fields | Format-Table Exports the return and lists every field with its row on the form (e.g. 3.7, 4.3a, 4.15), SRU code and amount, ready to copy onto the declaration. .EXAMPLE Export-LedgerIncomeTaxReturn -JournalPath .\Grönlund.ledger -FiscalYear '2025-09_2026-08' -Path .\sru -ConsultantAssisted $false -Audited $false Grönlund Konsult AB prepared its annual report itself and has no auditor, so Nej is answered to both questions at the bottom of INK2S (SRU 8041 and 8045). #> function Export-LedgerIncomeTaxReturn { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter()] [string]$JournalPath, [Parameter(ValueFromPipelineByPropertyName)] [Alias('Name')] [string]$FiscalYear, [Parameter(Mandatory)] [string]$Path, [Parameter()] [string]$PostalCode, [Parameter()] [string]$City, [Parameter()] [string]$ContactPerson, [Parameter()] [string]$Email, [Parameter()] [hashtable]$TaxAdjustment, [switch]$NoAutomaticAdjustment, [Parameter()] [Nullable[bool]]$ConsultantAssisted, [Parameter()] [Nullable[bool]]$Audited, [switch]$Force ) process { $JournalPath = Resolve-LedgerJournalPath -JournalPath $JournalPath $FiscalYear = Resolve-LedgerFiscalYear -FiscalYear $FiscalYear -JournalPath $JournalPath $journal = Get-LedgerJournal -Path $JournalPath if (-not $journal.OrgNumber) { throw "Journal has no OrgNumber; set one with Set-LedgerJournal before exporting an SRU return." } $orgNr = ConvertTo-SruOrgNr -OrgNumber $journal.OrgNumber $year = Get-LedgerFiscalYear -JournalPath $JournalPath | Where-Object { $_.Name -eq $FiscalYear } if (-not $year) { throw "Fiscal year year.txt missing for $FiscalYear" } $startDate = [datetime]$year.StartDate $endDate = [datetime]$year.EndDate # Submitter metadata: parameters win, otherwise fall back to free-form # journal metadata. Postal code and city are mandatory in INFO.SRU; # when 'PostalCode'/'City' are not set they are parsed from 'Address' # (e.g. 'Storgatan 1, 111 22 Stockholm, Sweden'), whose street part is # also written as the optional #ADRESS. $street = $null $address = [string]$journal.Metadata['Address'] if ($address) { $segments = @($address -split ',' | ForEach-Object Trim | Where-Object { $_ }) for ($i = 0; $i -lt $segments.Count; $i++) { if ($segments[$i] -match '^(?:SE-?\s?)?(\d{3}\s?\d{2})\s+(.+)$') { if (-not $PostalCode -and -not $journal.Metadata['PostalCode']) { $PostalCode = $Matches[1] } if (-not $City -and -not $journal.Metadata['City']) { $City = $Matches[2] } if ($i -gt 0) { $street = $segments[0..($i - 1)] -join ', ' } break } } } if (-not $PostalCode) { $PostalCode = [string]$journal.Metadata['PostalCode'] } if (-not $City) { $City = [string]$journal.Metadata['City'] } if (-not $ContactPerson) { $ContactPerson = [string]$journal.Metadata['ContactPerson'] } if (-not $Email) { $Email = [string]$journal.Metadata['Email'] } if (-not $PostalCode -or -not $City) { throw "INFO.SRU requires a postal code and city. Supply -PostalCode and -City, or set 'PostalCode' and 'City' (or an 'Address' such as 'Storgatan 1, 111 22 Stockholm') in the journal metadata." } # Whole kronor, öre truncated toward zero. function Format-SruAmount { param([decimal]$Value) [long][Math]::Truncate($Value) } $fieldDefs = @(Get-SruFieldDefinitions) $fieldByCode = @{} foreach ($f in $fieldDefs) { $fieldByCode[$f.Code] = $f } $rowOf = { param($code) $fieldByCode[[int]$code].Box.TrimEnd('+', '-') } # Aggregate the trial balance: balance sheet accounts per SRU code, # income statement accounts per form row (income positive). $rules = @(Get-SruAccountRules) $balance = @(Get-LedgerBalance -JournalPath $JournalPath -FiscalYear $FiscalYear) $sru = @{} $addSru = { param($code, $amount) $key = [int]$code if (-not $sru.ContainsKey($key)) { $sru[$key] = [decimal]0 } $sru[$key] += [decimal]$amount } $incomeRows = [ordered]@{} $bookedTax = [decimal]0 # debit balance of income-tax accounts (8900-8989) foreach ($row in $balance) { $acct = 0 if (-not [int]::TryParse($row.AccountNumber, [ref]$acct)) { continue } $bal = [decimal]$row.Balance if ($acct -ge 8900 -and $acct -le 8989) { $bookedTax += $bal } $rule = Resolve-SruAccountRule -Account $acct -Rules $rules if (-not $rule) { continue } switch ($rule.Kind) { 'Asset' { # debit-positive as-is; a credit balance may move to AltSru if ($rule.AltSru -and $bal -lt 0) { & $addSru $rule.AltSru (-$bal) } else { & $addSru $rule.Sru $bal } } 'Debt' { # credit -> positive; a debit balance may move to AltSru if ($rule.AltSru -and $bal -gt 0) { & $addSru $rule.AltSru $bal } else { & $addSru $rule.Sru (-$bal) } } 'Income' { $rowKey = & $rowOf $(if ($rule.Sru) { $rule.Sru } else { $rule.MinusSru }) if (-not $incomeRows.Contains($rowKey)) { $incomeRows[$rowKey] = [PSCustomObject]@{ Plus = $null; Minus = $null; Net = [decimal]0 } } $line = $incomeRows[$rowKey] if ($rule.Sru -and -not $line.Plus) { $line.Plus = $rule.Sru } if ($rule.MinusSru -and -not $line.Minus) { $line.Minus = $rule.MinusSru } $line.Net += (-$bal) } } } # Each income statement row is reported as it appears on the form: # the net goes to the row's (+) field when positive and to its (-) # field as a positive amount when negative. A row with a single field # reports the net against that field's printed sign. foreach ($line in $incomeRows.Values) { $net = [decimal](Format-SruAmount -Value $line.Net) if ($net -eq 0) { continue } if ($line.Plus -and ($net -gt 0 -or -not $line.Minus)) { & $addSru $line.Plus $net } else { & $addSru $line.Minus (-$net) } } # Report whole kronor per field, and derive the totals from the # truncated lines so the form ties out exactly: årets resultat is the # signed sum of the income statement rows, and fritt eget kapital (7302) # is the balancing figure of the balance sheet. That includes årets # resultat whether or not the closing entry 8999/2099 has been booked. $codes = @($sru.Keys) foreach ($code in $codes) { $sru[$code] = [decimal](Format-SruAmount -Value $sru[$code]) } $netResult = [decimal]0 $assetTotal = [decimal]0 $otherEquityLiab = [decimal]0 foreach ($code in $codes) { if ($code -ge 7400) { if ($fieldByCode[$code].Sign -eq '-') { $netResult -= $sru[$code] } else { $netResult += $sru[$code] } } elseif ($code -lt 7300) { $assetTotal += $sru[$code] } elseif ($code -ne 7302) { $otherEquityLiab += $sru[$code] } } $freeEquity = $assetTotal - $otherEquityLiab if ($freeEquity -ne 0) { $sru[7302] = $freeEquity } else { $sru.Remove(7302) } # Årets resultat is also the closing line of the INK2R income statement # (3.26 vinst / 3.27 förlust), both reported as positive amounts. if ($netResult -gt 0) { & $addSru 7450 $netResult } elseif ($netResult -lt 0) { & $addSru 7550 (-$netResult) } # The booked income tax (non-deductible) is added back on INK2S 4.3a; # a booked tax income gives a negative 4.3a. $tax = [decimal](Format-SruAmount -Value $bookedTax) # The caller's -TaxAdjustment keys may be a row on the form ('4.3b') # or an SRU code ('7652'). $explicit = @{} if ($TaxAdjustment) { foreach ($k in $TaxAdjustment.Keys) { $field = Resolve-SruInk2sField -Key ([string]$k) -Fields $fieldDefs $explicit[[int]$field.Code] = [decimal]$TaxAdjustment[$k] } } # INK2S adjustments derived from the trial balance (unless disabled), # then the caller's entries, which replace the derived amount for the # same field. $userAdjustments = @{} if (-not $NoAutomaticAdjustment) { $adjRules = @(Get-SruTaxAdjustmentRules) $ruleNet = @{} foreach ($row in $balance) { $acct = 0 if (-not [int]::TryParse($row.AccountNumber, [ref]$acct)) { continue } foreach ($adjRule in $adjRules) { if ($acct -ge $adjRule.Min -and $acct -le $adjRule.Max) { if (-not $ruleNet.ContainsKey($adjRule)) { $ruleNet[$adjRule] = [decimal]0 } $ruleNet[$adjRule] += [decimal]$row.Balance break } } } foreach ($adjRule in $adjRules) { if (-not $ruleNet.ContainsKey($adjRule)) { continue } $net = $ruleNet[$adjRule] $code = if ($net -gt 0) { $adjRule.DebitSru } elseif ($net -lt 0) { $adjRule.CreditSru } else { $null } if (-not $code) { continue } $amount = [decimal](Format-SruAmount -Value ([Math]::Abs($net))) if ($amount -eq 0) { continue } if ($adjRule.Assumption -and -not $explicit.ContainsKey([int]$code)) { $box = $fieldByCode[[int]$code].Box $what = if ($net -gt 0) { "write-downs of $amount kr treated as non-deductible (INK2S $box, SRU $code)" } else { "reversals of $amount kr treated as non-taxable (INK2S $box, SRU $code)" } Write-Warning ("Accounts $($adjRule.Min)-$($adjRule.Max) ($($adjRule.Label)): $what. " + 'This assumes the shares are kapitalplaceringsaktier. For lageraktier or other deductible ' + "write-downs, override with -TaxAdjustment @{ '$box' = <amount> } or use -NoAutomaticAdjustment.") } if (-not $userAdjustments.ContainsKey([int]$code)) { $userAdjustments[[int]$code] = [decimal]0 } $userAdjustments[[int]$code] += $amount } } foreach ($code in $explicit.Keys) { if ($code -eq 7651) { $tax = $explicit[$code] } else { $userAdjustments[$code] = $explicit[$code] } } # INK2S surplus/deficit: result + booked tax (4.3a) + every adjustment # with its printed sign. Information-only fields (4.17-4.22) do not # count. An explicit 4.15/4.16 (7670/7770) overrides the computation. $surplus = $netResult + $tax foreach ($code in $userAdjustments.Keys) { if ($code -in 7670, 7770) { continue } switch ($fieldByCode[$code].Sign) { '+' { $surplus += $userAdjustments[$code] } '-' { $surplus -= $userAdjustments[$code] } } } if ($userAdjustments.ContainsKey(7670) -or $userAdjustments.ContainsKey(7770)) { $surplus = [decimal]0 if ($userAdjustments.ContainsKey(7670)) { $surplus += $userAdjustments[7670] } if ($userAdjustments.ContainsKey(7770)) { $surplus -= $userAdjustments[7770] } } # Assemble every field per blankett, in form order. $ink2 = @{} if ($surplus -gt 0) { $ink2[7104] = $surplus } elseif ($surplus -lt 0) { $ink2[7114] = -$surplus } $ink2s = @{} if ($netResult -gt 0) { $ink2s[7650] = $netResult } elseif ($netResult -lt 0) { $ink2s[7750] = -$netResult } $ink2s[7651] = $tax foreach ($code in $userAdjustments.Keys) { if ($code -notin 7670, 7770) { $ink2s[$code] = $userAdjustments[$code] } } if ($surplus -gt 0) { $ink2s[7670] = $surplus } elseif ($surplus -lt 0) { $ink2s[7770] = -$surplus } $formValues = @{ INK2 = $ink2; INK2R = $sru; INK2S = $ink2s } $fields = foreach ($def in $fieldDefs) { $values = $formValues[$def.Form] if (-not $values.ContainsKey($def.Code)) { continue } $amount = [long](Format-SruAmount -Value $values[$def.Code]) if ($amount -eq 0) { continue } if ($def.NonNegative -and $amount -lt 0) { throw "$($def.Form) $($def.Box) (SRU $($def.Code), $($def.Description)) cannot be negative ($amount); Skatteverket only accepts positive amounts in this field." } [PSCustomObject]@{ Form = $def.Form Box = $def.Box SruCode = $def.Code Amount = $amount Description = $def.Description } } $fields = @($fields) # Ja/Nej questions at the bottom of INK2S, reported as the value 'X' # in the field for the chosen answer (data type Str_X). $question = 'Uppdragstagare (t.ex. redovisningskonsult) har biträtt vid upprättandet av årsredovisningen' if ($null -ne $ConsultantAssisted) { $fields += [PSCustomObject]@{ Form = 'INK2S'; Box = ''; SruCode = $(if ($ConsultantAssisted) { 8040 } else { 8041 }); Amount = 'X' Description = "${question}: $(if ($ConsultantAssisted) { 'Ja' } else { 'Nej' })" } } if ($null -ne $Audited) { $fields += [PSCustomObject]@{ Form = 'INK2S'; Box = ''; SruCode = $(if ($Audited) { 8044 } else { 8045 }); Amount = 'X' Description = "Årsredovisningen har varit föremål för revision: $(if ($Audited) { 'Ja' } else { 'Nej' })" } } $stamp = Get-Date $genDate = $stamp.ToString('yyyyMMdd') $genTime = $stamp.ToString('HHmmss') $startStr = $startDate.ToString('yyyyMMdd') $endStr = $endDate.ToString('yyyyMMdd') $incomeYear = $endDate.Year $period = "$incomeYear$(Get-SruPeriodSuffix -EndMonth $endDate.Month)" $moduleVersion = (Get-Module PSLedger).Version.ToString() $name = $journal.Name $nl = "`r`n" # --- INFO.SRU -------------------------------------------------------- $info = New-Object System.Text.StringBuilder $addInfo = { param($line) [void]$info.Append($line); [void]$info.Append($nl) } & $addInfo '#DATABESKRIVNING_START' & $addInfo '#PRODUKT SRU' & $addInfo "#SKAPAD $genDate $genTime" & $addInfo "#PROGRAM PSLedger $moduleVersion" & $addInfo '#FILNAMN BLANKETTER.SRU' & $addInfo '#DATABESKRIVNING_SLUT' & $addInfo '#MEDIELEV_START' & $addInfo "#ORGNR $orgNr" & $addInfo "#NAMN $name" if ($street) { & $addInfo "#ADRESS $street" } & $addInfo "#POSTNR $($PostalCode -replace '\s', '')" & $addInfo "#POSTORT $City" if ($ContactPerson) { & $addInfo "#KONTAKT $ContactPerson" } if ($Email) { & $addInfo "#EMAIL $Email" } & $addInfo '#MEDIELEV_SLUT' # --- BLANKETTER.SRU -------------------------------------------------- $blk = New-Object System.Text.StringBuilder $addBlk = { param($line) [void]$blk.Append($line); [void]$blk.Append($nl) } $addUppgift = { param($code, $value) $v = Format-SruAmount -Value $value if ($v -ne 0) { & $addBlk "#UPPGIFT $code $v" } } $openBlock = { param($blankettType) & $addBlk "#BLANKETT $blankettType" & $addBlk "#IDENTITET $orgNr $genDate $genTime" & $addBlk "#NAMN $name" & $addBlk "#UPPGIFT 7011 $startStr" & $addBlk "#UPPGIFT 7012 $endStr" } foreach ($form in 'INK2', 'INK2R', 'INK2S') { & $openBlock "$form-$period" foreach ($f in ($fields | Where-Object Form -eq $form | Sort-Object SruCode)) { if ($f.Amount -is [string]) { & $addBlk "#UPPGIFT $($f.SruCode) $($f.Amount)" } else { & $addUppgift $f.SruCode $f.Amount } } & $addBlk '#BLANKETTSLUT' } & $addBlk '#FIL_SLUT' # --- Write both files ------------------------------------------------ $DestDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) $infoPath = Join-Path $DestDir 'INFO.SRU' $blkPath = Join-Path $DestDir 'BLANKETTER.SRU' foreach ($p in $infoPath, $blkPath) { if ((Test-Path -LiteralPath $p) -and -not $Force) { throw "Destination file already exists: $p. Use -Force to overwrite." } } if (-not $PSCmdlet.ShouldProcess($DestDir, "Export SRU income tax return for $FiscalYear")) { return } if (-not (Test-Path -LiteralPath $DestDir)) { New-Item -ItemType Directory -Path $DestDir -Force | Out-Null } $encoding = [System.Text.Encoding]::GetEncoding('ISO-8859-1') [System.IO.File]::WriteAllText($infoPath, $info.ToString(), $encoding) [System.IO.File]::WriteAllText($blkPath, $blk.ToString(), $encoding) [PSCustomObject]@{ InfoPath = $infoPath BlanketterPath = $blkPath OrgNumber = $orgNr Period = $period NetResult = [long][Math]::Truncate($netResult) SurplusDeficit = [long][Math]::Truncate($surplus) TaxAdjustments = $userAdjustments Fields = $fields } } } |