Add GL-001 OpenXML simulation outputs

This commit is contained in:
GiorgioCALLIGARO
2026-05-08 09:33:06 +02:00
parent 9c791e394d
commit 4ce9711c3f
10 changed files with 424 additions and 0 deletions

View File

@@ -31,6 +31,7 @@ A decision is `Confirmed` when it has:
| GL001-PD005 | Decide whether commodity fields `payable_qty` and `uom` are required in the first implementation. | GL001-Q007, GL001-Q013 | Open |
| GL001-PD006 | Decide whether GL-001 requires a new flat reporting dataset. | GL001-Q008 | Open |
| GL001-PD007 | Decide whether audit and source reference mappings are generic across accounting reports or specific to GL-001 first. | GL001-Q010 | Open |
| GL001-PD008 | Decide the `Currency_Rate` source, rate date, direct/inverse convention, precision and field naming convention. | GL001-Q014, GL001-Q015 | Open |
## Decision details
@@ -173,3 +174,23 @@ TBD
Validation expectation:
TBD
### GL001-PD008 Currency rate mapping and naming
Status: Open
Linked open points: GL001-Q014, GL001-Q015
Impacted backlog items: DEV-GL001-002, DEV-GL001-011
Decision:
TBD
Rationale:
TBD
Impact:
TBD
Validation expectation:
TBD

View File

@@ -49,6 +49,7 @@ Tryton covers the accounting foundation and several filters, but the target repo
| Transaction-currency filter | Missing as an explicit GL parameter |
| Opening and closing rows by account and transaction currency | Missing |
| Transaction-currency running balance | Missing |
| FX rate exposure for `Currency_Rate` | Source, rate date, direct/inverse convention and precision not yet mapped |
| Rich audit columns | Raw ingredients exist, but not exposed in normalized GL output |
| Document, voucher and reference mapping | Requires functional arbitration |
| Dedicated XLSX export | Not identified |
@@ -85,3 +86,5 @@ Tryton supports important base-currency controls:
- Trial Balance reuses the same account model, helping base-currency reconciliation.
The main remaining control gap is transaction-currency reconciliation by account and currency, including opening, movement and closing balances.
The new GL-001 `Currency_Rate` field also requires a conversion-control rule: for non-base-currency lines, exported transaction-currency amounts, base-currency amounts and the displayed FX rate should reconcile within the agreed rounding precision.

View File

@@ -50,6 +50,7 @@
| Running balance in base currency | Available through line balance logic | Covered in base currency | Covered | Confirmed |
| Running balance in transaction currency | `amount_second_currency` is a signed amount | Missing transaction-currency running balance | High | Open |
| Debit / credit split in transaction currency | Tryton stores signed `amount_second_currency` | Missing explicit debit and credit transaction-currency columns | Medium | Open |
| FX rate between transaction and base currency | Currency conversion data may exist on accounting moves or lines depending on origin | Source, rate date, direct/inverse convention and precision not mapped | High | Open |
| Transaction-currency totals | Not explicit | Missing | High | Open |
## Output columns
@@ -73,6 +74,7 @@
| `debit_base_currency` | line `debit` | Sign convention transformation may be required | Open |
| `credit_base_currency` | line `credit` | Target expects negative credits | Open |
| `balance_base_currency` | line `balance` | None identified | Confirmed |
| `Currency_Rate` | likely accounting move line currency conversion rate, company currency rate or derived base / transaction amount ratio | Mapping, naming convention and rate direction to confirm | Open |
| `entered_date` | line or move `create_date` | Exposure in GL output required | Open |
| `modified_date` | line or move `write_date` | Exposure in GL output required | Open |
@@ -116,6 +118,7 @@
| --- | --- | --- |
| High | Build or expose a flat detailed GL dataset matching `GL-001`, not only account summary plus drill-down | Open |
| High | Add transaction-currency grouping, filtering and running balances | Open |
| High | Expose and document `Currency_Rate` with confirmed source, rate date, direct/inverse convention and precision | Open |
| High | Add opening and closing rows by account and transaction currency | Open |
| High | Define mapping for `document_number`, `journal_entry_number`, `voucher_number` and `reference` | Open |
| High | Normalize source document metadata: document type, document date and supplier invoice number | Open |

