Private/SupplierInvoiceStore.ps1

# Helpers for reading and writing supplier invoice files under a journal's
# 'supplierinvoices/' directory. A supplier invoice (leverantörsfaktura) is the
# accounts-payable mirror of a customer invoice: cost accounts are debited (net)
# together with input VAT, and the payable account (2440 Leverantörsskulder) is
# credited with the gross total. Files are stored as sup0001.txt with the same
# tab-separated layout as customer invoices (metadata + 'Rows:' + 'Payments:').

function Get-LedgerSupplierInvoiceDirectory {
    <#
    .SYNOPSIS
    Returns the path to a journal's supplier invoices directory, optionally
    creating it.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$JournalPath,

        [switch]$Create
    )
    $dir = Join-Path $JournalPath 'supplierinvoices'
    if ($Create -and -not (Test-Path $dir)) {
        New-Item -Path $dir -ItemType Directory | Out-Null
    }
    return $dir
}

function Get-LedgerSupplierInvoiceFileName {
    <#
    .SYNOPSIS
    Returns the file name (sup0001.txt) for a given supplier invoice number.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [int]$InvoiceNumber
    )
    return 'sup' + $InvoiceNumber.ToString('0000') + '.txt'
}

function ConvertTo-LedgerSupplierInvoiceObject {
    <#
    .SYNOPSIS
    Parses the lines of a supplier invoice file into a rich object with computed
    NetTotal, VatTotal, Total, PaidAmount and RemainingAmount.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [string[]]$Content,

        [Parameter(Mandatory)]
        [string]$FilePath
    )

    $meta = @{}
    $rows = @()
    $payments = @()
    $section = 'meta'

    foreach ($line in $Content) {
        if ($line -match '^\s*;') { continue }
        if ($line -eq 'Rows:') { $section = 'rows'; continue }
        if ($line -eq 'Payments:') { $section = 'payments'; continue }

        switch ($section) {
            'rows' {
                if ([string]::IsNullOrWhiteSpace($line)) { continue }
                $parts = $line -split "`t"
                if ($parts.Count -ge 2) {
                    $vatRate = if ($parts.Count -ge 3 -and $parts[2]) { ConvertFrom-LedgerInvoiceAmount -Text $parts[2] } else { [decimal]0 }
                    $net = ConvertFrom-LedgerInvoiceAmount -Text $parts[1]
                    $rows += [PSCustomObject]@{
                        Account    = $parts[0]
                        Amount     = $net
                        VatRate    = $vatRate
                        VatAccount = if ($parts.Count -ge 4) { $parts[3] } else { '' }
                        VatAmount  = [Math]::Round($net * $vatRate, 2)
                    }
                }
            }
            'payments' {
                if ([string]::IsNullOrWhiteSpace($line)) { continue }
                $parts = $line -split "`t"
                if ($parts.Count -ge 2) {
                    $payments += [PSCustomObject]@{
                        Date               = [datetime]::ParseExact($parts[0], 'yyyy-MM-dd', $null)
                        Amount             = ConvertFrom-LedgerInvoiceAmount -Text $parts[1]
                        VerificationNumber = if ($parts.Count -ge 3 -and $parts[2]) { [int]$parts[2] } else { $null }
                        FiscalYear         = if ($parts.Count -ge 4) { $parts[3] } else { '' }
                    }
                }
            }
            default {
                $parts = $line -split "`t", 2
                if ($parts.Count -ge 2) {
                    $meta[$parts[0].TrimEnd(':')] = $parts[1]
                }
            }
        }
    }

    $netTotal = ($rows | Measure-Object -Property Amount -Sum).Sum
    if (-not $netTotal) { $netTotal = [decimal]0 }
    $vatTotal = [decimal]0
    foreach ($r in $rows) { $vatTotal += $r.VatAmount }
    $total = [Math]::Round($netTotal + $vatTotal, 2)

    $paidAmount = ($payments | Measure-Object -Property Amount -Sum).Sum
    if (-not $paidAmount) { $paidAmount = [decimal]0 }

    [PSCustomObject]@{
        InvoiceNumber      = [int]$meta['InvoiceNumber']
        SupplierNumber     = $meta['SupplierNumber']
        SupplierReference  = $meta['SupplierInvoiceNo']
        InvoiceDate        = if ($meta['InvoiceDate']) { [datetime]::ParseExact($meta['InvoiceDate'], 'yyyy-MM-dd', $null) } else { $null }
        DueDate            = if ($meta['DueDate']) { [datetime]::ParseExact($meta['DueDate'], 'yyyy-MM-dd', $null) } else { $null }
        Description        = $meta['Description']
        Status             = $meta['Status']
        PayableAccount     = $meta['PayableAccount']
        Reference          = $meta['Reference']
        BookedVerification = if ($meta['BookedVerification']) { [int]$meta['BookedVerification'] } else { $null }
        BookedFiscalYear   = $meta['BookedFiscalYear']
        Rows               = $rows
        Payments           = $payments
        NetTotal           = [Math]::Round([decimal]$netTotal, 2)
        VatTotal           = [Math]::Round($vatTotal, 2)
        Total              = $total
        PaidAmount         = [Math]::Round([decimal]$paidAmount, 2)
        RemainingAmount    = [Math]::Round($total - $paidAmount, 2)
        FilePath           = $FilePath
    }
}

