Public/New-LedgerSupplierInvoice.ps1

<#
.SYNOPSIS
Creates a new supplier invoice (leverantörsfaktura) in the journal.

.DESCRIPTION
Creates a sequentially numbered supplier invoice file (sup0001.txt, sup0002.txt,
etc.) in the journal's 'supplierinvoices/' directory. A supplier invoice records
what a supplier has billed but is not yet posted to the ledger — use
Invoke-LedgerSupplierInvoicePosting to create the bookkeeping verification, and
Add-LedgerSupplierPayment to register payments.

Supplier invoices are the accounts-payable mirror of customer invoices: each row
is a cost line with a net amount (excluding VAT), an optional (input) VAT rate and
the account VAT is booked to (e.g. 2640). The gross total is booked to the payable
account (2440 Leverantörsskulder) when posted.

Supplier invoices live at the journal level (not inside a fiscal year) because an
invoice may be paid in a later fiscal year than the one it was received in.

.PARAMETER JournalPath
The path to an existing journal directory. If omitted, uses the current journal
set via Set-LedgerCurrentJournal.

.PARAMETER SupplierNumber
The number of the supplier billing us. Must exist in the supplier register
(see Add-LedgerSupplier).

.PARAMETER Date
The invoice date. Defaults to today.

.PARAMETER DueDate
The date the invoice is due. If omitted, it is calculated from the invoice date
plus the supplier's default payment terms.

.PARAMETER Description
A description of what is being billed.

.PARAMETER Rows
An array of hashtables describing the cost rows. Each row must have:
- Account: the cost account (e.g. '5010')
- Amount: the net amount excluding VAT (positive)
and may optionally have:
- VatRate: the input VAT rate as a decimal (e.g. 0.25 for 25%); defaults to 0
- VatAccount: the account input VAT is booked to (e.g. '2640'); required when
  VatRate is greater than 0

.PARAMETER PayableAccount
The accounts-payable account the invoice total is booked to when posted.
Defaults to '2440' (Leverantörsskulder).

.PARAMETER SupplierReference
Optional. The supplier's own invoice number, recorded for reference.

