405 lines
15 KiB
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;
|
|
}
|
|
}
|
|
}
|