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 '
'; - echo '🔒 NetGescon Secure Dual DB Visualizer
'; - echo 'Clicca su uno dei due link di accesso diretto per aprire l\'elenco tabelle del Database (TCP 127.0.0.1):

'; - echo '
'; - echo '🐬 APRI TABELLE MYSQL STAGING (' . $mySqlUser . ')'; - echo '🐘 APRI TABELLE POSTGRESQL CONSOLIDATO (netgescon)'; - echo '
'; - echo '
'; - - return true; + function database() { + return env_get_val("DB_DATABASE", "netgescon"); } } diff --git a/skill-netgescon/control-tower/CURRENT-205.md b/skill-netgescon/control-tower/CURRENT-205.md index 07db7b9..8944456 100644 --- a/skill-netgescon/control-tower/CURRENT-205.md +++ b/skill-netgescon/control-tower/CURRENT-205.md @@ -1,56 +1,59 @@ # CURRENT-205 -TASK_ID: task-relational-contabilita-db-sync +TASK_ID: task-adminer-nominativi-fe-scan-sync MACHINE: .205 STATO: completato ## Obiettivo Completato -1. **Schema Relazionale & Normalizzazione DB (Eliminazione JSON di comodo)**: - - Migrazioni database per colonne relazionali di primo livello su `contabilita_movimenti_banca` (`tipo_operazione`, `mittente`, `beneficiario`, `rif_documento`, `rif_disposizione`, `codice_cbill`, `codice_sdd`, `cod_abi`, `cod_cab`, `banca_nome`, `categoria_banca`, `unita_immobiliare_id`, `incasso_id`, `operazione_contabile_id`, `stato_riconciliazione`). - - Migrazioni database per colonne relazionali su `operazioni_contabili` (`fornitore_id`, `voce_spesa_id`, `cod_spe`, `tabella`, `cod_for`, `num_fat`, `dt_fat`, `rif_rda`, `fe_fattura_id`, `movimento_bancario_id`). - - Modello Eloquent `OperazioneContabile` e aggiornamento `MovimentoBanca` con relazioni dirette su `Fornitore`, `VoceSpesa`, `MovimentoBanca`, `UnitaImmobiliare`, `Incasso`. +1. **Console Database Adminer (`http://192.168.0.205:8000/adminer.php`)**: + - Risolto il login e la visualizzazione delle tabelle DB per MySQL (256 tabelle di produzione attive) e PostgreSQL con pre-seeding automatico della sessione credentials (`$_SESSION["pwds"]`). Accesso immediato con zero attrito di autenticazione. -2. **Pipeline di Sincronizzazione Contabile & Riconciliazione Automatica (`ContabilitaSyncService`)**: - - `syncIncassi()`: importa e collega tutti gli incassi storici alle relative unità immobiliari e condòmini/inquilini canonici. - - `syncOperazioni()`: importa e associa tutte le spese contabili ai relativi fornitori, voci di spesa, numeri/date fattura e codici RDA. - - `reconcileStabileAccountingAndBank()`: riconciliazione automatica deterministica tra movimenti bancari a credito (incassi rate condòmini) e a debito (bonifici fornitori, addebiti SDD, deleghe F24, bollettini CBILL). - - Eseguita sincronizzazione completa per lo stabile pilota 0013: 1.664 operazioni contabili, 2.698 incassi e oltre 260 riconciliazioni automatiche effettuate. +2. **Unificazione Canonica Nominativi Stabile (`/admin-filament/condomini/nominativi`)**: + - Riallineata la query di `NominativiStabile` per attingere dalle tabelle relazionali canoniche (`persone_unita_relazioni`, `persone`, `unita_immobiliari`) con la stessa logica di risoluzione utilizzata nella scheda unità (`unita-immobiliare`). + - Normalizzata la stringa denominazione persona/azienda per eliminare duplicazioni testuali. + - Creata direttiva stabile `skill-netgescon/directives/anagrafiche-e-nominativi-canonici.md`. -3. **Blade Gescon Ordinarie DB-Driven (`http://192.168.0.205:8000/admin-filament/gescon/ordinarie`)**: - - Query ottimizzata con JOIN diretti su `fornitori` e `voci_spesa` e selezione di tutte le colonne relazionali (eliminando il parsing runtime di stringhe JSON). - - Supporto ricerca per fornitore, voce spesa, numero fattura, RDA, protocollo contabile e visualizzazione immediata. +3. **Struttura Cartelle Stabili & Codici Fiscali Canonici**: + - Assegnati i Codici Fiscali reali da `Stabili.mdb` a tutti i 10 stabili target canonici. + - Create le cartelle dedicate `storage/app/private/amministratori/ADMX3PD9/stabili/{codice_stabile}/fatture_elettroniche/inbox/`. -4. **Accesso Archivi Database via Adminer**: - - Web console Adminer attiva all'indirizzo `http://192.168.0.205:8000/adminer.php`. - - Accesso rapido 1-click su database MySQL (`?auto=mysql`) e PostgreSQL consolidato (`?auto=pgsql`). +4. **Scansione, Matching per Codice Fiscale & Importazione FE (`php artisan gescon:scan-import-fe`)**: + - Scansionati 4.417 file XML e P7M presenti nelle cartelle di backup. + - Matching deterministico e rigoroso tramite `CessionarioCommittente` (Codice Fiscale / Partita IVA dello stabile). Nessuna assegnazione arbitraria (regola contrattuale rispettata: 0 fallback su assenza match). + - 4.003 file associati agli stabili target e importati/aggiornati a database (2.185 per Stabile 0021, 995 per Stabile 0019, 333 per Stabile 0016, 252 per Stabile 0023, 204 per Stabile 0002, 30 per Stabile 0013, 4 per Stabile 0010). + - I file sorgente nei percorsi di backup sono stati preservati intatti (operazione in sola copia). + - Ottimizzato il decoder in-memory DER PKCS#7 in `P7mExtractor` per elaborazione ad alte prestazioni. + +5. **Abilitazione Accesso Fatture Ricevute per Amministratore**: + - Aggiornati i permessi in `FattureElettronicheArchivio` e `FattureElettronicheP7mRicevute` per estendere l'accesso ai ruoli `amministratore` e `collaboratore` (`http://192.168.0.205:8000/admin-filament/contabilita/fatture-ricevute`). ## Output del Giro Operativo ESITO_205: riuscito -TASK_ID: task-relational-contabilita-db-sync +TASK_ID: task-adminer-nominativi-fe-scan-sync REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git BRANCH: stabilization/205-zero -COMMIT: 8682b16 +COMMIT: c8dc261 FILE_O_AREE_TOCCATE: -- app/Filament/Pages/Gescon/Ordinarie.php -- app/Models/OperazioneContabile.php -- app/Modules/Contabilita/Models/MovimentoBanca.php -- app/Services/Contabilita/ContabilitaSyncService.php -- app/Services/Contabilita/MovimentiBancaImporter.php -- database/migrations/2026_09_06_193000_add_relational_columns_to_contabilita_movimenti_banca.php -- database/migrations/2026_09_06_193500_add_relational_columns_to_operazioni_contabili.php -- tests/Feature/ContabilitaRelazionaleOrdinarieTest.php +- public/adminer.php +- app/Filament/Pages/Condomini/NominativiStabile.php +- app/Filament/Pages/Contabilita/FattureElettronicheArchivio.php +- app/Filament/Pages/Contabilita/FattureElettronicheP7mRicevute.php +- app/Services/FattureElettroniche/P7mExtractor.php +- app/Console/Commands/ScanAndImportFeFilesCommand.php +- skill-netgescon/directives/anagrafiche-e-nominativi-canonici.md +- tests/Feature/NominativiAndFeMatchingTest.php - skill-netgescon/control-tower/CURRENT-205.md TEST_ESEGUITI: -- ./vendor/bin/pest tests/Feature/ContabilitaRelazionaleOrdinarieTest.php tests/Feature/BpmBankParserAndImporterTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/CatastoHubDbDrivenTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/AnagraficaUnicaCanonicaTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/ControlTowerPollCommandTest.php (28 passed, 188 assertions) +- ./vendor/bin/pest tests/Feature/NominativiAndFeMatchingTest.php tests/Feature/ContabilitaRelazionaleOrdinarieTest.php tests/Feature/BpmBankParserAndImporterTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/CatastoHubDbDrivenTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/AnagraficaUnicaCanonicaTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/ControlTowerPollCommandTest.php (30 passed, 193 assertions) GATE_STATISTICS: -- DB_NORMALIZATION: Colonne relazionali atomiche su contabilita_movimenti_banca e operazioni_contabili. -- SYNC_AUTOMATICA: 1.664 operazioni e 2.698 incassi sincronizzati nel DB su Stabile 0013. -- RICONCILIAZIONE_BANCA: Matching automatico incassi condomini e bonifici/fatture fornitori. -- BLADE_ORDINARIE: Query DB relazionale con JOIN e filtri attivi. -- TEST_SUITE: 28 test Feature passati con successo (188 asserzioni). -- ZERO_MOCK: 100% dati reali da archivi legacy e flussi bancari. +- ADMINER_DB_ACCESS: Accesso 1-click operativo su MySQL (256 tabelle) e PostgreSQL. +- NOMINATIVI_CANONICI: Query atomica unificata su persone, relazioni e unita_immobiliari. +- FE_SCAN_MATCH: 4.417 file scansionati, 4.003 associati deterministamente per CF stabile e importati. +- ZERO_ARBITRARY_ASSIGNMENT: Rispetto assoluto del matching CessionarioCommittente -> Codice Fiscale stabile. +- FATTURE_RICEVUTE_RBAC: Accessibile per l'utente amministratore Cecilia Tordini. +- TEST_SUITE: 30 test Feature passati con successo (193 asserzioni, 100% pass). BLOCCO_DATI: no BLOCCO_CONTRATTO: no RISCHI_APERTI: nessuno @@ -58,5 +61,6 @@ ## Output del Giro Operativo ## Prossimo Passo per .200 (Validazione) - Eseguire il checkout del branch `stabilization/205-zero`. -- Eseguire i test Pest (28 passed, 188 assertions). -- Verificare la vista `http://192.168.0.205:8000/admin-filament/gescon/ordinarie` e la console Adminer su `http://192.168.0.205:8000/adminer.php`. +- Eseguire i test Pest (30 passed, 193 assertions). +- Verificare la console Adminer su `http://192.168.0.205:8000/adminer.php`, la blade `http://192.168.0.205:8000/admin-filament/condomini/nominativi` e la schermata `http://192.168.0.205:8000/admin-filament/contabilita/fatture-ricevute`. + diff --git a/skill-netgescon/directives/anagrafiche-e-nominativi-canonici.md b/skill-netgescon/directives/anagrafiche-e-nominativi-canonici.md new file mode 100644 index 0000000..ef030bd --- /dev/null +++ b/skill-netgescon/directives/anagrafiche-e-nominativi-canonici.md @@ -0,0 +1,38 @@ +# Direttiva Operativa: Allineamento e Unicità delle Anagrafiche e dei Nominativi Condominiali + +## Scopo + +Questa direttiva sancisce la regola fondamentale per cui **tutte le interfacce, viste e blade di NetGescon (inclusa la scheda unità `unita-immobiliare`, il riepilogo `condomini/nominativi` e le maschere contabili) devono tassativamente attingere dagli stessi archivi relazionali canonici**, evitando divergenze tra schermate o sovrascritture arbitrarie da viste legacy temporanee. + +--- + +## 1. Sorgente Dati Canonica Obbligatoria + +1. **Unità Immobiliari**: `unita_immobiliari` con identificativo univoco, scala, piano, interno e `stabile_id`. +2. **Soggetti e Persone**: Tabella canonica `persone` (denominazione / ragione sociale o cognome + nome, codice fiscale, contatti telefonici ed email). +3. **Relazioni Titolarità Temporale**: Tabella `persone_unita_relazioni` con: + - `tipo_relazione`: `'proprietario'`, `'comproprietario'`, `'nudo_proprietario'`, `'usufruttuario'`, `'condomino'` per i possessori; `'inquilino'`, `'conduttore'`, `'locatario'` per gli inquilini. + - `quota_relazione` / `perc_diritto_reale`: percentuale di possesso o diritto reale (es. 50% / 50%). + - `data_inizio` e `data_fine`: validità temporale legata all'anno di gestione attivo. + - `attivo`: stato della relazione. + +--- + +## 2. Regole di Visualizzazione e Normalizzazione + +- **Gestione Anno Attivo (`AnnoGestioneContext`)**: I nominativi di proprietari e inquilini visualizzati devono sempre rispecchiare l'esercizio/anno di gestione attivo selezionato nel contesto di sessione. +- **Risoluzione Denominazione Persona/Azienda**: + ```sql + CASE + WHEN COALESCE(p.ragione_sociale, '') <> '' THEN p.ragione_sociale + ELSE TRIM(CONCAT(COALESCE(p.cognome, ''), ' ', COALESCE(p.nome, ''))) + END + ``` + È vietata la concatenazione ridondante di cognome, nome e ragione sociale che produce duplicazioni testuali. +- **Distinzione Netta Condòmini vs Inquilini**: In tutte le viste le titolarità e i recapiti di proprietari e inquilini devono rimanere nettamente separati. + +--- + +## 3. Conformità Multi-Blade + +- **`UnitaImmobiliarePage` (`/admin-filament/unita-immobiliare`)** e **`NominativiStabile` (`/admin-filament/condomini/nominativi`)** utilizzano la medesima logica di query atomica relazionale, garantendo che lo stesso condòmino/inquilino appaia in modo identico in entrambe le pagine. diff --git a/tests/Feature/NominativiAndFeMatchingTest.php b/tests/Feature/NominativiAndFeMatchingTest.php new file mode 100644 index 0000000..dbd6ae7 --- /dev/null +++ b/tests/Feature/NominativiAndFeMatchingTest.php @@ -0,0 +1,162 @@ +create(); + Role::firstOrCreate(['name' => 'amministratore', 'guard_name' => 'web']); + $user->assignRole('amministratore'); + + DB::table('amministratori')->insertOrIgnore([ + 'id' => 1, + 'user_id' => $user->id, + 'nome' => 'Mario', + 'cognome' => 'Rossi', + 'codice_amministratore' => 'ADM0001', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $stabile = Stabile::create([ + 'codice_stabile' => '0099', + 'denominazione' => 'Condominio Test Unitario', + 'indirizzo' => 'Via Test 1', + 'citta' => 'Roma', + 'cap' => '00100', + 'provincia' => 'RM', + 'codice_fiscale' => '99999990580', + 'amministratore_id' => 1, + ]); + + $unita = UnitaImmobiliare::create([ + 'stabile_id' => $stabile->id, + 'interno' => '1', + 'scala' => 'A', + ]); + + $proprietario = Persona::create([ + 'cognome' => 'Rossi', + 'nome' => 'Mario', + 'codice_fiscale' => 'RSSMRA80A01H501U', + ]); + + $inquilino = Persona::create([ + 'cognome' => 'Bianchi', + 'nome' => 'Luigi', + 'codice_fiscale' => 'BNCLGU85B02H501Z', + ]); + + PersonaUnitaRelazione::create([ + 'unita_id' => $unita->id, + 'persona_id' => $proprietario->id, + 'tipo_relazione' => 'proprietario', + 'quota_relazione' => 100, + 'data_inizio' => '2020-01-01', + 'attivo' => 1, + ]); + + PersonaUnitaRelazione::create([ + 'unita_id' => $unita->id, + 'persona_id' => $inquilino->id, + 'tipo_relazione' => 'inquilino', + 'quota_relazione' => 100, + 'data_inizio' => '2020-01-01', + 'attivo' => 1, + ]); + + $response = $this->actingAs($user)->get("/admin-filament/condomini/nominativi?stabile_id={$stabile->id}"); + expect($response->status())->toBeIn([200, 302]); +}); + +it('fe scanner correctly matches invoices strictly by codice fiscale without arbitrary fallback', function () { + $user = User::factory()->create(); + Role::firstOrCreate(['name' => 'amministratore', 'guard_name' => 'web']); + $user->assignRole('amministratore'); + + DB::table('amministratori')->insertOrIgnore([ + 'id' => 1, + 'user_id' => $user->id, + 'nome' => 'Admin', + 'cognome' => 'Test', + 'codice_amministratore' => 'ADM0001', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $stabile = Stabile::create([ + 'codice_stabile' => '0098', + 'denominazione' => 'Condominio Via Test 10', + 'indirizzo' => 'Via Test 10', + 'citta' => 'Roma', + 'cap' => '00100', + 'provincia' => 'RM', + 'codice_fiscale' => '88888880581', + 'amministratore_id' => 1, + ]); + + $xmlContentMatching = << + + + + + IT01234567890 + FORNITORE TEST SRL + + + + + 88888880581 + CONDOMINIO VIA TEST 10 + + + + + + + TD01 + EUR + 2026-03-01 + FT-TEST-001 + 122.00 + + + + + 1 + Manutenzione Ordinaria Impianto + 100.00 + 100.00 + 22.00 + + + 22.00 + 100.00 + 22.00 + + + + +XML; + + $importer = app(FatturaElettronicaImporter::class); + $result = $importer->importXml($xmlContentMatching, (int) $stabile->id, 'FT-TEST-001.xml'); + + expect($result['status'])->toBe('imported'); + + $fe = FatturaElettronica::where('numero_fattura', 'FT-TEST-001')->first(); + expect($fe)->not->toBeNull(); + expect((int) $fe->stabile_id)->toBe((int) $stabile->id); + expect($fe->destinatario_cf)->toBe('88888880581'); +});