Files
accounting_report/build_gl001_openxml.ps1
2026-05-08 09:33:06 +02:00

338 lines
28 KiB
PowerShell

$ErrorActionPreference = "Stop"
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
$threadId = if ($env:CODEX_THREAD_ID) { $env:CODEX_THREAD_ID } else { "local" }
$outDir = Join-Path $root ("outputs\" + $threadId)
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
$outPath = Join-Path $outDir "GL-001_Simulation.xlsx"
if (Test-Path -LiteralPath $outPath) {
$outPath = Join-Path $outDir ("GL-001_Simulation_" + (Get-Date -Format "yyyyMMdd_HHmmss") + ".xlsx")
}
$buildDir = Join-Path $outDir ("_xlsx_build_" + [guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Force -Path $buildDir | Out-Null
function XmlEscape([object]$value) {
if ($null -eq $value) { return "" }
return [System.Security.SecurityElement]::Escape([string]$value)
}
function ColName([int]$index) {
$name = ""
while ($index -gt 0) {
$mod = ($index - 1) % 26
$name = [char](65 + $mod) + $name
$index = [math]::Floor(($index - $mod) / 26)
}
return $name
}
function CellXml([int]$row, [int]$col, [object]$value, [int]$style = 0, [string]$formula = $null) {
$ref = "$(ColName $col)$row"
$s = if ($style -gt 0) { " s=`"$style`"" } else { "" }
if ($formula) {
return "<c r=`"$ref`"$s><f>$(XmlEscape $formula)</f></c>"
}
if ($null -eq $value -or $value -eq "") {
return "<c r=`"$ref`"$s/>"
}
if ($value -is [string] -and $value -match '^\d{4}-\d{2}-\d{2}$') {
$date = [datetime]::ParseExact($value, "yyyy-MM-dd", [Globalization.CultureInfo]::InvariantCulture)
$serial = [int](($date - [datetime]"1899-12-30").TotalDays)
return "<c r=`"$ref`"$s><v>$serial</v></c>"
}
if ($value -is [int] -or $value -is [double] -or $value -is [decimal]) {
return "<c r=`"$ref`"$s><v>$value</v></c>"
}
return "<c r=`"$ref`" t=`"inlineStr`"$s><is><t>$(XmlEscape $value)</t></is></c>"
}
function RowXml([int]$rowNum, [object[]]$values, [int[]]$styles = $null) {
$cells = New-Object System.Collections.Generic.List[string]
for ($i = 0; $i -lt $values.Count; $i++) {
$style = if ($styles -and $i -lt $styles.Count) { $styles[$i] } else { 0 }
$cells.Add((CellXml $rowNum ($i + 1) $values[$i] $style))
}
return "<row r=`"$rowNum`">$($cells -join '')</row>"
}
function Write-Utf8File([string]$path, [string]$content) {
$dir = Split-Path -Parent $path
New-Item -ItemType Directory -Force -Path $dir | Out-Null
[System.IO.File]::WriteAllText($path, $content, [System.Text.UTF8Encoding]::new($false))
}
$accounts = @(
@("1000", "Cash and bank", "Asset", 85000, "Debit"),
@("1100", "Accounts receivable", "Asset", 42000, "Debit"),
@("1200", "Prepaid expenses", "Asset", 12000, "Debit"),
@("2000", "Accounts payable", "Liability", -36500, "Credit"),
@("2200", "VAT payable", "Liability", -8200, "Credit"),
@("3000", "Share capital", "Equity", -100000, "Credit"),
@("4000", "Consulting revenue", "Revenue", 0, "Credit"),
@("5000", "Subcontractor costs", "Expense", 0, "Debit"),
@("6000", "Operating expenses", "Expense", 0, "Debit"),
@("6900", "Bank fees", "Expense", 0, "Debit")
)
$glHeaders = @(
"entity", "account", "account_name", "transaction_currency", "base_currency",
"posting_date", "document_date", "journal", "journal_entry_number", "document_number",
"document_type", "posting_status", "supplier_invoice_number", "counterparty", "voucher_number",
"description", "reference", "payable_qty", "uom",
"debit_transaction_currency", "credit_transaction_currency", "balance_transaction_currency",
"debit_base_currency", "credit_base_currency", "balance_base_currency",
"entered_date", "entered_by", "modified_date", "modified_by"
)
$entries = @(
@("Open Squared SA", "1000", "Cash and bank", "CHF", "CHF", "2026-01-03", "2026-01-03", "BNK", "GL001-0001", "BNK-260103", "BANK", "Posted", "", "UBS", "VCH-260001", "Opening payroll funding transfer", "TRF-260103", "", "", 30000, 0, "", 30000, 0, "", "2026-01-03", "jsmith", "2026-01-03", "jsmith"),
@("Open Squared SA", "1000", "Cash and bank", "CHF", "CHF", "2026-01-20", "2026-01-20", "BNK", "GL001-0004", "PAY-260020", "PAYMENT", "Posted", "", "Partner X", "VCH-260004", "Supplier payment", "PAY-260020", "", "", 0, -18500, "", 0, -18500, "", "2026-01-20", "mdupont", "2026-01-20", "mdupont"),
@("Open Squared SA", "1000", "Cash and bank", "CHF", "CHF", "2026-02-17", "2026-02-17", "BNK", "GL001-0007", "REC-260217", "RECEIPT", "Posted", "", "Client B", "VCH-260007", "Receipt from Client B", "REC-260217", "", "", 48600, 0, "", 48600, 0, "", "2026-02-17", "jsmith", "2026-02-17", "jsmith"),
@("Open Squared SA", "1000", "Cash and bank", "CHF", "CHF", "2026-02-28", "2026-02-28", "BNK", "GL001-0008", "BNK-260228", "BANK", "Posted", "", "UBS", "VCH-260008", "Bank fees paid", "FEE-260228", "", "", 0, -85, "", 0, -85, "", "2026-02-28", "jsmith", "2026-02-28", "jsmith"),
@("Open Squared SA", "1000", "Cash and bank", "CHF", "CHF", "2026-03-22", "2026-03-22", "BNK", "GL001-0011", "PAY-260322", "PAYMENT", "Posted", "", "Microsoft", "VCH-260011", "Payment to Microsoft", "PAY-260322", "", "", 0, -2592, "", 0, -2592, "", "2026-03-22", "mdupont", "2026-03-22", "mdupont"),
@("Open Squared SA", "1000", "Cash and bank", "CHF", "CHF", "2026-03-29", "2026-03-29", "BNK", "GL001-0012", "REC-260329", "RECEIPT", "Posted", "", "Client C", "VCH-260012", "Receipt from Client C", "REC-260329", "", "", 70200, 0, "", 70200, 0, "", "2026-03-29", "jsmith", "2026-03-29", "jsmith"),
@("Open Squared SA", "1100", "Accounts receivable", "CHF", "CHF", "2026-01-03", "2026-01-03", "BNK", "GL001-0001", "BNK-260103", "BANK", "Posted", "", "Client A", "VCH-260001", "Collection of December receivable", "TRF-260103", "", "", 0, -30000, "", 0, -30000, "", "2026-01-03", "jsmith", "2026-01-03", "jsmith"),
@("Open Squared SA", "1100", "Accounts receivable", "CHF", "CHF", "2026-01-09", "2026-01-09", "SAJ", "GL001-0002", "INV-260001", "AR_INV", "Posted", "", "Client B", "VCH-260002", "January consulting invoice", "SOW-OS-001", "", "", 48600, 0, "", 48600, 0, "", "2026-01-09", "akeller", "2026-01-09", "akeller"),
@("Open Squared SA", "1100", "Accounts receivable", "CHF", "CHF", "2026-02-12", "2026-02-12", "SAJ", "GL001-0006", "INV-260019", "AR_INV", "Posted", "", "Client C", "VCH-260006", "February implementation invoice", "SOW-OS-019", "", "", 70200, 0, "", 70200, 0, "", "2026-02-12", "akeller", "2026-02-12", "akeller"),
@("Open Squared SA", "1100", "Accounts receivable", "CHF", "CHF", "2026-02-17", "2026-02-17", "BNK", "GL001-0007", "REC-260217", "RECEIPT", "Posted", "", "Client B", "VCH-260007", "Customer payment", "REC-260217", "", "", 0, -48600, "", 0, -48600, "", "2026-02-17", "jsmith", "2026-02-17", "jsmith"),
@("Open Squared SA", "1100", "Accounts receivable", "CHF", "CHF", "2026-03-15", "2026-03-15", "SAJ", "GL001-0010", "INV-260037", "AR_INV", "Posted", "", "Client A", "VCH-260010", "March advisory invoice", "SOW-OS-037", "", "", 37800, 0, "", 37800, 0, "", "2026-03-15", "akeller", "2026-03-15", "akeller"),
@("Open Squared SA", "1100", "Accounts receivable", "CHF", "CHF", "2026-03-29", "2026-03-29", "BNK", "GL001-0012", "REC-260329", "RECEIPT", "Posted", "", "Client C", "VCH-260012", "Customer payment", "REC-260329", "", "", 0, -70200, "", 0, -70200, "", "2026-03-29", "jsmith", "2026-03-29", "jsmith"),
@("Open Squared SA", "1200", "Prepaid expenses", "CHF", "CHF", "2026-03-04", "2026-03-01", "PUR", "GL001-0009", "BILL-260304", "AP_INV", "Posted", "INS-2026-031", "Insurer", "VCH-260009", "Annual insurance prepayment", "POL-2026", "", "", 9600, 0, "", 9600, 0, "", "2026-03-04", "mdupont", "2026-03-04", "mdupont"),
@("Open Squared SA", "2000", "Accounts payable", "CHF", "CHF", "2026-01-14", "2026-01-12", "PUR", "GL001-0003", "BILL-260014", "AP_INV", "Posted", "PX-2026-014", "Partner X", "VCH-260003", "Supplier payable", "PO-260014", "", "", 0, -18500, "", 0, -18500, "", "2026-01-14", "mdupont", "2026-01-14", "mdupont"),
@("Open Squared SA", "2000", "Accounts payable", "CHF", "CHF", "2026-01-20", "2026-01-20", "BNK", "GL001-0004", "PAY-260020", "PAYMENT", "Posted", "PX-2026-014", "Partner X", "VCH-260004", "Payment to supplier", "PAY-260020", "", "", 18500, 0, "", 18500, 0, "", "2026-01-20", "mdupont", "2026-01-20", "mdupont"),
@("Open Squared SA", "2000", "Accounts payable", "CHF", "CHF", "2026-02-01", "2026-01-31", "PUR", "GL001-0005", "BILL-260201", "AP_INV", "Posted", "MS-2026-002", "Microsoft", "VCH-260005", "Supplier payable", "MS-AZ-260201", "", "", 0, -2592, "", 0, -2592, "", "2026-02-01", "mdupont", "2026-02-01", "mdupont"),
@("Open Squared SA", "2000", "Accounts payable", "CHF", "CHF", "2026-03-04", "2026-03-01", "PUR", "GL001-0009", "BILL-260304", "AP_INV", "Posted", "INS-2026-031", "Insurer", "VCH-260009", "Insurance payable", "POL-2026", "", "", 0, -9600, "", 0, -9600, "", "2026-03-04", "mdupont", "2026-03-04", "mdupont"),
@("Open Squared SA", "2000", "Accounts payable", "CHF", "CHF", "2026-03-22", "2026-03-22", "BNK", "GL001-0011", "PAY-260322", "PAYMENT", "Posted", "MS-2026-002", "Microsoft", "VCH-260011", "Cloud subscriptions paid", "PAY-260322", "", "", 2592, 0, "", 2592, 0, "", "2026-03-22", "mdupont", "2026-03-22", "mdupont"),
@("Open Squared SA", "2200", "VAT payable", "CHF", "CHF", "2026-01-09", "2026-01-09", "SAJ", "GL001-0002", "INV-260001", "TAX", "Posted", "", "Client B", "VCH-260002", "Output VAT", "VAT-OUT", "", "", 0, -3600, "", 0, -3600, "", "2026-01-09", "akeller", "2026-01-09", "akeller"),
@("Open Squared SA", "2200", "VAT payable", "CHF", "CHF", "2026-02-01", "2026-01-31", "PUR", "GL001-0005", "BILL-260201", "TAX", "Posted", "MS-2026-002", "Microsoft", "VCH-260005", "Input VAT", "VAT-IN", "", "", 192, 0, "", 192, 0, "", "2026-02-01", "mdupont", "2026-02-01", "mdupont"),
@("Open Squared SA", "2200", "VAT payable", "CHF", "CHF", "2026-02-12", "2026-02-12", "SAJ", "GL001-0006", "INV-260019", "TAX", "Posted", "", "Client C", "VCH-260006", "Output VAT", "VAT-OUT", "", "", 0, -5200, "", 0, -5200, "", "2026-02-12", "akeller", "2026-02-12", "akeller"),
@("Open Squared SA", "2200", "VAT payable", "CHF", "CHF", "2026-03-15", "2026-03-15", "SAJ", "GL001-0010", "INV-260037", "TAX", "Posted", "", "Client A", "VCH-260010", "Output VAT", "VAT-OUT", "", "", 0, -2800, "", 0, -2800, "", "2026-03-15", "akeller", "2026-03-15", "akeller"),
@("Open Squared SA", "4000", "Consulting revenue", "CHF", "CHF", "2026-01-09", "2026-01-09", "SAJ", "GL001-0002", "INV-260001", "AR_INV", "Posted", "", "Client B", "VCH-260002", "January consulting revenue", "SOW-OS-001", "", "", 0, -45000, "", 0, -45000, "", "2026-01-09", "akeller", "2026-01-09", "akeller"),
@("Open Squared SA", "4000", "Consulting revenue", "CHF", "CHF", "2026-02-12", "2026-02-12", "SAJ", "GL001-0006", "INV-260019", "AR_INV", "Posted", "", "Client C", "VCH-260006", "February implementation revenue", "SOW-OS-019", "", "", 0, -65000, "", 0, -65000, "", "2026-02-12", "akeller", "2026-02-12", "akeller"),
@("Open Squared SA", "4000", "Consulting revenue", "CHF", "CHF", "2026-03-15", "2026-03-15", "SAJ", "GL001-0010", "INV-260037", "AR_INV", "Posted", "", "Client A", "VCH-260010", "March advisory revenue", "SOW-OS-037", "", "", 0, -35000, "", 0, -35000, "", "2026-03-15", "akeller", "2026-03-15", "akeller"),
@("Open Squared SA", "5000", "Subcontractor costs", "CHF", "CHF", "2026-01-14", "2026-01-12", "PUR", "GL001-0003", "BILL-260014", "AP_INV", "Posted", "PX-2026-014", "Partner X", "VCH-260003", "Subcontractor delivery costs", "PO-260014", "", "", 18500, 0, "", 18500, 0, "", "2026-01-14", "mdupont", "2026-01-14", "mdupont"),
@("Open Squared SA", "6000", "Operating expenses", "CHF", "CHF", "2026-02-01", "2026-01-31", "PUR", "GL001-0005", "BILL-260201", "AP_INV", "Posted", "MS-2026-002", "Microsoft", "VCH-260005", "Cloud subscriptions", "MS-AZ-260201", "", "", 2400, 0, "", 2400, 0, "", "2026-02-01", "mdupont", "2026-02-01", "mdupont"),
@("Open Squared SA", "6900", "Bank fees", "CHF", "CHF", "2026-02-28", "2026-02-28", "BNK", "GL001-0008", "BNK-260228", "BANK", "Posted", "", "UBS", "VCH-260008", "Monthly banking charges", "FEE-260228", "", "", 85, 0, "", 85, 0, "", "2026-02-28", "jsmith", "2026-02-28", "jsmith")
)
$styles = @'
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<numFmts count="2"><numFmt numFmtId="164" formatCode="#,##0.00;[Red](#,##0.00);-"/><numFmt numFmtId="165" formatCode="yyyy-mm-dd"/></numFmts>
<fonts count="5">
<font><sz val="10"/><name val="Aptos"/></font>
<font><b/><sz val="16"/><color rgb="FFFFFFFF"/><name val="Aptos"/></font>
<font><b/><sz val="10"/><name val="Aptos"/></font>
<font><color rgb="FF0000FF"/><sz val="10"/><name val="Aptos"/></font>
<font><b/><color rgb="FFFFFFFF"/><sz val="10"/><name val="Aptos"/></font>
</fonts>
<fills count="5"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill><fill><patternFill patternType="solid"><fgColor rgb="FF1F4E79"/><bgColor indexed="64"/></patternFill></fill><fill><patternFill patternType="solid"><fgColor rgb="FFDDEBF7"/><bgColor indexed="64"/></patternFill></fill><fill><patternFill patternType="solid"><fgColor rgb="FFC6EFCE"/><bgColor indexed="64"/></patternFill></fill></fills>
<borders count="2"><border><left/><right/><top/><bottom/><diagonal/></border><border><left style="thin"/><right style="thin"/><top style="thin"/><bottom style="thin"/><diagonal/></border></borders>
<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
<cellXfs count="8">
<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>
<xf numFmtId="0" fontId="1" fillId="2" borderId="0" xfId="0" applyFont="1" applyFill="1"/>
<xf numFmtId="0" fontId="2" fillId="3" borderId="1" xfId="0" applyFont="1" applyFill="1" applyBorder="1"/>
<xf numFmtId="164" fontId="0" fillId="0" borderId="1" xfId="0" applyNumberFormat="1" applyBorder="1"/>
<xf numFmtId="0" fontId="0" fillId="0" borderId="1" xfId="0" applyBorder="1"/>
<xf numFmtId="0" fontId="3" fillId="0" borderId="1" xfId="0" applyFont="1" applyBorder="1"/>
<xf numFmtId="165" fontId="0" fillId="0" borderId="1" xfId="0" applyNumberFormat="1" applyBorder="1"/>
<xf numFmtId="0" fontId="4" fillId="2" borderId="0" xfId="0" applyFont="1" applyFill="1"/>
</cellXfs>
<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>
</styleSheet>
'@
function SheetDoc([string]$sheetData, [string]$cols, [string]$merge = "", [string]$autoFilter = "", [string]$pane = "") {
return "<?xml version=`"1.0`" encoding=`"UTF-8`" standalone=`"yes`"?><worksheet xmlns=`"http://schemas.openxmlformats.org/spreadsheetml/2006/main`" xmlns:r=`"http://schemas.openxmlformats.org/officeDocument/2006/relationships`"><sheetViews><sheetView workbookViewId=`"0`">$pane</sheetView></sheetViews>$cols<sheetData>$sheetData</sheetData>$autoFilter$merge<pageMargins left=`"0.7`" right=`"0.7`" top=`"0.75`" bottom=`"0.75`" header=`"0.3`" footer=`"0.3`"/></worksheet>"
}
$coverRows = @()
$coverRows += "<row r=`"2`">$(CellXml 2 2 'GL-001 - Simulation de sortie du rapport' 1)</row>"
$coverRows += "<row r=`"4`">$(CellXml 4 2 'Ce classeur simule une sortie GL-001 pour validation de format, de colonnes, de totaux et de contrôles. Les écritures et soldes sont fictifs.' 0)</row>"
$coverData = @(
@("Entité", "Open Squared SA"),
@("Rapport", "GL-001 - General Ledger Detail"),
@("Période", "2026-01-01 à 2026-03-31"),
@("Devise", "CHF"),
@("Base", "Comptabilité d'exercice"),
@("Statut", "Simulation"),
@("Préparé le", (Get-Date -Format "yyyy-MM-dd"))
)
for ($i = 0; $i -lt $coverData.Count; $i++) { $coverRows += RowXml (6 + $i) @("", $coverData[$i][0], $coverData[$i][1]) @(0,4,4) }
$coverRows += "<row r=`"14`">$(CellXml 14 2 'Structure du classeur' 2)</row>"
$coverRows += RowXml 15 @("", "00_Cover", "Conventions, statut et navigation") @(0,4,4)
$coverRows += RowXml 16 @("", "01_Parameters", "Paramètres de simulation et soldes d'ouverture") @(0,4,4)
$coverRows += RowXml 17 @("", "02_GL001_Output", "Sortie détaillée simulée du rapport GL-001") @(0,4,4)
$coverRows += RowXml 18 @("", "03_Controls", "Contrôles de balance, complétude et cohérence") @(0,4,4)
$coverCols = "<cols><col min=`"2`" max=`"2`" width=`"18`" customWidth=`"1`"/><col min=`"3`" max=`"8`" width=`"22`" customWidth=`"1`"/></cols>"
$coverMerge = "<mergeCells count=`"3`"><mergeCell ref=`"B2:H2`"/><mergeCell ref=`"B4:H4`"/><mergeCell ref=`"B14:H14`"/></mergeCells>"
$paramRows = @()
$paramRows += "<row r=`"1`">$(CellXml 1 1 'Paramètres et soldes d''ouverture' 1)</row>"
$paramInfo = @(
@("Entity", "Open Squared SA"),
@("Report code", "GL-001"),
@("Period start", "2026-01-01"),
@("Period end", "2026-03-31"),
@("Currency", "CHF"),
@("Scenario", "Simulated output")
)
for ($i = 0; $i -lt $paramInfo.Count; $i++) {
$valueStyle = if ((3 + $i) -in @(5, 6)) { 6 } else { 5 }
$paramRows += RowXml (3 + $i) $paramInfo[$i] @(4,$valueStyle)
}
$paramRows += RowXml 10 @("Account", "Account name", "Type", "Opening balance CHF", "Normal sign") @(2,2,2,2,2)
for ($i = 0; $i -lt $accounts.Count; $i++) { $paramRows += RowXml (11 + $i) $accounts[$i] @(4,4,4,3,4) }
$paramCols = "<cols><col min=`"1`" max=`"1`" width=`"14`" customWidth=`"1`"/><col min=`"2`" max=`"2`" width=`"28`" customWidth=`"1`"/><col min=`"3`" max=`"3`" width=`"14`" customWidth=`"1`"/><col min=`"4`" max=`"4`" width=`"20`" customWidth=`"1`"/><col min=`"5`" max=`"5`" width=`"14`" customWidth=`"1`"/></cols>"
$paramMerge = "<mergeCells count=`"1`"><mergeCell ref=`"A1:F1`"/></mergeCells>"
$glRows = @()
$glRows += "<row r=`"1`">$(CellXml 1 1 'GL-001 - General Ledger Detail (simulation)' 1)</row>"
$glRows += RowXml 3 $glHeaders @(2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2)
for ($i = 0; $i -lt $entries.Count; $i++) {
$r = 4 + $i
$e = $entries[$i]
$stylesRow = @(4,4,4,4,4,6,6,4,4,4,4,4,4,4,4,4,4,3,4,3,3,3,3,3,3,6,4,6,4)
$cells = New-Object System.Collections.Generic.List[string]
for ($c = 1; $c -le $glHeaders.Count; $c++) {
if ($c -eq 22) {
if ($r -eq 4) {
$formula = "INDEX('01_Parameters'!`$D`$11:`$D`$20,MATCH(B4,'01_Parameters'!`$A`$11:`$A`$20,0))+T4+U4"
} else {
$formula = "IF(AND(B$r=B$($r-1),D$r=D$($r-1)),V$($r-1)+T$r+U$r,INDEX('01_Parameters'!`$D`$11:`$D`$20,MATCH(B$r,'01_Parameters'!`$A`$11:`$A`$20,0))+T$r+U$r)"
}
$cells.Add((CellXml $r $c "" 3 $formula))
} elseif ($c -eq 25) {
if ($r -eq 4) {
$formula = "INDEX('01_Parameters'!`$D`$11:`$D`$20,MATCH(B4,'01_Parameters'!`$A`$11:`$A`$20,0))+W4+X4"
} else {
$formula = "IF(AND(B$r=B$($r-1),E$r=E$($r-1)),Y$($r-1)+W$r+X$r,INDEX('01_Parameters'!`$D`$11:`$D`$20,MATCH(B$r,'01_Parameters'!`$A`$11:`$A`$20,0))+W$r+X$r)"
}
$cells.Add((CellXml $r $c "" 3 $formula))
} else {
$cells.Add((CellXml $r $c $e[$c-1] $stylesRow[$c-1]))
}
}
$glRows += "<row r=`"$r`">$($cells -join '')</row>"
}
$lastGlRow = 3 + $entries.Count
$glCols = "<cols><col min=`"1`" max=`"1`" width=`"18`" customWidth=`"1`"/><col min=`"2`" max=`"2`" width=`"12`" customWidth=`"1`"/><col min=`"3`" max=`"3`" width=`"26`" customWidth=`"1`"/><col min=`"4`" max=`"5`" width=`"14`" customWidth=`"1`"/><col min=`"6`" max=`"7`" width=`"13`" customWidth=`"1`"/><col min=`"8`" max=`"8`" width=`"10`" customWidth=`"1`"/><col min=`"9`" max=`"11`" width=`"18`" customWidth=`"1`"/><col min=`"12`" max=`"13`" width=`"20`" customWidth=`"1`"/><col min=`"14`" max=`"17`" width=`"20`" customWidth=`"1`"/><col min=`"18`" max=`"19`" width=`"12`" customWidth=`"1`"/><col min=`"20`" max=`"25`" width=`"20`" customWidth=`"1`"/><col min=`"26`" max=`"29`" width=`"16`" customWidth=`"1`"/></cols>"
$glMerge = "<mergeCells count=`"1`"><mergeCell ref=`"A1:AC1`"/></mergeCells>"
$glFilter = "<autoFilter ref=`"A3:AC$lastGlRow`"/>"
$glPane = "<pane ySplit=`"3`" topLeftCell=`"A4`" activePane=`"bottomLeft`" state=`"frozen`"/><selection pane=`"bottomLeft`"/>"
$checkRows = @()
$checkRows += "<row r=`"1`">$(CellXml 1 1 'Contrôles GL-001' 1)</row>"
$checkRows += RowXml 3 @("Check", "Actual", "Expected", "Difference", "Tolerance", "Status", "Notes") @(2,2,2,2,2,2,2)
$checkLabels = @(
@("Transaction currency movements net to zero", "SUM('02_GL001_Output'!T4:T$lastGlRow)+SUM('02_GL001_Output'!U4:U$lastGlRow)", 0, "B4-C4", 0.01, "IF(ABS(D4)<=E4,`"OK`",`"Review`")", "Credits are displayed as negative amounts per GL-001 sign convention."),
@("Base currency movements net to zero", "SUM('02_GL001_Output'!W4:W$lastGlRow)+SUM('02_GL001_Output'!X4:X$lastGlRow)", 0, "B5-C5", 0.01, "IF(ABS(D5)<=E5,`"OK`",`"Review`")", "Base currency should reconcile to the same balanced journal movements."),
@("No blank account codes", "COUNTBLANK('02_GL001_Output'!B4:B$lastGlRow)", 0, "B6-C6", 0, "IF(D6=0,`"OK`",`"Review`")", "Every transaction line should carry an account code."),
@("All rows posted", "COUNTIF('02_GL001_Output'!L4:L$lastGlRow,`"<>Posted`")", 0, "B7-C7", 0, "IF(D7=0,`"OK`",`"Review`")", "Simulation assumes only posted lines are in scope."),
@("Report period start", "MIN('02_GL001_Output'!F4:F$lastGlRow)", "'01_Parameters'!B5", "B8-C8", 0, "IF(B8>=C8,`"OK`",`"Review`")", "No transaction before selected period start."),
@("Report period end", "MAX('02_GL001_Output'!F4:F$lastGlRow)", "'01_Parameters'!B6", "B9-C9", 0, "IF(B9<=C9,`"OK`",`"Review`")", "No transaction after selected period end."),
@("AP invoices carry supplier invoice number", "COUNTIFS('02_GL001_Output'!K4:K$lastGlRow,`"AP_INV`",'02_GL001_Output'!M4:M$lastGlRow,`"`")", 0, "B10-C10", 0, "IF(D10=0,`"OK`",`"Review`")", "AP-related entries should expose supplier_invoice_number when available.")
)
for ($i = 0; $i -lt $checkLabels.Count; $i++) {
$r = 4 + $i
$item = $checkLabels[$i]
if ($item[2] -is [int]) {
$expectedCell = CellXml $r 3 $item[2] 3
} else {
$expectedCell = CellXml $r 3 "" 3 $item[2]
}
$cells = @(
(CellXml $r 1 $item[0] 4),
(CellXml $r 2 "" 3 $item[1]),
$expectedCell,
(CellXml $r 4 "" 3 $item[3]),
(CellXml $r 5 $item[4] 3),
(CellXml $r 6 "" 4 $item[5]),
(CellXml $r 7 $item[6] 4)
)
$checkRows += "<row r=`"$r`">$($cells -join '')</row>"
}
$checkRows += RowXml 13 @("Account", "Account name", "Currency", "Opening TC", "Debits TC", "Credits TC", "Closing TC", "Debits base", "Credits base", "Closing base") @(2,2,2,2,2,2,2,2,2,2)
for ($i = 0; $i -lt $accounts.Count; $i++) {
$r = 14 + $i
$src = 11 + $i
$cells = @(
(CellXml $r 1 "" 4 "'01_Parameters'!A$src"),
(CellXml $r 2 "" 4 "'01_Parameters'!B$src"),
(CellXml $r 3 "CHF" 4),
(CellXml $r 4 "" 3 "'01_Parameters'!D$src"),
(CellXml $r 5 "" 3 "SUMIFS('02_GL001_Output'!T:T,'02_GL001_Output'!B:B,A$r,'02_GL001_Output'!D:D,C$r)"),
(CellXml $r 6 "" 3 "SUMIFS('02_GL001_Output'!U:U,'02_GL001_Output'!B:B,A$r,'02_GL001_Output'!D:D,C$r)"),
(CellXml $r 7 "" 3 "D$r+E$r+F$r"),
(CellXml $r 8 "" 3 "SUMIFS('02_GL001_Output'!W:W,'02_GL001_Output'!B:B,A$r,'02_GL001_Output'!E:E,C$r)"),
(CellXml $r 9 "" 3 "SUMIFS('02_GL001_Output'!X:X,'02_GL001_Output'!B:B,A$r,'02_GL001_Output'!E:E,C$r)"),
(CellXml $r 10 "" 3 "D$r+H$r+I$r")
)
$checkRows += "<row r=`"$r`">$($cells -join '')</row>"
}
$checkCols = "<cols><col min=`"1`" max=`"1`" width=`"42`" customWidth=`"1`"/><col min=`"2`" max=`"10`" width=`"16`" customWidth=`"1`"/></cols>"
$checkMerge = "<mergeCells count=`"1`"><mergeCell ref=`"A1:J1`"/></mergeCells>"
Write-Utf8File (Join-Path $buildDir "[Content_Types].xml") @'
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
<Override PartName="/xl/worksheets/sheet2.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
<Override PartName="/xl/worksheets/sheet3.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
<Override PartName="/xl/worksheets/sheet4.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
</Types>
'@
Write-Utf8File (Join-Path $buildDir "_rels\.rels") @'
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
</Relationships>
'@
Write-Utf8File (Join-Path $buildDir "xl\_rels\workbook.xml.rels") @'
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet2.xml"/>
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet3.xml"/>
<Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet4.xml"/>
<Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
</Relationships>
'@
Write-Utf8File (Join-Path $buildDir "xl\workbook.xml") @'
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<sheets>
<sheet name="00_Cover" sheetId="1" r:id="rId1"/>
<sheet name="01_Parameters" sheetId="2" r:id="rId2"/>
<sheet name="02_GL001_Output" sheetId="3" r:id="rId3"/>
<sheet name="03_Controls" sheetId="4" r:id="rId4"/>
</sheets>
<calcPr calcMode="auto" fullCalcOnLoad="1" forceFullCalc="1"/>
</workbook>
'@
Write-Utf8File (Join-Path $buildDir "xl\styles.xml") $styles
Write-Utf8File (Join-Path $buildDir "xl\worksheets\sheet1.xml") (SheetDoc ($coverRows -join "") $coverCols $coverMerge)
Write-Utf8File (Join-Path $buildDir "xl\worksheets\sheet2.xml") (SheetDoc ($paramRows -join "") $paramCols $paramMerge "<autoFilter ref=`"A10:E20`"/>")
Write-Utf8File (Join-Path $buildDir "xl\worksheets\sheet3.xml") (SheetDoc ($glRows -join "") $glCols $glMerge $glFilter $glPane)
Write-Utf8File (Join-Path $buildDir "xl\worksheets\sheet4.xml") (SheetDoc ($checkRows -join "") $checkCols $checkMerge)
Add-Type -AssemblyName System.IO.Compression.FileSystem
Add-Type -AssemblyName System.IO.Compression
$zip = [System.IO.Compression.ZipFile]::Open($outPath, [System.IO.Compression.ZipArchiveMode]::Create)
try {
Get-ChildItem -LiteralPath $buildDir -Recurse -File | ForEach-Object {
$relative = $_.FullName.Substring($buildDir.Length + 1).Replace('\', '/')
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $_.FullName, $relative) | Out-Null
}
}
finally {
$zip.Dispose()
}
Remove-Item -LiteralPath $buildDir -Recurse -Force -ErrorAction SilentlyContinue
Write-Output $outPath