1184 lines
58 KiB
PHP
1184 lines
58 KiB
PHP
<?php
|
|
namespace App\Filament\Pages\Contabilita;
|
|
|
|
use App\Filament\Pages\Contabilita\FattureElettronicheArchivio;
|
|
use App\Models\Documento;
|
|
use App\Models\FatturaElettronica;
|
|
use App\Models\FeXsdCode;
|
|
use App\Models\FeXsdCodeDomain;
|
|
use App\Models\FeXsdVersion;
|
|
use App\Models\Fornitore;
|
|
use App\Models\RubricaUniversale;
|
|
use App\Models\Stabile;
|
|
use App\Models\User;
|
|
use App\Models\VoceSpesa;
|
|
use App\Modules\Contabilita\Models\FatturaFornitore as ContabilitaFatturaFornitore;
|
|
use App\Services\Consumi\AcquaPdfTextParser;
|
|
use App\Services\Consumi\ConsumiAcquaIngestionService;
|
|
use App\Services\Consumi\ConsumiAcquaTariffeIngestionService;
|
|
use App\Services\Documenti\PdfTextExtractionService;
|
|
use App\Services\FattureElettroniche\FatturaElettronicaImporter;
|
|
use App\Services\FattureElettroniche\FatturaElettronicaProtocolloService;
|
|
use App\Services\FattureElettroniche\FatturaElettronicaXmlParser;
|
|
use App\Support\ArchivioPaths;
|
|
use App\Support\StabileContext;
|
|
use Filament\Actions\Action;
|
|
use Filament\Forms\Components\DatePicker;
|
|
use Filament\Forms\Components\Select;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Pages\Page;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
|
|
class FatturaElettronicaScheda extends Page
|
|
{
|
|
protected static ?string $title = 'Scheda fattura ricevuta';
|
|
|
|
protected static ?string $slug = 'contabilita/fatture-ricevute/{record}';
|
|
|
|
protected static bool $shouldRegisterNavigation = false;
|
|
|
|
protected string $view = 'filament.pages.contabilita.fattura-elettronica-scheda';
|
|
|
|
public FatturaElettronica $fattura;
|
|
|
|
public ?Documento $documento = null;
|
|
|
|
/** @var array<string, mixed> */
|
|
public array $xmlDetails = [];
|
|
|
|
public ?int $contabilitaFatturaId = null;
|
|
|
|
/** @var array<string,mixed> */
|
|
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<string,mixed> $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),
|
|
];
|
|
}
|
|
}
|