Public/Export-LedgerInvoice.ps1

<#
.SYNOPSIS
Exports a customer invoice to a PDF, Word, Markdown or plain-text document.

.DESCRIPTION
Renders a single invoice as a printable document and writes it to a file. The
document contains the seller (from the journal), the customer (from the customer
register), the invoice metadata (number, dates, payment terms), a table of the
revenue rows with VAT, the totals and payment information.

Payment details (bankgiro, plusgiro, IBAN/BIC) are read from the journal metadata
when present — set them with Set-LedgerJournal -Metadata @{ Bankgiro = '...' }.

The PDF is produced by a dependency-free built-in writer (standard fonts, no
external module). Amounts use Swedish formatting.

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

.PARAMETER InvoiceNumber
The number of the invoice to export.

.PARAMETER Path
Destination path for the document.

.PARAMETER Format
The output format: 'Pdf' (default), 'Word' (.docx), 'Markdown' or 'Text'.

.PARAMETER Force
Overwrite the destination file if it already exists.

.EXAMPLE
Export-LedgerInvoice -JournalPath .\MinFirma.ledger -InvoiceNumber 1 -Path .\faktura-1.pdf

Writes invoice 1 as a PDF document.

.EXAMPLE
Export-LedgerInvoice -InvoiceNumber 1 -Path .\faktura-1.docx -Format Word -Force

Writes invoice 1 as a Word document, overwriting any existing file.
#>

function Export-LedgerInvoice {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter()]
        [string]$JournalPath,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [int]$InvoiceNumber,

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

        [Parameter()]
        [ValidateSet('Pdf', 'Word', 'Markdown', 'Text')]
        [string]$Format = 'Pdf',

        [Parameter()]
        [switch]$Force
    )
    process {
        $JournalPath = Resolve-LedgerJournalPath -JournalPath $JournalPath

        if ((Test-Path $Path) -and -not $Force) {
            throw "Destination file already exists: $Path. Use -Force to overwrite."
        }

        $invoice = Get-LedgerInvoice -JournalPath $JournalPath -InvoiceNumber $InvoiceNumber
        if (-not $invoice) {
            throw "Invoice $InvoiceNumber does not exist."
        }

        $journal = Get-LedgerJournal -Path $JournalPath
        $customer = Get-LedgerCustomer -JournalPath $JournalPath -CustomerNumber $invoice.CustomerNumber

        $blocks = @(Build-LedgerInvoiceBlock -Invoice $invoice -Journal $journal -Customer $customer)

        if (-not $PSCmdlet.ShouldProcess($Path, "Export invoice $InvoiceNumber")) {
            return
        }

        switch ($Format) {
            'Word' { ConvertTo-LedgerReportDocx -Block $blocks -Path $Path }
            'Pdf' { ConvertTo-LedgerReportPdf -Block $blocks -Path $Path }
            'Markdown' { ConvertTo-LedgerReportMarkdown -Block $blocks | Set-Content -Path $Path -Encoding UTF8 }
            default { ConvertTo-LedgerReportText -Block $blocks | Set-Content -Path $Path -Encoding UTF8 }
        }
    }
}

