359 lines
12 KiB
PHP
359 lines
12 KiB
PHP
<?php
|
|
namespace App\Console\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
class ImportGesconF24LegacyCommand extends Command
|
|
{
|
|
protected $signature = 'gescon:import-f24-legacy
|
|
{--stabile= : Codice stabile (es. 0023)}
|
|
{--base-path=/mnt/gescon-archives/gescon : Root archivi legacy}
|
|
{--years= : Anni da importare (csv, es. 2024,2025)}
|
|
{--truncate : Pulisce f24 staging per lo stabile prima dell\'import}
|
|
{--mark-obsolete : Marca record importati come storici in note}
|
|
{--dry-run : Simulazione senza scrittura}';
|
|
|
|
protected $description = 'Importa i dati F24 da generale_stabile.mdb espandendo i campi _1.._6 in record normalizzati.';
|
|
|
|
private bool $dryRun = false;
|
|
|
|
public function handle(): int
|
|
{
|
|
$this->dryRun = (bool) $this->option('dry-run');
|
|
|
|
$code = $this->normalizeCode((string) ($this->option('stabile') ?? ''));
|
|
if ($code === '') {
|
|
$this->error('Specificare --stabile=0000');
|
|
return self::FAILURE;
|
|
}
|
|
|
|
if (! Schema::connection('gescon_import')->hasTable('f24')) {
|
|
$this->error('Tabella gescon_import.f24 non disponibile.');
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$root = rtrim((string) ($this->option('base-path') ?? '/mnt/gescon-archives/gescon'), '/');
|
|
$mdb = $root . '/' . $code . '/generale_stabile.mdb';
|
|
if (! is_file($mdb)) {
|
|
$this->error('File generale_stabile.mdb non trovato: ' . $mdb);
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$rows = $this->exportTable($mdb, ['F24', 'f24']);
|
|
if (empty($rows)) {
|
|
$this->warn('Tabella F24 non trovata o vuota.');
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$yearsFilter = $this->parseYearsCsv((string) ($this->option('years') ?? ''));
|
|
|
|
if ((bool) $this->option('truncate')) {
|
|
$deleteQ = DB::connection('gescon_import')->table('f24')->where('cod_stabile', $code);
|
|
$toDelete = (int) $deleteQ->count();
|
|
if (! $this->dryRun && $toDelete > 0) {
|
|
$deleteQ->delete();
|
|
}
|
|
$this->line('Purge f24 staging: ' . $toDelete . ($this->dryRun ? ' (dry-run)' : ''));
|
|
}
|
|
|
|
$seen = [];
|
|
$stats = [
|
|
'rows' => count($rows),
|
|
'slots_seen' => 0,
|
|
'filtered_year' => 0,
|
|
'duplicates' => 0,
|
|
'inserted' => 0,
|
|
'already_present' => 0,
|
|
'invalid' => 0,
|
|
];
|
|
|
|
foreach ($rows as $row) {
|
|
$rowYear = $this->extractYear((string) ($row['anno'] ?? ''));
|
|
$rowMonth = $this->toInt($row['mese'] ?? null);
|
|
|
|
for ($i = 1; $i <= 6; $i++) {
|
|
$stats['slots_seen']++;
|
|
|
|
$codTributo = trim((string) ($row['cod_tributo_' . $i] ?? ''));
|
|
if ($codTributo === '') {
|
|
continue;
|
|
}
|
|
|
|
$slotYear = $this->extractYear((string) ($row['anno_' . $i] ?? '')) ?? $rowYear;
|
|
if (! empty($yearsFilter) && ($slotYear === null || ! in_array($slotYear, $yearsFilter, true))) {
|
|
$stats['filtered_year']++;
|
|
continue;
|
|
}
|
|
|
|
$deb = $this->toAmount($row['importo_tributo_deb_' . $i] ?? null);
|
|
$cre = $this->toAmount($row['importo_tributo_cre_' . $i] ?? null);
|
|
$importo = round($deb - $cre, 2);
|
|
|
|
$date = $this->resolveDateVersamento($row, $slotYear ?? $rowYear, $rowMonth);
|
|
if ($date === null) {
|
|
$stats['invalid']++;
|
|
continue;
|
|
}
|
|
|
|
$periodo = trim((string) ($row['rateiz_' . $i] ?? ''));
|
|
if ($slotYear !== null) {
|
|
$periodo = $periodo !== '' ? ($periodo . '/' . $slotYear) : (string) $slotYear;
|
|
}
|
|
|
|
$noteParts = [
|
|
'slot=' . $i,
|
|
'rif_f24=' . trim((string) ($row['rif_f24_mio'] ?? '')),
|
|
'banca=' . trim((string) ($row['banca_f24'] ?? '')),
|
|
'agenzia=' . trim((string) ($row['agenzia'] ?? '')),
|
|
];
|
|
if ((bool) $this->option('mark-obsolete')) {
|
|
$noteParts[] = 'obsolete=1';
|
|
}
|
|
$note = implode('; ', array_filter($noteParts, fn($v) => trim((string) $v) !== ''));
|
|
|
|
$importoFormatted = number_format($importo, 2, '.', '');
|
|
$fingerprint = implode('|', [$code, $date, $codTributo, $periodo, $importoFormatted]);
|
|
if (isset($seen[$fingerprint])) {
|
|
$stats['duplicates']++;
|
|
continue;
|
|
}
|
|
$seen[$fingerprint] = true;
|
|
|
|
$existsQ = DB::connection('gescon_import')
|
|
->table('f24')
|
|
->where('cod_stabile', $code)
|
|
->where('data_versamento', $date)
|
|
->where('codice_tributo', $codTributo)
|
|
->where('periodo_riferimento', $periodo)
|
|
->where('importo', $importoFormatted)
|
|
->where('importo_euro', $importoFormatted);
|
|
|
|
if ($existsQ->exists()) {
|
|
$stats['already_present']++;
|
|
continue;
|
|
}
|
|
|
|
if ($this->dryRun) {
|
|
$stats['inserted']++;
|
|
continue;
|
|
}
|
|
|
|
DB::connection('gescon_import')->table('f24')->insert([
|
|
'cod_stabile' => $code,
|
|
'data_versamento' => $date,
|
|
'codice_tributo' => $codTributo,
|
|
'importo' => $importoFormatted,
|
|
'importo_euro' => $importoFormatted,
|
|
'periodo_riferimento' => $periodo !== '' ? $periodo : null,
|
|
'note' => $note !== '' ? $note : null,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
$stats['inserted']++;
|
|
}
|
|
}
|
|
|
|
$this->info('Import F24 completato' . ($this->dryRun ? ' (DRY RUN)' : ''));
|
|
$this->line('- righe legacy lette: ' . $stats['rows']);
|
|
$this->line('- slot analizzati (_1.._6): ' . $stats['slots_seen']);
|
|
$this->line('- filtrati per anno: ' . $stats['filtered_year']);
|
|
$this->line('- duplicati intra-run: ' . $stats['duplicates']);
|
|
$this->line('- gia presenti: ' . $stats['already_present']);
|
|
$this->line('- record inseriti: ' . $stats['inserted']);
|
|
$this->line('- record invalidi: ' . $stats['invalid']);
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/** @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;
|
|
}
|
|
|
|
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 toInt(mixed $value): ?int
|
|
{
|
|
if ($value === null) {
|
|
return null;
|
|
}
|
|
$s = trim((string) $value);
|
|
return $s !== '' && is_numeric($s) ? (int) $s : null;
|
|
}
|
|
|
|
private function toAmount(mixed $value): float
|
|
{
|
|
if ($value === null) {
|
|
return 0.0;
|
|
}
|
|
$s = trim((string) $value);
|
|
if ($s === '') {
|
|
return 0.0;
|
|
}
|
|
$s = preg_replace('/[^0-9,.-]/', '', $s) ?? '0';
|
|
if (str_contains($s, ',') && str_contains($s, '.')) {
|
|
$s = str_replace('.', '', $s);
|
|
$s = str_replace(',', '.', $s);
|
|
} elseif (str_contains($s, ',')) {
|
|
$s = str_replace(',', '.', $s);
|
|
}
|
|
if (! is_numeric($s)) {
|
|
$s = '0';
|
|
}
|
|
return round((float) $s, 2);
|
|
}
|
|
|
|
private function resolveDateVersamento(array $row, ?int $fallbackYear, ?int $fallbackMonth): ?string
|
|
{
|
|
$raw = trim((string) ($row['dt_versamento'] ?? $row['dt_pagamento'] ?? ''));
|
|
if ($raw !== '') {
|
|
if (preg_match('/^(\d{1,2})\/(\d{1,2})\/(\d{2,4})/', $raw, $m)) {
|
|
$y = (int) $m[3];
|
|
if ($y < 100) {
|
|
$y += 2000;
|
|
}
|
|
$a = (int) $m[1];
|
|
$b = (int) $m[2];
|
|
// Legacy MDB usa talvolta mm/dd, talvolta dd/mm: gestiamo entrambi in modo difensivo.
|
|
$month = $a;
|
|
$day = $b;
|
|
if ($a > 12 && $b <= 12) {
|
|
$month = $b;
|
|
$day = $a;
|
|
}
|
|
$month = max(1, min(12, $month));
|
|
$day = max(1, min(31, $day));
|
|
return sprintf('%04d-%02d-%02d', $y, $month, $day);
|
|
}
|
|
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $raw, $m)) {
|
|
return sprintf('%04d-%02d-%02d', (int) $m[1], (int) $m[2], (int) $m[3]);
|
|
}
|
|
}
|
|
|
|
if ($fallbackYear === null) {
|
|
return null;
|
|
}
|
|
|
|
$month = $fallbackMonth !== null && $fallbackMonth >= 1 && $fallbackMonth <= 12 ? $fallbackMonth : 1;
|
|
return sprintf('%04d-%02d-01', $fallbackYear, $month);
|
|
}
|
|
|
|
/** @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(), 'mdbf24_');
|
|
$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;
|
|
}
|
|
}
|