View File

@@ -11,6 +11,8 @@
| GL001-Q005 | Q: Should credit columns be exported as negative values to match the target GL, even though Tryton UI displays credit as positive? | Sign convention | Open |
| GL001-Q006 | Q: Which origin models should feed `document_date` and `document_type` in the first implementation? | Source document metadata | Open |
| GL001-Q007 | Q: Can `payable_qty` and `uom` be derived reliably from `account.move.line.lot`, fees or invoice line origin for all commodity postings? | Commodity fields | Open |
| GL001-Q014 | Q: Which source, rate date, direct/inverse convention and precision should be used for `Currency_Rate`? | FX rate | Open |
| GL001-Q015 | Q: Should `Currency_Rate` keep its current report-specific capitalization or be harmonized to `currency_rate` with the wider field naming convention? | Field naming | Open |
## Implementation questions

View File

@@ -29,6 +29,7 @@ This file is a working conversion layer between the documented GL-001 gaps and f
| DEV-GL001-008 | Medium | Blocked | Audit metadata columns | GL001-PD007 |
| DEV-GL001-009 | Medium | Blocked | CSV and XLSX export | DEV-GL001-001 |
| DEV-GL001-010 | Low | Blocked | Commodity quantity fields | GL001-PD005 |
| DEV-GL001-011 | High | Blocked | Currency rate exposure | GL001-PD008 |
## DEV-GL001-001 Flat detailed GL dataset
@@ -76,16 +77,19 @@ Development scope:
- Add transaction currency as a grouping key for GL-001.
- Add transaction-currency filtering where supported by the selected implementation design.
- Calculate or expose `balance_transaction_currency`.
- Preserve the `Currency_Rate` value needed to explain conversion from transaction currency to base currency.
Out of scope:
- Deciding whether empty `second_currency` means company currency; this is GL001-PD002.
- Deciding the `Currency_Rate` source, direction and precision; this is GL001-PD008.
Acceptance criteria:
- GL-001 can be grouped by account and transaction currency.
- Running transaction-currency balances are available for movement rows.
- Base-currency lines have a documented transaction-currency fallback or exclusion rule.
- Non-base-currency rows retain a rate value or documented blank rule through DEV-GL001-011.
Validation:
@@ -319,3 +323,36 @@ Acceptance criteria:
Validation:
- Review a sample of commodity AP postings against source invoice, lot or fee evidence.
## DEV-GL001-011 Currency rate exposure
| Field | Value |
| --- | --- |
| Source gap | Expose and document `Currency_Rate` with confirmed source, rate date, direct/inverse convention and precision |
| Linked questions | GL001-Q014, GL001-Q015 |
| Linked decision | GL001-PD008 |
| Priority | High |
| Status | Blocked |
Development scope:
- Populate `Currency_Rate` for GL-001 rows where transaction currency and base currency differ.
- Define the blank or default rule for same-currency rows.
- Preserve enough information to reconcile transaction-currency amounts, base-currency amounts and the displayed FX rate.
- Keep the output column name aligned with `Reports/GL/GL_001_general_ledger.md` until naming is arbitrated.
Out of scope:
- Creating a global FX-rate service or replacing source accounting conversion logic.
- Renaming `Currency_Rate` to `currency_rate` before GL001-PD008 is confirmed.
Acceptance criteria:
- GL-001 output includes `Currency_Rate` in the required column order.
- Rate source and direct/inverse convention are documented for each supported origin.
- Same-currency and missing-rate cases follow a documented rule.
- Exported transaction/base amounts reconcile to `Currency_Rate` within agreed rounding tolerance.
Validation:
- Review a sample of foreign-currency accounting lines against source accounting amounts and stored currency rates.

View File

