diff --git a/MIGRATION_GUIDE_v2.0.0.md b/MIGRATION_GUIDE_v2.0.0.md
new file mode 100644
index 0000000..eb4db6b
--- /dev/null
+++ b/MIGRATION_GUIDE_v2.0.0.md
@@ -0,0 +1,248 @@
+# Migration Guide - Supplier Invoice Enhancements (v2.0.0)
+
+## 🎯 Hvad migreres:
+
+### Database Changes:
+- âś… `supplier_invoice_lines`: Nye kolonner (contra_account, line_purpose, resale_customer_id, resale_order_number)
+- âś… `economic_accounts`: Ny tabel til e-conomic kontoplan cache
+
+### Backend Changes:
+- âś… e-conomic accounts API integration
+- âś… Line item update endpoint med modkonto support
+
+### Frontend Changes:
+- ✅ 3 nye faneblade (Til Betaling, Klar til Bogføring, Varelinjer)
+- âś… Inline redigering af modkonto og formĂĄl
+- âś… Backup version pĂĄ /billing/supplier-invoices2
+
+---
+
+## đź“‹ Pre-Migration Checklist:
+
+- [ ] Commit alle ændringer til git
+- [ ] Test pĂĄ lokal udvikling fungerer
+- [ ] Backup af production database
+- [ ] Tag ny version (v2.0.0)
+- [ ] Push til Gitea
+
+---
+
+## 🚀 Migration Steps:
+
+### Step 1: Commit og Tag Release
+
+```bash
+cd /Users/christianthomas/DEV/bmc_hub_dev
+
+# Commit ændringer
+git add .
+git commit -m "Supplier invoice enhancements v2.0.0
+
+- Added modkonto (contra_account) support per line
+- Added line_purpose tracking (resale, internal, project, stock)
+- Added e-conomic accounts API integration
+- Redesigned frontend with 3 tabs: Payment, Ready for Booking, Line Items
+- Database migration 1000 included
+- Backup version available at /billing/supplier-invoices2"
+
+# Opdater VERSION fil
+echo "2.0.0" > VERSION
+
+git add VERSION
+git commit -m "Bump version to 2.0.0"
+
+# Tag release
+git tag v2.0.0
+git push origin main
+git push origin v2.0.0
+```
+
+### Step 2: Backup Production Database
+
+```bash
+# SSH til production
+ssh bmcadmin@172.16.31.183
+
+# Backup database
+cd /srv/podman/bmc_hub_v1.0
+podman exec bmc-hub-postgres-prod pg_dump -U bmc_hub bmc_hub > backup_pre_v2.0.0_$(date +%Y%m%d_%H%M%S).sql
+
+# Verificer backup
+ls -lh backup_pre_v2.0.0_*.sql
+```
+
+### Step 3: Deploy ny Version
+
+Fra lokal Mac:
+
+```bash
+cd /Users/christianthomas/DEV/bmc_hub_dev
+
+# Kør deployment script
+./deploy_to_prod.sh v2.0.0
+```
+
+Dette script:
+1. Opdaterer RELEASE_VERSION i .env
+2. Stopper containers
+3. Bygger nyt image fra Gitea tag v2.0.0
+4. Starter containers igen
+
+### Step 4: Kør Migration på Production
+
+```bash
+# SSH til production
+ssh bmcadmin@172.16.31.183
+cd /srv/podman/bmc_hub_v1.0
+
+# Kør migration SQL
+podman exec -i bmc-hub-postgres-prod psql -U bmc_hub -d bmc_hub < migrations/1000_supplier_invoice_enhancements.sql
+
+# ELLER hvis migrationen ikke er mounted:
+# Kopier migration til container først:
+podman cp migrations/1000_supplier_invoice_enhancements.sql bmc-hub-postgres-prod:/tmp/migration.sql
+podman exec bmc-hub-postgres-prod psql -U bmc_hub -d bmc_hub -f /tmp/migration.sql
+```
+
+### Step 5: Sync e-conomic Accounts
+
+```bash
+# Trigger initial sync af kontoplan
+curl -X GET "http://172.16.31.183:8001/api/v1/supplier-invoices/economic/accounts?refresh=true"
+
+# Verificer at konti er cached
+curl -s "http://172.16.31.183:8001/api/v1/supplier-invoices/economic/accounts" | jq '.accounts | length'
+# Skal returnere antal konti (fx 20)
+```
+
+### Step 6: Verificer Migration
+
+```bash
+# Tjek database kolonner
+podman exec bmc-hub-postgres-prod psql -U bmc_hub -d bmc_hub -c "\d supplier_invoice_lines"
+# Skal vise: contra_account, line_purpose, resale_customer_id, resale_order_number
+
+# Tjek economic_accounts tabel
+podman exec bmc-hub-postgres-prod psql -U bmc_hub -d bmc_hub -c "SELECT COUNT(*) FROM economic_accounts;"
+# Skal returnere antal accounts (fx 20)
+
+# Test frontend
+# Ă…bn: http://172.16.31.183:8001/billing/supplier-invoices
+# Skal vise: Til Betaling, Klar til Bogføring, Varelinjer tabs
+
+# Test backup version
+# Ă…bn: http://172.16.31.183:8001/billing/supplier-invoices2
+# Skal vise: Original version med Fakturaer, Mangler Behandling tabs
+```
+
+---
+
+## 🔄 Rollback Plan (hvis noget går galt):
+
+### Option 1: Rollback til forrige version
+
+```bash
+ssh bmcadmin@172.16.31.183
+cd /srv/podman/bmc_hub_v1.0
+
+# Opdater til forrige version (fx v1.3.123)
+sed -i 's/^RELEASE_VERSION=.*/RELEASE_VERSION=v1.3.123/' .env
+
+# Rebuild og restart
+podman-compose down
+podman-compose build --no-cache
+podman-compose up -d
+```
+
+### Option 2: Restore database backup
+
+```bash
+ssh bmcadmin@172.16.31.183
+cd /srv/podman/bmc_hub_v1.0
+
+# Stop API for at undgå data ændringer
+podman stop bmc-hub-api-prod
+
+# Restore database
+podman exec -i bmc-hub-postgres-prod psql -U bmc_hub -d bmc_hub < backup_pre_v2.0.0_XXXXXXXX.sql
+
+# Restart API
+podman start bmc-hub-api-prod
+```
+
+---
+
+## 📊 Post-Migration Validation:
+
+### Test Cases:
+
+1. **Upload Invoice**
+ - Upload PDF faktura
+ - Verificer Quick Analysis virker
+ - Tjek vendor auto-match
+
+2. **Process Invoice**
+ - Klik "Behandl" pĂĄ uploaded fil
+ - Verificer template extraction
+ - Tjek at linjer oprettes
+
+3. **Assign Modkonto**
+ - GĂĄ til "Varelinjer" tab
+ - Vælg modkonto fra dropdown (skal vise 20 konti)
+ - Vælg formål (Videresalg, Internt, osv.)
+ - Gem og verificer
+
+4. **Check Ready for Booking**
+ - Gå til "Klar til Bogføring" tab
+ - Skal kun vise fakturaer hvor ALLE linjer har modkonto
+ - Test "Send til e-conomic" knap
+
+5. **Payment View**
+ - GĂĄ til "Til Betaling" tab
+ - Verificer sortering efter forfaldsdato
+ - Test bulk selection
+
+---
+
+## 🎯 Success Criteria:
+
+- ✅ Migration SQL kørt uden fejl
+- âś… 20+ e-conomic accounts cached i database
+- âś… Nye faneblade vises korrekt
+- âś… Modkonto dropdown virker
+- âś… Inline editing af linjer fungerer
+- ✅ Backup version tilgængelig på /supplier-invoices2
+- âś… Send til e-conomic virker med nye modkonti
+
+---
+
+## ⚠️ Known Issues & Workarounds:
+
+### Issue 1: Accounts endpoint timeout
+**Symptom**: Første kald til accounts endpoint er langsomt (2-3 sek)
+**Reason**: Første gang syncer fra e-conomic API
+**Workaround**: Pre-trigger sync efter deployment (Step 5)
+
+### Issue 2: Eksisterende fakturaer har ingen modkonto
+**Symptom**: Gamle fakturaer vises ikke i "Klar til Bogføring"
+**Expected**: Kun nye fakturaer (efter migration) vil have modkonti
+**Solution**: Manuel assignment via "Varelinjer" tab for gamle fakturaer hvis nødvendigt
+
+### Issue 3: Browser cache
+**Symptom**: Gamle faneblade vises stadig
+**Solution**: Ctrl+Shift+R (hard refresh) i browser
+
+---
+
+## 📞 Support:
+
+Ved problemer, tjek:
+1. Container logs: `podman logs bmc-hub-api-prod --tail 100`
+2. Database logs: `podman logs bmc-hub-postgres-prod --tail 100`
+3. Migration status: `podman exec bmc-hub-postgres-prod psql -U bmc_hub -d bmc_hub -c "SELECT * FROM economic_accounts LIMIT 5;"`
+
+---
+
+**Version**: 2.0.0
+**Date**: 2026-01-07
+**Migration File**: 1000_supplier_invoice_enhancements.sql
diff --git a/app/billing/backend/supplier_invoices.py b/app/billing/backend/supplier_invoices.py
index 732bae2..c8c7b3e 100644
--- a/app/billing/backend/supplier_invoices.py
+++ b/app/billing/backend/supplier_invoices.py
@@ -329,9 +329,11 @@ async def get_file_pdf_text(file_id: int):
if not file_info:
raise HTTPException(status_code=404, detail="Fil ikke fundet")
+ file_data = file_info[0]
+
# Read PDF text
from pathlib import Path
- file_path = Path(file_info['file_path'])
+ file_path = Path(file_data['file_path'])
if not file_path.exists():
raise HTTPException(status_code=404, detail=f"Fil ikke fundet pĂĄ disk: {file_path}")
@@ -339,7 +341,7 @@ async def get_file_pdf_text(file_id: int):
return {
"file_id": file_id,
- "filename": file_info['filename'],
+ "filename": file_data['filename'],
"pdf_text": pdf_text
}
@@ -372,15 +374,23 @@ async def get_file_extracted_data(file_id: int):
if extraction and extraction.get('llm_response_json'):
import json
try:
- llm_json_data = json.loads(extraction['llm_response_json']) if isinstance(extraction['llm_response_json'], str) else extraction['llm_response_json']
+ raw_json = extraction['llm_response_json']
+ # Always parse if it's a string, even if psycopg2 returns it as JSON type
+ if isinstance(raw_json, str):
+ llm_json_data = json.loads(raw_json)
+ elif isinstance(raw_json, dict):
+ llm_json_data = raw_json
+ else:
+ # Fallback: try to parse as string
+ llm_json_data = json.loads(str(raw_json))
logger.info(f"📊 Parsed llm_response_json: invoice_number={llm_json_data.get('invoice_number')}")
except Exception as e:
- logger.warning(f"⚠️ Failed to parse llm_response_json: {e}")
+ logger.error(f"❌ Failed to parse llm_response_json: {e}")
- # Get extraction lines if exist
+ # Get extraction lines if exist (use execute_query for multiple rows)
extraction_lines = []
if extraction:
- extraction_lines = execute_query_single(
+ extraction_lines = execute_query(
"""SELECT * FROM extraction_lines
WHERE extraction_id = %s
ORDER BY line_number""",
@@ -485,13 +495,14 @@ async def download_pending_file(file_id: int):
try:
# Get file info
- file_info = execute_query(
+ file_result = execute_query(
"SELECT * FROM incoming_files WHERE file_id = %s",
(file_id,))
- if not file_info:
+ if not file_result:
raise HTTPException(status_code=404, detail="Fil ikke fundet")
+ file_info = file_result[0] # Get first row
file_path = Path(file_info['file_path'])
if not file_path.exists():
raise HTTPException(status_code=404, detail="Fil findes ikke pĂĄ disk")
@@ -1145,16 +1156,18 @@ async def create_template(request: Dict):
async def get_supplier_invoice(invoice_id: int):
"""Get single supplier invoice with lines"""
try:
- invoice = execute_query(
+ invoice_result = execute_query(
"""SELECT si.*, v.name as vendor_full_name, v.economic_supplier_number as vendor_economic_id
FROM supplier_invoices si
LEFT JOIN vendors v ON si.vendor_id = v.id
WHERE si.id = %s""",
(invoice_id,))
- if not invoice:
+ if not invoice_result:
raise HTTPException(status_code=404, detail=f"Invoice {invoice_id} not found")
+ invoice = invoice_result[0]
+
# Get lines
lines = execute_query_single(
"SELECT * FROM supplier_invoice_lines WHERE supplier_invoice_id = %s ORDER BY line_number",
@@ -1332,6 +1345,76 @@ async def update_supplier_invoice(invoice_id: int, data: Dict):
raise HTTPException(status_code=500, detail=str(e))
+@router.patch("/supplier-invoices/{invoice_id}/lines/{line_id}")
+async def update_invoice_line(invoice_id: int, line_id: int, data: Dict):
+ """
+ Update supplier invoice line item
+
+ Supports updating: contra_account, line_purpose, resale_customer_id, resale_order_number
+ """
+ try:
+ # Check if invoice exists and is not sent to e-conomic
+ invoice = execute_query(
+ "SELECT id, status FROM supplier_invoices WHERE id = %s",
+ (invoice_id,))
+
+ if not invoice:
+ raise HTTPException(status_code=404, detail=f"Invoice {invoice_id} not found")
+
+ invoice_data = invoice[0]
+
+ # Don't allow editing if already sent to e-conomic
+ if invoice_data['status'] == 'sent_to_economic':
+ raise HTTPException(
+ status_code=400,
+ detail="Cannot edit invoice line that has been sent to e-conomic"
+ )
+
+ # Check if line exists
+ line = execute_query(
+ "SELECT id FROM supplier_invoice_lines WHERE id = %s AND supplier_invoice_id = %s",
+ (line_id, invoice_id))
+
+ if not line:
+ raise HTTPException(status_code=404, detail=f"Line {line_id} not found in invoice {invoice_id}")
+
+ # Build update query
+ update_fields = []
+ params = []
+
+ allowed_fields = ['contra_account', 'line_purpose', 'resale_customer_id',
+ 'resale_order_number', 'description', 'quantity',
+ 'unit_price', 'vat_rate', 'total_amount']
+
+ for field in allowed_fields:
+ if field in data:
+ update_fields.append(f"{field} = %s")
+ params.append(data[field])
+
+ if not update_fields:
+ raise HTTPException(status_code=400, detail="No fields to update")
+
+ params.append(line_id)
+
+ query = f"""
+ UPDATE supplier_invoice_lines
+ SET {', '.join(update_fields)}
+ WHERE id = %s
+ """
+
+ execute_update(query, tuple(params))
+
+ logger.info(f"âś… Updated invoice line {line_id} (Invoice {invoice_id})")
+
+ return {"success": True, "line_id": line_id}
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"❌ Failed to update invoice line {line_id}: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
@router.delete("/supplier-invoices/{invoice_id}")
async def delete_supplier_invoice(invoice_id: int):
"""Delete supplier invoice (soft delete if integrated with e-conomic)"""
@@ -1582,6 +1665,65 @@ async def get_economic_journals():
raise HTTPException(status_code=500, detail=str(e))
+@router.get("/supplier-invoices/economic/accounts")
+async def get_economic_accounts(refresh: bool = False):
+ """
+ Get e-conomic chart of accounts (kontoplan) from cache
+
+ Args:
+ refresh: If True, fetch fresh data from e-conomic API
+
+ Returns:
+ List of accounts with accountNumber, name, accountType, vatCode
+ """
+ try:
+ # If refresh requested, sync from e-conomic first
+ if refresh:
+ economic = get_economic_service()
+ count = await economic.sync_accounts_to_database()
+ logger.info(f"âś… Refreshed {count} accounts from e-conomic")
+
+ # Fetch from database cache
+ accounts = execute_query("""
+ SELECT
+ account_number as "accountNumber",
+ name,
+ account_type as "accountType",
+ vat_code as "vatCode",
+ balance,
+ last_synced as "lastSynced"
+ FROM economic_accounts
+ WHERE is_active = TRUE
+ ORDER BY account_number
+ """)
+
+ # If no accounts in cache and not already refreshed, try syncing
+ if not accounts and not refresh:
+ economic = get_economic_service()
+ count = await economic.sync_accounts_to_database()
+ logger.info(f"âś… Initial sync: {count} accounts from e-conomic")
+
+ # Retry fetch
+ accounts = execute_query("""
+ SELECT
+ account_number as "accountNumber",
+ name,
+ account_type as "accountType",
+ vat_code as "vatCode",
+ balance,
+ last_synced as "lastSynced"
+ FROM economic_accounts
+ WHERE is_active = TRUE
+ ORDER BY account_number
+ """)
+
+ return {"accounts": accounts}
+
+ except Exception as e:
+ logger.error(f"❌ Failed to get e-conomic accounts: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
# ========== STATISTICS & REPORTS ==========
@router.get("/supplier-invoices/stats/overview")
@@ -1877,7 +2019,8 @@ async def upload_supplier_invoice(file: UploadFile = File(...)):
"""INSERT INTO extractions
(file_id, vendor_matched_id, document_id, document_date, due_date,
total_amount, currency, document_type, confidence, llm_response_json, status)
- VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 'extracted')""",
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 'extracted')
+ RETURNING extraction_id""",
(file_id, vendor_id,
extracted_fields.get('invoice_number'),
extracted_fields.get('invoice_date'),
diff --git a/app/billing/frontend/supplier_invoices.html b/app/billing/frontend/supplier_invoices.html
index eb33b2b..b46103b 100644
--- a/app/billing/frontend/supplier_invoices.html
+++ b/app/billing/frontend/supplier_invoices.html
@@ -1,6 +1,6 @@
{% extends "shared/frontend/base.html" %}
-{% block title %}Kassekladde - BMC Hub{% endblock %}
+{% block title %}Leverandør fakturaer - BMC Hub{% endblock %}
{% block extra_css %}
+{% endblock %}
+
+{% block content %}
+
+
+
+
📋 Leverandørfakturaer
+
Kassekladde - Integration med e-conomic
+
+
+
+
+
+
+
+
+
+
-
+
Forfald inden 7 dage
+
-
+
+
+
+
+
-
+
Afventer behandling
+
-
+
+
+
+
+
-
+
Ubetalt i alt
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Alle
+
+
+ Afventer
+
+
+ Godkendt
+
+
+ Sendt til e-conomic
+
+
+ Overskredet
+
+
+
+
+
+
+
+
+
+ 0 fakturaer valgt
+
+
+
+ Send til Kassekladde
+
+
+ Reset
+
+
+ Marker Betalt
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
⏳ Filer der mangler behandling
+
+ Opdater
+
+
+
+
+
+
+
+ 0 filer valgt
+
+
+
+ Opret Fakturaer
+
+
+ Genbehandle
+
+
+ Slet
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AI-Analyse: Systemet vil automatisk udtrække fakturadata (CVR, beløb, linjer) fra PDF/billede via AI.
+ Leverandør matches automatisk via CVR nummer.
+
+
+
+
+
Vælg filer (PDF, PNG, JPG) *
+
+
Max 50 MB pr. fil. Vælg flere filer med Cmd/Ctrl. AI vil udtrække CVR, fakturanummer, beløb og linjer.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PDF Dokument
+
+ Vis Original
+
+
+
+
+
Indlæser PDF tekst...
+
+
+
+
+
+ 💡 Tip: Markér tekst og klik på et felt for at indsætte
+
+
+
+
+
+
Faktura Detaljer
+
+
+
+ Leverandør *
+
+ Vælg leverandør...
+
+
+ Opret ny leverandør
+
+
+
+
+
+
+ Fakturanummer *
+
+
+
+ Type
+
+ Faktura
+ Kreditnota
+
+
+
+
+
+
+
+
+ Total Beløb *
+
+
+
+ Valuta
+
+ DKK
+ EUR
+ USD
+
+
+
+
+
+
+
Produktlinier
+
+ đź’ˇ Momskoder:
+ I25 25% moms (standard) ·
+ I52 Omvendt betalingspligt ·
+ I0 0% moms (momsfri)
+
+
+
+
+
+ Tilføj linje
+
+
+
+
+ Noter
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Fundet leverandør:
+ Navn:
+ CVR:
+
+
+
+
+ Link Eksisterende
+
+
+ Opret Ny
+
+
+
+
+
+
+
+ Søg efter leverandør
+
+
+
+
Søg for at finde leverandører
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block extra_js %}
+
+{% endblock %}
diff --git a/app/billing/frontend/views.py b/app/billing/frontend/views.py
index c471bae..4ee085f 100644
--- a/app/billing/frontend/views.py
+++ b/app/billing/frontend/views.py
@@ -16,7 +16,16 @@ async def supplier_invoices_page(request: Request):
"""Supplier invoices (kassekladde) page"""
return templates.TemplateResponse("billing/frontend/supplier_invoices.html", {
"request": request,
- "title": "Kassekladde"
+ "title": "Leverandør fakturaer"
+ })
+
+
+@router.get("/billing/supplier-invoices2", response_class=HTMLResponse)
+async def supplier_invoices_v1_backup(request: Request):
+ """Supplier invoices V1 backup - original version"""
+ return templates.TemplateResponse("billing/frontend/supplier_invoices_v1_backup.html", {
+ "request": request,
+ "title": "Leverandør fakturaer (V1 Backup)"
})
diff --git a/app/core/database.py b/app/core/database.py
index 6ce9d40..669d2f8 100644
--- a/app/core/database.py
+++ b/app/core/database.py
@@ -85,14 +85,17 @@ def execute_query(query: str, params: tuple = None, fetch: bool = True):
def execute_insert(query: str, params: tuple = None):
- """Execute INSERT query and return new ID (requires RETURNING id clause)"""
+ """Execute INSERT query and return new ID (requires RETURNING clause)"""
conn = get_db_connection()
try:
with conn.cursor(cursor_factory=RealDictCursor) as cursor:
cursor.execute(query, params)
conn.commit()
result = cursor.fetchone()
- return result['id'] if result and 'id' in result else None
+ if result:
+ # Return first column value regardless of name (id, extraction_id, file_id, etc.)
+ return result[list(result.keys())[0]] if result else None
+ return None
except Exception as e:
conn.rollback()
logger.error(f"Insert error: {e}")
diff --git a/app/emails/backend/router.py b/app/emails/backend/router.py
index e75108a..928eef8 100644
--- a/app/emails/backend/router.py
+++ b/app/emails/backend/router.py
@@ -1068,11 +1068,13 @@ async def execute_workflows_for_email(email_id: int):
FROM email_messages
WHERE id = %s AND deleted_at IS NULL
"""
- email_data = execute_query(query, (email_id,))
+ email_result = execute_query(query, (email_id,))
- if not email_data:
+ if not email_result:
raise HTTPException(status_code=404, detail="Email not found")
+ email_data = email_result[0] # Get first row as dict
+
# Execute workflows
result = await email_workflow_service.execute_workflows(email_data)
diff --git a/app/services/economic_service.py b/app/services/economic_service.py
index a9fbd86..30fd96c 100644
--- a/app/services/economic_service.py
+++ b/app/services/economic_service.py
@@ -452,6 +452,81 @@ class EconomicService:
logger.error(f"❌ Error fetching journals: {e}")
raise
+ async def get_accounts(self) -> List[Dict]:
+ """
+ Get chart of accounts (kontoplan) from e-conomic
+
+ Returns:
+ List of account dictionaries with accountNumber, name, accountType, vatCode
+ """
+ try:
+ url = f"{self.api_url}/accounts"
+
+ async with aiohttp.ClientSession() as session:
+ async with session.get(url, headers=self._get_headers()) as response:
+ if response.status != 200:
+ error_text = await response.text()
+ raise Exception(f"e-conomic API error: {response.status} - {error_text}")
+
+ data = await response.json()
+
+ # Extract relevant account info
+ accounts = []
+ for account in data.get('collection', []):
+ accounts.append({
+ 'accountNumber': account.get('accountNumber'),
+ 'name': account.get('name'),
+ 'accountType': account.get('accountType'),
+ 'vatCode': account.get('vatCode', {}).get('vatCode') if account.get('vatCode') else None,
+ 'balance': account.get('balance', 0.0)
+ })
+
+ logger.info(f"📥 Fetched {len(accounts)} accounts from e-conomic")
+ return accounts
+ except Exception as e:
+ logger.error(f"❌ Error fetching accounts: {e}")
+ raise
+
+ async def sync_accounts_to_database(self) -> int:
+ """
+ Fetch accounts from e-conomic and cache them in database
+
+ Returns:
+ Number of accounts synced
+ """
+ from app.core.database import execute_query
+
+ try:
+ accounts = await self.get_accounts()
+
+ # Upsert accounts into database
+ for account in accounts:
+ execute_query("""
+ INSERT INTO economic_accounts
+ (account_number, name, account_type, vat_code, balance, last_synced)
+ VALUES (%s, %s, %s, %s, %s, NOW())
+ ON CONFLICT (account_number)
+ DO UPDATE SET
+ name = EXCLUDED.name,
+ account_type = EXCLUDED.account_type,
+ vat_code = EXCLUDED.vat_code,
+ balance = EXCLUDED.balance,
+ last_synced = NOW()
+ """, (
+ account['accountNumber'],
+ account['name'],
+ account['accountType'],
+ account['vatCode'],
+ account['balance']
+ ))
+
+ logger.info(f"âś… Synced {len(accounts)} accounts to database")
+ return len(accounts)
+
+ except Exception as e:
+ logger.error(f"❌ Error syncing accounts to database: {e}")
+ raise
+
async def create_journal_supplier_invoice(self,
journal_number: int,
supplier_number: int,
diff --git a/app/services/email_workflow_service.py b/app/services/email_workflow_service.py
index f0cad7e..6168faf 100644
--- a/app/services/email_workflow_service.py
+++ b/app/services/email_workflow_service.py
@@ -104,7 +104,7 @@ class EmailWorkflowService:
ORDER BY priority ASC
"""
- workflows = execute_query_single(query, (classification, confidence))
+ workflows = execute_query(query, (classification, confidence))
# Filter by additional patterns
matching = []
@@ -406,9 +406,10 @@ class EmailWorkflowService:
vendor_id = result['id']
# Check if already linked to avoid duplicate updates
- current_vendor = execute_query_single(
+ result_vendor = execute_query(
"SELECT supplier_id FROM email_messages WHERE id = %s",
(email_data['id'],))
+ current_vendor = result_vendor[0] if result_vendor else None
if current_vendor and current_vendor.get('supplier_id') == vendor_id:
logger.info(f"âŹď¸Ź Email already linked to vendor {vendor_id}, skipping duplicate update")
@@ -457,7 +458,7 @@ class EmailWorkflowService:
vendor_id = email_data.get('supplier_id')
# Get PDF attachments from email
- attachments = execute_query_single(
+ attachments = execute_query(
"""SELECT filename, file_path, size_bytes, content_type
FROM email_attachments
WHERE email_id = %s AND content_type = 'application/pdf'""",
diff --git a/app/shared/frontend/base.html b/app/shared/frontend/base.html
index 29585ee..6bf69b2 100644
--- a/app/shared/frontend/base.html
+++ b/app/shared/frontend/base.html
@@ -259,7 +259,7 @@