netgescon-day0/app/Modules/Contabilita/Services/ChiusuraGestioneService.php

405 lines
15 KiB
PHP

<?php
namespace App\Modules\Contabilita\Services;
use App\Models\GestioneContabile;
use App\Models\User;
use App\Modules\Contabilita\Models\ChiusuraGestione;
use App\Modules\Contabilita\Models\Movimento;
use App\Modules\Contabilita\Models\PianoConti;
use App\Modules\Contabilita\Models\Registrazione;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use RuntimeException;
class ChiusuraGestioneService
{
public const CODICE_CONTO_CHIUSURA = '9999';
public const CODICE_CONTO_APERTURA = '9998';
public function isGestioneChiusa(int $gestioneId): bool
{
if (! Schema::hasTable('contabilita_chiusure_gestione')) {
return false;
}
return \App\Modules\Contabilita\Models\ChiusuraGestione::query()
->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<int, array{conto_id:int, codice:string, descrizione:string, tipo:?string, saldo:float}>
*/
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;
}
}
}