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 0000000..efca947 Binary files /dev/null and b/notes/accounting/excel_api/GL_Accounts_Client_Template.xlsx differ 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 +$$;