netgescon-master/netgescon-importer/advanced_importer.py
Pikappa2 480e7eafbd 🎯 NETGESCON - Setup iniziale repository completo
📋 Commit iniziale con:
-  Documentazione unificata in docs/
-  Codice Laravel in netgescon-laravel/
-  Script automazione in scripts/
-  Configurazione sync rsync
-  Struttura organizzata e pulita

🔄 Versione: 2025.07.19-1644
🎯 Sistema pronto per Git distribuito
2025-07-19 16:44:47 +02:00

519 lines
21 KiB
Python

#!/usr/bin/env python3
"""
NetGescon Importer - Advanced Bridge v2.0
Importazione dati da GESCON legacy a NetGescon Laravel
Author: NetGescon Development Team
Date: 15 Luglio 2025
"""
import os
import sys
import json
import logging
import sqlite3
import pandas as pd
import requests
import schedule
import time
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, List, Any, Optional, Tuple
from dataclasses import dataclass, asdict
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# === CONFIGURATION CLASSES ===
@dataclass
class GesconConfig:
"""Configurazione connessione GESCON"""
database_path: str
backup_frequency: str = "daily"
read_only: bool = True
timeout: int = 30
@dataclass
class NetGesconConfig:
"""Configurazione API NetGescon"""
base_url: str
api_token: str
api_version: str = "v1"
timeout: int = 60
max_retries: int = 3
@dataclass
class ImportConfig:
"""Configurazione importazione"""
batch_size: int = 100
enable_validation: bool = True
create_backups: bool = True
sync_mode: str = "incremental" # full, incremental
log_level: str = "INFO"
# === MAIN IMPORTER CLASS ===
class NetGesconImporter:
"""
Importer principale per sincronizzazione GESCON → NetGescon
"""
def __init__(self, config_file: str):
self.config = self._load_config(config_file)
self.gescon_db = None
self.session = None
self.logger = self._setup_logging()
self.stats = {
'stabili': {'imported': 0, 'errors': 0},
'unita': {'imported': 0, 'errors': 0},
'soggetti': {'imported': 0, 'errors': 0},
'movimenti': {'imported': 0, 'errors': 0}
}
def _load_config(self, config_file: str) -> Dict[str, Any]:
"""Carica configurazione da file JSON"""
try:
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
return config
except Exception as e:
print(f"Errore caricamento configurazione: {e}")
sys.exit(1)
def _setup_logging(self) -> logging.Logger:
"""Configura sistema di logging"""
log_dir = Path(self.config.get('log_directory', 'logs'))
log_dir.mkdir(exist_ok=True)
log_file = log_dir / f"netgescon_import_{datetime.now().strftime('%Y%m%d')}.log"
logging.basicConfig(
level=getattr(logging, self.config.get('log_level', 'INFO')),
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
handlers=[
logging.FileHandler(log_file, encoding='utf-8'),
logging.StreamHandler()
]
)
return logging.getLogger('NetGesconImporter')
def _setup_http_session(self) -> requests.Session:
"""Configura sessione HTTP con retry automatico"""
session = requests.Session()
retry_strategy = Retry(
total=self.config['netgescon']['max_retries'],
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
# Headers comuni
session.headers.update({
'Authorization': f"Bearer {self.config['netgescon']['api_token']}",
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'NetGescon-Importer/2.0'
})
return session
def connect_gescon(self) -> bool:
"""Connessione al database GESCON"""
try:
db_path = self.config['gescon']['database_path']
self.gescon_db = sqlite3.connect(db_path, timeout=30)
self.gescon_db.row_factory = sqlite3.Row # Per accesso by name
# Test connessione
cursor = self.gescon_db.cursor()
cursor.execute("SELECT COUNT(*) FROM sqlite_master WHERE type='table';")
table_count = cursor.fetchone()[0]
self.logger.info(f"Connesso a GESCON: {table_count} tabelle trovate")
return True
except Exception as e:
self.logger.error(f"Errore connessione GESCON: {e}")
return False
def test_netgescon_connection(self) -> bool:
"""Test connessione API NetGescon"""
try:
url = f"{self.config['netgescon']['base_url']}/api/health"
response = self.session.get(url, timeout=10)
response.raise_for_status()
data = response.json()
self.logger.info(f"NetGescon API: {data.get('status', 'OK')} - {data.get('version', 'unknown')}")
return True
except Exception as e:
self.logger.error(f"Errore connessione NetGescon API: {e}")
return False
# === MAPPING DATA METHODS ===
def map_stabile_data(self, gescon_row: sqlite3.Row) -> Dict[str, Any]:
"""Mappa dati stabile da GESCON a NetGescon"""
try:
return {
'codice_esterno': str(gescon_row.get('id_stabile', '')),
'denominazione': gescon_row.get('denominazione', '').strip(),
'indirizzo': gescon_row.get('indirizzo', '').strip(),
'citta': gescon_row.get('citta', '').strip(),
'cap': gescon_row.get('cap', '').strip(),
'provincia': gescon_row.get('provincia', '').strip(),
'codice_fiscale': gescon_row.get('codice_fiscale', '').strip(),
'partita_iva': gescon_row.get('partita_iva', '').strip(),
'telefono': gescon_row.get('telefono', '').strip(),
'email': gescon_row.get('email', '').strip(),
# Campi avanzati
'numero_unita': gescon_row.get('numero_unita', 0) or 0,
'anno_costruzione': gescon_row.get('anno_costruzione'),
'numero_piani': gescon_row.get('numero_piani', 0) or 0,
'superficie_totale': float(gescon_row.get('superficie_totale', 0) or 0),
'ha_ascensore': bool(gescon_row.get('ascensore', 0)),
'ha_riscaldamento_centralizzato': bool(gescon_row.get('riscaldamento_centrale', 0)),
# Configurazioni automatiche
'calcolo_automatico_ripartizioni': True,
'gestione_fondi_automatica': True,
'notifiche_attive': True,
# Metadati import
'importato_da_gescon': True,
'data_ultima_sincronizzazione': datetime.now().isoformat(),
'note_import': f"Importato da GESCON ID: {gescon_row.get('id_stabile')}"
}
except Exception as e:
self.logger.error(f"Errore mapping stabile {gescon_row.get('id_stabile')}: {e}")
return None
def map_unita_data(self, gescon_row: sqlite3.Row, stabile_id: int) -> Dict[str, Any]:
"""Mappa dati unità immobiliare da GESCON a NetGescon"""
try:
return {
'stabile_id': stabile_id,
'codice_esterno': str(gescon_row.get('id_unita', '')),
'denominazione': gescon_row.get('denominazione', '').strip() or f"Unità {gescon_row.get('numero_interno', '')}",
'numero_interno': gescon_row.get('numero_interno', '').strip(),
'piano': int(gescon_row.get('piano', 0) or 0),
'scala': gescon_row.get('scala', '').strip(),
'palazzina': gescon_row.get('palazzina', '').strip(),
# Superfici
'superficie_commerciale': float(gescon_row.get('superficie_commerciale', 0) or 0),
'superficie_calpestabile': float(gescon_row.get('superficie_calpestabile', 0) or 0),
'superficie_balconi': float(gescon_row.get('superficie_balconi', 0) or 0),
'superficie_terrazzi': float(gescon_row.get('superficie_terrazzi', 0) or 0),
# Dati tecnici
'numero_vani': int(gescon_row.get('vani', 0) or 0),
'numero_bagni': int(gescon_row.get('bagni', 0) or 0),
'numero_balconi': int(gescon_row.get('balconi', 0) or 0),
'classe_energetica': gescon_row.get('classe_energetica', '').strip(),
'anno_costruzione': gescon_row.get('anno_costruzione'),
'stato_conservazione': self._map_stato_conservazione(gescon_row.get('stato')),
'necessita_lavori': bool(gescon_row.get('necessita_lavori', 0)),
# Millesimi
'millesimi_proprieta': float(gescon_row.get('millesimi_proprieta', 0) or 0),
'millesimi_riscaldamento': float(gescon_row.get('millesimi_riscaldamento', 0) or 0),
'millesimi_ascensore': float(gescon_row.get('millesimi_ascensore', 0) or 0),
'millesimi_scale': float(gescon_row.get('millesimi_scale', 0) or 0),
'millesimi_pulizie': float(gescon_row.get('millesimi_pulizie', 0) or 0),
# Automazioni
'calcolo_automatico_millesimi': True,
'notifiche_subentri': True,
# Metadati import
'importato_da_gescon': True,
'data_ultima_sincronizzazione': datetime.now().isoformat(),
'note_tecniche': f"Importato da GESCON ID: {gescon_row.get('id_unita')}"
}
except Exception as e:
self.logger.error(f"Errore mapping unità {gescon_row.get('id_unita')}: {e}")
return None
def map_soggetto_data(self, gescon_row: sqlite3.Row) -> Dict[str, Any]:
"""Mappa dati soggetto da GESCON a NetGescon"""
try:
return {
'codice_esterno': str(gescon_row.get('id_soggetto', '')),
'tipo_soggetto': 'persona_fisica' if gescon_row.get('tipo') == 'F' else 'persona_giuridica',
'denominazione': gescon_row.get('denominazione', '').strip(),
'nome': gescon_row.get('nome', '').strip(),
'cognome': gescon_row.get('cognome', '').strip(),
'codice_fiscale': gescon_row.get('codice_fiscale', '').strip(),
'partita_iva': gescon_row.get('partita_iva', '').strip(),
# Indirizzo
'indirizzo': gescon_row.get('indirizzo', '').strip(),
'citta': gescon_row.get('citta', '').strip(),
'cap': gescon_row.get('cap', '').strip(),
'provincia': gescon_row.get('provincia', '').strip(),
# Contatti
'telefono': gescon_row.get('telefono', '').strip(),
'cellulare': gescon_row.get('cellulare', '').strip(),
'email': gescon_row.get('email', '').strip(),
'pec': gescon_row.get('pec', '').strip(),
# Metadati
'attivo': bool(gescon_row.get('attivo', 1)),
'importato_da_gescon': True,
'data_ultima_sincronizzazione': datetime.now().isoformat(),
'note': f"Importato da GESCON ID: {gescon_row.get('id_soggetto')}"
}
except Exception as e:
self.logger.error(f"Errore mapping soggetto {gescon_row.get('id_soggetto')}: {e}")
return None
def _map_stato_conservazione(self, stato_gescon: str) -> str:
"""Mappa stato conservazione da GESCON a NetGescon"""
mapping = {
'O': 'ottimo',
'B': 'buono',
'D': 'discreto',
'C': 'cattivo',
'1': 'ottimo',
'2': 'buono',
'3': 'discreto',
'4': 'cattivo'
}
return mapping.get(str(stato_gescon).upper(), 'buono')
# === IMPORT METHODS ===
def import_stabili(self) -> bool:
"""Importa tutti gli stabili da GESCON"""
try:
self.logger.info("Inizio importazione stabili...")
cursor = self.gescon_db.cursor()
cursor.execute("""
SELECT * FROM stabili
WHERE attivo = 1
ORDER BY id_stabile
""")
stabili = cursor.fetchall()
self.logger.info(f"Trovati {len(stabili)} stabili da importare")
for stabile_row in stabili:
try:
stabile_data = self.map_stabile_data(stabile_row)
if not stabile_data:
continue
# Invia a NetGescon
success = self._send_to_netgescon('stabili', stabile_data)
if success:
self.stats['stabili']['imported'] += 1
self.logger.debug(f"Stabile {stabile_data['denominazione']} importato")
else:
self.stats['stabili']['errors'] += 1
except Exception as e:
self.logger.error(f"Errore importazione stabile {stabile_row.get('id_stabile')}: {e}")
self.stats['stabili']['errors'] += 1
self.logger.info(f"Importazione stabili completata: {self.stats['stabili']['imported']} successi, {self.stats['stabili']['errors']} errori")
return True
except Exception as e:
self.logger.error(f"Errore importazione stabili: {e}")
return False
def import_unita_immobiliari(self) -> bool:
"""Importa tutte le unità immobiliari da GESCON"""
try:
self.logger.info("Inizio importazione unità immobiliari...")
cursor = self.gescon_db.cursor()
cursor.execute("""
SELECT u.*, s.id_stabile_netgescon
FROM unita_immobiliari u
JOIN stabili s ON u.id_stabile = s.id_stabile
WHERE u.attiva = 1 AND s.importato_netgescon = 1
ORDER BY u.id_stabile, u.id_unita
""")
unita = cursor.fetchall()
self.logger.info(f"Trovate {len(unita)} unità immobiliari da importare")
for unita_row in unita:
try:
unita_data = self.map_unita_data(unita_row, unita_row['id_stabile_netgescon'])
if not unita_data:
continue
# Invia a NetGescon
success = self._send_to_netgescon('unita-immobiliari', unita_data)
if success:
self.stats['unita']['imported'] += 1
self.logger.debug(f"Unità {unita_data['denominazione']} importata")
else:
self.stats['unita']['errors'] += 1
except Exception as e:
self.logger.error(f"Errore importazione unità {unita_row.get('id_unita')}: {e}")
self.stats['unita']['errors'] += 1
self.logger.info(f"Importazione unità completata: {self.stats['unita']['imported']} successi, {self.stats['unita']['errors']} errori")
return True
except Exception as e:
self.logger.error(f"Errore importazione unità: {e}")
return False
def _send_to_netgescon(self, endpoint: str, data: Dict[str, Any]) -> bool:
"""Invia dati a NetGescon API"""
try:
url = f"{self.config['netgescon']['base_url']}/api/v1/import/{endpoint}"
response = self.session.post(
url,
json=data,
timeout=self.config['netgescon']['timeout']
)
if response.status_code in [200, 201]:
return True
elif response.status_code == 409:
# Record già esistente - aggiorna
self.logger.debug(f"Record esistente per {endpoint}, tentativo aggiornamento...")
return self._update_netgescon_record(endpoint, data)
else:
self.logger.error(f"Errore API NetGescon {endpoint}: {response.status_code} - {response.text}")
return False
except Exception as e:
self.logger.error(f"Errore invio dati a {endpoint}: {e}")
return False
def _update_netgescon_record(self, endpoint: str, data: Dict[str, Any]) -> bool:
"""Aggiorna record esistente in NetGescon"""
try:
# Usa codice_esterno per identificare il record
codice_esterno = data.get('codice_esterno')
if not codice_esterno:
return False
url = f"{self.config['netgescon']['base_url']}/api/v1/import/{endpoint}/{codice_esterno}"
response = self.session.put(
url,
json=data,
timeout=self.config['netgescon']['timeout']
)
return response.status_code in [200, 204]
except Exception as e:
self.logger.error(f"Errore aggiornamento record {endpoint}: {e}")
return False
# === MAIN EXECUTION ===
def run_full_import(self) -> bool:
"""Esegue importazione completa"""
try:
self.logger.info("=== INIZIO IMPORTAZIONE COMPLETA GESCON → NETGESCON ===")
# Setup
if not self.connect_gescon():
return False
self.session = self._setup_http_session()
if not self.test_netgescon_connection():
return False
# Import sequence
start_time = datetime.now()
success = (
self.import_stabili() and
self.import_unita_immobiliari()
# TODO: add import_soggetti() and import_movimenti()
)
duration = datetime.now() - start_time
# Report finale
self.logger.info("=== REPORT IMPORTAZIONE COMPLETATA ===")
self.logger.info(f"Durata totale: {duration}")
self.logger.info(f"Stabili: {self.stats['stabili']['imported']} importati, {self.stats['stabili']['errors']} errori")
self.logger.info(f"Unità: {self.stats['unita']['imported']} importate, {self.stats['unita']['errors']} errori")
return success
except Exception as e:
self.logger.error(f"Errore importazione completa: {e}")
return False
finally:
if self.gescon_db:
self.gescon_db.close()
if self.session:
self.session.close()
# === MAIN EXECUTION ===
def main():
"""Entry point principale"""
import argparse
parser = argparse.ArgumentParser(description='NetGescon Importer v2.0')
parser.add_argument('--config', '-c', default='config/importer_config.json',
help='File di configurazione')
parser.add_argument('--mode', '-m', choices=['full', 'incremental', 'test'],
default='incremental', help='Modalità importazione')
parser.add_argument('--schedule', '-s', action='store_true',
help='Esegue in modalità scheduler')
args = parser.parse_args()
try:
importer = NetGesconImporter(args.config)
if args.schedule:
# Modalità scheduler
schedule.every().day.at("02:00").do(importer.run_full_import)
schedule.every().hour.do(importer.run_incremental_import)
importer.logger.info("Scheduler avviato. CTRL+C per terminare.")
while True:
schedule.run_pending()
time.sleep(60)
else:
# Modalità singola esecuzione
if args.mode == 'full':
success = importer.run_full_import()
elif args.mode == 'test':
success = importer.test_netgescon_connection()
else:
success = importer.run_incremental_import()
sys.exit(0 if success else 1)
except KeyboardInterrupt:
print("\nImportazione interrotta dall'utente")
sys.exit(0)
except Exception as e:
print(f"Errore fatale: {e}")
sys.exit(1)
if __name__ == "__main__":
main()