bmc_hub/app/customers/backend/bmc_office_router.py

220 lines
8.5 KiB
Python

"""
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")
# 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:
execute_update(
"""INSERT INTO customer_activities (customer_id, activity_type, description, user_id)
VALUES (%s, %s, %s, %s)""",
(customer['customer_id'], 'bmc_office_sync',
f"BMC Office abonnementer fjernet før ny import: {customer['count']} abonnementer (værdi: {float(customer['value']):,.2f} DKK)",
1) # System user
)
# 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))
# 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:
execute_update(
"""INSERT INTO customer_activities (customer_id, activity_type, description, user_id)
VALUES (%s, %s, %s, %s)""",
(customer['customer_id'], 'bmc_office_sync',
f"BMC Office abonnementer importeret: {customer['count']} abonnementer (værdi: {float(customer['value']):,.2f} DKK) - Produkter: {customer['products'][:200]}",
1) # System user
)
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)}")