diff --git a/VERSION b/VERSION index c5a6820..0431208 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.118 \ No newline at end of file +1.3.119 \ No newline at end of file diff --git a/app/customers/backend/bmc_office_router.py b/app/customers/backend/bmc_office_router.py new file mode 100644 index 0000000..93b9362 --- /dev/null +++ b/app/customers/backend/bmc_office_router.py @@ -0,0 +1,169 @@ +""" +BMC Office Subscriptions Upload Router +Allows admins to upload Excel files with subscription data +""" + +from fastapi import APIRouter, UploadFile, File, HTTPException +from fastapi.responses import JSONResponse +import pandas as pd +from datetime import datetime +import logging +from io import BytesIO + +from app.core.database import execute_query, execute_update + +logger = logging.getLogger(__name__) + +router = APIRouter() + + +def parse_danish_number(value): + """Convert Danish number format to float (2.995,00 -> 2995.00)""" + if pd.isna(value) or value == '': + return 0.0 + if isinstance(value, (int, float)): + return float(value) + + value_str = str(value).strip() + value_str = value_str.replace('.', '').replace(',', '.') + try: + return float(value_str) + except: + return 0.0 + + +def parse_danish_date(value): + """Convert DD.MM.YYYY to YYYY-MM-DD""" + if pd.isna(value) or value == '': + return None + + if isinstance(value, datetime): + return value.strftime('%Y-%m-%d') + + value_str = str(value).strip() + try: + dt = datetime.strptime(value_str, '%d.%m.%Y') + return dt.strftime('%Y-%m-%d') + except: + try: + dt = pd.to_datetime(value_str) + return dt.strftime('%Y-%m-%d') + except: + return None + + +@router.post("/admin/bmc-office-subscriptions/upload") +async def upload_bmc_office_subscriptions(file: UploadFile = File(...)): + """ + Upload Excel file with BMC Office subscriptions + Expected columns: FirmaID, Firma, Startdate, Text, Antal, Pris, Rabat, Beskrivelse, FakturaFirmaID, FakturaFirma + """ + + if not file.filename.endswith(('.xlsx', '.xls')): + raise HTTPException(status_code=400, detail="Kun Excel filer (.xlsx, .xls) er tilladt") + + try: + # Read Excel file + contents = await file.read() + df = pd.read_excel(BytesIO(contents)) + + logger.info(f"📂 Læst Excel fil: {file.filename} med {len(df)} rækker") + logger.info(f"📋 Kolonner: {', '.join(df.columns)}") + + # Get all customers from database + customers = execute_query("SELECT id, name FROM customers ORDER BY name") + customer_map = {c['name'].lower().strip(): c['id'] for c in customers} + + logger.info(f"✅ Fundet {len(customers)} kunder i databasen") + + # Clear existing subscriptions + execute_update("DELETE FROM bmc_office_subscriptions", ()) + logger.info("🗑️ Ryddet eksisterende abonnementer") + + # Process rows + imported = 0 + skipped = 0 + errors = [] + + for idx, row in df.iterrows(): + try: + firma_name = str(row.get('Firma', '')).strip() + if not firma_name: + skipped += 1 + continue + + # Find customer by name (case-insensitive) + customer_id = None + firma_lower = firma_name.lower() + + # Exact match first + if firma_lower in customer_map: + customer_id = customer_map[firma_lower] + else: + # Partial match + for cust_name, cust_id in customer_map.items(): + if firma_lower in cust_name or cust_name in firma_lower: + customer_id = cust_id + break + + if not customer_id: + skipped += 1 + errors.append(f"Række {idx+2}: Kunde '{firma_name}' ikke fundet") + continue + + # Parse data + firma_id = str(row.get('FirmaID', '')).strip() + start_date = parse_danish_date(row.get('Startdate')) + text = str(row.get('Text', '')).strip() + antal = parse_danish_number(row.get('Antal', 1)) + pris = parse_danish_number(row.get('Pris', 0)) + rabat = parse_danish_number(row.get('Rabat', 0)) + beskrivelse = str(row.get('Beskrivelse', '')).strip() + faktura_firma_id = str(row.get('FakturaFirmaID', '')).strip() + faktura_firma_name = str(row.get('FakturaFirma', '')).strip() + + # Insert subscription + execute_update( + """INSERT INTO bmc_office_subscriptions + (customer_id, firma_id, firma_name, start_date, text, antal, pris, rabat, + beskrivelse, faktura_firma_id, faktura_firma_name, active) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""", + (customer_id, firma_id, firma_name, start_date, text, antal, pris, rabat, + beskrivelse, faktura_firma_id, faktura_firma_name, True) + ) + + imported += 1 + + except Exception as e: + skipped += 1 + errors.append(f"Række {idx+2}: {str(e)}") + logger.error(f"❌ Fejl ved import af række {idx+2}: {e}") + + # Get statistics + stats = execute_query(""" + SELECT + COUNT(*) as total_records, + COUNT(CASE WHEN active THEN 1 END) as active_records, + ROUND(SUM(CASE WHEN active THEN total_inkl_moms ELSE 0 END)::numeric, 2) as total_value_dkk + FROM bmc_office_subscription_totals + """)[0] + + logger.info(f"✅ Import færdig: {imported} importeret, {skipped} sprunget over") + + return { + "success": True, + "filename": file.filename, + "total_rows": len(df), + "imported": imported, + "skipped": skipped, + "errors": errors[:10], # Max 10 error messages + "statistics": { + "total_records": stats['total_records'], + "active_records": stats['active_records'], + "total_value_dkk": float(stats['total_value_dkk'] or 0) + } + } + + except Exception as e: + logger.error(f"❌ Fejl ved upload: {e}") + raise HTTPException(status_code=500, detail=f"Fejl ved upload: {str(e)}") diff --git a/app/customers/backend/views.py b/app/customers/backend/views.py index 1e8bd41..ff88d7c 100644 --- a/app/customers/backend/views.py +++ b/app/customers/backend/views.py @@ -22,3 +22,12 @@ async def customer_detail_page(request: Request, customer_id: int): "request": request, "customer_id": customer_id }) + + +@router.get("/admin/bmc-office-upload", response_class=HTMLResponse) +async def bmc_office_upload_page(request: Request): + """BMC Office subscriptions upload page""" + return templates.TemplateResponse( + "customers/frontend/bmc_office_upload.html", + {"request": request} + ) diff --git a/app/customers/frontend/bmc_office_upload.html b/app/customers/frontend/bmc_office_upload.html new file mode 100644 index 0000000..4cb62b2 --- /dev/null +++ b/app/customers/frontend/bmc_office_upload.html @@ -0,0 +1,288 @@ + + + + + + BMC Office Abonnementer - Upload + + + + + +
+
+
+
+

