netgescon-day0/app/Console/Commands/GesconImportAlignCommand.php

445 lines
16 KiB
PHP

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class GesconImportAlignCommand extends Command
{
protected $signature = 'gescon:import-align
{--stabile=* : Codici stabili (ripetibile). Se omesso usa tutti gli stabili con cartella archivio}
{--path=/mnt/gescon-archives/gescon : Root archivi legacy}
{--years=2024,2025 : Anni da importare per operazioni/rate/incassi}
{--water-years=2024 : Anni acqua da importare (acqua_gen/acqua_dett)}
{--open-years=2024,2025 : Anni da tenere aperti in gestioni_contabili}
{--mark-obsolete-years : Marca come chiusi/non attivi gli anni importati (utile per storico)}
{--skip-acqua : Salta import acqua}
{--skip-f24 : Salta import modello F24 da generale_stabile.mdb}
{--skip-anagrafiche-sync : Salta sync anagrafiche/ruoli}
{--dry-run : Simulazione (propagata ai comandi compatibili)}';
protected $description = 'Comando unico di import + riallineamento stabili (anagrafiche, operazioni, rate, incassi, acqua) per anni target.';
public function handle(): int
{
$path = rtrim((string) ($this->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<int, int> */
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<int, string> */
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<int, string> 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<int, array<string, string>> */
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;
}
}