Public/Set-LedgerBankTransaction.ps1
|
<# .SYNOPSIS Resolves a bank transaction by hand: posts it, links it or ignores it. .DESCRIPTION Handles a bank transaction that Invoke-LedgerBankMatching could not match. Choose one of: - -Account: post the transaction against a counter account (for example a bank fee to 6570 or a tax payment to 1630), optionally with VAT split out with -VatRate and -VatAccount. - -InvoiceNumber: register an incoming payment on a customer invoice. - -SupplierInvoiceNumber: register an outgoing payment on a supplier invoice. - -VerificationNumber: link the transaction to a verification already in the ledger, without posting anything. - -Ignore: mark the transaction as handled outside PSLedger (for example a transaction dated before the journal's first fiscal year). - -Reset: return a matched or ignored transaction to 'Unmatched'. Any verification it created is left in place; reverse it with Add-LedgerReversal if needed. Only unmatched transactions can be newly posted, linked or ignored. Customer and supplier invoice matches with saved payment plans can also be retried after completion: the same invoice/payment is reused without another payment or verification. After interruption, repeat -InvoiceNumber or -SupplierInvoiceNumber with the same invoice, or rerun Invoke-LedgerBankMatching. Input or link conflicts are rejected. Reset preserves a saved payment plan. Only relinking that same payment (with -InvoiceNumber, -SupplierInvoiceNumber or its -VerificationNumber) is allowed afterwards; Reset does not permit paying another invoice, reposting or ignoring the transaction. Corrections to the bookkeeping are a separate operation. Account postings also retain a saved plan. Repeat the original -Account command after interruption; omitted optional inputs use the plan, while explicit changes are rejected. The same posting can be relinked with -VerificationNumber. Reset does not allow rerouting, ignoring or paying an invoice with that transaction. Transfers without VAT can link a counterpart to the same verification. When automatic selection is ambiguous, choose the counterpart transaction explicitly with -VerificationNumber and -FiscalYear. The selection is retained across retries. .PARAMETER JournalPath The path to an existing journal directory. If omitted, uses the current journal set via Set-LedgerCurrentJournal. .PARAMETER TransactionId The id of the bank transaction (see Get-LedgerBankTransaction). Accepts pipeline input by property name. .PARAMETER Account Posts the transaction against this counter account. .PARAMETER Description The verification description when posting with -Account. Defaults to the transaction's counterparty, text and reference. .PARAMETER VatRate With -Account: the VAT rate included in the amount, e.g. 0.25. The gross amount is split into a net part on -Account and VAT on -VatAccount. .PARAMETER VatAccount With -VatRate: the VAT account, e.g. '2640' (Ingående moms) for purchases. .PARAMETER InvoiceNumber Registers the transaction as a payment of this customer invoice. .PARAMETER SupplierInvoiceNumber Registers the transaction as a payment of this supplier invoice. .PARAMETER VerificationNumber Links the transaction to this existing verification. .PARAMETER FiscalYear The fiscal year of -VerificationNumber, or the fiscal year to post into with -Account. Defaults to the fiscal year containing the transaction date. .PARAMETER Ignore Marks the transaction as ignored. .PARAMETER Reset Returns the transaction to 'Unmatched' without clearing saved payment or posting ownership. .PARAMETER PassThru If specified, returns the updated transaction. By default the command produces no output. .EXAMPLE Set-LedgerBankTransaction -JournalPath .\MinFirma.ledger -TransactionId 12 -Account 6570 Posts bank transaction 12 (a bank fee) against 6570 (Bankkostnader). .EXAMPLE Set-LedgerBankTransaction -TransactionId 15 -Account 5410 -VatRate 0.25 -VatAccount 2640 -Description 'Kontorsmaterial Clas Ohlson' Posts a card purchase of office supplies with 25 % input VAT split out. .EXAMPLE Set-LedgerBankTransaction -TransactionId 18 -InvoiceNumber 7 Registers a customer payment that arrived without OCR reference against invoice 7. #> function Set-LedgerBankTransaction { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Ignore', Justification = 'Selects a parameter set')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Reset', Justification = 'Selects a parameter set')] [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Account')] param ( [Parameter()] [string]$JournalPath, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [int]$TransactionId, [Parameter(Mandatory, ParameterSetName = 'Account')] [string]$Account, [Parameter(ParameterSetName = 'Account')] [string]$Description, [Parameter(ParameterSetName = 'Account')] [decimal]$VatRate = 0, [Parameter(ParameterSetName = 'Account')] [string]$VatAccount, [Parameter(Mandatory, ParameterSetName = 'Invoice')] [int]$InvoiceNumber, [Parameter(Mandatory, ParameterSetName = 'SupplierInvoice')] [int]$SupplierInvoiceNumber, [Parameter(Mandatory, ParameterSetName = 'Verification')] [int]$VerificationNumber, [Parameter(ParameterSetName = 'Account')] [Parameter(ParameterSetName = 'Verification')] [string]$FiscalYear, [Parameter(Mandatory, ParameterSetName = 'Ignore')] [switch]$Ignore, [Parameter(Mandatory, ParameterSetName = 'Reset')] [switch]$Reset, [Parameter()] [switch]$PassThru ) begin { $JournalPath = Resolve-LedgerJournalPath -JournalPath $JournalPath -SchemaCheck Write } process { $journalLock = $null try { $journalLock = Enter-LedgerJournalLock -JournalPath $JournalPath Assert-LedgerJournalSchema -Path $JournalPath -Write $operations = @(Get-LedgerInvoicePaymentOperation -JournalPath $JournalPath) $postings = @(Get-LedgerBankPostingOperation -JournalPath $JournalPath) $owned = @($operations | Where-Object { $_.PSObject.Properties['BankTransactionId'] -and $_.BankTransactionId -eq $TransactionId }) $exceptId = if ($owned.Count -eq 1) { $owned[0].OperationId } else { [guid]::Empty } if ($PSCmdlet.ParameterSetName -notin 'Ignore', 'Reset') { Assert-LedgerNoPendingInvoicePayment -JournalPath $JournalPath -ExceptPaymentId $exceptId } $found = Find-LedgerBankTransaction -JournalPath $JournalPath -TransactionId $TransactionId $statement = $found.Statement $t = $found.Transaction $operation = Get-LedgerBankInvoicePaymentOperation -Transaction $t -Operations $operations $posting = Get-LedgerBankPostingOwner -Operations $postings -TransactionId $TransactionId if ($posting -and $operation) { throw "Bank transaction $TransactionId has conflicting payment and posting plans." } if ($posting) { Assert-LedgerBankPostingSource -Transaction $t -Operation $posting -Counterpart:($posting.TransactionId -ne $TransactionId) } if ($operation) { Assert-LedgerBankPaymentLink -Transaction $t -Operation $operation } $target = "Bank transaction $TransactionId ($($t.Date.ToString('yyyy-MM-dd')), $($t.Amount))" if ($PSCmdlet.ParameterSetName -eq 'Reset') { if ($t.Status -eq 'Unmatched') { return } if (-not $PSCmdlet.ShouldProcess($target, 'Reset to Unmatched')) { return } Set-LedgerBankTransactionState -Statement $statement -Transaction $t -Status Unmatched if ($PassThru) { ConvertTo-LedgerBankTransactionOutput -Transaction $t } return } if ($posting) { $sameVerification = $PSCmdlet.ParameterSetName -eq 'Verification' -and $VerificationNumber -eq $posting.VerificationNumber -and (-not $FiscalYear -or $FiscalYear -eq $posting.FiscalYear) $sameAccount = $PSCmdlet.ParameterSetName -eq 'Account' -and $posting.TransactionId -eq $TransactionId -and $Account -eq $posting.Account -and (-not $PSBoundParameters.ContainsKey('Description') -or (ConvertTo-LedgerTextField $Description) -eq $posting.Description) -and (-not $PSBoundParameters.ContainsKey('VatRate') -or $VatRate -eq $posting.VatRate) -and (-not $PSBoundParameters.ContainsKey('VatAccount') -or $VatAccount -eq $posting.VatAccount) -and (-not $FiscalYear -or $FiscalYear -eq $posting.FiscalYear) if (-not ($sameVerification -or $sameAccount)) { throw "Bank transaction $TransactionId has a saved posting plan; only relinking the same posting with its original inputs is allowed." } if (-not $PSCmdlet.ShouldProcess($target, 'Resume saved bank posting')) { return } $null = Complete-LedgerBankPosting -JournalPath $JournalPath -Operation $posting if ($PassThru) { ConvertTo-LedgerBankTransactionOutput -Transaction (Find-LedgerBankTransaction -JournalPath $JournalPath -TransactionId $TransactionId).Transaction } return } if ($operation) { $sameInvoice = ($operation.InvoiceType -eq 'Customer' -and $PSCmdlet.ParameterSetName -eq 'Invoice' -and $InvoiceNumber -eq $operation.InvoiceNumber) -or ($operation.InvoiceType -eq 'Supplier' -and $PSCmdlet.ParameterSetName -eq 'SupplierInvoice' -and $SupplierInvoiceNumber -eq $operation.InvoiceNumber) $sameVerification = $PSCmdlet.ParameterSetName -eq 'Verification' -and $VerificationNumber -eq $operation.VerificationNumber -and (-not $FiscalYear -or $FiscalYear -eq $operation.FiscalYear) if (-not ($sameInvoice -or $sameVerification)) { throw "Bank transaction $TransactionId belongs to the saved payment of $($operation.InvoiceType) invoice $($operation.InvoiceNumber). Reset does not undo that payment; only relinking the same payment is allowed." } if (-not $PSCmdlet.ShouldProcess($target, 'Resume saved invoice payment match')) { return } $null = Invoke-LedgerBankInvoicePayment -JournalPath $JournalPath -Transaction $t -Statement $statement ` -InvoiceNumber $operation.InvoiceNumber -InvoiceType $operation.InvoiceType if ($PassThru) { ConvertTo-LedgerBankTransactionOutput -Transaction $t } return } if ($t.Status -ne 'Unmatched') { throw "Bank transaction $TransactionId is already $($t.Status.ToLowerInvariant()). Use -Reset first to change it." } switch ($PSCmdlet.ParameterSetName) { 'Ignore' { if (-not $PSCmdlet.ShouldProcess($target, 'Ignore')) { return } Set-LedgerBankTransactionState -Statement $statement -Transaction $t -Status Ignored -MatchType 'Ignored' } 'Verification' { if (-not $FiscalYear) { $FiscalYear = Find-FiscalYearForDate -JournalPath $JournalPath -Date $t.Date if (-not $FiscalYear) { throw "No fiscal year covers the transaction date $($t.Date.ToString('yyyy-MM-dd')). Specify -FiscalYear." } } $reserved = @($postings | Where-Object { $_.FiscalYear -eq $FiscalYear -and $_.VerificationNumber -eq $VerificationNumber }) if ($reserved.Count) { if ($reserved.Count -ne 1 -or $reserved[0].Counterpart -or $reserved[0].Account -ne $t.BankAccount -or $reserved[0].VatRate -ne 0) { throw 'Verification is reserved for another bank transaction.' } if (-not $PSCmdlet.ShouldProcess($target, 'Select counterpart of saved bank posting')) { return } Save-LedgerBankPostingCounterpart -JournalPath $JournalPath -Operation $reserved[0] -Transaction $t $null = Complete-LedgerBankPosting -JournalPath $JournalPath -Operation $reserved[0] if ($PassThru) { ConvertTo-LedgerBankTransactionOutput -Transaction (Find-LedgerBankTransaction -JournalPath $JournalPath -TransactionId $TransactionId).Transaction } return } if (@($operations | Where-Object { $_.PSObject.Properties['BankTransactionId'] -and $_.BankTransactionId -ne $TransactionId -and $_.PaymentAccount -eq $t.BankAccount -and $_.FiscalYear -eq $FiscalYear -and $_.VerificationNumber -eq $VerificationNumber }).Count) { throw "Verification $VerificationNumber in $FiscalYear is reserved for another bank transaction's payment." } $movement = Get-LedgerBankAccountMovements -JournalPath $JournalPath -FiscalYear $FiscalYear -BankAccount $t.BankAccount | Where-Object VerificationNumber -eq $VerificationNumber if (-not $movement) { throw "Verification $VerificationNumber in $FiscalYear does not exist or has no row on account $($t.BankAccount)." } if ($movement.Amount -ne $t.Amount) { Write-Warning "Verification $VerificationNumber has $($movement.Amount) on account $($t.BankAccount), but the bank transaction is $($t.Amount)." } $linked = Get-LedgerBankLinkedVerifications -Statements @(Get-LedgerBankStatementData -JournalPath $JournalPath) if ($linked.Contains("$($t.BankAccount)|$FiscalYear|$VerificationNumber")) { Write-Warning "Verification $VerificationNumber in $FiscalYear is already linked to another bank transaction." } if (-not $PSCmdlet.ShouldProcess($target, "Link to verification $VerificationNumber in $FiscalYear")) { return } Set-LedgerBankTransactionState -Statement $statement -Transaction $t -Status Matched -MatchType 'Entry' ` -MatchRef "$FiscalYear/$VerificationNumber" -FiscalYear $FiscalYear -VerificationNumber $VerificationNumber } 'Invoice' { if (-not $PSCmdlet.ShouldProcess($target, "Register payment of customer invoice $InvoiceNumber")) { return } $null = Invoke-LedgerBankInvoicePayment -JournalPath $JournalPath -Transaction $t -Statement $statement -InvoiceNumber $InvoiceNumber } 'SupplierInvoice' { if (-not $PSCmdlet.ShouldProcess($target, "Register payment of supplier invoice $SupplierInvoiceNumber")) { return } $null = Invoke-LedgerBankSupplierPayment -JournalPath $JournalPath -Transaction $t -Statement $statement -InvoiceNumber $SupplierInvoiceNumber } 'Account' { if (-not $PSCmdlet.ShouldProcess($target, "Post against account $Account")) { return } $entryParams = @{ JournalPath = $JournalPath Transaction = $t Account = $Account Description = $Description VatRate = $VatRate VatAccount = $VatAccount FiscalYear = $FiscalYear } $null = Invoke-LedgerBankAccountEntry @entryParams $t = (Find-LedgerBankTransaction -JournalPath $JournalPath -TransactionId $TransactionId).Transaction } } if ($PassThru) { ConvertTo-LedgerBankTransactionOutput -Transaction $t } } finally { Exit-LedgerJournalLock -Lock $journalLock } } } |