Go to matching

This commit is contained in:
2026-05-26 18:54:07 +02:00
parent 8b4daeac84
commit 0fffb341fe
8 changed files with 667 additions and 26 deletions

View File

@@ -0,0 +1,178 @@
Attribute VB_Name = "GLAccountsExcelApi"
Option Explicit
' Finaccount-like 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.
'
' Example:
' =GetAccountAmount("COMPANY01","400000","USD",DATE(2026,5,31))
Private Const GL_API_CONNECTION_STRING As String = _
"Driver={PostgreSQL Unicode(x64)};" & _
"Server=localhost;" & _
"Port=5432;" & _
"Database=tradon;" & _
"Uid=tradon_readonly;" & _
"Pwd=change_me;"
Private gConnection As Object
Private gCache As Object
Private Function ApiCache() As Object
If gCache Is Nothing Then
Set gCache = CreateObject("Scripting.Dictionary")
End If
Set ApiCache = gCache
End Function
Private Function DbConnection() As Object
If gConnection Is Nothing Then
Set gConnection = CreateObject("ADODB.Connection")
gConnection.ConnectionString = GL_API_CONNECTION_STRING
gConnection.Open
ElseIf gConnection.State = 0 Then
gConnection.Open
End If
Set DbConnection = gConnection
End Function
Private Sub ResetDbConnection()
On Error Resume Next
If Not gConnection Is Nothing Then
If gConnection.State <> 0 Then gConnection.Close
End If
Set gConnection = Nothing
On Error GoTo 0
End Sub
Public Sub ClearGLApiCache()
If Not gCache Is Nothing Then gCache.RemoveAll
End Sub
Public Sub CloseGLApiConnection()
If Not gConnection Is Nothing Then
If gConnection.State <> 0 Then gConnection.Close
End If
Set gConnection = Nothing
End Sub
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
Private Function QueryScalar( _
ByVal functionName As String, _
ByVal companyKey As Variant, _
ByVal accountCode As Variant, _
ByVal currencyIso As Variant, _
ByVal valueDate As Variant) As Variant
On Error GoTo ErrorHandler
Dim key As String
key = functionName & "|" & CStr(companyKey) & "|" & CStr(accountCode) _
& "|" & CStr(currencyIso) & "|" & SqlDate(valueDate)
If ApiCache.Exists(key) Then
QueryScalar = ApiCache(key)
Exit Function
End If
Dim sql As String
sql = "select excel_api." & functionName & "('" _
& SqlText(companyKey) & "','" _
& SqlText(accountCode) & "','" _
& SqlText(currencyIso) & "','" _
& SqlDate(valueDate) & "'::date)"
Dim rs As Object
Dim attempt As Integer
RetryQuery:
Set rs = CreateObject("ADODB.Recordset")
rs.Open sql, DbConnection, 0, 1
If rs.EOF Or IsNull(rs.Fields(0).Value) Then
QueryScalar = Empty
Else
QueryScalar = rs.Fields(0).Value
End If
rs.Close
ApiCache.Add key, QueryScalar
Exit Function
ErrorHandler:
If attempt = 0 Then
attempt = 1
If Not rs Is Nothing Then
If rs.State <> 0 Then rs.Close
End If
ResetDbConnection
Resume RetryQuery
End If
QueryScalar = "#ERR " & Err.Number & ": " & Err.Description
End Function
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)
End Function
Public Function GetAccountRealBaseAmount( _
ByVal companyKey As Variant, _
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)
End Function
Public Function GetAccountAmount( _
ByVal companyKey As Variant, _
ByVal accountCode As Variant, _
ByVal currencyIso As Variant, _
ByVal valueDate As Variant) As Variant
GetAccountAmount = QueryScalar("get_account_amount", _
companyKey, accountCode, currencyIso, valueDate)
End Function
Public Function GetAccountAbbr( _
ByVal companyKey As Variant, _
ByVal accountCode As Variant, _
ByVal currencyIso As Variant, _
ByVal valueDate As Variant) As Variant
GetAccountAbbr = QueryScalar("get_account_abbr", _
companyKey, accountCode, currencyIso, valueDate)
End Function
Public Function GetAccountName( _
ByVal companyKey As Variant, _
ByVal accountCode As Variant, _
ByVal currencyIso As Variant, _
ByVal valueDate As Variant) As Variant
GetAccountName = QueryScalar("get_account_name", _
companyKey, accountCode, currencyIso, valueDate)
End Function
Public Function GetAccountContact( _
ByVal companyKey As Variant, _
ByVal accountCode As Variant, _
ByVal currencyIso As Variant, _
ByVal valueDate As Variant) As Variant
GetAccountContact = QueryScalar("get_account_contact", _
companyKey, accountCode, currencyIso, valueDate)
End Function

