diff --git a/app/Console/Commands/ScanAndImportFeFilesCommand.php b/app/Console/Commands/ScanAndImportFeFilesCommand.php new file mode 100644 index 0000000..ac233ff --- /dev/null +++ b/app/Console/Commands/ScanAndImportFeFilesCommand.php @@ -0,0 +1,292 @@ +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; + } +} diff --git a/app/Filament/Pages/Condomini/NominativiStabile.php b/app/Filament/Pages/Condomini/NominativiStabile.php index 999627a..3bcf763 100755 --- a/app/Filament/Pages/Condomini/NominativiStabile.php +++ b/app/Filament/Pages/Condomini/NominativiStabile.php @@ -470,34 +470,91 @@ protected function getTableQuery(): Builder protected function buildDomainConsolidatedQuery(int $stabileId): Builder { + $user = Auth::user(); + $activeAnno = $user instanceof \App\Models\User ? \App\Support\AnnoGestioneContext::resolveActiveAnno($user) : (int) date('Y'); + $codStabile = $this->resolveLegacyStabileCode($stabileId); + + $nameExpr = "CASE WHEN COALESCE(p.ragione_sociale, '') <> '' THEN p.ragione_sociale ELSE TRIM(CONCAT(COALESCE(p.cognome, ''), ' ', COALESCE(p.nome, ''))) END"; + + $isSqlite = DB::connection()->getDriverName() === 'sqlite'; + $groupConcatOwner = $isSqlite + ? "GROUP_CONCAT(DISTINCT ({$nameExpr}))" + : "GROUP_CONCAT(DISTINCT ({$nameExpr}) SEPARATOR ' / ')"; + $groupConcatCell = $isSqlite + ? "GROUP_CONCAT(DISTINCT COALESCE(p.telefono_principale, ''))" + : "GROUP_CONCAT(DISTINCT COALESCE(p.telefono_principale, '') SEPARATOR ' / ')"; + $groupConcatCf = $isSqlite + ? "GROUP_CONCAT(DISTINCT COALESCE(p.codice_fiscale, ''))" + : "GROUP_CONCAT(DISTINCT COALESCE(p.codice_fiscale, '') SEPARATOR ' / ')"; + $ownerNameSub = DB::table('persone_unita_relazioni as pur') ->join('persone as p', 'p.id', '=', 'pur.persona_id') ->whereColumn('pur.unita_id', 'unita_immobiliari.id') - ->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario']) + ->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario', 'condomino', 'usufrutto']) ->where('pur.attivo', true) - ->selectRaw("GROUP_CONCAT(DISTINCT TRIM(CONCAT(COALESCE(p.cognome, ''), ' ', COALESCE(p.nome, ''), ' ', COALESCE(p.ragione_sociale, ''))) SEPARATOR ' / ')"); + ->where(function ($q) use ($activeAnno) { + $q->whereNull('pur.data_fine') + ->orWhereRaw("SUBSTR(pur.data_fine, 1, 4) >= ?", [(string) $activeAnno]) + ->orWhere('pur.data_fine', ''); + }) + ->where(function ($q) use ($activeAnno) { + $q->whereNull('pur.data_inizio') + ->orWhereRaw("SUBSTR(pur.data_inizio, 1, 4) <= ?", [(string) $activeAnno]) + ->orWhere('pur.data_inizio', ''); + }) + ->selectRaw($groupConcatOwner); $tenantNameSub = DB::table('persone_unita_relazioni as pur') ->join('persone as p', 'p.id', '=', 'pur.persona_id') ->whereColumn('pur.unita_id', 'unita_immobiliari.id') - ->where('pur.tipo_relazione', 'inquilino') + ->whereIn('pur.tipo_relazione', ['inquilino', 'conduttore', 'locatario']) ->where('pur.attivo', true) + ->where(function ($q) use ($activeAnno) { + $q->whereNull('pur.data_fine') + ->orWhereRaw("SUBSTR(pur.data_fine, 1, 4) >= ?", [(string) $activeAnno]) + ->orWhere('pur.data_fine', ''); + }) + ->where(function ($q) use ($activeAnno) { + $q->whereNull('pur.data_inizio') + ->orWhereRaw("SUBSTR(pur.data_inizio, 1, 4) <= ?", [(string) $activeAnno]) + ->orWhere('pur.data_inizio', ''); + }) ->orderByDesc('pur.id') - ->selectRaw("TRIM(CONCAT(COALESCE(p.cognome, ''), ' ', COALESCE(p.nome, ''), ' ', COALESCE(p.ragione_sociale, '')))") + ->selectRaw("{$nameExpr}") ->limit(1); $ownerCellSub = DB::table('persone_unita_relazioni as pur') ->join('persone as p', 'p.id', '=', 'pur.persona_id') ->whereColumn('pur.unita_id', 'unita_immobiliari.id') - ->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario']) + ->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario', 'condomino', 'usufrutto']) ->where('pur.attivo', true) - ->selectRaw("GROUP_CONCAT(DISTINCT COALESCE(p.telefono_principale, '') SEPARATOR ' / ')"); + ->where(function ($q) use ($activeAnno) { + $q->whereNull('pur.data_fine') + ->orWhereRaw("SUBSTR(pur.data_fine, 1, 4) >= ?", [(string) $activeAnno]) + ->orWhere('pur.data_fine', ''); + }) + ->where(function ($q) use ($activeAnno) { + $q->whereNull('pur.data_inizio') + ->orWhereRaw("SUBSTR(pur.data_inizio, 1, 4) <= ?", [(string) $activeAnno]) + ->orWhere('pur.data_inizio', ''); + }) + ->selectRaw($groupConcatCell); $tenantCellSub = DB::table('persone_unita_relazioni as pur') ->join('persone as p', 'p.id', '=', 'pur.persona_id') ->whereColumn('pur.unita_id', 'unita_immobiliari.id') - ->where('pur.tipo_relazione', 'inquilino') + ->whereIn('pur.tipo_relazione', ['inquilino', 'conduttore', 'locatario']) ->where('pur.attivo', true) + ->where(function ($q) use ($activeAnno) { + $q->whereNull('pur.data_fine') + ->orWhereRaw("SUBSTR(pur.data_fine, 1, 4) >= ?", [(string) $activeAnno]) + ->orWhere('pur.data_fine', ''); + }) + ->where(function ($q) use ($activeAnno) { + $q->whereNull('pur.data_inizio') + ->orWhereRaw("SUBSTR(pur.data_inizio, 1, 4) <= ?", [(string) $activeAnno]) + ->orWhere('pur.data_inizio', ''); + }) ->orderByDesc('pur.id') ->selectRaw("COALESCE(p.telefono_principale, '')") ->limit(1); @@ -505,20 +562,40 @@ protected function buildDomainConsolidatedQuery(int $stabileId): Builder $ownerCfSub = DB::table('persone_unita_relazioni as pur') ->join('persone as p', 'p.id', '=', 'pur.persona_id') ->whereColumn('pur.unita_id', 'unita_immobiliari.id') - ->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario']) + ->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario', 'condomino', 'usufrutto']) ->where('pur.attivo', true) - ->selectRaw("GROUP_CONCAT(DISTINCT COALESCE(p.codice_fiscale, '') SEPARATOR ' / ')"); + ->where(function ($q) use ($activeAnno) { + $q->whereNull('pur.data_fine') + ->orWhereRaw("SUBSTR(pur.data_fine, 1, 4) >= ?", [(string) $activeAnno]) + ->orWhere('pur.data_fine', ''); + }) + ->where(function ($q) use ($activeAnno) { + $q->whereNull('pur.data_inizio') + ->orWhereRaw("SUBSTR(pur.data_inizio, 1, 4) <= ?", [(string) $activeAnno]) + ->orWhere('pur.data_inizio', ''); + }) + ->selectRaw($groupConcatCf); $tenantCfSub = DB::table('persone_unita_relazioni as pur') ->join('persone as p', 'p.id', '=', 'pur.persona_id') ->whereColumn('pur.unita_id', 'unita_immobiliari.id') - ->where('pur.tipo_relazione', 'inquilino') + ->whereIn('pur.tipo_relazione', ['inquilino', 'conduttore', 'locatario']) ->where('pur.attivo', true) + ->where(function ($q) use ($activeAnno) { + $q->whereNull('pur.data_fine') + ->orWhereRaw("SUBSTR(pur.data_fine, 1, 4) >= ?", [(string) $activeAnno]) + ->orWhere('pur.data_fine', ''); + }) + ->where(function ($q) use ($activeAnno) { + $q->whereNull('pur.data_inizio') + ->orWhereRaw("SUBSTR(pur.data_inizio, 1, 4) <= ?", [(string) $activeAnno]) + ->orWhere('pur.data_inizio', ''); + }) ->orderByDesc('pur.id') ->selectRaw("COALESCE(p.codice_fiscale, '')") ->limit(1); - return UnitaImmobiliare::query() + $orderQuery = UnitaImmobiliare::query() ->where('stabile_id', $stabileId) ->whereNull('deleted_at') ->when($this->cumulato, function (Builder $query) use ($stabileId): void { @@ -546,8 +623,15 @@ protected function buildDomainConsolidatedQuery(int $stabileId): Builder ->selectSub($ownerCfSub, 'cond_cod_fisc') ->selectSub($tenantCfSub, 'inquil_cod_fisc') ->orderBy('unita_immobiliari.scala') - ->orderByRaw("CASE WHEN unita_immobiliari.interno IS NULL OR unita_immobiliari.interno = '' THEN 1 ELSE 0 END") - ->orderByRaw("CASE WHEN unita_immobiliari.interno REGEXP '^[0-9]+' THEN CAST(unita_immobiliari.interno AS UNSIGNED) ELSE 999999 END") + ->orderByRaw("CASE WHEN unita_immobiliari.interno IS NULL OR unita_immobiliari.interno = '' THEN 1 ELSE 0 END"); + + if ($isSqlite) { + $orderQuery->orderByRaw("CASE WHEN unita_immobiliari.interno GLOB '[0-9]*' THEN CAST(unita_immobiliari.interno AS INTEGER) ELSE 999999 END"); + } else { + $orderQuery->orderByRaw("CASE WHEN unita_immobiliari.interno REGEXP '^[0-9]+' THEN CAST(unita_immobiliari.interno AS UNSIGNED) ELSE 999999 END"); + } + + return $orderQuery ->orderBy('unita_immobiliari.interno') ->orderBy('unita_immobiliari.id'); } diff --git a/app/Filament/Pages/Contabilita/FattureElettronicheArchivio.php b/app/Filament/Pages/Contabilita/FattureElettronicheArchivio.php index ab4155f..a1860b2 100755 --- a/app/Filament/Pages/Contabilita/FattureElettronicheArchivio.php +++ b/app/Filament/Pages/Contabilita/FattureElettronicheArchivio.php @@ -54,7 +54,7 @@ public static function canAccess(): bool return false; } - if ($user->hasAnyRole(['super-admin', 'admin'])) { + if ($user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore'])) { return true; } diff --git a/app/Filament/Pages/Contabilita/FattureElettronicheP7mRicevute.php b/app/Filament/Pages/Contabilita/FattureElettronicheP7mRicevute.php index bb08212..937ab44 100755 --- a/app/Filament/Pages/Contabilita/FattureElettronicheP7mRicevute.php +++ b/app/Filament/Pages/Contabilita/FattureElettronicheP7mRicevute.php @@ -807,7 +807,7 @@ public static function canAccess(): bool return false; } - if ($user->hasAnyRole(['super-admin', 'admin'])) { + if ($user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore'])) { return true; } diff --git a/app/Services/FattureElettroniche/P7mExtractor.php b/app/Services/FattureElettroniche/P7mExtractor.php index 94459eb..206ba6e 100755 --- a/app/Services/FattureElettroniche/P7mExtractor.php +++ b/app/Services/FattureElettroniche/P7mExtractor.php @@ -13,6 +13,12 @@ public function extractXmlFromP7m(string $p7mPath): string throw new RuntimeException("File non trovato: {$p7mPath}"); } + // 1. Fast path: estrazione diretta in memoria da DER PKCS#7 (elimina overhead di processi openssl) + $fastXml = $this->tryExtractXmlFromDerPkcs7($p7mPath); + if (is_string($fastXml) && trim($fastXml) !== '') { + return $fastXml; + } + $tmpOut = tempnam(sys_get_temp_dir(), 'fatturapa_'); if (! is_string($tmpOut) || $tmpOut === '') { throw new RuntimeException('Impossibile creare file temporaneo.'); diff --git a/public/adminer.php b/public/adminer.php index 5412a2e..fc4a51b 100644 --- a/public/adminer.php +++ b/public/adminer.php @@ -1,11 +1,7 @@ 'pgsql', - 'server' => '127.0.0.1', - 'username' => 'netgescon', - 'password' => 'netgescon_pass', - 'db' => $dbName, - ]; - } else { - $_GET['server'] = '127.0.0.1'; - $_GET['username'] = $mySqlUser; - $_GET['db'] = $dbName; - $_POST['auth'] = [ - 'driver' => 'server', - 'server' => '127.0.0.1', - 'username' => $mySqlUser, - 'password' => $mySqlPass, - 'db' => $dbName, - ]; - } +// Pre-authorize session credentials for Adminer +$_SESSION["pwds"]["server"]["127.0.0.1"][$mySqlUser] = $mySqlPass; +$_SESSION["pwds"]["server"]["localhost"][$mySqlUser] = $mySqlPass; +$_SESSION["pwds"]["pgsql"]["127.0.0.1"]["netgescon"] = "netgescon_pass"; +$_SESSION["pwds"]["pgsql"]["localhost"]["netgescon"] = "netgescon_pass"; + +if (empty($_GET) || (isset($_GET['auto']) && $_GET['auto'] === 'mysql')) { + header("Location: adminer.php?server=127.0.0.1&username=" . urlencode($mySqlUser) . "&db=" . urlencode($dbName)); + exit; } -// 3. ADMINER OBJECT CUSTOMIZATION +if (isset($_GET['auto']) && $_GET['auto'] === 'pgsql') { + header("Location: adminer.php?pgsql=127.0.0.1&username=netgescon&db=" . urlencode($dbName)); + exit; +} + +// 3. ADMINER PLUGIN/CUSTOMIZATION function adminer_object() { class NetGesconAdminerSecurity extends \Adminer\Adminer { function name() { return "NetGescon Database Visualizer"; } - function credentials() { - $driver = $_POST["auth"]["driver"] ?? $_GET["driver"] ?? (isset($_GET["pgsql"]) ? "pgsql" : "server"); - $dbHost = "127.0.0.1"; - - if ($driver === "pgsql" || isset($_GET["pgsql"]) || (isset($_GET["auto"]) && $_GET["auto"] === "pgsql")) { - return array($dbHost, "netgescon", "netgescon_pass"); - } - - $dbUser = env_get_val("DB_USERNAME", "netgescon_user"); - $dbPass = env_get_val("DB_PASSWORD", "NetGescon2024!"); - - return array($dbHost, $dbUser, $dbPass); - } - - function database() { - return env_get_val("DB_DATABASE", "netgescon"); - } - function login($login, $password) { return true; } - function loginForm() { - $mySqlUser = env_get_val("DB_USERNAME", "netgescon_user"); - $dbName = env_get_val("DB_DATABASE", "netgescon"); - - echo '