450 lines
16 KiB
PHP
Executable File
450 lines
16 KiB
PHP
Executable File
<?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}
|
|
{--amministratore-id= : Forza l\'assegnazione dello stabile a questo amministratore}
|
|
{--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');
|
|
$amminId = $this->option('amministratore-id');
|
|
|
|
$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;
|
|
}
|
|
if ($amminId) {
|
|
$params['--amministratore-id'] = $amminId;
|
|
}
|
|
$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;
|
|
}
|
|
}
|