.PARAMETER Reference
Optional. A payment reference (e.g. the supplier's OCR) recorded for the payment.

.PARAMETER PassThru
If specified, returns the created supplier invoice object. By default the command
produces no output.

.EXAMPLE
$rows = @(
    @{ Account = '5010'; Amount = 8000; VatRate = 0.25; VatAccount = '2640' }
)
New-LedgerSupplierInvoice -JournalPath .\MinFirma.ledger -SupplierNumber '100' -Date '2024-03-15' -Description 'Lokalhyra mars' -Rows $rows

Creates a supplier invoice for 8 000 kr plus 25% input VAT from supplier 100.

.EXAMPLE
$rows = @(
    @{ Account = '6110'; Amount = 1200; VatRate = 0.25; VatAccount = '2640' }
)
New-LedgerSupplierInvoice -JournalPath .\MinFirma.ledger -SupplierNumber 'L012' -Description 'Kontorsmateriel' -SupplierReference 'F-99123' -Reference '1234567' -Rows $rows -PassThru

Creates a supplier invoice recording the supplier's invoice number and payment
reference, and returns the created object.
#>

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

        [Parameter(Mandatory)]
        [string]$SupplierNumber,

        [Parameter()]
        [datetime]$Date = (Get-Date).Date,

        [Parameter()]
        [datetime]$DueDate,

        [Parameter(Mandatory)]
        [string]$Description,

        [Parameter(Mandatory)]
        [hashtable[]]$Rows,

        [Parameter()]
        [string]$PayableAccount = '2440',

        [Parameter()]
        [string]$SupplierReference,

        [Parameter()]
        [string]$Reference,

        [Parameter()]
        [switch]$PassThru
    )
    $JournalPath = Resolve-LedgerJournalPath -JournalPath $JournalPath -SchemaCheck Write

    if (-not (Test-Path $JournalPath -PathType Container)) {
        throw "Journal not found: $JournalPath"
    }

    # Validate the supplier exists.
    $supplier = Get-LedgerSupplier -JournalPath $JournalPath -SupplierNumber $SupplierNumber
    if (-not $supplier) {
        throw "Supplier '$SupplierNumber' does not exist. Add it with Add-LedgerSupplier first."
    }

    # Validate and normalise rows.
    if ($Rows.Count -eq 0) {
        throw "A supplier invoice must have at least one row."
    }
    $normalizedRows = foreach ($row in $Rows) {
        if (-not $row.ContainsKey('Account') -or [string]::IsNullOrWhiteSpace([string]$row.Account)) {
            throw "Each supplier invoice row must have an Account."
        }
        if (-not $row.ContainsKey('Amount')) {
            throw "Each supplier invoice row must have an Amount."
        }
        $amount = [decimal]$row.Amount
        if ($amount -le 0) {
            throw "Supplier invoice row amount for account $($row.Account) must be greater than zero."
        }
        $vatRate = if ($row.ContainsKey('VatRate') -and $row.VatRate) { [decimal]$row.VatRate } else { [decimal]0 }
        $vatAccount = if ($row.ContainsKey('VatAccount')) { [string]$row.VatAccount } else { '' }
        if ($vatRate -gt 0 -and [string]::IsNullOrWhiteSpace($vatAccount)) {
            throw "Row for account $($row.Account) has VatRate $vatRate but no VatAccount."
        }
        [PSCustomObject]@{
            Account    = [string]$row.Account
            Amount     = $amount
            VatRate    = $vatRate
            VatAccount = $vatAccount
            VatAmount  = [Math]::Round($amount * $vatRate, 2)
        }
    }
    $normalizedRows = @($normalizedRows)

    # Calculate the due date from supplier payment terms when not supplied.
    if (-not $PSBoundParameters.ContainsKey('DueDate')) {
        $DueDate = $Date.AddDays($supplier.PaymentTermsDays)
    }

    $invoiceDir = Get-LedgerSupplierInvoiceDirectory -JournalPath $JournalPath -Create

    # Determine the next supplier invoice number by scanning existing files.
    $existing = Get-ChildItem -Path $invoiceDir -Filter 'sup*.txt' -File -ErrorAction SilentlyContinue
    if ($existing) {
        $maxNum = $existing |
            ForEach-Object { if ($_.BaseName -match '^sup(\d+)$') { [int]$Matches[1] } } |
            Measure-Object -Maximum |
            Select-Object -ExpandProperty Maximum
        $nextNum = $maxNum + 1
    }
    else {
        $nextNum = 1
    }

    $filePath = Join-Path $invoiceDir (Get-LedgerSupplierInvoiceFileName -InvoiceNumber $nextNum)

    $invoice = [PSCustomObject]@{
        InvoiceNumber      = $nextNum
        SupplierNumber     = $SupplierNumber
        SupplierReference  = if ($PSBoundParameters.ContainsKey('SupplierReference')) { $SupplierReference } else { '' }
        InvoiceDate        = $Date
        DueDate            = $DueDate
        Description        = $Description
        Status             = 'Draft'
        PayableAccount     = $PayableAccount
        Reference          = if ($PSBoundParameters.ContainsKey('Reference')) { $Reference } else { '' }
        BookedVerification = $null
        BookedFiscalYear   = ''
        Rows               = $normalizedRows
        Payments           = @()
        FilePath           = $filePath
    }

    if ($PSCmdlet.ShouldProcess("Supplier $SupplierNumber", "Create supplier invoice $nextNum")) {
        Save-LedgerSupplierInvoiceFile -Invoice $invoice

        if ($PassThru) {
            Get-LedgerSupplierInvoice -JournalPath $JournalPath -InvoiceNumber $nextNum
        }
    }
}