feat: Add BMC Office subscriptions Excel upload interface with auto customer mapping
This commit is contained in:
parent
da5ec19188
commit
bd746b7f9c
169
app/customers/backend/bmc_office_router.py
Normal file
169
app/customers/backend/bmc_office_router.py
Normal file
@ -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)}")
|
||||||
@ -22,3 +22,12 @@ async def customer_detail_page(request: Request, customer_id: int):
|
|||||||
"request": request,
|
"request": request,
|
||||||
"customer_id": customer_id
|
"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}
|
||||||
|
)
|
||||||
|
|||||||
288
app/customers/frontend/bmc_office_upload.html
Normal file
288
app/customers/frontend/bmc_office_upload.html
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="da">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>BMC Office Abonnementer - Upload</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary: #0f4c75;
|
||||||
|
--accent: #3282b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-zone {
|
||||||
|
border: 3px dashed var(--accent);
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 60px 20px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-zone:hover {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-zone.dragover {
|
||||||
|
background: #e3f2fd;
|
||||||
|
border-color: var(--primary);
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
display: none;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-list {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8 mx-auto">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="fw-bold">
|
||||||
|
<i class="bi bi-cloud-upload text-primary me-2"></i>
|
||||||
|
BMC Office Abonnementer
|
||||||
|
</h2>
|
||||||
|
<a href="/customers" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-arrow-left me-2"></i>Tilbage
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Card -->
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h5 class="alert-heading">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>Om Upload
|
||||||
|
</h5>
|
||||||
|
<p class="mb-0">Upload en Excel fil (.xlsx eller .xls) med BMC Office abonnementsdata.
|
||||||
|
Systemet vil automatisk matche firma navne med kunder i databasen.</p>
|
||||||
|
<hr>
|
||||||
|
<small class="text-muted">
|
||||||
|
<strong>Forventede kolonner:</strong> FirmaID, Firma, Startdate, Text, Antal, Pris, Rabat, Beskrivelse, FakturaFirmaID, FakturaFirma
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload Zone -->
|
||||||
|
<div class="upload-zone" id="uploadZone">
|
||||||
|
<i class="bi bi-cloud-arrow-up" style="font-size: 4rem; color: var(--accent);"></i>
|
||||||
|
<h4 class="mt-3">Træk Excel fil hertil</h4>
|
||||||
|
<p class="text-muted">eller klik for at vælge fil</p>
|
||||||
|
<input type="file" id="fileInput" accept=".xlsx,.xls" style="display: none;">
|
||||||
|
<button class="btn btn-primary mt-3" onclick="document.getElementById('fileInput').click()">
|
||||||
|
<i class="bi bi-folder2-open me-2"></i>Vælg Fil
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress -->
|
||||||
|
<div class="progress-container" id="progressContainer">
|
||||||
|
<div class="progress" style="height: 30px;">
|
||||||
|
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||||
|
role="progressbar"
|
||||||
|
id="progressBar"
|
||||||
|
style="width: 0%">
|
||||||
|
<span id="progressText">Uploader...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Results -->
|
||||||
|
<div id="resultsContainer" class="mt-4" style="display: none;">
|
||||||
|
<!-- Statistics -->
|
||||||
|
<div class="stats-card">
|
||||||
|
<h5 class="fw-bold mb-4">
|
||||||
|
<i class="bi bi-bar-chart text-success me-2"></i>Import Resultat
|
||||||
|
</h5>
|
||||||
|
<div class="row text-center">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-number text-primary" id="statImported">0</div>
|
||||||
|
<div class="stat-label">Importeret</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-number text-warning" id="statSkipped">0</div>
|
||||||
|
<div class="stat-label">Sprunget Over</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-number text-success" id="statActive">0</div>
|
||||||
|
<div class="stat-label">Aktive</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-number text-info" id="statValue">0</div>
|
||||||
|
<div class="stat-label">DKK (Total)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Errors (if any) -->
|
||||||
|
<div id="errorsCard" class="stats-card" style="display: none;">
|
||||||
|
<h5 class="fw-bold text-danger mb-3">
|
||||||
|
<i class="bi bi-exclamation-triangle me-2"></i>Advarsler
|
||||||
|
</h5>
|
||||||
|
<div class="error-list" id="errorList"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<button class="btn btn-primary btn-lg" onclick="location.reload()">
|
||||||
|
<i class="bi bi-arrow-clockwise me-2"></i>Upload Ny Fil
|
||||||
|
</button>
|
||||||
|
<a href="/customers" class="btn btn-outline-secondary btn-lg">
|
||||||
|
<i class="bi bi-people me-2"></i>Gå til Kunder
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const uploadZone = document.getElementById('uploadZone');
|
||||||
|
const fileInput = document.getElementById('fileInput');
|
||||||
|
const progressContainer = document.getElementById('progressContainer');
|
||||||
|
const progressBar = document.getElementById('progressBar');
|
||||||
|
const progressText = document.getElementById('progressText');
|
||||||
|
const resultsContainer = document.getElementById('resultsContainer');
|
||||||
|
|
||||||
|
// Drag & Drop
|
||||||
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||||
|
uploadZone.addEventListener(eventName, preventDefaults, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
function preventDefaults(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
['dragenter', 'dragover'].forEach(eventName => {
|
||||||
|
uploadZone.addEventListener(eventName, () => {
|
||||||
|
uploadZone.classList.add('dragover');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
['dragleave', 'drop'].forEach(eventName => {
|
||||||
|
uploadZone.addEventListener(eventName, () => {
|
||||||
|
uploadZone.classList.remove('dragover');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
uploadZone.addEventListener('drop', handleDrop);
|
||||||
|
fileInput.addEventListener('change', handleFileSelect);
|
||||||
|
|
||||||
|
function handleDrop(e) {
|
||||||
|
const dt = e.dataTransfer;
|
||||||
|
const files = dt.files;
|
||||||
|
if (files.length > 0) {
|
||||||
|
uploadFile(files[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileSelect(e) {
|
||||||
|
const files = e.target.files;
|
||||||
|
if (files.length > 0) {
|
||||||
|
uploadFile(files[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadFile(file) {
|
||||||
|
if (!file.name.endsWith('.xlsx') && !file.name.endsWith('.xls')) {
|
||||||
|
alert('Kun Excel filer (.xlsx, .xls) er tilladt');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadZone.style.display = 'none';
|
||||||
|
progressContainer.style.display = 'block';
|
||||||
|
progressBar.style.width = '50%';
|
||||||
|
progressText.textContent = 'Uploader og importerer...';
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/v1/admin/bmc-office-subscriptions/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
progressBar.style.width = '100%';
|
||||||
|
progressText.textContent = 'Færdig!';
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(result.detail || 'Upload fejlede');
|
||||||
|
}
|
||||||
|
|
||||||
|
displayResults(result);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload error:', error);
|
||||||
|
alert('Fejl ved upload: ' + error.message);
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayResults(result) {
|
||||||
|
progressContainer.style.display = 'none';
|
||||||
|
resultsContainer.style.display = 'block';
|
||||||
|
|
||||||
|
document.getElementById('statImported').textContent = result.imported;
|
||||||
|
document.getElementById('statSkipped').textContent = result.skipped;
|
||||||
|
document.getElementById('statActive').textContent = result.statistics.active_records;
|
||||||
|
document.getElementById('statValue').textContent = result.statistics.total_value_dkk.toFixed(2);
|
||||||
|
|
||||||
|
if (result.errors && result.errors.length > 0) {
|
||||||
|
document.getElementById('errorsCard').style.display = 'block';
|
||||||
|
const errorList = document.getElementById('errorList');
|
||||||
|
errorList.innerHTML = result.errors.map(err =>
|
||||||
|
`<div class="alert alert-warning py-2 mb-2"><small>${err}</small></div>`
|
||||||
|
).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2
main.py
2
main.py
@ -25,6 +25,7 @@ def get_version():
|
|||||||
# Import Feature Routers
|
# Import Feature Routers
|
||||||
from app.customers.backend import router as customers_api
|
from app.customers.backend import router as customers_api
|
||||||
from app.customers.backend import views as customers_views
|
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.hardware.backend import router as hardware_api
|
||||||
from app.billing.backend import router as billing_api
|
from app.billing.backend import router as billing_api
|
||||||
from app.billing.frontend import views as billing_views
|
from app.billing.frontend import views as billing_views
|
||||||
@ -107,6 +108,7 @@ app.add_middleware(
|
|||||||
|
|
||||||
# Include routers
|
# Include routers
|
||||||
app.include_router(customers_api.router, prefix="/api/v1", tags=["Customers"])
|
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(hardware_api.router, prefix="/api/v1", tags=["Hardware"])
|
||||||
app.include_router(billing_api.router, prefix="/api/v1", tags=["Billing"])
|
app.include_router(billing_api.router, prefix="/api/v1", tags=["Billing"])
|
||||||
app.include_router(system_api.router, prefix="/api/v1", tags=["System"])
|
app.include_router(system_api.router, prefix="/api/v1", tags=["System"])
|
||||||
|
|||||||
36
scripts/debug_bmc_subs.sh
Executable file
36
scripts/debug_bmc_subs.sh
Executable file
@ -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"
|
||||||
61
scripts/export_bmc_subs_to_prod.sh
Normal file
61
scripts/export_bmc_subs_to_prod.sh
Normal file
@ -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."
|
||||||
118
scripts/sync_bmc_subs_to_prod.sh
Executable file
118
scripts/sync_bmc_subs_to_prod.sh
Executable file
@ -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)"
|
||||||
Loading…
Reference in New Issue
Block a user