diff --git a/Assessments/Tradon/GL_001/decisions.md b/Assessments/Tradon/GL_001/decisions.md
index ba32d67..d66f1e1 100644
--- a/Assessments/Tradon/GL_001/decisions.md
+++ b/Assessments/Tradon/GL_001/decisions.md
@@ -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
diff --git a/Assessments/Tradon/GL_001/exposure_notes.md b/Assessments/Tradon/GL_001/exposure_notes.md
index 806c72b..9afb81d 100644
--- a/Assessments/Tradon/GL_001/exposure_notes.md
+++ b/Assessments/Tradon/GL_001/exposure_notes.md
@@ -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.
diff --git a/Assessments/Tradon/GL_001/gap_mapping.md b/Assessments/Tradon/GL_001/gap_mapping.md
index 77f4894..bab158b 100644
--- a/Assessments/Tradon/GL_001/gap_mapping.md
+++ b/Assessments/Tradon/GL_001/gap_mapping.md
@@ -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 |
diff --git a/Assessments/Tradon/GL_001/open_points.md b/Assessments/Tradon/GL_001/open_points.md
index 8942582..4c8a844 100644
--- a/Assessments/Tradon/GL_001/open_points.md
+++ b/Assessments/Tradon/GL_001/open_points.md
@@ -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
diff --git a/DevelopmentBacklog/Tradon/GL_001_backlog.md b/DevelopmentBacklog/Tradon/GL_001_backlog.md
index 12d3c7a..74e90fb 100644
--- a/DevelopmentBacklog/Tradon/GL_001_backlog.md
+++ b/DevelopmentBacklog/Tradon/GL_001_backlog.md
@@ -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.
diff --git a/Docs/reporting_fields_catalog.md b/Docs/reporting_fields_catalog.md
index 7e1e201..cd6d9d8 100644
--- a/Docs/reporting_fields_catalog.md
+++ b/Docs/reporting_fields_catalog.md
@@ -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. |
diff --git a/Docs/reporting_standards.md b/Docs/reporting_standards.md
index 016e4c2..a6ad84c 100644
--- a/Docs/reporting_standards.md
+++ b/Docs/reporting_standards.md
@@ -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
diff --git a/Reports/GL/GL_001_general_ledger.md b/Reports/GL/GL_001_general_ledger.md
index b6a0091..69e5100 100644
--- a/Reports/GL/GL_001_general_ledger.md
+++ b/Reports/GL/GL_001_general_ledger.md
@@ -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?
diff --git a/build_gl001_openxml.ps1 b/build_gl001_openxml.ps1
new file mode 100644
index 0000000..2223a8a
--- /dev/null
+++ b/build_gl001_openxml.ps1
@@ -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 "$(XmlEscape $formula)"
+ }
+ if ($null -eq $value -or $value -eq "") {
+ return ""
+ }
+ 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 "$serial"
+ }
+ if ($value -is [int] -or $value -is [double] -or $value -is [decimal]) {
+ return "$value"
+ }
+ return "$(XmlEscape $value)"
+}
+
+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 "$($cells -join '')
"
+}
+
+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 = @'
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+'@
+
+function SheetDoc([string]$sheetData, [string]$cols, [string]$merge = "", [string]$autoFilter = "", [string]$pane = "") {
+ return "$pane$cols$sheetData$autoFilter$merge"
+}
+
+$coverRows = @()
+$coverRows += "$(CellXml 2 2 'GL-001 - Simulation de sortie du rapport' 1)
"
+$coverRows += "$(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)
"
+$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 += "$(CellXml 14 2 'Structure du classeur' 2)
"
+$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 = ""
+$coverMerge = ""
+
+$paramRows = @()
+$paramRows += "$(CellXml 1 1 'Paramètres et soldes d''ouverture' 1)
"
+$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 = ""
+$paramMerge = ""
+
+$glRows = @()
+$glRows += "$(CellXml 1 1 'GL-001 - General Ledger Detail (simulation)' 1)
"
+$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 += "$($cells -join '')
"
+}
+$lastGlRow = 3 + $entries.Count
+$glCols = ""
+$glMerge = ""
+$glFilter = ""
+$glPane = ""
+
+$checkRows = @()
+$checkRows += "$(CellXml 1 1 'Contrôles GL-001' 1)
"
+$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 += "$($cells -join '')
"
+}
+$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 += "$($cells -join '')
"
+}
+$checkCols = ""
+$checkMerge = ""
+
+Write-Utf8File (Join-Path $buildDir "[Content_Types].xml") @'
+
+
+
+
+
+
+
+
+
+
+
+'@
+Write-Utf8File (Join-Path $buildDir "_rels\.rels") @'
+
+
+
+
+'@
+Write-Utf8File (Join-Path $buildDir "xl\_rels\workbook.xml.rels") @'
+
+
+
+
+
+
+
+
+'@
+Write-Utf8File (Join-Path $buildDir "xl\workbook.xml") @'
+
+
+
+
+
+
+
+
+
+
+'@
+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 "")
+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
diff --git a/outputs/019e065a-3d5e-7d70-9797-7e208041f996/GL-001_Simulation_20260508_091451.xlsx b/outputs/019e065a-3d5e-7d70-9797-7e208041f996/GL-001_Simulation_20260508_091451.xlsx
new file mode 100644
index 0000000..8c459d9
Binary files /dev/null and b/outputs/019e065a-3d5e-7d70-9797-7e208041f996/GL-001_Simulation_20260508_091451.xlsx differ