From 0fffb341fe25eedc128b1ba14aa82161ab0d7f4b Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Tue, 26 May 2026 18:54:07 +0200 Subject: [PATCH] Go to matching --- modules/purchase_trade/lot.py | 98 ++++++-- modules/purchase_trade/tests/test_module.py | 71 ++++++ .../excel_api/GLAccountsExcelApi.bas | 178 +++++++++++++ .../accounting/excel_api/GL_Accounts_POC.xlsx | Bin 0 -> 5652 bytes .../GL_Accounts_POC_with_formulas.xlsm | Bin 0 -> 24696 bytes .../GL_Accounts_POC_with_formulas.xlsx | Bin 0 -> 9808 bytes .../excel_api/README_GL_ACCOUNTS_POC.md | 111 +++++++++ .../excel_api/gl_accounts_sql_poc.sql | 235 ++++++++++++++++++ 8 files changed, 667 insertions(+), 26 deletions(-) create mode 100644 notes/accounting/excel_api/GLAccountsExcelApi.bas create mode 100644 notes/accounting/excel_api/GL_Accounts_POC.xlsx create mode 100644 notes/accounting/excel_api/GL_Accounts_POC_with_formulas.xlsm create mode 100644 notes/accounting/excel_api/GL_Accounts_POC_with_formulas.xlsx create mode 100644 notes/accounting/excel_api/README_GL_ACCOUNTS_POC.md create mode 100644 notes/accounting/excel_api/gl_accounts_sql_poc.sql diff --git a/modules/purchase_trade/lot.py b/modules/purchase_trade/lot.py index ea4052a..4318f9a 100755 --- a/modules/purchase_trade/lot.py +++ b/modules/purchase_trade/lot.py @@ -1328,8 +1328,13 @@ class LotQt( qt_p = Decimal(str(lp.lot_matched_qt or 0)) if qt_p <= 0: continue - lqt = LotQt(lp.lot_r_id) - available = cls._matching_tolerated_available(lqt, 'purchase') + lqt = LotQt(lp.lot_r_id) if lp.lot_r_id else None + if lqt: + available = cls._matching_tolerated_available(lqt, 'purchase') + else: + lot = Lot(lp.lot_id) + available = Decimal(str( + lot.get_current_quantity_converted() or 0)) if qt_p > available: raise UserError( "Quantity to match exceeds the available purchase " @@ -1348,8 +1353,8 @@ class LotQt( qt_p = lp.lot_matched_qt if qt_p == 0: continue - lqt = LotQt(lp.lot_r_id) - if not lqt.lot_p or lqt.lot_s: + lqt = LotQt(lp.lot_r_id) if lp.lot_r_id else None + if lqt and (not lqt.lot_p or lqt.lot_s): raise UserError( "Please select only unmatched purchase quantities.") for ls in lot_s: @@ -1368,28 +1373,29 @@ class LotQt( raise UserError( "Please select only unmatched sale quantities.") if l.lot_type == 'physic': - Sale = Pool().get('sale.sale') - s = Sale(ls.lot_sale) - ll = s.lines[0].lots[0] - #Decrease forecasted virtual parts matched - vlot_p = l.getVlot_p() - vlot_s = ll.getVlot_s() - logger.info("SALEVIRTUALLOT:%s",vlot_p) - logger.info("SALEVIRTUALLOT2:%s",vlot_s) + ll = lqt2.lot_s + #Decrease forecasted virtual parts matched + vlot_p = l.getVlot_p() + vlot_s = ll.getVlot_s() + logger.info("SALEVIRTUALLOT:%s",vlot_p) + logger.info("SALEVIRTUALLOT2:%s",vlot_s) #l.updateVirtualPart(-qt,l.lot_shipment_origin,l.getVlot_s()) - #Increase forecasted virtual part non matched - #if not l.updateVirtualPart(qt,l.lot_shipment_origin,None): - # l.createVirtualPart(qt,l.lot_shipment_origin,None) - l.updateVirtualPart(-qt,None,ll,'only sale') - lqt.lot_s = ll - lqt.lot_av = 'reserved' - LotQt.save([lqt]) - l.sale_line = s.lines[0] - Lot.save([l]) - #ll.lot_gross_quantity -= qt - ll.lot_quantity -= qt - ll.lot_qt -= float(qt) - Lot.save([ll]) + #Increase forecasted virtual part non matched + #if not l.updateVirtualPart(qt,l.lot_shipment_origin,None): + # l.createVirtualPart(qt,l.lot_shipment_origin,None) + l.updateVirtualPart(-qt,None,ll,'only sale') + if lqt: + lqt.lot_s = ll + lqt.lot_av = 'reserved' + LotQt.save([lqt]) + else: + l.createVirtualPart(qt, None, ll) + l.sale_line = ll.sale_line + Lot.save([l]) + #ll.lot_gross_quantity -= qt + ll.lot_quantity -= qt + ll.lot_qt -= float(qt) + Lot.save([ll]) else: #Increase forecasted virtual part matched if not l.updateVirtualPart(qt,lqt.lot_shipment_origin,lqt2.lot_s): @@ -2938,13 +2944,45 @@ class LotMatching(Wizard): values.update(LotQt._matching_line_tolerance_info(trade_line)) return values + @classmethod + def _physical_purchase_matching_value(cls, lot): + line = getattr(lot, 'line', None) + purchase = getattr(line, 'purchase', None) + quantity = Decimal(str(lot.get_current_quantity_converted() or 0)) + values = { + 'lot_id': cls._record_id(lot), + 'lot_r_id': None, + 'lot_purchase': cls._record_id(purchase), + 'lot_sale': None, + 'lot_shipment_in': cls._record_id( + getattr(lot, 'lot_shipment_in', None)), + 'lot_shipment_internal': cls._record_id( + getattr(lot, 'lot_shipment_internal', None)), + 'lot_shipment_out': cls._record_id( + getattr(lot, 'lot_shipment_out', None)), + 'lot_type': getattr(lot, 'lot_type', None), + 'lot_product': cls._record_id(getattr(lot, 'lot_product', None)), + 'lot_quantity': quantity, + 'lot_matched_qt': 0, + 'lot_already_matched_qt': 0, + 'lot_cp': cls._record_id(getattr(purchase, 'party', None)), + } + values.update(LotQt._matching_line_tolerance_info(line)) + return values + @classmethod def _selected_matching_defaults(cls, active_ids): LotQt = Pool().get('lot.qt') + Lot = Pool().get('lot.lot') lot_p = [] lot_s = [] for active_id in active_ids or []: if active_id <= 10000000: + lot = Lot(active_id) + if (getattr(lot, 'lot_type', None) == 'physic' + and getattr(lot, 'line', None) + and not getattr(lot, 'sale_line', None)): + lot_p.append(cls._physical_purchase_matching_value(lot)) continue lqt = LotQt(active_id - 10000000) if lqt.lot_p and lqt.lot_s: @@ -2961,12 +2999,12 @@ class LotMatching(Wizard): def transition_matching(self): Warning = Pool().get('res.user.warning') LotQt = Pool().get('lot.qt') + Lot = Pool().get('lot.lot') if self.match.tot_p != self.match.tot_s: warning_name = Warning.format("Quantity's issue", []) if Warning.check(warning_name): raise QtWarning(warning_name, "Quantities not compatibles") - Lot = Pool().get('lot.lot') with Lot.skip_quantity_consistency(): LotQt.match_lots(self.match.lot_p,self.match.lot_s) affected_lines = [] @@ -3011,10 +3049,18 @@ class LotGoMatching(Wizard): context = Transaction().context values = LotMatching._selected_matching_defaults( context.get('active_ids') or []) + if not values.get('lot_p') or not values.get('lot_s'): + raise UserError( + "Please select at least one purchase line and one sale line " + "before using Go to matching.") values['qt_type'] = 'all' return values def transition_matching(self): + if not self.match.lot_p or not self.match.lot_s: + raise UserError( + "Please select at least one purchase line and one sale line " + "before using Go to matching.") return LotMatching.transition_matching(self) def end(self): diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index ebbc9f5..90e6bdf 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -3417,6 +3417,77 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertEqual(result['lot_s'][0]['lot_qt_max'], Decimal('80.00000')) self.assertEqual(result['lot_s'][0]['lot_cp'], 40) + def test_go_matching_defaults_selected_physical_purchase_lot(self): + 'go to matching preloads selected unmatched physical purchase lots' + purchase = Mock(id=10, party=Mock(id=20)) + sale = Mock(id=30, party=Mock(id=40)) + product = Mock(id=50) + physical_lot = Mock( + id=101, + line=Mock( + purchase=purchase, inherit_tol=False, + quantity_theorical=Decimal('100'), + targeted_qt=Decimal('96'), tol_min=0, tol_max=0), + sale_line=None, + lot_type='physic', + lot_product=product, + lot_shipment_in=None, + lot_shipment_internal=None, + lot_shipment_out=None) + physical_lot.get_current_quantity_converted.return_value = ( + Decimal('70')) + sale_lot = Mock( + id=102, line=None, + sale_line=Mock( + sale=sale, inherit_tol=False, + quantity_theorical=Decimal('80'), + targeted_qt=Decimal('84'), tol_min=0, tol_max=0), + lot_type='virtual', lot_product=product) + sale_lqt = Mock( + id=2, lot_p=None, lot_s=sale_lot, + lot_quantity=Decimal('80'), + lot_shipment_in=None, lot_shipment_internal=None, + lot_shipment_out=None) + + class LotQtMock: + def __call__(self, _id): + return sale_lqt + + class LotMock: + def __call__(self, _id): + return physical_lot + + pool = Mock() + pool.get.side_effect = ( + lambda name: LotQtMock() if name == 'lot.qt' else LotMock()) + transaction = Mock() + transaction.context = { + 'active_ids': [101, 10000002], + } + + with patch.object(lot_module, 'Pool', return_value=pool): + with patch.object( + lot_module, 'Transaction', return_value=transaction): + result = lot_module.LotGoMatching().default_match([]) + + self.assertEqual(len(result['lot_p']), 1) + self.assertEqual(len(result['lot_s']), 1) + self.assertEqual(result['lot_p'][0]['lot_id'], 101) + self.assertIsNone(result['lot_p'][0]['lot_r_id']) + self.assertEqual(result['lot_p'][0]['lot_type'], 'physic') + self.assertEqual(result['lot_p'][0]['lot_quantity'], Decimal('70')) + self.assertEqual(result['lot_p'][0]['lot_targeted_qt'], Decimal('96')) + self.assertEqual(result['lot_p'][0]['lot_cp'], 20) + self.assertEqual(result['lot_s'][0]['lot_r_id'], 2) + + def test_go_matching_requires_purchase_and_sale_selection(self): + 'go to matching requires one purchase and one sale row' + wizard = lot_module.LotGoMatching() + wizard.match = Mock(lot_p=[], lot_s=[Mock()]) + + with self.assertRaises(UserError): + wizard.transition_matching() + def test_purchase_tolerance_used_is_weighted_line_average(self): 'purchase tolerance gauge averages line gauges by theoretical quantity' unit = Mock() diff --git a/notes/accounting/excel_api/GLAccountsExcelApi.bas b/notes/accounting/excel_api/GLAccountsExcelApi.bas new file mode 100644 index 0000000..06599c8 --- /dev/null +++ b/notes/accounting/excel_api/GLAccountsExcelApi.bas @@ -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 diff --git a/notes/accounting/excel_api/GL_Accounts_POC.xlsx b/notes/accounting/excel_api/GL_Accounts_POC.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..82c505976c172d832b09cd49e404cc3dbbf659bf GIT binary patch literal 5652 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 literal 0 HcmV?d00001 diff --git a/notes/accounting/excel_api/GL_Accounts_POC_with_formulas.xlsm b/notes/accounting/excel_api/GL_Accounts_POC_with_formulas.xlsm new file mode 100644 index 0000000000000000000000000000000000000000..3f4e183591a81d1d4733685e88aae193d8335298 GIT binary patch literal 24696 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 literal 0 HcmV?d00001 diff --git a/notes/accounting/excel_api/GL_Accounts_POC_with_formulas.xlsx b/notes/accounting/excel_api/GL_Accounts_POC_with_formulas.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..dd5334b44d2da53b8e2d4b43ae6f99c1692b56b6 GIT binary patch literal 9808 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: `) 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. diff --git a/notes/accounting/excel_api/gl_accounts_sql_poc.sql b/notes/accounting/excel_api/gl_accounts_sql_poc.sql new file mode 100644 index 0000000..5b6377f --- /dev/null +++ b/notes/accounting/excel_api/gl_accounts_sql_poc.sql @@ -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 +$$;