- 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.
274 lines
9.7 KiB
Python
Executable File
274 lines
9.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Relink Hub Customers to e-conomic Customer Numbers
|
|
===================================================
|
|
|
|
Dette script matcher Hub kunder med e-conomic kunder baseret på NAVN matching
|
|
og opdaterer economic_customer_number feltet.
|
|
|
|
VIGTIG: Dette script ændrer IKKE data i e-conomic - det opdaterer kun Hub's links.
|
|
|
|
Usage:
|
|
python scripts/relink_economic_customers.py [--dry-run] [--force]
|
|
|
|
Options:
|
|
--dry-run Vis hvad der ville blive ændret uden at gemme
|
|
--force Overskriv eksisterende links (standard: skip hvis allerede linket)
|
|
|
|
Note: Matcher på kundenavn (case-insensitive, fjerner mellemrum/punktum)
|
|
"""
|
|
|
|
import sys
|
|
import asyncio
|
|
import logging
|
|
from typing import Dict, List, Optional
|
|
import argparse
|
|
|
|
import aiohttp
|
|
|
|
sys.path.insert(0, '/app')
|
|
|
|
from app.core.config import settings
|
|
from app.core.database import execute_query, execute_update, init_db
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(levelname)s - %(message)s'
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class EconomicRelinkService:
|
|
"""Service til at relinke Hub kunder til e-conomic"""
|
|
|
|
def __init__(self, dry_run: bool = False, force: bool = False):
|
|
self.api_url = settings.ECONOMIC_API_URL
|
|
self.app_secret_token = settings.ECONOMIC_APP_SECRET_TOKEN
|
|
self.agreement_grant_token = settings.ECONOMIC_AGREEMENT_GRANT_TOKEN
|
|
self.dry_run = dry_run
|
|
self.force = force
|
|
|
|
if dry_run:
|
|
logger.info("🔍 DRY RUN MODE - ingen ændringer gemmes")
|
|
if force:
|
|
logger.info("⚠️ FORCE MODE - overskriver eksisterende links")
|
|
|
|
async def fetch_economic_customers(self) -> List[Dict]:
|
|
"""Hent alle kunder fra e-conomic API"""
|
|
logger.info("📥 Henter kunder fra e-conomic...")
|
|
|
|
headers = {
|
|
'X-AppSecretToken': self.app_secret_token,
|
|
'X-AgreementGrantToken': self.agreement_grant_token,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
|
|
all_customers = []
|
|
page = 0
|
|
page_size = 1000
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
while True:
|
|
url = f"{self.api_url}/customers?skippages={page}&pagesize={page_size}"
|
|
|
|
try:
|
|
async with session.get(url, headers=headers) as response:
|
|
if response.status != 200:
|
|
error_text = await response.text()
|
|
logger.error(f"❌ e-conomic API fejl: {response.status} - {error_text}")
|
|
break
|
|
|
|
data = await response.json()
|
|
customers = data.get('collection', [])
|
|
|
|
if not customers:
|
|
break
|
|
|
|
all_customers.extend(customers)
|
|
logger.info(f" Hentet side {page + 1}: {len(customers)} kunder")
|
|
|
|
if len(customers) < page_size:
|
|
break
|
|
|
|
page += 1
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Fejl ved hentning fra e-conomic: {e}")
|
|
break
|
|
|
|
logger.info(f"✅ Hentet {len(all_customers)} kunder fra e-conomic")
|
|
return all_customers
|
|
|
|
def get_hub_customers(self) -> List[Dict]:
|
|
"""Hent alle Hub kunder"""
|
|
logger.info("📥 Henter kunder fra Hub database...")
|
|
|
|
query = """
|
|
SELECT id, name, cvr_number, economic_customer_number
|
|
FROM customers
|
|
WHERE name IS NOT NULL AND name != ''
|
|
ORDER BY name
|
|
"""
|
|
|
|
customers = execute_query(query)
|
|
logger.info(f"✅ Hentet {len(customers)} Hub kunder")
|
|
|
|
return customers
|
|
|
|
def normalize_name(self, name: str) -> str:
|
|
"""Normaliser kundenavn for matching"""
|
|
if not name:
|
|
return ""
|
|
# Lowercase, fjern punktum, mellemrum, A/S, ApS osv
|
|
name = name.lower()
|
|
name = name.replace('a/s', '').replace('aps', '').replace('i/s', '')
|
|
name = name.replace('.', '').replace(',', '').replace('-', '')
|
|
name = ''.join(name.split()) # Fjern alle mellemrum
|
|
return name
|
|
|
|
def build_name_mapping(self, economic_customers: List[Dict]) -> Dict[str, int]:
|
|
"""
|
|
Byg mapping fra normaliseret kundenavn til e-conomic customerNumber.
|
|
"""
|
|
name_map = {}
|
|
|
|
for customer in economic_customers:
|
|
customer_number = customer.get('customerNumber')
|
|
customer_name = customer.get('name', '').strip()
|
|
|
|
if not customer_number or not customer_name:
|
|
continue
|
|
|
|
normalized = self.normalize_name(customer_name)
|
|
|
|
if normalized:
|
|
if normalized in name_map:
|
|
logger.warning(f"⚠️ Duplikat navn '{customer_name}' i e-conomic (kunde {customer_number} og {name_map[normalized]})")
|
|
else:
|
|
name_map[normalized] = customer_number
|
|
|
|
logger.info(f"✅ Bygget navn mapping med {len(name_map)} unikke navne")
|
|
return name_map
|
|
|
|
async def relink_customers(self):
|
|
"""Hovedfunktion - relink alle kunder"""
|
|
logger.info("🚀 Starter relink proces...")
|
|
logger.info("=" * 60)
|
|
|
|
# Hent data
|
|
economic_customers = await self.fetch_economic_customers()
|
|
if not economic_customers:
|
|
logger.error("❌ Kunne ikke hente e-conomic kunder - afbryder")
|
|
return
|
|
|
|
hub_customers = self.get_hub_customers()
|
|
if not hub_customers:
|
|
logger.warning("⚠️ Ingen Hub kunder fundet")
|
|
return
|
|
|
|
# Byg navn mapping
|
|
name_map = self.build_name_mapping(economic_customers)
|
|
|
|
# Match og opdater
|
|
logger.info("")
|
|
logger.info("🔗 Matcher og opdaterer links...")
|
|
logger.info("=" * 60)
|
|
|
|
stats = {
|
|
'matched': 0,
|
|
'updated': 0,
|
|
'skipped_already_linked': 0,
|
|
'skipped_no_match': 0,
|
|
'errors': 0
|
|
}
|
|
|
|
for hub_customer in hub_customers:
|
|
hub_id = hub_customer['id']
|
|
hub_name = hub_customer['name']
|
|
current_economic_number = hub_customer.get('economic_customer_number')
|
|
|
|
# Normaliser navn og find match
|
|
normalized_name = self.normalize_name(hub_name)
|
|
|
|
if not normalized_name:
|
|
continue
|
|
|
|
# Find match i e-conomic
|
|
economic_number = name_map.get(normalized_name)
|
|
|
|
if not economic_number:
|
|
stats['skipped_no_match'] += 1
|
|
logger.debug(f" ⏭️ {hub_name} - ingen match i e-conomic")
|
|
continue
|
|
|
|
stats['matched'] += 1
|
|
|
|
# Check om allerede linket
|
|
if current_economic_number and not self.force:
|
|
if current_economic_number == economic_number:
|
|
stats['skipped_already_linked'] += 1
|
|
logger.debug(f" ✓ {hub_name} allerede linket til {economic_number}")
|
|
else:
|
|
stats['skipped_already_linked'] += 1
|
|
logger.warning(f" ⚠️ {hub_name} allerede linket til {current_economic_number} (ville være {economic_number}) - brug --force")
|
|
continue
|
|
|
|
# Opdater link
|
|
if self.dry_run:
|
|
logger.info(f" 🔍 {hub_name} → e-conomic kunde {economic_number}")
|
|
stats['updated'] += 1
|
|
else:
|
|
try:
|
|
execute_update(
|
|
"UPDATE customers SET economic_customer_number = %s WHERE id = %s",
|
|
(economic_number, hub_id)
|
|
)
|
|
logger.info(f" ✅ {hub_name} → e-conomic kunde {economic_number}")
|
|
stats['updated'] += 1
|
|
except Exception as e:
|
|
logger.error(f" ❌ Fejl ved opdatering af {hub_name}: {e}")
|
|
stats['errors'] += 1
|
|
|
|
# Vis statistik
|
|
logger.info("")
|
|
logger.info("=" * 60)
|
|
logger.info("📊 RESULTAT:")
|
|
logger.info(f" Kunder matchet: {stats['matched']}")
|
|
logger.info(f" Links opdateret: {stats['updated']}")
|
|
logger.info(f" Allerede linket (skipped): {stats['skipped_already_linked']}")
|
|
logger.info(f" Ingen match i e-conomic: {stats['skipped_no_match']}")
|
|
logger.info(f" Fejl: {stats['errors']}")
|
|
logger.info("=" * 60)
|
|
|
|
if self.dry_run:
|
|
logger.info("🔍 DRY RUN - ingen ændringer blev gemt")
|
|
logger.info(" Kør uden --dry-run for at gemme ændringer")
|
|
|
|
|
|
async def main():
|
|
parser = argparse.ArgumentParser(
|
|
description='Relink Hub kunder til e-conomic baseret på CVR-nummer'
|
|
)
|
|
parser.add_argument(
|
|
'--dry-run',
|
|
action='store_true',
|
|
help='Vis hvad der ville blive ændret uden at gemme'
|
|
)
|
|
parser.add_argument(
|
|
'--force',
|
|
action='store_true',
|
|
help='Overskriv eksisterende links'
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Initialize database connection
|
|
init_db()
|
|
|
|
service = EconomicRelinkService(dry_run=args.dry_run, force=args.force)
|
|
await service.relink_customers()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
asyncio.run(main())
|