Bug fee cog
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
*.pyc
|
||||
notes/accounting/excel_web_api/.env
|
||||
|
||||
@@ -216,9 +216,10 @@ class Fee(ModelSQL,ModelView):
|
||||
('origin', 'ilike', '%stock.move%'),
|
||||
])
|
||||
|
||||
logger.info("GET_COG_FEE:%s",ml)
|
||||
if ml:
|
||||
return round(Decimal(sum([e.credit-e.debit for e in ml if e.description != 'Delivery fee'])),2)
|
||||
logger.info("GET_COG_FEE:%s",ml)
|
||||
if ml:
|
||||
return round(Decimal(sum([e.credit-e.debit for e in ml if e.description != 'Delivery fee'])),2)
|
||||
return Decimal(0)
|
||||
|
||||
def get_non_cog(self,lot):
|
||||
MoveLine = Pool().get('account.move.line')
|
||||
@@ -233,9 +234,10 @@ class Fee(ModelSQL,ModelView):
|
||||
('account', '=', self.product.account_stock_in_used.id),
|
||||
])
|
||||
|
||||
logger.info("GET_NON_COG_FEE:%s",ml)
|
||||
if ml:
|
||||
return round(Decimal(sum([e.credit-e.debit for e in ml])),2)
|
||||
logger.info("GET_NON_COG_FEE:%s",ml)
|
||||
if ml:
|
||||
return round(Decimal(sum([e.credit-e.debit for e in ml])),2)
|
||||
return Decimal(0)
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
|
||||
@@ -1181,6 +1181,46 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
'fee currency is required for every fee mode'
|
||||
self.assertTrue(fee_module.Fee.currency.required)
|
||||
|
||||
def test_fee_get_non_cog_returns_zero_without_move_lines(self):
|
||||
'fee non-cog amount is zero before any accounting move line exists'
|
||||
fee = fee_module.Fee()
|
||||
fee.id = 1
|
||||
fee.product = Mock(account_stock_in_used=Mock(id=2))
|
||||
lot = Mock(id=3)
|
||||
move_line = Mock()
|
||||
move_line.search.return_value = []
|
||||
|
||||
with patch('trytond.modules.purchase_trade.fee.Pool') as PoolMock:
|
||||
PoolMock.return_value.get.side_effect = lambda name: {
|
||||
'account.move.line': move_line,
|
||||
'currency.currency': Mock(),
|
||||
'ir.date': Mock(),
|
||||
'account.configuration': Mock(return_value=Mock()),
|
||||
'product.uom': Mock(),
|
||||
}[name]
|
||||
|
||||
self.assertEqual(fee.get_non_cog(lot), Decimal(0))
|
||||
|
||||
def test_fee_get_cog_returns_zero_without_move_lines(self):
|
||||
'fee cog amount is zero before any stock accounting move line exists'
|
||||
fee = fee_module.Fee()
|
||||
fee.id = 1
|
||||
fee.product = Mock(account_stock_in_used=Mock(id=2))
|
||||
lot = Mock(id=3)
|
||||
move_line = Mock()
|
||||
move_line.search.return_value = []
|
||||
|
||||
with patch('trytond.modules.purchase_trade.fee.Pool') as PoolMock:
|
||||
PoolMock.return_value.get.side_effect = lambda name: {
|
||||
'account.move.line': move_line,
|
||||
'currency.currency': Mock(),
|
||||
'ir.date': Mock(),
|
||||
'account.configuration': Mock(return_value=Mock()),
|
||||
'product.uom': Mock(),
|
||||
}[name]
|
||||
|
||||
self.assertEqual(fee.get_cog(lot), Decimal(0))
|
||||
|
||||
def test_valuation_rejects_fee_without_currency_cleanly(self):
|
||||
'pnl generation reports a missing fee currency before currency conversion'
|
||||
fee = Mock(currency=None, product=Mock(name='Maritime freight'))
|
||||
|
||||
@@ -73,7 +73,7 @@ this repository contains the full copyright notices and license terms. -->
|
||||
</page>
|
||||
<page string="Fees" col="4" id="fees">
|
||||
<button name="apply_default_fees" icon="tryton-launch"/>
|
||||
<field name="fees" view_ids="purchase_trade.fee_view_tree_sequence"/>
|
||||
<field name="fees" colspan="4" view_ids="purchase_trade.fee_view_tree_sequence"/>
|
||||
</page>
|
||||
<page string="Mtm" col="4" id="mtm">
|
||||
<field name="mtm" colspan="4"/>
|
||||
|
||||
71
notes/accounting/excel_api/CLIENT_DELIVERY_README.md
Normal file
71
notes/accounting/excel_api/CLIENT_DELIVERY_README.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# GL Accounts Excel API - Client Delivery
|
||||
|
||||
## Recommended Files
|
||||
|
||||
Send these files to the client:
|
||||
|
||||
- `GL_Accounts_Client_Template.xlsx`
|
||||
- `GLAccountsExcelApi.bas`
|
||||
- `gl_accounts_sql_poc.sql`
|
||||
- `GL_Accounts_Functions_Documentation.pdf`
|
||||
|
||||
## Why This Package Is Stable
|
||||
|
||||
`GL_Accounts_Client_Template.xlsx` is macro-free and contains no active UDF
|
||||
formulas. It does not open ODBC connections when the workbook starts, so Excel
|
||||
can load it normally even before the client database connection is configured.
|
||||
|
||||
The previous `.xlsm` proof-of-concept workbooks are not recommended for client
|
||||
delivery because Microsoft 365 may inspect or recalculate embedded VBA/UDF
|
||||
formulas while opening the workbook. That can block Excel at `Opening ... 0%`
|
||||
if ODBC, macro trust, or the database is not ready.
|
||||
|
||||
## Client Setup Flow
|
||||
|
||||
1. Apply `gl_accounts_sql_poc.sql` on the target database.
|
||||
2. Create a PostgreSQL ODBC System DSN, for example `TradonGLApi`, then
|
||||
configure `GL_API_CONNECTION_STRING` in `GLAccountsExcelApi.bas` as
|
||||
`DSN=TradonGLApi;Uid=tradon_readonly;Pwd=...;`.
|
||||
3. Import `GLAccountsExcelApi.bas` into Excel VBA, or deploy it through the
|
||||
client's approved add-in/macro process.
|
||||
4. Open `GL_Accounts_Client_Template.xlsx`.
|
||||
5. On the `Functions` sheet, run `InstallGLApiFormulas` to mark result cells
|
||||
as ready. This does not write live Excel formulas.
|
||||
6. Run `RefreshGLApi` to call the database, write plain result values, then
|
||||
disable database calls again.
|
||||
|
||||
## Implemented Excel Functions
|
||||
|
||||
- `GetAccountBaseAmount`
|
||||
- `GetAccountRealBaseAmount`
|
||||
- `GetAccountAmount`
|
||||
- `GetAccountAbbr`
|
||||
- `GetAccountName`
|
||||
- `GetAccountContact`
|
||||
- `GetAccountContactCode`
|
||||
- `GetAccountContactName`
|
||||
|
||||
All functions use:
|
||||
|
||||
```excel
|
||||
(companyKey, accountCode, currencyIso, valueDate)
|
||||
```
|
||||
|
||||
## Operational Notes
|
||||
|
||||
- `currencyIso` currently filters the transaction currency. It is not yet an
|
||||
output conversion currency.
|
||||
- `GetAccountContact` is kept for backward compatibility and returns party
|
||||
code first, name fallback.
|
||||
- `GetAccountContactCode` returns party code first.
|
||||
- `GetAccountContactName` returns party name first.
|
||||
- The VBA module starts with database calls disabled, so copy/paste cannot
|
||||
open ODBC connections by itself.
|
||||
- `InstallGLApiFormulas` prepares result cells without creating live
|
||||
`GetAccount...` worksheet formulas.
|
||||
- `RefreshGLApi` enables database calls only during the macro execution, clears
|
||||
the cache, evaluates each formula text, writes plain values, then disables
|
||||
database calls again.
|
||||
- `ClearGLApiFormulas` removes any live formulas from result cells and replaces
|
||||
them with `#GLAPI_OFF`.
|
||||
- `DisableGLApi` turns database calls off again and closes the ODBC connection.
|
||||
BIN
notes/accounting/excel_api/GL_Accounts_Client_Delivery.zip
Normal file
BIN
notes/accounting/excel_api/GL_Accounts_Client_Delivery.zip
Normal file
Binary file not shown.
BIN
notes/accounting/excel_api/GL_Accounts_Client_Template.xlsm
Normal file
BIN
notes/accounting/excel_api/GL_Accounts_Client_Template.xlsm
Normal file
Binary file not shown.
@@ -0,0 +1,117 @@
|
||||
# GL Accounts Excel Functions
|
||||
|
||||
This document lists the Excel functions currently implemented in the GL
|
||||
Accounts proof of concept.
|
||||
|
||||
## Common Parameters
|
||||
|
||||
All functions use the same four parameters:
|
||||
|
||||
- `companyKey`: company party code or company party name, for example
|
||||
`ICT TRADING`.
|
||||
- `accountCode`: general ledger account code, for example `20011`.
|
||||
- `currencyIso`: transaction currency filter. In the current database this may
|
||||
be the currency id, code, name, symbol, or numeric code, for example `1`,
|
||||
`USD`, or `EUR`.
|
||||
- `valueDate`: accounting cutoff date. Only posted moves up to this date are
|
||||
included.
|
||||
|
||||
Example parameter shape:
|
||||
|
||||
```excel
|
||||
=GetAccountAmount("ICT TRADING","20011","USD",DATE(2026,5,13))
|
||||
```
|
||||
|
||||
## Implemented Functions
|
||||
|
||||
### GetAccountBaseAmount
|
||||
|
||||
```excel
|
||||
=GetAccountBaseAmount(companyKey, accountCode, currencyIso, valueDate)
|
||||
```
|
||||
|
||||
Returns the account balance converted to the company base currency using the
|
||||
currency rate at `valueDate`.
|
||||
|
||||
### GetAccountRealBaseAmount
|
||||
|
||||
```excel
|
||||
=GetAccountRealBaseAmount(companyKey, accountCode, currencyIso, valueDate)
|
||||
```
|
||||
|
||||
Returns the posted balance already stored in company base currency.
|
||||
|
||||
### GetAccountAmount
|
||||
|
||||
```excel
|
||||
=GetAccountAmount(companyKey, accountCode, currencyIso, valueDate)
|
||||
```
|
||||
|
||||
Returns the posted balance in transaction currency for the selected currency
|
||||
filter.
|
||||
|
||||
### GetAccountAbbr
|
||||
|
||||
```excel
|
||||
=GetAccountAbbr(companyKey, accountCode, currencyIso, valueDate)
|
||||
```
|
||||
|
||||
Returns the account abbreviation. In this POC it returns the account code
|
||||
because no separate GL abbreviation field is available.
|
||||
|
||||
### GetAccountName
|
||||
|
||||
```excel
|
||||
=GetAccountName(companyKey, accountCode, currencyIso, valueDate)
|
||||
```
|
||||
|
||||
Returns the general ledger account name.
|
||||
|
||||
### GetAccountContact
|
||||
|
||||
```excel
|
||||
=GetAccountContact(companyKey, accountCode, currencyIso, valueDate)
|
||||
```
|
||||
|
||||
Backward-compatible contact formula. It returns contact codes when available,
|
||||
otherwise contact names.
|
||||
|
||||
### GetAccountContactCode
|
||||
|
||||
```excel
|
||||
=GetAccountContactCode(companyKey, accountCode, currencyIso, valueDate)
|
||||
```
|
||||
|
||||
Returns a comma-separated list of contact party codes found on posted GL lines.
|
||||
If a party has no code, its name is used as fallback.
|
||||
|
||||
### GetAccountContactName
|
||||
|
||||
```excel
|
||||
=GetAccountContactName(companyKey, accountCode, currencyIso, valueDate)
|
||||
```
|
||||
|
||||
Returns a comma-separated list of contact party names found on posted GL lines.
|
||||
If a party has no name, its code is used as fallback.
|
||||
|
||||
## Operational Notes
|
||||
|
||||
- Only posted account moves are included.
|
||||
- The currency parameter is currently a transaction-currency filter, not an
|
||||
output currency selector.
|
||||
- The VBA module caches results. Use `ClearGLApiCache` after changing database
|
||||
data or SQL functions.
|
||||
- Database calls are disabled after importing the module and after each
|
||||
refresh. Formula cells return `#GLAPI_OFF` outside `RefreshGLApi`.
|
||||
- Use `InstallGLApiFormulas` on the `Functions` sheet to install formulas from
|
||||
formula text into result cells as inert placeholders. Do not remove leading
|
||||
apostrophes manually in Excel's formula bar.
|
||||
- Use `ClearGLApiFormulas` if a previous workbook version already contains
|
||||
live `GetAccount...` worksheet formulas.
|
||||
- Use `CloseGLApiConnection` to force Excel to reopen the ODBC connection.
|
||||
- Use `RefreshGLApi` to enable database calls only during that refresh, clear
|
||||
the cache, evaluate formula text, and write plain values.
|
||||
- Use `DisableGLApi` before editing/copying large formula areas if Excel should
|
||||
not contact the database.
|
||||
- `GetAccountContact` remains available so existing workbook formulas keep
|
||||
working.
|
||||
@@ -0,0 +1,101 @@
|
||||
%PDF-1.4
|
||||
1 0 obj
|
||||
<< /Type /Catalog /Pages 2 0 R >>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Type /Pages /Kids [5 0 R 7 0 R] /Count 2 >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
|
||||
endobj
|
||||
4 0 obj
|
||||
<< /Length 2717 >>
|
||||
stream
|
||||
BT
|
||||
/F1 18 Tf 1 0 0 1 50 800 Tm (GL Accounts Excel Functions) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 766 Tm (This document lists the Excel functions currently implemented in the GL) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 752 Tm (Accounts proof of concept.) Tj
|
||||
/F1 14 Tf 1 0 0 1 50 728 Tm (Common Parameters) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 698 Tm (All functions use the same four parameters:) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 674 Tm (- `companyKey`: company party code or company party name, for example) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 660 Tm ( `ICT TRADING`.) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 646 Tm (- `accountCode`: general ledger account code, for example `20011`.) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 632 Tm (- `currencyIso`: transaction currency filter. In the current database this may) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 618 Tm ( be the currency id, code, name, symbol, or numeric code, for example `1`,) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 604 Tm ( `USD`, or `EUR`.) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 590 Tm (- `valueDate`: accounting cutoff date. Only posted moves up to this date are) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 576 Tm ( included.) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 552 Tm (Example parameter shape:) Tj
|
||||
/F1 9 Tf 1 0 0 1 65 528 Tm (=GetAccountAmount\("ICT TRADING","20011","USD",DATE\(2026,5,13\)\)) Tj
|
||||
/F1 14 Tf 1 0 0 1 50 504 Tm (Implemented Functions) Tj
|
||||
/F1 12 Tf 1 0 0 1 50 474 Tm (GetAccountBaseAmount) Tj
|
||||
/F1 9 Tf 1 0 0 1 65 447 Tm (=GetAccountBaseAmount\(companyKey, accountCode, currencyIso, valueDate\)) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 423 Tm (Returns the account balance converted to the company base currency using the) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 409 Tm (currency rate at `valueDate`.) Tj
|
||||
/F1 12 Tf 1 0 0 1 50 385 Tm (GetAccountRealBaseAmount) Tj
|
||||
/F1 9 Tf 1 0 0 1 65 358 Tm (=GetAccountRealBaseAmount\(companyKey, accountCode, currencyIso, valueDate\)) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 334 Tm (Returns the posted balance already stored in company base currency.) Tj
|
||||
/F1 12 Tf 1 0 0 1 50 310 Tm (GetAccountAmount) Tj
|
||||
/F1 9 Tf 1 0 0 1 65 283 Tm (=GetAccountAmount\(companyKey, accountCode, currencyIso, valueDate\)) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 259 Tm (Returns the posted balance in transaction currency for the selected currency) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 245 Tm (filter.) Tj
|
||||
/F1 12 Tf 1 0 0 1 50 221 Tm (GetAccountAbbr) Tj
|
||||
/F1 9 Tf 1 0 0 1 65 194 Tm (=GetAccountAbbr\(companyKey, accountCode, currencyIso, valueDate\)) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 170 Tm (Returns the account abbreviation. In this POC it returns the account code) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 156 Tm (because no separate GL abbreviation field is available.) Tj
|
||||
/F1 12 Tf 1 0 0 1 50 132 Tm (GetAccountName) Tj
|
||||
/F1 9 Tf 1 0 0 1 65 105 Tm (=GetAccountName\(companyKey, accountCode, currencyIso, valueDate\)) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 81 Tm (Returns the general ledger account name.) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
5 0 obj
|
||||
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R >> >> /Contents 4 0 R >>
|
||||
endobj
|
||||
6 0 obj
|
||||
<< /Length 1880 >>
|
||||
stream
|
||||
BT
|
||||
/F1 12 Tf 1 0 0 1 50 790 Tm (GetAccountContact) Tj
|
||||
/F1 9 Tf 1 0 0 1 65 763 Tm (=GetAccountContact\(companyKey, accountCode, currencyIso, valueDate\)) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 739 Tm (Backward-compatible contact formula. It returns contact codes when available,) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 725 Tm (otherwise contact names.) Tj
|
||||
/F1 12 Tf 1 0 0 1 50 701 Tm (GetAccountContactCode) Tj
|
||||
/F1 9 Tf 1 0 0 1 65 674 Tm (=GetAccountContactCode\(companyKey, accountCode, currencyIso, valueDate\)) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 650 Tm (Returns a comma-separated list of contact party codes found on posted GL lines.) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 636 Tm (If a party has no code, its name is used as fallback.) Tj
|
||||
/F1 12 Tf 1 0 0 1 50 612 Tm (GetAccountContactName) Tj
|
||||
/F1 9 Tf 1 0 0 1 65 585 Tm (=GetAccountContactName\(companyKey, accountCode, currencyIso, valueDate\)) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 561 Tm (Returns a comma-separated list of contact party names found on posted GL lines.) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 547 Tm (If a party has no name, its code is used as fallback.) Tj
|
||||
/F1 14 Tf 1 0 0 1 50 523 Tm (Operational Notes) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 493 Tm (- Only posted account moves are included.) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 479 Tm (- The currency parameter is currently a transaction-currency filter, not an) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 465 Tm ( output currency selector.) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 451 Tm (- The VBA module caches results. Use `ClearGLApiCache` after changing database) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 437 Tm ( data or SQL functions.) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 423 Tm (- Use `CloseGLApiConnection` to force Excel to reopen the ODBC connection.) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 409 Tm (- Use `RefreshGLApi` to clear the cache and calculate the active sheet.) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 395 Tm (- `GetAccountContact` remains available so existing workbook formulas keep) Tj
|
||||
/F1 10 Tf 1 0 0 1 50 381 Tm ( working.) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
7 0 obj
|
||||
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R >> >> /Contents 6 0 R >>
|
||||
endobj
|
||||
xref
|
||||
0 8
|
||||
0000000000 65535 f
|
||||
0000000009 00000 n
|
||||
0000000058 00000 n
|
||||
0000000121 00000 n
|
||||
0000000191 00000 n
|
||||
0000002960 00000 n
|
||||
0000003086 00000 n
|
||||
0000005018 00000 n
|
||||
trailer
|
||||
<< /Size 8 /Root 1 0 R >>
|
||||
startxref
|
||||
5144
|
||||
%%EOF
|
||||
Binary file not shown.
@@ -1,2 +1,2 @@
|
||||
@echo off
|
||||
start "" "C:\Program Files\Microsoft Office\root\Office16\EXCEL.EXE" /safe "%~dp0GL_Accounts_Client_Template.xlsx"
|
||||
start "" "C:\Program Files\Microsoft Office\root\Office16\EXCEL.EXE" /safe "%~dp0GL_Accounts_Client_Template.xlsm"
|
||||
|
||||
5
notes/accounting/excel_web_api/.env.example
Normal file
5
notes/accounting/excel_web_api/.env.example
Normal file
@@ -0,0 +1,5 @@
|
||||
DB_HOST=vps107.geneva.hosting
|
||||
DB_PORT=5432
|
||||
DB_NAME=tradon
|
||||
DB_USER=tradon_readonly
|
||||
DB_PASSWORD=change_me
|
||||
109
notes/accounting/excel_web_api/RefreshGLApi.ts
Normal file
109
notes/accounting/excel_web_api/RefreshGLApi.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* GL Accounts refresh — Office Script for Excel Web.
|
||||
*
|
||||
* Setup:
|
||||
* 1. Start the Python API: uvicorn main:app --port 8000
|
||||
* 2. Expose via ngrok: ngrok http 8000
|
||||
* 3. Paste the ngrok URL in API_BASE below.
|
||||
* 4. In Excel Web: Automate tab > New Script > paste > Save > Run.
|
||||
*
|
||||
* Sheet layout (GL_Accounts_Client_Template.xlsx):
|
||||
* Setup!B6 = Company Functions!C6 = get_account_base_amount
|
||||
* Setup!B7 = Account Functions!C7 = get_account_real_base_amount
|
||||
* Setup!B8 = Currency Functions!C8 = get_account_amount
|
||||
* Setup!B9 = Date Functions!C9 = get_account_abbr
|
||||
* Functions!C10 = get_account_name
|
||||
* Functions!C11 = get_account_contact
|
||||
* Functions!C12 = get_account_contact_code
|
||||
* Functions!C13 = get_account_contact_name
|
||||
*/
|
||||
|
||||
interface GlResults {
|
||||
get_account_base_amount: number;
|
||||
get_account_real_base_amount: number;
|
||||
get_account_amount: number;
|
||||
get_account_abbr: string;
|
||||
get_account_name: string;
|
||||
get_account_contact: string;
|
||||
get_account_contact_code: string;
|
||||
get_account_contact_name: string;
|
||||
}
|
||||
|
||||
interface GlResponse {
|
||||
results: GlResults;
|
||||
errors: { [key: string]: string };
|
||||
}
|
||||
|
||||
async function main(workbook: ExcelScript.Workbook): Promise<void> {
|
||||
// ── CONFIG ─────────────────────────────────────────────────────────────────
|
||||
const API_BASE = "https://obscure-boggle-sprig.ngrok-free.dev";
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const setupSheet = workbook.getWorksheet("Setup");
|
||||
const functionsSheet = workbook.getWorksheet("Functions");
|
||||
|
||||
if (!setupSheet || !functionsSheet) {
|
||||
console.log("Error: 'Setup' or 'Functions' sheet not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const company: string = String(setupSheet.getRange("B6").getValue());
|
||||
const account: string = String(setupSheet.getRange("B7").getValue());
|
||||
const currency: string = String(setupSheet.getRange("B8").getValue());
|
||||
const date: string = toIsoDate(setupSheet.getRange("B9").getValue());
|
||||
|
||||
// Mark as loading
|
||||
for (let row = 6; row <= 13; row++) {
|
||||
functionsSheet.getRange("C" + row).setValue("...");
|
||||
}
|
||||
|
||||
const url = API_BASE + "/gl/all"
|
||||
+ "?company=" + encodeURIComponent(company)
|
||||
+ "&account=" + encodeURIComponent(account)
|
||||
+ "¤cy=" + encodeURIComponent(currency)
|
||||
+ "&date=" + encodeURIComponent(date);
|
||||
|
||||
let data: GlResponse;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: { "ngrok-skip-browser-warning": "true" }
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("HTTP " + response.status);
|
||||
}
|
||||
data = (await response.json()) as GlResponse;
|
||||
} catch (err) {
|
||||
console.log("API call failed: " + err);
|
||||
for (let row = 6; row <= 13; row++) {
|
||||
functionsSheet.getRange("C" + row).setValue("#API_ERROR");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const r = data.results;
|
||||
functionsSheet.getRange("C6").setValue(r.get_account_base_amount !== null ? r.get_account_base_amount : "");
|
||||
functionsSheet.getRange("C7").setValue(r.get_account_real_base_amount !== null ? r.get_account_real_base_amount : "");
|
||||
functionsSheet.getRange("C8").setValue(r.get_account_amount !== null ? r.get_account_amount : "");
|
||||
functionsSheet.getRange("C9").setValue(r.get_account_abbr !== null ? r.get_account_abbr : "");
|
||||
functionsSheet.getRange("C10").setValue(r.get_account_name !== null ? r.get_account_name : "");
|
||||
functionsSheet.getRange("C11").setValue(r.get_account_contact !== null ? r.get_account_contact : "");
|
||||
functionsSheet.getRange("C12").setValue(r.get_account_contact_code !== null ? r.get_account_contact_code : "");
|
||||
functionsSheet.getRange("C13").setValue(r.get_account_contact_name !== null ? r.get_account_contact_name : "");
|
||||
|
||||
console.log("Refreshed: " + company + " / " + account + " / " + currency + " / " + date);
|
||||
}
|
||||
|
||||
function toIsoDate(value: string | number | boolean): string {
|
||||
if (typeof value === "number") {
|
||||
// Excel date serial → YYYY-MM-DD
|
||||
const d = new Date(Date.UTC(1899, 11, 30) + value * 86400000);
|
||||
return d.toISOString().split("T")[0];
|
||||
}
|
||||
const s = String(value);
|
||||
const d = new Date(s);
|
||||
if (!isNaN(d.getTime())) {
|
||||
return d.toISOString().split("T")[0];
|
||||
}
|
||||
return s;
|
||||
}
|
||||
262
notes/accounting/excel_web_api/main.py
Normal file
262
notes/accounting/excel_web_api/main.py
Normal file
@@ -0,0 +1,262 @@
|
||||
"""
|
||||
GL Accounts API — backend for Excel Web Office Script.
|
||||
|
||||
Architecture:
|
||||
Excel Web (Office Script) → HTTP GET /gl/all → this API → PostgreSQL on vps107.geneva.hosting
|
||||
|
||||
Run locally:
|
||||
pip install -r requirements.txt
|
||||
uvicorn main:app --reload --port 8000
|
||||
|
||||
Expose for Excel Web (Office Scripts run on Microsoft servers, need a public URL):
|
||||
ngrok http 8000
|
||||
→ copy the https://xxxx.ngrok.io URL into the Office Script API_BASE constant
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Query, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import HTMLResponse, PlainTextResponse
|
||||
from dotenv import load_dotenv
|
||||
import psycopg2
|
||||
import os
|
||||
|
||||
load_dotenv()
|
||||
|
||||
app = FastAPI(title="GL Accounts API")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # Excel Web / Office Scripts need open CORS for POC
|
||||
allow_methods=["GET"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
DB_CONFIG = {
|
||||
"host": os.getenv("DB_HOST", "vps107.geneva.hosting"),
|
||||
"port": int(os.getenv("DB_PORT", "5432")),
|
||||
"dbname": os.getenv("DB_NAME", "tradon"),
|
||||
"user": os.getenv("DB_USER", "tradon_readonly"),
|
||||
"password": os.getenv("DB_PASSWORD", "change_me"),
|
||||
"connect_timeout": 10,
|
||||
}
|
||||
|
||||
# Ordered list used by the Office Script to map rows 6-13 in the Functions sheet
|
||||
GL_FUNCTIONS = [
|
||||
"get_account_base_amount",
|
||||
"get_account_real_base_amount",
|
||||
"get_account_amount",
|
||||
"get_account_abbr",
|
||||
"get_account_name",
|
||||
"get_account_contact",
|
||||
"get_account_contact_code",
|
||||
"get_account_contact_name",
|
||||
]
|
||||
|
||||
|
||||
def run_query(function_name: str, company: str, account: str, currency: str, value_date: str):
|
||||
with psycopg2.connect(**DB_CONFIG) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
f"SELECT excel_api.{function_name}(%s, %s, %s, %s::date)",
|
||||
(company, account, currency, value_date),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return row[0] if row and row[0] is not None else None
|
||||
|
||||
|
||||
@app.get("/gl/all")
|
||||
def get_all(
|
||||
company: str = Query(..., description="Company party code or name"),
|
||||
account: str = Query(..., description="GL account code"),
|
||||
currency: str = Query(..., description="Currency code / name / id"),
|
||||
date: str = Query(..., description="Cutoff date YYYY-MM-DD"),
|
||||
):
|
||||
"""
|
||||
Returns all 8 GL function results in one round-trip.
|
||||
Called by the Office Script RefreshGLApi.ts.
|
||||
"""
|
||||
if function_name_invalid := [f for f in GL_FUNCTIONS if f not in GL_FUNCTIONS]:
|
||||
raise HTTPException(400, f"Unknown function: {function_name_invalid}")
|
||||
|
||||
results: dict = {}
|
||||
errors: dict = {}
|
||||
|
||||
for fn in GL_FUNCTIONS:
|
||||
try:
|
||||
results[fn] = run_query(fn, company, account, currency, date)
|
||||
except Exception as e:
|
||||
results[fn] = None
|
||||
errors[fn] = str(e)
|
||||
|
||||
return {"results": results, "errors": errors or None}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
try:
|
||||
with psycopg2.connect(**DB_CONFIG) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT 1")
|
||||
return {"status": "ok", "db": "connected"}
|
||||
except Exception as e:
|
||||
raise HTTPException(503, f"DB unreachable: {e}")
|
||||
|
||||
|
||||
# ── Excel Custom Functions Add-in ────────────────────────────────────────────
|
||||
|
||||
ADDIN_GUID = "3F4A8B2C-9D1E-4F5A-B6C7-8D9E0F1A2B3C"
|
||||
|
||||
|
||||
@app.get("/addin/icon.png")
|
||||
def addin_icon():
|
||||
# Minimal 1x1 green PNG
|
||||
import base64
|
||||
png = base64.b64decode(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
||||
)
|
||||
from fastapi.responses import Response
|
||||
return Response(content=png, media_type="image/png")
|
||||
|
||||
|
||||
@app.get("/addin/manifest.xml", response_class=PlainTextResponse)
|
||||
def addin_manifest(request: Request):
|
||||
base = str(request.base_url).rstrip("/")
|
||||
xml = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0"
|
||||
xsi:type="TaskPaneApp">
|
||||
<Id>{ADDIN_GUID}</Id>
|
||||
<Version>1.0.0.0</Version>
|
||||
<ProviderName>Tradon</ProviderName>
|
||||
<DefaultLocale>en-US</DefaultLocale>
|
||||
<DisplayName DefaultValue="Tradon GL API"/>
|
||||
<Description DefaultValue="GL account functions backed by PostgreSQL"/>
|
||||
<IconUrl DefaultValue="{base}/addin/icon.png"/>
|
||||
<HighResolutionIconUrl DefaultValue="{base}/addin/icon.png"/>
|
||||
<SupportUrl DefaultValue="https://vps107.geneva.hosting"/>
|
||||
<AppDomains>
|
||||
<AppDomain>{base}</AppDomain>
|
||||
</AppDomains>
|
||||
<Hosts>
|
||||
<Host Name="Workbook"/>
|
||||
</Hosts>
|
||||
<DefaultSettings>
|
||||
<SourceLocation DefaultValue="{base}/addin/taskpane.html"/>
|
||||
</DefaultSettings>
|
||||
<Permissions>ReadWriteDocument</Permissions>
|
||||
<VersionOverrides xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides"
|
||||
xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0"
|
||||
xsi:type="VersionOverridesV1_0">
|
||||
<Hosts>
|
||||
<Host xsi:type="Workbook">
|
||||
<AllFormFactors>
|
||||
<ExtensionPoint xsi:type="CustomFunctions">
|
||||
<Script>
|
||||
<SourceLocation resid="Functions.Script.Url"/>
|
||||
</Script>
|
||||
<Page>
|
||||
<SourceLocation resid="Functions.Page.Url"/>
|
||||
</Page>
|
||||
<Metadata>
|
||||
<SourceLocation resid="Functions.Metadata.Url"/>
|
||||
</Metadata>
|
||||
<Namespace resid="Functions.Namespace"/>
|
||||
</ExtensionPoint>
|
||||
</AllFormFactors>
|
||||
</Host>
|
||||
</Hosts>
|
||||
<Resources>
|
||||
<bt:Urls>
|
||||
<bt:Url id="Functions.Script.Url" DefaultValue="{base}/addin/functions.js"/>
|
||||
<bt:Url id="Functions.Page.Url" DefaultValue="{base}/addin/functions.html"/>
|
||||
<bt:Url id="Functions.Metadata.Url" DefaultValue="{base}/addin/functions.json"/>
|
||||
</bt:Urls>
|
||||
<bt:ShortStrings>
|
||||
<bt:String id="Functions.Namespace" DefaultValue="TRADON"/>
|
||||
</bt:ShortStrings>
|
||||
</Resources>
|
||||
</VersionOverrides>
|
||||
</OfficeApp>"""
|
||||
return PlainTextResponse(content=xml, media_type="application/xml")
|
||||
|
||||
|
||||
@app.get("/addin/functions.html", response_class=HTMLResponse)
|
||||
def addin_functions_html():
|
||||
return HTMLResponse("""<!DOCTYPE html><html><head>
|
||||
<script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
|
||||
<script src="/addin/functions.js"></script>
|
||||
</head><body></body></html>""")
|
||||
|
||||
|
||||
@app.get("/addin/taskpane.html", response_class=HTMLResponse)
|
||||
def addin_taskpane():
|
||||
return HTMLResponse("""<!DOCTYPE html><html><head>
|
||||
<script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
|
||||
</head><body><p>Tradon GL API Add-in loaded.</p></body></html>""")
|
||||
|
||||
|
||||
@app.get("/addin/functions.json")
|
||||
def addin_functions_json():
|
||||
functions = [
|
||||
("GET_ACCOUNT_BASE_AMOUNT", "get_account_base_amount", "Balance converted to base currency"),
|
||||
("GET_ACCOUNT_REAL_BASE_AMOUNT", "get_account_real_base_amount", "Posted balance in base currency"),
|
||||
("GET_ACCOUNT_AMOUNT", "get_account_amount", "Balance in transaction currency"),
|
||||
("GET_ACCOUNT_ABBR", "get_account_abbr", "Account abbreviation/code"),
|
||||
("GET_ACCOUNT_NAME", "get_account_name", "Account name"),
|
||||
("GET_ACCOUNT_CONTACT", "get_account_contact", "Contact (code or name)"),
|
||||
("GET_ACCOUNT_CONTACT_CODE", "get_account_contact_code", "Contact party codes"),
|
||||
("GET_ACCOUNT_CONTACT_NAME", "get_account_contact_name", "Contact party names"),
|
||||
]
|
||||
params = [
|
||||
{"name": "company", "dimensionality": "scalar", "type": "string"},
|
||||
{"name": "account", "dimensionality": "scalar", "type": "string"},
|
||||
{"name": "currency", "dimensionality": "scalar", "type": "string"},
|
||||
{"name": "date", "dimensionality": "scalar", "type": "string"},
|
||||
]
|
||||
return {
|
||||
"functions": [
|
||||
{"id": fn_id, "name": fn_id, "description": desc,
|
||||
"parameters": params, "result": {"dimensionality": "scalar", "type": "string"}}
|
||||
for fn_id, _, desc in functions
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@app.get("/addin/functions.js", response_class=PlainTextResponse)
|
||||
def addin_functions_js(request: Request):
|
||||
base = str(request.base_url).rstrip("/")
|
||||
js = f"""/* Tradon GL Custom Functions — auto-generated, base={base} */
|
||||
const _API = "{base}";
|
||||
const _HDR = {{"ngrok-skip-browser-warning": "true"}};
|
||||
|
||||
async function _gl(fn, company, account, currency, date) {{
|
||||
const url = _API + "/gl/all?company=" + encodeURIComponent(company)
|
||||
+ "&account=" + encodeURIComponent(account)
|
||||
+ "¤cy=" + encodeURIComponent(currency)
|
||||
+ "&date=" + encodeURIComponent(date);
|
||||
const r = await fetch(url, {{headers: _HDR}});
|
||||
if (!r.ok) throw new Error("HTTP " + r.status);
|
||||
const d = await r.json();
|
||||
const v = d.results[fn];
|
||||
return v !== null && v !== undefined ? v : "";
|
||||
}}
|
||||
|
||||
CustomFunctions.associate("GET_ACCOUNT_BASE_AMOUNT",
|
||||
(c,a,u,d) => _gl("get_account_base_amount", c,a,u,d));
|
||||
CustomFunctions.associate("GET_ACCOUNT_REAL_BASE_AMOUNT",
|
||||
(c,a,u,d) => _gl("get_account_real_base_amount", c,a,u,d));
|
||||
CustomFunctions.associate("GET_ACCOUNT_AMOUNT",
|
||||
(c,a,u,d) => _gl("get_account_amount", c,a,u,d));
|
||||
CustomFunctions.associate("GET_ACCOUNT_ABBR",
|
||||
(c,a,u,d) => _gl("get_account_abbr", c,a,u,d));
|
||||
CustomFunctions.associate("GET_ACCOUNT_NAME",
|
||||
(c,a,u,d) => _gl("get_account_name", c,a,u,d));
|
||||
CustomFunctions.associate("GET_ACCOUNT_CONTACT",
|
||||
(c,a,u,d) => _gl("get_account_contact", c,a,u,d));
|
||||
CustomFunctions.associate("GET_ACCOUNT_CONTACT_CODE",
|
||||
(c,a,u,d) => _gl("get_account_contact_code", c,a,u,d));
|
||||
CustomFunctions.associate("GET_ACCOUNT_CONTACT_NAME",
|
||||
(c,a,u,d) => _gl("get_account_contact_name", c,a,u,d));
|
||||
"""
|
||||
return PlainTextResponse(content=js, media_type="application/javascript")
|
||||
48
notes/accounting/excel_web_api/manifest.xml
Normal file
48
notes/accounting/excel_web_api/manifest.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:type="TaskPaneApp">
|
||||
<Id>3F4A8B2C-9D1E-4F5A-B6C7-8D9E0F1A2B3C</Id>
|
||||
<Version>1.0.0.0</Version>
|
||||
<ProviderName>Tradon</ProviderName>
|
||||
<DefaultLocale>en-US</DefaultLocale>
|
||||
<DisplayName DefaultValue="Tradon GL API"/>
|
||||
<Description DefaultValue="GL account functions backed by PostgreSQL"/>
|
||||
<Hosts>
|
||||
<Host Name="Workbook"/>
|
||||
</Hosts>
|
||||
<DefaultSettings>
|
||||
<SourceLocation DefaultValue="https://obscure-boggle-sprig.ngrok-free.dev/addin/taskpane.html"/>
|
||||
</DefaultSettings>
|
||||
<Permissions>ReadWriteDocument</Permissions>
|
||||
<VersionOverrides xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="VersionOverridesV1_0">
|
||||
<Hosts>
|
||||
<Host xsi:type="Workbook">
|
||||
<AllFormFactors>
|
||||
<ExtensionPoint xsi:type="CustomFunctions">
|
||||
<Script>
|
||||
<SourceLocation resid="Functions.Script.Url"/>
|
||||
</Script>
|
||||
<Page>
|
||||
<SourceLocation resid="Functions.Page.Url"/>
|
||||
</Page>
|
||||
<Metadata>
|
||||
<SourceLocation resid="Functions.Metadata.Url"/>
|
||||
</Metadata>
|
||||
<Namespace resid="Functions.Namespace"/>
|
||||
</ExtensionPoint>
|
||||
</AllFormFactors>
|
||||
</Host>
|
||||
</Hosts>
|
||||
<Resources>
|
||||
<bt:Urls>
|
||||
<bt:Url id="Functions.Script.Url" DefaultValue="https://obscure-boggle-sprig.ngrok-free.dev/addin/functions.js"/>
|
||||
<bt:Url id="Functions.Page.Url" DefaultValue="https://obscure-boggle-sprig.ngrok-free.dev/addin/functions.html"/>
|
||||
<bt:Url id="Functions.Metadata.Url" DefaultValue="https://obscure-boggle-sprig.ngrok-free.dev/addin/functions.json"/>
|
||||
</bt:Urls>
|
||||
<bt:ShortStrings>
|
||||
<bt:String id="Functions.Namespace" DefaultValue="TRADON"/>
|
||||
</bt:ShortStrings>
|
||||
</Resources>
|
||||
</VersionOverrides>
|
||||
</OfficeApp>
|
||||
4
notes/accounting/excel_web_api/requirements.txt
Normal file
4
notes/accounting/excel_web_api/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
fastapi==0.115.0
|
||||
uvicorn[standard]==0.30.0
|
||||
psycopg2-binary==2.9.9
|
||||
python-dotenv==1.0.1
|
||||
Reference in New Issue
Block a user