1332 lines
63 KiB
PHP
1332 lines
63 KiB
PHP
<?php
|
||
namespace App\Filament\Pages\Gescon;
|
||
|
||
use App\Models\Documento;
|
||
use App\Models\FatturaElettronica;
|
||
use App\Models\Fornitore;
|
||
use App\Models\FornitoreStabileImpostazione;
|
||
use App\Models\RegistroRitenuteAcconto;
|
||
use App\Models\RubricaUniversale;
|
||
use App\Models\Stabile;
|
||
use App\Models\StabileServizio;
|
||
use App\Models\StgFatturaAde;
|
||
use App\Models\User;
|
||
use App\Models\VoceSpesa;
|
||
use App\Modules\Contabilita\Models\FatturaFornitore as ContabilitaFatturaFornitore;
|
||
use App\Modules\Contabilita\Models\PianoConti;
|
||
use App\Services\Consumi\AcquaPdfTextParser;
|
||
use App\Services\Consumi\ConsumiAcquaIngestionService;
|
||
use App\Services\Consumi\ConsumiAcquaTariffeIngestionService;
|
||
use App\Services\Documenti\PdfTextExtractionService;
|
||
use App\Services\FattureElettroniche\FatturaElettronicaProtocolloService;
|
||
use App\Support\StabileContext;
|
||
use Filament\Actions\Action;
|
||
use Filament\Forms\Components\Hidden;
|
||
use Filament\Forms\Components\Repeater;
|
||
use Filament\Forms\Components\Select;
|
||
use Filament\Forms\Components\TextInput;
|
||
use Filament\Forms\Components\Toggle;
|
||
use Filament\Notifications\Notification;
|
||
use Filament\Pages\Page;
|
||
use Illuminate\Support\Facades\Artisan;
|
||
use Illuminate\Support\Facades\Auth;
|
||
use Illuminate\Support\Facades\Schema;
|
||
|
||
class FornitoreScheda extends Page
|
||
{
|
||
protected static ?string $title = 'Scheda fornitore';
|
||
|
||
protected static ?string $slug = 'anagrafica/fornitori/{record}';
|
||
|
||
protected static bool $shouldRegisterNavigation = false;
|
||
|
||
protected string $view = 'filament.pages.gescon.fornitore-scheda';
|
||
|
||
public Fornitore $fornitore;
|
||
|
||
/** @var array<string, mixed> */
|
||
public array $box = [];
|
||
|
||
/** @var array<int, array<string, mixed>> */
|
||
public array $anteprimaAde = [];
|
||
|
||
/** @var array<int, array<string, mixed>> */
|
||
public array $anteprimaFe = [];
|
||
|
||
/** @var array<int, array<string, mixed>> */
|
||
public array $anteprimaContabilita = [];
|
||
|
||
/** @var array<int, array<string, mixed>> */
|
||
public array $fattureAssociate = [];
|
||
|
||
/** @var array<int, array<string, mixed>> */
|
||
public array $raVersateRows = [];
|
||
|
||
public string $tagsInput = '';
|
||
|
||
/** @var array<int, string> */
|
||
public array $tagSuggestions = [];
|
||
|
||
public function cleanDisplayValue(?string $value, string $fallback = '-'): string
|
||
{
|
||
$v = trim((string) $value);
|
||
if ($v === '') {
|
||
return $fallback;
|
||
}
|
||
|
||
$upper = strtoupper($v);
|
||
if (in_array($upper, ['NULL', 'N/D', 'ND', 'N.A.', '-', '--', '[]', '{}'], true)) {
|
||
return $fallback;
|
||
}
|
||
|
||
return $v;
|
||
}
|
||
|
||
public function mount(int | string $record): void
|
||
{
|
||
$user = Auth::user();
|
||
if (! $user instanceof User) {
|
||
abort(403);
|
||
}
|
||
|
||
if (! $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore'])) {
|
||
abort(403);
|
||
}
|
||
|
||
$this->fornitore = Fornitore::query()->with(['rubrica', 'amministratore'])->findOrFail((int) $record);
|
||
|
||
// Tenant guard: impedisce accesso cross-amministratore via URL.
|
||
if (! $user->hasAnyRole(['super-admin', 'admin'])) {
|
||
$allowedAdminIds = [];
|
||
|
||
$activeStabile = StabileContext::getActiveStabile($user);
|
||
$activeAdminId = (int) ($activeStabile?->amministratore_id ?: 0);
|
||
if ($activeAdminId > 0) {
|
||
$allowedAdminIds[] = $activeAdminId;
|
||
}
|
||
|
||
$userAdminId = (int) ($user->amministratore?->id ?: 0);
|
||
if ($userAdminId > 0) {
|
||
$allowedAdminIds[] = $userAdminId;
|
||
}
|
||
|
||
$ownerAdminId = (int) ($user->amministratoreOwner?->id ?: 0);
|
||
if ($ownerAdminId > 0) {
|
||
$allowedAdminIds[] = $ownerAdminId;
|
||
}
|
||
|
||
// Collaboratori: includi anche gli amministratori degli stabili assegnati.
|
||
try {
|
||
$assigned = $user->stabiliAssegnati()->pluck('amministratore_id')->all();
|
||
foreach ($assigned as $aid) {
|
||
$aid = (int) $aid;
|
||
if ($aid > 0) {
|
||
$allowedAdminIds[] = $aid;
|
||
}
|
||
}
|
||
} catch (\Throwable) {
|
||
// ignore
|
||
}
|
||
|
||
$allowedAdminIds = array_values(array_unique(array_filter($allowedAdminIds, fn($v) => (int) $v > 0)));
|
||
$fornitoreAdminId = (int) ($this->fornitore->amministratore_id ?? 0);
|
||
|
||
if ($fornitoreAdminId <= 0 || ! in_array($fornitoreAdminId, $allowedAdminIds, true)) {
|
||
abort(404);
|
||
}
|
||
}
|
||
|
||
$this->hydrateBoxData($user);
|
||
$this->tagsInput = (string) ($this->fornitore->tags ?? '');
|
||
$this->loadTagSuggestions();
|
||
}
|
||
|
||
public function saveTags(): void
|
||
{
|
||
if (! Schema::hasColumn('fornitori', 'tags')) {
|
||
Notification::make()->title('Colonna tags non presente')->warning()->send();
|
||
return;
|
||
}
|
||
|
||
$normalized = $this->normalizeTags($this->tagsInput);
|
||
$this->fornitore->tags = implode(', ', $normalized);
|
||
$this->fornitore->save();
|
||
|
||
$this->tagsInput = (string) ($this->fornitore->tags ?? '');
|
||
$this->loadTagSuggestions();
|
||
|
||
Notification::make()->title('Tag fornitore salvati')->success()->send();
|
||
}
|
||
|
||
public function importLegacyTags(): void
|
||
{
|
||
if (! Schema::hasColumn('fornitori', 'tags')) {
|
||
Notification::make()->title('Colonna tags non presente')->warning()->send();
|
||
return;
|
||
}
|
||
|
||
try {
|
||
Artisan::call('fornitori:import-legacy-tags', [
|
||
'--fornitore-id' => [(int) $this->fornitore->id],
|
||
]);
|
||
} catch (\Throwable $e) {
|
||
Notification::make()->title('Import tag legacy fallito')->body($e->getMessage())->danger()->send();
|
||
return;
|
||
}
|
||
|
||
$this->fornitore->refresh();
|
||
$this->tagsInput = (string) ($this->fornitore->tags ?? '');
|
||
$this->loadTagSuggestions();
|
||
|
||
Notification::make()->title('Tag legacy importati')->success()->send();
|
||
}
|
||
|
||
public function syncRubricaFromFornitore(): void
|
||
{
|
||
$payload = [
|
||
'tipo_contatto' => 'persona_giuridica',
|
||
'categoria' => 'fornitore',
|
||
'stato' => 'attivo',
|
||
'ragione_sociale' => $this->fornitore->ragione_sociale ?: trim(($this->fornitore->nome ?? '') . ' ' . ($this->fornitore->cognome ?? '')),
|
||
'nome' => $this->fornitore->nome,
|
||
'cognome' => $this->fornitore->cognome,
|
||
'partita_iva' => $this->fornitore->partita_iva,
|
||
'codice_fiscale' => $this->fornitore->codice_fiscale,
|
||
'indirizzo' => $this->fornitore->indirizzo,
|
||
'civico' => $this->fornitore->civico,
|
||
'cap' => $this->fornitore->cap,
|
||
'citta' => $this->fornitore->citta,
|
||
'provincia' => $this->fornitore->provincia,
|
||
'nazione' => $this->fornitore->nazione,
|
||
'email' => $this->fornitore->email,
|
||
'pec' => $this->fornitore->pec,
|
||
'telefono_ufficio' => $this->fornitore->telefono,
|
||
'telefono_cellulare' => $this->fornitore->cellulare,
|
||
'sito_web' => $this->fornitore->sito_web,
|
||
'note' => $this->fornitore->note,
|
||
'data_ultima_modifica' => now()->toDateString(),
|
||
];
|
||
|
||
$rubrica = null;
|
||
if ((int) ($this->fornitore->rubrica_id ?? 0) > 0) {
|
||
$rubrica = RubricaUniversale::query()->find((int) $this->fornitore->rubrica_id);
|
||
}
|
||
|
||
if (! $rubrica instanceof RubricaUniversale) {
|
||
$rubrica = RubricaUniversale::query()->create(array_merge($payload, [
|
||
'data_inserimento' => now()->toDateString(),
|
||
]));
|
||
} else {
|
||
$rubrica->fill($payload);
|
||
$rubrica->save();
|
||
}
|
||
|
||
if ((int) ($this->fornitore->rubrica_id ?? 0) !== (int) $rubrica->id) {
|
||
$this->fornitore->rubrica_id = (int) $rubrica->id;
|
||
$this->fornitore->save();
|
||
}
|
||
|
||
$this->fornitore->refresh();
|
||
|
||
Notification::make()->title('Rubrica aggiornata dal fornitore')->success()->send();
|
||
}
|
||
|
||
private function loadTagSuggestions(): void
|
||
{
|
||
if (! Schema::hasColumn('fornitori', 'tags')) {
|
||
$this->tagSuggestions = [];
|
||
return;
|
||
}
|
||
|
||
$all = Fornitore::query()
|
||
->whereNotNull('tags')
|
||
->pluck('tags')
|
||
->all();
|
||
|
||
$pool = [];
|
||
foreach ($all as $row) {
|
||
foreach ($this->splitTags((string) $row) as $tag) {
|
||
$pool[] = $tag;
|
||
}
|
||
}
|
||
|
||
$pool = array_values(array_unique($pool));
|
||
sort($pool, SORT_NATURAL | SORT_FLAG_CASE);
|
||
$this->tagSuggestions = array_slice($pool, 0, 300);
|
||
}
|
||
|
||
/**
|
||
* @return array<int, string>
|
||
*/
|
||
private function normalizeTags(string $input): array
|
||
{
|
||
$tags = [];
|
||
foreach ($this->splitTags($input) as $tag) {
|
||
$normalized = $this->canonicalizeTag($tag);
|
||
if ($normalized !== null) {
|
||
$tags[] = $normalized;
|
||
}
|
||
}
|
||
|
||
$tags = array_values(array_unique($tags));
|
||
sort($tags, SORT_NATURAL | SORT_FLAG_CASE);
|
||
|
||
return $tags;
|
||
}
|
||
|
||
/**
|
||
* @return array<int, string>
|
||
*/
|
||
private function splitTags(string $value): array
|
||
{
|
||
$parts = preg_split('/[,;|\n\r\/]+/', $value) ?: [];
|
||
|
||
return array_values(array_filter(array_map(function (string $part): string {
|
||
$clean = trim($part);
|
||
$clean = preg_replace('/\s+/', ' ', $clean) ?? '';
|
||
return $clean;
|
||
}, $parts), fn (string $p): bool => $p !== ''));
|
||
}
|
||
|
||
private function canonicalizeTag(string $raw): ?string
|
||
{
|
||
$clean = trim(mb_strtolower($raw));
|
||
if ($clean === '' || mb_strlen($clean) < 3) {
|
||
return null;
|
||
}
|
||
|
||
$map = [
|
||
'idr' => 'idraulico',
|
||
'idraul' => 'idraulico',
|
||
'elett' => 'elettricista',
|
||
'elettric' => 'elettricista',
|
||
'ascens' => 'ascensorista',
|
||
'puliz' => 'pulizie',
|
||
'giardin' => 'giardiniere',
|
||
'assicur' => 'assicurazione',
|
||
'manut' => 'manutenzione',
|
||
];
|
||
|
||
foreach ($map as $prefix => $canonical) {
|
||
if (str_starts_with($clean, $prefix)) {
|
||
return $canonical;
|
||
}
|
||
}
|
||
|
||
return $clean;
|
||
}
|
||
|
||
private function hydrateBoxData(User $user): void
|
||
{
|
||
$this->fattureAssociate = [];
|
||
$this->raVersateRows = [];
|
||
|
||
$this->box = [
|
||
'stabile_id' => null,
|
||
'fornitore_features' => [],
|
||
'fornitore_defaults' => [
|
||
'voce_spesa_default_id' => null,
|
||
'conto_costo_default_id' => null,
|
||
],
|
||
'ade' => ['count' => 0, 'lordo' => 0.0],
|
||
'fe' => ['count' => 0, 'totale' => 0.0, 'contabilizzate' => 0],
|
||
'contabilita' => [
|
||
'aperte_count' => 0,
|
||
'aperte_netto' => 0.0,
|
||
'pagate_count' => 0,
|
||
'pagate_netto' => 0.0,
|
||
],
|
||
'da_pagare' => ['source' => null, 'importo' => 0.0],
|
||
'ra' => [
|
||
'da_versare' => 0.0,
|
||
'versata' => 0.0,
|
||
'compensata' => 0.0,
|
||
],
|
||
];
|
||
|
||
$activeStabileId = StabileContext::resolveActiveStabileId($user);
|
||
if (! $activeStabileId) {
|
||
return;
|
||
}
|
||
|
||
$stabile = Stabile::query()->select(['id', 'amministratore_id'])->find($activeStabileId);
|
||
$amministratoreId = (int) ($stabile?->amministratore_id ?: 0);
|
||
if ($amministratoreId <= 0) {
|
||
return;
|
||
}
|
||
|
||
$this->box['stabile_id'] = $activeStabileId;
|
||
|
||
// Feature flags & defaults (per stabile) + fallback (per fornitore)
|
||
$settings = null;
|
||
if (Schema::hasTable('fornitore_stabile_impostazioni')) {
|
||
$settings = FornitoreStabileImpostazione::query()
|
||
->where('stabile_id', (int) $activeStabileId)
|
||
->where('fornitore_id', (int) $this->fornitore->id)
|
||
->first();
|
||
|
||
if ($settings) {
|
||
$this->box['fornitore_defaults']['voce_spesa_default_id'] = $settings->voce_spesa_default_id ? (int) $settings->voce_spesa_default_id : null;
|
||
$this->box['fornitore_defaults']['conto_costo_default_id'] = $settings->conto_costo_default_id ? (int) $settings->conto_costo_default_id : null;
|
||
}
|
||
}
|
||
|
||
$features = [];
|
||
if (Schema::hasColumn('fornitori', 'fe_features')) {
|
||
$features = is_array($this->fornitore->fe_features ?? null) ? $this->fornitore->fe_features : [];
|
||
} elseif ($settings && is_array($settings->meta ?? null)) {
|
||
$features = is_array($settings->meta['fe_features'] ?? null) ? $settings->meta['fe_features'] : [];
|
||
}
|
||
$this->box['fornitore_features'] = $features;
|
||
|
||
$normalize = static function (?string $value): string {
|
||
$value = strtoupper(trim((string) $value));
|
||
$value = str_replace(' ', '', $value);
|
||
return $value;
|
||
};
|
||
|
||
$ids = [];
|
||
$piva = $normalize($this->fornitore->partita_iva);
|
||
$cf = $normalize($this->fornitore->codice_fiscale);
|
||
if ($piva !== '') {
|
||
$ids[] = $piva;
|
||
if (str_starts_with($piva, 'IT') && strlen($piva) > 2) {
|
||
$ids[] = substr($piva, 2);
|
||
}
|
||
}
|
||
if ($cf !== '') {
|
||
$ids[] = $cf;
|
||
}
|
||
$ids = array_values(array_unique(array_filter($ids, fn($v) => $v !== '')));
|
||
|
||
// AdE (staging)
|
||
if (! empty($ids) && Schema::hasTable('stg_fatture_ade')) {
|
||
$adeBase = StgFatturaAde::query()
|
||
->where('amministratore_id', $amministratoreId)
|
||
->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($activeStabileId): void {
|
||
$q->where('stabile_id', $activeStabileId)->orWhereNull('stabile_id');
|
||
})
|
||
->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($ids): void {
|
||
foreach ($ids as $id) {
|
||
$q->orWhereRaw("REPLACE(UPPER(identificativo_fornitore), ' ', '') = ?", [$id]);
|
||
}
|
||
});
|
||
|
||
$this->box['ade']['count'] = (int) (clone $adeBase)->count();
|
||
$lordo = (float) (clone $adeBase)->selectRaw('COALESCE(SUM(imponibile + imposta), 0) as lordo')->value('lordo');
|
||
$this->box['ade']['lordo'] = $lordo;
|
||
|
||
$this->anteprimaAde = (clone $adeBase)
|
||
->orderByDesc('data_emissione')
|
||
->orderByDesc('id')
|
||
->limit(8)
|
||
->get(['id', 'data_emissione', 'numero_documento', 'imponibile', 'imposta', 'sdi_file'])
|
||
->map(fn(StgFatturaAde $r) => [
|
||
'id' => $r->id,
|
||
'data' => $r->data_emissione?->format('d/m/Y'),
|
||
'numero' => (string) ($r->numero_documento ?? ''),
|
||
'lordo' => (float) ($r->imponibile ?? 0) + (float) ($r->imposta ?? 0),
|
||
'sdi_file' => (string) ($r->sdi_file ?? ''),
|
||
])
|
||
->all();
|
||
}
|
||
|
||
// FE importate
|
||
if (Schema::hasTable('fatture_elettroniche')) {
|
||
$feBase = FatturaElettronica::query()
|
||
->where('stabile_id', $activeStabileId)
|
||
->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($ids): void {
|
||
$q->where('fornitore_id', $this->fornitore->id);
|
||
|
||
if (! empty($ids)) {
|
||
$q->orWhere(function (\Illuminate\Database\Eloquent\Builder $qq) use ($ids): void {
|
||
foreach ($ids as $id) {
|
||
$qq->orWhereRaw("REPLACE(UPPER(fornitore_piva), ' ', '') = ?", [$id])
|
||
->orWhereRaw("REPLACE(UPPER(fornitore_cf), ' ', '') = ?", [$id]);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
$this->box['fe']['count'] = (int) (clone $feBase)->count();
|
||
$this->box['fe']['totale'] = (float) (clone $feBase)->selectRaw('COALESCE(SUM(totale), 0) as totale')->value('totale');
|
||
$this->box['fe']['contabilizzate'] = (int) (clone $feBase)
|
||
->where(function (\Illuminate\Database\Eloquent\Builder $q): void {
|
||
$q->whereNotNull('registrazione_contabile_id')->orWhere('stato', 'contabilizzata');
|
||
})
|
||
->count();
|
||
|
||
$this->anteprimaFe = (clone $feBase)
|
||
->orderByDesc('data_fattura')
|
||
->orderByDesc('id')
|
||
->limit(8)
|
||
->get(['id', 'data_fattura', 'numero_fattura', 'totale', 'sdi_file', 'stato'])
|
||
->map(fn(FatturaElettronica $r) => [
|
||
'id' => $r->id,
|
||
'data' => $r->data_fattura?->format('d/m/Y'),
|
||
'numero' => (string) ($r->numero_fattura ?? ''),
|
||
'totale' => (float) ($r->totale ?? 0),
|
||
'sdi_file' => (string) ($r->sdi_file ?? ''),
|
||
'stato' => (string) ($r->stato ?? ''),
|
||
])
|
||
->all();
|
||
}
|
||
|
||
// Contabilità (fatture fornitori)
|
||
if (Schema::hasTable('contabilita_fatture_fornitori')) {
|
||
$contabBase = ContabilitaFatturaFornitore::query()
|
||
->where('stabile_id', $activeStabileId)
|
||
->where('fornitore_id', (int) $this->fornitore->id);
|
||
|
||
// Coerente con la logica della scheda contabile: aperto = totale - pagato.
|
||
$totNetto = (float) (clone $contabBase)->selectRaw('COALESCE(SUM(netto_da_pagare), 0) as netto')->value('netto');
|
||
$this->box['contabilita']['pagate_count'] = (int) (clone $contabBase)->where('stato', 'pagato')->count();
|
||
$this->box['contabilita']['pagate_netto'] = (float) (clone $contabBase)->where('stato', 'pagato')->selectRaw('COALESCE(SUM(netto_da_pagare), 0) as netto')->value('netto');
|
||
$this->box['contabilita']['aperte_netto'] = round($totNetto - (float) $this->box['contabilita']['pagate_netto'], 2);
|
||
$this->box['contabilita']['aperte_count'] = (int) (clone $contabBase)->where('stato', '!=', 'pagato')->count();
|
||
|
||
$this->anteprimaContabilita = (clone $contabBase)
|
||
->orderByDesc('data_documento')
|
||
->orderByDesc('id')
|
||
->limit(8)
|
||
->get(['id', 'data_documento', 'numero_documento', 'netto_da_pagare', 'stato'])
|
||
->map(fn(ContabilitaFatturaFornitore $r) => [
|
||
'id' => $r->id,
|
||
'data' => $r->data_documento?->format('d/m/Y'),
|
||
'numero' => (string) ($r->numero_documento ?? ''),
|
||
'netto' => (float) ($r->netto_da_pagare ?? 0),
|
||
'stato' => (string) ($r->stato ?? ''),
|
||
])
|
||
->all();
|
||
|
||
$this->fattureAssociate = (clone $contabBase)
|
||
->orderByDesc('data_documento')
|
||
->orderByDesc('id')
|
||
->limit(30)
|
||
->get(['id', 'data_documento', 'numero_documento', 'totale', 'netto_da_pagare', 'stato', 'fattura_elettronica_id'])
|
||
->map(fn(ContabilitaFatturaFornitore $r) => [
|
||
'id' => (int) $r->id,
|
||
'data' => $r->data_documento?->format('d/m/Y'),
|
||
'numero' => (string) ($r->numero_documento ?? ''),
|
||
'totale' => (float) ($r->totale ?? 0),
|
||
'netto' => (float) ($r->netto_da_pagare ?? 0),
|
||
'stato' => (string) ($r->stato ?? ''),
|
||
'source' => ((int) ($r->fattura_elettronica_id ?? 0) > 0) ? 'FE' : 'manuale',
|
||
])
|
||
->all();
|
||
}
|
||
|
||
// Registro RA
|
||
if (Schema::hasTable('registro_ritenute_acconto')) {
|
||
$raBase = RegistroRitenuteAcconto::query()
|
||
->where('fornitore_id', (int) $this->fornitore->id)
|
||
->whereHas('gestione', fn(\Illuminate\Database\Eloquent\Builder $q) => $q->where('stabile_id', (int) $activeStabileId));
|
||
|
||
foreach (['da_versare', 'versata', 'compensata'] as $stato) {
|
||
$this->box['ra'][$stato] = (float) (clone $raBase)
|
||
->where('stato_versamento', $stato)
|
||
->selectRaw('COALESCE(SUM(importo_ritenuta), 0) as ra')
|
||
->value('ra');
|
||
}
|
||
|
||
$this->raVersateRows = (clone $raBase)
|
||
->whereIn('stato_versamento', ['versata', 'compensata'])
|
||
->orderByDesc('data_versamento')
|
||
->orderByDesc('id')
|
||
->limit(30)
|
||
->get(['id', 'data_versamento', 'importo_ritenuta', 'stato_versamento', 'codice_tributo', 'f24_riferimento'])
|
||
->map(fn(RegistroRitenuteAcconto $r) => [
|
||
'id' => (int) $r->id,
|
||
'data_versamento' => $r->data_versamento?->format('d/m/Y'),
|
||
'importo' => (float) ($r->importo_ritenuta ?? 0),
|
||
'stato' => (string) ($r->stato_versamento ?? ''),
|
||
'tributo' => (string) ($r->codice_tributo ?? ''),
|
||
'f24' => (string) ($r->f24_riferimento ?? ''),
|
||
])
|
||
->all();
|
||
}
|
||
|
||
// Totale da pagare (priorità: contabilità → FE → AdE)
|
||
if ((float) ($this->box['contabilita']['aperte_netto'] ?? 0) > 0) {
|
||
$this->box['da_pagare'] = ['source' => 'contabilita', 'importo' => (float) $this->box['contabilita']['aperte_netto']];
|
||
} elseif ((int) ($this->box['fe']['count'] ?? 0) > 0) {
|
||
$this->box['da_pagare'] = ['source' => 'fe', 'importo' => (float) ($this->box['fe']['totale'] ?? 0)];
|
||
} elseif ((int) ($this->box['ade']['count'] ?? 0) > 0) {
|
||
$this->box['da_pagare'] = ['source' => 'ade', 'importo' => (float) ($this->box['ade']['lordo'] ?? 0)];
|
||
}
|
||
}
|
||
|
||
protected function getHeaderActions(): array
|
||
{
|
||
return [
|
||
Action::make('apri_rubrica')
|
||
->label('Apri anagrafica unica')
|
||
->icon('heroicon-o-user')
|
||
->visible(fn(): bool => (int) ($this->fornitore->rubrica_id ?? 0) > 0)
|
||
->url(fn() => RubricaUniversaleScheda::getUrl([
|
||
'record' => (int) $this->fornitore->rubrica_id,
|
||
], panel: 'admin-filament')),
|
||
|
||
Action::make('sync_rubrica')
|
||
->label('Sincronizza Rubrica')
|
||
->icon('heroicon-o-arrow-path')
|
||
->action(fn() => $this->syncRubricaFromFornitore()),
|
||
|
||
Action::make('importa_tag_legacy')
|
||
->label('Importa TAG legacy')
|
||
->icon('heroicon-o-tag')
|
||
->action(fn() => $this->importLegacyTags()),
|
||
|
||
Action::make('impostazioni_fe')
|
||
->label('Impostazioni FE')
|
||
->icon('heroicon-o-adjustments-horizontal')
|
||
->visible(fn(): bool => Schema::hasColumn('fornitori', 'escludi_righe_fe'))
|
||
->form([
|
||
Toggle::make('escludi_righe_fe')
|
||
->label('Escludi righe FE (import e visualizzazione)')
|
||
->helperText('Utile per utenze (acqua/luce/gas): si userà il PDF per i dati di consumo invece delle righe XML.')
|
||
->default((bool) ($this->fornitore->escludi_righe_fe ?? false)),
|
||
|
||
Select::make('conto_costo_default_id')
|
||
->label('Voce spesa predefinita (sottoconto costo)')
|
||
->helperText('Valore suggerito per imputare i costi quando le righe FE non hanno sottoconto o sono assenti.')
|
||
->options(function (): array {
|
||
if (! Schema::hasTable('contabilita_piano_conti')) {
|
||
return [];
|
||
}
|
||
|
||
return PianoConti::query()
|
||
->orderBy('codice')
|
||
->limit(1000)
|
||
->get(['id', 'codice', 'descrizione'])
|
||
->mapWithKeys(fn(PianoConti $c) => [(string) $c->id => $c->codice . ' - ' . $c->descrizione])
|
||
->all();
|
||
})
|
||
->searchable()
|
||
->nullable()
|
||
->default(fn(): ?int => $this->box['fornitore_defaults']['conto_costo_default_id'] ? (int) $this->box['fornitore_defaults']['conto_costo_default_id'] : null),
|
||
|
||
Select::make('voce_spesa_default_id')
|
||
->label('Voce di spesa predefinita (ripartizione)')
|
||
->helperText('Collegamento “logico” alla voce (utile per consumi/statistiche/ripartizione; es. acqua).')
|
||
->options(function (): array {
|
||
$user = Auth::user();
|
||
if (! $user instanceof User) {
|
||
return [];
|
||
}
|
||
|
||
$stabileId = StabileContext::resolveActiveStabileId($user);
|
||
if (! $stabileId || ! Schema::hasTable('voci_spesa')) {
|
||
return [];
|
||
}
|
||
|
||
return VoceSpesa::query()
|
||
->where('stabile_id', (int) $stabileId)
|
||
->orderBy('descrizione')
|
||
->limit(800)
|
||
->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()
|
||
->default(fn(): ?int => $this->box['fornitore_defaults']['voce_spesa_default_id'] ? (int) $this->box['fornitore_defaults']['voce_spesa_default_id'] : null),
|
||
])
|
||
->action(function (array $data): void {
|
||
if (! Schema::hasColumn('fornitori', 'escludi_righe_fe')) {
|
||
Notification::make()->title('Colonna mancante: escludi_righe_fe')->warning()->send();
|
||
return;
|
||
}
|
||
$this->fornitore->escludi_righe_fe = (bool) ($data['escludi_righe_fe'] ?? false);
|
||
$this->fornitore->save();
|
||
|
||
$user = Auth::user();
|
||
if (! $user instanceof User) {
|
||
return;
|
||
}
|
||
|
||
$stabileId = StabileContext::resolveActiveStabileId($user);
|
||
if (! $stabileId) {
|
||
return;
|
||
}
|
||
|
||
if (! Schema::hasTable('fornitore_stabile_impostazioni')) {
|
||
return;
|
||
}
|
||
|
||
$voceSpesaId = isset($data['voce_spesa_default_id']) && is_numeric($data['voce_spesa_default_id']) ? (int) $data['voce_spesa_default_id'] : null;
|
||
$contoCostoId = isset($data['conto_costo_default_id']) && is_numeric($data['conto_costo_default_id']) ? (int) $data['conto_costo_default_id'] : null;
|
||
|
||
FornitoreStabileImpostazione::query()->updateOrCreate(
|
||
[
|
||
'stabile_id' => (int) $stabileId,
|
||
'fornitore_id' => (int) $this->fornitore->id,
|
||
],
|
||
[
|
||
'voce_spesa_default_id' => $voceSpesaId ?: null,
|
||
'conto_costo_default_id' => $contoCostoId ?: null,
|
||
]
|
||
);
|
||
|
||
$this->hydrateBoxData($user);
|
||
})
|
||
->successNotificationTitle('Impostazioni FE salvate'),
|
||
|
||
Action::make('acqua_config')
|
||
->label('Configura acqua')
|
||
->icon('heroicon-o-beaker')
|
||
->form([
|
||
Toggle::make('acqua_enabled')
|
||
->label('Abilita acquisizione dati acqua (PDF)')
|
||
->default((bool) ((is_array($this->box['fornitore_features'] ?? null) ? ($this->box['fornitore_features']['acqua']['enabled'] ?? false) : false)))
|
||
->helperText('Attiva le funzioni acqua su questo fornitore (scan + letture/periodi).'),
|
||
|
||
Toggle::make('acqua_scan_enabled')
|
||
->label('Abilita scansione massiva su FE già importate')
|
||
->default((bool) ((is_array($this->box['fornitore_features'] ?? null) ? ($this->box['fornitore_features']['acqua']['scan_enabled'] ?? false) : false))),
|
||
|
||
Toggle::make('acqua_ticket_on_anomaly')
|
||
->label('Apri ticket su anomalie/mismatch')
|
||
->default(true)
|
||
->helperText('Se il parser non trova dati o cambia utenza/contatore, apre un ticket interno.'),
|
||
|
||
Repeater::make('utenze_acqua')
|
||
->label('Utenze acqua (stabile attivo)')
|
||
->helperText('Compila i campi per ogni contatore/utenza (lo stabile può averne più di uno).')
|
||
->default(function (): array {
|
||
$user = Auth::user();
|
||
if (! $user instanceof User) {
|
||
return [];
|
||
}
|
||
$stabileId = StabileContext::resolveActiveStabileId($user);
|
||
if (! $stabileId || ! Schema::hasTable('stabile_servizi')) {
|
||
return [];
|
||
}
|
||
|
||
return StabileServizio::query()
|
||
->where('stabile_id', (int) $stabileId)
|
||
->where('tipo', 'acqua')
|
||
->orderByDesc('attivo')
|
||
->orderBy('nome')
|
||
->limit(50)
|
||
->get([
|
||
'id',
|
||
'nome',
|
||
'attivo',
|
||
'voce_spesa_id',
|
||
'contatore_matricola',
|
||
'codice_utenza',
|
||
'codice_cliente',
|
||
'codice_contratto',
|
||
])
|
||
->map(function (StabileServizio $s): array {
|
||
return [
|
||
'id' => (int) $s->id,
|
||
'nome' => (string) ($s->nome ?? ''),
|
||
'attivo' => (bool) $s->attivo,
|
||
'voce_spesa_id' => $s->voce_spesa_id ? (int) $s->voce_spesa_id : null,
|
||
'contatore_matricola' => (string) ($s->contatore_matricola ?? ''),
|
||
'codice_utenza' => (string) ($s->codice_utenza ?? ''),
|
||
'codice_cliente' => (string) ($s->codice_cliente ?? ''),
|
||
'codice_contratto' => (string) ($s->codice_contratto ?? ''),
|
||
];
|
||
})
|
||
->all();
|
||
})
|
||
->schema([
|
||
Hidden::make('id'),
|
||
TextInput::make('nome')
|
||
->label('Nome utenza')
|
||
->maxLength(255)
|
||
->nullable(),
|
||
Toggle::make('attivo')
|
||
->label('Attiva')
|
||
->default(true),
|
||
|
||
TextInput::make('contatore_matricola')
|
||
->label('Matricola contatore')
|
||
->maxLength(64)
|
||
->nullable(),
|
||
TextInput::make('codice_utenza')
|
||
->label('Codice utenza')
|
||
->maxLength(64)
|
||
->nullable(),
|
||
TextInput::make('codice_cliente')
|
||
->label('Codice cliente')
|
||
->maxLength(64)
|
||
->nullable(),
|
||
TextInput::make('codice_contratto')
|
||
->label('Codice contratto')
|
||
->maxLength(64)
|
||
->nullable(),
|
||
|
||
Select::make('voce_spesa_id')
|
||
->label('Voce di spesa (solo acqua)')
|
||
->options(function (): array {
|
||
$user = Auth::user();
|
||
if (! $user instanceof User) {
|
||
return [];
|
||
}
|
||
$stabileId = StabileContext::resolveActiveStabileId($user);
|
||
if (! $stabileId || ! Schema::hasTable('voci_spesa')) {
|
||
return [];
|
||
}
|
||
|
||
return VoceSpesa::query()
|
||
->where('stabile_id', (int) $stabileId)
|
||
->where('categoria', 'acqua')
|
||
->orderBy('descrizione')
|
||
->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(),
|
||
])
|
||
->addActionLabel('Aggiungi utenza acqua')
|
||
->reorderable(false)
|
||
->collapsed(false),
|
||
])
|
||
->action(function (array $data): void {
|
||
$user = Auth::user();
|
||
if (! $user instanceof User) {
|
||
Notification::make()->title('Utente non valido')->danger()->send();
|
||
return;
|
||
}
|
||
|
||
// Salva toggle feature (preferenza: colonna fornitori.fe_features, fallback: meta per-stabile)
|
||
$features = is_array($this->box['fornitore_features'] ?? null) ? $this->box['fornitore_features'] : [];
|
||
$features['acqua'] = array_merge(is_array($features['acqua'] ?? null) ? $features['acqua'] : [], [
|
||
'enabled' => (bool) ($data['acqua_enabled'] ?? false),
|
||
'scan_enabled' => (bool) ($data['acqua_scan_enabled'] ?? false),
|
||
'ticket_on_anomaly' => (bool) ($data['acqua_ticket_on_anomaly'] ?? true),
|
||
]);
|
||
|
||
$stabileId = StabileContext::resolveActiveStabileId($user);
|
||
if (! $stabileId) {
|
||
Notification::make()->title('Seleziona uno stabile attivo')->warning()->send();
|
||
return;
|
||
}
|
||
|
||
if (Schema::hasColumn('fornitori', 'fe_features')) {
|
||
$this->fornitore->fe_features = $features;
|
||
$this->fornitore->save();
|
||
} else {
|
||
if (! Schema::hasTable('fornitore_stabile_impostazioni')) {
|
||
Notification::make()
|
||
->title('Impossibile salvare feature')
|
||
->body('Manca la colonna fornitori.fe_features e non esiste la tabella fornitore_stabile_impostazioni (eseguire migrazioni).')
|
||
->danger()
|
||
->send();
|
||
return;
|
||
}
|
||
$settings = FornitoreStabileImpostazione::query()->firstOrNew([
|
||
'stabile_id' => (int) $stabileId,
|
||
'fornitore_id' => (int) $this->fornitore->id,
|
||
]);
|
||
$meta = is_array($settings->meta ?? null) ? $settings->meta : [];
|
||
$meta['fe_features'] = $features;
|
||
$settings->meta = $meta;
|
||
$settings->save();
|
||
}
|
||
|
||
if (! Schema::hasTable('stabile_servizi')) {
|
||
Notification::make()->title('Tabella servizi non presente')->warning()->send();
|
||
return;
|
||
}
|
||
|
||
$utenze = is_array($data['utenze_acqua'] ?? null) ? $data['utenze_acqua'] : [];
|
||
foreach ($utenze as $row) {
|
||
$rowId = isset($row['id']) && is_numeric($row['id']) ? (int) $row['id'] : null;
|
||
|
||
$nome = is_string($row['nome'] ?? null) ? trim((string) $row['nome']) : '';
|
||
$attivo = (bool) ($row['attivo'] ?? true);
|
||
$voceSpesaId = isset($row['voce_spesa_id']) && is_numeric($row['voce_spesa_id']) ? (int) $row['voce_spesa_id'] : null;
|
||
|
||
$matricola = is_string($row['contatore_matricola'] ?? null) ? trim((string) $row['contatore_matricola']) : '';
|
||
$utenzaCod = is_string($row['codice_utenza'] ?? null) ? trim((string) $row['codice_utenza']) : '';
|
||
$cliente = is_string($row['codice_cliente'] ?? null) ? trim((string) $row['codice_cliente']) : '';
|
||
$contratto = is_string($row['codice_contratto'] ?? null) ? trim((string) $row['codice_contratto']) : '';
|
||
|
||
$hasAny = ($nome !== '') || ($matricola !== '') || ($utenzaCod !== '') || ($cliente !== '') || ($contratto !== '') || ($voceSpesaId && $voceSpesaId > 0);
|
||
if (! $hasAny) {
|
||
continue;
|
||
}
|
||
|
||
$servizio = null;
|
||
if ($rowId) {
|
||
$servizio = StabileServizio::query()
|
||
->where('stabile_id', (int) $stabileId)
|
||
->where('tipo', 'acqua')
|
||
->find($rowId);
|
||
}
|
||
|
||
if (! $servizio) {
|
||
$autoName = 'Acqua';
|
||
if ($matricola !== '') {
|
||
$autoName .= ' - Contatore ' . $matricola;
|
||
} elseif ($utenzaCod !== '') {
|
||
$autoName .= ' - Utenza ' . $utenzaCod;
|
||
}
|
||
|
||
$servizio = new StabileServizio([
|
||
'stabile_id' => (int) $stabileId,
|
||
'tipo' => 'acqua',
|
||
'nome' => $nome !== '' ? $nome : $autoName,
|
||
'attivo' => true,
|
||
]);
|
||
}
|
||
|
||
$servizio->fornitore_id = (int) $this->fornitore->id;
|
||
$servizio->attivo = $attivo;
|
||
if ($nome !== '') {
|
||
$servizio->nome = $nome;
|
||
}
|
||
|
||
$servizio->voce_spesa_id = $voceSpesaId ?: null;
|
||
$servizio->contatore_matricola = $matricola !== '' ? $matricola : null;
|
||
$servizio->codice_utenza = $utenzaCod !== '' ? $utenzaCod : null;
|
||
$servizio->codice_cliente = $cliente !== '' ? $cliente : null;
|
||
$servizio->codice_contratto = $contratto !== '' ? $contratto : null;
|
||
$servizio->save();
|
||
}
|
||
|
||
$this->hydrateBoxData($user);
|
||
|
||
Notification::make()->title('Configurazione acqua salvata')->success()->send();
|
||
}),
|
||
|
||
Action::make('acqua_scan')
|
||
->label('Scansiona FE acqua')
|
||
->icon('heroicon-o-magnifying-glass')
|
||
->requiresConfirmation()
|
||
->form([
|
||
Toggle::make('solo_stabile_attivo')
|
||
->label('Solo stabile attivo')
|
||
->default(true),
|
||
|
||
Toggle::make('solo_non_estratte')
|
||
->label('Solo FE non ancora estratte (acqua)')
|
||
->default(true)
|
||
->helperText('Processa solo fatture dove non risulta già salvato consumo acqua.'),
|
||
|
||
Select::make('voce_spesa_id')
|
||
->label('Voce di spesa (opzionale)')
|
||
->options(function (): array {
|
||
$user = Auth::user();
|
||
if (! $user instanceof User) {
|
||
return [];
|
||
}
|
||
$stabileId = StabileContext::resolveActiveStabileId($user);
|
||
if (! $stabileId || ! Schema::hasTable('voci_spesa')) {
|
||
return [];
|
||
}
|
||
|
||
return VoceSpesa::query()
|
||
->where('stabile_id', (int) $stabileId)
|
||
->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(),
|
||
|
||
Select::make('force_servizio_id')
|
||
->label('Forza servizio acqua (opzionale)')
|
||
->helperText('Se impostato, blocca l’aggancio se il PDF indica un contatore/utenza diversa (apre ticket).')
|
||
->options(function (): array {
|
||
$user = Auth::user();
|
||
if (! $user instanceof User) {
|
||
return [];
|
||
}
|
||
$stabileId = StabileContext::resolveActiveStabileId($user);
|
||
if (! $stabileId || ! Schema::hasTable('stabile_servizi')) {
|
||
return [];
|
||
}
|
||
|
||
return StabileServizio::query()
|
||
->where('stabile_id', (int) $stabileId)
|
||
->where('tipo', 'acqua')
|
||
->orderByDesc('attivo')
|
||
->orderBy('nome')
|
||
->limit(200)
|
||
->get(['id', 'nome', 'contatore_matricola', 'codice_utenza'])
|
||
->mapWithKeys(function (StabileServizio $s) {
|
||
$label = (string) ($s->nome ?: ('Servizio #' . $s->id));
|
||
$bits = [];
|
||
if (is_string($s->contatore_matricola) && $s->contatore_matricola !== '') {
|
||
$bits[] = 'Contatore ' . $s->contatore_matricola;
|
||
}
|
||
if (is_string($s->codice_utenza) && $s->codice_utenza !== '') {
|
||
$bits[] = 'Utenza ' . $s->codice_utenza;
|
||
}
|
||
if (count($bits) > 0) {
|
||
$label .= ' — ' . implode(' · ', $bits);
|
||
}
|
||
return [(string) $s->id => $label];
|
||
})
|
||
->all();
|
||
})
|
||
->searchable()
|
||
->nullable(),
|
||
|
||
TextInput::make('limit')
|
||
->label('Limite FE da processare')
|
||
->numeric()
|
||
->default(50)
|
||
->minValue(1)
|
||
->maxValue(500),
|
||
|
||
Toggle::make('crea_ticket')
|
||
->label('Crea ticket su anomalie')
|
||
->default(true),
|
||
])
|
||
->action(function (array $data): void {
|
||
$user = Auth::user();
|
||
if (! $user instanceof User) {
|
||
Notification::make()->title('Utente non valido')->danger()->send();
|
||
return;
|
||
}
|
||
|
||
$features = is_array($this->box['fornitore_features'] ?? null) ? $this->box['fornitore_features'] : [];
|
||
$acqua = is_array($features['acqua'] ?? null) ? $features['acqua'] : [];
|
||
if (! (bool) ($acqua['enabled'] ?? false)) {
|
||
Notification::make()->title('Acqua non attiva su questo fornitore')->warning()->send();
|
||
return;
|
||
}
|
||
|
||
$soloStabileAttivo = (bool) ($data['solo_stabile_attivo'] ?? true);
|
||
$soloNonEstratte = (bool) ($data['solo_non_estratte'] ?? true);
|
||
$activeStabileId = StabileContext::resolveActiveStabileId($user);
|
||
if ($soloStabileAttivo && ! $activeStabileId) {
|
||
Notification::make()->title('Seleziona uno stabile attivo')->warning()->send();
|
||
return;
|
||
}
|
||
|
||
$stabileIds = [];
|
||
if ($soloStabileAttivo) {
|
||
$stabileIds = [(int) $activeStabileId];
|
||
} else {
|
||
$stabileIds = Stabile::query()
|
||
->where('amministratore_id', (int) $this->fornitore->amministratore_id)
|
||
->pluck('id')
|
||
->map(fn($v) => (int) $v)
|
||
->all();
|
||
}
|
||
|
||
if (count($stabileIds) < 1) {
|
||
Notification::make()->title('Nessuno stabile trovato')->warning()->send();
|
||
return;
|
||
}
|
||
|
||
$limit = isset($data['limit']) && is_numeric($data['limit']) ? (int) $data['limit'] : 50;
|
||
$limit = max(1, min(500, $limit));
|
||
|
||
$voceSpesaId = isset($data['voce_spesa_id']) && is_numeric($data['voce_spesa_id']) ? (int) $data['voce_spesa_id'] : null;
|
||
$forceServizioId = isset($data['force_servizio_id']) && is_numeric($data['force_servizio_id']) ? (int) $data['force_servizio_id'] : null;
|
||
$creaTicket = (bool) ($data['crea_ticket'] ?? true);
|
||
|
||
$normalize = static function (?string $value): string {
|
||
$value = strtoupper(trim((string) $value));
|
||
$value = str_replace(' ', '', $value);
|
||
return $value;
|
||
};
|
||
|
||
$ids = [];
|
||
$piva = $normalize($this->fornitore->partita_iva);
|
||
$cf = $normalize($this->fornitore->codice_fiscale);
|
||
if ($piva !== '') {
|
||
$ids[] = $piva;
|
||
if (str_starts_with($piva, 'IT') && strlen($piva) > 2) {
|
||
$ids[] = substr($piva, 2);
|
||
}
|
||
}
|
||
if ($cf !== '') {
|
||
$ids[] = $cf;
|
||
}
|
||
$ids = array_values(array_unique(array_filter($ids, fn($v) => $v !== '')));
|
||
|
||
$feQuery = FatturaElettronica::query()
|
||
->whereIn('stabile_id', $stabileIds)
|
||
->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($ids): void {
|
||
$q->where('fornitore_id', (int) $this->fornitore->id);
|
||
if (! empty($ids)) {
|
||
$q->orWhere(function (\Illuminate\Database\Eloquent\Builder $qq) use ($ids): void {
|
||
foreach ($ids as $id) {
|
||
$qq->orWhereRaw("REPLACE(UPPER(fornitore_piva), ' ', '') = ?", [$id])
|
||
->orWhereRaw("REPLACE(UPPER(fornitore_cf), ' ', '') = ?", [$id]);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
if ($soloNonEstratte) {
|
||
$feQuery->where(function (\Illuminate\Database\Eloquent\Builder $q): void {
|
||
$q->whereNull('consumo_raw')
|
||
->orWhere('consumo_raw', 'not like', '%"type":"acqua"%');
|
||
});
|
||
}
|
||
|
||
$feQuery
|
||
->orderByDesc('data_fattura')
|
||
->orderByDesc('id')
|
||
->limit($limit);
|
||
|
||
$fatture = $feQuery->get(['id', 'stabile_id', 'fornitore_id', 'sdi_file', 'totale', 'data_fattura', 'allegato_pdf_path', 'allegato_pdf_hash', 'xml_content', 'consumo_raw']);
|
||
|
||
$processed = 0;
|
||
$ok = 0;
|
||
$skipped = 0;
|
||
$tickets = 0;
|
||
|
||
foreach ($fatture as $fe) {
|
||
$processed++;
|
||
|
||
// Ensure Documento (best-effort)
|
||
try {
|
||
if ((int) $user->id > 0) {
|
||
app(FatturaElettronicaProtocolloService::class)->ensureDocumentoProtocollato($fe, (int) $user->id);
|
||
$fe->refresh();
|
||
}
|
||
} catch (\Throwable) {
|
||
// ignore
|
||
}
|
||
|
||
$doc = Documento::query()
|
||
->where('documentable_type', FatturaElettronica::class)
|
||
->where('documentable_id', (int) $fe->id)
|
||
->first();
|
||
|
||
if (! $doc) {
|
||
$skipped++;
|
||
if ($creaTicket) {
|
||
try {
|
||
$ing = app(ConsumiAcquaIngestionService::class)->ingest(
|
||
$fe,
|
||
['error' => 'Documento non presente', 'consumi' => []],
|
||
$voceSpesaId,
|
||
(int) $user->id,
|
||
true,
|
||
$forceServizioId
|
||
);
|
||
$tickets += (int) ($ing['tickets'] ?? 0);
|
||
} catch (\Throwable) {
|
||
// ignore
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
|
||
$text = is_string($doc->contenuto_ocr) ? trim($doc->contenuto_ocr) : '';
|
||
if ($text === '') {
|
||
$res = app(PdfTextExtractionService::class)->extractAndStore($doc, false);
|
||
if (($res['status'] ?? null) !== 'ok') {
|
||
$skipped++;
|
||
if ($creaTicket) {
|
||
try {
|
||
$ing = app(ConsumiAcquaIngestionService::class)->ingest(
|
||
$fe,
|
||
['error' => (string) ($res['message'] ?? 'OCR fallita'), 'consumi' => []],
|
||
$voceSpesaId,
|
||
(int) $user->id,
|
||
true,
|
||
$forceServizioId
|
||
);
|
||
$tickets += (int) ($ing['tickets'] ?? 0);
|
||
} catch (\Throwable) {
|
||
// ignore
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
$doc->refresh();
|
||
$text = is_string($doc->contenuto_ocr) ? trim($doc->contenuto_ocr) : '';
|
||
}
|
||
|
||
if ($text === '') {
|
||
$skipped++;
|
||
if ($creaTicket) {
|
||
try {
|
||
$ing = app(ConsumiAcquaIngestionService::class)->ingest(
|
||
$fe,
|
||
['error' => 'Testo OCR vuoto', 'consumi' => []],
|
||
$voceSpesaId,
|
||
(int) $user->id,
|
||
true,
|
||
$forceServizioId
|
||
);
|
||
$tickets += (int) ($ing['tickets'] ?? 0);
|
||
} catch (\Throwable) {
|
||
// ignore
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
|
||
$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) {
|
||
$skipped++;
|
||
if ($creaTicket) {
|
||
try {
|
||
$ing = app(ConsumiAcquaIngestionService::class)->ingest($fe, $parsed, $voceSpesaId, (int) $user->id, true, $forceServizioId);
|
||
$tickets += (int) ($ing['tickets'] ?? 0);
|
||
} catch (\Throwable) {
|
||
// ignore
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// Salva anche su FE (consumo_raw + campi generici)
|
||
$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' => $generale,
|
||
'finestra_autolettura' => $finestra,
|
||
'riepilogo_letture' => array_values($letture),
|
||
'quadro_dettaglio' => $quadro,
|
||
'tariffe' => $tariffe,
|
||
'iva' => [
|
||
'codice' => $ivaInfo['codice'] ?? null,
|
||
'aliquota_percentuale' => $ivaInfo['aliquota_percentuale'] ?? null,
|
||
'descrizione' => $ivaInfo['descrizione'] ?? null,
|
||
],
|
||
'parsed_at' => now()->toISOString(),
|
||
];
|
||
|
||
$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;
|
||
}
|
||
}
|
||
|
||
$fe->consumo_unita = 'mc';
|
||
$fe->consumo_valore = $totMc;
|
||
$fe->consumo_riferimento = $ref;
|
||
$fe->consumo_raw = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||
$fe->save();
|
||
|
||
try {
|
||
app(ConsumiAcquaTariffeIngestionService::class)->ingest($fe, $parsed, null);
|
||
} catch (\Throwable) {
|
||
// best-effort
|
||
}
|
||
|
||
$ing = app(ConsumiAcquaIngestionService::class)->ingest($fe, $parsed, $voceSpesaId, (int) $user->id, $creaTicket, $forceServizioId);
|
||
|
||
if (($ing['status'] ?? null) === 'ok') {
|
||
try {
|
||
app(ConsumiAcquaTariffeIngestionService::class)->ingest(
|
||
$fe,
|
||
$parsed,
|
||
isset($ing['servizio_id']) && is_numeric($ing['servizio_id']) ? (int) $ing['servizio_id'] : null
|
||
);
|
||
} catch (\Throwable) {
|
||
// best-effort
|
||
}
|
||
}
|
||
|
||
if (($ing['status'] ?? null) === 'ok') {
|
||
$ok++;
|
||
} elseif (($ing['status'] ?? null) === 'no-data' && $hasTariffData) {
|
||
$ok++;
|
||
} elseif (($ing['status'] ?? null) === 'mismatch') {
|
||
$tickets += (int) ($ing['tickets'] ?? 1);
|
||
$skipped++;
|
||
} elseif (($ing['status'] ?? null) === 'no-data') {
|
||
$tickets += (int) ($ing['tickets'] ?? 0);
|
||
$skipped++;
|
||
} else {
|
||
$skipped++;
|
||
}
|
||
}
|
||
|
||
$body = 'Processate: ' . $processed . ' · OK: ' . $ok . ' · Skipped: ' . $skipped;
|
||
if ($tickets > 0) {
|
||
$body .= ' · Ticket: ' . $tickets;
|
||
}
|
||
|
||
Notification::make()->title('Scansione acqua completata')->body($body)->success()->send();
|
||
}),
|
||
|
||
Action::make('torna')
|
||
->label('Torna')
|
||
->icon('heroicon-o-arrow-left')
|
||
->url(fn() => url()->previous()),
|
||
];
|
||
}
|
||
}
|