259 lines
8.6 KiB
PHP
Executable File
259 lines
8.6 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\Stabile;
|
|
use App\Models\StgFatturaAde;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
|
|
class GesconImportFattureAdeCsv extends Command
|
|
{
|
|
protected $signature = 'gescon:import-fatture-ade-csv
|
|
{amministratore_id : ID amministratore target}
|
|
{--stabile_id= : ID stabile target (opzionale)}
|
|
{--cartella= : Cartella legacy stabile (es. 0021) opzionale, usata solo come metadato}
|
|
{--path= : File CSV specifico da importare}
|
|
{--dir=/home/michele/netgescon/docs/Importazione : Directory da cui importare (ricorsiva)}
|
|
{--dry-run : Non scrive su DB}
|
|
{--limit=0 : Limita righe per file (0 = nessun limite)}';
|
|
|
|
protected $description = 'Importa i CSV AdE (lista fatture FE disponibili) nello staging, deduplicando per Sdi/file e target (amministratore+stabile).';
|
|
|
|
public function handle(): int
|
|
{
|
|
$amministratoreId = (int) $this->argument('amministratore_id');
|
|
$stabileId = $this->option('stabile_id') !== null ? (int) $this->option('stabile_id') : null;
|
|
$cartella = $this->option('cartella') ? trim((string) $this->option('cartella')) : null;
|
|
$path = $this->option('path') ? (string) $this->option('path') : null;
|
|
$dir = (string) ($this->option('dir') ?: '');
|
|
$dry = (bool) $this->option('dry-run');
|
|
$limit = max(0, (int) $this->option('limit'));
|
|
|
|
if ($amministratoreId <= 0) {
|
|
$this->error('amministratore_id non valido');
|
|
return self::FAILURE;
|
|
}
|
|
|
|
if ($stabileId !== null && $stabileId > 0) {
|
|
$exists = Stabile::query()->whereKey($stabileId)->where('amministratore_id', $amministratoreId)->exists();
|
|
if (!$exists) {
|
|
$this->error("stabile_id={$stabileId} non appartiene ad amministratore_id={$amministratoreId}");
|
|
return self::FAILURE;
|
|
}
|
|
}
|
|
|
|
$files = [];
|
|
if ($path) {
|
|
if (!is_file($path)) {
|
|
$this->error("CSV non trovato: {$path}");
|
|
return self::FAILURE;
|
|
}
|
|
$files = [$path];
|
|
} else {
|
|
if ($dir === '' || !is_dir($dir)) {
|
|
$this->error("Directory non valida: {$dir}");
|
|
return self::FAILURE;
|
|
}
|
|
$it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir));
|
|
foreach ($it as $file) {
|
|
if (!$file instanceof \SplFileInfo) {
|
|
continue;
|
|
}
|
|
if (!$file->isFile()) {
|
|
continue;
|
|
}
|
|
$name = $file->getFilename();
|
|
if (!Str::endsWith(Str::lower($name), '.csv')) {
|
|
continue;
|
|
}
|
|
$files[] = $file->getPathname();
|
|
}
|
|
sort($files);
|
|
}
|
|
|
|
if (empty($files)) {
|
|
$this->warn('Nessun CSV trovato');
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$stats = [
|
|
'files' => count($files),
|
|
'rows' => 0,
|
|
'created' => 0,
|
|
'updated' => 0,
|
|
'skipped' => 0,
|
|
];
|
|
|
|
$runner = function () use (&$stats, $files, $amministratoreId, $stabileId, $cartella, $dry, $limit): void {
|
|
foreach ($files as $filePath) {
|
|
[$rows, $headers] = $this->readCsvSemicolon($filePath);
|
|
|
|
if (empty($rows)) {
|
|
continue;
|
|
}
|
|
|
|
if ($limit > 0) {
|
|
$rows = array_slice($rows, 0, $limit);
|
|
}
|
|
|
|
foreach ($rows as $row) {
|
|
$stats['rows']++;
|
|
|
|
$sdi = $this->clean($row['sdi/file'] ?? null);
|
|
if ($sdi === null) {
|
|
$stats['skipped']++;
|
|
continue;
|
|
}
|
|
|
|
$payload = [
|
|
'amministratore_id' => $amministratoreId,
|
|
'stabile_id' => $stabileId,
|
|
'cartella_legacy' => $cartella,
|
|
'tipo_documento' => $this->clean($row['tipo documento'] ?? null),
|
|
'numero_documento' => $this->clean($row['numero fattura / documento'] ?? null),
|
|
'data_emissione' => $this->parseDateIt($row['data emissione fattura'] ?? null),
|
|
'identificativo_fornitore' => $this->clean($row['identificativo fornitore'] ?? null),
|
|
'denominazione_fornitore' => $this->clean($row['denominazione fornitore'] ?? null),
|
|
'imponibile' => $this->parseDecimalIt($row['imponibile / importo'] ?? null),
|
|
'imposta' => $this->parseDecimalIt($row['imposta (totale in euro)'] ?? null),
|
|
'sdi_file' => $sdi,
|
|
'stato_visualizzazione' => $this->clean($row['fatture visualizzate'] ?? null),
|
|
'source_file' => $filePath,
|
|
'raw' => $row,
|
|
];
|
|
|
|
if ($dry) {
|
|
continue;
|
|
}
|
|
|
|
$existing = StgFatturaAde::query()
|
|
->where('amministratore_id', $amministratoreId)
|
|
->where('stabile_id', $stabileId)
|
|
->where('sdi_file', $sdi)
|
|
->first();
|
|
|
|
if ($existing) {
|
|
$existing->fill($payload);
|
|
if ($existing->isDirty()) {
|
|
$existing->save();
|
|
$stats['updated']++;
|
|
}
|
|
} else {
|
|
StgFatturaAde::create($payload);
|
|
$stats['created']++;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
if ($dry) {
|
|
$runner();
|
|
} else {
|
|
DB::transaction($runner);
|
|
}
|
|
|
|
$this->info('✅ Import AdE CSV completato' . ($dry ? ' (dry-run)' : ''));
|
|
$this->line(json_encode($stats, JSON_UNESCAPED_UNICODE));
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
/** @return array{0: array<int,array<string,mixed>>, 1: array<int,string>} */
|
|
private function readCsvSemicolon(string $filePath): array
|
|
{
|
|
$content = @file_get_contents($filePath);
|
|
if (!is_string($content) || trim($content) === '') {
|
|
return [[], []];
|
|
}
|
|
|
|
// Normalize line endings
|
|
$content = str_replace(["\r\n", "\r"], "\n", $content);
|
|
|
|
$fh = fopen('php://temp', 'r+');
|
|
fwrite($fh, $content);
|
|
rewind($fh);
|
|
|
|
$headers = fgetcsv($fh, separator: ';');
|
|
if (!$headers) {
|
|
return [[], []];
|
|
}
|
|
|
|
$headers = array_map(fn($h) => mb_strtolower(trim((string) $h)), $headers);
|
|
|
|
$rows = [];
|
|
while (($cols = fgetcsv($fh, separator: ';')) !== false) {
|
|
if (empty($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 (!$assoc) {
|
|
continue;
|
|
}
|
|
$rows[] = $assoc;
|
|
}
|
|
|
|
return [$rows, $headers];
|
|
}
|
|
|
|
private function parseDateIt($v): ?string
|
|
{
|
|
$s = $this->clean($v);
|
|
if ($s === null) {
|
|
return null;
|
|
}
|
|
|
|
// Expected dd/mm/yyyy
|
|
$parts = explode('/', $s);
|
|
if (count($parts) !== 3) {
|
|
return null;
|
|
}
|
|
|
|
[$d, $m, $y] = $parts;
|
|
$d = (int) $d;
|
|
$m = (int) $m;
|
|
$y = (int) $y;
|
|
if ($d <= 0 || $m <= 0 || $y <= 0) {
|
|
return null;
|
|
}
|
|
|
|
return sprintf('%04d-%02d-%02d', $y, $m, $d);
|
|
}
|
|
|
|
private function parseDecimalIt($v): float
|
|
{
|
|
$s = $this->clean($v);
|
|
if ($s === null) {
|
|
return 0.0;
|
|
}
|
|
|
|
// Remove thousand separators and convert comma decimal
|
|
$s = str_replace('.', '', $s);
|
|
$s = str_replace(',', '.', $s);
|
|
$s = preg_replace('/[^0-9\.-]/', '', $s) ?? $s;
|
|
|
|
if ($s === '' || !is_numeric($s)) {
|
|
return 0.0;
|
|
}
|
|
|
|
return (float) $s;
|
|
}
|
|
|
|
private function clean($v): ?string
|
|
{
|
|
if ($v === null) {
|
|
return null;
|
|
}
|
|
$s = trim((string) $v);
|
|
return $s === '' ? null : $s;
|
|
}
|
|
}
|