Public/Set-LedgerHolding.ps1
|
<# .SYNOPSIS Records or updates a securities holding (innehav) for a fiscal year. .DESCRIPTION Stores a holding at the balance date in the fiscal year's holdings.txt (UTF-8, tab-separated). A holding is identified by Account and Name: if a holding with the same Account and Name exists it is updated, otherwise a new one is added. When updating, only the supplied fields are changed; pass an empty string to clear Isin or Source, and $null to clear PriceDate, BookValue or Cost. The market value in SEK is computed as Quantity * Price * FxRate. It is used by Get-LedgerShareholdingNote and the annual report instead of the single SecuritiesMarketValue recorded with Set-LedgerReportInput, and by Test-LedgerFiscalYear to flag holdings whose market value is below book value. Holdings cannot be changed in a closed fiscal year. .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-09_2025-08'). If omitted, uses the current fiscal year set via Set-LedgerCurrentFiscalYear. .PARAMETER Account The account the holding is booked on, e.g. 1350 (long-term) or 1810 (short-term). .PARAMETER Name The name of the holding, e.g. 'Investor B'. Together with Account it identifies the holding. .PARAMETER Isin Optional ISIN (International Securities Identification Number), e.g. 'SE0015811963'. The format and check digit are validated. .PARAMETER Quantity Number of shares or fund units held at the balance date. Required for a new holding. .PARAMETER Price Price per unit at the balance date, in Currency. Required for a new holding. .PARAMETER Currency ISO currency code of Price. Defaults to SEK. .PARAMETER FxRate Exchange rate in SEK per one unit of Currency at the balance date. Required when Currency is not SEK; must be 1 (or omitted) for SEK. .PARAMETER PriceDate The date of the price (normally the balance date), e.g. '2025-08-29'. Stored as yyyy-MM-dd. .PARAMETER Source Where the price was taken from, e.g. 'Nasdaq Stockholm' or 'Avanza årsbesked'. .PARAMETER BookValue Optional book value (redovisat värde) of this holding in SEK: acquisition cost less any write-downs. Enables a per-holding impairment comparison when several holdings share an account. .PARAMETER Cost Optional acquisition cost (anskaffningsvärde) of this holding in SEK. Together with BookValue it shows how much of an earlier write-down may be reversed (återföring) when the market value recovers; a reversal never takes the book value above the cost. .PARAMETER PassThru If specified, returns the created/updated holding. By default the command produces no output. .EXAMPLE Set-LedgerHolding -JournalPath .\HEG.ledger -FiscalYear '2024-09_2025-08' ` -Account 1350 -Name 'Investor B' -Quantity 500 -Price 265.40 Records 500 Investor B shares at 265.40 SEK. .EXAMPLE Set-LedgerHolding -JournalPath .\HEG.ledger -FiscalYear '2024-09_2025-08' ` -Account 1350 -Name 'Vanguard FTSE All-World' -Isin 'IE00BK5BQT80' ` -Quantity 120 -Price 118.20 -Currency USD -FxRate 9.5312 ` -PriceDate '2025-08-29' -Source 'Avanza årsbesked' -BookValue 110000 -Cost 110000 Records a USD-denominated fund with ISIN, price source, book value and acquisition cost. #> function Set-LedgerHolding { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter()] [string]$JournalPath, [Parameter()] [string]$FiscalYear, [Parameter(Mandatory)] [ValidatePattern('^\d+$')] [string]$Account, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Name, [Parameter()] [AllowEmptyString()] [string]$Isin, [Parameter()] [ValidateRange(0, [double]::MaxValue)] [decimal]$Quantity, [Parameter()] [ValidateRange(0, [double]::MaxValue)] [decimal]$Price, [Parameter()] [ValidatePattern('^[A-Za-z]{3}$')] [string]$Currency, [Parameter()] [decimal]$FxRate, [Parameter()] [AllowNull()] [Nullable[datetime]]$PriceDate, [Parameter()] [AllowEmptyString()] [string]$Source, [Parameter()] [AllowNull()] [Nullable[decimal]]$BookValue, [Parameter()] [AllowNull()] [Nullable[decimal]]$Cost, [Parameter()] [switch]$PassThru ) 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" } Assert-LedgerFiscalYearOpen -YearDir $YearDir -FiscalYear $FiscalYear -Action 'change holdings' if ($Name -match "[`t`r`n]") { throw 'Name must not contain tabs or line breaks.' } if ($Source -match "[`t`r`n]") { throw 'Source must not contain tabs or line breaks.' } if ($PSBoundParameters.ContainsKey('Isin') -and $Isin -and -not (Test-LedgerIsin -Isin $Isin)) { throw "Invalid ISIN '$Isin'. Expected 2 letters, 9 alphanumerics and a valid check digit (e.g. SE0015811963)." } $existing = Read-LedgerHoldings -YearDir $YearDir $current = $existing | Where-Object { $_.Account -eq $Account -and $_.Name -eq $Name } | Select-Object -First 1 if (-not $current) { foreach ($required in 'Quantity', 'Price') { if (-not $PSBoundParameters.ContainsKey($required)) { throw "-$required is required when adding a new holding." } } } $bound = @{} + $PSBoundParameters if ($bound.ContainsKey('PriceDate') -and $null -ne $PriceDate) { $bound['PriceDate'] = $PriceDate.ToString('yyyy-MM-dd', [cultureinfo]::InvariantCulture) } $pick = { param($Key, $Fallback) if ($bound.ContainsKey($Key)) { $bound[$Key] } else { $Fallback } } $newCurrency = & $pick 'Currency' $current.Currency $newFxRate = & $pick 'FxRate' $current.FxRate # Switching to SEK resets an inherited foreign exchange rate. if ($PSBoundParameters.ContainsKey('Currency') -and -not $PSBoundParameters.ContainsKey('FxRate') -and $newCurrency.ToUpperInvariant() -ne "$($current.Currency)") { $newFxRate = $null } $record = New-LedgerHoldingRecord -Account $Account -Name $Name ` -Isin (& $pick 'Isin' $current.Isin) ` -Quantity (& $pick 'Quantity' $current.Quantity) ` -Price (& $pick 'Price' $current.Price) ` -Currency $newCurrency -FxRate $newFxRate ` -PriceDate (& $pick 'PriceDate' $current.PriceDate) ` -Source (& $pick 'Source' $current.Source) ` -BookValue (& $pick 'BookValue' $current.BookValue) ` -Cost (& $pick 'Cost' $current.Cost) if ($null -ne $record.BookValue -and $null -ne $record.Cost -and $record.BookValue -gt $record.Cost) { Write-Warning "Holding '$Name': book value $($record.BookValue) exceeds the acquisition cost $($record.Cost)." } if ($record.Isin) { $dup = $existing | Where-Object { $_.Isin -eq $record.Isin -and -not ($_.Account -eq $Account -and $_.Name -eq $Name) } if ($dup) { Write-Warning "ISIN $($record.Isin) is already used by holding '$(@($dup)[0].Name)' on account $(@($dup)[0].Account)." } } if (-not (Get-LedgerAccount -JournalPath $JournalPath -AccountNumber $Account)) { Write-Warning "Account $Account is not in the chart of accounts." } if ((Get-LedgerHoldingValuationRule $Account) -eq 'Other') { Write-Warning "Account $Account is not a 13xx or 18xx securities account." } $rows = @($existing | Where-Object { -not ($_.Account -eq $Account -and $_.Name -eq $Name) }) + $record $action = if ($current) { "Update holding '$Name' on account $Account" } else { "Add holding '$Name' on account $Account" } $Path = Get-LedgerHoldingsPath -YearDir $YearDir if ($PSCmdlet.ShouldProcess($Path, $action)) { Write-LedgerHoldings -YearDir $YearDir -Rows $rows if ($PassThru) { Get-LedgerHolding -JournalPath $JournalPath -FiscalYear $FiscalYear -Account $Account -Name $Name } } } } |