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

585 lines
22 KiB
PHP
Executable File

<?php
namespace App\Console\Commands;
use App\Models\GestioneContabile;
use App\Models\Stabile;
use Illuminate\Console\Command;
use Symfony\Component\Process\Process;
class ImportGestioniFromLegacy extends Command
{
protected $signature = 'gescon:import-gestioni \
{--stabile= : Codice stabile a 4 cifre (es. 0019)} \
{--root=/mnt/gescon-archives/gescon : Root archivi legacy} \
{--stabili-mdb= : Path al master MDB (Stabili/cartellestabili)} \
{--tenant=default : Tenant ID da usare} \
{--force-update-months : Aggiorna i mesi anche se la gestione esiste} \
{--dry-run : Non scrivere, solo report}';
protected $description = 'Crea le gestioni contabili (per anno) leggendo MDB legacy: mesi da Stabili (ORD_RATA_/RIS_RATA_), anni da generale_stabile.mdb';
public function handle(): int
{
$stabileCode = (string) $this->option('stabile');
if (! $stabileCode) {
$this->error('Specificare --stabile=0000');
return 1;
}
$stabileCode = str_pad($stabileCode, 4, '0', STR_PAD_LEFT);
$root = rtrim((string) $this->option('root') ?: '/mnt/gescon-archives/gescon', '/');
$tenant = (string) $this->option('tenant') ?: 'default';
$dry = (bool) $this->option('dry-run');
// Heuristics per master MDB (Stabili)
$stabiliCandidates = [];
if ($this->option('stabili-mdb')) {
$stabiliCandidates[] = (string) $this->option('stabili-mdb');
}
foreach (['Stabili.mdb', 'stabili.mdb', 'cartellestabili_e_anni.mdb'] as $n) {
$stabiliCandidates[] = $root . '/' . $n;
$stabiliCandidates[] = $root . '/dbc/' . $n;
}
$stabiliMdb = null;
foreach ($stabiliCandidates as $cand) {
if (is_file($cand)) {
$stabiliMdb = $cand;
break;
}
}
if (! $stabiliMdb) {
$this->error('Master MDB Stabili non trovato: senza Stabili.mdb non leggo ORD_RATA_/RIS_RATA_ e non applico fallback.');
return 1;
}
// 1) Leggi mesi da Stabili (ORD_RATA_*, RIS_RATA_*) e risolvi directory
$ordMesi = [];
$risMesi = [];
$stableDir = $root . '/' . $stabileCode;
if ($stabiliMdb) {
[$row, $headers] = $this->findRowInMdb($stabiliMdb, ['cod', 'codice', 'cod_stabile', 'stabile', 'nome_directory', 'codice_directory', 'internet_cod_stab'], $stabileCode);
if ($row) {
$hmap = array_change_key_case($headers, CASE_LOWER);
// Estrai mesi
foreach ($hmap as $col => $idx) {
if (str_starts_with($col, 'ord_rata_')) {
$num = (int) preg_replace('/[^0-9]/', '', substr($col, strlen('ord_rata_')));
if ($num >= 1 && $num <= 12) {
if (self::truthy($row[$idx] ?? null)) {
$ordMesi[] = $num;
}
}
} elseif (str_starts_with($col, 'ris_rata_')) {
$num = (int) preg_replace('/[^0-9]/', '', substr($col, strlen('ris_rata_')));
if ($num >= 1 && $num <= 12) {
if (self::truthy($row[$idx] ?? null)) {
$risMesi[] = $num;
}
}
}
}
// Directory se presente (campi possibili)
foreach (['directory', 'dir', 'cartella', 'path', 'folder', 'dir_stabile', 'nome_directory', 'codice_directory', 'internet_cod_stab'] as $k) {
if (isset($hmap[$k])) {
$val = trim((string) ($row[$hmap[$k]] ?? ''));
if ($val) {
$stableDir = $root . '/' . trim($val, '/');
break;
}
}
}
}
}
if (! is_dir($stableDir)) {
$this->error("Directory stabile non trovata: {$stableDir}");
return 1;
}
// 2) Determina anni gestione da generale_stabile.mdb; se assenti prova i singolo_anno.mdb.
$generale = $stableDir . '/generale_stabile.mdb';
$anni = [];
$gestioniAnnuali = [];
if (is_file($generale)) {
$gestioniAnnuali = $this->extractGestioniAnnualiFromGenerale($generale);
$anni = $this->extractAnniFromGenerale($generale);
}
if (! $anni) {
foreach (glob($stableDir . '/*/singolo_anno.mdb') as $mdb) {
$anni = array_values(array_unique(array_merge($anni, $this->extractAnniFromSingoloAnno($mdb))));
}
}
sort($anni);
if (! $anni) {
$this->error('Nessun anno trovato negli archivi legacy dello stabile; import gestioni interrotto.');
return 1;
}
$this->info("Stabile {$stabileCode} → dir: {$stableDir}");
$this->line('Mesi Ordinaria (master Stabili): ' . json_encode($ordMesi) . ' | Mesi Riscaldamento: ' . json_encode($risMesi));
if ($ordMesi === [] && $risMesi === []) {
$this->warn('In Stabili.mdb non risultano mesi ORD_RATA_/RIS_RATA_ attivi per questo stabile.');
}
$this->line('Anni trovati: ' . implode(', ', $anni));
if ($dry) {
$this->warn('Dry-run attivo: non scrivo su database.');
return 0;
}
// 3) Crea/aggiorna gestioni per ciascun anno
$created = 0;
$skipped = 0;
$updated = 0;
// Recupera Stabile record
$stabileModel = Stabile::where('codice_stabile', $stabileCode)->first();
$stabileId = $stabileModel?->id;
if (($tenant === '' || $tenant === 'default') && $stabileId) {
$tenant = (string) (GestioneContabile::query()
->where('stabile_id', $stabileId)
->whereNotNull('tenant_id')
->where('tenant_id', '!=', '')
->orderByDesc('updated_at')
->value('tenant_id') ?: $tenant);
}
$forceUpdate = (bool) $this->option('force-update-months');
foreach ($anni as $anno) {
foreach (['ordinaria' => $ordMesi, 'riscaldamento' => $risMesi] as $tipo => $mesi) {
$exists = GestioneContabile::where('tenant_id', $tenant)
->when($stabileId, fn($q) => $q->where('stabile_id', $stabileId))
->where('anno_gestione', $anno)
->where('tipo_gestione', $tipo)
->first();
$meta = $gestioniAnnuali[$anno] ?? [];
[$denominazione, $dataInizio, $dataFine] = $this->buildGestioneLegacyPayload($tipo, $anno, $meta);
if ($exists) {
// Aggiorna mesi se forzato o vuoti
$campo = $tipo === 'ordinaria' ? 'mesi_rate_ordinaria' : 'mesi_rate_riscaldamento';
$valEsist = (array) ($exists->{$campo} ?? []);
$dirty = false;
if ($forceUpdate || (empty($valEsist) && ! empty($mesi))) {
$exists->{$campo} = array_values(array_unique(array_map('intval', $mesi)));
$dirty = true;
}
if ((string) ($exists->denominazione ?? '') !== $denominazione) {
$exists->denominazione = $denominazione;
$dirty = true;
}
if ((string) ($exists->data_inizio?->format('Y-m-d') ?? $exists->data_inizio ?? '') !== $dataInizio) {
$exists->data_inizio = $dataInizio;
$dirty = true;
}
if ((string) ($exists->data_fine?->format('Y-m-d') ?? $exists->data_fine ?? '') !== $dataFine) {
$exists->data_fine = $dataFine;
$dirty = true;
}
if ($dirty) {
if (! $dry) {
$exists->save();
}
$updated++;
} else {
$skipped++;
}
continue;
}
$gestione = new GestioneContabile();
$gestione->tenant_id = $tenant;
$gestione->stabile_id = $stabileId;
$gestione->anno_gestione = $anno;
$gestione->tipo_gestione = $tipo;
$gestione->denominazione = $denominazione;
$gestione->data_inizio = $dataInizio;
$gestione->data_fine = $dataFine;
$gestione->stato = 'aperta';
$gestione->protocollo_prefix = strtoupper(substr($tipo, 0, 1)) . $anno;
$gestione->ultimo_protocollo = 0;
if ($tipo === 'ordinaria') {
$gestione->mesi_rate_ordinaria = array_values(array_unique(array_map('intval', $mesi)));
}
if ($tipo === 'riscaldamento') {
$gestione->mesi_rate_riscaldamento = array_values(array_unique(array_map('intval', $mesi)));
}
$gestione->gestione_attiva = true;
$gestione->codice_archivio_legacy = $stableDir;
if (! $dry) {
$gestione->save();
}
$created++;
}
}
$this->info("Gestioni create: {$created}, aggiornate: {$updated}, saltate: {$skipped}");
return 0;
}
private static function truthy($v): bool
{
$s = strtolower(trim((string) $v));
return $s !== '' && ! in_array($s, ['0', 'no', 'false', 'off', 'n'], true);
}
/**
* Trova una riga nel MDB master dove un campo (tra keys) corrisponde al valore cercato
* @return array{0: array|null, 1: array<string,int>} row e mappa header->index
*/
private function findRowInMdb(string $mdbPath, array $keys, string $value): array
{
$table = $this->resolveTable($mdbPath, ['stabili', 'Stabili', 'STABILI']);
if (! $table) {
return [null, []];
}
$csv = $this->mdbExport($mdbPath, $table);
if ($csv === null) {
return [null, []];
}
$fh = fopen('php://temp', 'r+');
fwrite($fh, $csv);
rewind($fh);
$header = fgetcsv($fh);
if (! $header) {
return [null, []];
}
$hmap = [];
foreach ($header as $i => $h) {
$hmap[strtolower(trim((string) $h))] = $i;
}
while (($row = fgetcsv($fh)) !== false) {
foreach ($keys as $k) {
$i = $hmap[strtolower($k)] ?? null;
if ($i === null) {
continue;
}
if (str_pad(trim((string) ($row[$i] ?? '')), 4, '0', STR_PAD_LEFT) === $value) {
return [$row, $hmap];
}
}
}
return [null, $hmap];
}
private function extractAnniFromGenerale(string $mdbPath): array
{
$years = [];
$table = $this->resolveTable($mdbPath, ['anni', 'Anni', 'ANNI', 'gestioni', 'gestione', 'esercizi', 'Esercizi']);
if (! $table) {
return $years;
}
$csv = $this->mdbExport($mdbPath, $table);
if ($csv === null) {
return $years;
}
$fh = fopen('php://temp', 'r+');
fwrite($fh, $csv);
rewind($fh);
$header = fgetcsv($fh);
if (! $header) {
return $years;
}
$hmap = [];
foreach ($header as $i => $h) {
$hmap[strtolower(trim((string) $h))] = $i;
}
while (($row = fgetcsv($fh)) !== false) {
$y = null;
foreach (['anno_o', 'anno_r', 'anno', 'esercizio', 'str_anno'] as $k) {
if (isset($hmap[$k])) {
$val = trim((string) ($row[$hmap[$k]] ?? ''));
if ($val !== '' && preg_match('/(19|20)\d{2}/', $val, $m)) {
$y = (int) $m[0];
break;
}
if ($val !== '' && is_numeric($val)) {
$y = (int) $val;
break;
}
}
}
if ($y === null) {
foreach (['data_inizio', 'inizio', 'dal'] as $k) {
if (isset($hmap[$k])) {
$val = trim((string) ($row[$hmap[$k]] ?? ''));
if ($val && preg_match('/(19|20)\d{2}/', $val, $m)) {
$y = (int) $m[0];
break;
}
}
}
}
if ($y !== null) {
$years[] = $y;
}
}
return array_values(array_unique($years));
}
private function extractGestioniAnnualiFromGenerale(string $mdbPath): array
{
$rows = [];
$table = $this->resolveTable($mdbPath, ['anni', 'Anni', 'ANNI', 'gestioni', 'gestione', 'esercizi', 'Esercizi']);
if (! $table) {
return $rows;
}
$csv = $this->mdbExport($mdbPath, $table);
if ($csv === null) {
return $rows;
}
$fh = fopen('php://temp', 'r+');
fwrite($fh, $csv);
rewind($fh);
$header = fgetcsv($fh);
if (! $header) {
return $rows;
}
$hmap = [];
foreach ($header as $i => $h) {
$hmap[strtolower(trim((string) $h))] = $i;
}
while (($row = fgetcsv($fh)) !== false) {
$annoRaw = trim((string) ($row[$hmap['anno_o'] ?? $hmap['anno'] ?? $hmap['anno_gestione'] ?? -1] ?? ''));
if ($annoRaw === '' || ! preg_match('/(19|20)\d{2}/', $annoRaw, $match)) {
continue;
}
$anno = (int) $match[0];
$descrizione = trim((string) ($row[$hmap['anno_o'] ?? -1] ?? $row[$hmap['descrizione'] ?? -1] ?? $annoRaw));
$rows[$anno] = [
'label' => $descrizione !== '' ? preg_replace('/\s+/', '', $descrizione) : (string) $anno,
'ordinaria_dal' => $this->normalizeLegacyDateValue($row[$hmap['ordinarie_dal'] ?? $hmap['data_inizio'] ?? $hmap['dal'] ?? -1] ?? null),
'ordinaria_al' => $this->normalizeLegacyDateValue($row[$hmap['ordinarie_al'] ?? $hmap['data_fine'] ?? $hmap['al'] ?? -1] ?? null),
'riscaldamento_dal' => $this->normalizeLegacyDateValue($row[$hmap['riscaldamento_dal'] ?? -1] ?? null),
'riscaldamento_al' => $this->normalizeLegacyDateValue($row[$hmap['riscaldamento_al'] ?? -1] ?? null),
];
}
return $rows;
}
private function buildGestioneLegacyPayload(string $tipo, int $anno, array $meta): array
{
$label = trim((string) ($meta['label'] ?? ''));
$dataInizio = $tipo === 'riscaldamento'
? ($meta['riscaldamento_dal'] ?? null)
: ($meta['ordinaria_dal'] ?? null);
$dataFine = $tipo === 'riscaldamento'
? ($meta['riscaldamento_al'] ?? null)
: ($meta['ordinaria_al'] ?? null);
return [
ucfirst($tipo) . ' ' . ($label !== '' ? $label : (string) $anno),
$dataInizio ?: sprintf('%04d-01-01', $anno),
$dataFine ?: sprintf('%04d-12-31', $anno),
];
}
private function normalizeLegacyDateValue($value): ?string
{
$value = trim((string) ($value ?? ''));
if ($value === '') {
return null;
}
foreach (['d/m/Y', 'd-m-Y', 'Y-m-d', 'Y/m/d'] as $format) {
$dt = \DateTimeImmutable::createFromFormat($format, $value);
if ($dt instanceof \DateTimeImmutable) {
return $dt->format('Y-m-d');
}
}
$ts = strtotime($value);
return $ts ? date('Y-m-d', $ts) : null;
}
private function extractAnniFromSingoloAnno(string $mdbPath): array
{
$table = $this->resolveTable($mdbPath, ['incassi', 'Incassi']);
if (! $table) {
return [];
}
$csv = $this->mdbExport($mdbPath, $table);
if ($csv === null) {
return [];
}
$fh = fopen('php://temp', 'r+');
fwrite($fh, $csv);
rewind($fh);
$header = fgetcsv($fh);
if (! $header) {
return [];
}
$hmap = [];
foreach ($header as $i => $h) {
$hmap[strtolower(trim((string) $h))] = $i;
}
$years = [];
while (($row = fgetcsv($fh)) !== false) {
$ar = $hmap['anno_rif'] ?? null;
if ($ar !== null) {
$val = trim((string) ($row[$ar] ?? ''));
if ($val && is_numeric($val)) {
$years[] = (int) $val;
continue;
}
}
$dt = $hmap['dt_empag'] ?? null;
if ($dt !== null) {
$val = trim((string) ($row[$dt] ?? ''));
if ($val && preg_match('/^(\d{4})-/', $val, $m)) {
$years[] = (int) $m[1];
}
}
}
return array_values(array_unique($years));
}
private function resolveTable(string $mdbPath, array $candidates): ?string
{
$bin = trim((string) @shell_exec('command -v mdb-tables')) ?: '/usr/bin/mdb-tables';
$list = @shell_exec(escapeshellcmd($bin) . ' -1 ' . escapeshellarg($mdbPath));
if (! $list) {
return null;
}
$names = array_filter(array_map('trim', explode("\n", $list)));
foreach ($candidates as $c) {
foreach ($names as $n) {
if (strcasecmp($n, $c) === 0) {
return $n;
}
}
}
// fallback: first contains substring
foreach ($candidates as $c) {
foreach ($names as $n) {
if (stripos($n, $c) !== false) {
return $n;
}
}
}
return null;
}
private function mdbExport(string $mdbPath, string $table): ?string
{
$bin = trim((string) @shell_exec('command -v mdb-export')) ?: '/usr/bin/mdb-export';
$p = new Process([$bin, '-D', '%Y-%m-%d', $mdbPath, $table]);
$p->setTimeout(120);
$p->run();
if (! $p->isSuccessful()) {
return null;
}
return $p->getOutput();
}
/** Estrae mesi per anno dalla tabella rate del singolo_anno.mdb: ritorna [anno=>['O'=>[mesi],'R'=>[mesi]]] */
private function extractMesiPerAnnoFromSingoloAnno(string $mdbPath): array
{
$out = [];
$table = $this->resolveTable($mdbPath, ['rate', 'Rate']);
if (! $table) {
return $out;
}
$csv = $this->mdbExport($mdbPath, $table);
if ($csv === null) {
return $out;
}
$fh = fopen('php://temp', 'r+');
fwrite($fh, $csv);
rewind($fh);
$header = fgetcsv($fh);
if (! $header) {
return $out;
}
$hmap = [];
foreach ($header as $i => $h) {
$hmap[strtolower(trim((string) $h))] = $i;
}
while (($row = fgetcsv($fh)) !== false) {
$tipo = null;
if (isset($hmap['o_r_s'])) {
$tipo = strtoupper(trim((string) ($row[$hmap['o_r_s']] ?? '')));
}
if (! in_array($tipo, ['O', 'R'], true)) {
continue;
}
$mese = null;
if (isset($hmap['n_mese'])) {
$val = trim((string) ($row[$hmap['n_mese']] ?? ''));
if ($val !== '' && is_numeric($val)) {
$mese = max(1, min(12, (int) $val));
}
}
if ($mese === null) {
continue;
}
$anno = null;
foreach (['str_anno'] as $k) {
if (isset($hmap[$k])) {
$s = trim((string) ($row[$hmap[$k]] ?? ''));
if ($s && preg_match('/(\d{4})/', $s, $m)) {
$anno = (int) $m[1];
break;
}
}
}
if ($anno === null) {
foreach (['dt_empag', 'dt1_da', 'data_emissione'] as $k) {
if (isset($hmap[$k])) {
$s = trim((string) ($row[$hmap[$k]] ?? ''));
if ($s && preg_match('/^(\d{4})-/', $s, $m)) {
$anno = (int) $m[1];
break;
}
}
}
}
if ($anno === null) {
continue;
}
if (! isset($out[$anno])) {
$out[$anno] = ['O' => [], 'R' => []];
}
if ($tipo === 'O') {
$out[$anno]['O'][] = $mese;
} else {
$out[$anno]['R'][] = $mese;
}
}
// Unique/sorted
foreach ($out as $y => $tipi) {
$out[$y]['O'] = array_values(array_unique(array_filter($out[$y]['O'], fn($m) => $m >= 1 && $m <= 12)));
sort($out[$y]['O']);
$out[$y]['R'] = array_values(array_unique(array_filter($out[$y]['R'], fn($m) => $m >= 1 && $m <= 12)));
sort($out[$y]['R']);
}
return $out;
}
}