getDocumentoProtocolColumn(); $existing = Documento::query() ->where('documentable_type', FatturaElettronica::class) ->where('documentable_id', $fattura->id) ->first(); $stabile = Stabile::query() ->with(['amministratore:id,codice_amministratore,codice_univoco']) ->select(['id', 'amministratore_id', 'codice_stabile', 'codice_univoco']) ->findOrFail((int) $fattura->stabile_id); $dataDoc = $fattura->data_fattura; $anno = $dataDoc ? (int) $dataDoc->format('Y') : (int) now()->format('Y'); $ym = $dataDoc ? $dataDoc->format('Y/m') : now()->format('Y/m'); $adminCode = $stabile->amministratore?->codice_amministratore; $stabileCode = $stabile->codice_stabile; $adminFolder = $adminCode ?: ('ID-' . (int) $stabile->amministratore_id); $stabileFolder = $stabileCode ?: ('ID-' . (int) $fattura->stabile_id); $stabileBase = ArchivioPaths::stabileBase($stabile, $stabile->amministratore); $gestione = $this->resolveGestioneForYear($stabile, $anno); $pdfBase = ArchivioPaths::fatturePdfGestionePath($stabile, $gestione, $anno, 'ordinaria', $stabile->amministratore) ?: $this->basePath($adminFolder, $stabileFolder, $anno); $xmlBase = ArchivioPaths::fattureXmlYearPath($stabile, $anno, $stabile->amministratore) ?: $this->basePath($adminFolder, $stabileFolder, $anno); $legacyBase = (is_string($stabileBase) && $stabileBase !== '') ? ($stabileBase . '/documenti/' . $anno . '/fatture-ricevute') : $this->basePath($adminFolder, $stabileFolder, $anno); // Se il Documento punta a un path valido (o viceversa), sincronizziamo prima. if ($existing) { $this->syncDocumentoAndPaths($fattura, $existing); } // Recupero soft dal base path (file già spostati manualmente). $this->recoverMissingPathsFromBase($fattura, $legacyBase); $this->recoverMissingPathsFromBase($fattura, $pdfBase); $this->recoverMissingPathsFromBase($fattura, $xmlBase); // Ripara PDF se salvato in vecchio schema con ID numerici. $newBase = (is_string($stabileBase) && $stabileBase !== '') ? ($stabileBase . '/fatture-ricevute/' . $ym . '/allegati') : null; $possibleAttachmentDirs = [ $newBase, 'amministratori/' . $adminFolder . '/stabili/' . $stabileFolder . '/fatture-ricevute/' . $ym . '/allegati', 'amministratori/' . (int) $stabile->amministratore_id . '/stabili/' . (int) $fattura->stabile_id . '/fatture-ricevute/' . $ym . '/allegati', ]; $possibleAttachmentDirs = array_values(array_filter($possibleAttachmentDirs, fn($v) => is_string($v) && $v !== '')); $pdfPath = $fattura->allegato_pdf_path; if ((! is_string($pdfPath) || $pdfPath === '' || ! Storage::disk('local')->exists($pdfPath)) && is_string($fattura->allegato_pdf_hash) && $fattura->allegato_pdf_hash !== '' ) { foreach ($possibleAttachmentDirs as $dir) { $found = $this->findFirstFileStartingWith($dir, $fattura->allegato_pdf_hash . '-'); if ($found) { $fattura->allegato_pdf_path = $found; $pdfPath = $found; break; } } } // Guardrail: se il PDF è presente ma il nome file sembra chiaramente di un'altra fattura/fornitore, // evitiamo di mostrarlo/ri-nominarlo e rigeneriamo da XML (se disponibile). // Applichiamo la regola solo a file già "protocollati" (FT-xxxx...) per non rompere naming legacy. try { $pdfPathNow = is_string($fattura->allegato_pdf_path) ? trim($fattura->allegato_pdf_path) : ''; $pdfNameNow = is_string($fattura->allegato_pdf_nome) && trim($fattura->allegato_pdf_nome) !== '' ? trim($fattura->allegato_pdf_nome) : ($pdfPathNow !== '' ? basename($pdfPathNow) : ''); $isProtocollato = ($pdfNameNow !== '' && preg_match('/^FT-\d{4}\b/i', $pdfNameNow) === 1); $supplierToken = $this->sanitizeFilename((string) ($fattura->fornitore_denominazione ?? '')); $supplierToken = trim((string) $supplierToken); if ($isProtocollato && $supplierToken !== '' && mb_strlen($supplierToken) >= 4) { $nameUpper = mb_strtoupper($pdfNameNow); $tokenUpper = mb_strtoupper($supplierToken); if (! str_contains($nameUpper, $tokenUpper)) { $fattura->allegato_pdf_path = null; $fattura->allegato_pdf_nome = null; } } } catch (\Throwable) { // ignore } $protocollo = null; if ($existing) { $existingProtocol = null; try { $existingProtocol = is_string($existing->numero_protocollo ?? null) ? $existing->numero_protocollo : null; } catch (\Throwable) { $existingProtocol = null; } if (! $existingProtocol) { try { $existingProtocol = is_string($existing->protocollo ?? null) ? $existing->protocollo : null; } catch (\Throwable) { $existingProtocol = null; } } if (is_string($existingProtocol) && $existingProtocol !== '') { $protocollo = $existingProtocol; } } if (! $protocollo) { $protocollo = $this->detectProtocolloFromExistingFiles($fattura); } if (! $protocollo) { if ($protocolCol) { $next = $this->nextNumeroProtocollo((int) $fattura->stabile_id, $anno); $protocollo = 'FT-' . str_pad((string) $next, 4, '0', STR_PAD_LEFT); } else { // Se la tabella documenti non ha una colonna protocollo, assicuriamo comunque un nome unico per i file. $protocollo = 'FT-' . str_pad((string) ((int) $fattura->id), 6, '0', STR_PAD_LEFT); } } // Se il protocollo è già usato da un altro Documento (caso tipico: nome_file_xml "sporco" / recovery errato), // rigeneriamo un nuovo protocollo per evitare collisioni con l'unique. if (! $existing) { $tries = 0; while ($tries < 5) { $alreadyUsed = false; if ($protocolCol) { $alreadyUsed = Documento::query() ->where($protocolCol, $protocollo) ->exists(); } if (! $alreadyUsed) { break; } if ($protocolCol) { $next = $this->nextNumeroProtocollo((int) $fattura->stabile_id, $anno); $protocollo = 'FT-' . str_pad((string) $next, 4, '0', STR_PAD_LEFT); } else { $protocollo = 'FT-' . str_pad((string) ((int) $fattura->id), 6, '0', STR_PAD_LEFT); } $tries++; } } $sanFornitore = $this->sanitizeFilename((string) ($fattura->fornitore_denominazione ?? 'FORNITORE')); $sanNumero = $this->sanitizeFilename((string) ($fattura->numero_fattura ?? ('ID-' . $fattura->id))); $dateForName = $dataDoc ? $dataDoc->format('Ymd') : now()->format('Ymd'); // Se manca il PDF (ma abbiamo almeno XML/metadata), generiamo un prospetto PDF archiviabile. $pdfPathNow = is_string($fattura->allegato_pdf_path) ? $fattura->allegato_pdf_path : ''; $hasPdfNow = is_string($pdfPathNow) && $pdfPathNow !== '' && Storage::disk('local')->exists($pdfPathNow); $hasXmlNow = (is_string($fattura->xml_content) && trim($fattura->xml_content) !== '') || (is_string($fattura->xml_path) && $fattura->xml_path !== '' && Storage::disk('local')->exists($fattura->xml_path)); if (! $hasPdfNow && $hasXmlNow) { $targetName = $protocollo . ' ' . $dateForName . ' ' . $sanFornitore . ' ' . $sanNumero . '.pdf'; $targetPath = $pdfBase . '/' . $targetName; $this->ensureDir($pdfBase); // Evita overwrite: se esiste già, lo riusiamo. if (! Storage::disk('local')->exists($targetPath)) { // 1) Prova a generare un PDF "umano" usando XSL Assosoftware + dompdf. // 2) Se non disponibile (ext/librerie/errore), fallback al PDF minimale. $pdf = $this->tryGenerateHumanPdfFromXml($fattura, $protocollo) ?: $this->generateFallbackPdfForFattura($fattura, $protocollo); Storage::disk('local')->put($targetPath, $pdf); } $fattura->allegato_pdf_path = $targetPath; $fattura->allegato_pdf_nome = $targetName; } if (is_string($fattura->allegato_pdf_path) && $fattura->allegato_pdf_path !== '' && Storage::disk('local')->exists($fattura->allegato_pdf_path)) { $targetName = $protocollo . ' ' . $dateForName . ' ' . $sanFornitore . ' ' . $sanNumero . '.pdf'; $targetPath = $pdfBase . '/' . $targetName; $this->ensureDir($pdfBase); if ($fattura->allegato_pdf_path !== $targetPath) { Storage::disk('local')->move($fattura->allegato_pdf_path, $targetPath); $fattura->allegato_pdf_path = $targetPath; } $fattura->allegato_pdf_nome = $targetName; } if (is_string($fattura->xml_path) && $fattura->xml_path !== '' && Storage::disk('local')->exists($fattura->xml_path)) { $targetName = $protocollo . ' ' . $dateForName . ' ' . $sanFornitore . ' ' . $sanNumero . '.xml'; $targetPath = $xmlBase . '/' . $targetName; $this->ensureDir($xmlBase); if ($fattura->xml_path !== $targetPath) { Storage::disk('local')->move($fattura->xml_path, $targetPath); $fattura->xml_path = $targetPath; } $fattura->nome_file_xml = $targetName; } $fattura->save(); $docData = [ 'utente_id' => $userId, 'nome' => $protocollo . ' - ' . $sanFornitore, 'tipologia' => 'fattura', 'tipo_documento' => 'fattura', 'fornitore' => $fattura->fornitore_denominazione, 'data_documento' => $fattura->data_fattura, 'data_scadenza' => $fattura->data_scadenza, 'importo_collegato' => $fattura->totale, 'numero_protocollo' => $protocollo, 'protocollo' => $protocollo, 'nome_file' => $fattura->allegato_pdf_nome ?: ($fattura->nome_file_xml ?: null), 'path_file' => $fattura->allegato_pdf_path ?: ($fattura->xml_path ?: null), 'percorso_file' => $fattura->allegato_pdf_path ?: ($fattura->xml_path ?: null), ]; // Compatibilità: alcune installazioni non hanno (più) certi campi su documenti. // Persistiamo solo colonne esistenti per evitare errori SQL. foreach (array_keys($docData) as $col) { try { if (! Schema::hasColumn('documenti', $col)) { unset($docData[$col]); } } catch (\Throwable) { // Se Schema non è disponibile, non rischiamo: rimuoviamo campi "non core". if (in_array($col, ['mime_type', 'hash_file'], true)) { unset($docData[$col]); } } } if ($existing) { $existing->fill(array_filter($docData, fn($v) => $v !== null && $v !== '')); $existing->save(); return $existing; } $createData = array_merge($docData, [ 'documentable_type' => FatturaElettronica::class, 'documentable_id' => $fattura->id, 'stabile_id' => (int) $fattura->stabile_id, 'data_upload' => now(), 'approvato' => false, 'archiviato' => false, 'is_demo' => false, ]); foreach (array_keys($createData) as $col) { try { if (! Schema::hasColumn('documenti', $col)) { unset($createData[$col]); } } catch (\Throwable) { // ignore } } return Documento::query()->create($createData); }); } private function resolveGestioneForYear(Stabile $stabile, int $year): ?GestioneContabile { if (! Schema::hasTable('gestioni_contabili')) { return null; } $query = GestioneContabile::query() ->where('stabile_id', (int) $stabile->id) ->where('anno_gestione', $year); $ordinaria = (clone $query) ->where('tipo_gestione', 'ordinaria') ->orderByDesc('gestione_attiva') ->orderByRaw("CASE WHEN stato = 'aperta' THEN 0 ELSE 1 END") ->orderByDesc('id') ->first(); if ($ordinaria) { return $ordinaria; } return $query ->orderByRaw("CASE WHEN tipo_gestione = 'straordinaria' THEN 0 ELSE 1 END") ->orderByDesc('gestione_attiva') ->orderByRaw("CASE WHEN stato = 'aperta' THEN 0 ELSE 1 END") ->orderByDesc('id') ->first(); } private function getDocumentoProtocolColumn(): ?string { static $cached = null; static $resolved = false; if ($resolved) { return $cached; } $resolved = true; try { if (Schema::hasColumn('documenti', 'numero_protocollo')) { $cached = 'numero_protocollo'; return $cached; } if (Schema::hasColumn('documenti', 'protocollo')) { $cached = 'protocollo'; return $cached; } } catch (\Throwable) { // ignore } $cached = null; return null; } private function generateFallbackPdfForFattura(FatturaElettronica $fattura, string $protocollo): string { $lines = []; $lines[] = 'NETGESCON - Prospetto fattura (generato automaticamente)'; $lines[] = 'Protocollo: ' . $protocollo; $lines[] = ''; $fornitore = trim((string) ($fattura->fornitore_denominazione ?? '')); $num = trim((string) ($fattura->numero_fattura ?? '')); $data = $fattura->data_fattura ? $fattura->data_fattura->format('d/m/Y') : ''; $tot = is_numeric($fattura->totale ?? null) ? number_format((float) $fattura->totale, 2, ',', '.') : ''; $piva = trim((string) ($fattura->fornitore_piva ?? '')); $cf = trim((string) ($fattura->fornitore_cf ?? '')); if ($fornitore !== '') { $lines[] = 'Fornitore: ' . $fornitore; } if ($piva !== '' || $cf !== '') { $lines[] = 'P.IVA/CF: ' . ($piva !== '' ? $piva : '—') . ' / ' . ($cf !== '' ? $cf : '—'); } if ($num !== '' || $data !== '') { $lines[] = 'Fattura: ' . ($num !== '' ? $num : '—') . ' - ' . ($data !== '' ? $data : '—'); } if ($tot !== '') { $lines[] = 'Totale: EUR ' . $tot; } $lines[] = ''; $lines[] = 'Nota: il PDF originale non era disponibile; questo documento serve solo per archiviazione e protocollazione.'; return $this->buildSimplePdf($lines); } private function tryGenerateHumanPdfFromXml(FatturaElettronica $fattura, string $protocollo): ?string { try { $xml = $this->getXmlContentForFattura($fattura); if (! is_string($xml) || trim($xml) === '') { return null; } $html = $this->transformFatturaPaXmlToHtmlWithAssosoftwareXsl($xml); if (! is_string($html) || trim($html) === '') { return null; } // Inietta una riga protocollo (utile anche in PDF/archivio). $html = $this->injectProtocolloIntoHtml($html, $protocollo); return $this->renderHtmlToPdfWithDompdf($html); } catch (\Throwable) { return null; } } private function getXmlContentForFattura(FatturaElettronica $fattura): ?string { $xml = is_string($fattura->xml_content) ? $fattura->xml_content : null; if (is_string($xml) && trim($xml) !== '') { return $xml; } $path = is_string($fattura->xml_path) ? $fattura->xml_path : ''; if ($path !== '' && Storage::disk('local')->exists($path)) { try { $data = Storage::disk('local')->get($path); return is_string($data) ? $data : null; } catch (\Throwable) { return null; } } return null; } private function transformFatturaPaXmlToHtmlWithAssosoftwareXsl(string $xml): ?string { if (! class_exists(\XSLTProcessor::class) || ! class_exists(\DOMDocument::class)) { return null; } $xslPath = config('contabilita.fatture_elettroniche.assosoftware_xsl_path'); if (! is_string($xslPath) || trim($xslPath) === '' || ! is_file($xslPath)) { $fallback = base_path('docs/Importazione/FoglioStileAssoSoftware.xsl'); if (is_file($fallback)) { $xslPath = $fallback; } else { return null; } } $xmlDoc = new \DOMDocument(); $xslDoc = new \DOMDocument(); $xmlLoaded = @$xmlDoc->loadXML($xml, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_COMPACT); if (! $xmlLoaded) { return null; } $xslLoaded = @$xslDoc->load($xslPath, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_COMPACT); if (! $xslLoaded) { return null; } $proc = new \XSLTProcessor(); @$proc->importStylesheet($xslDoc); $html = @$proc->transformToXML($xmlDoc); if (! is_string($html) || trim($html) === '') { return null; } // Assicura charset per dompdf. if (! str_contains($html, 'charset=')) { $html = preg_replace('/
]*)>/', '', $html, 1) ?: $html; } return $html; } private function injectProtocolloIntoHtml(string $html, string $protocollo): string { $safe = htmlspecialchars($protocollo, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); $badge = '