where('gestione_id', $gestioneId) ->where('stato', 'chiusa') ->exists(); } public function resolveOrCreateConto(string $codice, string $descrizione, string $tipo): PianoConti { return $this->resolveContoSpeciale($codice, $descrizione, $tipo); } /** * Calcola saldi per conto per una gestione. * Saldo = Dare - Avere. * * @return array */ public function calcolaSaldiConti(int $gestioneId): array { if (! Schema::hasTable('contabilita_registrazioni') || ! Schema::hasTable('contabilita_movimenti')) { return []; } if (! Schema::hasColumn('contabilita_registrazioni', 'gestione_id')) { throw new RuntimeException('Prima nota non supporta gestione_id. Eseguire le migrazioni.'); } $hasTipo = Schema::hasColumn('contabilita_piano_conti', 'tipo'); $query = DB::table('contabilita_movimenti as cm') ->join('contabilita_registrazioni as cr', 'cr.id', '=', 'cm.registrazione_id') ->join('contabilita_piano_conti as cpc', 'cpc.id', '=', 'cm.conto_id') ->where('cr.gestione_id', $gestioneId) ->select([ 'cm.conto_id', 'cpc.codice', 'cpc.descrizione', $hasTipo ? 'cpc.tipo' : DB::raw('NULL as tipo'), DB::raw("SUM(CASE WHEN cm.tipo = 'dare' THEN cm.importo ELSE 0 END) as totale_dare"), DB::raw("SUM(CASE WHEN cm.tipo = 'avere' THEN cm.importo ELSE 0 END) as totale_avere"), ]); if ($hasTipo) { $query->groupBy('cm.conto_id', 'cpc.codice', 'cpc.descrizione', 'cpc.tipo'); } else { $query->groupBy('cm.conto_id', 'cpc.codice', 'cpc.descrizione'); } $rows = $query->orderBy('cpc.codice')->get(); $out = []; foreach ($rows as $r) { $saldo = (float) $r->totale_dare - (float) $r->totale_avere; // Skip zero-ish balances if (abs($saldo) < 0.005) { continue; } $out[] = [ 'conto_id' => (int) $r->conto_id, 'codice' => (string) $r->codice, 'descrizione' => (string) $r->descrizione, 'tipo' => $r->tipo !== null ? (string) $r->tipo : null, 'saldo' => round($saldo, 2), ]; } return $out; } public function chiudiGestione(User $user, int $stabileId, GestioneContabile $gestione, ?string $dataChiusura = null): ChiusuraGestione { if (! Schema::hasTable('contabilita_chiusure_gestione')) { throw new RuntimeException('Tabella chiusure gestione mancante. Eseguire le migrazioni.'); } if (! Schema::hasColumn('contabilita_registrazioni', 'gestione_id')) { throw new RuntimeException('Prima nota non supporta gestione_id.'); } $existing = ChiusuraGestione::query()->where('gestione_id', $gestione->id)->first(); if ($existing && $existing->stato === 'chiusa') { throw new RuntimeException('Gestione già chiusa.'); } $data = $dataChiusura ?: ($gestione->data_fine?->toDateString() ?? now()->toDateString()); return DB::transaction(function () use ($user, $stabileId, $gestione, $data, $existing) { $saldi = $this->calcolaSaldiConti($gestione->id); if (count($saldi) === 0) { throw new RuntimeException('Nessun movimento da chiudere per questa gestione.'); } $contoChiusura = $this->resolveContoSpeciale( (string) config('contabilita.conti_speciali.chiusura_codice', self::CODICE_CONTO_CHIUSURA), (string) config('contabilita.conti_speciali.chiusura_descrizione', 'Conto chiusura esercizio'), (string) config('contabilita.conti_speciali.chiusura_tipo', 'Patrimonio Netto'), ); $regPayload = [ 'stabile_id' => $stabileId, 'gestione_id' => Registrazione::resolveGestioneIdOrDefault((int) $gestione->id, $stabileId, $data), 'data_registrazione' => $data, 'descrizione' => 'Chiusura gestione: ' . ($gestione->denominazione ?? ('ID ' . $gestione->id)), '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); $totDare = 0.0; $totAvere = 0.0; foreach ($saldi as $s) { $saldo = (float) $s['saldo']; if ($saldo > 0) { // saldo dare -> credito per azzerare Movimento::query()->create([ 'registrazione_id' => $reg->id, 'conto_id' => (int) $s['conto_id'], 'tipo' => 'avere', 'importo' => $saldo, ]); $totAvere += $saldo; } else { $importo = abs($saldo); Movimento::query()->create([ 'registrazione_id' => $reg->id, 'conto_id' => (int) $s['conto_id'], 'tipo' => 'dare', 'importo' => $importo, ]); $totDare += $importo; } } $diff = round($totDare - $totAvere, 2); if ($diff > 0) { Movimento::query()->create([ 'registrazione_id' => $reg->id, 'conto_id' => $contoChiusura->id, 'tipo' => 'avere', 'importo' => $diff, ]); } elseif ($diff < 0) { Movimento::query()->create([ 'registrazione_id' => $reg->id, 'conto_id' => $contoChiusura->id, 'tipo' => 'dare', 'importo' => abs($diff), ]); } $reg->assertBilanciata(); $chiusura = $existing ?: new ChiusuraGestione(); $chiusura->fill([ 'stabile_id' => $stabileId, 'gestione_id' => $gestione->id, 'data_chiusura' => $data, 'stato' => 'chiusa', 'registrazione_chiusura_id' => $reg->id, 'saldi_conti' => $saldi, 'created_by' => $chiusura->exists ? $chiusura->created_by : $user->id, 'updated_by' => $user->id, ]); $chiusura->save(); return $chiusura; }); } public function generaAperturaDaChiusura(User $user, int $stabileId, ChiusuraGestione $chiusura, GestioneContabile $gestioneDestinazione, ?string $dataApertura = null): Registrazione { $saldi = $chiusura->saldi_conti; if (! is_array($saldi) || count($saldi) === 0) { throw new RuntimeException('Saldi di chiusura non disponibili.'); } $data = $dataApertura ?: ($gestioneDestinazione->data_inizio?->toDateString() ?? now()->toDateString()); $contoApertura = $this->resolveContoSpeciale( (string) config('contabilita.conti_speciali.apertura_codice', self::CODICE_CONTO_APERTURA), (string) config('contabilita.conti_speciali.apertura_descrizione', 'Conto apertura esercizio'), (string) config('contabilita.conti_speciali.apertura_tipo', 'Patrimonio Netto'), ); return DB::transaction(function () use ($user, $stabileId, $gestioneDestinazione, $data, $saldi, $contoApertura) { $regPayload = [ 'stabile_id' => $stabileId, 'gestione_id' => Registrazione::resolveGestioneIdOrDefault((int) $gestioneDestinazione->id, $stabileId, $data), 'data_registrazione' => $data, 'descrizione' => 'Apertura gestione: ' . ($gestioneDestinazione->denominazione ?? ('ID ' . $gestioneDestinazione->id)), '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); $totDare = 0.0; $totAvere = 0.0; foreach ($saldi as $s) { $tipo = isset($s['tipo']) ? (string) $s['tipo'] : null; // Riportiamo solo conti patrimoniali: debiti/crediti/cassa/banca/fondi. if (! in_array($tipo, ['Attività', 'Passività', 'Patrimonio Netto'], true)) { continue; } $saldo = (float) ($s['saldo'] ?? 0); if (abs($saldo) < 0.005) { continue; } if ($saldo > 0) { // saldo dare -> riapertura con dare Movimento::query()->create([ 'registrazione_id' => $reg->id, 'conto_id' => (int) $s['conto_id'], 'tipo' => 'dare', 'importo' => $saldo, ]); $totDare += $saldo; } else { $importo = abs($saldo); Movimento::query()->create([ 'registrazione_id' => $reg->id, 'conto_id' => (int) $s['conto_id'], 'tipo' => 'avere', 'importo' => $importo, ]); $totAvere += $importo; } } $diff = round($totDare - $totAvere, 2); if ($diff > 0) { Movimento::query()->create([ 'registrazione_id' => $reg->id, 'conto_id' => $contoApertura->id, 'tipo' => 'avere', 'importo' => $diff, ]); } elseif ($diff < 0) { Movimento::query()->create([ 'registrazione_id' => $reg->id, 'conto_id' => $contoApertura->id, 'tipo' => 'dare', 'importo' => abs($diff), ]); } $reg->assertBilanciata(); return $reg; }); } public function stornaChiusura(User $user, int $stabileId, ChiusuraGestione $chiusura, ?string $dataStorno = null): ChiusuraGestione { if ($chiusura->stato !== 'chiusa') { throw new RuntimeException('La gestione non risulta chiusa.'); } if (! $chiusura->registrazione_chiusura_id) { throw new RuntimeException('Registrazione di chiusura mancante.'); } $data = $dataStorno ?: now()->toDateString(); return DB::transaction(function () use ($user, $stabileId, $chiusura, $data) { $orig = Registrazione::query()->with('movimenti')->findOrFail($chiusura->registrazione_chiusura_id); $regPayload = [ 'stabile_id' => $stabileId, 'gestione_id' => Registrazione::resolveGestioneIdOrDefault((int) $chiusura->gestione_id, $stabileId, $data), 'data_registrazione' => $data, 'descrizione' => 'Storno chiusura gestione (riapertura controllata)', '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 ($orig->movimenti as $m) { Movimento::query()->create([ 'registrazione_id' => $reg->id, 'conto_id' => (int) $m->conto_id, 'tipo' => $m->tipo === 'dare' ? 'avere' : 'dare', 'importo' => (float) $m->importo, ]); } $reg->assertBilanciata(); $chiusura->update([ 'stato' => 'riaperta', 'registrazione_storno_id' => $reg->id, 'updated_by' => $user->id, ]); return $chiusura; }); } private function resolveContoSpeciale(string $codice, string $descrizione, string $tipo): PianoConti { /** @var PianoConti|null $conto */ $conto = PianoConti::query()->where('codice', $codice)->first(); if ($conto) { return $conto; } $model = new PianoConti(); $connection = $model->getConnection(); $schema = $connection->getSchemaBuilder(); $table = $model->getTable(); $data = [ 'codice' => $codice, ]; $data['descrizione'] = $descrizione; $data['denominazione'] = $descrizione; if ($schema->hasColumn($table, 'is_master')) { $data['is_master'] = false; } if ($schema->hasColumn($table, 'tipo')) { $data['tipo'] = $tipo; } if ($schema->hasColumn($table, 'parent_id')) { $data['parent_id'] = null; } try { $model = new PianoConti(); $model->forceFill($data); $model->save(); return $model; } catch (QueryException $e) { $message = $e->getMessage(); $columns = ['denominazione', 'descrizione', 'is_master', 'tipo', 'parent_id']; foreach ($columns as $column) { if (str_contains($message, "Unknown column '{$column}'")) { unset($data[$column]); $model = new PianoConti(); $model->forceFill($data); $model->save(); return $model; } } if (str_contains($message, 'denominazione') && ! isset($data['denominazione'])) { $data['denominazione'] = $descrizione; $model = new PianoConti(); $model->forceFill($data); $model->save(); return $model; } throw $e; } } }