360 lines
12 KiB
PHP
Executable File
360 lines
12 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class ImportGesconFattureLegacyMdbCommand extends Command
|
|
{
|
|
protected $signature = 'import:gescon-fatture-legacy
|
|
{tenant : Tenant ID}
|
|
{--mdb= : Percorso file generale_stabile.mdb}
|
|
{--table=Fatture : Nome tabella Access}
|
|
{--into=stg_fatture_gescon : Tabella staging destinazione}
|
|
{--truncate : Svuota la tabella staging prima dell\'import}
|
|
{--preview : Mostra una preview e non importa}
|
|
{--limit=10 : Limite righe per preview}';
|
|
|
|
protected $description = 'Importa fatture legacy (tabella Fatture) da generale_stabile.mdb in staging (idempotente per legacy_id)';
|
|
|
|
public function handle(): int
|
|
{
|
|
$tenant = (string) $this->argument('tenant');
|
|
$mdb = (string) ($this->option('mdb') ?: '');
|
|
$table = (string) ($this->option('table') ?: 'Fatture');
|
|
$into = (string) ($this->option('into') ?: 'stg_fatture_gescon');
|
|
|
|
if ($mdb === '' || !is_file($mdb)) {
|
|
$this->error('File MDB non trovato. Passa --mdb=/path/generale_stabile.mdb');
|
|
return self::FAILURE;
|
|
}
|
|
|
|
if (!DB::getSchemaBuilder()->hasTable($into)) {
|
|
$this->error("Tabella staging non trovata: {$into} (esegui migrate)");
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$bin = trim((string) @shell_exec('command -v mdb-export')) ?: '';
|
|
if ($bin === '') {
|
|
$this->error('mdb-export non trovato (installa mdbtools)');
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$csv = $this->runShell(sprintf(
|
|
"%s -D %s -d %s -q %s -R \"\\n\" %s %s",
|
|
escapeshellarg($bin),
|
|
escapeshellarg('%Y-%m-%d %H:%M:%S'),
|
|
escapeshellarg('|'),
|
|
escapeshellarg('"'),
|
|
escapeshellarg($mdb),
|
|
escapeshellarg($table),
|
|
));
|
|
|
|
if ($csv === null || trim($csv) === '') {
|
|
$this->error('Nessun dato esportato (tabella inesistente o file non leggibile)');
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$rows = $this->parseDelimited($csv, '|');
|
|
|
|
if ($this->option('preview')) {
|
|
$limit = max(1, (int) $this->option('limit'));
|
|
$this->line('Preview:');
|
|
foreach (array_slice($rows, 0, $limit) as $r) {
|
|
$this->line(json_encode($this->mapRow($tenant, $mdb, $r), JSON_UNESCAPED_UNICODE));
|
|
}
|
|
$this->line('Totale righe: ' . count($rows));
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
if ($this->option('truncate')) {
|
|
DB::table($into)->truncate();
|
|
}
|
|
|
|
$now = now();
|
|
$payloads = [];
|
|
$count = 0;
|
|
|
|
foreach ($rows as $row) {
|
|
$payload = $this->mapRow($tenant, $mdb, $row);
|
|
if ($payload['legacy_id_fatture'] === null) {
|
|
continue;
|
|
}
|
|
$payload['created_at'] = $now;
|
|
$payload['updated_at'] = $now;
|
|
$payloads[] = $payload;
|
|
|
|
if (count($payloads) >= 500) {
|
|
DB::table($into)->upsert($payloads, ['tenant_id', 'legacy_id_fatture'], [
|
|
'legacy_id_stabile',
|
|
'riferimento',
|
|
'cod_fornitore',
|
|
'data_fattura',
|
|
'data_pagamento',
|
|
'numero_fattura',
|
|
'descrizione_sintetica',
|
|
'descrizione_corpo',
|
|
'imponibile',
|
|
'importo_iva',
|
|
'totale_fattura',
|
|
'importo_netto',
|
|
'onorario',
|
|
'rimborsi',
|
|
'aliq_cassa',
|
|
'importo_cassa',
|
|
'aliq_rda',
|
|
'importo_rda',
|
|
'aliq_inps',
|
|
'importo_inps',
|
|
'aliq_iva',
|
|
'legacy_id_anno',
|
|
'reg_gestione',
|
|
'reg_nstra',
|
|
'note',
|
|
'raw',
|
|
'sorgente_archivio',
|
|
'updated_at',
|
|
]);
|
|
$count += count($payloads);
|
|
$payloads = [];
|
|
}
|
|
}
|
|
|
|
if (!empty($payloads)) {
|
|
DB::table($into)->upsert($payloads, ['tenant_id', 'legacy_id_fatture'], [
|
|
'legacy_id_stabile',
|
|
'riferimento',
|
|
'cod_fornitore',
|
|
'data_fattura',
|
|
'data_pagamento',
|
|
'numero_fattura',
|
|
'descrizione_sintetica',
|
|
'descrizione_corpo',
|
|
'imponibile',
|
|
'importo_iva',
|
|
'totale_fattura',
|
|
'importo_netto',
|
|
'onorario',
|
|
'rimborsi',
|
|
'aliq_cassa',
|
|
'importo_cassa',
|
|
'aliq_rda',
|
|
'importo_rda',
|
|
'aliq_inps',
|
|
'importo_inps',
|
|
'aliq_iva',
|
|
'legacy_id_anno',
|
|
'reg_gestione',
|
|
'reg_nstra',
|
|
'note',
|
|
'raw',
|
|
'sorgente_archivio',
|
|
'updated_at',
|
|
]);
|
|
$count += count($payloads);
|
|
}
|
|
|
|
$this->info("✅ Import completato: {$count} righe elaborate (upsert)");
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
private function runShell(string $cmd): ?string
|
|
{
|
|
$out = shell_exec($cmd . ' 2>/dev/null');
|
|
if ($out === null || $out === '') {
|
|
return null;
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
private function parseDelimited(string $text, string $sep): array
|
|
{
|
|
$lines = preg_split("/\r?\n/", trim($text));
|
|
if (!$lines || count($lines) === 0) {
|
|
return [];
|
|
}
|
|
|
|
$headers = str_getcsv(array_shift($lines), $sep);
|
|
$headers = array_map(fn($h) => strtolower(trim((string) $h)), $headers);
|
|
|
|
$rows = [];
|
|
foreach ($lines as $line) {
|
|
if ($line === '') {
|
|
continue;
|
|
}
|
|
$cols = str_getcsv($line, $sep);
|
|
if (count($cols) < count($headers)) {
|
|
$cols = array_pad($cols, count($headers), null);
|
|
}
|
|
if (count($cols) > count($headers)) {
|
|
$cols = array_slice($cols, 0, count($headers));
|
|
}
|
|
$row = @array_combine($headers, $cols);
|
|
if (!$row) {
|
|
continue;
|
|
}
|
|
$rows[] = $row;
|
|
}
|
|
return $rows;
|
|
}
|
|
|
|
private function mapRow(string $tenant, string $mdb, array $row): array
|
|
{
|
|
$get = function (string ...$keys) use ($row) {
|
|
foreach ($keys as $k) {
|
|
$lk = strtolower($k);
|
|
if (array_key_exists($lk, $row)) {
|
|
$v = $row[$lk];
|
|
return is_string($v) ? trim($v) : $v;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
$legacyId = $this->toInt($get('id_fatture', 'id_fattura', 'id'));
|
|
|
|
$raw = $row;
|
|
|
|
return [
|
|
'tenant_id' => $tenant,
|
|
'sorgente_archivio' => $mdb,
|
|
'legacy_id_fatture' => $legacyId,
|
|
'legacy_id_stabile' => $this->toInt($get('id_stabile')),
|
|
'riferimento' => $this->toInt($get('riferimento')),
|
|
'cod_fornitore' => $this->toInt($get('cod_fornitore', 'cod_fornitore')),
|
|
'data_fattura' => $this->toDate($get('data_fattura')),
|
|
'data_pagamento' => $this->toDate($get('data_pagamento')),
|
|
'numero_fattura' => $this->nullIfEmpty($get('num_fattura', 'numero_fattura')),
|
|
'descrizione_sintetica' => $this->nullIfEmpty($get('descrizione_sintetica')),
|
|
'descrizione_corpo' => $this->nullIfEmpty($get('descriz_corpo', 'descrizione_corpo')),
|
|
'imponibile' => $this->money($get('imponibile') ?? 0),
|
|
'importo_iva' => $this->money($get('importo_iva') ?? 0),
|
|
'totale_fattura' => $this->money($get('totale_fattura') ?? 0),
|
|
'importo_netto' => $this->money($get('importo_netto') ?? 0),
|
|
'onorario' => $this->money($get('onorario') ?? 0),
|
|
'rimborsi' => $this->money($get('rimborsi') ?? 0),
|
|
'aliq_cassa' => $this->decimal($get('aliq_cassa')),
|
|
'importo_cassa' => $this->money($get('importo_cassa') ?? 0),
|
|
'aliq_rda' => $this->decimal($get('aliq_rda')),
|
|
'importo_rda' => $this->money($get('importo_rda') ?? 0),
|
|
'aliq_inps' => $this->decimal($get('aliq_inps')),
|
|
'importo_inps' => $this->money($get('importo_inps') ?? 0),
|
|
'aliq_iva' => $this->decimal($get('aliq_iva')),
|
|
'legacy_id_anno' => $this->toInt($get('id_anno')),
|
|
'reg_gestione' => $this->nullIfEmpty($get('reg_gestione')),
|
|
'reg_nstra' => $this->toInt($get('reg_nstra')),
|
|
'note' => $this->nullIfEmpty($get('note')),
|
|
'raw' => json_encode($raw, JSON_UNESCAPED_UNICODE),
|
|
];
|
|
}
|
|
|
|
private function nullIfEmpty($v): ?string
|
|
{
|
|
if ($v === null) {
|
|
return null;
|
|
}
|
|
$s = trim((string) $v);
|
|
return $s === '' ? null : $s;
|
|
}
|
|
|
|
private function toInt($v): ?int
|
|
{
|
|
if ($v === null) {
|
|
return null;
|
|
}
|
|
$s = trim((string) $v);
|
|
if ($s === '' || !is_numeric($s)) {
|
|
return null;
|
|
}
|
|
return (int) $s;
|
|
}
|
|
|
|
private function toDate($v): ?string
|
|
{
|
|
if ($v === null) {
|
|
return null;
|
|
}
|
|
$s = trim((string) $v);
|
|
if ($s === '') {
|
|
return null;
|
|
}
|
|
if (preg_match('/^\d{4}-\d{2}-\d{2}/', $s)) {
|
|
return substr($s, 0, 10);
|
|
}
|
|
if (preg_match('/^\d{2}\/\d{2}\/\d{2} /', $s)) {
|
|
$dt = \DateTime::createFromFormat('m/d/y H:i:s', $s);
|
|
if (!$dt) {
|
|
return null;
|
|
}
|
|
|
|
// Fix 2-digit years (e.g. 69 should be 1969, not 2069)
|
|
$yy = (int) $dt->format('y');
|
|
$curYY = (int) now()->format('y');
|
|
$year = ($yy > $curYY) ? (1900 + $yy) : (2000 + $yy);
|
|
return sprintf('%04d-%02d-%02d', $year, (int) $dt->format('m'), (int) $dt->format('d'));
|
|
}
|
|
if (preg_match('/^\d{2}\/\d{2}\/\d{4}$/', $s)) {
|
|
$dt = \DateTime::createFromFormat('d/m/Y', $s);
|
|
return $dt?->format('Y-m-d');
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private function money($v): float
|
|
{
|
|
if ($v === null || $v === '') {
|
|
return 0.0;
|
|
}
|
|
|
|
$s = trim((string) $v);
|
|
if ($s === '') {
|
|
return 0.0;
|
|
}
|
|
|
|
// Remove spaces (including non-breaking)
|
|
$s = str_replace([" ", "\u{00A0}"], '', $s);
|
|
|
|
// If it looks like a number with dots as thousand separators and comma as decimal:
|
|
// e.g. 1.234.567,89
|
|
if (preg_match('/^-?\d{1,3}(?:\.\d{3})+(?:,\d+)?$/', $s)) {
|
|
$s = str_replace('.', '', $s);
|
|
$s = str_replace(',', '.', $s);
|
|
return (float) $s;
|
|
}
|
|
|
|
// If it looks like a number with commas as thousand separators and dot as decimal:
|
|
// e.g. 1,234,567.89
|
|
if (preg_match('/^-?\d{1,3}(?:,\d{3})+(?:\.\d+)?$/', $s)) {
|
|
$s = str_replace(',', '', $s);
|
|
return (float) $s;
|
|
}
|
|
|
|
// Otherwise: treat comma as decimal separator if present, keep dot as decimal.
|
|
// This correctly handles MDB exports like 79.95999999999999
|
|
if (str_contains($s, ',') && !str_contains($s, '.')) {
|
|
$s = str_replace(',', '.', $s);
|
|
} elseif (str_contains($s, ',') && str_contains($s, '.')) {
|
|
// Mixed but not matching a clear thousands pattern: assume comma is decimal and dots are thousands
|
|
$s = str_replace('.', '', $s);
|
|
$s = str_replace(',', '.', $s);
|
|
}
|
|
|
|
// Strip anything not number-ish
|
|
$s = preg_replace('/[^0-9\-\.]/', '', $s) ?? '';
|
|
if ($s === '' || $s === '-' || $s === '.') {
|
|
return 0.0;
|
|
}
|
|
|
|
return (float) $s;
|
|
}
|
|
|
|
private function decimal($v): ?float
|
|
{
|
|
if ($v === null || $v === '') {
|
|
return null;
|
|
}
|
|
return $this->money($v);
|
|
}
|
|
}
|