Binary file not shown.

View File

@@ -0,0 +1,111 @@
# Excel API POC - GL Accounts
This POC covers only the PDF section **GL ACCOUNTS FUNCTIONS**.
## Files
- `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.
## SQL Objects
The SQL creates:
- `excel_api.gl_account_lines`
- `excel_api.currency_rate_at(currency_id, value_date)`
- `excel_api.get_account_base_amount(company, account, currency, date)`
- `excel_api.get_account_real_base_amount(company, account, currency, date)`
- `excel_api.get_account_amount(company, account, currency, date)`
- `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 Formulas
After importing the VBA module:
```excel
=GetAccountBaseAmount("COMPANY01","400000","USD",DATE(2026,5,31))
=GetAccountRealBaseAmount("COMPANY01","400000","USD",DATE(2026,5,31))
=GetAccountAmount("COMPANY01","400000","USD",DATE(2026,5,31))
=GetAccountAbbr("COMPANY01","400000","USD",DATE(2026,5,31))
=GetAccountName("COMPANY01","400000","USD",DATE(2026,5,31))
=GetAccountContact("COMPANY01","400000","USD",DATE(2026,5,31))
```
The sample workbook already contains these formulas in the `GL Accounts POC`
sheet. Save it as `.xlsm` after importing the VBA module.
## Current POC Assumptions
- The `company` parameter accepts either `party_party.code` of the company
party or the company party name.
- Only posted moves are included.
- The currency parameter filters transaction currency. It accepts the internal
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.
- `GetAccountBaseAmount` revalues the transaction amount at `value_date` using
`currency_currency_rate`.
## Session Notes - 2026-05-26
Validated manually with Excel Desktop in safe mode after Excel initially hung
while opening from the web flow.
What worked:
- Workbook saved as macro-enabled file:
`GL_Accounts_POC_with_formulas.xlsm`.
- VBA module imported into a standard module after removing the raw
`Attribute VB_Name = ...` line when pasted manually.
- `#NOM?` was resolved once macros/VBA compilation were correct.
- Temporary VBA error display (`#ERR <number>: <message>`) helped expose ODBC
failures instead of generic `#VALEUR!`.
- `ICT TRADING` can be entered as the company value after SQL support for
matching by company party name.
- Currency matching now supports values such as `1`, `EUR`, and `USD` by
checking currency id/code/name/symbol/numeric code.
- Verified live examples:
- `ICT TRADING`, account `20011`, currency `1`/`USD`, date `2026-05-13`
returns Debtors values and contact list.
- `ICT TRADING`, account `30011`, currency `EUR`, date `2026-05-13`
returns Creditors values and contact `1741`.
Important findings:
- In the tested database, `currency_currency.code` is not the ISO label:
`id=1` has `code=1`, `name=USD`, `symbol=USD`; `id=2` has `code=2`,
`name=EUR`, `symbol=EUR`.
- The currency parameter currently means transaction-currency filter, not
output currency conversion. `EUR` and `USD` can legitimately return different
subsets for the same account.
- The VBA cache can make testing confusing; run `ClearGLApiCache` and
`CloseGLApiConnection` before comparing changed parameters.
- Excel/ODBC may return `SQLExecDirectW unable due to the connection lost`;
the VBA now resets the ADODB connection and retries once.
- The `@` shown before UDF names in modern Excel formulas is normal implicit
intersection syntax and was not the issue.
Next session:
- Decide whether the currency parameter should remain a transaction-currency
filter or become a requested output currency with conversion.
- Add an explicit workbook refresh button or visible refresh instructions for
`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.
## Recommended Production Shape
For production, prefer a readonly DB user and an ODBC DSN. The VBA module is a
minimal proof of concept; a production workbook should add:
- a settings sheet for connection parameters;
- refresh/cache controls;
- error messages visible to users;
- optional Power Query preload for large reports.

View File

@@ -0,0 +1,235 @@
-- Finaccount-like Excel API POC for GL ACCOUNTS FUNCTIONS.
--
-- Scope:
-- - GetAccountBaseAmount
-- - GetAccountRealBaseAmount
-- - GetAccountAmount
-- - GetAccountAbbr
-- - GetAccountName
-- - GetAccountContact
--
-- Design notes:
-- - PostgreSQL lowercases function names; VBA calls these SQL functions.
-- - Balances are computed as of p_value_date.
-- - This POC uses posted account moves only.
-- - p_currency_iso is treated as the transaction currency.
-- - p_company_key accepts either the company party code or company name.
-- - p_currency_iso accepts the currency code/name/symbol/numeric code/id.
-- - If account.move.line.second_currency is empty, the company currency is
-- used as transaction currency and the company amount is reused.
create schema if not exists excel_api;
create or replace view excel_api.gl_account_lines as
select
aml.id as move_line_id,
am.id as move_id,
cc.id as company_id,
cp.code as company_key,
cp.name as company_name,
aa.id as account_id,
aa.code as account_code,
aa.name as account_name,
coalesce(tx_cur.code, base_cur.code) as transaction_currency_code,
coalesce(aml.second_currency, cc.currency) as transaction_currency_id,
base_cur.code as base_currency_code,
cc.currency as base_currency_id,
am.date as posting_date,
am.state as posting_status,
aml.party as party_id,
party.code as party_code,
party.name as party_name,
coalesce(aml.debit, 0) as debit_base_currency,
coalesce(aml.credit, 0) as credit_base_currency,
coalesce(aml.debit, 0) - coalesce(aml.credit, 0)
as balance_base_currency,
case
when aml.second_currency is not null then
coalesce(aml.amount_second_currency, 0)
else
coalesce(aml.debit, 0) - coalesce(aml.credit, 0)
end as balance_transaction_currency
from account_move_line aml
join account_move am on am.id = aml.move
join account_account aa on aa.id = aml.account
join company_company cc on cc.id = am.company
left join party_party cp on cp.id = cc.party
left join currency_currency base_cur on base_cur.id = cc.currency
left join currency_currency tx_cur on tx_cur.id = aml.second_currency
left join party_party party on party.id = aml.party
where aa.type is not null
and coalesce(aa.closed, false) is false;
create or replace function excel_api.currency_rate_at(
p_currency_id integer,
p_value_date date)
returns numeric
language sql
stable
as $$
select cr.rate
from currency_currency_rate cr
where cr.currency = p_currency_id
and cr.date <= p_value_date
order by cr.date desc
limit 1
$$;
create or replace function excel_api.company_matches(
p_company_key text,
p_company_input text)
returns boolean
language sql
stable
as $$
select upper(btrim(coalesce(p_company_key, '')))
= upper(btrim(coalesce(p_company_input, '')))
or upper(btrim(coalesce(p_company_input, ''))) = (
select upper(btrim(coalesce(pp.name, '')))
from company_company cc
left join party_party pp on pp.id = cc.party
where pp.code = p_company_key
limit 1)
$$;
create or replace function excel_api.currency_matches(
p_currency_id integer,
p_currency_input text)
returns boolean
language sql
stable
as $$
select exists (
select 1
from currency_currency cur
where cur.id = p_currency_id
and upper(btrim(coalesce(p_currency_input, ''))) in (
upper(btrim(cur.id::text)),
upper(btrim(coalesce(cur.code, ''))),
upper(btrim(coalesce(cur.name, ''))),
upper(btrim(coalesce(cur.symbol, ''))),
upper(btrim(coalesce(cur.numeric_code, '')))))
$$;
create or replace function excel_api.get_account_amount(
p_company_key text,
p_account_code text,
p_currency_iso text,
p_value_date date)
returns numeric
language sql
stable
as $$
select coalesce(sum(gl.balance_transaction_currency), 0)
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'
$$;
create or replace function excel_api.get_account_real_base_amount(
p_company_key text,
p_account_code text,
p_currency_iso text,
p_value_date date)
returns numeric
language sql
stable
as $$
select coalesce(sum(gl.balance_base_currency), 0)
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'
$$;
create or replace function excel_api.get_account_base_amount(
p_company_key text,
p_account_code text,
p_currency_iso text,
p_value_date date)
returns numeric
language sql
stable
as $$
select coalesce(sum(
case
when gl.transaction_currency_id = gl.base_currency_id then
gl.balance_transaction_currency
else
gl.balance_transaction_currency
* excel_api.currency_rate_at(gl.base_currency_id, p_value_date)
/ nullif(
excel_api.currency_rate_at(
gl.transaction_currency_id, p_value_date),
0)
end), 0)
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'
$$;
create or replace function excel_api.get_account_abbr(
p_company_key text,
p_account_code text,
p_currency_iso text,
p_value_date date)
returns text
language sql
stable
as $$
select gl.account_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
order by gl.posting_date desc, gl.move_line_id desc
limit 1
$$;
create or replace function excel_api.get_account_name(
p_company_key text,
p_account_code text,
p_currency_iso text,
p_value_date date)
returns text
language sql
stable
as $$
select gl.account_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
order by gl.posting_date desc, gl.move_line_id desc
limit 1
$$;
create or replace function excel_api.get_account_contact(
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
$$;