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 0000000..82c5059 Binary files /dev/null and b/notes/accounting/excel_api/GL_Accounts_POC.xlsx differ 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 0000000..3f4e183 Binary files /dev/null and b/notes/accounting/excel_api/GL_Accounts_POC_with_formulas.xlsm differ 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 0000000..dd5334b Binary files /dev/null and b/notes/accounting/excel_api/GL_Accounts_POC_with_formulas.xlsx differ diff --git a/notes/accounting/excel_api/README_GL_ACCOUNTS_POC.md b/notes/accounting/excel_api/README_GL_ACCOUNTS_POC.md new file mode 100644 index 0000000..ff9d46d --- /dev/null +++ b/notes/accounting/excel_api/README_GL_ACCOUNTS_POC.md @@ -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 : `) 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 +$$;