759 lines
31 KiB
PHP
759 lines
31 KiB
PHP
<?php
|
|
|
|
namespace App\Services\FattureElettroniche;
|
|
|
|
use App\Models\Documento;
|
|
use App\Models\FatturaElettronica;
|
|
use App\Models\Stabile;
|
|
use App\Support\ArchivioPaths;
|
|
use Dompdf\Dompdf;
|
|
use Dompdf\Options;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
|
|
class FatturaElettronicaProtocolloService
|
|
{
|
|
public function ensureDocumentoProtocollato(FatturaElettronica $fattura, ?int $userId = null): Documento
|
|
{
|
|
return DB::transaction(function () use ($fattura, $userId) {
|
|
$protocolCol = $this->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);
|
|
$base = (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, $base);
|
|
|
|
// 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 = $base . '/' . $targetName;
|
|
$this->ensureDir($base);
|
|
|
|
// 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 = $base . '/' . $targetName;
|
|
$this->ensureDir($base);
|
|
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 = $base . '/' . $targetName;
|
|
$this->ensureDir($base);
|
|
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 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('/<head(\b[^>]*)>/', '<head$1><meta charset="utf-8" />', $html, 1) ?: $html;
|
|
}
|
|
|
|
return $html;
|
|
}
|
|
|
|
private function injectProtocolloIntoHtml(string $html, string $protocollo): string
|
|
{
|
|
$safe = htmlspecialchars($protocollo, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
$badge = '<div style="font-size:12px;margin:10px 0 0 0;color:#333;"><strong>Protocollo:</strong> ' . $safe . '</div>';
|
|
|
|
// Inserisce subito dopo l'apertura del body, se possibile.
|
|
$out = preg_replace('/<body(\b[^>]*)>/', '<body$1>' . $badge, $html, 1);
|
|
return is_string($out) && $out !== '' ? $out : ($badge . $html);
|
|
}
|
|
|
|
private function renderHtmlToPdfWithDompdf(string $html): ?string
|
|
{
|
|
if (! class_exists(Dompdf::class)) {
|
|
return null;
|
|
}
|
|
|
|
// Forza A4 anche via CSS (oltre a setPaper).
|
|
// Alcuni fogli XSL includono @page size: Legal; in tal caso, rimuoviamo le regole @page e iniettiamo A4.
|
|
try {
|
|
if (str_contains($html, '@page')) {
|
|
$html = preg_replace('/@page\s*\{[^}]*\}/s', '', $html) ?: $html;
|
|
}
|
|
} catch (\Throwable) {
|
|
// ignore
|
|
}
|
|
|
|
$a4Css = '<style>@page { size: A4 portrait !important; margin: 12mm !important; }</style>';
|
|
|
|
// Alcuni HTML prodotti da XSL hanno layout fisso (width in px) che può eccedere l'area stampabile,
|
|
// causando "taglio" a destra. Forziamo un contenimento soft.
|
|
$fitCss = '<style>'
|
|
. 'html,body{max-width:100% !important;}'
|
|
. '*{box-sizing:border-box;}'
|
|
. 'table{max-width:100% !important; width:100% !important; table-layout:fixed !important;}'
|
|
. 'th,td{word-wrap:break-word; overflow-wrap:break-word;}'
|
|
. 'img{max-width:100% !important; height:auto !important;}'
|
|
. '</style>';
|
|
|
|
$html = preg_replace('/<head(\b[^>]*)>/', '<head$1>' . $a4Css . $fitCss, $html, 1) ?: ($a4Css . $fitCss . $html);
|
|
|
|
$options = new Options();
|
|
$options->setIsRemoteEnabled(false);
|
|
$options->setIsHtml5ParserEnabled(true);
|
|
$options->setDefaultFont('DejaVu Sans');
|
|
// IMPORTANT: molti XSL producono layout a larghezza fissa in px.
|
|
// Aumentando la DPI, i px diventano più piccoli in punti => meno rischio di taglio a destra.
|
|
$options->setDpi(120);
|
|
|
|
$dompdf = new Dompdf($options);
|
|
$dompdf->setPaper('A4', 'portrait');
|
|
$dompdf->loadHtml($html, 'UTF-8');
|
|
$dompdf->render();
|
|
|
|
$out = $dompdf->output();
|
|
return is_string($out) && $out !== '' ? $out : null;
|
|
}
|
|
|
|
private function buildSimplePdf(array $lines): string
|
|
{
|
|
$escape = function (string $text): string {
|
|
$text = str_replace('\\', '\\\\', $text);
|
|
$text = str_replace('(', '\\(', $text);
|
|
$text = str_replace(')', '\\)', $text);
|
|
$text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $text);
|
|
return (string) $text;
|
|
};
|
|
|
|
$content = "BT\n/F1 12 Tf\n14 TL\n50 800 Td\n";
|
|
foreach ($lines as $i => $line) {
|
|
$line = $escape((string) $line);
|
|
if ($i === 0) {
|
|
$content .= '(' . $line . ") Tj\n";
|
|
} else {
|
|
$content .= "T*\n(" . $line . ") Tj\n";
|
|
}
|
|
}
|
|
$content .= "\nET";
|
|
|
|
$objects = [];
|
|
$objects[] = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n";
|
|
$objects[] = "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n";
|
|
$objects[] = "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>\nendobj\n";
|
|
$objects[] = "4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n";
|
|
$objects[] = "5 0 obj\n<< /Length " . strlen($content) . " >>\nstream\n" . $content . "\nendstream\nendobj\n";
|
|
|
|
$pdf = "%PDF-1.4\n";
|
|
$offsets = [0];
|
|
foreach ($objects as $obj) {
|
|
$offsets[] = strlen($pdf);
|
|
$pdf .= $obj;
|
|
}
|
|
|
|
$xrefPos = strlen($pdf);
|
|
$pdf .= "xref\n0 " . (count($objects) + 1) . "\n";
|
|
$pdf .= "0000000000 65535 f \n";
|
|
for ($i = 1; $i <= count($objects); $i++) {
|
|
$pdf .= str_pad((string) $offsets[$i], 10, '0', STR_PAD_LEFT) . " 00000 n \n";
|
|
}
|
|
$pdf .= "trailer\n<< /Size " . (count($objects) + 1) . " /Root 1 0 R >>\n";
|
|
$pdf .= "startxref\n" . $xrefPos . "\n%%EOF";
|
|
|
|
return $pdf;
|
|
}
|
|
|
|
private function syncDocumentoAndPaths(FatturaElettronica $fattura, Documento $doc): void
|
|
{
|
|
// Se il Documento punta a un file valido ma la FE no, aggiorniamo la FE.
|
|
$docPath = (string) ($doc->percorso_file ?: ($doc->path_file ?: ''));
|
|
$docName = (string) ($doc->nome_file ?: '');
|
|
|
|
$needsSaveFattura = false;
|
|
if ($docPath !== '' && Storage::disk('local')->exists($docPath)) {
|
|
if (! is_string($fattura->allegato_pdf_path) || $fattura->allegato_pdf_path === '' || ! Storage::disk('local')->exists((string) $fattura->allegato_pdf_path)) {
|
|
if (str_ends_with(strtolower($docPath), '.pdf')) {
|
|
$fattura->allegato_pdf_path = $docPath;
|
|
if ($docName !== '') {
|
|
$fattura->allegato_pdf_nome = $docName;
|
|
}
|
|
$needsSaveFattura = true;
|
|
}
|
|
}
|
|
|
|
if (! is_string($fattura->xml_path) || $fattura->xml_path === '' || ! Storage::disk('local')->exists((string) $fattura->xml_path)) {
|
|
if (str_ends_with(strtolower($docPath), '.xml')) {
|
|
$fattura->xml_path = $docPath;
|
|
if ($docName !== '') {
|
|
$fattura->nome_file_xml = $docName;
|
|
}
|
|
$needsSaveFattura = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($needsSaveFattura) {
|
|
$fattura->save();
|
|
}
|
|
|
|
// Se la FE ha un file valido ma il Documento no, aggiorniamo Documento.
|
|
$fatturaPath = null;
|
|
$fatturaName = null;
|
|
|
|
if (is_string($fattura->allegato_pdf_path) && $fattura->allegato_pdf_path !== '' && Storage::disk('local')->exists($fattura->allegato_pdf_path)) {
|
|
$fatturaPath = $fattura->allegato_pdf_path;
|
|
$fatturaName = $fattura->allegato_pdf_nome ?: null;
|
|
} elseif (is_string($fattura->xml_path) && $fattura->xml_path !== '' && Storage::disk('local')->exists($fattura->xml_path)) {
|
|
$fatturaPath = $fattura->xml_path;
|
|
$fatturaName = $fattura->nome_file_xml ?: null;
|
|
}
|
|
|
|
if ($fatturaPath && (! is_string($docPath) || $docPath === '' || ! Storage::disk('local')->exists($docPath))) {
|
|
$doc->forceFill([
|
|
'nome_file' => $fatturaName,
|
|
'path_file' => $fatturaPath,
|
|
'percorso_file' => $fatturaPath,
|
|
])->save();
|
|
}
|
|
}
|
|
|
|
private function recoverMissingPathsFromBase(FatturaElettronica $fattura, string $base): void
|
|
{
|
|
$needsSave = false;
|
|
|
|
$pdfOk = is_string($fattura->allegato_pdf_path) && $fattura->allegato_pdf_path !== '' && Storage::disk('local')->exists($fattura->allegato_pdf_path);
|
|
$xmlOk = is_string($fattura->xml_path) && $fattura->xml_path !== '' && Storage::disk('local')->exists($fattura->xml_path);
|
|
|
|
if ($pdfOk && $xmlOk) {
|
|
return;
|
|
}
|
|
|
|
if (! Storage::disk('local')->exists($base)) {
|
|
return;
|
|
}
|
|
|
|
$files = Storage::disk('local')->files($base);
|
|
|
|
$needleNumero = trim((string) $this->sanitizeFilename((string) ($fattura->numero_fattura ?? '')));
|
|
$needleFornitore = trim((string) $this->sanitizeFilename((string) ($fattura->fornitore_denominazione ?? '')));
|
|
$needlePiva = trim((string) ($fattura->fornitore_piva ?? ''));
|
|
$needleCf = trim((string) ($fattura->fornitore_cf ?? ''));
|
|
|
|
// Evita agganci sbagliati: se il numero è vuoto o troppo corto (es. "1", "2"), non proviamo recovery.
|
|
// In quei casi è più sicuro rigenerare da XML.
|
|
if ($needleNumero === '' || mb_strlen($needleNumero) < 4) {
|
|
return;
|
|
}
|
|
|
|
// Se manca anche il fornitore, il rischio di collisione è alto.
|
|
if ($needleFornitore === '' || mb_strlen($needleFornitore) < 4) {
|
|
return;
|
|
}
|
|
|
|
$pdfCandidates = [];
|
|
$xmlCandidates = [];
|
|
|
|
foreach ($files as $file) {
|
|
$name = basename($file);
|
|
|
|
$nameUpper = mb_strtoupper($name);
|
|
$numUpper = mb_strtoupper($needleNumero);
|
|
$fornUpper = mb_strtoupper($needleFornitore);
|
|
$matchNumero = str_contains($nameUpper, $numUpper);
|
|
$matchFornitore = str_contains($nameUpper, $fornUpper);
|
|
$matchPiva = ($needlePiva !== '' && str_contains($nameUpper, $needlePiva));
|
|
$matchCf = ($needleCf !== '' && str_contains($nameUpper, $needleCf));
|
|
$strongMatch = $matchNumero && ($matchFornitore || $matchPiva || $matchCf);
|
|
|
|
if (! $pdfOk && str_ends_with(strtolower($name), '.pdf')) {
|
|
if ($strongMatch) {
|
|
$pdfCandidates[] = ['path' => (string) $file, 'name' => (string) $name];
|
|
}
|
|
}
|
|
|
|
if (! $xmlOk && str_ends_with(strtolower($name), '.xml')) {
|
|
if ($strongMatch) {
|
|
$xmlCandidates[] = ['path' => (string) $file, 'name' => (string) $name];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (! $pdfOk && count($pdfCandidates) === 1) {
|
|
$fattura->allegato_pdf_path = $pdfCandidates[0]['path'];
|
|
$fattura->allegato_pdf_nome = $pdfCandidates[0]['name'];
|
|
$pdfOk = true;
|
|
$needsSave = true;
|
|
}
|
|
|
|
if (! $xmlOk && count($xmlCandidates) === 1) {
|
|
$fattura->xml_path = $xmlCandidates[0]['path'];
|
|
$fattura->nome_file_xml = $xmlCandidates[0]['name'];
|
|
$xmlOk = true;
|
|
$needsSave = true;
|
|
}
|
|
|
|
if ($needsSave) {
|
|
$fattura->save();
|
|
}
|
|
}
|
|
|
|
private function detectProtocolloFromExistingFiles(FatturaElettronica $fattura): ?string
|
|
{
|
|
$candidates = [];
|
|
if (is_string($fattura->allegato_pdf_nome) && $fattura->allegato_pdf_nome !== '') {
|
|
$candidates[] = $fattura->allegato_pdf_nome;
|
|
}
|
|
if (is_string($fattura->nome_file_xml) && $fattura->nome_file_xml !== '') {
|
|
$candidates[] = $fattura->nome_file_xml;
|
|
}
|
|
|
|
foreach ($candidates as $name) {
|
|
if (preg_match('/^(FT-\d{4})\b/', $name, $m)) {
|
|
return (string) $m[1];
|
|
}
|
|
}
|
|
|
|
// fallback: prova dal path
|
|
foreach ([(string) ($fattura->allegato_pdf_path ?? ''), (string) ($fattura->xml_path ?? '')] as $path) {
|
|
if ($path !== '' && preg_match('/\/(FT-\d{4})\b/', $path, $m)) {
|
|
return (string) $m[1];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function nextNumeroProtocollo(int $stabileId, int $anno): int
|
|
{
|
|
$protocolCol = $this->getDocumentoProtocolColumn();
|
|
if (! $protocolCol) {
|
|
return 1;
|
|
}
|
|
|
|
// Nota: la colonna protocollo è unique a livello tabella.
|
|
// Quindi il protocollo FT-* non può "resettare" per anno o per stabile senza includere l'anno nel prefisso.
|
|
// Per evitare collisioni, calcoliamo il prossimo numero globalmente.
|
|
$ultimo = Documento::query()
|
|
->whereNotNull($protocolCol)
|
|
->where($protocolCol, 'like', 'FT-%')
|
|
->lockForUpdate()
|
|
->orderByDesc($protocolCol)
|
|
->first();
|
|
|
|
$value = null;
|
|
if ($ultimo) {
|
|
try {
|
|
$value = is_string($ultimo->{$protocolCol} ?? null) ? (string) $ultimo->{$protocolCol} : null;
|
|
} catch (\Throwable) {
|
|
$value = null;
|
|
}
|
|
}
|
|
|
|
if (! is_string($value) || $value === '') {
|
|
return 1;
|
|
}
|
|
|
|
if (preg_match('/^FT-(\d{1,})$/', $value, $m)) {
|
|
return ((int) $m[1]) + 1;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
private function basePath(string $adminFolder, string $stabileFolder, int $anno): string
|
|
{
|
|
return 'amministratori/' . $adminFolder . '/stabili/' . $stabileFolder . '/documenti/' . $anno . '/fatture-ricevute';
|
|
}
|
|
|
|
private function findFirstFileStartingWith(string $dir, string $prefix): ?string
|
|
{
|
|
try {
|
|
if (! Storage::disk('local')->exists($dir)) {
|
|
return null;
|
|
}
|
|
$files = Storage::disk('local')->files($dir);
|
|
foreach ($files as $file) {
|
|
if (str_starts_with(basename((string) $file), $prefix)) {
|
|
return (string) $file;
|
|
}
|
|
}
|
|
} catch (\Throwable) {
|
|
return null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private function sanitizeFilename(string $value): string
|
|
{
|
|
$value = trim($value);
|
|
$value = preg_replace('/\s+/', ' ', $value);
|
|
$value = str_replace(['/', '\\', ':', '*', '?', '"', '<', '>', '|'], '-', (string) $value);
|
|
$value = preg_replace('/[^\pL\pN \-_.]/u', '', (string) $value);
|
|
$value = trim((string) $value);
|
|
if ($value === '') {
|
|
$value = (string) Str::uuid();
|
|
}
|
|
return $value;
|
|
}
|
|
|
|
private function ensureDir(string $dir): void
|
|
{
|
|
if (! Storage::disk('local')->exists($dir)) {
|
|
Storage::disk('local')->makeDirectory($dir);
|
|
}
|
|
}
|
|
}
|