Private/InvoiceAdjustment.ps1
|
# Recoverable operation plans for late charges (fees and interest) and credit # notes on customer invoices. Plans are immutable, written before the # verification and reserve its number, so an interrupted command can be # completed by repeating it. function Read-LedgerInvoiceOperationFields { [CmdletBinding()] param ([Parameter(Mandatory)] [string]$Path) $fields = @{} foreach ($line in Get-Content -LiteralPath $Path -Encoding UTF8 -ErrorAction Stop) { if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith(';')) { continue } if ($line -notmatch '^([A-Za-z]+):\t(.+)$' -or $fields.ContainsKey($Matches[1])) { throw "Invalid invoice operation record '$Path': malformed or duplicate field." } $fields[$Matches[1]] = $Matches[2] } $id = [guid]::Empty $invoiceNumber = 0 $verificationNumber = 0 if ($fields.FormatVersion -ne '1' -or -not [guid]::TryParseExact([string]$fields.OperationId, 'D', [ref]$id) -or $id -eq [guid]::Empty -or -not [int]::TryParse([string]$fields.InvoiceNumber, [ref]$invoiceNumber) -or $invoiceNumber -le 0 -or -not [int]::TryParse([string]$fields.VerificationNumber, [ref]$verificationNumber) -or $verificationNumber -le 0 -or [string]::IsNullOrWhiteSpace($fields.FiscalYear) -or $fields.InvoiceHash -cnotmatch '^[0-9a-f]{64}$' -or $fields.VerificationHash -cnotmatch '^[0-9a-f]{64}$') { throw "Invalid invoice operation record '$Path': missing or invalid identity, reservation or hash." } $null = Assert-LedgerSafeLeafName -Name $fields.FiscalYear -Kind 'fiscal year' [pscustomobject]@{ Fields = $fields Operation = [pscustomobject]@{ OperationId = $id; InvoiceNumber = $invoiceNumber; FiscalYear = $fields.FiscalYear VerificationNumber = $verificationNumber; InvoiceHash = $fields.InvoiceHash; VerificationHash = $fields.VerificationHash } } } function ConvertFrom-LedgerInvoiceOperationDate { [CmdletBinding()] param ([string]$Text) $date = [datetime]::MinValue if (-not [datetime]::TryParseExact($Text, 'yyyy-MM-dd', [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::None, [ref]$date)) { return $null } $date } function Read-LedgerInvoiceChargeOperation { [CmdletBinding()] param ([Parameter(Mandatory)] [string]$Path) $record = Read-LedgerInvoiceOperationFields -Path $Path $fields = $record.Fields $operation = $record.Operation $date = ConvertFrom-LedgerInvoiceOperationDate -Text $fields.ChargeDate $amount = [decimal]0 $reminderCount = 0 if ($fields.ChargeType -cnotin 'Fee', 'Interest' -or $null -eq $date -or $fields.ChargeAmount -notmatch '^\d+(\.\d{1,2})?$' -or -not [decimal]::TryParse([string]$fields.ChargeAmount, [Globalization.NumberStyles]::AllowDecimalPoint, [Globalization.CultureInfo]::InvariantCulture, [ref]$amount) -or $amount -le 0 -or $fields.ChargeAccount -notmatch '^\d+$' -or ($fields.ContainsKey('ReminderCount') -and ($fields.ChargeType -ne 'Fee' -or -not [int]::TryParse([string]$fields.ReminderCount, [ref]$reminderCount) -or $reminderCount -le 0))) { throw "Invalid invoice charge record '$Path': invalid charge type, date, amount, account or reminder count." } if ((Split-Path $Path -Leaf) -ne "$($operation.OperationId).txt") { throw "Invalid invoice charge record '$Path': identity does not match its filename." } $operation | Add-Member -NotePropertyMembers @{ ChargeType = $fields.ChargeType; ChargeDate = $date; ChargeAmount = $amount ChargeAccount = $fields.ChargeAccount; ReminderCount = $reminderCount } $operation } function Get-LedgerInvoiceChargeOperation { [CmdletBinding()] param ([Parameter(Mandatory)] [string]$JournalPath) $directory = Join-Path $JournalPath 'invoices' 'charges' if (-not (Test-Path -LiteralPath $directory)) { return } foreach ($file in Get-ChildItem -LiteralPath $directory -Filter '*.txt' -File -ErrorAction Stop) { Read-LedgerInvoiceChargeOperation -Path $file.FullName } } function Save-LedgerInvoiceChargeOperation { [CmdletBinding()] param ( [Parameter(Mandatory)] [string]$Path, [Parameter(Mandatory)] [psobject]$Operation ) $null = New-Item -ItemType Directory -Path (Split-Path $Path -Parent) -Force -ErrorAction Stop $lines = @( '; PSLedger customer invoice charge' "FormatVersion:`t1" "OperationId:`t$($Operation.OperationId)" "InvoiceNumber:`t$($Operation.InvoiceNumber)" "FiscalYear:`t$($Operation.FiscalYear)" "VerificationNumber:`t$($Operation.VerificationNumber)" "InvoiceHash:`t$($Operation.InvoiceHash)" "VerificationHash:`t$($Operation.VerificationHash)" "ChargeType:`t$($Operation.ChargeType)" "ChargeDate:`t$($Operation.ChargeDate.ToString('yyyy-MM-dd'))" "ChargeAmount:`t$(Format-LedgerInvoiceAmount -Value $Operation.ChargeAmount)" "ChargeAccount:`t$($Operation.ChargeAccount)" ) if ($Operation.ReminderCount) { $lines += "ReminderCount:`t$($Operation.ReminderCount)" } Set-LedgerFileContent -Path $Path -NoClobber -Value $lines } function Get-LedgerRecordedInvoiceCharge { [CmdletBinding()] param ( [Parameter(Mandatory)] [psobject]$Invoice, [Parameter(Mandatory)] [psobject]$Operation ) $charges = @($Invoice.Charges | Where-Object { $_.VerificationNumber -eq $Operation.VerificationNumber -and $_.FiscalYear -eq $Operation.FiscalYear }) if ($charges.Count -gt 1 -or ($charges.Count -eq 1 -and ($charges[0].Date -ne $Operation.ChargeDate -or $charges[0].Amount -ne $Operation.ChargeAmount -or $charges[0].Type -ne $Operation.ChargeType -or $charges[0].Account -ne $Operation.ChargeAccount -or ($Operation.ReminderCount -and [int]$Invoice.ReminderCount -lt $Operation.ReminderCount)))) { throw "Invoice $($Invoice.InvoiceNumber) has conflicting charge references for ChargeId $($Operation.OperationId). Recovery stopped." } $charges } function Get-LedgerInvoiceChargeCommand { [CmdletBinding()] param ([Parameter(Mandatory)] [psobject]$Operation, [Parameter(Mandatory)] [string]$JournalPath) $common = "-JournalPath '$JournalPath' -InvoiceNumber $($Operation.InvoiceNumber) -ChargeId '$($Operation.OperationId)'" $amount = Format-LedgerInvoiceAmount -Value $Operation.ChargeAmount if ($Operation.ReminderCount) { "Add-LedgerInvoiceReminder $common -Fee $amount -BookFee" } elseif ($Operation.ChargeType -eq 'Interest') { "Add-LedgerInvoiceInterest $common" } else { "Add-LedgerInvoiceFee $common -Amount $amount" } } function Get-LedgerInvoiceCreditPath { [CmdletBinding()] param ( [Parameter(Mandatory)] [string]$JournalPath, [Parameter(Mandatory)] [int]$InvoiceNumber ) Join-Path $JournalPath 'invoices' 'credits' (Get-LedgerInvoiceFileName -InvoiceNumber $InvoiceNumber) } function Read-LedgerInvoiceCreditOperation { [CmdletBinding()] param ([Parameter(Mandatory)] [string]$Path) $record = Read-LedgerInvoiceOperationFields -Path $Path $fields = $record.Fields $operation = $record.Operation $creditNumber = 0 $date = ConvertFrom-LedgerInvoiceOperationDate -Text $fields.CreditDate if (-not [int]::TryParse([string]$fields.CreditInvoiceNumber, [ref]$creditNumber) -or $creditNumber -le 0 -or $creditNumber -eq $operation.InvoiceNumber -or $null -eq $date) { throw "Invalid credit invoice record '$Path': invalid credit invoice number or date." } if ((Split-Path $Path -Leaf) -ne (Get-LedgerInvoiceFileName -InvoiceNumber $operation.InvoiceNumber)) { throw "Invalid credit invoice record '$Path': identity does not match its filename." } $operation | Add-Member -NotePropertyMembers @{ CreditInvoiceNumber = $creditNumber; CreditDate = $date } $operation } function Get-LedgerInvoiceCreditOperation { [CmdletBinding()] param ([Parameter(Mandatory)] [string]$JournalPath) $directory = Join-Path $JournalPath 'invoices' 'credits' if (-not (Test-Path -LiteralPath $directory)) { return } $numbers = [Collections.Generic.HashSet[int]]::new() foreach ($file in Get-ChildItem -LiteralPath $directory -Filter 'inv*.txt' -File -ErrorAction Stop) { $operation = Read-LedgerInvoiceCreditOperation -Path $file.FullName if (-not $numbers.Add($operation.CreditInvoiceNumber)) { throw "Credit invoice number $($operation.CreditInvoiceNumber) is reserved by more than one credit plan." } $operation } } function Save-LedgerInvoiceCreditOperation { [CmdletBinding()] param ( [Parameter(Mandatory)] [string]$Path, [Parameter(Mandatory)] [psobject]$Operation ) $null = New-Item -ItemType Directory -Path (Split-Path $Path -Parent) -Force -ErrorAction Stop Set-LedgerFileContent -Path $Path -NoClobber -Value @( '; PSLedger customer credit invoice' "FormatVersion:`t1" "OperationId:`t$($Operation.OperationId)" "InvoiceNumber:`t$($Operation.InvoiceNumber)" "CreditInvoiceNumber:`t$($Operation.CreditInvoiceNumber)" "FiscalYear:`t$($Operation.FiscalYear)" "VerificationNumber:`t$($Operation.VerificationNumber)" "InvoiceHash:`t$($Operation.InvoiceHash)" "VerificationHash:`t$($Operation.VerificationHash)" "CreditDate:`t$($Operation.CreditDate.ToString('yyyy-MM-dd'))" ) } function Assert-LedgerCreditNote { [CmdletBinding()] param ( [Parameter(Mandatory)] [psobject]$CreditNote, [Parameter(Mandatory)] [psobject]$Original, [Parameter(Mandatory)] [psobject]$Operation ) if ($CreditNote.InvoiceNumber -ne $Operation.CreditInvoiceNumber -or $CreditNote.Status -ne 'Credited' -or $CreditNote.CustomerNumber -ne $Original.CustomerNumber -or $CreditNote.InvoiceDate -ne $Operation.CreditDate -or $CreditNote.BookedVerification -ne $Operation.VerificationNumber -or $CreditNote.BookedFiscalYear -ne $Operation.FiscalYear -or $CreditNote.Description -ne "Kreditfaktura för faktura $($Operation.InvoiceNumber)" -or $CreditNote.Total -ne -$Original.Total) { throw "Credit invoice $($Operation.CreditInvoiceNumber) differs from the credit plan for invoice $($Operation.InvoiceNumber). Recovery stopped." } } function Get-LedgerNextInvoiceNumber { [CmdletBinding()] param ([Parameter(Mandatory)] [string]$JournalPath) $invoiceDir = Get-LedgerInvoiceDirectory -JournalPath $JournalPath $numbers = @(Get-ChildItem -LiteralPath $invoiceDir -Filter 'inv*.txt' -File -ErrorAction SilentlyContinue | ForEach-Object { if ($_.BaseName -match '^inv(\d+)$') { [int]$Matches[1] } }) $numbers += @(Get-LedgerInvoiceCreditOperation -JournalPath $JournalPath | ForEach-Object CreditInvoiceNumber) if ($numbers.Count) { [int]($numbers | Measure-Object -Maximum).Maximum + 1 } else { 1 } } |