*/ private array $fatturaCache = []; /** @var array */ private array $movimentiTotaliCache = []; /** @var array|null */ private ?array $bilancioCache = null; public string $archivioScope = 'all'; 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.prima-nota'); } public function mount(): void { $scope = strtolower((string) request()->query('archivio', 'all')); $this->archivioScope = in_array($scope, ['all', 'gescon', 'netgescon'], true) ? $scope : 'all'; $this->mountInteractsWithTable(); } public function getActiveStabile(): bool { $user = Auth::user(); if (! $user instanceof User) { return false; } return (bool) StabileContext::resolveActiveStabileId($user); } protected function getHeaderActions(): array { return [ Action::make('sync_legacy_operazioni') ->label('Aggiorna da legacy') ->icon('heroicon-o-arrow-path') ->form([ Select::make('tipo') ->label('Gestione') ->options([ 'all' => 'Tutte', 'ordinaria' => 'Ordinaria', 'straordinaria' => 'Straordinaria', 'riscaldamento' => 'Riscaldamento', ]) ->default('all') ->required(), TextInput::make('anno') ->label('Anno (opzionale)') ->numeric() ->minValue(2000) ->maxValue(2100) ->nullable(), \Filament\Forms\Components\Toggle::make('dry') ->label('Solo simulazione (dry-run)') ->default(false), ]) ->action(function (array $data): void { $user = Auth::user(); if (! $user instanceof User) { Notification::make()->title('Utente non valido')->danger()->send(); return; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { Notification::make()->title('Seleziona uno stabile')->danger()->send(); return; } $params = [ '--stabile-id' => (string) $stabileId, '--tipo' => (string) ($data['tipo'] ?? 'all'), ]; if (! empty($data['anno']) && is_numeric($data['anno'])) { $params['--year'] = (string) ((int) $data['anno']); } if (! empty($data['dry'])) { $params['--dry'] = true; } $code = Artisan::call('gescon:sync-operazioni-prima-nota', $params); $output = trim(Artisan::output()); if ($code === 0) { Notification::make() ->title('Sync legacy completato') ->body($output !== '' ? $output : 'Operazione completata con successo.') ->success() ->send(); $this->resetTable(); return; } Notification::make() ->title('Errore sync legacy') ->body($output !== '' ? $output : 'Comando terminato con errore.') ->danger() ->send(); }), Action::make('archivio_chiusure') ->label('Archivio chiusure') ->icon('heroicon-o-archive-box') ->visible(fn () => Schema::hasTable('contabilita_chiusure_gestione')) ->url(fn (): string => ChiusureGestioneArchivio::getUrl()), Action::make('chiudi_gestione') ->label('Chiudi gestione') ->icon('heroicon-o-lock-closed') ->visible(fn () => Schema::hasTable('gestioni_contabili') && Schema::hasTable('contabilita_registrazioni') && Schema::hasColumn('contabilita_registrazioni', 'gestione_id') && Schema::hasTable('contabilita_chiusure_gestione')) ->form([ Select::make('gestione_id') ->label('Gestione da chiudere') ->options(fn () => $this->getGestioniOptions()) ->searchable() ->required(), DatePicker::make('data_chiusura') ->label('Data chiusura') ->required() ->default(fn () => now()->toDateString()), ]) ->action(function (array $data): void { $user = Auth::user(); if (! $user instanceof User) { Notification::make()->title('Utente non valido')->danger()->send(); return; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { Notification::make()->title('Seleziona uno stabile')->danger()->send(); return; } $gestione = GestioneContabile::query()->where('stabile_id', $stabileId)->find((int) $data['gestione_id']); if (! $gestione) { Notification::make()->title('Gestione non trovata')->danger()->send(); return; } try { $service = app(ChiusuraGestioneService::class); $service->chiudiGestione($user, $stabileId, $gestione, (string) $data['data_chiusura']); Notification::make()->title('Gestione chiusa')->success()->send(); $this->resetTable(); } catch (\Throwable $e) { Notification::make()->title('Errore chiusura')->body($e->getMessage())->danger()->send(); } }), Action::make('nuova_registrazione') ->label('Nuova registrazione') ->icon('heroicon-o-plus') ->form([ Select::make('gestione_id') ->label('Gestione') ->options(fn () => $this->getGestioniOptions()) ->searchable() ->visible(fn () => Schema::hasTable('contabilita_registrazioni') && Schema::hasColumn('contabilita_registrazioni', 'gestione_id')) ->default(function (): ?int { if (! Schema::hasTable('gestioni_contabili')) { return null; } $user = Auth::user(); if (! $user instanceof User) { return null; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { return null; } $anno = AnnoGestioneContext::resolveActiveAnno($user); $tipo = GestioneContext::resolveActiveGestione($user); $match = GestioneContabile::query() ->where('stabile_id', $stabileId) ->where('anno_gestione', $anno) ->where('tipo_gestione', $tipo) ->orderBy('numero_straordinaria') ->first(); return $match?->id; }), DatePicker::make('data_registrazione') ->label('Data') ->required() ->default(now()->toDateString()), Textarea::make('descrizione') ->label('Descrizione') ->rows(2) ->required(), Repeater::make('movimenti') ->label('Movimenti (Dare/Avere)') ->minItems(2) ->schema([ Select::make('conto_id') ->label('Conto') ->options(fn () => PianoConti::query() ->orderBy('codice') ->get() ->mapWithKeys(fn (PianoConti $c) => [(string) $c->id => $c->codice . ' - ' . $c->descrizione]) ->all()) ->searchable() ->required(), Select::make('tipo') ->label('Tipo') ->options(['dare' => 'Dare', 'avere' => 'Avere']) ->required(), TextInput::make('importo') ->label('Importo') ->required(), ]) ->columns(3) ->required(), ]) ->action(function (array $data): void { $user = Auth::user(); if (! $user instanceof User) { Notification::make()->title('Utente non valido')->danger()->send(); return; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { Notification::make()->title('Seleziona uno stabile')->danger()->send(); return; } $movimenti = $data['movimenti'] ?? []; if (! is_array($movimenti) || count($movimenti) < 2) { Notification::make()->title('Inserisci almeno 2 movimenti')->danger()->send(); return; } $totDare = 0.0; $totAvere = 0.0; foreach ($movimenti as $m) { $tipo = (string) ($m['tipo'] ?? ''); $importo = (float) str_replace(',', '.', str_replace('.', '', (string) ($m['importo'] ?? '0'))); if ($tipo === 'dare') { $totDare += $importo; } elseif ($tipo === 'avere') { $totAvere += $importo; } } if (round($totDare, 2) !== round($totAvere, 2)) { Notification::make() ->title('Registrazione non bilanciata') ->body('Totale Dare e Totale Avere devono coincidere.') ->danger() ->send(); return; } DB::transaction(function () use ($user, $stabileId, $data, $movimenti): void { $regPayload = [ 'stabile_id' => $stabileId, 'gestione_id' => (Schema::hasColumn('contabilita_registrazioni', 'gestione_id') ? (isset($data['gestione_id']) ? Registrazione::resolveGestioneIdOrDefault((int) $data['gestione_id'], $stabileId, (string) ($data['data_registrazione'] ?? '')) : null) : null), 'data_registrazione' => $data['data_registrazione'], 'description' => $data['descrizione'], 'created_by' => $user->id, 'updated_by' => $user->id, ]; if (Schema::hasColumn('contabilita_registrazioni', 'user_id')) { $regPayload['user_id'] = (int) $user->id; } /** @var Registrazione $reg */ $reg = Registrazione::query()->create($regPayload); foreach ($movimenti as $m) { $contoId = (int) ($m['conto_id'] ?? 0); $tipo = (string) ($m['tipo'] ?? ''); $imp = (float) str_replace(',', '.', str_replace('.', '', (string) ($m['importo'] ?? '0'))); if ($contoId <= 0 || ! in_array($tipo, ['dare', 'avere'], true) || $imp <= 0) { continue; } $conto = PianoConti::query()->find($contoId); $payload = $this->buildMovimentoPayload((int) $reg->id, $stabileId, $contoId, $conto, $tipo, $imp, (string) $data['descrizione']); Movimento::query()->create($payload); } $reg->assertBilanciata(); }); Notification::make()->title('Registrazione creata')->success()->send(); $this->resetTable(); }), Action::make('bilancio_apertura_manual') ->label('Bilancio di apertura') ->icon('heroicon-o-scale') ->visible(fn () => Schema::hasTable('contabilita_registrazioni') && Schema::hasTable('contabilita_movimenti') && Schema::hasTable('gestioni_contabili') && Schema::hasColumn('contabilita_registrazioni', 'gestione_id')) ->form([ Select::make('gestione_id') ->label('Gestione') ->options(fn () => $this->getGestioniOptions()) ->searchable() ->required(), DatePicker::make('data_registrazione') ->label('Data') ->required() ->default(fn () => now()->toDateString()), Textarea::make('descrizione') ->label('Descrizione') ->default('Bilancio di apertura (manuale)') ->rows(2) ->required(), Repeater::make('movimenti') ->label('Righe (Dare / Avere)') ->minItems(2) ->schema([ Select::make('conto_id') ->label('Conto') ->options(fn () => PianoConti::query() ->orderBy('codice') ->get() ->mapWithKeys(fn (PianoConti $c) => [(string) $c->id => $c->codice . ' - ' . $c->descrizione]) ->all()) ->searchable() ->required(), Select::make('tipo') ->label('Tipo') ->options(['dare' => 'Dare', 'avere' => 'Avere']) ->required(), TextInput::make('importo') ->label('Importo €') ->numeric() ->required(), ]) ->columns(3) ->required(), ]) ->action(function (array $data): void { $user = Auth::user(); if (! $user instanceof User) { Notification::make()->title('Utente non valido')->danger()->send(); return; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { Notification::make()->title('Seleziona uno stabile')->danger()->send(); return; } $gestioneId = (int) ($data['gestione_id'] ?? 0); if ($gestioneId <= 0) { Notification::make()->title('Seleziona una gestione')->danger()->send(); return; } if (Schema::hasTable('contabilita_chiusure_gestione')) { $chiusa = app(ChiusuraGestioneService::class)->isGestioneChiusa($gestioneId); if ($chiusa) { Notification::make()->title('Gestione chiusa')->body('Non è possibile inserire movimenti.')->danger()->send(); return; } } try { DB::transaction(function () use ($user, $stabileId, $gestioneId, $data): void { $regPayload = [ 'stabile_id' => $stabileId, 'gestione_id' => Registrazione::resolveGestioneIdOrDefault($gestioneId, $stabileId, (string) ($data['data_registrazione'] ?? '')), 'data_registrazione' => (string) $data['data_registrazione'], 'description' => (string) $data['descrizione'], 'created_by' => (int) $user->id, 'updated_by' => (int) $user->id, ]; if (Schema::hasColumn('contabilita_registrazioni', 'user_id')) { $regPayload['user_id'] = (int) $user->id; } /** @var Registrazione $reg */ $reg = Registrazione::query()->create($regPayload); $righe = is_array($data['movimenti'] ?? null) ? $data['movimenti'] : []; foreach ($righe as $r) { $contoId = (int) ($r['conto_id'] ?? 0); $tipo = (string) ($r['tipo'] ?? ''); $imp = round((float) ($r['importo'] ?? 0), 2); if ($contoId <= 0 || ! in_array($tipo, ['dare', 'avere'], true) || $imp <= 0) { continue; } $conto = PianoConti::query()->find($contoId); $payload = $this->buildMovimentoPayload((int) $reg->id, $stabileId, $contoId, $conto, $tipo, $imp, (string) $data['descrizione']); Movimento::query()->create($payload); } $reg->assertBilanciata(); }); Notification::make()->title('Bilancio di apertura salvato')->success()->send(); $this->resetTable(); } catch (\Throwable $e) { Notification::make()->title('Errore')->body($e->getMessage())->danger()->send(); } }), Action::make('modifica_registrazione') ->label('Modifica registrazione') ->icon('heroicon-o-pencil-square') ->form([ TextInput::make('id') ->label('ID registrazione') ->required() ->numeric(), Select::make('gestione_id') ->label('Gestione') ->options(fn () => $this->getGestioniOptions()) ->searchable() ->nullable() ->visible(fn () => Schema::hasColumn('contabilita_registrazioni', 'gestione_id')), DatePicker::make('data_registrazione') ->label('Data') ->required(), Textarea::make('descrizione') ->label('Descrizione') ->rows(2) ->required(), ]) ->action(function (array $data): void { $user = Auth::user(); if (! $user instanceof User) { Notification::make()->title('Utente non valido')->danger()->send(); return; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { Notification::make()->title('Seleziona uno stabile')->danger()->send(); return; } $id = (int) ($data['id'] ?? 0); if ($id <= 0) { Notification::make()->title('ID non valido')->danger()->send(); return; } /** @var Registrazione|null $reg */ $reg = Registrazione::query()->where('stabile_id', $stabileId)->find($id); if (! $reg) { Notification::make()->title('Registrazione non trovata')->danger()->send(); return; } $payload = [ 'data_registrazione' => (string) $data['data_registrazione'], 'description' => (string) $data['descrizione'], ]; if (Schema::hasColumn('contabilita_registrazioni', 'gestione_id')) { $payload['gestione_id'] = ! empty($data['gestione_id']) ? Registrazione::resolveGestioneIdOrDefault((int) $data['gestione_id'], $stabileId, (string) $data['data_registrazione']) : null; } $reg->fill($payload); $reg->save(); Notification::make()->title('Registrazione aggiornata')->success()->send(); $this->resetTable(); }), Action::make('elimina_registrazione') ->label('Elimina registrazione') ->icon('heroicon-o-trash') ->requiresConfirmation() ->form([ TextInput::make('id') ->label('ID registrazione') ->required() ->numeric(), ]) ->action(function (array $data): void { $user = Auth::user(); if (! $user instanceof User) { Notification::make()->title('Utente non valido')->danger()->send(); return; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { Notification::make()->title('Seleziona uno stabile')->danger()->send(); return; } $id = (int) ($data['id'] ?? 0); if ($id <= 0) { Notification::make()->title('ID non valido')->danger()->send(); return; } DB::transaction(function () use ($id, $stabileId): void { $reg = Registrazione::query()->where('stabile_id', $stabileId)->find($id); if (! $reg) { return; } if (Schema::hasTable('contabilita_movimenti')) { DB::table('contabilita_movimenti')->where('registrazione_id', $id)->delete(); } $reg->delete(); }); Notification::make()->title('Registrazione eliminata')->success()->send(); $this->resetTable(); }), ]; } protected function getTableQuery(): Builder { $user = Auth::user(); if (! $user instanceof User) { return Registrazione::query()->whereRaw('1 = 0'); } $activeStabileId = StabileContext::resolveActiveStabileId($user); if (! $activeStabileId) { return Registrazione::query()->whereRaw('1 = 0'); } $query = Registrazione::query() ->where('stabile_id', $activeStabileId) ->orderByDesc('data_registrazione') ->orderByDesc('id'); if ($this->archivioScope === 'gescon') { $this->applyGesconSourceCondition($query); } elseif ($this->archivioScope === 'netgescon') { $query->where(function ($q): void { $q->whereNull('external_reference') ->orWhere(function ($sub): void { $sub->where('external_reference', 'not like', 'GESCON:%') ->where('external_reference', 'not like', 'LEGACY:%') ->where('external_reference', 'not like', 'IMPORT_GESCON:%') ->where('external_reference', 'not like', 'OPERAZIONE_LEGACY:%'); }); })->where(function ($q): void { $q->whereNull('document_type') ->orWhereNotIn(DB::raw('UPPER(document_type)'), ['GESCON', 'LEGACY', 'LEGACY_GESCON']); }); } return $query; } public function table(Table $table): Table { return $table ->striped() ->filters([ SelectFilter::make('gestione_id') ->label('Gestione') ->options(fn () => $this->getGestioniOptions()) ->visible(fn () => Schema::hasColumn('contabilita_registrazioni', 'gestione_id')), ]) ->columns([ TextColumn::make('protocol') ->label('Prot. unico') ->getStateUsing(function (Registrazione $record): string { $protocol = trim((string) ($record->protocol ?? '')); return $protocol !== '' ? $protocol : ('PN-' . (int) $record->id); }) ->searchable() ->toggleable(), TextColumn::make('protocollo_completo') ->label('Prot. gestione') ->getStateUsing(function (Registrazione $record): string { $value = trim((string) ($record->protocollo_completo ?? '')); if ($value !== '') { return $value; } $prefix = trim((string) ($record->protocollo_prefix ?? '')); $num = (int) ($record->protocollo_numero ?? 0); return $prefix !== '' && $num > 0 ? ($prefix . '-' . $num) : '—'; }) ->toggleable(), TextColumn::make('protocollo_prefix') ->label('Prot. tipo') ->badge() ->color('gray') ->getStateUsing(function (Registrazione $record): string { $prefix = trim((string) ($record->protocollo_prefix ?? '')); return $prefix !== '' ? $prefix : '—'; }) ->toggleable(), TextColumn::make('source') ->label('Archivio') ->badge() ->color(fn (string $state): string => $state === 'GESCON' ? 'warning' : 'success') ->getStateUsing(fn (Registrazione $record): string => $this->isGesconRecord($record) ? 'GESCON' : 'NetGescon') ->toggleable(), TextColumn::make('data_registrazione') ->label('Data') ->date('d/m/Y') ->sortable(), TextColumn::make('gestioneContabile.denominazione') ->label('Gestione') ->toggleable() ->visible(fn () => Schema::hasColumn('contabilita_registrazioni', 'gestione_id')) ->getStateUsing(function (Registrazione $record): string { return $this->getGestioneLabel($record); }), TextColumn::make('tipo_movimento') ->label('Tipo movimento') ->getStateUsing(function (Registrazione $record): string { $info = $this->getFatturaSummary($record); $causale = $info['causale'] !== '' ? $info['causale'] : 'Operazione'; $fornitore = $info['fornitore'] !== '' ? (' · ' . $info['fornitore']) : ''; return $causale . $fornitore; }) ->wrap(), TextColumn::make('description') ->label('Descrizione') ->wrap() ->searchable() ->getStateUsing(function (Registrazione $record): string { $descr = trim((string) ($record->description ?? '')); if ($descr === '') { $descr = trim((string) ($record->descrizione ?? '')); } if ($descr === '') { $descr = trim((string) ($record->notes ?? '')); } return $descr !== '' ? $descr : '—'; }), TextColumn::make('totale_operazione') ->label('Totale') ->getStateUsing(function (Registrazione $record): string { $info = $this->getFatturaSummary($record); $tot = (float) ($info['totale'] ?? 0); if ($tot <= 0) { $totali = $this->getMovimentiTotals($record); $tot = max($totali['dare'], $totali['avere']); } return '€ ' . number_format($tot, 2, ',', '.'); }) ->alignRight(), TextColumn::make('ritenuta_importo') ->label('RA') ->getStateUsing(function (Registrazione $record): string { $info = $this->getFatturaSummary($record); $rit = (float) ($info['ritenuta'] ?? 0); return $rit > 0 ? ('€ ' . number_format($rit, 2, ',', '.')) : '—'; }) ->alignRight(), TextColumn::make('registro_rda') ->label('Registro RDA') ->getStateUsing(function (Registrazione $record): string { $info = $this->getFatturaSummary($record); return ((float) ($info['ritenuta'] ?? 0)) > 0 ? 'Apri' : '—'; }) ->url(function (Registrazione $record): ?string { $info = $this->getFatturaSummary($record); if (((float) ($info['ritenuta'] ?? 0)) <= 0) { return null; } return \App\Filament\Pages\Contabilita\RegistroRitenuteAccontoArchivio::getUrl(panel: 'admin-filament'); }) ->openUrlInNewTab(), TextColumn::make('dettaglio') ->label('Dettaglio') ->getStateUsing(fn (): string => 'Apri') ->url(fn (Registrazione $record): string => PrimaNotaDettaglio::getUrl(['record' => $record->id], panel: 'admin-filament')) ->openUrlInNewTab(), TextColumn::make('modifica') ->label('Modifica') ->getStateUsing(fn (): string => 'Modifica') ->url(fn (Registrazione $record): string => PrimaNotaModifica::getUrl(['record' => $record->id], panel: 'admin-filament')) ->openUrlInNewTab(), TextColumn::make('id') ->label('ID') ->toggleable(isToggledHiddenByDefault: true), ]); } private function applyGesconSourceCondition(Builder $query): void { $query->where(function ($q): void { $q->where('external_reference', 'like', 'GESCON:%') ->orWhere('external_reference', 'like', 'LEGACY:%') ->orWhere('external_reference', 'like', 'IMPORT_GESCON:%') ->orWhere('external_reference', 'like', 'OPERAZIONE_LEGACY:%') ->orWhereIn(DB::raw('UPPER(document_type)'), ['GESCON', 'LEGACY', 'LEGACY_GESCON']); }); } private function isGesconRecord(Registrazione $record): bool { $ref = strtoupper(trim((string) ($record->external_reference ?? ''))); if ( str_starts_with($ref, 'GESCON:') || str_starts_with($ref, 'LEGACY:') || str_starts_with($ref, 'IMPORT_GESCON:') || str_starts_with($ref, 'OPERAZIONE_LEGACY:') ) { return true; } $docType = strtoupper(trim((string) ($record->document_type ?? ''))); if (in_array($docType, ['GESCON', 'LEGACY', 'LEGACY_GESCON'], true)) { return true; } return false; } /** * @return array */ private function getGestioniOptions(): array { if (! Schema::hasTable('gestioni_contabili')) { return []; } $user = Auth::user(); if (! $user instanceof User) { return []; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { return []; } return GestioneContabile::query() ->where('stabile_id', $stabileId) ->orderByDesc('anno_gestione') ->orderBy('tipo_gestione') ->orderBy('numero_straordinaria') ->get() ->mapWithKeys(fn (GestioneContabile $g) => [ (string) $g->id => $g->denominazione . ' (' . $g->tipo_gestione . ' ' . $g->anno_gestione . ($g->numero_straordinaria ? ' #' . $g->numero_straordinaria : '') . ')', ]) ->all(); } private function getGestioneLabel(Registrazione $record): string { $gestioneId = (int) ($record->gestione_id ?? 0); if ($gestioneId <= 0) { return '—'; } if (Schema::hasTable('gestioni_contabili')) { $g = GestioneContabile::query() ->select(['id', 'denominazione', 'anno_gestione', 'tipo_gestione', 'protocollo_prefix']) ->find($gestioneId); if ($g) { $label = trim((string) ($g->denominazione ?? '')); if ($label === '') { $label = (string) $g->anno_gestione . ' - ' . (string) $g->tipo_gestione; } $prefix = trim((string) ($g->protocollo_prefix ?? '')); if ($prefix !== '') { $label = $prefix . ' — ' . $label; } return $label; } } if (Schema::hasTable('gestioni')) { $g = DB::table('gestioni') ->select(['id_gestione', 'anno_gestione', 'tipo_gestione']) ->where('id_gestione', $gestioneId) ->first(); if ($g) { return (string) ($g->anno_gestione ?? '') . ' - ' . (string) ($g->tipo_gestione ?? ''); } } return '—'; } /** * @return array{causale: string, fornitore: string, totale: float, ritenuta: float} */ private function getFatturaSummary(Registrazione $record): array { $id = (int) $record->id; if (isset($this->fatturaCache[$id])) { return $this->fatturaCache[$id]; } $summary = [ 'causale' => '', 'fornitore' => '', 'totale' => 0.0, 'ritenuta' => 0.0, ]; if (Schema::hasTable('contabilita_fatture_fornitori')) { $select = [ 'f.causale_contabile', 'f.totale', 'f.ritenuta_importo', 'fo.ragione_sociale', ]; if (Schema::hasColumn('fornitori', 'denominazione')) { $select[] = 'fo.denominazione'; } $row = DB::table('contabilita_fatture_fornitori as f') ->leftJoin('fornitori as fo', 'fo.id', '=', 'f.fornitore_id') ->where('f.registrazione_id', $id) ->select($select) ->first(); if ($row) { $causale = trim((string) ($row->causale_contabile ?? '')); $fornitore = trim((string) ($row->ragione_sociale ?? '')); if ($fornitore === '' && Schema::hasColumn('fornitori', 'denominazione')) { $fornitore = trim((string) ($row->denominazione ?? '')); } $summary = [ 'causale' => $causale !== '' ? $causale : 'Fattura ricevuta', 'fornitore' => $fornitore, 'totale' => (float) ($row->totale ?? 0), 'ritenuta' => (float) ($row->ritenuta_importo ?? 0), ]; } } if ($summary['causale'] === '') { $docType = trim((string) ($record->document_type ?? '')); if ($docType !== '') { $summary['causale'] = $docType; } } $this->fatturaCache[$id] = $summary; return $summary; } /** * @return array{dare: float, avere: float} */ private function getMovimentiTotals(Registrazione $record): array { $id = (int) $record->id; if (isset($this->movimentiTotaliCache[$id])) { return $this->movimentiTotaliCache[$id]; } $totali = ['dare' => 0.0, 'avere' => 0.0]; if (! Schema::hasTable('contabilita_movimenti')) { return $totali; } $rows = DB::table('contabilita_movimenti') ->select('tipo', DB::raw('SUM(importo) as totale')) ->where('registrazione_id', $id) ->groupBy('tipo') ->get(); foreach ($rows as $row) { $tipo = (string) ($row->tipo ?? ''); $val = (float) ($row->totale ?? 0); if ($tipo === 'dare') { $totali['dare'] = $val; } elseif ($tipo === 'avere') { $totali['avere'] = $val; } } $this->movimentiTotaliCache[$id] = $totali; return $totali; } /** * @return array */ public function getBilancioData(): array { if ($this->bilancioCache !== null) { return $this->bilancioCache; } $user = Auth::user(); if (! $user instanceof User) { return $this->bilancioCache = [ 'totale_dare' => 0.0, 'totale_avere' => 0.0, 'sezioni' => [], ]; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId || ! Schema::hasTable('contabilita_movimenti') || ! Schema::hasTable('contabilita_registrazioni')) { return $this->bilancioCache = [ 'totale_dare' => 0.0, 'totale_avere' => 0.0, 'sezioni' => [], ]; } $select = [ 'm.conto_id', 'c.codice', 'c.denominazione', 'c.descrizione', 'c.tipo_conto', DB::raw("SUM(CASE WHEN m.tipo = 'dare' THEN m.importo ELSE 0 END) as dare"), DB::raw("SUM(CASE WHEN m.tipo = 'avere' THEN m.importo ELSE 0 END) as avere"), ]; $query = DB::table('contabilita_movimenti as m') ->join('contabilita_registrazioni as r', 'r.id', '=', 'm.registrazione_id') ->leftJoin('contabilita_piano_conti as c', 'c.id', '=', 'm.conto_id') ->where('r.stabile_id', $stabileId); if (Schema::hasColumn('contabilita_registrazioni', 'deleted_at')) { $query->whereNull('r.deleted_at'); } $rows = $query ->select($select) ->groupBy('m.conto_id', 'c.codice', 'c.denominazione', 'c.descrizione', 'c.tipo_conto') ->get(); $totaleDare = 0.0; $totaleAvere = 0.0; $finanziari = []; $variazioni = []; $economici = []; foreach ($rows as $row) { $dare = (float) ($row->dare ?? 0); $avere = (float) ($row->avere ?? 0); if (abs($dare) < 0.005 && abs($avere) < 0.005) { continue; } $totaleDare += $dare; $totaleAvere += $avere; $codice = trim((string) ($row->codice ?? '')); $desc = trim((string) ($row->denominazione ?? $row->descrizione ?? '')); $label = $codice !== '' ? $codice : ('Conto #' . (int) ($row->conto_id ?? 0)); if ($desc !== '') { $label .= ' - ' . $desc; } $item = [ 'conto_id' => (int) ($row->conto_id ?? 0), 'label' => $label, 'dare' => $dare, 'avere' => $avere, 'saldo' => $dare - $avere, ]; $tipoConto = strtoupper(trim((string) ($row->tipo_conto ?? ''))); if (in_array($tipoConto, ['ATTIVO', 'PASSIVO'], true)) { $finanziari[] = $item; } elseif (in_array($tipoConto, ['RICAVO', 'COSTO'], true)) { $economici[] = $item; } else { $variazioni[] = $item; } } $this->bilancioCache = [ 'totale_dare' => $totaleDare, 'totale_avere' => $totaleAvere, 'sezioni' => [ [ 'titolo' => 'Conti finanziari (crediti/debiti)', 'righe' => $finanziari, ], [ 'titolo' => 'Variazioni finanziarie (entrate/uscite)', 'righe' => $variazioni, ], [ 'titolo' => 'Conti economici (costi/ricavi)', 'righe' => $economici, ], ], ]; return $this->bilancioCache; } private function buildMovimentoPayload( int $registrazioneId, int $stabileId, int $contoId, ?PianoConti $conto, string $tipo, float $importo, string $descrizione ): array { $payload = [ 'registrazione_id' => $registrazioneId, 'conto_id' => $contoId, 'tipo' => $tipo, 'importo' => $importo, ]; if (Schema::hasColumn('contabilita_movimenti', 'entry_id')) { $payload['entry_id'] = $registrazioneId; } $code = trim((string) ($conto?->codice ?? '')); if ($code === '') { $code = (string) $contoId; } $normalized = preg_replace('/[^A-Za-z0-9]/', '', strtoupper($code)) ?: (string) $contoId; $masterCode = str_pad(substr($normalized, 0, 3), 3, '0', STR_PAD_LEFT); $accountCode = str_pad(substr($normalized, 0, 8), 8, '0', STR_PAD_LEFT); $subAccountCode = str_pad(substr($normalized, -5), 5, '0', STR_PAD_LEFT); if (Schema::hasColumn('contabilita_movimenti', 'chart_account_id')) { $chartAccountId = $this->resolveLegacyChartAccountId($stabileId, $contoId, $conto, $masterCode, $accountCode, $subAccountCode); if ($chartAccountId > 0) { $payload['chart_account_id'] = $chartAccountId; } } if (Schema::hasColumn('contabilita_movimenti', 'master_code')) { $payload['master_code'] = $masterCode; } if (Schema::hasColumn('contabilita_movimenti', 'account_code')) { $payload['account_code'] = $accountCode; } if (Schema::hasColumn('contabilita_movimenti', 'subaccount_code')) { $payload['subaccount_code'] = $subAccountCode; } if (Schema::hasColumn('contabilita_movimenti', 'debit_amount')) { $payload['debit_amount'] = $tipo === 'dare' ? $importo : 0.0; } if (Schema::hasColumn('contabilita_movimenti', 'credit_amount')) { $payload['credit_amount'] = $tipo === 'avere' ? $importo : 0.0; } if (Schema::hasColumn('contabilita_movimenti', 'movement_description')) { $payload['movement_description'] = $descrizione !== '' ? $descrizione : null; } if (Schema::hasColumn('contabilita_movimenti', 'sequence')) { $payload['sequence'] = 1; } return $payload; } private function resolveLegacyChartAccountId( int $stabileId, int $contoId, ?PianoConti $conto, string $masterCode, string $accountCode, string $subAccountCode ): int { if (! Schema::hasTable('piano_conti')) { return 0; } $mastro = str_pad(substr($masterCode, 0, 4), 4, '0', STR_PAD_LEFT); $contoCode = str_pad(substr($accountCode, 0, 8), 8, '0', STR_PAD_LEFT); $sotto = str_pad(substr($subAccountCode, -5), 5, '0', STR_PAD_LEFT); $quarto = '00000'; $query = DB::table('piano_conti') ->where('mastro', $mastro) ->where('conto', $contoCode) ->where('sottoconto', $sotto) ->where('quarto_livello', $quarto); if ($stabileId > 0) { $query->where(function ($q) use ($stabileId) { $q->whereNull('stabile_id')->orWhere('stabile_id', $stabileId); }); } $existingId = $query->value('id'); if (is_numeric($existingId)) { return (int) $existingId; } $descr = trim((string) ($conto?->descrizione ?? $conto?->denominazione ?? '')); if ($descr === '') { $descr = 'Conto ' . $mastro . '.' . $contoCode . '.' . $sotto; } $tipo = strtoupper(trim((string) ($conto?->tipo ?? ''))); $tipoEnum = match ($tipo) { 'ATTIVITÀ', 'ATTIVITA', 'ATTIVO' => 'ATTIVO', 'PASSIVITÀ', 'PASSIVITA', 'PASSIVO' => 'PASSIVO', 'RICAVO' => 'RICAVO', 'PATRIMONIO NETTO', 'PATRIMONIO' => 'PATRIMONIO', default => 'COSTO', }; $tipologia = match ($tipoEnum) { 'RICAVO' => 'RIC', 'ATTIVO' => 'ATT', 'PASSIVO' => 'PAS', 'PATRIMONIO' => 'PAT', default => 'SPE', }; $id = DB::table('piano_conti')->insertGetId([ 'stabile_id' => $stabileId > 0 ? $stabileId : null, 'mastro' => $mastro, 'conto' => $contoCode, 'sottoconto' => $sotto, 'quarto_livello' => $quarto, 'denominazione' => $descr, 'tipo' => $tipoEnum, 'tipologia' => $tipologia, 'attivo' => 1, 'created_at' => now(), 'updated_at' => now(), ]); return is_numeric($id) ? (int) $id : 0; } }