# MIGRAZIONE ARCHITETTURA CONTABILE MULTI-ANNO ## 🎯 PROBLEMI IDENTIFICATI ### 1. **Collisione Dati Multi-Anno** ``` PROBLEMA: Import 2024 + 2025 → stesso database senza separazione SOLUZIONE: Campo `gestione_id` + `anno_gestione` in tutte le tabelle ``` ### 2. **Mancanza Continuità Contabile** ``` PROBLEMA: Dati non passano alla gestione successiva SOLUZIONE: Bilancio apertura + scritture partita doppia + saldi portati ``` ### 3. **Gestioni Separate Non Collegate** ``` PROBLEMA: O/R/S gestite separatamente → perdita visione d'insieme SOLUZIONE: Gestione unificata con campo tipo_gestione ``` ### 4. **Mancanza Tracciabilità Fiscale** ``` PROBLEMA: Ritenute d'acconto disperse in codici SOLUZIONE: Registro RA dedicato + collegamento operazioni ``` ## 🏗️ NUOVA ARCHITETTURA PROPOSTA ### **1. Gestioni Contabili (Master)** ```sql CREATE TABLE gestioni_contabili ( id BIGINT PRIMARY KEY, tenant_id VARCHAR(50), anno_gestione INTEGER, tipo_gestione ENUM('ordinaria', 'riscaldamento', 'straordinaria'), numero_straordinaria INTEGER NULL, -- per N_STRA denominazione VARCHAR(200), data_inizio DATE, data_fine DATE, stato ENUM('aperta', 'chiusa', 'consolidata'), bilancio_apertura JSON, -- saldi iniziali protocollo_prefix VARCHAR(10), -- O2024, R2024, S2024-001 INDEX(tenant_id, anno_gestione, tipo_gestione) ); ``` ### **2. Operazioni Contabili (Unificata)** ```sql ALTER TABLE operazioni_contabili ADD COLUMN ( gestione_id BIGINT REFERENCES gestioni_contabili(id), compet ENUM('C', 'P') DEFAULT 'C', natura2 VARCHAR(10), -- per partita doppia n_stra INTEGER NULL, -- collegamento straordinarie protocollo_numero INTEGER, -- auto-increment per gestione protocollo_completo VARCHAR(50), -- O2024-001, S2024-003-015 -- Tracciabilità modifiche voce_spesa_snapshot JSON, -- snapshot voce al momento registrazione modificato_da_user_id BIGINT NULL, modificato_il TIMESTAMP NULL, motivo_modifica TEXT NULL, -- Collegamento fiscale ritenuta_acconto_id BIGINT NULL, fattura_elettronica_id BIGINT NULL, INDEX(gestione_id, protocollo_numero), INDEX(tenant_id, compet), INDEX(natura2) ); ``` ### **3. Registro Ritenute d'Acconto** ```sql CREATE TABLE registro_ritenute_acconto ( id BIGINT PRIMARY KEY, tenant_id VARCHAR(50), gestione_id BIGINT REFERENCES gestioni_contabili(id), -- Dati ritenuta numero_progressivo INTEGER, data_competenza DATE, fornitore_id BIGINT, operazione_id BIGINT, -- collegamento operazione che ha generato RA -- Importi imponibile DECIMAL(15,4), aliquota_ritenuta DECIMAL(5,2), importo_ritenuta DECIMAL(15,4), -- Codici fiscali codice_tributo VARCHAR(10), rif_rda VARCHAR(50), -- riferimento Gescon -- Versamento stato_versamento ENUM('da_versare', 'versata', 'compensata'), data_versamento DATE NULL, f24_riferimento VARCHAR(100) NULL, -- Comunicazioni comunicazione_fornitore_inviata BOOLEAN DEFAULT FALSE, data_comunicazione_fornitore DATE NULL, INDEX(gestione_id, numero_progressivo), INDEX(fornitore_id, data_competenza), INDEX(stato_versamento) ); ``` ### **4. Incassi da Estratto Conto** ```sql CREATE TABLE incassi_estratto_conto ( id BIGINT PRIMARY KEY, tenant_id VARCHAR(50), gestione_id BIGINT REFERENCES gestioni_contabili(id), -- Dati Gescon Inc_da_ec id_gescon INTEGER, data_operazione DATE, data_valuta DATE, importo DECIMAL(15,4), descrizione TEXT, -- Collegamento incasso_id BIGINT NULL REFERENCES incassi(id), movimento_bancario_id BIGINT NULL, -- Stato riconciliazione stato ENUM('da_riconciliare', 'riconciliato', 'anomalo'), note_riconciliazione TEXT, INDEX(gestione_id, data_operazione), INDEX(stato) ); ``` ## 📊 ESTRATTI E IMPORT MULTIPLI DATABASE Invece di CSV, creiamo connessioni dirette ai file MDB: 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; } /** * Import rate emesse da generale_stabile */ private function importRateEmesse(string $mdbPath, int $year): int { // Questa funzione opera a livello di anno, non di gestione specifica $imported = 0; $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 { // TODO: Import rate in tabella dedicata $imported++; } catch (\Exception $e) { Log::error("Error importing rata", [ 'table' => $table, 'rata' => $rata, '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"; } } ]); // Gestione Riscaldamento $this->gestioni[$year]['riscaldamento'] = GestioneContabile::create([ 'tenant_id' => $this->tenantId, 'anno_gestione' => (int)$year, 'tipo_gestione' => 'riscaldamento', 'denominazione' => "Gestione Riscaldamento {$year}", 'data_inizio' => "{$year}-01-01", 'data_fine' => "{$year}-12-31", 'stato' => 'aperta', 'protocollo_prefix' => "R{$year}" ]); // Le straordinarie verranno create dinamicamente } } /** * 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; } } // Altri metodi helper... }