Bug fee cog

This commit is contained in:
2026-05-29 08:07:09 +02:00
parent a9b048d4cf
commit 83f7e43a6e
16 changed files with 768 additions and 8 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
*.pyc
notes/accounting/excel_web_api/.env

View File

@@ -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):

View File

@@ -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'))

View File

@@ -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"/>

View 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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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"

View File

@@ -0,0 +1,5 @@
DB_HOST=vps107.geneva.hosting
DB_PORT=5432
DB_NAME=tradon
DB_USER=tradon_readonly
DB_PASSWORD=change_me

View 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)
+ "&currency=" + 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;
}

View 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)
+ "&currency=" + 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")

View 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>

View File

@@ -0,0 +1,4 @@
fastapi==0.115.0
uvicorn[standard]==0.30.0
psycopg2-binary==2.9.9
python-dotenv==1.0.1