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,
|
||||
"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
|
||||
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"])
|
||||
|
||||
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