netgescon-day0/app/Filament/Pages/Gescon/RegistrazioneUnificata.php

690 lines
27 KiB
PHP
Executable File

<?php
namespace App\Filament\Pages\Gescon;
use App\Models\FatturaElettronica;
use App\Models\Preventivo;
use App\Models\TabellaMillesimale;
use App\Models\User;
use App\Models\VocePreventivo;
use App\Models\VoceSpesa;
use App\Services\FattureElettroniche\FatturaElettronicaXmlParser;
use App\Services\RipartizioneSpesaService;
use App\Support\ModuleVisibility;
use App\Support\StabileContext;
use BackedEnum;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use UnitEnum;
class RegistrazioneUnificata extends Page implements HasForms
{
use InteractsWithForms;
protected static bool $shouldRegisterNavigation = false;
protected static ?string $navigationLabel = 'Registrazione unificata';
protected static ?string $title = 'Registrazione unificata';
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-pencil-square';
protected static UnitEnum|string|null $navigationGroup = 'GESCON';
protected static ?int $navigationSort = 21;
protected static ?string $slug = 'gescon/ordinarie/registrazione-unificata';
protected string $view = 'filament.pages.gescon.registrazione-unificata';
public ?\App\Models\Stabile $stabileAttivo = null;
public array $data = [];
public static function canAccess() : bool
{
$user = Auth::user();
if (! ModuleVisibility::canAccessGesconInternal($user, ['super-admin', 'admin', 'amministratore'])) {
return false;
}
if ($user->hasAnyRole(['super-admin', 'admin'])) {
return true;
}
return $user->can('contabilita.gestioni');
}
public function mount(): void
{
$user = Auth::user();
if (! $user instanceof User) {
return;
}
$activeId = StabileContext::resolveActiveStabileId($user);
if (! $activeId) {
$this->stabileAttivo = null;
return;
}
$this->stabileAttivo = StabileContext::accessibleStabili($user)
->firstWhere('id', $activeId);
$this->getSchema('form')?->fill([
'tipo_spesa' => 'singola',
'anno_gestione' => (int) now()->format('Y'),
]);
}
public function form(Schema $schema): Schema
{
return $schema
->statePath('data')
->components($this->getFormSchema());
}
protected function getFormSchema(): array
{
return [
Grid::make(3)->schema([
Section::make('Registrazione')
->columnSpan(2)
->schema([
Select::make('tipo_spesa')
->label('Tipo')
->options([
'singola' => 'Spesa singola',
'aggregata' => 'Acqua / Riscaldamento (aggregata)',
])
->required()
->live(),
Select::make('legacy_voce_id')
->label('Voce (GESCON)')
->options([])
->searchable()
->getSearchResultsUsing(fn(string $search): array=> $this->searchLegacyVoci($search))
->getOptionLabelUsing(fn($value): ?string => $this->getLegacyVoceLabel($value))
->live()
->afterStateUpdated(function ($state): void {
if (! $state) {
return;
}
$this->loadLegacyVoce((int) $state);
})
->required(),
Grid::make(3)->schema([
Placeholder::make('legacy_tabella')
->label('Tabella')
->content(fn(callable $get) => $get('legacy_tabella') ?: '—'),
Placeholder::make('legacy_prev_euro')
->label('Prev. (€)')
->content(fn(callable $get) => is_numeric($get('legacy_prev_euro')) ? number_format((float) $get('legacy_prev_euro'), 2, ',', '.') : '—'),
Placeholder::make('legacy_cons_euro')
->label('Cons. (€)')
->content(fn(callable $get) => is_numeric($get('legacy_cons_euro')) ? number_format((float) $get('legacy_cons_euro'), 2, ',', '.') : '—'),
]),
Select::make('tabella_millesimale_id')
->label('Tabella millesimale')
->options(fn() => $this->getTabelleOptions())
->searchable()
->required(),
TextInput::make('descrizione')
->label('Descrizione')
->maxLength(255)
->required(),
Grid::make(2)->schema([
TextInput::make('anno_gestione')
->label('Anno gestione')
->numeric()
->required(),
TextInput::make('importo_consuntivo')
->label('Importo consuntivo (€)')
->numeric()
->required(),
]),
]),
Section::make('Fattura elettronica (XML)')
->columnSpan(1)
->schema([
Select::make('fattura_elettronica_id')
->label('Seleziona fattura')
->options(fn() => $this->getFattureOptions())
->searchable()
->visible(fn(callable $get) => $get('tipo_spesa') === 'singola')
->live(),
Select::make('fatture_elettroniche_ids')
->label('Seleziona fatture (multiple)')
->options(fn() => $this->getFattureOptions())
->searchable()
->multiple()
->visible(fn(callable $get) => $get('tipo_spesa') === 'aggregata')
->live(),
Placeholder::make('xml_preview_supplier')
->label('Fornitore')
->content(fn(callable $get) => $get('fornitore_id') ? (string) ($this->getFornitoreDenominazione((int) $get('fornitore_id')) ?: '—'): '—'),
Placeholder::make('xml_preview_doc')
->label('Documento')
->content(fn(callable $get) => trim(((string) ($get('numero_documento') ?: '—')) . ' • ' . ((string) ($get('data_documento') ?: '—')))),
Placeholder::make('xml_preview_totale')
->label('Totale')
->content(fn(callable $get) => is_numeric($get('totale')) ? number_format((float) $get('totale'), 2, ',', '.') . ' €' : '—'),
Placeholder::make('xml_preview_pagamento')
->label('Pagamento')
->content(fn(callable $get) => trim(((string) ($get('modalita_pagamento') ?: '—')) . ' • ' . ((string) ($get('iban') ?: '—')))),
Placeholder::make('xml_preview_consumo')
->label('Consumo/contatore')
->content(function (callable $get) {
$val = $get('consumo_valore');
$rif = $get('consumo_riferimento');
if (! is_numeric($val) && ! $rif) {
return '—';
}
$a = is_numeric($val) ? (string) $val : '';
$b = (string) ($get('consumo_unita') ?: '');
$c = (string) ($rif ?: '');
return trim(trim($a . ' ' . $b) . ' ' . $c);
}),
]),
]),
];
}
public function applyXml(): void
{
if (! $this->stabileAttivo) {
Notification::make()->title('Seleziona uno stabile')->danger()->send();
return;
}
$state = $this->getSchema('form')?->getState() ?? [];
$fatture = $this->getSelectedFatture($state);
if ($fatture->isEmpty()) {
Notification::make()->title('Seleziona una fattura XML')->warning()->send();
return;
}
$totale = (float) $fatture->sum('totale');
$imponibile = (float) $fatture->sum('imponibile');
$iva = (float) $fatture->sum('iva');
$first = $fatture->first();
if (! $first instanceof FatturaElettronica) {
return;
}
$data = $this->getParsedOrStoredXmlData($first);
$fornitoreId = $first->fornitore_id;
$numero = $first->numero_fattura;
$dataDoc = optional($first->data_fattura)->format('Y-m-d');
$descr = (string) ($state['descrizione'] ?? '');
if ($descr === '') {
$descr = trim('Fattura ' . ($numero ?: '') . ' ' . ($fornitoreId ? ($this->getFornitoreDenominazione((int) $fornitoreId) ?: ''): ''));
}
$this->getSchema('form')?->fill([
'fornitore_id' => $fornitoreId,
'numero_documento' => $numero,
'data_documento' => $dataDoc,
'data_scadenza' => optional($first->data_scadenza)->format('Y-m-d'),
'modalita_pagamento' => $data['pagamento_modalita'] ?? null,
'iban' => $data['pagamento_iban'] ?? null,
'bic' => $data['pagamento_bic'] ?? null,
'imponibile' => $imponibile,
'iva' => $iva,
'totale' => $totale,
'importo_consuntivo' => $totale,
'consumo_valore' => isset($data['consumo_valore']) && is_numeric($data['consumo_valore']) ? (float) $data['consumo_valore'] : null,
'consumo_unita' => $data['consumo_unita'] ?? null,
'consumo_riferimento' => $data['consumo_riferimento'] ?? null,
'descrizione' => $descr,
]);
Notification::make()->title('Dati XML applicati')->success()->send();
}
public function creaRegistrazione(): void
{
if (! $this->stabileAttivo) {
Notification::make()->title('Seleziona uno stabile')->danger()->send();
return;
}
$state = $this->getSchema('form')?->getState() ?? [];
validator($state, [
'tipo_spesa' => 'required|in:singola,aggregata',
'legacy_voce_id' => 'required|integer',
'tabella_millesimale_id' => 'required|integer',
'descrizione' => 'required|string|max:255',
'anno_gestione' => 'required|integer|min:2000|max:2100',
'importo_consuntivo' => 'required|numeric|min:0',
])->validate();
$fatture = $this->getSelectedFatture($state);
if ($fatture->isEmpty()) {
Notification::make()->title('Seleziona una fattura XML')->warning()->send();
return;
}
$legacyVoceId = (int) ($state['legacy_voce_id'] ?? 0);
$legacy = $this->fetchLegacyVoce($legacyVoceId);
if (! $legacy) {
Notification::make()->title('Voce GESCON non trovata')->danger()->send();
return;
}
$tabellaId = (int) ($state['tabella_millesimale_id'] ?? 0);
$voceSpesa = $this->firstOrCreateVoceSpesaFromLegacy($legacy, $tabellaId);
DB::transaction(function () use ($fatture, $legacy, $tabellaId, $voceSpesa, $state) {
// PREVENTIVO: usa sempre prev_euro se presente
if (isset($legacy['preventivo_euro']) && is_numeric($legacy['preventivo_euro']) && (float) $legacy['preventivo_euro'] > 0) {
$preventivo = Preventivo::query()->firstOrCreate(
[
'stabile_id' => $this->stabileAttivo->id,
'anno_gestione' => (int) ($state['anno_gestione'] ?? now()->format('Y')),
'tipo_gestione' => 'ordinaria',
],
[
'descrizione' => 'Preventivo ordinario ' . ($state['anno_gestione'] ?? now()->format('Y')),
'stato' => 'bozza',
'data_creazione' => now()->toDateString(),
'versione' => 1,
],
);
$vp = VocePreventivo::query()->firstOrNew([
'preventivo_id' => $preventivo->id,
'voce_spesa_id' => $voceSpesa->id,
]);
$vp->fill([
'codice' => (string) ($legacy['cod'] ?? $voceSpesa->codice ?? ''),
'descrizione' => (string) ($legacy['descriz'] ?? $voceSpesa->descrizione),
'importo_preventivato' => (float) $legacy['preventivo_euro'],
'tabella_millesimale_id' => $tabellaId,
'ordinamento' => 0,
]);
$vp->save();
$preventivo->calcolaImportoTotale();
}
// CONSUNTIVO: registra fattura fornitore + ripartizione
$totale = (float) ($state['importo_consuntivo'] ?? 0);
$imponibile = (float) ($state['imponibile'] ?? $fatture->sum('imponibile'));
$iva = (float) ($state['iva'] ?? $fatture->sum('iva'));
$fatturaElettronicaId = null;
if (($state['tipo_spesa'] ?? 'singola') === 'singola' && ! empty($state['fattura_elettronica_id'])) {
$fatturaElettronicaId = (int) $state['fattura_elettronica_id'];
}
$fattura = \App\Modules\Contabilita\Models\FatturaFornitore::query()->create([
'stabile_id' => $this->stabileAttivo->id,
'fattura_elettronica_id' => $fatturaElettronicaId,
'fornitore_id' => $state['fornitore_id'] ?? null,
'data_registrazione' => now()->toDateString(),
'data_documento' => $state['data_documento'] ?? null,
'numero_documento' => $state['numero_documento'] ?? null,
'descrizione' => $state['descrizione'] ?? null,
'imponibile' => $imponibile,
'iva' => $iva,
'totale' => $totale,
'data_scadenza' => $state['data_scadenza'] ?? null,
'modalita_pagamento' => $state['modalita_pagamento'] ?? null,
'netto_da_pagare' => $totale,
'stato' => 'inserito',
]);
\App\Modules\Contabilita\Models\FatturaFornitoreRiga::query()->create([
'fattura_id' => $fattura->id,
'riga' => 1,
'descrizione' => (string) (($state['descrizione'] ?? '') ?: ($legacy['descriz'] ?? 'Spesa')),
'imponibile_euro' => $imponibile,
'iva_euro' => $iva,
'totale_euro' => $totale,
'tabella_millesimale_id' => $tabellaId,
]);
app(RipartizioneSpesaService::class)->calcolaRipartizione(
$voceSpesa,
$totale,
$tabellaId,
[
'tipo_ripartizione' => 'millesimale',
'note' => $state['descrizione'] ?? null,
],
);
});
Notification::make()->title('Registrazione creata')->success()->send();
}
private function getLegacyVociOptions(): array
{
if (! $this->stabileAttivo) {
return [];
}
try {
$rows = DB::connection('gescon_import')
->table('voc_spe')
->where('cod_stabile', $this->stabileAttivo->codice_stabile)
->orderBy('descriz')
->get(['id_vocspe', 'cod', 'descriz', 'tabella']);
$out = [];
foreach ($rows as $r) {
$out[(int) $r->id_vocspe] = trim((string) $r->cod) . ' — ' . trim((string) $r->descriz) . ' (TAB ' . trim((string) $r->tabella) . ')';
}
return $out;
} catch (\Throwable $e) {
return [];
}
}
private function searchLegacyVoci(string $search): array
{
if (! $this->stabileAttivo) {
return [];
}
$search = trim($search);
if ($search === '') {
return [];
}
try {
$rows = DB::connection('gescon_import')
->table('voc_spe')
->where('cod_stabile', $this->stabileAttivo->codice_stabile)
->where(function ($q) use ($search) {
$like = '%' . str_replace(['%', '_'], ['\\%', '\\_'], $search) . '%';
$q->where('cod', 'like', $like)
->orWhere('descriz', 'like', $like)
->orWhere('tabella', 'like', $like);
})
->orderBy('descriz')
->limit(50)
->get(['id_vocspe', 'cod', 'descriz', 'tabella']);
$out = [];
foreach ($rows as $r) {
$out[(int) $r->id_vocspe] = trim((string) $r->cod) . ' — ' . trim((string) $r->descriz) . ' (TAB ' . trim((string) $r->tabella) . ')';
}
return $out;
} catch (\Throwable $e) {
return [];
}
}
private function getLegacyVoceLabel($value): ?string
{
$id = is_numeric($value) ? (int) $value : null;
if (! $id || ! $this->stabileAttivo) {
return null;
}
try {
$row = DB::connection('gescon_import')
->table('voc_spe')
->where('cod_stabile', $this->stabileAttivo->codice_stabile)
->where('id_vocspe', $id)
->first(['cod', 'descriz', 'tabella']);
if (! $row) {
return null;
}
return trim((string) $row->cod) . ' — ' . trim((string) $row->descriz) . ' (TAB ' . trim((string) $row->tabella) . ')';
} catch (\Throwable $e) {
return null;
}
}
private function fetchLegacyVoce(?int $id): ?array
{
if (! $this->stabileAttivo || ! $id) {
return null;
}
try {
$row = DB::connection('gescon_import')
->table('voc_spe')
->where('cod_stabile', $this->stabileAttivo->codice_stabile)
->where('id_vocspe', $id)
->first();
if (! $row) {
return null;
}
return (array) $row;
} catch (\Throwable $e) {
return null;
}
}
private function loadLegacyVoce(int $id): void
{
$legacy = $this->fetchLegacyVoce($id);
if (! $legacy) {
return;
}
$legacyTabella = isset($legacy['tabella']) ? (string) $legacy['tabella'] : null;
$mapped = $this->mapLegacyTabellaToTabellaMillesimaleId($legacyTabella);
$current = $this->getSchema('form')?->getState() ?? [];
$descr = (string) ($current['descrizione'] ?? '');
if ($descr === '') {
$descr = (string) ($legacy['descriz'] ?? '');
}
$fill = [
'legacy_tabella' => $legacyTabella,
'legacy_prev_euro' => isset($legacy['preventivo_euro']) && is_numeric($legacy['preventivo_euro']) ? (float) $legacy['preventivo_euro'] : null,
'legacy_cons_euro' => isset($legacy['consuntivo_euro']) && is_numeric($legacy['consuntivo_euro']) ? (float) $legacy['consuntivo_euro'] : null,
'descrizione' => $descr,
];
if ($mapped) {
$fill['tabella_millesimale_id'] = $mapped;
}
$this->getSchema('form')?->fill($fill);
}
private function mapLegacyTabellaToTabellaMillesimaleId(?string $legacyTabella): ?int
{
if (! $this->stabileAttivo || ! $legacyTabella) {
return null;
}
$raw = strtoupper(trim($legacyTabella));
$candidates = [$raw];
if (! str_starts_with($raw, 'TAB')) {
$candidates[] = 'TAB.' . $raw;
}
$q = TabellaMillesimale::query()->where('stabile_id', $this->stabileAttivo->id);
foreach ($candidates as $c) {
$found = (clone $q)
->where(function ($qq) use ($c) {
$qq->where('codice_tabella', $c)
->orWhere('nome_tabella', $c);
})
->value('id');
if ($found) {
return (int) $found;
}
}
return null;
}
private function getTabelleOptions(): array
{
if (! $this->stabileAttivo) {
return [];
}
return TabellaMillesimale::query()
->where('stabile_id', $this->stabileAttivo->id)
->ordinato()
->get(['id', 'codice_tabella', 'nome_tabella', 'denominazione'])
->mapWithKeys(function (TabellaMillesimale $t) {
$label = $t->codice_tabella ?: ($t->denominazione ?: ($t->nome_tabella ?: null));
return [$t->id => ($label ?: ('Tabella #' . (int) $t->id))];
})
->all();
}
private function getFattureOptions(): array
{
if (! $this->stabileAttivo) {
return [];
}
return FatturaElettronica::query()
->where('stabile_id', $this->stabileAttivo->id)
->orderByDesc('data_fattura')
->limit(200)
->get()
->mapWithKeys(function (FatturaElettronica $f) {
$label = trim(($f->data_fattura?->format('Y-m-d') ?: '') . ' — ' . ($f->fornitore_denominazione ?: 'ND') . ' — ' . ($f->numero_fattura ?: 'ND') . ' — ' . number_format((float) $f->totale, 2, ',', '.') . '€');
return [$f->id => $label];
})
->all();
}
private function getSelectedFatture(array $state)
{
if (! $this->stabileAttivo) {
return collect();
}
if (($state['tipo_spesa'] ?? 'singola') === 'aggregata') {
$ids = array_values(array_filter(array_map('intval', (array) ($state['fatture_elettroniche_ids'] ?? []))));
if (count($ids) === 0) {
return collect();
}
return FatturaElettronica::query()
->where('stabile_id', $this->stabileAttivo->id)
->whereIn('id', $ids)
->get();
}
$id = (int) ($state['fattura_elettronica_id'] ?? 0);
if (! $id) {
return collect();
}
return FatturaElettronica::query()
->where('stabile_id', $this->stabileAttivo->id)
->where('id', $id)
->get();
}
private function getParsedOrStoredXmlData(FatturaElettronica $fattura): array
{
$data = [
'pagamento_modalita' => $fattura->pagamento_modalita,
'pagamento_iban' => $fattura->pagamento_iban,
'pagamento_bic' => $fattura->pagamento_bic,
'consumo_valore' => $fattura->consumo_valore,
'consumo_unita' => $fattura->consumo_unita,
'consumo_riferimento' => $fattura->consumo_riferimento,
];
$needsParse = empty($data['pagamento_modalita']) && empty($data['pagamento_iban']) && empty($data['consumo_valore']) && empty($data['consumo_riferimento']);
if ($needsParse && is_string($fattura->xml_content) && $fattura->xml_content !== '') {
try {
return app(FatturaElettronicaXmlParser::class)->parse($fattura->xml_content);
} catch (\Throwable $e) {
return $data;
}
}
return $data;
}
private function getFornitoreDenominazione(int $fornitoreId): ?string
{
return (string) (\App\Models\Fornitore::query()->whereKey($fornitoreId)->value('ragione_sociale')
?: \App\Models\Fornitore::query()->whereKey($fornitoreId)->value('nome'));
}
private function firstOrCreateVoceSpesaFromLegacy(array $legacy, int $tabellaId): VoceSpesa
{
$descr = trim((string) ($legacy['descriz'] ?? 'Spesa'));
$existing = VoceSpesa::query()
->where('stabile_id', $this->stabileAttivo->id)
->where('descrizione', $descr)
->first();
if ($existing) {
if (! $existing->tabella_millesimale_default_id) {
$existing->tabella_millesimale_default_id = $tabellaId;
$existing->save();
}
return $existing;
}
$categoria = $this->guessCategoria($descr);
return VoceSpesa::query()->create([
'stabile_id' => $this->stabileAttivo->id,
'descrizione' => $descr,
'tipo_gestione' => 'ordinaria',
'categoria' => $categoria,
'tabella_millesimale_default_id' => $tabellaId,
'ritenuta_acconto_default' => 0,
'attiva' => true,
'ordinamento' => 0,
]);
}
private function guessCategoria(string $descrizione): string
{
$d = mb_strtolower($descrizione);
if (str_contains($d, 'acqua') || str_contains($d, 'idrico')) {
return 'acqua';
}
if (str_contains($d, 'riscald')) {
return 'riscaldamento';
}
if (str_contains($d, 'energia') || str_contains($d, 'luce') || str_contains($d, 'elettr')) {
return 'energia';
}
return 'generale';
}
}