netgescon-day0/app/Filament/Pages/Contabilita/FattureElettronicheP7mRicevute.php

1682 lines
78 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Filament\Pages\Contabilita;
use App\Filament\Pages\Contabilita\FatturaElettronicaScheda;
use App\Jobs\RunCassettoFiscaleDownload;
use App\Models\FatturaElettronica;
use App\Models\Stabile;
use App\Models\User;
use App\Services\FattureElettroniche\CassettoFiscaleDownloadService;
use App\Services\FattureElettroniche\FatturaElettronicaImporter;
use App\Services\FattureElettroniche\FatturaElettronicaProtocolloService;
use App\Services\FattureElettroniche\FatturaElettronicaXmlParser;
use App\Services\FattureElettroniche\P7mExtractor;
use App\Support\ArchivioPaths;
use App\Support\StabileContext;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Tables\Columns\BadgeColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use UnitEnum;
use ZipArchive;
class FattureElettronicheP7mRicevute extends Page implements HasTable
{
use InteractsWithTable;
public array $quarterDownloads = [];
public function getActiveStabileLabelProperty(): string
{
$user = Auth::user();
if (! $user instanceof User) {
return 'Stabile non selezionato';
}
$stabile = StabileContext::getActiveStabile($user);
if (! $stabile instanceof Stabile) {
return 'Stabile non selezionato';
}
$parts = array_values(array_filter([
trim((string) ($stabile->cod_stabile ?? '')),
trim((string) ($stabile->codice_stabile ?? '')),
trim((string) ($stabile->denominazione ?? '')),
]));
return $parts !== [] ? implode(' - ', $parts) : ('Stabile #' . (int) $stabile->id);
}
public function getActiveStabileDestinationSummaryProperty(): string
{
$user = Auth::user();
if (! $user instanceof User) {
return 'Stabile non selezionato';
}
$stabile = StabileContext::getActiveStabile($user);
if (! $stabile instanceof Stabile) {
return 'Stabile non selezionato';
}
$summary = $this->getActiveStabileLabelProperty();
$cf = $this->resolveStabileSoggettoCf($stabile);
if ($cf !== '') {
$summary .= "\nCodice fiscale soggetto usato per lo scarico: {$cf}";
}
if ($this->isLikelyTechnicalFeShell($stabile)) {
$summary .= "\nATTENZIONE: lo stabile attivo sembra uno shell tecnico FE separato. Per scaricare le fatture del condominio cambia lo stabile in topbar prima di procedere.";
}
return $summary;
}
private function safeUtf8(string $value): string
{
$value = preg_replace('/^\xEF\xBB\xBF/', '', $value) ?? $value;
if (function_exists('mb_check_encoding') && ! mb_check_encoding($value, 'UTF-8')) {
if (function_exists('mb_convert_encoding')) {
$value = mb_convert_encoding($value, 'UTF-8', 'UTF-8,ISO-8859-1,Windows-1252');
}
}
$fixed = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
if (is_string($fixed) && $fixed !== '') {
$value = $fixed;
}
return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $value) ?? $value;
}
public int $cassettoAnno;
/**
* Ultimi scarichi da Cassetto Fiscale per lo stabile attivo.
*/
public function getCassettoDownloadLogs(int $limit = 10): \Illuminate\Support\Collection
{
$user = Auth::user();
if (! $user instanceof User) {
return collect();
}
$stabile = StabileContext::getActiveStabile($user);
if (! $stabile instanceof Stabile) {
return collect();
}
$amministratore = $stabile->amministratore;
if (! $amministratore) {
return collect();
}
return DB::table('fe_cassetto_download_logs')
->where('stabile_id', (int) $stabile->id)
->where('amministratore_id', (int) $amministratore->id)
->orderByDesc('created_at')
->limit(max(1, min(50, $limit)))
->get();
}
/**
* Stato trimestri per un anno (Q1..Q4) per lo stabile attivo.
* @return array<int, array{quarter: string, dal: string, al: string, stato: string, log_id: int|null, files_total: int, imported: int, duplicates: int, errors: int, created_at: string|null}>
*/
public function getCassettoQuarterStatus(int $anno): array
{
$anno = max(2000, min(2100, (int) $anno));
$user = Auth::user();
if (! $user instanceof User) {
return [];
}
$stabile = StabileContext::getActiveStabile($user);
if (! $stabile instanceof Stabile) {
return [];
}
$amministratore = $stabile->amministratore;
if (! $amministratore) {
return [];
}
$ranges = [
'Q1' => [$anno . '-01-01', $anno . '-03-31'],
'Q2' => [$anno . '-04-01', $anno . '-06-30'],
'Q3' => [$anno . '-07-01', $anno . '-09-30'],
'Q4' => [$anno . '-10-01', $anno . '-12-31'],
];
$out = [];
foreach ($ranges as $q => [$dal, $al]) {
$log = DB::table('fe_cassetto_download_logs')
->where('amministratore_id', (int) $amministratore->id)
->where('stabile_id', (int) $stabile->id)
->whereDate('dal', $dal)
->whereDate('al', $al)
->orderByDesc('id')
->first();
$stato = 'MAI';
$filesTotal = 0;
$imported = 0;
$duplicates = 0;
$errors = 0;
$createdAt = null;
$logId = null;
if ($log) {
$logId = is_numeric($log->id ?? null) ? (int) $log->id : null;
$filesTotal = (int) ($log->files_total ?? 0);
$imported = (int) ($log->imported ?? 0);
$duplicates = (int) ($log->duplicates ?? 0);
$errors = (int) ($log->errors ?? 0);
$createdAt = is_string($log->created_at ?? null) ? (string) $log->created_at : null;
$status = strtolower((string) ($log->status ?? ''));
$isComplete = (
$status === 'ok'
&& $errors === 0
&& $filesTotal > 0
&& ($imported + $duplicates) >= $filesTotal
);
if ($isComplete) {
$stato = 'COMPLETO';
} elseif (in_array($status, ['started', 'running', 'in_progress'], true)) {
$stato = 'IN CORSO';
} elseif ($errors > 0 || $status === 'error' || $status === 'failed') {
$stato = 'ERRORE';
} else {
$stato = 'PARZIALE';
}
}
$out[] = [
'quarter' => $q,
'dal' => $dal,
'al' => $al,
'stato' => $stato,
'log_id' => $logId,
'files_total' => $filesTotal,
'imported' => $imported,
'duplicates' => $duplicates,
'errors' => $errors,
'created_at' => $createdAt,
];
}
return $out;
}
public function downloadCassettoQuarter(string $quarter, string $dal, string $al, int $anno): void
{
$key = $quarter . '-' . $anno;
if (! empty($this->quarterDownloads[$key])) {
return;
}
$this->quarterDownloads[$key] = time();
$user = Auth::user();
if (! $user instanceof User) {
Notification::make()->title('Utente non valido')->danger()->send();
$this->quarterDownloads[$key] = false;
return;
}
$stabile = StabileContext::getActiveStabile($user);
if (! $stabile instanceof Stabile) {
Notification::make()->title('Seleziona uno stabile')->danger()->send();
$this->quarterDownloads[$key] = false;
return;
}
if ($this->isLikelyTechnicalFeShell($stabile)) {
$this->notifyTechnicalFeShellBlocked();
$this->quarterDownloads[$key] = false;
return;
}
$amministratoreId = (int) ($stabile->amministratore_id ?? 0);
if ($amministratoreId <= 0) {
Notification::make()->title('Amministratore non valido')->danger()->send();
$this->quarterDownloads[$key] = false;
return;
}
if ($this->isCassettoQuarterCompleteFor($amministratoreId, (int) $stabile->id, $dal, $al)) {
Notification::make()
->title('Trimestre già completo')
->body($this->safeUtf8("{$quarter} {$anno} risulta già scaricato (COMPLETO). Per rieseguire lo stesso periodo usa 'Scarica da Cassetto Fiscale' e abilita 'Reimport / completa' oppure 'Non saltare'."))
->warning()
->send();
return;
}
try {
$job = new RunCassettoFiscaleDownload(
$amministratoreId,
(int) $stabile->id,
$dal,
$al,
false,
(int) $user->id,
[
'force_update' => false,
'importa_righe' => 'auto',
'create_fornitore_if_missing' => true,
],
);
} catch (\Throwable $e) {
report($e);
Notification::make()->title('Errore')->body($this->safeUtf8($e->getMessage()))->danger()->send();
return;
}
// Eseguiamo dopo la risposta HTTP per non dipendere da queue worker attivo.
app()->terminating(function () use ($job): void {
try {
$job->handle(app(CassettoFiscaleDownloadService::class));
} catch (\Throwable $e) {
report($e);
}
});
Notification::make()
->title('Scarico avviato')
->body($this->safeUtf8("Trimestre {$quarter} {$anno} avviato. Aggiorna tra poco la tab scarichi per vedere il log."))
->success()
->send();
}
private function isCassettoQuarterCompleteFor(int $amministratoreId, int $stabileId, string $dalYmd, string $alYmd): bool
{
$row = DB::table('fe_cassetto_download_logs')
->where('amministratore_id', $amministratoreId)
->where('stabile_id', $stabileId)
->whereDate('dal', $dalYmd)
->whereDate('al', $alYmd)
->where('status', 'ok')
->orderByDesc('id')
->first();
if (! $row) {
return false;
}
$filesTotal = (int) ($row->files_total ?? 0);
$imported = (int) ($row->imported ?? 0);
$duplicates = (int) ($row->duplicates ?? 0);
$errors = (int) ($row->errors ?? 0);
return $errors === 0 && $filesTotal > 0 && ($imported + $duplicates) >= $filesTotal;
}
private function resolveStabileSoggettoCf(Stabile $stabile): string
{
$cf = trim((string) ($stabile->codice_fiscale ?? ''));
if ($cf === '') {
try {
$stabile->loadMissing('rubrica');
$cf = trim((string) ($stabile->rubrica?->codice_fiscale ?? ''));
} catch (\Throwable) {
// ignore
}
}
return strtoupper(trim($cf));
}
private function isLikelyTechnicalFeShell(Stabile $stabile): bool
{
$stabileCf = $this->resolveStabileSoggettoCf($stabile);
$adminCf = strtoupper(trim((string) ($stabile->amministratore?->codice_fiscale_studio ?? '')));
$code = strtoupper(trim((string) ($stabile->codice_stabile ?? '')));
$legacyCode = trim((string) ($stabile->cod_stabile ?? ''));
return $stabileCf !== ''
&& $adminCf !== ''
&& $stabileCf === $adminCf
&& (str_starts_with($code, 'FE') || $legacyCode === '');
}
private function notifyTechnicalFeShellBlocked(): void
{
Notification::make()
->title('Stabile tecnico FE separato')
->body('Lo scarico è bloccato perché lo stabile attivo usa il codice fiscale dello studio/amministratore. Se devi scaricare le fatture del condominio, cambia lo stabile attivo in topbar e seleziona il condominio reale.')
->warning()
->send();
}
public function getArchivioRigheVisteCount(): int
{
$records = $this->getTableRecords();
if ($records instanceof LengthAwarePaginator) {
return (int) $records->total();
}
if ($records instanceof Paginator) {
return (int) $records->count();
}
return is_countable($records) ? count($records) : 0;
}
protected static ?string $navigationLabel = 'Fatture ricevute';
protected static ?string $title = 'Fatture ricevute';
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-shield-check';
protected static UnitEnum|string|null $navigationGroup = 'Contabilità';
protected static ?int $navigationSort = 35;
protected static ?string $slug = 'contabilita/fatture-ricevute';
protected string $view = 'filament.pages.contabilita.fatture-elettroniche-p7m-ricevute';
public static function canAccess(): bool
{
$user = Auth::user();
if (! $user instanceof User) {
return false;
}
if ($user->hasAnyRole(['super-admin', 'admin'])) {
return true;
}
return $user->can('contabilita.fatture-elettroniche');
}
public function mount(): void
{
$this->cassettoAnno = (int) now()->format('Y');
$this->mountInteractsWithTable();
}
protected function getHeaderActions(): array
{
return [
Action::make('rigenera_pdf_da_xml')
->label('Rigenera PDF da XML')
->icon('heroicon-o-document-arrow-up')
->visible(function (): bool {
$u = Auth::user();
return $u instanceof User && $u->hasAnyRole(['super-admin', 'admin']);
})
->form([
Toggle::make('solo_mancanti')
->label('Solo fatture senza PDF')
->default(true),
TextInput::make('limit')
->label('Max fatture')
->numeric()
->default(200)
->required(),
])
->action(function (array $data): void {
$user = Auth::user();
if (! $user instanceof User) {
Notification::make()->title('Utente non valido')->danger()->send();
return;
}
$stabile = StabileContext::getActiveStabile($user);
if (! $stabile instanceof Stabile) {
Notification::make()->title('Seleziona uno stabile')->danger()->send();
return;
}
$limit = (int) ($data['limit'] ?? 200);
$limit = max(1, min(2000, $limit));
$soloMancanti = (bool) ($data['solo_mancanti'] ?? true);
$q = FatturaElettronica::query()
->where('stabile_id', (int) $stabile->id)
->where(function (Builder $qq) {
$qq
->where(function (Builder $q2) {
$q2->whereNotNull('xml_content')->where('xml_content', '!=', '');
})
->orWhere(function (Builder $q2) {
$q2->whereNotNull('xml_path')->where('xml_path', '!=', '');
});
});
if ($soloMancanti) {
$q->where(function (Builder $qq) {
$qq
->whereNull('allegato_pdf_path')
->orWhere('allegato_pdf_path', '')
->orWhereNull('allegato_pdf_hash')
->orWhere('allegato_pdf_hash', '');
});
} else {
// Include anche i PDF "generati" (non allegati originali), per rigenerazione con XSL Assosoftware.
$q->where(function (Builder $qq) {
$qq->whereNull('allegato_pdf_hash')->orWhere('allegato_pdf_hash', '');
});
}
$rows = $q
->orderBy('id')
->limit($limit)
->get(['id', 'allegato_pdf_path', 'allegato_pdf_hash', 'xml_path', 'xml_content', 'stabile_id']);
if ($rows->isEmpty()) {
Notification::make()->title('Nessuna fattura da rigenerare')->warning()->send();
return;
}
$svc = app(FatturaElettronicaProtocolloService::class);
$ok = 0;
$skipped = 0;
$errors = 0;
$errorLines = [];
foreach ($rows as $fattura) {
try {
$hasOriginalAttachment = is_string($fattura->allegato_pdf_hash ?? null)
&& trim((string) $fattura->allegato_pdf_hash) !== '';
if ($hasOriginalAttachment) {
$skipped++;
continue;
}
$pdfPath = is_string($fattura->allegato_pdf_path ?? null) ? trim((string) $fattura->allegato_pdf_path) : '';
if ($pdfPath !== '' && Storage::disk('local')->exists($pdfPath)) {
$oldDir = trim((string) (dirname($pdfPath))) . '/__OLD';
$oldPath = $oldDir . '/' . now()->format('Ymd-His') . '-' . basename($pdfPath);
try {
Storage::disk('local')->makeDirectory($oldDir);
Storage::disk('local')->move($pdfPath, $oldPath);
} catch (\Throwable) {
// ignore: se non riesce a spostare, rigeneriamo comunque (il service eviterà overwrite)
}
}
$fattura->allegato_pdf_path = null;
$fattura->allegato_pdf_nome = null;
$fattura->save();
$svc->ensureDocumentoProtocollato($fattura, (int) $user->id);
$ok++;
} catch (\Throwable $e) {
$errors++;
if (count($errorLines) < 10) {
$errorLines[] = $this->safeUtf8('#' . (int) $fattura->id . ' — ' . $e->getMessage());
}
}
}
$body = 'Selezionate: ' . (int) $rows->count();
$body .= ' · Rigenerate: ' . (int) $ok;
$body .= ' · Saltate (PDF allegato): ' . (int) $skipped;
$body .= ' · Errori: ' . (int) $errors;
$n = Notification::make()->title('Rigenerazione completata');
if ($errors > 0) {
$n->body($this->safeUtf8($body . "\n" . implode("\n", $errorLines)))->warning()->send();
} else {
$n->body($this->safeUtf8($body))->success()->send();
}
}),
Action::make('importa_zip_cassetto')
->label('Importa ZIP Cassetto')
->icon('heroicon-o-arrow-up-tray')
->visible(function (): bool {
$u = Auth::user();
return $u instanceof User && $u->hasAnyRole(['super-admin', 'admin', 'amministratore']);
})
->form([
FileUpload::make('zip_file')
->label('ZIP del Cassetto (già scaricato)')
->directory(function (): string {
$user = Auth::user();
if (! $user instanceof User) {
return 'tmp/cassetto-zip';
}
$stabile = StabileContext::getActiveStabile($user);
if (! $stabile) {
return 'tmp/cassetto-zip';
}
$adminCode = $stabile?->amministratore?->codice_amministratore;
$adminId = (int) ($stabile?->amministratore_id ?: 0);
$adminFolder = $adminCode ?: ('ID-' . $adminId);
return 'amministratori/' . $adminFolder . '/temp/upload/cassetto-zip';
})
->disk('local')
->acceptedFileTypes(['application/zip', 'application/x-zip-compressed'])
->required(),
Toggle::make('force_update')
->label('Reimport / completa anche se già presente')
->default(false),
Toggle::make('no_skip')
->label('Non saltare se già importato (no_skip)')
->default(false),
Toggle::make('metadati')
->label('Considera anche metadati')
->default(false),
Select::make('importa_righe')
->label('Import righe')
->options([
'auto' => 'Auto (segue impostazione fornitore)',
'si' => 'Sì (forza import righe)',
'no' => 'No (non importare righe)',
])
->default('auto')
->required(),
Toggle::make('create_fornitore_if_missing')
->label('Crea fornitore se non esiste')
->default(true),
])
->action(function (array $data): void {
$user = Auth::user();
if (! $user instanceof User) {
Notification::make()->title('Utente non valido')->danger()->send();
return;
}
$stabile = StabileContext::getActiveStabile($user);
if (! $stabile instanceof Stabile) {
Notification::make()->title('Seleziona uno stabile')->danger()->send();
return;
}
$zipPath = (string) ($data['zip_file'] ?? '');
if ($zipPath === '') {
Notification::make()->title('ZIP mancante')->danger()->send();
return;
}
$absolute = Storage::disk('local')->path($zipPath);
if (! is_file($absolute)) {
Notification::make()->title('ZIP non trovato')->danger()->send();
return;
}
$exit = Artisan::call('fe:cassetto-import-local', [
'stabile_id' => (int) $stabile->id,
'path' => $absolute,
'--user_id' => (int) $user->id,
'--metadati' => (bool) ($data['metadati'] ?? false) ? 1 : 0,
'--force_update' => (bool) ($data['force_update'] ?? false) ? 1 : 0,
'--no_skip' => (bool) ($data['no_skip'] ?? false) ? 1 : 0,
'--importa_righe' => (string) ($data['importa_righe'] ?? 'auto'),
'--create_fornitore_if_missing' => (bool) ($data['create_fornitore_if_missing'] ?? true) ? 1 : 0,
]);
$out = trim((string) Artisan::output());
$lines = $out === '' ? [] : preg_split('/\r\n|\r|\n/', $out);
$tail = $lines ? implode("\n", array_slice($lines, -10)) : '';
if ($exit !== 0) {
Notification::make()
->title('Import ZIP fallito')
->body($this->safeUtf8($tail !== '' ? $tail : 'Comando terminato con errore'))
->danger()
->send();
return;
}
Notification::make()
->title('Import ZIP completato')
->body($this->safeUtf8($tail !== '' ? $tail : 'Controlla la tab “Scarichi” per log/stato trimestri.'))
->success()
->send();
}),
Action::make('scarica_cassetto')
->label('Scarica da Cassetto Fiscale')
->icon('heroicon-o-arrow-down-tray')
->visible(function (): bool {
$u = Auth::user();
return $u instanceof User && $u->hasAnyRole(['super-admin', 'admin', 'amministratore']);
})
->form([
Placeholder::make('stabile_attivo_info')
->label('Stabile di destinazione')
->content(fn(): string => $this->getActiveStabileDestinationSummaryProperty()),
Select::make('anno')
->label('Anno')
->options(function (): array {
$y = (int) now()->format('Y');
$out = [];
for ($i = 0; $i < 10; $i++) {
$out[(string) ($y - $i)] = (string) ($y - $i);
}
return $out;
})
->default(fn() => now()->format('Y'))
->reactive(),
Select::make('periodo')
->label('Periodo suggerito')
->options([
'Q1' => '01/01 - 31/03',
'Q2' => '01/04 - 30/06',
'Q3' => '01/07 - 30/09',
'Q4' => '01/10 - 31/12',
'M_CUR' => 'Mese corrente',
'D1' => 'Solo oggi (1 giorno)',
'CUSTOM' => 'Personalizzato (manuale)',
])
->default('Q1')
->reactive()
->afterStateUpdated(function ($state, callable $set, callable $get): void {
$anno = (string) ($get('anno') ?? now()->format('Y'));
$q = (string) ($state ?? 'Q1');
[$dal, $al] = match ($q) {
'Q2' => ['01-04-' . $anno, '30-06-' . $anno],
'Q3' => ['01-07-' . $anno, '30-09-' . $anno],
'Q4' => ['01-10-' . $anno, '31-12-' . $anno],
'M_CUR' => [now()->startOfMonth()->format('d-m-Y'), now()->endOfMonth()->format('d-m-Y')],
'D1' => [now()->format('d-m-Y'), now()->format('d-m-Y')],
'CUSTOM' => [
(string) ($get('dal') ?? now()->format('d-m-Y')),
(string) ($get('al') ?? now()->format('d-m-Y')),
],
default => ['01-01-' . $anno, '31-03-' . $anno],
};
$set('dal', $dal);
$set('al', $al);
}),
TextInput::make('dal')
->label('Dal (gg-mm-aaaa)')
->default(fn(callable $get) => '01-01-' . (string) ($get('anno') ?? now()->format('Y')))
->helperText('Puoi inserire anche un solo giorno (dal = al).')
->required(),
TextInput::make('al')
->label('Al (gg-mm-aaaa)')
->default(fn(callable $get) => '31-03-' . (string) ($get('anno') ?? now()->format('Y')))
->helperText('Intervallo massimo 92 giorni.')
->required(),
Toggle::make('metadati')
->label('Scarica anche metadati')
->default(false),
Toggle::make('force_update')
->label('Reimport / completa anche se già presente')
->default(false),
Toggle::make('no_skip')
->label('Non saltare se lo stesso periodo ha già un log OK')
->default(false),
Select::make('importa_righe')
->label('Import righe')
->options([
'auto' => 'Auto (segue impostazione fornitore)',
'si' => 'Sì (forza import righe)',
'no' => 'No (non importare righe)',
])
->default('auto')
->required(),
Toggle::make('create_fornitore_if_missing')
->label('Crea fornitore se non esiste')
->default(true),
])
->action(function (array $data): void {
$user = Auth::user();
if (! $user instanceof User) {
Notification::make()->title('Utente non valido')->danger()->send();
return;
}
$stabile = StabileContext::getActiveStabile($user);
if (! $stabile instanceof Stabile) {
Notification::make()->title('Seleziona uno stabile')->danger()->send();
return;
}
if ($this->isLikelyTechnicalFeShell($stabile)) {
$this->notifyTechnicalFeShellBlocked();
return;
}
$amministratore = $stabile->amministratore;
if (! $amministratore) {
Notification::make()->title('Amministratore non trovato')->danger()->send();
return;
}
$dalStr = (string) ($data['dal'] ?? '');
$alStr = (string) ($data['al'] ?? '');
$dal = \DateTimeImmutable::createFromFormat('d-m-Y', $dalStr) ?: null;
$al = \DateTimeImmutable::createFromFormat('d-m-Y', $alStr) ?: null;
if (! $dal || ! $al) {
Notification::make()->title('Date non valide')->danger()->send();
return;
}
$forceUpdate = (bool) ($data['force_update'] ?? false);
$noSkip = (bool) ($data['no_skip'] ?? false);
$allowRerun = $forceUpdate || $noSkip;
// Blocco trimestri: se il trimestre è già completo, evita ri-scarico (salvo force_update).
$periodo = (string) ($data['periodo'] ?? '');
$anno = is_numeric($data['anno'] ?? null) ? (int) $data['anno'] : (int) $dal->format('Y');
if (! $allowRerun && in_array($periodo, ['Q1', 'Q2', 'Q3', 'Q4'], true)) {
[$expectedDal, $expectedAl] = match ($periodo) {
'Q2' => [$anno . '-04-01', $anno . '-06-30'],
'Q3' => [$anno . '-07-01', $anno . '-09-30'],
'Q4' => [$anno . '-10-01', $anno . '-12-31'],
default => [$anno . '-01-01', $anno . '-03-31'],
};
if ($dal->format('Y-m-d') === $expectedDal && $al->format('Y-m-d') === $expectedAl) {
if ($this->isCassettoQuarterCompleteFor((int) $amministratore->id, (int) $stabile->id, $expectedDal, $expectedAl)) {
Notification::make()
->title('Trimestre già completo')
->body('Per questo trimestre risulta uno scarico OK completo. Per forzare, abilita “Reimport / completa anche se già presente”.')
->warning()
->send();
return;
}
}
}
// Anti-duplicati veloce: se esiste già un OK per lo stesso periodo, non avviamo niente.
if (! $allowRerun) {
$cf = trim((string) ($stabile->codice_fiscale ?? ''));
if ($cf === '') {
try {
$stabile->loadMissing('rubrica');
$cf = trim((string) ($stabile->rubrica?->codice_fiscale ?? ''));
} catch (\Throwable) {
// ignore
}
}
$cf = strtoupper(trim($cf));
if ($cf !== '') {
$requestHash = hash('sha256', implode('|', [
'cf=' . $cf,
'dal=' . $dal->format('d-m-Y'),
'al=' . $al->format('d-m-Y'),
'metadati=' . (((bool) ($data['metadati'] ?? false)) ? '1' : '0'),
'stabile=' . (int) $stabile->id,
'amm=' . (int) $amministratore->id,
]));
$existingOk = DB::table('fe_cassetto_download_logs')
->where('amministratore_id', (int) $amministratore->id)
->where('stabile_id', (int) $stabile->id)
->where('request_hash', $requestHash)
->where('status', 'ok')
->orderByDesc('id')
->first();
if ($existingOk) {
Notification::make()
->title('Già scaricato')
->body('Esiste già uno scarico OK per questo periodo (log #' . (int) $existingOk->id . ').')
->warning()
->send();
return;
}
}
}
// Max 3 mesi (circa 92 giorni) per richiesta
$diffDays = (int) $dal->diff($al)->format('%r%a');
if ($diffDays < 0) {
Notification::make()->title('Intervallo date non valido')->danger()->send();
return;
}
if ($diffDays > 92) {
Notification::make()->title('Intervallo troppo ampio (max 3 mesi)')->danger()->send();
return;
}
// Esecuzione "in background" senza dipendere da un queue worker:
// registriamo un callback di terminazione che parte dopo l'invio della risposta HTTP.
app()->terminating(function () use ($amministratore, $stabile, $dal, $al, $data, $user): void {
try {
$job = new RunCassettoFiscaleDownload(
(int) $amministratore->id,
(int) $stabile->id,
$dal->format('Y-m-d'),
$al->format('Y-m-d'),
(bool) ($data['metadati'] ?? false),
(int) $user->id,
[
'force_update' => (bool) ($data['force_update'] ?? false),
'no_skip' => (bool) ($data['no_skip'] ?? false),
'importa_righe' => (string) ($data['importa_righe'] ?? 'auto'),
'create_fornitore_if_missing' => (bool) ($data['create_fornitore_if_missing'] ?? true),
],
);
$job->handle(app(CassettoFiscaleDownloadService::class));
} catch (\Throwable $e) {
report($e);
}
});
Notification::make()
->title('Scarico avviato')
->body("Operazione avviata in background. Se hai abilitato 'Reimport / completa' o 'Non saltare', lo stesso periodo verrà rieseguito.\nAggiorna tra poco e controlla la sezione 'Ultimi scarichi'.")
->success()
->send();
}),
Action::make('importa_fe')
->label('Importa FE')
->icon('heroicon-o-arrow-up-tray')
->form([
FileUpload::make('fe_files')
->label('File FE (XML/P7M/ZIP)')
->directory(function (): string {
$user = Auth::user();
if (! $user instanceof User) {
return 'tmp/fatture-fe';
}
$stabile = StabileContext::getActiveStabile($user);
if (! $stabile) {
return 'tmp/fatture-fe';
}
$adminCode = $stabile?->amministratore?->codice_amministratore;
$adminId = (int) ($stabile?->amministratore_id ?: 0);
$adminFolder = $adminCode ?: ('ID-' . $adminId);
return 'amministratori/' . $adminFolder . '/temp/upload/fatture-fe';
})
->disk('local')
->acceptedFileTypes([
'.p7m',
'.xml',
'.zip',
'application/xml',
'text/xml',
'application/pkcs7-mime',
'application/x-pkcs7-mime',
'application/pkcs7-signature',
'application/x-pkcs7-signature',
'application/octet-stream',
'application/zip',
'application/x-zip-compressed',
])
->multiple()
->required(),
Select::make('stabile_forzato_id')
->label('Destinazione stabile (solo super-admin)')
->helperText('Se valorizzato, ignora lo smistamento automatico (CF/SDI/PEC).')
->options(function () {
$user = Auth::user();
if (! $user instanceof User) {
return [];
}
return StabileContext::accessibleStabili($user)
->mapWithKeys(function (Stabile $s) {
$adminCode = (string) ($s->amministratore?->codice_amministratore ?: $s->amministratore?->codice_univoco ?: '');
$prefix = $adminCode !== '' ? ($adminCode . ' — ') : '';
return [(string) $s->id => $prefix . $s->codice_stabile . ' - ' . $s->denominazione];
})
->all();
})
->searchable()
->nullable()
->visible(function (): bool {
$u = Auth::user();
return $u instanceof User && $u->hasAnyRole(['super-admin', 'admin']);
}),
Toggle::make('force_update')
->label('Reimport / completa anche se già presente')
->default(false),
Select::make('importa_righe')
->label('Import righe')
->options([
'auto' => 'Auto (segue impostazione fornitore)',
'si' => 'Sì (forza import righe)',
'no' => 'No (non importare righe)',
])
->default('auto')
->required(),
Toggle::make('create_fornitore_if_missing')
->label('Crea fornitore se non esiste')
->helperText('Se il fornitore non viene trovato (P.IVA/CF), crea una nuova anagrafica fornitore collegata allo stabile.')
->default(true),
])
->action(function (array $data): void {
$user = Auth::user();
if (! $user instanceof User) {
Notification::make()->title('Utente non valido')->danger()->send();
return;
}
$forcedStabileId = (int) ($data['stabile_forzato_id'] ?? 0);
$canForce = $forcedStabileId > 0 && $user->hasAnyRole(['super-admin', 'admin']);
if ($canForce && ! Stabile::query()->attivi()->whereKey($forcedStabileId)->exists()) {
Notification::make()->title('Stabile forzato non valido')->danger()->send();
return;
}
$paths = $data['fe_files'] ?? [];
$paths = is_array($paths) ? $paths : [$paths];
$importer = new FatturaElettronicaImporter(new FatturaElettronicaXmlParser());
$extractor = app(P7mExtractor::class);
$parser = new FatturaElettronicaXmlParser();
$accessibleStabili = StabileContext::accessibleStabili($user)->pluck('id')->map(fn($v) => (int) $v)->all();
$accessibleMap = array_fill_keys($accessibleStabili, true);
$imported = 0;
$duplicates = 0;
$errors = 0;
$duplicateDetails = [];
$errorDetails = [];
$debugLines = [];
$debugPath = null;
$debugYm = now()->format('Y/m');
$debugPath = 'fe-import-debug/' . $debugYm . '/' . (string) Str::uuid() . '.log';
foreach ($paths as $path) {
$permanentP7mPath = null;
$permanentXmlPath = null;
try {
$path = (string) $path;
$filename = basename($path);
$lower = Str::lower($filename);
// ZIP: estrai e importa tutti gli XML/P7M contenuti
if (Str::endsWith($lower, '.zip')) {
$absZip = Storage::disk('local')->path($path);
if (! is_file($absZip)) {
throw new \RuntimeException('ZIP non trovato');
}
$tmpDir = 'tmp/fe-extract/' . now()->format('Y/m') . '/' . (string) Str::uuid();
Storage::disk('local')->makeDirectory($tmpDir);
$absTmp = Storage::disk('local')->path($tmpDir);
$zip = new ZipArchive();
$open = $zip->open($absZip);
if ($open !== true) {
throw new \RuntimeException('ZIP non valido');
}
$zip->extractTo($absTmp);
$zip->close();
$it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($absTmp));
foreach ($it as $fileInfo) {
if (! $fileInfo instanceof \SplFileInfo || ! $fileInfo->isFile()) {
continue;
}
$innerName = (string) $fileInfo->getFilename();
$innerLower = Str::lower($innerName);
if (! (Str::endsWith($innerLower, '.xml') || Str::endsWith($innerLower, '.p7m'))) {
continue;
}
$bytes = file_get_contents($fileInfo->getPathname());
if (! is_string($bytes) || $bytes === '') {
$errors++;
$errorDetails[] = $filename . '::' . $innerName . ' — file vuoto';
continue;
}
$isP7m = Str::endsWith($innerLower, '.p7m');
$xml = $isP7m
? $extractor->extractXmlFromP7m($fileInfo->getPathname())
: $bytes;
if (! is_string($xml) || trim($xml) === '') {
$errors++;
$errorDetails[] = $filename . '::' . $innerName . ' — XML non valido/assente';
continue;
}
$parsed = $parser->parse($xml);
$stabileId = $canForce ? $forcedStabileId : $this->resolveStabileIdFromParsed($parsed);
if (! $stabileId) {
$errors++;
$errorDetails[] = $filename . '::' . $innerName . ' — impossibile determinare lo stabile dalla FE';
continue;
}
if (! isset($accessibleMap[$stabileId])) {
$errors++;
$errorDetails[] = $filename . '::' . $innerName . ' — stabile non accessibile per lutente';
continue;
}
$unique = (string) Str::uuid();
$ym = now()->format('Y/m');
$inbox = $this->inboxBaseForStabile($stabileId, $ym);
$permanentP7mPath = null;
if ($isP7m) {
$permanentP7mPath = $inbox . '/' . $unique . '-' . $innerName;
Storage::disk('local')->put($permanentP7mPath, $bytes);
}
$xmlFilename = $isP7m
? (pathinfo($innerName, PATHINFO_FILENAME) . '.xml')
: $innerName;
$permanentXmlPath = $inbox . '/' . $unique . '-' . $xmlFilename;
Storage::disk('local')->put($permanentXmlPath, $xml);
$result = $importer->importXml($xml, 0, $xmlFilename, [
'xml_path' => $permanentXmlPath,
'p7m_path' => $permanentP7mPath,
'nome_file_p7m' => $isP7m ? $innerName : null,
'force_update' => (bool) ($data['force_update'] ?? false),
'auto_repair_duplicate' => true,
'importa_righe' => (string) ($data['importa_righe'] ?? 'auto'),
'create_fornitore_if_missing' => (bool) ($data['create_fornitore_if_missing'] ?? false),
'allow_fallback_stabile' => false,
'force_stabile_id' => $canForce ? $forcedStabileId : 0,
'user_id' => (int) $user->id,
]);
try {
$status = (string) ($result['status'] ?? 'unknown');
$msg = (string) ($result['message'] ?? '');
$dbg = $result['debug'] ?? null;
$debugLines[] = now()->format('c') . ' | ' . $status . ' | ' . $filename . '::' . $innerName
. ($msg !== '' ? ' | ' . $msg : '')
. (is_array($dbg) ? ' | debug=' . json_encode($dbg, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : '');
} catch (\Throwable) {
// ignore
}
if (($result['status'] ?? null) === 'imported') {
$imported++;
} elseif (($result['status'] ?? null) === 'duplicate') {
$duplicates++;
$duplicateDetails[] = $filename . '::' . $innerName . (($result['message'] ?? null) ? ' — ' . (string) $result['message'] : '');
try {
if ($permanentXmlPath) {
Storage::disk('local')->delete($permanentXmlPath);
}
if ($permanentP7mPath) {
Storage::disk('local')->delete($permanentP7mPath);
}
} catch (\Throwable) {
// ignore
}
} else {
$errors++;
$errorDetails[] = $filename . '::' . $innerName . (($result['message'] ?? null) ? ' — ' . (string) $result['message'] : '');
try {
if ($permanentXmlPath) {
Storage::disk('local')->delete($permanentXmlPath);
}
if ($permanentP7mPath) {
Storage::disk('local')->delete($permanentP7mPath);
}
} catch (\Throwable) {
// ignore
}
}
}
try {
Storage::disk('local')->deleteDirectory($tmpDir);
} catch (\Throwable) {
// ignore
}
continue;
}
$bytes = Storage::disk('local')->get($path);
if (! is_string($bytes) || $bytes === '') {
throw new \RuntimeException('File vuoto');
}
$isP7m = Str::endsWith($lower, '.p7m');
$xml = $isP7m
? $extractor->extractXmlFromP7m(Storage::disk('local')->path($path))
: $bytes;
if (! is_string($xml) || trim($xml) === '') {
throw new \RuntimeException('XML non valido/assente');
}
// Smistamento obbligatorio: deve risolvere a uno stabile univoco e accessibile.
$parsed = $parser->parse($xml);
$stabileId = $canForce ? $forcedStabileId : $this->resolveStabileIdFromParsed($parsed);
if (! $stabileId) {
throw new \RuntimeException('Impossibile determinare lo stabile dalla FE (CF/SDI/PEC non trovati o non univoci)');
}
if (! isset($accessibleMap[$stabileId])) {
throw new \RuntimeException('Stabile non accessibile per lutente');
}
$unique = (string) Str::uuid();
$ym = now()->format('Y/m');
$inbox = $this->inboxBaseForStabile($stabileId, $ym);
if ($isP7m) {
$permanentP7mPath = $inbox . '/' . $unique . '-' . $filename;
Storage::disk('local')->put($permanentP7mPath, $bytes);
}
$xmlFilename = $isP7m
? (pathinfo($filename, PATHINFO_FILENAME) . '.xml')
: $filename;
$permanentXmlPath = $inbox . '/' . $unique . '-' . $xmlFilename;
Storage::disk('local')->put($permanentXmlPath, $xml);
$result = $importer->importXml($xml, 0, $xmlFilename, [
'xml_path' => $permanentXmlPath,
'p7m_path' => $permanentP7mPath,
'nome_file_p7m' => $isP7m ? $filename : null,
'force_update' => (bool) ($data['force_update'] ?? false),
'auto_repair_duplicate' => true,
'importa_righe' => (string) ($data['importa_righe'] ?? 'auto'),
'create_fornitore_if_missing' => (bool) ($data['create_fornitore_if_missing'] ?? false),
'allow_fallback_stabile' => false,
'force_stabile_id' => $canForce ? $forcedStabileId : 0,
'user_id' => (int) $user->id,
]);
try {
$status = (string) ($result['status'] ?? 'unknown');
$msg = (string) ($result['message'] ?? '');
$dbg = $result['debug'] ?? null;
$debugLines[] = now()->format('c') . ' | ' . $status . ' | ' . $filename
. ($msg !== '' ? ' | ' . $msg : '')
. (is_array($dbg) ? ' | debug=' . json_encode($dbg, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : '');
} catch (\Throwable) {
// ignore
}
if (($result['status'] ?? null) === 'imported') {
$imported++;
} elseif (($result['status'] ?? null) === 'duplicate') {
$duplicates++;
$duplicateDetails[] = $this->safeUtf8($filename . (($result['message'] ?? null) ? ' — ' . (string) $result['message'] : ''));
try {
if ($permanentXmlPath) {
Storage::disk('local')->delete($permanentXmlPath);
}
if ($permanentP7mPath) {
Storage::disk('local')->delete($permanentP7mPath);
}
} catch (\Throwable) {
// ignore
}
} else {
$errors++;
$errorDetails[] = $this->safeUtf8($filename . (($result['message'] ?? null) ? ' — ' . (string) $result['message'] : ''));
try {
if ($permanentXmlPath) {
Storage::disk('local')->delete($permanentXmlPath);
}
if ($permanentP7mPath) {
Storage::disk('local')->delete($permanentP7mPath);
}
} catch (\Throwable) {
// ignore
}
}
} catch (\Throwable $e) {
$errors++;
$filename = basename((string) $path);
$debugNote = '';
try {
$lower = Str::lower($filename);
if (Str::endsWith($lower, '.p7m') && isset($bytes) && is_string($bytes) && $bytes !== '') {
$debugDir = 'fe-import-errors/' . now()->format('Y/m');
$debugPath = $debugDir . '/' . (string) Str::uuid() . '-' . $filename;
Storage::disk('local')->put($debugPath, $bytes);
$debugNote = ' (salvato per debug: ' . $debugPath . ')';
}
} catch (\Throwable) {
// ignore
}
$errorDetails[] = $this->safeUtf8($filename . ' — ' . $e->getMessage() . $debugNote);
try {
$debugLines[] = $this->safeUtf8(now()->format('c') . ' | error | ' . $filename . ' | ' . $e->getMessage() . $debugNote);
} catch (\Throwable) {
// ignore
}
try {
if ($permanentXmlPath) {
Storage::disk('local')->delete($permanentXmlPath);
}
if ($permanentP7mPath) {
Storage::disk('local')->delete($permanentP7mPath);
}
} catch (\Throwable) {
// ignore
}
} finally {
try {
Storage::disk('local')->delete($path);
} catch (\Throwable) {
// ignore
}
}
}
$logNote = '';
if (($duplicates > 0 || $errors > 0) && $debugPath && count($debugLines) > 0) {
try {
Storage::disk('local')->put($debugPath, implode("\n", $debugLines) . "\n");
$logNote = "\nLog diagnostica: {$debugPath}";
} catch (\Throwable) {
// ignore
}
}
Notification::make()
->title('Import completato')
->body($this->safeUtf8("Importate: {$imported} · Duplicate: {$duplicates} · Errori: {$errors}" . $logNote))
->success()
->send();
if ($duplicates > 0 && count($duplicateDetails) > 0) {
Notification::make()
->title('Duplicati (non importati)')
->body($this->safeUtf8(implode("\n", array_slice($duplicateDetails, 0, 10)) . (count($duplicateDetails) > 10 ? "\n" : '')))
->warning()
->send();
}
if ($errors > 0 && count($errorDetails) > 0) {
Notification::make()
->title('Errori durante limport')
->body($this->safeUtf8(implode("\n", array_slice($errorDetails, 0, 10)) . (count($errorDetails) > 10 ? "\n" : '')))
->danger()
->send();
}
}),
Action::make('importa_ade_csv')
->label('Importa CSV AdE')
->icon('heroicon-o-clipboard-document-list')
->form([
FileUpload::make('csv_file')
->label('CSV AdE (separatore ; )')
->directory(function (): string {
$user = Auth::user();
if (! $user instanceof User) {
return 'tmp/ade-csv';
}
$stabile = StabileContext::getActiveStabile($user);
if (! $stabile) {
return 'tmp/ade-csv';
}
$adminCode = $stabile?->amministratore?->codice_amministratore;
$adminId = (int) ($stabile?->amministratore_id ?: 0);
$adminFolder = $adminCode ?: ('ID-' . $adminId);
return 'amministratori/' . $adminFolder . '/temp/upload/ade-csv';
})
->disk('local')
->acceptedFileTypes([
'text/csv',
'text/plain',
'application/vnd.ms-excel',
])
->required(),
Select::make('stabile_target_id')
->label('Target stabile (solo super-admin)')
->helperText('Se non valorizzato, usa lo stabile attivo.')
->options(function () {
$user = Auth::user();
if (! $user instanceof User) {
return [];
}
return StabileContext::accessibleStabili($user)
->mapWithKeys(function (Stabile $s) {
$adminCode = (string) ($s->amministratore?->codice_amministratore ?: $s->amministratore?->codice_univoco ?: '');
$prefix = $adminCode !== '' ? ($adminCode . ' — ') : '';
return [(string) $s->id => $prefix . $s->codice_stabile . ' - ' . $s->denominazione];
})
->all();
})
->searchable()
->nullable()
->visible(function (): bool {
$u = Auth::user();
return $u instanceof User && $u->hasAnyRole(['super-admin', 'admin']);
}),
TextInput::make('cartella_legacy')
->label('Cartella legacy (opzionale)')
->helperText('Esempio: 0021 (solo metadato, non influenza il matching).')
->maxLength(10)
->nullable(),
])
->action(function (array $data): void {
$user = Auth::user();
if (! $user instanceof User) {
Notification::make()->title('Utente non valido')->danger()->send();
return;
}
$activeStabileId = StabileContext::resolveActiveStabileId($user);
if (! $activeStabileId) {
Notification::make()->title('Stabile attivo non impostato')->danger()->send();
return;
}
$forcedStabileId = (int) ($data['stabile_target_id'] ?? 0);
$canForce = $forcedStabileId > 0 && $user->hasAnyRole(['super-admin', 'admin']);
$targetStabileId = $canForce ? $forcedStabileId : (int) $activeStabileId;
$accessibleStabili = StabileContext::accessibleStabili($user)->pluck('id')->map(fn($v) => (int) $v)->all();
$accessibleMap = array_fill_keys($accessibleStabili, true);
if (! isset($accessibleMap[$targetStabileId])) {
Notification::make()->title('Stabile non accessibile per lutente')->danger()->send();
return;
}
$stabile = Stabile::query()->select(['id', 'amministratore_id'])->find($targetStabileId);
$amministratoreId = (int) ($stabile?->amministratore_id ?: 0);
if ($amministratoreId <= 0) {
Notification::make()->title('Amministratore non valido')->danger()->send();
return;
}
$csvPath = (string) ($data['csv_file'] ?? '');
if ($csvPath === '') {
Notification::make()->title('CSV mancante')->danger()->send();
return;
}
$absolute = Storage::disk('local')->path($csvPath);
$cartella = isset($data['cartella_legacy']) ? trim((string) $data['cartella_legacy']) : '';
Artisan::call('gescon:import-fatture-ade-csv', array_filter([
'amministratore_id' => $amministratoreId,
'--stabile_id' => $targetStabileId,
'--cartella' => $cartella !== '' ? $cartella : null,
'--path' => $absolute,
], fn($v) => $v !== null && $v !== ''));
$out = trim((string) Artisan::output());
$lines = $out === '' ? [] : preg_split('/\r\n|\r|\n/', $out);
$tail = $lines ? implode("\n", array_slice($lines, -8)) : 'Import completato.';
Notification::make()
->title('Import AdE completato')
->body($this->safeUtf8($tail))
->success()
->send();
}),
];
}
private function inboxBaseForStabile(int $stabileId, string $ym): string
{
$stabile = Stabile::query()
->with(['amministratore:id,codice_amministratore,codice_univoco'])
->select(['id', 'amministratore_id', 'codice_stabile', 'codice_univoco'])
->find($stabileId);
if ($stabile) {
$base = ArchivioPaths::stabileBase($stabile, $stabile->amministratore);
if (is_string($base) && $base !== '') {
return $base . '/fatture-ricevute/inbox/' . $ym;
}
}
$adminId = (int) ($stabile?->amministratore_id ?: 0);
return 'amministratori/ID-' . $adminId . '/stabili/ID-' . (int) $stabileId . '/fatture-ricevute/inbox/' . $ym;
}
private function resolveStabileIdFromParsed(array $parsed): ?int
{
$cf = isset($parsed['destinatario_cf']) ? strtoupper(str_replace(' ', '', trim((string) $parsed['destinatario_cf']))) : null;
$piva = isset($parsed['destinatario_piva']) ? strtoupper(str_replace(' ', '', trim((string) $parsed['destinatario_piva']))) : null;
$codice = isset($parsed['codice_destinatario']) ? strtoupper(str_replace(' ', '', trim((string) $parsed['codice_destinatario']))) : null;
$pec = $parsed['pec_destinatario'] ?? null;
if ($cf) {
$match = Stabile::query()
->whereNotNull('codice_fiscale')
->whereRaw("REPLACE(UPPER(codice_fiscale), ' ', '') = ?", [$cf])
->where('attivo', true)
->get();
if ($match->count() === 1) {
return (int) $match->first()->id;
}
}
if ($codice) {
$match = Stabile::query()
->whereNotNull('codice_destinatario_sdi')
->whereRaw("REPLACE(UPPER(codice_destinatario_sdi), ' ', '') = ?", [$codice])
->where('attivo', true)
->get();
if ($match->count() === 1) {
return (int) $match->first()->id;
}
}
if ($pec) {
$pec = trim(strtolower((string) $pec));
$match = Stabile::query()
->where(function ($q) use ($pec) {
$q->whereRaw('LOWER(pec_condominio) = ?', [$pec])
->orWhereRaw('LOWER(pec_amministratore) = ?', [$pec]);
})
->where('attivo', true)
->get();
if ($match->count() === 1) {
return (int) $match->first()->id;
}
}
if ($piva) {
$match = Stabile::query()
->select('stabili.id')
->join('amministratori', 'amministratori.id', '=', 'stabili.amministratore_id')
->where('stabili.attivo', true)
->whereRaw("REPLACE(UPPER(amministratori.partita_iva), ' ', '') = ?", [$piva])
->get();
if ($match->count() === 1) {
return (int) $match->first()->id;
}
}
return null;
}
private function archiveBaseForStabile(int $stabileId): string
{
$stabile = Stabile::query()
->with(['amministratore:id,codice_amministratore,codice_univoco'])
->select(['id', 'amministratore_id', 'codice_stabile', 'codice_univoco'])
->find($stabileId);
$ym = now()->format('Y/m');
if ($stabile) {
$base = ArchivioPaths::stabileBase($stabile, $stabile->amministratore);
if (is_string($base) && $base !== '') {
return $base . '/fatture-ricevute/' . $ym;
}
}
$adminId = (int) ($stabile?->amministratore_id ?: 0);
return 'amministratori/ID-' . $adminId . '/stabili/ID-' . (int) $stabileId . '/fatture-ricevute/' . $ym;
}
protected function getTableQuery(): Builder
{
$user = Auth::user();
if (! $user instanceof User) {
return FatturaElettronica::query()->whereRaw('1 = 0');
}
$activeStabileId = StabileContext::resolveActiveStabileId($user);
if (! $activeStabileId) {
return FatturaElettronica::query()->whereRaw('1 = 0');
}
return FatturaElettronica::query()
->where('stabile_id', $activeStabileId);
}
public function table(Table $table): Table
{
return $table
->striped()
->paginationPageOptions([10, 25, 50, 100])
->defaultPaginationPageOption(50)
->defaultSort('data_fattura', 'desc')
->filters([
SelectFilter::make('anno')
->label('Anno fattura')
->options(function (): array {
$user = Auth::user();
if (! $user instanceof User) {
return [];
}
$activeStabileId = StabileContext::resolveActiveStabileId($user);
if (! $activeStabileId) {
return [];
}
return FatturaElettronica::query()
->where('stabile_id', $activeStabileId)
->whereNotNull('data_fattura')
->selectRaw('YEAR(data_fattura) as anno')
->distinct()
->orderByDesc('anno')
->pluck('anno', 'anno')
->mapWithKeys(fn($anno) => [(string) $anno => (string) $anno])
->all();
})
->query(function (Builder $query, array $data): Builder {
$anno = is_numeric($data['value'] ?? null) ? (int) $data['value'] : 0;
return $anno > 0 ? $query->whereYear('data_fattura', $anno) : $query;
}),
SelectFilter::make('ordine')
->label('Ordine')
->options([
'data_desc' => 'Data (più recenti)',
'data_asc' => 'Data (più vecchie)',
'numero_asc' => 'Numero (A → Z)',
'numero_desc' => 'Numero (Z → A)',
'totale_desc' => 'Totale (più alto)',
'totale_asc' => 'Totale (più basso)',
])
->default('data_desc')
->query(function (Builder $query, array $data): Builder {
$v = (string) ($data['value'] ?? 'data_desc');
return match ($v) {
'data_asc' => $query->reorder('data_fattura', 'asc')->orderBy('id', 'asc'),
'numero_asc' => $query->reorder('numero_fattura', 'asc')->orderBy('id', 'asc'),
'numero_desc' => $query->reorder('numero_fattura', 'desc')->orderBy('id', 'desc'),
'totale_asc' => $query->reorder('totale', 'asc')->orderBy('id', 'asc'),
'totale_desc' => $query->reorder('totale', 'desc')->orderBy('id', 'desc'),
default => $query->reorder('data_fattura', 'desc')->orderBy('id', 'desc'),
};
}),
])
->columns([
TextColumn::make('fornitore_denominazione')
->label('Fornitore')
->searchable()
->wrap(),
TextColumn::make('numero_fattura')
->label('Numero')
->searchable()
->sortable(),
TextColumn::make('data_fattura')
->label('Data')
->date('d/m/Y')
->sortable(),
TextColumn::make('totale')
->label('Totale')
->money('EUR')
->sortable(),
BadgeColumn::make('stato')
->label('Stato')
->colors([
'warning' => 'ricevuta',
'success' => 'pagata',
'primary' => 'contabilizzata',
'danger' => 'rifiutata',
]),
TextColumn::make('nome_file_p7m')
->label('P7M')
->toggleable(isToggledHiddenByDefault: true)
->wrap(),
TextColumn::make('destinatario_cf')
->label('Identificativo destinatario')
->formatStateUsing(fn(FatturaElettronica $record): string => trim((string) ($record->destinatario_cf ?: $record->destinatario_piva ?: '—')))
->toggleable(isToggledHiddenByDefault: true)
->wrap(),
])
->actions([
Action::make('apri')
->label('Apri')
->icon('heroicon-o-eye')
->url(fn(FatturaElettronica $record) => FatturaElettronicaScheda::getUrl([
'record' => $record->id,
'back' => request()->getRequestUri(),
], panel: 'admin-filament')),
]);
}
}