function Build-LedgerInvoiceBlock {
    <#
    .SYNOPSIS
    Builds the shared report block model for a single invoice document.
    #>

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

        [Parameter(Mandatory)]
        [psobject]$Journal,

        [Parameter()]
        [psobject]$Customer
    )

    $blocks = New-Object System.Collections.Generic.List[object]

    # --- Seller ---------------------------------------------------------------
    $blocks.Add(@{ Type = 'Title'; Text = $Journal.Name })
    $sellerBits = @()
    if ($Journal.OrgNumber) { $sellerBits += "Org.nr $($Journal.OrgNumber)" }
    if ($Journal.Metadata -and $Journal.Metadata.Contains('VatNumber') -and $Journal.Metadata['VatNumber']) {
        $sellerBits += "Momsreg.nr $($Journal.Metadata['VatNumber'])"
    }
    if ($sellerBits.Count -gt 0) {
        $blocks.Add(@{ Type = 'Paragraph'; Text = ($sellerBits -join ' ') })
    }

    # --- Invoice heading + metadata ------------------------------------------
    $blocks.Add(@{ Type = 'Heading'; Level = 1; Text = "Faktura $($Invoice.InvoiceNumber)" })

    $terms = [int]([datetime]$Invoice.DueDate - [datetime]$Invoice.InvoiceDate).TotalDays
    $blocks.Add(@{ Type = 'Paragraph'; Text = "Fakturadatum: $($Invoice.InvoiceDate.ToString('yyyy-MM-dd'))" })
    $blocks.Add(@{ Type = 'Paragraph'; Text = "Förfallodatum: $($Invoice.DueDate.ToString('yyyy-MM-dd'))" })
    $blocks.Add(@{ Type = 'Paragraph'; Text = "Betalningsvillkor: $terms dagar netto" })
    if ($Invoice.Description) {
        $blocks.Add(@{ Type = 'Paragraph'; Text = "Avser: $($Invoice.Description)" })
    }

    # --- Customer -------------------------------------------------------------
    $blocks.Add(@{ Type = 'Spacer' })
    $custName = if ($Invoice.PSObject.Properties['CustomerName'] -and $Invoice.CustomerName) { $Invoice.CustomerName } elseif ($Customer) { $Customer.Name } else { '' }
    $blocks.Add(@{ Type = 'Heading'; Level = 2; Text = 'Kund' })
    $blocks.Add(@{ Type = 'Paragraph'; Text = "$($Invoice.CustomerNumber) $custName" })
    if ($Customer) {
        if ($Customer.OrgNumber) { $blocks.Add(@{ Type = 'Paragraph'; Text = "Org.nr $($Customer.OrgNumber)" }) }
        if ($Customer.Email) { $blocks.Add(@{ Type = 'Paragraph'; Text = $Customer.Email }) }
    }

    # --- Rows table -----------------------------------------------------------
    $blocks.Add(@{ Type = 'Spacer' })
    $accountNames = @{}
    if ($Invoice.Rows | Where-Object { -not ($_.PSObject.Properties['Description'] -and $_.Description) }) {
        foreach ($a in @(Get-LedgerAccount -JournalPath $Journal.Path)) { $accountNames[[string]$a.AccountNumber] = $a.AccountName }
    }
    $rows = [System.Collections.Generic.List[object]]::new()
    foreach ($r in $Invoice.Rows) {
        $text = if ($r.PSObject.Properties['Description'] -and $r.Description) { $r.Description }
                elseif ($accountNames.ContainsKey([string]$r.Account)) { $accountNames[[string]$r.Account] }
                else { [string]$r.Account }
        $qty = if ($r.PSObject.Properties['Quantity'] -and $null -ne $r.Quantity) {
            $q = [decimal]$r.Quantity
            $qText = Format-LedgerAmount -Value $q -Decimals ($(if ($q -eq [Math]::Round($q, 0)) { 0 } elseif ($q -eq [Math]::Round($q, 1)) { 1 } else { 2 }))
            if ($r.Unit) { "$qText $($r.Unit)" } else { $qText }
        } else { '' }
        $price = if ($r.PSObject.Properties['UnitPrice'] -and $null -ne $r.UnitPrice) { Format-LedgerAmount -Value ([decimal]$r.UnitPrice) } else { '' }
        $vatPct = (Format-LedgerAmount -Value ([decimal]$r.VatRate * 100) -Decimals 0) + ' %'
        $rows.Add(@(
            [string]$text
            $qty
            $price
            $vatPct
            (Format-LedgerAmount -Value ([decimal]$r.Amount))
        ))
    }
    $blocks.Add(@{
            Type   = 'Table'
            Header = @('Beskrivning', 'Antal', 'À-pris', 'Moms %', 'Belopp')
            Align  = @('left', 'right', 'right', 'right', 'right')
            Rows   = $rows.ToArray()
        })

    # --- Totals ---------------------------------------------------------------
    $blocks.Add(@{ Type = 'Paragraph'; Text = "Netto: $(Format-LedgerAmount -Value $Invoice.NetTotal) kr" })
    $blocks.Add(@{ Type = 'Paragraph'; Text = "Moms: $(Format-LedgerAmount -Value $Invoice.VatTotal) kr" })
    $blocks.Add(@{ Type = 'Heading'; Level = 2; Text = "Att betala: $(Format-LedgerAmount -Value $Invoice.Total) kr" })

    if ($Invoice.Status -in @('Partial', 'Paid')) {
        $blocks.Add(@{ Type = 'Paragraph'; Text = "Betalt: $(Format-LedgerAmount -Value $Invoice.PaidAmount) kr" })
        $blocks.Add(@{ Type = 'Paragraph'; Text = "Kvar att betala: $(Format-LedgerAmount -Value $Invoice.RemainingAmount) kr" })
    }

    # --- Payment information --------------------------------------------------
    $payBits = @()
    if ($Journal.Metadata) {
        foreach ($pair in @(@('Bankgiro', 'Bankgiro'), @('Plusgiro', 'Plusgiro'), @('Iban', 'IBAN'), @('Bic', 'BIC'))) {
            $key = $pair[0]; $label = $pair[1]
            if ($Journal.Metadata.Contains($key) -and $Journal.Metadata[$key]) {
                $payBits += "$label $($Journal.Metadata[$key])"
            }
        }
    }
    $blocks.Add(@{ Type = 'Spacer' })
    if ($payBits.Count -gt 0) {
        $blocks.Add(@{ Type = 'Paragraph'; Text = "Betalning: $($payBits -join ' ')" })
    }
    if ($Invoice.PSObject.Properties['OcrReference'] -and $Invoice.OcrReference) {
        $blocks.Add(@{ Type = 'Paragraph'; Text = "OCR-referens: $($Invoice.OcrReference)" })
    }
    $blocks.Add(@{ Type = 'Paragraph'; Text = "Vänligen ange fakturanummer $($Invoice.InvoiceNumber) vid betalning." })

    $blocks
}