- Added a new column `subscriptions_locked` to the `customers` table to manage subscription access. - Implemented a script to create new modules from a template, including updates to various files (module.json, README.md, router.py, views.py, and migration SQL). - Developed a script to import BMC Office subscriptions from an Excel file into the database, including error handling and statistics reporting. - Created a script to lookup and update missing CVR numbers using the CVR.dk API. - Implemented a script to relink Hub customers to e-conomic customer numbers based on name matching. - Developed scripts to sync CVR numbers from Simply-CRM and vTiger to the local customers database.
221 lines
7.5 KiB
Python
221 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Import BMC Office abonnementer fra Excel fil til database
|
|
"""
|
|
import sys
|
|
import os
|
|
import pandas as pd
|
|
from datetime import datetime
|
|
from dotenv import load_dotenv
|
|
|
|
# Load environment variables
|
|
load_dotenv()
|
|
|
|
# Override DATABASE_URL for local execution (postgres:5432 -> localhost:5433)
|
|
if "postgres:5432" in os.getenv("DATABASE_URL", ""):
|
|
os.environ["DATABASE_URL"] = os.getenv("DATABASE_URL").replace("postgres:5432", "localhost:5433")
|
|
|
|
# Add parent directory to path to import app modules
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
|
|
|
from app.core.database import execute_query, execute_update, get_db_connection, init_db
|
|
|
|
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)
|
|
|
|
# Convert string: remove dots (thousands separator) and replace comma with dot
|
|
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:
|
|
# Try DD.MM.YYYY format
|
|
dt = datetime.strptime(value_str, '%d.%m.%Y')
|
|
return dt.strftime('%Y-%m-%d')
|
|
except:
|
|
try:
|
|
# Try other common formats
|
|
dt = pd.to_datetime(value_str)
|
|
return dt.strftime('%Y-%m-%d')
|
|
except:
|
|
return None
|
|
|
|
def find_customer_by_name(name, all_customers):
|
|
"""Find customer in database by name (case-insensitive, fuzzy match)"""
|
|
if not name:
|
|
return None
|
|
|
|
name_lower = name.lower().strip()
|
|
|
|
# First try exact match
|
|
for customer in all_customers:
|
|
if customer['name'].lower().strip() == name_lower:
|
|
return customer['id']
|
|
|
|
# Then try partial match
|
|
for customer in all_customers:
|
|
customer_name = customer['name'].lower().strip()
|
|
if name_lower in customer_name or customer_name in name_lower:
|
|
return customer['id']
|
|
|
|
return None
|
|
|
|
def import_subscriptions(excel_file):
|
|
"""Import subscriptions from Excel file"""
|
|
print(f"📂 Læser Excel fil: {excel_file}")
|
|
|
|
# Read Excel file
|
|
df = pd.read_excel(excel_file)
|
|
print(f"✅ Fundet {len(df)} rækker i Excel filen")
|
|
print(f"📋 Kolonner: {', '.join(df.columns)}")
|
|
|
|
# Get all customers from database
|
|
customers = execute_query("SELECT id, name, vtiger_id FROM customers ORDER BY name")
|
|
print(f"✅ Fundet {len(customers)} kunder i databasen")
|
|
|
|
# Clear existing BMC Office subscriptions
|
|
print("\n🗑️ Rydder eksisterende BMC Office abonnementer...")
|
|
execute_update("DELETE FROM bmc_office_subscriptions", ())
|
|
|
|
# Process each row
|
|
imported = 0
|
|
skipped = 0
|
|
errors = []
|
|
|
|
print("\n📥 Importerer abonnementer...")
|
|
for idx, row in df.iterrows():
|
|
try:
|
|
# Extract data
|
|
firma_id = str(row.get('FirmaID', ''))
|
|
firma_name = str(row.get('Firma', ''))
|
|
start_date = parse_danish_date(row.get('Startdate'))
|
|
text = str(row.get('Text', ''))
|
|
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', ''))
|
|
faktura_firma_id = str(row.get('FakturaFirmaID', ''))
|
|
faktura_firma_name = str(row.get('Fakturafirma', ''))
|
|
|
|
# Find customer by faktura firma name (most reliable)
|
|
customer_id = find_customer_by_name(faktura_firma_name, customers)
|
|
|
|
# If not found, try firma name
|
|
if not customer_id:
|
|
customer_id = find_customer_by_name(firma_name, customers)
|
|
|
|
if not customer_id:
|
|
skipped += 1
|
|
errors.append(f"Række {idx+2}: Kunde ikke fundet: {faktura_firma_name} / {firma_name}")
|
|
continue
|
|
|
|
# Determine if active (rabat < 100%)
|
|
active = (pris - rabat) > 0
|
|
|
|
# Insert into database
|
|
query = """
|
|
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)
|
|
"""
|
|
execute_update(query, (
|
|
customer_id, firma_id, firma_name, start_date, text,
|
|
antal, pris, rabat, beskrivelse if beskrivelse != 'nan' else '',
|
|
faktura_firma_id, faktura_firma_name, active
|
|
))
|
|
|
|
imported += 1
|
|
if imported % 50 == 0:
|
|
print(f" ✓ Importeret {imported} abonnementer...")
|
|
|
|
except Exception as e:
|
|
skipped += 1
|
|
errors.append(f"Række {idx+2}: {str(e)}")
|
|
|
|
# Summary
|
|
print(f"\n{'='*60}")
|
|
print(f"✅ Import færdig!")
|
|
print(f" Importeret: {imported}")
|
|
print(f" Sprunget over: {skipped}")
|
|
print(f"{'='*60}")
|
|
|
|
if errors:
|
|
print(f"\n⚠️ Fejl og advarsler:")
|
|
for error in errors[:20]: # Show first 20 errors
|
|
print(f" {error}")
|
|
if len(errors) > 20:
|
|
print(f" ... og {len(errors)-20} flere")
|
|
|
|
# Show statistics
|
|
stats_query = """
|
|
SELECT
|
|
COUNT(*) as total,
|
|
COUNT(*) FILTER (WHERE active = true) as active,
|
|
SUM((antal * pris - rabat) * 1.25) FILTER (WHERE active = true) as total_value
|
|
FROM bmc_office_subscriptions
|
|
"""
|
|
stats = execute_query(stats_query, fetchone=True)
|
|
|
|
print(f"\n📊 Statistik:")
|
|
print(f" Total abonnementer: {stats['total']}")
|
|
print(f" Aktive abonnementer: {stats['active']}")
|
|
print(f" Total værdi (aktive): {stats['total_value']:.2f} DKK inkl. moms")
|
|
|
|
# Show top customers
|
|
top_query = """
|
|
SELECT
|
|
c.name,
|
|
COUNT(*) as antal_abonnementer,
|
|
SUM((bos.antal * bos.pris - bos.rabat) * 1.25) as total_value
|
|
FROM bmc_office_subscriptions bos
|
|
JOIN customers c ON c.id = bos.customer_id
|
|
WHERE bos.active = true
|
|
GROUP BY c.id, c.name
|
|
ORDER BY total_value DESC
|
|
LIMIT 10
|
|
"""
|
|
top_customers = execute_query(top_query)
|
|
|
|
if top_customers:
|
|
print(f"\n🏆 Top 10 kunder (efter værdi):")
|
|
for i, cust in enumerate(top_customers, 1):
|
|
print(f" {i}. {cust['name']}: {cust['antal_abonnementer']} abonnementer = {cust['total_value']:.2f} DKK")
|
|
|
|
if __name__ == '__main__':
|
|
excel_file = '/Users/christianthomas/DEV/bmc_hub_dev/uploads/BMC_FasteOmkostninger_20251211.xlsx'
|
|
|
|
if not os.path.exists(excel_file):
|
|
print(f"❌ Fil ikke fundet: {excel_file}")
|
|
sys.exit(1)
|
|
|
|
try:
|
|
# Initialize database connection pool
|
|
print("🔌 Forbinder til database...")
|
|
init_db()
|
|
|
|
import_subscriptions(excel_file)
|
|
print("\n✅ Import succesfuld!")
|
|
except Exception as e:
|
|
print(f"\n❌ Import fejlede: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
sys.exit(1)
|