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