tenantId = $tenantId; } /** * Import completo multi-anno da directory Gescon */ public function importFromGesconDirectory(string $baseDir): array { Log::info("Starting multi-year Gescon import", ['tenant' => $this->tenantId, 'dir' => $baseDir]); // 1. Scansiona directory per anni $years = $this->scanGesconYears($baseDir); // 2. Crea gestioni contabili per ogni anno $this->createGestioni($years); // 3. Import dati per ogni gestione $results = []; foreach ($years as $year => $paths) { $results[$year] = $this->importGestioneYear($year, $paths); } // 4. Crea bilanci di apertura per continuità $this->createBilanciApertura(); return [ 'success' => true, 'gestioni_create' => count($this->gestioni), 'years_imported' => array_keys($results), 'details' => $results, ]; } /** * Scansiona directory Gescon per identificare anni */ private function scanGesconYears(string $baseDir): array { $years = []; // Pattern directory: /Gescon/2024/singolo_anno.mdb, /Gescon/2025/singolo_anno.mdb $gesconDirs = glob($baseDir . '/*/'); foreach ($gesconDirs as $dir) { $year = basename(rtrim($dir, '/')); if (is_numeric($year) && strlen($year) == 4) { $paths = [ 'singolo_anno' => $dir . 'singolo_anno.mdb', 'generale_stabile' => $dir . 'generale_stabile.mdb', ]; // Verifica esistenza file if (file_exists($paths['singolo_anno'])) { $years[$year] = $paths; } } } return $years; } /** * Scansiona gestioni direttamente dal file MDB */ private function scanGestioniFromMdb(string $mdbPath, int $year): array { $gestioni = []; // Gestione Ordinaria (sempre presente) $gestioni[] = [ 'anno_gestione' => $year, 'tipo_gestione' => 'ordinaria', 'denominazione' => "Gestione Ordinaria {$year}", 'data_inizio' => "{$year}-01-01", 'data_fine' => "{$year}-12-31", 'protocollo_prefix' => "O{$year}", ]; // Gestione Riscaldamento (se presente) $opRiscaldamento = $this->queryMdb($mdbPath, " SELECT COUNT(*) as cnt FROM operazioni WHERE gestione = 'R' OR gestione = 'r' "); if (! empty($opRiscaldamento) && ($opRiscaldamento[0]['cnt'] ?? 0) > 0) { $gestioni[] = [ 'anno_gestione' => $year, 'tipo_gestione' => 'riscaldamento', 'denominazione' => "Gestione Riscaldamento {$year}", 'data_inizio' => "{$year}-01-01", 'data_fine' => "{$year}-12-31", 'protocollo_prefix' => "R{$year}", ]; } // Gestioni Straordinarie (dinamiche) $straordinarie = $this->queryMdb($mdbPath, " SELECT DISTINCT N_STRA as numero FROM operazioni WHERE (gestione = 'S' OR gestione = 's') AND N_STRA IS NOT NULL AND N_STRA > 0 ORDER BY N_STRA "); foreach ($straordinarie as $stra) { $numero = (int) $stra['numero']; $gestioni[] = [ 'anno_gestione' => $year, 'tipo_gestione' => 'straordinaria', 'numero_straordinaria' => $numero, 'denominazione' => "Gestione Straordinaria {$year} - {$numero}", 'data_inizio' => "{$year}-01-01", 'data_fine' => "{$year}-12-31", 'protocollo_prefix' => "S{$year}-{$numero}", ]; } return $gestioni; } /** * Crea o aggiorna gestione */ private function createOrUpdateGestione(array $gestioneData): GestioneContabile { $gestione = GestioneContabile::updateOrCreate([ 'tenant_id' => $this->tenantId, 'anno_gestione' => $gestioneData['anno_gestione'], 'tipo_gestione' => $gestioneData['tipo_gestione'], 'numero_straordinaria' => $gestioneData['numero_straordinaria'] ?? null, ], [ 'denominazione' => $gestioneData['denominazione'], 'data_inizio' => $gestioneData['data_inizio'], 'data_fine' => $gestioneData['data_fine'], 'stato' => 'aperta', 'protocollo_prefix' => $gestioneData['protocollo_prefix'], ]); // Cache per lookup veloce $year = $gestioneData['anno_gestione']; $tipo = $gestioneData['tipo_gestione']; if ($tipo === 'straordinaria') { $key = "straordinaria_{$gestioneData['numero_straordinaria']}"; } else { $key = $tipo; } $this->gestioni[$year][$key] = $gestione; return $gestione; } /** * Import operazioni per gestione specifica */ private function importOperazioniForGestione(GestioneContabile $gestione, string $mdbPath): int { // Filtro per tipo gestione $whereClause = $this->buildGestioneWhereClause($gestione); $operazioni = $this->queryMdb($mdbPath, " SELECT *, IIF(ISNULL(COMPET), 'C', COMPET) as compet_clean, IIF(ISNULL(NATURA2), '', NATURA2) as natura2_clean, IIF(ISNULL(N_STRA), 0, N_STRA) as n_stra_clean FROM operazioni WHERE {$whereClause} ORDER BY n_operazione "); $imported = 0; foreach ($operazioni as $op) { try { $protocollo = $this->generateProtocollo($gestione->id, $op); $voceSnapshot = $this->getVoceSpesaSnapshot($mdbPath, $op['cod_voce'] ?? ''); OperazioneContabile::create([ 'tenant_id' => $this->tenantId, 'gestione_id' => $gestione->id, 'numero_operazione' => $op['n_operazione'], 'data_operazione' => $this->parseGesconDate($op['data_oper']), 'importo' => (float) ($op['euro'] ?? 0), 'descrizione' => $op['descrizione'] ?? '', 'cod_voce_gescon' => $op['cod_voce'] ?? '', 'cod_beneficiario_gescon' => $op['cod_ben'] ?? '', 'compet' => $op['compet_clean'], 'natura2' => $op['natura2_clean'], 'n_stra' => (int) $op['n_stra_clean'], 'protocollo_numero' => $protocollo['numero'], 'protocollo_completo' => $protocollo['completo'], 'voce_spesa_snapshot' => json_encode($voceSnapshot), 'metadati_gescon' => json_encode($op), ]); $imported++; } catch (\Exception $e) { Log::error("Error importing operazione for gestione", [ 'gestione_id' => $gestione->id, 'operazione' => $op, 'error' => $e->getMessage(), ]); } } return $imported; } /** * Import incassi per gestione */ private function importIncassiForGestione(GestioneContabile $gestione, string $mdbPath): int { $whereClause = $this->buildGestioneWhereClause($gestione); $incassi = $this->queryMdb($mdbPath, " SELECT * FROM incassi WHERE {$whereClause} ORDER BY data_inc "); $imported = 0; foreach ($incassi as $inc) { try { // Import incasso... $imported++; } catch (\Exception $e) { Log::error("Error importing incasso", [ 'gestione_id' => $gestione->id, 'incasso' => $inc, 'error' => $e->getMessage(), ]); } } return $imported; } /** * Import ritenute per gestione */ private function importRitenuteFOrGestione(GestioneContabile $gestione, string $mdbPath): int { $ritenute = $this->queryMdb($mdbPath, " SELECT n.*, o.data_oper, o.euro as imponibile_operazione FROM Nettovers_RDA n LEFT JOIN operazioni o ON n.Rif_RDA = o.n_operazione WHERE n.Rif_RDA IS NOT NULL AND {$this->buildGestioneJoinClause($gestione, 'o')} ORDER BY n.Rif_RDA "); $imported = 0; foreach ($ritenute as $ra) { try { RegistroRitenuteAcconto::create([ 'tenant_id' => $this->tenantId, 'gestione_id' => $gestione->id, 'numero_progressivo' => $ra['Rif_RDA'], 'data_competenza' => $this->parseGesconDate($ra['data_oper']), 'imponibile' => (float) ($ra['imponibile_operazione'] ?? 0), 'aliquota_ritenuta' => (float) ($ra['aliquota'] ?? 20), 'importo_ritenuta' => (float) ($ra['ritenuta'] ?? 0), 'rif_rda' => $ra['Rif_RDA'], 'stato_versamento' => 'da_versare', 'metadati_gescon' => json_encode($ra), ]); $imported++; } catch (\Exception $e) { Log::error("Error importing ritenuta", [ 'gestione_id' => $gestione->id, 'ritenuta' => $ra, 'error' => $e->getMessage(), ]); } } return $imported; } /** * Import incassi estratto conto per gestione */ private function importIncassiEstrattoContoForGestione(GestioneContabile $gestione, string $mdbPath): int { $incassiEc = $this->queryMdb($mdbPath, " SELECT * FROM Inc_da_ec ORDER BY data_oper "); $imported = 0; foreach ($incassiEc as $inc) { try { IncassoEstrattoConto::create([ 'tenant_id' => $this->tenantId, 'gestione_id' => $gestione->id, 'id_gescon' => $inc['id'] ?? null, 'data_operazione' => $this->parseGesconDate($inc['data_oper']), 'data_valuta' => $this->parseGesconDate($inc['data_valuta']), 'importo' => (float) ($inc['importo'] ?? 0), 'descrizione' => $inc['descrizione'] ?? '', 'stato' => 'da_riconciliare', 'metadati_gescon' => json_encode($inc), ]); $imported++; } catch (\Exception $e) { Log::error("Error importing incasso EC", [ 'gestione_id' => $gestione->id, 'incasso' => $inc, 'error' => $e->getMessage(), ]); } } return $imported; } /** * Costruisce WHERE clause per filtrare per gestione */ private function buildGestioneWhereClause(GestioneContabile $gestione): string { switch ($gestione->tipo_gestione) { case 'ordinaria': return "(gestione = 'O' OR gestione = 'o' OR gestione IS NULL OR gestione = '')"; case 'riscaldamento': return "(gestione = 'R' OR gestione = 'r')"; case 'straordinaria': $numero = $gestione->numero_straordinaria; return "(gestione = 'S' OR gestione = 's') AND N_STRA = {$numero}"; default: return "1=1"; } } /** * Costruisce JOIN clause per filtrare per gestione */ private function buildGestioneJoinClause(GestioneContabile $gestione, string $alias = ''): string { $prefix = $alias ? "{$alias}." : ''; switch ($gestione->tipo_gestione) { case 'ordinaria': return "({$prefix}gestione = 'O' OR {$prefix}gestione = 'o' OR {$prefix}gestione IS NULL OR {$prefix}gestione = '')"; case 'riscaldamento': return "({$prefix}gestione = 'R' OR {$prefix}gestione = 'r')"; case 'straordinaria': $numero = $gestione->numero_straordinaria; return "({$prefix}gestione = 'S' OR {$prefix}gestione = 's') AND {$prefix}N_STRA = {$numero}"; default: return "1=1"; } } /** * Import dati per una specifica gestione/anno */ private function importGestioneYear(string $year, array $paths): array { $results = [ 'operazioni' => 0, 'incassi' => 0, 'ritenute' => 0, 'rate_emesse' => 0, 'incassi_ec' => 0, ]; // 1. Import operazioni contabili $results['operazioni'] = $this->importOperazioni($year, $paths['singolo_anno']); // 2. Import incassi $results['incassi'] = $this->importIncassi($year, $paths['singolo_anno']); // 3. Import ritenute d'acconto $results['ritenute'] = $this->importRitenuteAcconto($year, $paths['singolo_anno']); // 4. Import rate emesse (da generale_stabile.mdb) if (file_exists($paths['generale_stabile'])) { $results['rate_emesse'] = $this->importRateEmesse($year, $paths['generale_stabile']); } // 5. Import incassi estratto conto $results['incassi_ec'] = $this->importIncassiEstrattoConto($year, $paths['singolo_anno']); return $results; } /** * Import operazioni con gestione COMPET e NATURA2 */ private function importOperazioni(string $year, string $mdbPath): int { $imported = 0; // Query MDB per operazioni $operazioni = $this->queryMdb($mdbPath, " SELECT *, IIF(ISNULL(COMPET), 'C', COMPET) as compet_clean, IIF(ISNULL(NATURA2), '', NATURA2) as natura2_clean, IIF(ISNULL(N_STRA), 0, N_STRA) as n_stra_clean FROM operazioni ORDER BY n_operazione "); foreach ($operazioni as $op) { try { // Determina gestione $gestioneId = $this->determineGestioneId($year, $op); // Genera protocollo $protocollo = $this->generateProtocollo($gestioneId, $op); // Snapshot voce spesa $voceSnapshot = $this->getVoceSpesaSnapshot($mdbPath, $op['cod_voce'] ?? ''); OperazioneContabile::create([ 'tenant_id' => $this->tenantId, 'gestione_id' => $gestioneId, 'numero_operazione' => $op['n_operazione'], 'data_operazione' => $this->parseGesconDate($op['data_oper']), 'importo' => (float) ($op['euro'] ?? 0), 'descrizione' => $op['descrizione'] ?? '', 'cod_voce_gescon' => $op['cod_voce'] ?? '', 'cod_beneficiario_gescon' => $op['cod_ben'] ?? '', 'compet' => $op['compet_clean'], 'natura2' => $op['natura2_clean'], 'n_stra' => (int) $op['n_stra_clean'], 'protocollo_numero' => $protocollo['numero'], 'protocollo_completo' => $protocollo['completo'], 'voce_spesa_snapshot' => json_encode($voceSnapshot), 'metadati_gescon' => json_encode($op), ]); $imported++; } catch (\Exception $e) { Log::error("Error importing operazione", [ 'year' => $year, 'operazione' => $op, 'error' => $e->getMessage(), ]); } } return $imported; } /** * Import ritenute d'acconto da Nettovers_RDA */ private function importRitenuteAcconto(string $year, string $mdbPath): int { $imported = 0; // Query per ritenute $ritenute = $this->queryMdb($mdbPath, " SELECT n.*, o.data_oper, o.euro as imponibile_operazione FROM Nettovers_RDA n LEFT JOIN operazioni o ON n.Rif_RDA = o.n_operazione WHERE n.Rif_RDA IS NOT NULL ORDER BY n.Rif_RDA "); foreach ($ritenute as $ra) { try { $gestioneId = $this->gestioni[$year]['ordinaria']->id; RegistroRitenuteAcconto::create([ 'tenant_id' => $this->tenantId, 'gestione_id' => $gestioneId, 'numero_progressivo' => $ra['Rif_RDA'], 'data_competenza' => $this->parseGesconDate($ra['data_oper']), 'imponibile' => (float) ($ra['imponibile_operazione'] ?? 0), 'aliquota_ritenuta' => (float) ($ra['aliquota'] ?? 20), 'importo_ritenuta' => (float) ($ra['ritenuta'] ?? 0), 'rif_rda' => $ra['Rif_RDA'], 'stato_versamento' => 'da_versare', // Default 'metadati_gescon' => json_encode($ra), ]); $imported++; } catch (\Exception $e) { Log::error("Error importing ritenuta", [ 'year' => $year, 'ritenuta' => $ra, 'error' => $e->getMessage(), ]); } } return $imported; } /** * Import incassi estratto conto da Inc_da_ec */ private function importIncassiEstrattoConto(string $year, string $mdbPath): int { $imported = 0; $incassiEc = $this->queryMdb($mdbPath, " SELECT * FROM Inc_da_ec ORDER BY data_oper "); foreach ($incassiEc as $inc) { try { $gestioneId = $this->gestioni[$year]['ordinaria']->id; IncassoEstrattoConto::create([ 'tenant_id' => $this->tenantId, 'gestione_id' => $gestioneId, 'id_gescon' => $inc['id'] ?? null, 'data_operazione' => $this->parseGesconDate($inc['data_oper']), 'data_valuta' => $this->parseGesconDate($inc['data_valuta']), 'importo' => (float) ($inc['importo'] ?? 0), 'descrizione' => $inc['descrizione'] ?? '', 'stato' => 'da_riconciliare', 'metadati_gescon' => json_encode($inc), ]); $imported++; } catch (\Exception $e) { Log::error("Error importing incasso EC", [ 'year' => $year, 'incasso' => $inc, 'error' => $e->getMessage(), ]); } } return $imported; } /** * Import rate emesse da generale_stabile.mdb */ private function importRateEmesse(string $year, string $mdbPath): int { $imported = 0; // Unifica EMESS_DET, EMESS_DET_2, EMESS_GEN $tables = ['EMESS_DET', 'EMESS_DET_2', 'EMESS_GEN']; foreach ($tables as $table) { $rate = $this->queryMdb($mdbPath, "SELECT * FROM {$table} ORDER BY data_em"); foreach ($rate as $rata) { try { // Logica import rate... $imported++; } catch (\Exception $e) { Log::error("Error importing rata", ['table' => $table, 'rata' => $rata, 'error' => $e->getMessage()]); } } } return $imported; } /** * Determina gestione ID basata su dati operazione */ private function determineGestioneId(string $year, array $operazione): int { $gestione = $operazione['gestione'] ?? 'O'; $nStra = (int) ($operazione['N_STRA'] ?? 0); switch (strtoupper($gestione)) { case 'R': return $this->gestioni[$year]['riscaldamento']->id; case 'S': // Crea gestione straordinaria se non esiste return $this->getOrCreateStraordinaria($year, $nStra); default: return $this->gestioni[$year]['ordinaria']->id; } } /** * Ottiene o crea gestione straordinaria */ private function getOrCreateStraordinaria(string $year, int $nStra): int { $key = "straordinaria_{$nStra}"; if (! isset($this->gestioni[$year][$key])) { $this->gestioni[$year][$key] = GestioneContabile::create([ 'tenant_id' => $this->tenantId, 'anno_gestione' => (int) $year, 'tipo_gestione' => 'straordinaria', 'numero_straordinaria' => $nStra, 'denominazione' => "Gestione Straordinaria {$year} - {$nStra}", 'data_inizio' => "{$year}-01-01", 'data_fine' => "{$year}-12-31", 'stato' => 'aperta', 'protocollo_prefix' => "S{$year}-{$nStra}", ]); } return $this->gestioni[$year][$key]->id; } /** * Query diretta su file MDB */ private function queryMdb(string $mdbPath, string $query): array { // Usa mdb-tools per query diretta $command = sprintf( 'mdb-sql "%s" <<< "%s"', escapeshellarg($mdbPath), escapeshellarg($query) ); $output = shell_exec($command); // Parser output MDB in array associativo return $this->parseMdbOutput($output); } /** * Parser output mdb-sql in array */ private function parseMdbOutput(string $output): array { $lines = explode("\n", trim($output)); if (empty($lines)) { return []; } $headers = array_map('trim', explode("\t", array_shift($lines))); $data = []; foreach ($lines as $line) { if (empty(trim($line))) { continue; } $values = explode("\t", $line); $row = array_combine($headers, $values); $data[] = $row; } return $data; } /** * Genera protocollo per operazione */ private function generateProtocollo(int $gestioneId, array $operazione): array { $gestione = GestioneContabile::find($gestioneId); $numero = $gestione->getNextProtocolNumber(); return [ 'numero' => $numero, 'completo' => $gestione->protocollo_prefix . '-' . str_pad($numero, 4, '0', STR_PAD_LEFT), ]; } /** * Snapshot voce spesa */ private function getVoceSpesaSnapshot(string $mdbPath, string $codVoce): array { if (empty($codVoce)) { return []; } $voci = $this->queryMdb($mdbPath, " SELECT * FROM voci_spesa WHERE cod_voce = '{$codVoce}' "); return $voci[0] ?? []; } /** * Parse data Gescon */ private function parseGesconDate($dateValue): ?string { if (empty($dateValue)) { return null; } // Prova vari formati date Gescon/Access $formats = ['Y-m-d', 'd/m/Y', 'm/d/Y', 'Y-m-d H:i:s']; foreach ($formats as $format) { $date = \DateTime::createFromFormat($format, $dateValue); if ($date !== false) { return $date->format('Y-m-d'); } } return null; } }