📋 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
519 lines
21 KiB
Python
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()
|