*/ public array $xmlDetails = []; public ?int $contabilitaFatturaId = null; /** @var array */ public array $acquaPdfPreview = []; public function getFornitoreAgganciatoLabel(): ?string { $f = $this->fattura->fornitore; if (! $f) { return null; } $ragione = is_string($f->ragione_sociale ?? null) ? trim((string) $f->ragione_sociale) : ''; if ($ragione !== '') { return $ragione; } $nome = is_string($f->nome ?? null) ? trim((string) $f->nome) : ''; $cognome = is_string($f->cognome ?? null) ? trim((string) $f->cognome) : ''; $full = trim($nome . ' ' . $cognome); if ($full !== '') { return $full; } $code = is_string($f->codice_univoco ?? null) ? trim((string) $f->codice_univoco) : ''; return $code !== '' ? $code : ('Fornitore #' . (int) $f->id); } public function getPagamentoModalitaXsdLabel(): ?string { $code = is_string($this->fattura->pagamento_modalita) ? trim($this->fattura->pagamento_modalita) : ''; if ($code === '') { return null; } $latestId = FeXsdVersion::query()->orderByDesc('imported_at')->orderByDesc('id')->value('id'); $latestId = is_numeric($latestId) ? (int) $latestId : null; if (! $latestId) { return null; } $domainId = FeXsdCodeDomain::query() ->where('version_id', $latestId) ->where('type_name', 'ModalitaPagamentoType') ->value('id'); $domainId = is_numeric($domainId) ? (int) $domainId : null; if (! $domainId) { return null; } $doc = FeXsdCode::query() ->where('domain_id', $domainId) ->where('code', $code) ->value('documentation'); return is_string($doc) && trim($doc) !== '' ? trim($doc) : null; } public function mount(int | string $record): void { $user = Auth::user(); if (! $user instanceof User) { abort(403); } if (! $user->hasAnyRole(['super-admin', 'admin']) && ! $user->can('contabilita.fatture-elettroniche')) { abort(403); } $this->fattura = FatturaElettronica::query()->with(['stabile', 'fornitore'])->findOrFail((int) $record); $this->documento = Documento::query() ->where('documentable_type', FatturaElettronica::class) ->where('documentable_id', (int) $this->fattura->id) ->first(); $activeStabileId = StabileContext::resolveActiveStabileId($user); if ($activeStabileId && (int) $this->fattura->stabile_id !== (int) $activeStabileId) { // Non blocchiamo la view, ma evitiamo accesso accidentale cross-stabile via URL. abort(404); } // Parse XML (best-effort) per righe/ritenuta/riferimenti. $this->xmlDetails = []; if (is_string($this->fattura->xml_content) && $this->fattura->xml_content !== '') { try { $parsed = app(FatturaElettronicaXmlParser::class)->parse($this->fattura->xml_content); $this->xmlDetails = is_array($parsed) ? $parsed : []; } catch (\Throwable) { $this->xmlDetails = []; } } // Se esiste una fattura contabile collegata alla FE, esponiamo l'id per link rapido. if (Schema::hasTable('contabilita_fatture_fornitori')) { $this->contabilitaFatturaId = ContabilitaFatturaFornitore::query() ->where('fattura_elettronica_id', (int) $this->fattura->id) ->value('id'); $this->contabilitaFatturaId = $this->contabilitaFatturaId ? (int) $this->contabilitaFatturaId : null; } // Aggancio automatico fornitore (se manca) basato su P.IVA/CF. $this->tryAutoLinkFornitore(); // Best-effort: assicurati che esista sempre un Documento (e un PDF archiviabile). // Questo evita casi in cui l'utente vede "PDF non disponibile" e non ha nulla da archiviare. try { $userId = (int) (Auth::id() ?: 0); $pdfOk = (is_string($this->fattura->allegato_pdf_path) && trim($this->fattura->allegato_pdf_path) !== '') || (is_string($this->fattura->allegato_pdf_hash ?? null) && trim((string) $this->fattura->allegato_pdf_hash) !== ''); if ($userId > 0 && (! $this->documento || ! $pdfOk)) { app(FatturaElettronicaProtocolloService::class)->ensureDocumentoProtocollato($this->fattura, $userId); $this->fattura->refresh(); $this->documento = Documento::query() ->where('documentable_type', FatturaElettronica::class) ->where('documentable_id', (int) $this->fattura->id) ->first(); } } catch (\Throwable) { // ignore } } private function tryAutoLinkFornitore(): void { $stabile = $this->fattura->stabile; $amministratoreId = (int) ($stabile?->amministratore_id ?: 0); if ($amministratoreId <= 0) { return; } // Guard: se la FE è agganciata a un fornitore di un altro amministratore (es. legacy), ripariamo. $currentFornitoreId = (int) ($this->fattura->fornitore_id ?? 0); if ($currentFornitoreId > 0) { try { $current = Fornitore::query()->select(['id', 'amministratore_id'])->find($currentFornitoreId); if ($current && (int) $current->amministratore_id !== $amministratoreId) { $this->fattura->fornitore_id = null; $this->fattura->save(); $this->fattura->unsetRelation('fornitore'); } else { return; } } catch (\Throwable) { // Se non riusciamo a validare, non blocchiamo la view. } } $normalize = static function (?string $value): string { $value = strtoupper(trim((string) $value)); $value = str_replace(' ', '', $value); return $value; }; $piva = $normalize($this->fattura->fornitore_piva); $cf = $normalize($this->fattura->fornitore_cf); if ($piva === '' || $piva === 'ND') { $piva = ''; } if ($cf === '' || $cf === 'ND') { $cf = ''; } $query = Fornitore::query()->where('amministratore_id', $amministratoreId); $match = null; if ($piva !== '') { $match = (clone $query)->whereRaw("REPLACE(UPPER(partita_iva), ' ', '') = ?", [$piva])->first(); if (! $match && str_starts_with($piva, 'IT') && strlen($piva) > 2) { $match = (clone $query)->whereRaw("REPLACE(UPPER(partita_iva), ' ', '') = ?", [substr($piva, 2)])->first(); } } if (! $match && $cf !== '') { $match = (clone $query)->whereRaw("REPLACE(UPPER(codice_fiscale), ' ', '') = ?", [$cf])->first(); } if (! $match) { return; } $this->fattura->fornitore_id = (int) $match->id; $this->fattura->save(); // Refresh relazione per UI. $this->fattura->setRelation('fornitore', $match); } public function getBreadcrumbs(): array { return [ 'Contabilità' => null, 'Fatture ricevute (XML)' => FattureElettronicheArchivio::getUrl(panel: 'admin-filament'), 'Fattura ' . ($this->fattura->numero_fattura ?? '#') => null, ]; } public function getBackUrl(): string { $back = request()->query('back'); if (! is_string($back) || $back === '') { return FattureElettronicheArchivio::getUrl(panel: 'admin-filament'); } $back = trim($back); if (Str::startsWith($back, ['http://', 'https://', '//'])) { return FattureElettronicheArchivio::getUrl(panel: 'admin-filament'); } if (! Str::startsWith($back, '/')) { return FattureElettronicheArchivio::getUrl(panel: 'admin-filament'); } // Never allow browser navigation to Livewire internal endpoints. if (Str::startsWith($back, ['/livewire/', '/livewire'])) { return FattureElettronicheArchivio::getUrl(panel: 'admin-filament'); } return $back; } protected function getHeaderActions(): array { return [ Action::make('visualizza_pdf_modal') ->label('Vedi PDF') ->icon('heroicon-o-eye') ->visible(function (): bool { $pdfPath = is_string($this->fattura->allegato_pdf_path ?? null) ? trim((string) $this->fattura->allegato_pdf_path) : ''; $pdfHash = is_string($this->fattura->allegato_pdf_hash ?? null) ? trim((string) $this->fattura->allegato_pdf_hash) : ''; return $pdfPath !== '' || $pdfHash !== '' || (bool) $this->documento; }) ->modalHeading('Fattura (PDF)') ->modalWidth('7xl') ->modalContent(function () { $url = route('admin.fatture-elettroniche.download-pdf', ['fattura' => $this->fattura->id]) . '?inline=1'; return view('filament.modals.pdf-viewer', ['url' => $url]); }) ->modalSubmitAction(false) ->modalCancelActionLabel('Chiudi'), Action::make('completa') ->label('Completa') ->icon('heroicon-o-sparkles') ->visible(fn(): bool => $this->needsCompleta()) ->requiresConfirmation() ->action(function (): void { if (! is_string($this->fattura->xml_content) || trim($this->fattura->xml_content) === '') { Notification::make()->title('XML non disponibile')->danger()->send(); return; } $xml = (string) $this->fattura->xml_content; $filename = is_string($this->fattura->nome_file_xml) && $this->fattura->nome_file_xml !== '' ? $this->fattura->nome_file_xml : ('FE-' . (int) $this->fattura->id . '.xml'); $unique = (string) Str::uuid(); $ym = now()->format('Y/m'); $base = null; if ($this->fattura->stabile) { $base = ArchivioPaths::stabileBase($this->fattura->stabile, $this->fattura->stabile->amministratore); } if (! is_string($base) || $base === '') { $admin = $this->fattura->stabile?->amministratore; $adminFolder = $admin?->codice_univoco ?: ($admin?->codice_amministratore ?: 'ID-0'); $stabileFolder = $this->fattura->stabile?->codice_univoco ?: ($this->fattura->stabile?->codice_stabile ?: ('ID-' . (int) $this->fattura->stabile_id)); $base = 'amministratori/' . $adminFolder . '/stabili/' . $stabileFolder; } $inbox = $base . '/fatture-ricevute/inbox/' . $ym; $permanentXmlPath = $inbox . '/' . $unique . '-' . $filename; try { Storage::disk('local')->put($permanentXmlPath, $xml); } catch (\Throwable $e) { Notification::make()->title('Errore salvataggio XML')->body($e->getMessage())->danger()->send(); return; } $importer = new FatturaElettronicaImporter(new FatturaElettronicaXmlParser()); $stabileId = $this->fattura->stabile_id ? (int) $this->fattura->stabile_id : 0; $result = $importer->importXml($xml, $stabileId, $filename, [ 'xml_path' => $permanentXmlPath, 'force_update' => true, 'importa_righe' => 'auto', 'auto_repair_duplicate' => true, 'allow_fallback_stabile' => true, 'force_stabile_id' => $stabileId, 'user_id' => (int) (Auth::id() ?: 0), ]); if (($result['status'] ?? null) === 'imported') { // Best-effort: ripara/aggiorna protocollazione e percorso PDF, se mancanti. try { $userId = (int) (Auth::id() ?: 0); if ($userId > 0) { app(FatturaElettronicaProtocolloService::class)->ensureDocumentoProtocollato($this->fattura, $userId); $this->fattura->refresh(); } } catch (\Throwable) { // ignore } Notification::make()->title('Completata')->success()->send(); $this->redirect(self::getUrl(['record' => $this->fattura->id], panel: 'admin-filament')); return; } $message = (string) ($result['message'] ?? 'Operazione non riuscita'); Notification::make()->title('Errore durante completa')->body($message)->danger()->send(); }), Action::make('rigenera_pdf') ->label('Rigenera PDF (da XML)') ->icon('heroicon-o-arrow-path') ->visible(function (): bool { $hasXml = (is_string($this->fattura->xml_content) && trim($this->fattura->xml_content) !== '') || (is_string($this->fattura->xml_path) && $this->fattura->xml_path !== ''); return $hasXml; }) ->requiresConfirmation() ->action(function (): void { $userId = (int) (Auth::id() ?: 0); if ($userId <= 0) { Notification::make()->title('Utente non valido')->danger()->send(); return; } $hasXml = (is_string($this->fattura->xml_content) && trim($this->fattura->xml_content) !== '') || (is_string($this->fattura->xml_path) && $this->fattura->xml_path !== '' && Storage::disk('local')->exists($this->fattura->xml_path)); if (! $hasXml) { Notification::make()->title('XML non disponibile')->danger()->send(); return; } // Metti in quarantena l'eventuale PDF già agganciato (evita perdita dati / facilita diagnosi). try { $oldPath = is_string($this->fattura->allegato_pdf_path) ? trim($this->fattura->allegato_pdf_path) : ''; if ($oldPath !== '' && Storage::disk('local')->exists($oldPath)) { $oldDir = trim((string) dirname($oldPath)); $quarantineDir = $oldDir . '/__OLD'; if (! Storage::disk('local')->exists($quarantineDir)) { Storage::disk('local')->makeDirectory($quarantineDir); } $stamp = now()->format('Ymd_His'); $target = $quarantineDir . '/' . $stamp . '-' . basename($oldPath); Storage::disk('local')->move($oldPath, $target); } } catch (\Throwable) { // ignore } // Forza rigenerazione: svuota riferimenti e riprotocollo. $this->fattura->allegato_pdf_path = null; $this->fattura->allegato_pdf_nome = null; $this->fattura->allegato_pdf_hash = null; $this->fattura->save(); try { app(FatturaElettronicaProtocolloService::class)->ensureDocumentoProtocollato($this->fattura, $userId); $this->fattura->refresh(); $this->documento = Documento::query() ->where('documentable_type', FatturaElettronica::class) ->where('documentable_id', (int) $this->fattura->id) ->first(); Notification::make()->title('PDF rigenerato')->success()->send(); $this->redirect(self::getUrl(['record' => $this->fattura->id], panel: 'admin-filament')); } catch (\Throwable $e) { Notification::make()->title('Errore rigenerazione PDF')->body($e->getMessage())->danger()->send(); } }), Action::make('estrai_testo_pdf') ->label('Estrai testo PDF') ->icon('heroicon-o-document-magnifying-glass') ->visible(function (): bool { if ($this->documento && is_string($this->documento->percorso_file) && $this->documento->percorso_file !== '') { return true; } if (is_string($this->fattura->allegato_pdf_path) && $this->fattura->allegato_pdf_path !== '') { return true; } return false; }) ->requiresConfirmation() ->action(function (): void { $userId = (int) (Auth::id() ?: 0); // Best-effort: assicurati che esista Documento e percorso PDF aggiornato. try { if ($userId > 0) { app(FatturaElettronicaProtocolloService::class)->ensureDocumentoProtocollato($this->fattura, $userId); $this->fattura->refresh(); } } catch (\Throwable) { // ignore } // Refresh documento (può essere stato creato ora). $this->documento = Documento::query() ->where('documentable_type', FatturaElettronica::class) ->where('documentable_id', (int) $this->fattura->id) ->first(); // Fallback: se il Documento non esiste ma abbiamo un path PDF sulla FE, creiamo un Documento minimale. if (! $this->documento) { $pdfPath = is_string($this->fattura->allegato_pdf_path) ? trim($this->fattura->allegato_pdf_path) : ''; if ($pdfPath !== '' && Storage::disk('local')->exists($pdfPath)) { $this->documento = Documento::query()->create([ 'documentable_type' => FatturaElettronica::class, 'documentable_id' => (int) $this->fattura->id, 'stabile_id' => $this->fattura->stabile_id ? (int) $this->fattura->stabile_id : null, 'utente_id' => $userId > 0 ? $userId : null, 'tipologia' => 'fattura', 'nome' => 'Fattura elettronica #' . (int) $this->fattura->id, 'nome_file' => is_string($this->fattura->allegato_pdf_nome) && $this->fattura->allegato_pdf_nome !== '' ? $this->fattura->allegato_pdf_nome : ('fattura-' . (int) $this->fattura->id . '.pdf'), 'mime_type' => 'application/pdf', // legacy field (some parts of the app still rely on it) 'path_file' => $pdfPath, // new field 'percorso_file' => $pdfPath, 'data_documento' => $this->fattura->data_fattura, 'data_scadenza' => $this->fattura->data_scadenza, 'importo_collegato' => $this->fattura->totale, 'fornitore' => is_string($this->fattura->fornitore_denominazione) ? $this->fattura->fornitore_denominazione : null, ]); } } if (! $this->documento) { Notification::make()->title('Documento non disponibile')->danger()->send(); return; } $result = app(PdfTextExtractionService::class)->extractAndStore($this->documento, false); if (($result['status'] ?? null) === 'ok') { $chars = (int) ($result['chars'] ?? 0); $engine = (string) ($result['engine'] ?? ''); $body = 'Testo estratto (' . number_format($chars, 0, ',', '.') . ' caratteri)'; if ($engine !== '') { $body .= ' via ' . $engine; } Notification::make()->title('Estrazione completata')->body($body)->success()->send(); $this->redirect(self::getUrl(['record' => $this->fattura->id], panel: 'admin-filament')); return; } $message = (string) ($result['message'] ?? 'Operazione non riuscita'); $status = (string) ($result['status'] ?? 'error'); if ($status === 'skipped') { Notification::make()->title('Nessuna modifica')->body($message)->warning()->send(); return; } Notification::make()->title('Errore estrazione PDF')->body($message)->danger()->send(); }), Action::make('estrai_dati_acqua') ->label('Estrai dati acqua') ->icon('heroicon-o-beaker') ->visible(function (): bool { if ($this->documento && is_string($this->documento->percorso_file) && $this->documento->percorso_file !== '') { return true; } if (is_string($this->fattura->allegato_pdf_path) && $this->fattura->allegato_pdf_path !== '') { return true; } return false; }) ->form([ Select::make('voce_spesa_id') ->label('Voce di spesa (opzionale)') ->helperText('Se selezionata, i consumi/periodi verranno collegati alla voce (utile per statistiche e ripartizione).') ->options(function (): array { if (! $this->fattura?->stabile_id || ! Schema::hasTable('voci_spesa')) { return []; } return VoceSpesa::query() ->where('stabile_id', (int) $this->fattura->stabile_id) ->orderBy('descrizione') ->where('categoria', 'acqua') ->limit(300) ->get(['id', 'codice', 'descrizione']) ->mapWithKeys(function (VoceSpesa $v) { $label = trim((string) ($v->codice ?? '')); if ($label !== '') { $label .= ' — '; } $label .= (string) ($v->descrizione ?? ('Voce #' . $v->id)); return [(string) $v->id => $label]; }) ->all(); }) ->searchable() ->nullable(), ]) ->action(function (array $data): void { $userId = (int) (Auth::id() ?: 0); // Assicura Documento + testo OCR disponibile. try { if ($userId > 0) { app(FatturaElettronicaProtocolloService::class)->ensureDocumentoProtocollato($this->fattura, $userId); $this->fattura->refresh(); } } catch (\Throwable) { // ignore } $this->documento = Documento::query() ->where('documentable_type', FatturaElettronica::class) ->where('documentable_id', (int) $this->fattura->id) ->first(); // Fallback: se il Documento non esiste ma abbiamo un path PDF sulla FE, creiamo un Documento minimale. if (! $this->documento) { $pdfPath = is_string($this->fattura->allegato_pdf_path) ? trim($this->fattura->allegato_pdf_path) : ''; if ($pdfPath !== '' && Storage::disk('local')->exists($pdfPath)) { $this->documento = Documento::query()->create([ 'documentable_type' => FatturaElettronica::class, 'documentable_id' => (int) $this->fattura->id, 'stabile_id' => $this->fattura->stabile_id ? (int) $this->fattura->stabile_id : null, 'utente_id' => $userId > 0 ? $userId : null, 'tipologia' => 'fattura', 'nome' => 'Fattura elettronica #' . (int) $this->fattura->id, 'nome_file' => is_string($this->fattura->allegato_pdf_nome) && $this->fattura->allegato_pdf_nome !== '' ? $this->fattura->allegato_pdf_nome : ('fattura-' . (int) $this->fattura->id . '.pdf'), 'mime_type' => 'application/pdf', // legacy field (some parts of the app still rely on it) 'path_file' => $pdfPath, 'percorso_file' => $pdfPath, 'data_documento' => $this->fattura->data_fattura, 'data_scadenza' => $this->fattura->data_scadenza, 'importo_collegato' => $this->fattura->totale, 'fornitore' => is_string($this->fattura->fornitore_denominazione) ? $this->fattura->fornitore_denominazione : null, ]); } } if (! $this->documento) { Notification::make()->title('Documento non disponibile')->danger()->send(); return; } $text = is_string($this->documento->contenuto_ocr) ? trim($this->documento->contenuto_ocr) : ''; if ($text === '') { $result = app(PdfTextExtractionService::class)->extractAndStore($this->documento, false); if (($result['status'] ?? null) !== 'ok') { $message = (string) ($result['message'] ?? 'Testo PDF non disponibile'); Notification::make()->title('Impossibile estrarre testo PDF')->body($message)->danger()->send(); return; } $this->documento->refresh(); $text = is_string($this->documento->contenuto_ocr) ? trim($this->documento->contenuto_ocr) : ''; } if ($text === '') { Notification::make()->title('Testo PDF vuoto')->warning()->send(); return; } $parsed = app(AcquaPdfTextParser::class)->parse($text); $codici = is_array($parsed['codici'] ?? null) ? $parsed['codici'] : []; $contatore = is_array($parsed['contatore'] ?? null) ? $parsed['contatore'] : []; $consumi = is_array($parsed['consumi'] ?? null) ? $parsed['consumi'] : []; $generale = is_array($parsed['generale'] ?? null) ? $parsed['generale'] : []; $finestra = is_array($parsed['finestra_autolettura'] ?? null) ? $parsed['finestra_autolettura'] : []; $letture = is_array($parsed['riepilogo_letture'] ?? null) ? $parsed['riepilogo_letture'] : []; $quadro = is_array($parsed['quadro_dettaglio'] ?? null) ? $parsed['quadro_dettaglio'] : []; $tariffe = is_array($parsed['tariffe'] ?? null) ? $parsed['tariffe'] : []; $ivaInfo = is_array($parsed['iva'] ?? null) ? $parsed['iva'] : []; $hasTariffData = count(array_filter([ $generale['periodicita_fatturazione'] ?? null, $tariffe['profilo'] ?? null, $ivaInfo['codice'] ?? null, $quadro['quota_fissa'] ?? null, ], static fn($v): bool => $v !== null && $v !== '')) > 0; $hasAny = false; foreach ([$codici['utenza'] ?? null, $codici['cliente'] ?? null, $codici['contratto'] ?? null, $contatore['matricola'] ?? null] as $v) { if (is_string($v) && trim($v) !== '') { $hasAny = true; break; } } if (! $hasAny && count($consumi) < 1 && ! $hasTariffData) { Notification::make()->title('Dati acqua non trovati')->warning()->send(); return; } $payload = [ 'type' => 'acqua', 'source' => 'pdf_ocr', 'codici' => [ 'utenza' => $codici['utenza'] ?? null, 'cliente' => $codici['cliente'] ?? null, 'contratto' => $codici['contratto'] ?? null, ], 'contatore' => [ 'matricola' => $contatore['matricola'] ?? null, ], 'consumi' => array_values($consumi), 'generale' => [ 'tipologia_utenza' => $generale['tipologia_utenza'] ?? null, 'tariffa_applicata' => $generale['tariffa_applicata'] ?? null, 'numero_componenti_totali' => $generale['numero_componenti_totali'] ?? null, 'deposito_cauzionale_euro' => $generale['deposito_cauzionale_euro'] ?? null, 'numero_unita_abitative' => $generale['numero_unita_abitative'] ?? null, 'periodicita_fatturazione' => $generale['periodicita_fatturazione'] ?? null, 'numero_fattura_pdf' => $generale['numero_fattura_pdf'] ?? null, 'data_emissione_pdf' => $generale['data_emissione_pdf'] ?? null, ], 'finestra_autolettura' => [ 'dal' => $finestra['dal'] ?? null, 'al' => $finestra['al'] ?? null, 'alert' => (bool) ($finestra['alert'] ?? false), 'messaggio' => $finestra['messaggio'] ?? null, ], 'riepilogo_letture' => array_values($letture), 'quadro_dettaglio' => [ 'quota_fissa' => $quadro['quota_fissa'] ?? null, 'acquedotto' => $quadro['acquedotto'] ?? null, 'fognatura' => $quadro['fognatura'] ?? null, 'depurazione' => $quadro['depurazione'] ?? null, 'oneri_perequazione' => $quadro['oneri_perequazione'] ?? null, 'restituzione_acconti' => $quadro['restituzione_acconti'] ?? null, ], 'tariffe' => $tariffe, 'iva' => [ 'codice' => $ivaInfo['codice'] ?? null, 'aliquota_percentuale' => $ivaInfo['aliquota_percentuale'] ?? null, 'descrizione' => $ivaInfo['descrizione'] ?? null, ], 'parsed_at' => now()->toISOString(), ]; // Compila anche i campi generici consumo_* (best-effort) $totMc = null; $ref = null; if (count($consumi) > 0) { $sum = 0.0; $has = false; foreach ($consumi as $c) { if (isset($c['valore']) && is_numeric($c['valore'])) { $sum += (float) $c['valore']; $has = true; } } if ($has) { $totMc = $sum; } $first = $consumi[0] ?? []; $dal = $first['dal'] ?? null; $al = $first['al'] ?? null; if (is_string($dal) && is_string($al) && $dal !== '' && $al !== '') { $ref = $dal . ' - ' . $al; } } $this->fattura->consumo_unita = 'mc'; $this->fattura->consumo_valore = $totMc; $this->fattura->consumo_riferimento = $ref; $this->fattura->consumo_raw = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $this->fattura->save(); try { app(ConsumiAcquaTariffeIngestionService::class)->ingest($this->fattura, $parsed, null); } catch (\Throwable) { // best-effort } // Persistenza strutturale: servizio + letture periodiche (uno-a-molti) per statistiche/ripartizione. $voceSpesaId = isset($data['voce_spesa_id']) && is_numeric($data['voce_spesa_id']) ? (int) $data['voce_spesa_id'] : null; $ing = app(ConsumiAcquaIngestionService::class)->ingest($this->fattura, $parsed, $voceSpesaId, $userId, true, null); if (($ing['status'] ?? null) === 'no-data' && $hasTariffData) { Notification::make() ->title('Tariffe acqua salvate') ->body('Nessuna lettura consumi trovata, ma il quadro tariffario e IVA sono stati archiviati.') ->success() ->send(); $this->redirect(self::getUrl(['record' => $this->fattura->id], panel: 'admin-filament')); return; } if (($ing['status'] ?? null) === 'skipped') { Notification::make() ->title('Dati acqua salvati (solo su FE)') ->body((string) ($ing['message'] ?? 'Tabelle servizi/letture non presenti')) ->warning() ->send(); $this->redirect(self::getUrl(['record' => $this->fattura->id], panel: 'admin-filament')); return; } if (($ing['status'] ?? null) === 'mismatch') { Notification::make() ->title('Attenzione: mismatch utenza/contatore') ->body((string) ($ing['message'] ?? 'Controllare il PDF: aperto un ticket per verifica.')) ->warning() ->send(); $this->redirect(self::getUrl(['record' => $this->fattura->id], panel: 'admin-filament')); return; } if (($ing['status'] ?? null) !== 'ok') { Notification::make() ->title('Errore salvataggio letture') ->body((string) ($ing['message'] ?? 'Operazione non riuscita')) ->danger() ->send(); return; } try { app(ConsumiAcquaTariffeIngestionService::class)->ingest( $this->fattura, $parsed, isset($ing['servizio_id']) && is_numeric($ing['servizio_id']) ? (int) $ing['servizio_id'] : null ); } catch (\Throwable) { // best-effort: non bloccare il salvataggio letture } $body = 'Servizio: #' . (int) ($ing['servizio_id'] ?? 0); $body .= ' · Letture create: ' . (int) ($ing['created_letture'] ?? 0); $upd = (int) ($ing['updated_letture'] ?? 0); if ($upd > 0) { $body .= ' · aggiornate: ' . $upd; } Notification::make()->title('Dati acqua salvati')->body($body)->success()->send(); $this->redirect(self::getUrl(['record' => $this->fattura->id], panel: 'admin-filament')); }), Action::make('leggi_pdf_acqua_guidata') ->label('Leggi PDF acqua (guidata)') ->icon('heroicon-o-clipboard-document-list') ->color('warning') ->visible(function (): bool { if ($this->documento && is_string($this->documento->percorso_file) && $this->documento->percorso_file !== '') { return true; } if (is_string($this->fattura->allegato_pdf_path) && $this->fattura->allegato_pdf_path !== '') { return true; } return false; }) ->action(function (): void { if (! $this->isFatturaCompatibleWithCurrentStabileCf()) { Notification::make() ->title('Filtro stabile non superato') ->body('La FE non risulta allineata al codice fiscale dello stabile attivo. Verifica il CF dello stabile e il destinatario FE.') ->danger() ->send(); return; } $userId = (int) (Auth::id() ?: 0); try { if ($userId > 0) { app(FatturaElettronicaProtocolloService::class)->ensureDocumentoProtocollato($this->fattura, $userId); $this->fattura->refresh(); } } catch (\Throwable) { // ignore } $this->documento = Documento::query() ->where('documentable_type', FatturaElettronica::class) ->where('documentable_id', (int) $this->fattura->id) ->first(); if (! $this->documento) { Notification::make()->title('Documento non disponibile')->danger()->send(); return; } $text = is_string($this->documento->contenuto_ocr) ? trim($this->documento->contenuto_ocr) : ''; if ($text === '') { $result = app(PdfTextExtractionService::class)->extractAndStore($this->documento, false); if (($result['status'] ?? null) !== 'ok') { $message = (string) ($result['message'] ?? 'Testo PDF non disponibile'); Notification::make()->title('Impossibile leggere PDF')->body($message)->danger()->send(); return; } $this->documento->refresh(); $text = is_string($this->documento->contenuto_ocr) ? trim($this->documento->contenuto_ocr) : ''; } if ($text === '') { Notification::make()->title('Testo PDF vuoto')->warning()->send(); return; } $parsed = app(AcquaPdfTextParser::class)->parse($text); $orderedParsed = $this->orderAcquaParsedPayload(is_array($parsed) ? $parsed : []); $maxChars = 250000; $textPreview = mb_substr($text, 0, $maxChars); $wasTruncated = mb_strlen($text) > $maxChars; $this->acquaPdfPreview = [ 'fattura_id' => (int) $this->fattura->id, 'stabile_id' => (int) ($this->fattura->stabile_id ?? 0), 'stabile_cf' => $this->normalizeMatchValue($this->fattura->stabile?->codice_fiscale), 'destinatario_cf' => $this->normalizeMatchValue($this->fattura->destinatario_cf), 'chars' => mb_strlen($text), 'truncated' => $wasTruncated, 'raw_text' => $textPreview, 'parsed' => $orderedParsed, 'generated_at' => now()->format('d/m/Y H:i:s'), ]; Notification::make() ->title('Lettura PDF completata') ->body('Anteprima disponibile in fondo pagina. Indicami ora i campi da memorizzare.') ->success() ->send(); }), Action::make('modifica') ->label('Modifica') ->icon('heroicon-o-pencil-square') ->form([ Select::make('fornitore_id') ->label('Fornitore (opzionale)') ->options(fn() => Fornitore::query() ->orderBy('ragione_sociale') ->orderBy('id') ->limit(500) ->get(['id', 'ragione_sociale', 'codice_fiscale', 'partita_iva']) ->mapWithKeys(function (Fornitore $f) { $label = trim((string) ($f->ragione_sociale ?? '')); if ($label === '') { $cf = trim((string) ($f->codice_fiscale ?? '')); $piva = trim((string) ($f->partita_iva ?? '')); $label = $cf !== '' ? $cf : ($piva !== '' ? $piva : ('Fornitore #' . $f->id)); } return [(string) $f->id => $label]; }) ->all()) ->searchable() ->nullable(), DatePicker::make('data_scadenza') ->label('Scadenza') ->nullable(), Select::make('stato') ->label('Stato') ->options([ 'ricevuta' => 'Ricevuta', 'contabilizzata' => 'Contabilizzata', 'pagata' => 'Pagata', 'rifiutata' => 'Rifiutata', ]) ->required(), ]) ->fillForm(fn() => [ 'fornitore_id' => $this->fattura->fornitore_id, 'data_scadenza' => $this->fattura->data_scadenza, 'stato' => $this->fattura->stato, ]) ->action(function (array $data): void { $this->fattura->fornitore_id = $data['fornitore_id'] ? (int) $data['fornitore_id'] : null; $this->fattura->data_scadenza = $data['data_scadenza'] ?? null; $this->fattura->stato = (string) $data['stato']; $this->fattura->save(); Notification::make()->title('Fattura aggiornata')->success()->send(); }), Action::make('apri_contabilita') ->label('Apri in contabilità') ->icon('heroicon-o-banknotes') ->visible(fn(): bool => ! empty($this->contabilitaFatturaId)) ->url(fn() => \App\Filament\Pages\Contabilita\FatturaFornitoreScheda::getUrl([ 'record' => $this->contabilitaFatturaId, ], panel: 'admin-filament')), Action::make('apri_fornitore') ->label('Apri fornitore') ->icon('heroicon-o-arrow-top-right-on-square') ->visible(fn(): bool => (int) ($this->fattura->fornitore_id ?? 0) > 0) ->url(fn() => \App\Filament\Pages\Gescon\FornitoreScheda::getUrl([ 'record' => (int) $this->fattura->fornitore_id, ], panel: 'admin-filament'), shouldOpenInNewTab: true), Action::make('crea_fornitore') ->label('Crea fornitore') ->icon('heroicon-o-plus') ->visible(function (): bool { $user = Auth::user(); if (! $user instanceof User) { return false; } return (int) ($this->fattura->fornitore_id ?? 0) <= 0 && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); }) ->requiresConfirmation() ->action(function (): void { $user = Auth::user(); if (! $user instanceof User) { Notification::make()->title('Utente non valido')->danger()->send(); return; } if (! $this->fattura->stabile) { Notification::make()->title('Stabile non disponibile')->danger()->send(); return; } $amministratoreId = (int) ($this->fattura->stabile->amministratore_id ?: 0); if ($amministratoreId <= 0) { Notification::make()->title('Amministratore non disponibile')->danger()->send(); return; } $fornitore = $this->ensureFornitoreFromFattura($amministratoreId, (int) $user->id); if (! $fornitore) { Notification::make()->title('Impossibile creare il fornitore')->danger()->send(); return; } $this->fattura->fornitore_id = (int) $fornitore->id; $this->fattura->save(); Notification::make()->title('Fornitore creato')->success()->send(); $this->redirect(\App\Filament\Pages\Gescon\FornitoreScheda::getUrl([ 'record' => (int) $fornitore->id, ], panel: 'admin-filament')); }), Action::make('torna') ->label('Indietro') ->icon('heroicon-o-arrow-left') ->url(fn() => $this->getBackUrl()), ]; } private function ensureFornitoreFromFattura(int $amministratoreId, int $userId): ?Fornitore { $piva = is_string($this->fattura->fornitore_piva) ? trim($this->fattura->fornitore_piva) : null; $cf = is_string($this->fattura->fornitore_cf) ? trim($this->fattura->fornitore_cf) : null; $den = is_string($this->fattura->fornitore_denominazione) ? trim($this->fattura->fornitore_denominazione) : ''; if ($piva === '' || $piva === 'ND') { $piva = null; } if ($cf === '') { $cf = null; } if ($den === '') { $den = $piva ?: ($cf ?: 'Fornitore'); } $query = Fornitore::query()->where('amministratore_id', $amministratoreId); if ($piva) { $existing = (clone $query)->where('partita_iva', $piva)->first(); if ($existing) { return $existing; } } if ($cf) { $existing = (clone $query)->where('codice_fiscale', $cf)->first(); if ($existing) { return $existing; } } $now = now(); $rubrica = null; if ($piva) { $rubrica = RubricaUniversale::query()->where('partita_iva', $piva)->first(); } if (! $rubrica && $cf) { $rubrica = RubricaUniversale::query()->where('codice_fiscale', $cf)->first(); } if (! $rubrica) { $rubrica = RubricaUniversale::query() ->where('tipo_contatto', 'persona_giuridica') ->where('categoria', 'fornitore') ->whereRaw('LOWER(ragione_sociale) = ?', [mb_strtolower($den)]) ->first(); } if (! $rubrica) { $rubrica = RubricaUniversale::query()->create([ 'ragione_sociale' => $den, 'tipo_contatto' => 'persona_giuridica', 'categoria' => 'fornitore', 'stato' => 'attivo', 'partita_iva' => $piva, 'codice_fiscale' => $cf, 'data_inserimento' => $now, 'data_ultima_modifica' => $now, 'creato_da' => $userId > 0 ? $userId : null, 'modificato_da' => $userId > 0 ? $userId : null, ]); } return Fornitore::query()->create([ 'amministratore_id' => $amministratoreId, 'rubrica_id' => (int) $rubrica->id, 'ragione_sociale' => $den, 'partita_iva' => $piva, 'codice_fiscale' => $cf, 'iban' => is_string($this->fattura->pagamento_iban) ? $this->fattura->pagamento_iban : null, 'bic' => is_string($this->fattura->pagamento_bic) ? $this->fattura->pagamento_bic : null, 'modalita_pagamento_predefinita' => is_string($this->fattura->pagamento_modalita) ? $this->fattura->pagamento_modalita : null, ]); } private function needsCompleta(): bool { // Documento mancante if (! $this->documento) { return true; } // PDF non disponibile sullo storage if (! is_string($this->fattura->allegato_pdf_path) || $this->fattura->allegato_pdf_path === '' || ! Storage::disk('local')->exists($this->fattura->allegato_pdf_path)) { return true; } // XML su storage mancante (se abbiamo xml_path) if (is_string($this->fattura->xml_path) && $this->fattura->xml_path !== '' && ! Storage::disk('local')->exists($this->fattura->xml_path)) { return true; } return false; } private function isFatturaCompatibleWithCurrentStabileCf(): bool { $candidateIds = array_filter([ $this->normalizeMatchValue($this->fattura->stabile?->codice_fiscale), $this->normalizeMatchValue($this->fattura->stabile?->cod_fisc_amministratore), $this->normalizeMatchValue($this->fattura->stabile?->amministratore?->codice_fiscale_studio), $this->normalizeMatchValue($this->fattura->stabile?->amministratore?->partita_iva), ]); $destIds = array_filter([ $this->normalizeMatchValue($this->fattura->destinatario_cf), $this->normalizeMatchValue($this->fattura->destinatario_piva ?? null), ]); if ($candidateIds === [] || $destIds === []) { return false; } foreach ($destIds as $destId) { if (in_array($destId, $candidateIds, true)) { return true; } } return false; } private function normalizeMatchValue(?string $value): string { return strtoupper(str_replace(' ', '', trim((string) $value))); } /** @param array $parsed */ private function orderAcquaParsedPayload(array $parsed): array { $codici = is_array($parsed['codici'] ?? null) ? $parsed['codici'] : []; $contatore = is_array($parsed['contatore'] ?? null) ? $parsed['contatore'] : []; $generale = is_array($parsed['generale'] ?? null) ? $parsed['generale'] : []; $finestra = is_array($parsed['finestra_autolettura'] ?? null) ? $parsed['finestra_autolettura'] : []; $letture = is_array($parsed['riepilogo_letture'] ?? null) ? $parsed['riepilogo_letture'] : []; $quadro = is_array($parsed['quadro_dettaglio'] ?? null) ? $parsed['quadro_dettaglio'] : []; $tariffe = is_array($parsed['tariffe'] ?? null) ? $parsed['tariffe'] : []; $iva = is_array($parsed['iva'] ?? null) ? $parsed['iva'] : []; $consumi = is_array($parsed['consumi'] ?? null) ? $parsed['consumi'] : []; return [ 'codici' => [ 'utenza' => $codici['utenza'] ?? null, 'cliente' => $codici['cliente'] ?? null, 'contratto' => $codici['contratto'] ?? null, ], 'contatore' => [ 'matricola' => $contatore['matricola'] ?? null, ], 'generale' => [ 'tipologia_utenza' => $generale['tipologia_utenza'] ?? null, 'tariffa_applicata' => $generale['tariffa_applicata'] ?? null, 'numero_componenti_totali' => $generale['numero_componenti_totali'] ?? null, 'deposito_cauzionale_euro' => $generale['deposito_cauzionale_euro'] ?? null, 'numero_unita_abitative' => $generale['numero_unita_abitative'] ?? null, 'periodicita_fatturazione' => $generale['periodicita_fatturazione'] ?? null, 'numero_fattura_pdf' => $generale['numero_fattura_pdf'] ?? null, 'data_emissione_pdf' => $generale['data_emissione_pdf'] ?? null, ], 'finestra_autolettura' => [ 'dal' => $finestra['dal'] ?? null, 'al' => $finestra['al'] ?? null, 'alert' => (bool) ($finestra['alert'] ?? false), 'messaggio' => $finestra['messaggio'] ?? null, ], 'riepilogo_letture' => array_values($letture), 'quadro_dettaglio' => [ 'quota_fissa' => $quadro['quota_fissa'] ?? null, 'acquedotto' => $quadro['acquedotto'] ?? null, 'fognatura' => $quadro['fognatura'] ?? null, 'depurazione' => $quadro['depurazione'] ?? null, 'oneri_perequazione' => $quadro['oneri_perequazione'] ?? null, 'restituzione_acconti' => $quadro['restituzione_acconti'] ?? null, ], 'tariffe' => $tariffe, 'iva' => [ 'codice' => $iva['codice'] ?? null, 'aliquota_percentuale' => $iva['aliquota_percentuale'] ?? null, 'descrizione' => $iva['descrizione'] ?? null, ], 'consumi' => array_values($consumi), ]; } }