From c855f5d0270907aee283cac7bc20a40bbbf9b1e0 Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 7 Jan 2026 10:32:41 +0100 Subject: [PATCH] feat(migrations): add supplier invoice enhancements for accounting integration - Added new columns to supplier_invoice_lines for contra_account, line_purpose, resale_customer_id, resale_order_number, is_invoiced_to_customer, and invoiced_date. - Created indexes for faster filtering by purpose and resale status. - Introduced economic_accounts table to cache e-conomic chart of accounts with relevant fields and indexes. - Added comments for documentation on new columns and tables. - Included success message for migration completion. --- MIGRATION_GUIDE_v2.0.0.md | 248 ++ app/billing/backend/supplier_invoices.py | 165 +- app/billing/frontend/supplier_invoices.html | 811 +++- .../frontend/supplier_invoices_v1_backup.html | 3487 +++++++++++++++++ app/billing/frontend/views.py | 11 +- app/core/database.py | 7 +- app/emails/backend/router.py | 6 +- app/services/economic_service.py | 75 + app/services/email_workflow_service.py | 7 +- app/shared/frontend/base.html | 2 +- .../1000_supplier_invoice_enhancements.sql | 61 + requirements.txt | 1 + 12 files changed, 4782 insertions(+), 99 deletions(-) create mode 100644 MIGRATION_GUIDE_v2.0.0.md create mode 100644 app/billing/frontend/supplier_invoices_v1_backup.html create mode 100644 migrations/1000_supplier_invoice_enhancements.sql 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

+
+
+ + Se Templates + + + Template Builder + + +
+
+ + +
+
+
+

-

+

Overskredet

+ - +
+
+
+
+

-

+

Forfald inden 7 dage

+ - +
+
+
+
+

-

+

Afventer behandling

+ - +
+
+
+
+

-

+

Ubetalt i alt

+ - +
+
+
+ + + + + +
+ + +
+ + +
+
+
+
+ Alle +
+
+ Afventer +
+
+ Godkendt +
+
+ Sendt til e-conomic +
+
+ Overskredet +
+
+
+
+ + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + +
+ + Fakturanr.LeverandørFakturadatoForfaldsdatoBeløbStatuse-conomicHandlinger
+
+ Indlæser... +
+
+
+
+
+ +
+ + +
+ + +
+
+
+
⏳ Filer der mangler behandling
+ +
+ + + + +
+ + + + + + + + + + + + + + + + + + +
+ + FilnavnUpload DatoStatusQuick AnalysisLeverandørTemplateHandlinger
+
+ Indlæser... +
+
+
+
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + +{% 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 @@