Public/Close-LedgerFiscalYear.ps1

<#
.SYNOPSIS
Closes a fiscal year, booking the net result and preventing further entries.

.DESCRIPTION
Performs the year-end closing of a fiscal year. Unless -SkipResultEntry is used,
the net result for the year (the sum of the profit and loss accounts, classes
3-8) is booked as a normal verification transferring it to equity: debit the
result account (8999 Årets resultat by default) and credit the equity account for
a profit, and the reverse for a loss. The equity account defaults to the one that
matches the journal's CompanyType (2099 for AB, 2019 for EF) and can be overridden
with -EquityAccount.

Booking the result as a real verification means it appears in the ledger and in
SIE exports, and it consumes a verification number. When there is no result to
book (the profit and loss accounts net to zero) no verification is created and the
year is simply flagged as closed.

After any result entry has been written the Status field in year.txt is set to
'Closed'. Once closed, Add-LedgerEntry refuses to create new verifications in that
fiscal year.

Finally every verification and attachment in the year that is not yet in the
integrity chain is sealed (see Protect-LedgerFiscalYear), and the command returns
an object with FiscalYear, ResultVerification (the result entry's number, or
empty) and ChainHash. Save the ChainHash outside the journal, for example in the
annual report archive, and check it later with
Test-LedgerIntegrity -ExpectedHash.

.PARAMETER JournalPath
The path to an existing journal directory.

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

.PARAMETER EquityAccount
The equity account the net result is transferred to. When omitted it is derived
from the journal's CompanyType (AB -> 2099, EF -> 2019). Required for other
company forms (HB, KB) or journals without a CompanyType when there is a result to
book.

.PARAMETER ResultAccount
The profit and loss closing account debited/credited against equity. Defaults to
'8999' (Årets resultat).

.PARAMETER SkipResultEntry
Closes the year by only setting its status, without booking the net result. Use
this to keep the legacy behaviour where the result is carried forward
synthetically by Copy-LedgerOpeningBalance instead of being booked.

.EXAMPLE
Close-LedgerFiscalYear -JournalPath .\MinFirma.ledger -FiscalYear '2024-01_2024-12'

Books the 2024 net result to equity (using the journal's CompanyType to pick the
account), closes the year so no more entries can be added and returns the chain
hash to keep.

.EXAMPLE
Close-LedgerFiscalYear -JournalPath .\Enskild.ledger -FiscalYear '2024-01_2024-12' -EquityAccount '2019'

Closes an enskild firma year, transferring the result to account 2019 (Årets
resultat, eget kapital).

.EXAMPLE
Get-LedgerFiscalYear -JournalPath .\MinFirma.ledger |
    Where-Object { $_.Status -eq 'Open' } |
    ForEach-Object { Close-LedgerFiscalYear -JournalPath .\MinFirma.ledger -FiscalYear $_.Name -SkipResultEntry }

Closes all open fiscal years without booking a result verification.
#>

function Close-LedgerFiscalYear {
    [OutputType([pscustomobject])]
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter()]
        [string]$JournalPath,

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

        [Parameter()]
        [string]$EquityAccount,

        [Parameter()]
        [string]$ResultAccount = '8999',

        [Parameter()]
        [switch]$SkipResultEntry
    )
    process {
        $JournalPath = Resolve-LedgerJournalPath -JournalPath $JournalPath -SchemaCheck Write
        $FiscalYear = Resolve-LedgerFiscalYear -FiscalYear $FiscalYear -JournalPath $JournalPath

        $YearDir = Join-Path $JournalPath $FiscalYear
        if (-not (Test-Path $YearDir -PathType Container)) {
            throw "Fiscal year not found: $FiscalYear"
        }

        $YearFile = Join-Path $YearDir 'year.txt'
        if (-not (Test-Path $YearFile)) {
            throw "Invalid fiscal year - year.txt not found in: $FiscalYear"
        }

        $Lines = Get-Content $YearFile

        # Check current status and capture the year's end date for the result entry.
        $EndDate = $null
        $resultEntry = $null
        foreach ($Line in $Lines) {
            if ($Line -match '^Status:\s*Closed') {
                throw "Fiscal year $FiscalYear is already closed."
            }
            elseif ($Line -match '^EndDate:\s*(.+)$') {
                $EndDate = [datetime]$Matches[1]
            }
        }

        if (-not $PSCmdlet.ShouldProcess($FiscalYear, 'Close fiscal year and book net result')) {
            return
        }

        # Book the net result to equity unless explicitly skipped. When the profit
        # and loss accounts net to zero there is nothing to book, so the year is
        # simply flagged as closed (this also keeps legacy no-result years working).
        if (-not $SkipResultEntry) {
            $Balance = Get-LedgerBalance -JournalPath $JournalPath -FiscalYear $FiscalYear
            $ResultRows = @($Balance | Where-Object { $_.AccountNumber -match '^[3-8]' })
            $RawResult = if ($ResultRows) {
                ($ResultRows | Measure-Object -Property Balance -Sum).Sum
            }
            else {
                [decimal]0
            }
            $RawResult = [Math]::Round([decimal]$RawResult, 2)

            if ($RawResult -ne 0) {
                $Equity = if ($EquityAccount) {
                    $EquityAccount
                }
                else {
                    Resolve-LedgerEquityAccount -JournalPath $JournalPath
                }

                if (-not $EndDate) {
                    throw "Cannot determine the result date - EndDate missing from year.txt in $FiscalYear."
                }

                # Debit the result account and credit equity for a profit; the raw
                # balance of the P&L accounts is negative for a profit, so negating
                # it gives the debit amount on the result account.
                $Rows = @(
                    @{ Account = $ResultAccount; Amount = -$RawResult }
                    @{ Account = $Equity; Amount = $RawResult }
                )

                $resultEntry = Add-LedgerEntry -JournalPath $JournalPath -FiscalYear $FiscalYear `
                    -Date $EndDate -Description 'Årets resultat (bokslut)' -Rows $Rows -PassThru
            }
        }

        # Rewrite year.txt with updated status.
        $NewLines = foreach ($Line in $Lines) {
            if ($Line -match '^Status:\s*') {
                'Status: Closed'
            }
            else {
                $Line
            }
        }

        Set-LedgerFileContent -Path $YearFile -Value $NewLines

        [void](Protect-LedgerYearContent -YearDir $YearDir)
        $integrity = Test-LedgerIntegrity -JournalPath $JournalPath -FiscalYear $FiscalYear
        if (-not $integrity.IsValid) {
            Write-Warning "Fiscal year $FiscalYear has integrity issues: $(@($integrity.Issues | ForEach-Object Detail) -join ' ')"
        }

        [PSCustomObject]@{
            FiscalYear         = $FiscalYear
            ResultVerification = if ($resultEntry) { $resultEntry.VerificationNumber } else { $null }
            ChainHash          = $integrity.ChainHash
        }
    }
}