hasAnyRole(['super-admin', 'admin'])) { return true; } return $user->can('contabilita.prima-nota'); } public function mount(int|string $record): void { $user = Auth::user(); if (! $user instanceof User) { abort(403); } $reg = Registrazione::query()->find((int) $record); if (! $reg) { abort(404); } $allowed = StabileContext::accessibleStabili($user)->pluck('id')->map(fn ($v) => (int) $v)->all(); $recordStabileId = (int) ($reg->stabile_id ?? 0); if ($recordStabileId > 0 && ! in_array($recordStabileId, $allowed, true)) { abort(403); } if ($recordStabileId > 0) { StabileContext::setActiveStabileId($user, $recordStabileId); } $this->registrazione = $reg; $movimenti = Movimento::query() ->where('registrazione_id', (int) $reg->id) ->orderBy('id') ->get() ->map(fn (Movimento $m) => [ 'conto_id' => $m->conto_id, 'tipo' => $m->tipo, 'importo' => $m->importo, ]) ->all(); $this->getSchema('form')?->fill([ 'gestione_id' => $reg->gestione_id, 'data_registrazione' => optional($reg->data_registrazione)->toDateString(), 'descrizione' => $reg->description ?? $reg->descrizione ?? $reg->notes ?? '', 'movimenti' => $movimenti, ]); } public function form(Schema $schema): Schema { return $schema ->statePath('data') ->schema([ Section::make('Dati registrazione') ->columns(2) ->schema([ Select::make('gestione_id') ->label('Gestione') ->options(fn () => $this->getGestioniOptions()) ->searchable() ->visible(fn () => DbSchema::hasColumn('contabilita_registrazioni', 'gestione_id')), DatePicker::make('data_registrazione') ->label('Data') ->required(), Textarea::make('descrizione') ->label('Descrizione') ->rows(2) ->columnSpanFull() ->required(), ]), Section::make('Movimenti (Partita doppia)') ->schema([ Repeater::make('movimenti') ->label('Righe') ->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() ->live(), TextInput::make('importo') ->label('Importo') ->numeric() ->required() ->live(onBlur: true), ]) ->columns(3) ->required(), ]), ]); } public function getHeaderSummary(): array { $state = is_array($this->data ?? null) ? $this->data : []; $movimenti = $state['movimenti'] ?? []; if (empty($movimenti) && isset($this->registrazione) && $this->registrazione->id) { $movimenti = Movimento::query() ->where('registrazione_id', (int) $this->registrazione->id) ->orderBy('id') ->get() ->map(fn (Movimento $m) => [ 'conto_id' => $m->conto_id, 'tipo' => $m->tipo, 'importo' => $m->importo, ]) ->all(); } $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; } } $delta = round($totDare - $totAvere, 2); return [ 'totale_dare' => $totDare, 'totale_avere' => $totAvere, 'delta' => $delta, 'is_bilanciata' => abs($delta) < 0.005, ]; } protected function getHeaderActions(): array { return [ Action::make('salva') ->label('Salva') ->icon('heroicon-o-check') ->action('submit'), Action::make('torna') ->label('Torna alla lista') ->icon('heroicon-o-arrow-left') ->url(fn (): string => PrimaNotaArchivio::getUrl(panel: 'admin-filament')), ]; } public function submit(): void { $state = $this->getSchema('form')?->getState() ?? []; if (is_array($state['data'] ?? null)) { $state = $state['data']; } elseif (is_array($this->data ?? null) && ! empty($this->data)) { $state = $this->data; } $movimenti = $state['movimenti'] ?? []; if (! is_array($movimenti) || count($movimenti) < 2) { Notification::make()->title('Inserisci almeno 2 movimenti')->danger()->send(); return; } 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) { Notification::make()->title('Righe non valide')->body('Ogni riga deve avere Conto, Tipo e Importo validi.')->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; } $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; } DB::transaction(function () use ($stabileId, $state, $movimenti): void { $payload = [ 'data_registrazione' => (string) ($state['data_registrazione'] ?? ''), 'description' => (string) ($state['descrizione'] ?? ''), ]; if (DbSchema::hasColumn('contabilita_registrazioni', 'gestione_id')) { $payload['gestione_id'] = ! empty($state['gestione_id']) ? Registrazione::resolveGestioneIdOrDefault((int) $state['gestione_id'], $stabileId, (string) ($state['data_registrazione'] ?? '')) : null; } $this->registrazione->fill($payload); $this->registrazione->save(); if (DbSchema::hasTable('contabilita_movimenti')) { DB::table('contabilita_movimenti') ->where('registrazione_id', (int) $this->registrazione->id) ->delete(); } 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) $this->registrazione->id, $stabileId, $contoId, $conto, $tipo, $imp, (string) ($state['descrizione'] ?? '')); Movimento::query()->create($payload); } $this->registrazione->assertBilanciata(); }); Notification::make()->title('Registrazione aggiornata')->success()->send(); } /** * @return array */ private function getGestioniOptions(): array { if (! DbSchema::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 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 (DbSchema::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 (DbSchema::hasColumn('contabilita_movimenti', 'chart_account_id')) { $chartAccountId = $this->resolveLegacyChartAccountId($stabileId, $contoId, $conto, $masterCode, $accountCode, $subAccountCode); if ($chartAccountId > 0) { $payload['chart_account_id'] = $chartAccountId; } } if (DbSchema::hasColumn('contabilita_movimenti', 'master_code')) { $payload['master_code'] = $masterCode; } if (DbSchema::hasColumn('contabilita_movimenti', 'account_code')) { $payload['account_code'] = $accountCode; } if (DbSchema::hasColumn('contabilita_movimenti', 'subaccount_code')) { $payload['subaccount_code'] = $subAccountCode; } if (DbSchema::hasColumn('contabilita_movimenti', 'debit_amount')) { $payload['debit_amount'] = $tipo === 'dare' ? $importo : 0.0; } if (DbSchema::hasColumn('contabilita_movimenti', 'credit_amount')) { $payload['credit_amount'] = $tipo === 'avere' ? $importo : 0.0; } if (DbSchema::hasColumn('contabilita_movimenti', 'movement_description')) { $payload['movement_description'] = $descrizione !== '' ? $descrizione : null; } if (DbSchema::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 (! DbSchema::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; } }