@@ -36,6 +36,7 @@ It is intended to prepare the future mapping with the target CTRM / ERP. The map
| `counterparty` | Customer, supplier, broker or other party. | `GL-001`, `GL-002` |
| `credit_base_currency` | Credit amount or movement in base currency. | `GL-001`, `GL-002`, `FIN-003`, `AR-001`, `AR-003`, `AP-001`, `AP-003` |
| `credit_transaction_currency` | Credit amount or movement in transaction currency. | `GL-001`, `GL-002`, `FIN-003`, `AR-001`, `AR-003`, `AP-001`, `AP-003` |
| `Currency_Rate` | FX rate applied to convert the transaction-currency amount to base currency for the GL-001 line. | `GL-001` |
| `customer` | Customer account code. | `AR-001`, `AR-002`, `AR-003` |
| `customer_name` | Customer legal or commercial name. | `AR-001`, `AR-002`, `AR-003` |
| `customer_reference` | Customer reference or external customer document reference, when available. | `AR-003` |
@@ -95,6 +96,7 @@ It is intended to prepare the future mapping with the target CTRM / ERP. The map
| `settlement_status` | Open, partially settled, settled and reversed logic depends on the ERP subledger. |
| `voucher_number` | Should be mapped only if it is distinct from `document_number`, `supplier_invoice_number`, customer references and payment references. |
| `supplier_invoice_number` | May map to a vendor invoice number, supplier reference or AP invoice field depending on ERP terminology. |
| `Currency_Rate` | GL-001-specific FX-rate field; mapping must confirm rate source, rate date, direct versus inverse convention, precision and naming convention. |
| `purchase_invoice_number` | Should be mapped only if the ERP exposes a separate internal purchase invoice number. |
| `customer_reference` | Should be mapped only if the ERP exposes a distinct customer-facing reference. |
| `payment_reference` | Should be mapped only if payment references are stored separately from document references. |

View File

@@ -19,6 +19,15 @@ These conventions define the default reporting vocabulary and presentation rules
There is no separate `reporting_currency` in the current reporting scope.
When a report exposes an FX rate between transaction currency and base currency, it should document:
- the rate source;
- the rate date;
- whether the rate is direct or inverse;
- the number of decimal places preserved in the export.
`GL-001 General Ledger` currently defines the FX-rate field as `Currency_Rate`. This report-specific name should be preserved in GL-001 exports until the field naming convention is explicitly harmonized.
---
## Amount fields

View File

@@ -140,6 +140,7 @@ Required columns:
| `debit_transaction_currency` | Debit amount in transaction currency |
| `credit_transaction_currency` | Credit amount in transaction currency |
| `balance_transaction_currency` | Running balance in transaction currency |
| `Currency_Rate` | FX rate of the transaction |
| `debit_base_currency` | Debit amount in base currency |
| `credit_base_currency` | Credit amount in base currency |
| `balance_base_currency` | Running balance in base currency |
@@ -158,6 +159,12 @@ Amount sign convention for this report:
- credit amounts are displayed as negative amounts;
- running balances are calculated using this debit-positive and credit-negative convention.
FX rate presentation for this report:
- `Currency_Rate` should display the FX rate used to convert transaction-currency amounts into base-currency amounts;
- for same-currency transactions, `Currency_Rate` should be either `1.000000` or blank according to the confirmed ERP export convention;
- the source of the rate, rate date, direct versus inverse convention and precision remain to be confirmed during mapping.
---
## 9 Estimated volume
@@ -181,6 +188,7 @@ The report may contain a large number of accounting lines and should support exp
- Total debit and credit amounts should reconcile by selected period, account and transaction currency.
- For each account and transaction currency grouping, opening balance plus movements should reconcile to closing balance.
- The report total in base currency should reconcile with the trial balance for the same entity, period and account selection.
- For non-base-currency lines, transaction-currency amounts, `Currency_Rate` and base-currency amounts should reconcile within the agreed rounding tolerance.
- Each line should retain enough audit references to trace back to the accounting document and source document where available.
- AP-related entries should be identifiable by both `document_number` and `supplier_invoice_number` when the supplier invoice number is available.
- Debit, credit and balance signs should follow the GL-specific convention defined in this report.
@@ -190,3 +198,5 @@ The report may contain a large number of accounting lines and should support exp
## 12 Open questions
- Should `posting_status` be displayed in the first implementation? This depends on the target ERP capabilities.
- Should `Currency_Rate` use the ERP stored accounting rate, a reporting FX rate, or a derived rate from transaction/base amounts?
- Should `Currency_Rate` remain capitalized as currently specified, or be renamed to `currency_rate` to match the wider field naming convention?

337
build_gl001_openxml.ps1 Normal file
View File

@@ -0,0 +1,337 @@
$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