""" 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 from app.services.customer_activity_logger import CustomerActivityLogger 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") # Get current statistics before deletion old_stats = execute_query(""" SELECT COUNT(*) as total_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] old_total = old_stats['total_records'] old_value = float(old_stats['total_value_dkk'] or 0) # Get affected customers before deletion (for activity log) affected_customers = execute_query(""" SELECT DISTINCT customer_id, COUNT(*) as count, ROUND(SUM(total_inkl_moms)::numeric, 2) as value FROM bmc_office_subscription_totals WHERE active = true GROUP BY customer_id """) # Clear existing subscriptions execute_update("DELETE FROM bmc_office_subscriptions", ()) logger.info(f"🗑️ Ryddet {old_total} eksisterende abonnementer (værdi: {old_value:,.2f} DKK)") # Log deletion for each affected customer for customer in affected_customers: CustomerActivityLogger.log( customer_id=customer['customer_id'], activity_type='bmc_office_sync', description=f"BMC Office abonnementer fjernet før ny import: {customer['count']} abonnementer (værdi: {float(customer['value']):,.2f} DKK)" ) # 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] # Get new subscriptions per customer for activity log new_subscriptions = execute_query(""" SELECT customer_id, COUNT(*) as count, ROUND(SUM(total_inkl_moms)::numeric, 2) as value, string_agg(text, ', ' ORDER BY text) as products FROM bmc_office_subscription_totals WHERE active = true GROUP BY customer_id """) # Log import for each customer for customer in new_subscriptions: CustomerActivityLogger.log( customer_id=customer['customer_id'], activity_type='bmc_office_sync', description=f"BMC Office abonnementer importeret: {customer['count']} abonnementer (værdi: {float(customer['value']):,.2f} DKK) - Produkter: {customer['products'][:200]}" ) 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)}")