293 lines
11 KiB
PHP
293 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\FatturaElettronica;
|
|
use App\Models\OperazioneContabile;
|
|
use App\Models\Stabile;
|
|
use App\Services\FattureElettroniche\FatturaElettronicaImporter;
|
|
use App\Services\FattureElettroniche\FatturaElettronicaXmlParser;
|
|
use App\Services\FattureElettroniche\P7mExtractor;
|
|
use App\Services\TenantArchivePathService;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\Log;
|
|
use RecursiveDirectoryIterator;
|
|
use RecursiveIteratorIterator;
|
|
use SplFileInfo;
|
|
|
|
class ScanAndImportFeFilesCommand extends Command
|
|
{
|
|
protected $signature = 'gescon:scan-import-fe
|
|
{--dir= : Directory specifica da scansionare}
|
|
{--stabile= : Filtra per codice stabile (es. 0013, 0021)}
|
|
{--dry-run : Esegui solo simulazione senza copiare né scrivere su DB}';
|
|
|
|
protected $description = 'Scansiona l\'intero archivio di backup, assegna le FE agli stabili tramite Codice Fiscale, copia i file nelle rispettive cartelle e importa nel DB';
|
|
|
|
public function handle(
|
|
FatturaElettronicaXmlParser $parser,
|
|
FatturaElettronicaImporter $importer,
|
|
P7mExtractor $p7mExtractor,
|
|
TenantArchivePathService $pathService
|
|
): int {
|
|
$this->info('======================================================================');
|
|
$this->info(' NETGESCON: SCANSIONE, MATCHING FISCALE & IMPORTAZIONE FATTURE FE ');
|
|
$this->info('======================================================================');
|
|
|
|
$isDryRun = (bool) $this->option('dry-run');
|
|
$filterStabile = $this->option('stabile');
|
|
$customDir = $this->option('dir');
|
|
|
|
// 1. Carica gli stabili canonici con Codice Fiscale
|
|
$stabiliQuery = Stabile::whereNotNull('codice_fiscale')->where('codice_fiscale', '<>', '');
|
|
if ($filterStabile) {
|
|
$stabiliQuery->where('codice_stabile', $filterStabile);
|
|
}
|
|
$stabili = $stabiliQuery->get();
|
|
|
|
if ($stabili->isEmpty()) {
|
|
$this->error('Nessuno stabile canonico con Codice Fiscale configurato trovato.');
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
$cfMap = [];
|
|
foreach ($stabili as $s) {
|
|
$cleanCf = strtoupper(preg_replace('/[^A-Z0-9]/i', '', (string) $s->codice_fiscale));
|
|
if ($cleanCf !== '') {
|
|
$cfMap[$cleanCf] = $s;
|
|
}
|
|
}
|
|
|
|
$this->info("Stabili target attivi: " . count($cfMap));
|
|
foreach ($cfMap as $cf => $s) {
|
|
$this->line(" - Stabile [{$s->codice_stabile}] {$s->denominazione} (CF: {$cf})");
|
|
}
|
|
|
|
// 2. Determina le directory sorgente da scansionare
|
|
$scanRoots = [];
|
|
if ($customDir) {
|
|
if (! is_dir($customDir)) {
|
|
$this->error("Directory non trovata: {$customDir}");
|
|
return Command::FAILURE;
|
|
}
|
|
$scanRoots[] = realpath($customDir);
|
|
} else {
|
|
$basePath = base_path();
|
|
$candidates = [
|
|
$basePath . '/_legacy-vault',
|
|
$basePath . '/Miki-Bug-workspace',
|
|
$basePath . '/storage/gescon',
|
|
$basePath . '/storage/app/amministratori',
|
|
$basePath . '/storage/app/private/amministratori',
|
|
$basePath . '/storage/app/public',
|
|
];
|
|
foreach ($candidates as $c) {
|
|
if (is_dir($c)) {
|
|
$scanRoots[] = realpath($c);
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->info("\nCartelle sorgente incluse nella scansione: " . count($scanRoots));
|
|
|
|
// 3. Raccogli tutti i file .xml e .p7m
|
|
$foundFiles = [];
|
|
$excludedFolders = ['/vendor/', '/node_modules/', '/.git/', '/.gemini/', '/storage/framework/', '/bootstrap/cache/'];
|
|
|
|
foreach ($scanRoots as $root) {
|
|
$this->line(" Scansione: {$root}...");
|
|
$iterator = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS),
|
|
RecursiveIteratorIterator::SELF_FIRST
|
|
);
|
|
|
|
/** @var SplFileInfo $file */
|
|
foreach ($iterator as $file) {
|
|
if (! $file->isFile()) {
|
|
continue;
|
|
}
|
|
$path = $file->getRealPath();
|
|
$lowerPath = strtolower($path);
|
|
|
|
$skip = false;
|
|
foreach ($excludedFolders as $ex) {
|
|
if (str_contains($lowerPath, $ex)) {
|
|
$skip = true;
|
|
break;
|
|
}
|
|
}
|
|
if ($skip) {
|
|
continue;
|
|
}
|
|
|
|
if (str_ends_with($lowerPath, '.xml') || str_ends_with($lowerPath, '.p7m')) {
|
|
$foundFiles[] = $path;
|
|
}
|
|
}
|
|
}
|
|
|
|
$foundFiles = array_values(array_unique($foundFiles));
|
|
$totalFiles = count($foundFiles);
|
|
$this->info("Totale file XML/P7M individuati: {$totalFiles}\n");
|
|
|
|
if ($totalFiles === 0) {
|
|
$this->warn('Nessun file XML o P7M trovato.');
|
|
return Command::SUCCESS;
|
|
}
|
|
|
|
// 4. Scansione e parsing semantico di ciascun file
|
|
$stats = [
|
|
'scanned' => 0,
|
|
'matched' => 0,
|
|
'unmatched' => 0,
|
|
'copied' => 0,
|
|
'imported' => 0,
|
|
'duplicates' => 0,
|
|
'errors' => 0,
|
|
'by_stabile' => [],
|
|
];
|
|
|
|
$progressBar = $this->output->createProgressBar($totalFiles);
|
|
$progressBar->start();
|
|
|
|
foreach ($foundFiles as $filePath) {
|
|
$stats['scanned']++;
|
|
$progressBar->advance();
|
|
|
|
$lowerPath = strtolower($filePath);
|
|
$filename = basename($filePath);
|
|
$isP7m = str_ends_with($lowerPath, '.p7m');
|
|
|
|
try {
|
|
if ($isP7m) {
|
|
$xml = $p7mExtractor->extractXmlFromP7m($filePath);
|
|
} else {
|
|
$xml = file_get_contents($filePath);
|
|
}
|
|
|
|
if (! is_string($xml) || trim($xml) === '') {
|
|
continue;
|
|
}
|
|
|
|
$data = $parser->parse($xml);
|
|
$destCf = strtoupper(preg_replace('/[^A-Z0-9]/i', '', (string) ($data['destinatario_cf'] ?? '')));
|
|
$destPiva = strtoupper(preg_replace('/[^A-Z0-9]/i', '', (string) ($data['destinatario_piva'] ?? '')));
|
|
|
|
$matchedStabile = $cfMap[$destCf] ?? ($cfMap[$destPiva] ?? null);
|
|
|
|
if (! $matchedStabile) {
|
|
$stats['unmatched']++;
|
|
continue;
|
|
}
|
|
|
|
$stats['matched']++;
|
|
$codStabile = $matchedStabile->codice_stabile;
|
|
$stats['by_stabile'][$codStabile] = ($stats['by_stabile'][$codStabile] ?? 0) + 1;
|
|
|
|
if ($isDryRun) {
|
|
continue;
|
|
}
|
|
|
|
// Determina la cartella di destinazione corretta
|
|
$inboxAbsolute = $pathService->stabileAbsolutePath($matchedStabile, 'fatture_elettroniche/inbox');
|
|
if (! is_dir($inboxAbsolute)) {
|
|
@mkdir($inboxAbsolute, 0755, true);
|
|
}
|
|
|
|
$destinationFile = $inboxAbsolute . '/' . $filename;
|
|
if (! file_exists($destinationFile) && realpath($filePath) !== realpath($destinationFile)) {
|
|
// Copia preservando sorgente intatta
|
|
@copy($filePath, $destinationFile);
|
|
$stats['copied']++;
|
|
}
|
|
|
|
// Importazione nel DB NetGescon
|
|
$importRes = $importer->importXml(
|
|
$xml,
|
|
(int) $matchedStabile->id,
|
|
$filename,
|
|
[
|
|
'force_stabile_id' => (int) $matchedStabile->id,
|
|
'create_fornitore_if_missing'=> true,
|
|
'auto_repair_duplicate' => true,
|
|
'p7m_path' => $isP7m ? $pathService->stabileRelativePath($matchedStabile, 'fatture_elettroniche/inbox/' . $filename) : null,
|
|
]
|
|
);
|
|
|
|
if ($importRes['status'] === 'imported') {
|
|
$stats['imported']++;
|
|
} elseif ($importRes['status'] === 'duplicate') {
|
|
$stats['duplicates']++;
|
|
} else {
|
|
$stats['errors']++;
|
|
}
|
|
|
|
} catch (\Throwable $e) {
|
|
$stats['errors']++;
|
|
}
|
|
}
|
|
|
|
$progressBar->finish();
|
|
$this->newLine(2);
|
|
|
|
// 5. Riconciliazione automatica con operazioni_contabili (num_fat, dt_fat)
|
|
$this->info("Riconciliazione automatica FE con operazioni contabili e registri...");
|
|
$reconciledOps = 0;
|
|
foreach ($stabili as $stabile) {
|
|
$gestioneIds = \App\Models\GestioneContabile::where('stabile_id', $stabile->id)->pluck('id');
|
|
if ($gestioneIds->isEmpty()) {
|
|
continue;
|
|
}
|
|
$feRows = FatturaElettronica::where('stabile_id', $stabile->id)->get();
|
|
foreach ($feRows as $fe) {
|
|
$numFat = trim((string) $fe->numero_fattura);
|
|
$dtFat = $fe->data_fattura ? Carbon::parse($fe->data_fattura)->format('Y-m-d') : null;
|
|
if ($numFat === '' || ! $dtFat) {
|
|
continue;
|
|
}
|
|
|
|
$updated = OperazioneContabile::whereIn('gestione_id', $gestioneIds)
|
|
->whereNull('fe_fattura_id')
|
|
->where('num_fat', $numFat)
|
|
->whereDate('dt_fat', $dtFat)
|
|
->update(['fe_fattura_id' => $fe->id]);
|
|
|
|
if ($updated > 0) {
|
|
$reconciledOps += $updated;
|
|
}
|
|
}
|
|
}
|
|
$this->info("Operazioni contabili collegate a Fattura Elettronica ID: {$reconciledOps}");
|
|
|
|
// 6. Riepilogo finale
|
|
$this->info('======================================================================');
|
|
$this->info(' RIEPILOGO SCANSIONE FE ');
|
|
$this->info('======================================================================');
|
|
$this->table(
|
|
['Metrica', 'Valore'],
|
|
[
|
|
['File scansionati', $stats['scanned']],
|
|
['File associati a Stabili (Match CF)', $stats['matched']],
|
|
['File senza riscontro CF stabili', $stats['unmatched']],
|
|
['File copiati nelle cartelle stabili', $stats['copied']],
|
|
['Fatture importate a nuovo su DB', $stats['imported']],
|
|
['Fatture già presenti / aggiornate', $stats['duplicates']],
|
|
['Errori di lettura/parsing', $stats['errors']],
|
|
['Operazioni contabili riconciliate a FE', $reconciledOps],
|
|
]
|
|
);
|
|
|
|
$this->info("\nDistribuzione fatture per Stabile:");
|
|
foreach ($stats['by_stabile'] as $cod => $count) {
|
|
$stabileObj = $stabili->firstWhere('codice_stabile', $cod);
|
|
$denom = $stabileObj ? $stabileObj->denominazione : '';
|
|
$this->line(" - Stabile [{$cod}] {$denom}: {$count} fatture");
|
|
}
|
|
|
|
return Command::SUCCESS;
|
|
}
|
|
}
|