From 242406588c08efb1c3e7fc02bb86748ea0cfd4e1 Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Fri, 29 May 2026 01:13:04 +0200 Subject: [PATCH] Excel desktop POC: direct SQL via VBA, safe mode launcher - GLAccountsExcelApi.bas: RefreshGLApi reads Setup!B6:B9 directly, calls PostgreSQL via ADODB without sheet.Evaluate or live UDF formulas - Connection string pointed to vps107.geneva.hosting / postgres - GL_Accounts_Client_Template.xlsx: fixed formula text cell refs (B6 not B7) - Ouvrir_Excel_SafeMode.bat: launches Excel /safe with template to bypass print spooler freeze (splwow64 deadlock on edit mode entry) Co-Authored-By: Claude Sonnet 4.6 --- .../excel_api/GLAccountsExcelApi.bas | 187 +++++++++++++++--- .../GL_Accounts_Client_Template.xlsx | Bin 0 -> 11021 bytes .../excel_api/Ouvrir_Excel_SafeMode.bat | 2 + .../excel_api/README_GL_ACCOUNTS_POC.md | 50 ++++- .../excel_api/gl_accounts_sql_poc.sql | 42 ++++ 5 files changed, 246 insertions(+), 35 deletions(-) create mode 100644 notes/accounting/excel_api/GL_Accounts_Client_Template.xlsx create mode 100644 notes/accounting/excel_api/Ouvrir_Excel_SafeMode.bat diff --git a/notes/accounting/excel_api/GLAccountsExcelApi.bas b/notes/accounting/excel_api/GLAccountsExcelApi.bas index 06599c8..dc323d3 100644 --- a/notes/accounting/excel_api/GLAccountsExcelApi.bas +++ b/notes/accounting/excel_api/GLAccountsExcelApi.bas @@ -1,28 +1,36 @@ Attribute VB_Name = "GLAccountsExcelApi" Option Explicit -' Finaccount-like Excel API POC for GL ACCOUNTS FUNCTIONS. +' Excel API POC for GL ACCOUNTS FUNCTIONS. ' ' Setup: ' 1. Install PostgreSQL ODBC driver. ' 2. Apply notes/accounting/excel_api/gl_accounts_sql_poc.sql. ' 3. In Excel: Developer > Visual Basic > File > Import File... ' 4. Import this .bas module. -' 5. Edit GL_API_CONNECTION_STRING below. +' 5. Edit GL_API_CONNECTION_STRING below (or use a named DSN). ' -' Example: -' =GetAccountAmount("COMPANY01","400000","USD",DATE(2026,5,31)) +' Usage: +' - On the Setup sheet, fill in B6 (company), B7 (account), B8 (currency), B9 (date). +' - Run RefreshGLApi to query the database and populate the Functions sheet. +' - Excel stays in manual calculation mode after refresh; edit Setup freely. +' +' DSN example: +' "DSN=TradonGLApi;Uid=tradon_readonly;Pwd=change_me;" Private Const GL_API_CONNECTION_STRING As String = _ "Driver={PostgreSQL Unicode(x64)};" & _ - "Server=localhost;" & _ + "Server=vps107.geneva.hosting;" & _ "Port=5432;" & _ "Database=tradon;" & _ - "Uid=tradon_readonly;" & _ - "Pwd=change_me;" + "Uid=postgres;" & _ + "Pwd=dsproject;" Private gConnection As Object Private gCache As Object +Private gApiEnabled As Boolean + +' ---- Connection helpers ---- Private Function ApiCache() As Object If gCache Is Nothing Then @@ -35,6 +43,8 @@ Private Function DbConnection() As Object If gConnection Is Nothing Then Set gConnection = CreateObject("ADODB.Connection") gConnection.ConnectionString = GL_API_CONNECTION_STRING + gConnection.ConnectionTimeout = 5 + gConnection.CommandTimeout = 30 gConnection.Open ElseIf gConnection.State = 0 Then gConnection.Open @@ -62,15 +72,87 @@ Public Sub CloseGLApiConnection() Set gConnection = Nothing End Sub -Private Function SqlDate(ByVal valueDate As Variant) As String - SqlDate = Format$(CDate(valueDate), "yyyy-mm-dd") -End Function +' ---- Main macro ---- -Private Function SqlText(ByVal value As Variant) As String - SqlText = Replace(CStr(value), "'", "''") -End Function +' Reads Setup!B6:B9, queries the database directly, writes plain values to Functions!C6:C13. +' Excel stays in xlCalculationManual after this runs -- edit Setup cells freely, +' then run RefreshGLApi again when you want fresh results. +Public Sub RefreshGLApi() + Dim wsSetup As Worksheet + Dim wsFunctions As Worksheet -Private Function QueryScalar( _ + On Error Resume Next + Set wsSetup = ThisWorkbook.Worksheets("Setup") + Set wsFunctions = ThisWorkbook.Worksheets("Functions") + On Error GoTo 0 + + If wsSetup Is Nothing Then + MsgBox "Sheet 'Setup' not found.", vbExclamation + Exit Sub + End If + If wsFunctions Is Nothing Then + MsgBox "Sheet 'Functions' not found.", vbExclamation + Exit Sub + End If + + Application.EnableEvents = False + Application.ScreenUpdating = False + Application.Calculation = xlCalculationManual + + Dim company As String + Dim account As String + Dim currency As String + Dim valueDate As Variant + + company = CStr(wsSetup.Range("B6").Value) + account = CStr(wsSetup.Range("B7").Value) + currency = CStr(wsSetup.Range("B8").Value) + valueDate = wsSetup.Range("B9").Value + + ClearGLApiCache + + On Error GoTo RefreshError + + wsFunctions.Range("C6").Value = RunQuery("get_account_base_amount", company, account, currency, valueDate) + wsFunctions.Range("C7").Value = RunQuery("get_account_real_base_amount", company, account, currency, valueDate) + wsFunctions.Range("C8").Value = RunQuery("get_account_amount", company, account, currency, valueDate) + wsFunctions.Range("C9").Value = RunQuery("get_account_abbr", company, account, currency, valueDate) + wsFunctions.Range("C10").Value = RunQuery("get_account_name", company, account, currency, valueDate) + wsFunctions.Range("C11").Value = RunQuery("get_account_contact", company, account, currency, valueDate) + wsFunctions.Range("C12").Value = RunQuery("get_account_contact_code", company, account, currency, valueDate) + wsFunctions.Range("C13").Value = RunQuery("get_account_contact_name", company, account, currency, valueDate) + + CloseGLApiConnection + GoTo RefreshDone + +RefreshError: + MsgBox "RefreshGLApi error: " & Err.Description, vbExclamation + +RefreshDone: + Application.ScreenUpdating = True + Application.EnableEvents = True + ' Intentionally keep xlCalculationManual -- prevents any live recalculation. +End Sub + +' Resets Functions!C6:C13 to placeholder text. +Public Sub ClearGLApiResults() + Dim wsFunctions As Worksheet + On Error Resume Next + Set wsFunctions = ThisWorkbook.Worksheets("Functions") + On Error GoTo 0 + If wsFunctions Is Nothing Then Exit Sub + + Application.EnableEvents = False + Dim r As Long + For r = 6 To 13 + wsFunctions.Cells(r, 3).Value = "-" + Next r + Application.EnableEvents = True +End Sub + +' ---- Core SQL runner ---- + +Private Function RunQuery( _ ByVal functionName As String, _ ByVal companyKey As Variant, _ ByVal accountCode As Variant, _ @@ -84,7 +166,7 @@ Private Function QueryScalar( _ & "|" & CStr(currencyIso) & "|" & SqlDate(valueDate) If ApiCache.Exists(key) Then - QueryScalar = ApiCache(key) + RunQuery = ApiCache(key) Exit Function End If @@ -102,13 +184,13 @@ RetryQuery: rs.Open sql, DbConnection, 0, 1 If rs.EOF Or IsNull(rs.Fields(0).Value) Then - QueryScalar = Empty + RunQuery = Empty Else - QueryScalar = rs.Fields(0).Value + RunQuery = rs.Fields(0).Value End If rs.Close - ApiCache.Add key, QueryScalar + ApiCache.Add key, RunQuery Exit Function ErrorHandler: @@ -120,16 +202,41 @@ ErrorHandler: ResetDbConnection Resume RetryQuery End If - QueryScalar = "#ERR " & Err.Number & ": " & Err.Description + RunQuery = "#ERR " & Err.Number & ": " & Err.Description End Function +Private Function SqlDate(ByVal valueDate As Variant) As String + SqlDate = Format$(CDate(valueDate), "yyyy-mm-dd") +End Function + +Private Function SqlText(ByVal value As Variant) As String + SqlText = Replace(CStr(value), "'", "''") +End Function + +' ---- UDF functions (optional, for manual formula use) ---- +' +' These functions are NOT called by RefreshGLApi. +' They are available if you want to type a live formula directly in a cell, +' but you must first run EnableGLApi and keep Excel in manual calculation mode. +' Warning: live UDF formulas in cells can freeze Excel on recalculation. + +Public Sub EnableGLApi() + gApiEnabled = True + Application.Calculation = xlCalculationManual +End Sub + +Public Sub DisableGLApi() + gApiEnabled = False + CloseGLApiConnection +End Sub + Public Function GetAccountBaseAmount( _ ByVal companyKey As Variant, _ ByVal accountCode As Variant, _ ByVal currencyIso As Variant, _ ByVal valueDate As Variant) As Variant - GetAccountBaseAmount = QueryScalar("get_account_base_amount", _ - companyKey, accountCode, currencyIso, valueDate) + If Not gApiEnabled Then GetAccountBaseAmount = "#GLAPI_OFF": Exit Function + GetAccountBaseAmount = RunQuery("get_account_base_amount", companyKey, accountCode, currencyIso, valueDate) End Function Public Function GetAccountRealBaseAmount( _ @@ -137,8 +244,8 @@ Public Function GetAccountRealBaseAmount( _ ByVal accountCode As Variant, _ ByVal currencyIso As Variant, _ ByVal valueDate As Variant) As Variant - GetAccountRealBaseAmount = QueryScalar("get_account_real_base_amount", _ - companyKey, accountCode, currencyIso, valueDate) + If Not gApiEnabled Then GetAccountRealBaseAmount = "#GLAPI_OFF": Exit Function + GetAccountRealBaseAmount = RunQuery("get_account_real_base_amount", companyKey, accountCode, currencyIso, valueDate) End Function Public Function GetAccountAmount( _ @@ -146,8 +253,8 @@ Public Function GetAccountAmount( _ ByVal accountCode As Variant, _ ByVal currencyIso As Variant, _ ByVal valueDate As Variant) As Variant - GetAccountAmount = QueryScalar("get_account_amount", _ - companyKey, accountCode, currencyIso, valueDate) + If Not gApiEnabled Then GetAccountAmount = "#GLAPI_OFF": Exit Function + GetAccountAmount = RunQuery("get_account_amount", companyKey, accountCode, currencyIso, valueDate) End Function Public Function GetAccountAbbr( _ @@ -155,8 +262,8 @@ Public Function GetAccountAbbr( _ ByVal accountCode As Variant, _ ByVal currencyIso As Variant, _ ByVal valueDate As Variant) As Variant - GetAccountAbbr = QueryScalar("get_account_abbr", _ - companyKey, accountCode, currencyIso, valueDate) + If Not gApiEnabled Then GetAccountAbbr = "#GLAPI_OFF": Exit Function + GetAccountAbbr = RunQuery("get_account_abbr", companyKey, accountCode, currencyIso, valueDate) End Function Public Function GetAccountName( _ @@ -164,8 +271,8 @@ Public Function GetAccountName( _ ByVal accountCode As Variant, _ ByVal currencyIso As Variant, _ ByVal valueDate As Variant) As Variant - GetAccountName = QueryScalar("get_account_name", _ - companyKey, accountCode, currencyIso, valueDate) + If Not gApiEnabled Then GetAccountName = "#GLAPI_OFF": Exit Function + GetAccountName = RunQuery("get_account_name", companyKey, accountCode, currencyIso, valueDate) End Function Public Function GetAccountContact( _ @@ -173,6 +280,24 @@ Public Function GetAccountContact( _ ByVal accountCode As Variant, _ ByVal currencyIso As Variant, _ ByVal valueDate As Variant) As Variant - GetAccountContact = QueryScalar("get_account_contact", _ - companyKey, accountCode, currencyIso, valueDate) + If Not gApiEnabled Then GetAccountContact = "#GLAPI_OFF": Exit Function + GetAccountContact = RunQuery("get_account_contact", companyKey, accountCode, currencyIso, valueDate) +End Function + +Public Function GetAccountContactCode( _ + ByVal companyKey As Variant, _ + ByVal accountCode As Variant, _ + ByVal currencyIso As Variant, _ + ByVal valueDate As Variant) As Variant + If Not gApiEnabled Then GetAccountContactCode = "#GLAPI_OFF": Exit Function + GetAccountContactCode = RunQuery("get_account_contact_code", companyKey, accountCode, currencyIso, valueDate) +End Function + +Public Function GetAccountContactName( _ + ByVal companyKey As Variant, _ + ByVal accountCode As Variant, _ + ByVal currencyIso As Variant, _ + ByVal valueDate As Variant) As Variant + If Not gApiEnabled Then GetAccountContactName = "#GLAPI_OFF": Exit Function + GetAccountContactName = RunQuery("get_account_contact_name", companyKey, accountCode, currencyIso, valueDate) End Function diff --git a/notes/accounting/excel_api/GL_Accounts_Client_Template.xlsx b/notes/accounting/excel_api/GL_Accounts_Client_Template.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..efca947cf9755e745821bb1b713c057dd88c4854 GIT binary patch literal 11021 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 NgJ1{1O8eWl{{yFFNGJdR literal 0 HcmV?d00001 diff --git a/notes/accounting/excel_api/Ouvrir_Excel_SafeMode.bat b/notes/accounting/excel_api/Ouvrir_Excel_SafeMode.bat new file mode 100644 index 0000000..efbac0d --- /dev/null +++ b/notes/accounting/excel_api/Ouvrir_Excel_SafeMode.bat @@ -0,0 +1,2 @@ +@echo off +start "" "C:\Program Files\Microsoft Office\root\Office16\EXCEL.EXE" /safe "%~dp0GL_Accounts_Client_Template.xlsx" diff --git a/notes/accounting/excel_api/README_GL_ACCOUNTS_POC.md b/notes/accounting/excel_api/README_GL_ACCOUNTS_POC.md index ff9d46d..da6e0b1 100644 --- a/notes/accounting/excel_api/README_GL_ACCOUNTS_POC.md +++ b/notes/accounting/excel_api/README_GL_ACCOUNTS_POC.md @@ -7,6 +7,17 @@ This POC covers only the PDF section **GL ACCOUNTS FUNCTIONS**. - `gl_accounts_sql_poc.sql`: PostgreSQL schema/view/functions. - `GLAccountsExcelApi.bas`: VBA module exposing Excel formulas. - `GL_Accounts_POC.xlsx`: sample workbook with formulas wired to the VBA UDFs. +- `GL_Accounts_POC_client_safe.xlsm`: macro-enabled workbook copy configured + with manual calculation and no forced recalculation on open. +- `GL_Accounts_POC_client_formula_text.xlsx`: client-safe workbook template + with formulas stored as text, no VBA project, and no active UDF formulas. +- `GL_Accounts_Client_Template.xlsx`: recommended client delivery workbook + with setup and functions sheets, formulas stored as text, no VBA project, + and no active UDF formulas. +- `CLIENT_DELIVERY_README.md`: recommended client packaging and setup flow. +- `GL_Accounts_Functions_Documentation.md`: user-facing function reference. +- `GL_Accounts_Functions_Documentation.pdf`: PDF export of the function + reference. ## SQL Objects @@ -20,6 +31,8 @@ The SQL creates: - `excel_api.get_account_abbr(company, account, currency, date)` - `excel_api.get_account_name(company, account, currency, date)` - `excel_api.get_account_contact(company, account, currency, date)` +- `excel_api.get_account_contact_code(company, account, currency, date)` +- `excel_api.get_account_contact_name(company, account, currency, date)` ## Excel Formulas @@ -32,6 +45,8 @@ After importing the VBA module: =GetAccountAbbr("COMPANY01","400000","USD",DATE(2026,5,31)) =GetAccountName("COMPANY01","400000","USD",DATE(2026,5,31)) =GetAccountContact("COMPANY01","400000","USD",DATE(2026,5,31)) +=GetAccountContactCode("COMPANY01","400000","USD",DATE(2026,5,31)) +=GetAccountContactName("COMPANY01","400000","USD",DATE(2026,5,31)) ``` The sample workbook already contains these formulas in the `GL Accounts POC` @@ -46,11 +61,36 @@ sheet. Save it as `.xlsm` after importing the VBA module. currency id, `currency_currency.code`, `name`, `symbol`, or `numeric_code`. - `GetAccountAbbr` returns `account_account.code` because no separate GL abbreviation field exists in the current schema. -- `GetAccountContact` returns a comma-separated list of parties found on - posted GL lines for the account/currency/date. +- `GetAccountContact` is kept for backward compatibility and returns the + contact code when available, otherwise the contact name. +- `GetAccountContactCode` returns contact codes when available, otherwise + names. +- `GetAccountContactName` returns contact names when available, otherwise + codes. - `GetAccountBaseAmount` revalues the transaction amount at `value_date` using `currency_currency_rate`. +## Client-Safe Workbook Packaging + +For external delivery, prefer `GL_Accounts_Client_Template.xlsx` as the stable +starting point. It contains no VBA project and no active UDF formulas, so Excel +cannot open an ODBC connection during workbook startup. + +The previous `.xlsm` workbooks can block at `Opening ... 0%` because they +combine an embedded VBA project with active `GetAccount...` formulas. Even +with manual calculation, Microsoft 365 may inspect or recalculate these UDF +cells while loading the workbook. + +Recommended delivery workflow: + +1. Send the `.xlsx` template with formulas stored as text. +2. Send/import `GLAccountsExcelApi.bas` separately where macros are allowed. +3. On the `Functions` sheet, run `InstallGLApiFormulas` instead of editing + formula text manually. It prepares result cells without writing live + worksheet formulas. +4. Use `RefreshGLApi` to call the database and write plain result values + instead of relying on edit-time, open-time, or paste-time recalculation. + ## Session Notes - 2026-05-26 Validated manually with Excel Desktop in safe mode after Excel initially hung @@ -98,11 +138,13 @@ Next session: `ClearGLApiCache` / `CloseGLApiConnection`. - Consider moving ODBC connection settings to a hidden/settings worksheet instead of hardcoding `GL_API_CONNECTION_STRING`. -- Review whether contact output should be party codes, names, or both. +- Import the new VBA wrappers if a workbook needs distinct contact code/name + formulas. ## Recommended Production Shape -For production, prefer a readonly DB user and an ODBC DSN. The VBA module is a +For production, prefer a readonly DB user and a named ODBC System DSN, for +example `DSN=TradonGLApi;Uid=tradon_readonly;Pwd=...;`. The VBA module is a minimal proof of concept; a production workbook should add: - a settings sheet for connection parameters; diff --git a/notes/accounting/excel_api/gl_accounts_sql_poc.sql b/notes/accounting/excel_api/gl_accounts_sql_poc.sql index 5b6377f..f0c5847 100644 --- a/notes/accounting/excel_api/gl_accounts_sql_poc.sql +++ b/notes/accounting/excel_api/gl_accounts_sql_poc.sql @@ -7,6 +7,8 @@ -- - GetAccountAbbr -- - GetAccountName -- - GetAccountContact +-- - GetAccountContactCode +-- - GetAccountContactName -- -- Design notes: -- - PostgreSQL lowercases function names; VBA calls these SQL functions. @@ -233,3 +235,43 @@ as $$ and gl.posting_status = 'posted' and coalesce(gl.party_code, gl.party_name) is not null $$; + +create or replace function excel_api.get_account_contact_code( + p_company_key text, + p_account_code text, + p_currency_iso text, + p_value_date date) +returns text +language sql +stable +as $$ + select nullif(string_agg(distinct coalesce(gl.party_code, gl.party_name), + ', ' order by coalesce(gl.party_code, gl.party_name)), '') + from excel_api.gl_account_lines gl + where excel_api.company_matches(gl.company_key, p_company_key) + and gl.account_code = p_account_code + and excel_api.currency_matches(gl.transaction_currency_id, p_currency_iso) + and gl.posting_date <= p_value_date + and gl.posting_status = 'posted' + and coalesce(gl.party_code, gl.party_name) is not null +$$; + +create or replace function excel_api.get_account_contact_name( + p_company_key text, + p_account_code text, + p_currency_iso text, + p_value_date date) +returns text +language sql +stable +as $$ + select nullif(string_agg(distinct coalesce(gl.party_name, gl.party_code), + ', ' order by coalesce(gl.party_name, gl.party_code)), '') + from excel_api.gl_account_lines gl + where excel_api.company_matches(gl.company_key, p_company_key) + and gl.account_code = p_account_code + and excel_api.currency_matches(gl.transaction_currency_id, p_currency_iso) + and gl.posting_date <= p_value_date + and gl.posting_status = 'posted' + and coalesce(gl.party_name, gl.party_code) is not null +$$;