+ + BMC Office Abonnementer +

+ + Tilbage + +
+ + +
+
+ Om Upload +
+

Upload en Excel fil (.xlsx eller .xls) med BMC Office abonnementsdata. + Systemet vil automatisk matche firma navne med kunder i databasen.

+
+ + Forventede kolonner: FirmaID, Firma, Startdate, Text, Antal, Pris, Rabat, Beskrivelse, FakturaFirmaID, FakturaFirma + +
+ + +
+ +

Træk Excel fil hertil

+

eller klik for at vælge fil

+ + +
+ + +
+
+
+ Uploader... +
+
+
+ + + +
+
+
+ + + + diff --git a/main.py b/main.py index 90d2836..e7ae29a 100644 --- a/main.py +++ b/main.py @@ -25,6 +25,7 @@ def get_version(): # Import Feature Routers from app.customers.backend import router as customers_api from app.customers.backend import views as customers_views +from app.customers.backend import bmc_office_router from app.hardware.backend import router as hardware_api from app.billing.backend import router as billing_api from app.billing.frontend import views as billing_views @@ -107,6 +108,7 @@ app.add_middleware( # Include routers app.include_router(customers_api.router, prefix="/api/v1", tags=["Customers"]) +app.include_router(bmc_office_router.router, prefix="/api/v1", tags=["BMC Office"]) app.include_router(hardware_api.router, prefix="/api/v1", tags=["Hardware"]) app.include_router(billing_api.router, prefix="/api/v1", tags=["Billing"]) app.include_router(system_api.router, prefix="/api/v1", tags=["System"]) diff --git a/scripts/debug_bmc_subs.sh b/scripts/debug_bmc_subs.sh new file mode 100755 index 0000000..2b426ff --- /dev/null +++ b/scripts/debug_bmc_subs.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Debug: Check BMC Office subscriptions on prod + +echo "🔍 Tjekker BMC Office abonnementer på prod..." +echo "" + +ssh bmcadmin@172.16.31.183 << 'ENDSSH' + echo "📊 Tabel statistik:" + podman exec bmc-hub-postgres-prod psql -U bmc_hub -d bmc_hub -c " + SELECT COUNT(*) as total_rows FROM bmc_office_subscriptions; + " + + echo "" + echo "📊 Hvis der er data - vis sample:" + podman exec bmc-hub-postgres-prod psql -U bmc_hub -d bmc_hub -c " + SELECT id, customer_id, firma_name, text, active + FROM bmc_office_subscriptions + LIMIT 5; + " + + echo "" + echo "📊 Customer name matching check:" + podman exec bmc-hub-postgres-prod psql -U bmc_hub -d bmc_hub -c " + SELECT + c.id, + c.name, + COUNT(bos.id) as subscription_count + FROM customers c + LEFT JOIN bmc_office_subscriptions bos ON bos.customer_id = c.id + WHERE c.name LIKE '%Mate%' + GROUP BY c.id, c.name; + " +ENDSSH + +echo "" +echo "✅ Færdig" diff --git a/scripts/export_bmc_subs_to_prod.sh b/scripts/export_bmc_subs_to_prod.sh new file mode 100644 index 0000000..3ce3763 --- /dev/null +++ b/scripts/export_bmc_subs_to_prod.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# Export BMC Office subscriptions from local dev and import to production + +set -e + +echo "🔍 Eksporterer BMC Office abonnementer fra lokal dev..." + +# Export to SQL file with proper escaping +docker exec bmc-hub-postgres psql -U bmc_hub -d bmc_hub -t -A -F',' -c " +SELECT + id, + customer_id, + firma_id, + firma_name, + start_date, + text, + antal, + pris, + rabat, + beskrivelse, + faktura_firma_id, + faktura_firma_name, + active +FROM bmc_office_subscriptions +ORDER BY id;" > /tmp/bmc_subs_data.csv + +echo "✅ Eksporteret $(wc -l < /tmp/bmc_subs_data.csv) records" + +echo "" +echo "📤 Uploader til prod server..." + +# Copy file to prod +scp /tmp/bmc_subs_data.csv root@172.16.31.183:/tmp/ + +echo "" +echo "📥 Importerer på prod server..." + +# Import on prod +ssh root@172.16.31.183 << 'ENDSSH' + echo "🗑️ Rydder eksisterende data..." + sudo podman exec bmc-hub-postgres-prod psql -U bmchub -d bmchub -c "TRUNCATE TABLE bmc_office_subscriptions RESTART IDENTITY CASCADE;" + + echo "📥 Importerer ny data..." + sudo podman exec -i bmc-hub-postgres-prod psql -U bmchub -d bmchub -c " + COPY bmc_office_subscriptions (id, customer_id, firma_id, firma_name, start_date, text, antal, pris, rabat, beskrivelse, faktura_firma_id, faktura_firma_name, active) + FROM '/tmp/bmc_subs_data.csv' + WITH (FORMAT csv, DELIMITER ','); + " < /tmp/bmc_subs_data.csv + + echo "✅ Import færdig!" + + sudo podman exec bmc-hub-postgres-prod psql -U bmchub -d bmchub -c " + SELECT COUNT(*) as imported_total, + ROUND(SUM(total_inkl_moms)::numeric, 2) as total_value + FROM bmc_office_subscription_totals + WHERE active = true; + " +ENDSSH + +echo "" +echo "🎉 Færdig! BMC Office abonnementer overført til prod." diff --git a/scripts/sync_bmc_subs_to_prod.sh b/scripts/sync_bmc_subs_to_prod.sh new file mode 100755 index 0000000..4957bdb --- /dev/null +++ b/scripts/sync_bmc_subs_to_prod.sh @@ -0,0 +1,118 @@ +#!/bin/bash +# Synkroniser BMC Office abonnementer med customer ID mapping + +set -e + +PROD_SERVER="bmcadmin@172.16.31.183" + +echo "🔍 Eksporterer BMC Office abonnementer fra lokal dev..." + +# Export with firma_name for mapping (not customer_id) +docker exec bmc-hub-postgres psql -U bmc_hub -d bmc_hub -t -A -F'|' -c " +SELECT + firma_id, + firma_name, + start_date, + text, + antal, + pris, + rabat, + beskrivelse, + faktura_firma_id, + faktura_firma_name, + active +FROM bmc_office_subscriptions +ORDER BY firma_name, id;" > /tmp/bmc_subs_export.csv + +LINES=$(wc -l < /tmp/bmc_subs_export.csv | tr -d ' ') +echo "✅ Eksporteret $LINES records" + +echo "" +echo "📤 Uploader og importerer til prod med customer mapping..." + +# SSH to prod and run import with customer mapping +ssh $PROD_SERVER << 'ENDSSH' + echo "📥 Modtager data..." + cat > /tmp/bmc_subs_data.csv + + echo "🗑️ Rydder eksisterende abonnementer..." + podman exec bmc-hub-postgres-prod psql -U bmc_hub -d bmc_hub -c "TRUNCATE TABLE bmc_office_subscriptions RESTART IDENTITY CASCADE;" > /dev/null + + echo "📥 Importerer med customer ID mapping..." + podman exec -i bmc-hub-postgres-prod psql -U bmc_hub -d bmc_hub << 'EOSQL' + CREATE TEMP TABLE bmc_import ( + firma_id VARCHAR(50), + firma_name VARCHAR(255), + start_date DATE, + text VARCHAR(500), + antal DECIMAL(10,2), + pris DECIMAL(10,2), + rabat DECIMAL(10,2), + beskrivelse TEXT, + faktura_firma_id VARCHAR(50), + faktura_firma_name VARCHAR(255), + active BOOLEAN + ); + + \COPY bmc_import FROM '/tmp/bmc_subs_data.csv' WITH (FORMAT csv, DELIMITER '|'); + + INSERT INTO bmc_office_subscriptions ( + customer_id, firma_id, firma_name, start_date, text, antal, pris, rabat, + beskrivelse, faktura_firma_id, faktura_firma_name, active + ) + SELECT + c.id as customer_id, + bi.firma_id, + bi.firma_name, + bi.start_date, + bi.text, + bi.antal, + bi.pris, + bi.rabat, + bi.beskrivelse, + bi.faktura_firma_id, + bi.faktura_firma_name, + bi.active + FROM bmc_import bi + LEFT JOIN customers c ON LOWER(TRIM(c.name)) = LOWER(TRIM(bi.firma_name)) + WHERE c.id IS NOT NULL; + + SELECT + COUNT(*) as total_imported, + COUNT(DISTINCT customer_id) as unique_customers + FROM bmc_office_subscriptions; +EOSQL + + echo "" + echo "✅ Import færdig! Verificerer..." + podman exec bmc-hub-postgres-prod psql -U bmc_hub -d bmc_hub -c " + SELECT + COUNT(*) as total_records, + COUNT(CASE WHEN active THEN 1 END) as active_records, + ROUND(SUM(CASE WHEN active THEN total_inkl_moms ELSE 0 END)::numeric, 2) as total_value_dkk + FROM bmc_office_subscription_totals; + " + + echo "" + echo "📊 Sample kunde check (Mate.Bike):" + podman exec bmc-hub-postgres-prod psql -U bmc_hub -d bmc_hub -c " + SELECT + c.id as customer_id, + c.name, + COUNT(bos.id) as bmc_subs_count, + ROUND(SUM(bos.total_inkl_moms)::numeric, 2) as total_value + FROM customers c + JOIN bmc_office_subscription_totals bos ON bos.customer_id = c.id + WHERE c.name LIKE '%Mate%' + GROUP BY c.id, c.name; + " + + echo "" + echo "🧹 Rydder op..." + rm /tmp/bmc_subs_data.csv +ENDSSH < /tmp/bmc_subs_export.csv + +echo "" +echo "🎉 Færdig! BMC Office abonnementer er synkroniseret til produktion." +echo "" +echo "🔗 Test på: http://172.16.31.183:8000/customers/183 -> Abonnnents tjek tab (Mate.Bike)"