function Read-LedgerSupplierInvoiceFile {
    <#
    .SYNOPSIS
    Reads and parses a single supplier invoice file, returning an object.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$Path
    )
    if (-not (Test-Path $Path -PathType Leaf)) {
        throw "Supplier invoice file not found: $Path"
    }
    $content = @(Get-Content -Path $Path -Encoding UTF8)
    return ConvertTo-LedgerSupplierInvoiceObject -Content $content -FilePath $Path
}

function Save-LedgerSupplierInvoiceFile {
    <#
    .SYNOPSIS
    Serialises a supplier invoice object back to its file.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [psobject]$Invoice
    )

    $lines = @(
        '; PSLedger Supplier Invoice'
        "InvoiceNumber:`t$($Invoice.InvoiceNumber)"
        "SupplierNumber:`t$($Invoice.SupplierNumber)"
        "SupplierInvoiceNo:`t$($Invoice.SupplierReference)"
        "InvoiceDate:`t$($Invoice.InvoiceDate.ToString('yyyy-MM-dd'))"
        "DueDate:`t$($Invoice.DueDate.ToString('yyyy-MM-dd'))"
        "Description:`t$($Invoice.Description)"
        "Status:`t$($Invoice.Status)"
        "PayableAccount:`t$($Invoice.PayableAccount)"
        "Reference:`t$($Invoice.Reference)"
        "BookedVerification:`t$(if ($null -ne $Invoice.BookedVerification) { $Invoice.BookedVerification } else { '' })"
        "BookedFiscalYear:`t$($Invoice.BookedFiscalYear)"
        'Rows:'
    )

    foreach ($row in $Invoice.Rows) {
        $lines += "$($row.Account)`t$(Format-LedgerInvoiceAmount -Value ([decimal]$row.Amount))`t$(Format-LedgerInvoiceAmount -Value ([decimal]$row.VatRate))`t$($row.VatAccount)"
    }

    $lines += 'Payments:'
    foreach ($p in $Invoice.Payments) {
        $veriField = if ($null -ne $p.VerificationNumber) { $p.VerificationNumber } else { '' }
        $lines += "$($p.Date.ToString('yyyy-MM-dd'))`t$(Format-LedgerInvoiceAmount -Value ([decimal]$p.Amount))`t$veriField`t$($p.FiscalYear)"
    }

    Set-LedgerFileContent -Path $Invoice.FilePath -Value $lines
}