option('path') ?? '/mnt/gescon-archives/gescon'), '/'); $dry = (bool) $this->option('dry-run'); $years = $this->parseYearsCsv((string) ($this->option('years') ?? '2024,2025')); $waterYearsRaw = (string) ($this->option('water-years') ?? '2024'); $openYearsRaw = (string) ($this->option('open-years') ?? '2024,2025'); if (empty($years)) { $this->error('Nessun anno valido in --years.'); return self::FAILURE; } $stabili = $this->resolveStabili($path); if (empty($stabili)) { $this->warn('Nessuno stabile risolto da processare.'); return self::SUCCESS; } $this->info('Riallineamento stabili avviato' . ($dry ? ' (DRY RUN)' : '')); $this->line('- path: ' . $path); $this->line('- anni contabili: ' . implode(', ', $years)); $this->line('- anni acqua: ' . $waterYearsRaw); $summary = [ 'processed' => 0, 'skipped' => 0, 'failed' => 0, ]; foreach ($stabili as $code) { $summary['processed']++; $this->newLine(); $this->line('=== STABILE ' . $code . ' ==='); $stableDir = $path . '/' . $code; $generaleMdb = $stableDir . '/generale_stabile.mdb'; if (! is_dir($stableDir) || ! is_file($generaleMdb)) { $this->warn('Skip: archivio mancante (' . $stableDir . ').'); $summary['skipped']++; continue; } $yearToDirMap = $this->buildLegacyYearDirectoryMap($generaleMdb); if (empty($yearToDirMap)) { $this->warn('Tabella anni non disponibile o non leggibile: fallback diretto su cartelle anno numeriche.'); } try { $this->callGesconImportGestioni($code, $path, $dry); foreach ($years as $year) { $legacyDir = $yearToDirMap[$year] ?? sprintf('%04d', $year); $singoloMdb = $stableDir . '/' . $legacyDir . '/singolo_anno.mdb'; if (! is_file($singoloMdb)) { $this->warn('Anno ' . $year . ' (dir ' . $legacyDir . ') non trovato: ' . $singoloMdb); continue; } // Import completo step-by-step per anno target, evitando anni storici non richiesti. foreach (['unita', 'soggetti', 'diritti', 'millesimi', 'voci', 'banche', 'operazioni', 'rate', 'incassi', 'giroconti', 'straord', 'addebiti', 'detrazioni'] as $step) { $params = [ '--stabile' => $code, '--solo' => $step, '--path' => $path, '--anno' => $legacyDir, ]; if ($dry) { $params['--dry-run'] = true; } $exit = Artisan::call('gescon:import-full', $params); $this->tailOutput($step . ' [' . $year . ' -> ' . $legacyDir . ']', $exit); } } if (! (bool) $this->option('skip-anagrafiche-sync')) { try { $syncParams = [ '--stabile' => $code, '--limit' => 20000, '--only' => 'both', '--link-ruolo' => true, '--include-comproprietari' => true, ]; if (! $dry) { $syncParams['--apply'] = true; } $exitSync = Artisan::call('gescon:auto-sync-anagrafiche', $syncParams); $this->tailOutput('auto-sync-anagrafiche', $exitSync); } catch (\Throwable $syncError) { $this->warn('auto-sync-anagrafiche fallito: ' . $syncError->getMessage()); } } if (! (bool) $this->option('skip-acqua')) { try { $acquaParams = [ '--stabile' => $code, '--base-path' => $path, '--acqua-years' => $waterYearsRaw, '--purge-non-target-water' => true, '--open-years' => $openYearsRaw, ]; if ($dry) { $acquaParams['--dry-run'] = true; } $exitAcqua = Artisan::call('gescon:import-acqua-legacy', $acquaParams); $this->tailOutput('import-acqua-legacy', $exitAcqua); } catch (\Throwable $acquaError) { $this->warn('import-acqua-legacy fallito: ' . $acquaError->getMessage()); } } if (! (bool) $this->option('skip-f24')) { try { $f24Params = [ '--stabile' => $code, '--base-path' => $path, '--years' => implode(',', $years), ]; if ((bool) $this->option('mark-obsolete-years')) { $f24Params['--mark-obsolete'] = true; } if ($dry) { $f24Params['--dry-run'] = true; } $exitF24 = Artisan::call('gescon:import-f24-legacy', $f24Params); $this->tailOutput('import-f24-legacy', $exitF24); } catch (\Throwable $f24Error) { $this->warn('import-f24-legacy fallito: ' . $f24Error->getMessage()); } } if ((bool) $this->option('mark-obsolete-years')) { $marked = $this->markYearsAsObsolete($code, $years, $dry); $this->line(' [OK] mark-obsolete-years -> anni marcati: ' . $marked); } } catch (\Throwable $e) { $summary['failed']++; $this->error('Errore stabile ' . $code . ': ' . $e->getMessage()); continue; } } $this->newLine(); $this->info('Riallineamento completato'); $this->line('- stabili processati: ' . $summary['processed']); $this->line('- stabili skipped: ' . $summary['skipped']); $this->line('- stabili failed: ' . $summary['failed']); return $summary['failed'] > 0 ? self::FAILURE : self::SUCCESS; } private function callGesconImportGestioni(string $code, string $path, bool $dry): void { $params = [ '--stabile' => $code, '--root' => $path, '--force-update-months' => true, ]; if ($dry) { $params['--dry-run'] = true; } $exit = Artisan::call('gescon:import-gestioni', $params); $this->tailOutput('import-gestioni', $exit); } /** @return array */ private function parseYearsCsv(string $value): array { $years = []; foreach (explode(',', $value) as $raw) { $v = trim($raw); if (preg_match('/^\d{4}$/', $v)) { $years[] = (int) $v; } } $years = array_values(array_unique($years)); sort($years); return $years; } /** @return array */ private function resolveStabili(string $path): array { $requested = array_values(array_filter(array_map(function ($v) { $s = trim((string) $v); return $s !== '' ? $s : null; }, (array) $this->option('stabile')))); if (! empty($requested)) { return array_values(array_unique(array_map([$this, 'normalizeCode'], $requested))); } if (! Schema::hasTable('stabili')) { return []; } $codes = DB::table('stabili') ->whereNotNull('codice_stabile') ->pluck('codice_stabile') ->map(fn($v) => $this->normalizeCode((string) $v)) ->filter(fn($v) => $v !== '') ->unique() ->values() ->all(); $out = []; foreach ($codes as $code) { if (is_file($path . '/' . $code . '/generale_stabile.mdb')) { $out[] = $code; } } sort($out); return $out; } private function normalizeCode(string $code): string { $code = trim($code); if ($code === '') { return ''; } if (is_numeric($code)) { return str_pad((string) ((int) $code), 4, '0', STR_PAD_LEFT); } return $code; } private function tailOutput(string $label, int $exitCode): void { $out = trim((string) Artisan::output()); $line = $out; if ($out !== '') { $parts = preg_split('/\R/', $out) ?: []; $line = trim((string) end($parts)); } if ($line === '') { $line = 'ok'; } $prefix = $exitCode === 0 ? 'OK' : 'ERR'; $this->line(' [' . $prefix . '] ' . $label . ' -> ' . $line); } /** * @return array Mappa anno_reale => directory legacy (es. 2024 => 0102) */ private function buildLegacyYearDirectoryMap(string $generaleMdb): array { $rows = $this->exportTable($generaleMdb, ['anni', 'Anni', 'ANNI']); if (empty($rows)) { return []; } $map = []; foreach ($rows as $row) { $year = $this->extractYear((string) ($row['anno_o'] ?? $row['anno_r'] ?? $row['anno'] ?? '')); if ($year === null) { continue; } $dir = trim((string) ($row['nome_dir'] ?? '')); if ($dir === '') { $idAnno = trim((string) ($row['id_anno'] ?? '')); if (is_numeric($idAnno)) { $dir = str_pad((string) ((int) $idAnno), 4, '0', STR_PAD_LEFT); } } if ($dir === '') { continue; } $map[(int) $year] = $dir; } ksort($map); return $map; } /** @return array> */ private function exportTable(string $mdbPath, array $candidates): array { if (! is_file($mdbPath) || ! is_readable($mdbPath)) { return []; } $binTables = trim((string) @shell_exec('command -v mdb-tables')) ?: '/usr/bin/mdb-tables'; $binExport = trim((string) @shell_exec('command -v mdb-export')) ?: '/usr/bin/mdb-export'; $tablesRaw = @shell_exec(sprintf('%s -1 %s 2>/dev/null', escapeshellarg($binTables), escapeshellarg($mdbPath))) ?: ''; if ($tablesRaw === '') { return []; } $tables = array_values(array_filter(preg_split('/\R/', trim($tablesRaw)) ?: [])); $table = null; foreach ($candidates as $cand) { foreach ($tables as $name) { if (strcasecmp($name, $cand) === 0) { $table = $name; break 2; } } } if ($table === null) { return []; } $tmp = tempnam(sys_get_temp_dir(), 'mdbx_'); $cmd = sprintf( '%s -D %s -d %s -q %s %s %s > %s 2>/dev/null', escapeshellarg($binExport), escapeshellarg('%Y-%m-%d'), escapeshellarg('|'), escapeshellarg('"'), escapeshellarg($mdbPath), escapeshellarg($table), escapeshellarg($tmp) ); @shell_exec($cmd); if (! is_file($tmp) || filesize($tmp) === 0) { @unlink($tmp); return []; } $fh = fopen($tmp, 'r'); if (! $fh) { @unlink($tmp); return []; } $headers = fgetcsv($fh, 0, '|'); if (! is_array($headers) || empty($headers)) { fclose($fh); @unlink($tmp); return []; } $headers = array_map(fn($h) => strtolower(trim((string) $h)), $headers); $rows = []; while (($cols = fgetcsv($fh, 0, '|')) !== false) { if (! is_array($cols)) { continue; } if (count($cols) < count($headers)) { $cols = array_pad($cols, count($headers), null); } if (count($cols) > count($headers)) { $cols = array_slice($cols, 0, count($headers)); } $assoc = @array_combine($headers, $cols); if (is_array($assoc)) { $rows[] = $assoc; } } fclose($fh); @unlink($tmp); return $rows; } private function extractYear(string $value): ?int { $value = trim($value); if ($value === '') { return null; } if (preg_match('/(19|20)\d{2}/', $value, $m)) { return (int) $m[0]; } return null; } private function markYearsAsObsolete(string $code, array $years, bool $dry): int { if (! Schema::hasTable('stabili') || ! Schema::hasTable('gestioni_contabili')) { return 0; } $stabileId = DB::table('stabili') ->where('codice_stabile', $code) ->value('id'); if (! $stabileId) { return 0; } $q = DB::table('gestioni_contabili') ->where('stabile_id', (int) $stabileId) ->whereIn('anno_gestione', $years); $count = (clone $q)->count(); if ($dry || $count === 0) { return (int) $count; } $update = []; if (Schema::hasColumn('gestioni_contabili', 'stato')) { $update['stato'] = 'chiusa'; } if (Schema::hasColumn('gestioni_contabili', 'gestione_attiva')) { $update['gestione_attiva'] = false; } if (Schema::hasColumn('gestioni_contabili', 'updated_at')) { $update['updated_at'] = now(); } if (! empty($update)) { $q->update($update); } return (int) $count; } }