861 lines
33 KiB
Markdown
861 lines
33 KiB
Markdown
# 📊 PIANO DEI CONTI - Struttura Contabile Condominiale
|
||
|
||
## 🎯 **OVERVIEW**
|
||
Il Piano dei Conti rappresenta la struttura contabile standardizzata per la gestione condominiale secondo la normativa italiana. Ogni conto è collegato ai millesimi di ripartizione e consente la generazione automatica di mastrini e chiusure per gestione.
|
||
|
||
---
|
||
|
||
## 🗄️ **STRUTTURA MYSQL**
|
||
|
||
```sql
|
||
CREATE TABLE piano_conti (
|
||
-- Identificativi
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||
codice_conto VARCHAR(20) NOT NULL, -- Codice gerarchico (es: 100.001)
|
||
denominazione VARCHAR(255) NOT NULL, -- Nome del conto
|
||
|
||
-- Struttura Gerarchica
|
||
padre_id BIGINT NULL, -- FK piano_conti.id (struttura ad albero)
|
||
livello TINYINT NOT NULL DEFAULT 1, -- Livello gerarchico (1-5)
|
||
tipo_conto ENUM('ATTIVO','PASSIVO','RICAVO','COSTO','PATRIMONIO') NOT NULL,
|
||
sottotipo VARCHAR(50) NULL, -- Sottocategoria specifica
|
||
|
||
-- Gestione Multi-Tenant
|
||
gestione_id BIGINT NOT NULL, -- FK gestioni.id
|
||
|
||
-- Configurazione Contabile
|
||
natura_saldo ENUM('DARE','AVERE') NOT NULL, -- Natura normale del saldo
|
||
utilizzabile_registrazioni BOOLEAN DEFAULT TRUE, -- Se utilizzabile per registrazioni dirette
|
||
richiede_dettaglio BOOLEAN DEFAULT FALSE, -- Se richiede sottoconto obbligatorio
|
||
|
||
-- Ripartizione Condominiale
|
||
ripartizione_default ENUM('MILLESIMI_GENERALI','MILLESIMI_SCALE','MILLESIMI_ASCENSORE','MILLESIMI_RISCALDAMENTO','PARTI_UGUALI','PERSONALIZZATA') DEFAULT 'MILLESIMI_GENERALI',
|
||
ripartizione_personalizzata JSON NULL, -- Configurazione ripartizione custom
|
||
|
||
-- Normativa e Compliance
|
||
art_1131_cc BOOLEAN DEFAULT FALSE, -- Art. 1131 Cod. Civile (obblighi amministratore)
|
||
art_1133_cc BOOLEAN DEFAULT FALSE, -- Art. 1133 Cod. Civile (manutenzione ordinaria)
|
||
art_1134_cc BOOLEAN DEFAULT FALSE, -- Art. 1134 Cod. Civile (innovazioni)
|
||
categoria_tributi VARCHAR(50) NULL, -- Categoria per dichiarazioni fiscali
|
||
|
||
-- Automazioni e Calcoli
|
||
formula_automatica TEXT NULL, -- Formula per calcoli automatici
|
||
soglia_attenzione DECIMAL(12,2) NULL, -- Soglia per alert
|
||
budget_annuale DECIMAL(12,2) NULL, -- Budget previsto per l'anno
|
||
|
||
-- Integrazione Sistema
|
||
codice_importazione VARCHAR(50) NULL, -- Codice per importazioni esterne
|
||
conto_collegato_id BIGINT NULL, -- FK piano_conti.id (conto di contropartita)
|
||
|
||
-- Stato e Configurazione
|
||
attivo BOOLEAN DEFAULT TRUE,
|
||
chiuso_esercizio BOOLEAN DEFAULT FALSE, -- Se chiuso per l'esercizio corrente
|
||
note TEXT NULL,
|
||
|
||
-- Audit
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
created_by BIGINT NULL, -- FK users.id
|
||
updated_by BIGINT NULL, -- FK users.id
|
||
|
||
-- Indici
|
||
UNIQUE KEY unique_codice_gestione (codice_conto, gestione_id),
|
||
INDEX idx_gestione (gestione_id),
|
||
INDEX idx_padre (padre_id),
|
||
INDEX idx_tipo_conto (tipo_conto),
|
||
INDEX idx_ripartizione (ripartizione_default),
|
||
INDEX idx_attivo (attivo),
|
||
INDEX idx_livello (livello),
|
||
|
||
-- Foreign Keys
|
||
FOREIGN KEY (gestione_id) REFERENCES gestioni(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (padre_id) REFERENCES piano_conti(id) ON DELETE SET NULL,
|
||
FOREIGN KEY (conto_collegato_id) REFERENCES piano_conti(id) ON DELETE SET NULL,
|
||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
|
||
FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL,
|
||
|
||
-- Constraints
|
||
CONSTRAINT chk_livello CHECK (livello BETWEEN 1 AND 5),
|
||
CONSTRAINT chk_soglia_positiva CHECK (soglia_attenzione >= 0),
|
||
CONSTRAINT chk_budget_positivo CHECK (budget_annuale >= 0),
|
||
CONSTRAINT chk_padre_diverso CHECK (padre_id != id)
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
## 📋 **MIGRATION LARAVEL**
|
||
|
||
```php
|
||
<?php
|
||
|
||
use Illuminate\Database\Migrations\Migration;
|
||
use Illuminate\Database\Schema\Blueprint;
|
||
use Illuminate\Support\Facades\Schema;
|
||
|
||
return new class extends Migration
|
||
{
|
||
public function up()
|
||
{
|
||
Schema::create('piano_conti', function (Blueprint $table) {
|
||
// Identificativi
|
||
$table->id();
|
||
$table->string('codice_conto', 20);
|
||
$table->string('denominazione');
|
||
|
||
// Struttura Gerarchica
|
||
$table->foreignId('padre_id')->nullable()->constrained('piano_conti')->onDelete('set null');
|
||
$table->tinyInteger('livello')->default(1);
|
||
$table->enum('tipo_conto', ['ATTIVO','PASSIVO','RICAVO','COSTO','PATRIMONIO']);
|
||
$table->string('sottotipo', 50)->nullable();
|
||
|
||
// Multi-Tenant
|
||
$table->foreignId('gestione_id')->constrained('gestioni')->onDelete('cascade');
|
||
|
||
// Configurazione Contabile
|
||
$table->enum('natura_saldo', ['DARE','AVERE']);
|
||
$table->boolean('utilizzabile_registrazioni')->default(true);
|
||
$table->boolean('richiede_dettaglio')->default(false);
|
||
|
||
// Ripartizione
|
||
$table->enum('ripartizione_default', [
|
||
'MILLESIMI_GENERALI',
|
||
'MILLESIMI_SCALE',
|
||
'MILLESIMI_ASCENSORE',
|
||
'MILLESIMI_RISCALDAMENTO',
|
||
'PARTI_UGUALI',
|
||
'PERSONALIZZATA'
|
||
])->default('MILLESIMI_GENERALI');
|
||
$table->json('ripartizione_personalizzata')->nullable();
|
||
|
||
// Normativa
|
||
$table->boolean('art_1131_cc')->default(false);
|
||
$table->boolean('art_1133_cc')->default(false);
|
||
$table->boolean('art_1134_cc')->default(false);
|
||
$table->string('categoria_tributi', 50)->nullable();
|
||
|
||
// Automazioni
|
||
$table->text('formula_automatica')->nullable();
|
||
$table->decimal('soglia_attenzione', 12, 2)->nullable();
|
||
$table->decimal('budget_annuale', 12, 2)->nullable();
|
||
|
||
// Sistema
|
||
$table->string('codice_importazione', 50)->nullable();
|
||
$table->foreignId('conto_collegato_id')->nullable()->constrained('piano_conti')->onDelete('set null');
|
||
|
||
// Stato
|
||
$table->boolean('attivo')->default(true);
|
||
$table->boolean('chiuso_esercizio')->default(false);
|
||
$table->text('note')->nullable();
|
||
|
||
// Audit
|
||
$table->timestamps();
|
||
$table->foreignId('created_by')->nullable()->constrained('users')->onDelete('set null');
|
||
$table->foreignId('updated_by')->nullable()->constrained('users')->onDelete('set null');
|
||
|
||
// Indici
|
||
$table->unique(['codice_conto', 'gestione_id'], 'unique_codice_gestione');
|
||
$table->index(['gestione_id']);
|
||
$table->index(['padre_id']);
|
||
$table->index(['tipo_conto']);
|
||
$table->index(['ripartizione_default']);
|
||
$table->index(['attivo']);
|
||
$table->index(['livello']);
|
||
|
||
// Constraints
|
||
$table->check('livello >= 1 AND livello <= 5');
|
||
$table->check('soglia_attenzione >= 0');
|
||
$table->check('budget_annuale >= 0');
|
||
});
|
||
|
||
// Trigger per impedire autoreferenza padre
|
||
DB::unprepared('
|
||
CREATE TRIGGER trg_piano_conti_no_self_parent
|
||
BEFORE INSERT ON piano_conti
|
||
FOR EACH ROW
|
||
BEGIN
|
||
IF NEW.padre_id = NEW.id THEN
|
||
SIGNAL SQLSTATE "45000" SET MESSAGE_TEXT = "Un conto non può essere padre di se stesso";
|
||
END IF;
|
||
END
|
||
');
|
||
|
||
DB::unprepared('
|
||
CREATE TRIGGER trg_piano_conti_no_self_parent_update
|
||
BEFORE UPDATE ON piano_conti
|
||
FOR EACH ROW
|
||
BEGIN
|
||
IF NEW.padre_id = NEW.id THEN
|
||
SIGNAL SQLSTATE "45000" SET MESSAGE_TEXT = "Un conto non può essere padre di se stesso";
|
||
END IF;
|
||
END
|
||
');
|
||
}
|
||
|
||
public function down()
|
||
{
|
||
DB::unprepared('DROP TRIGGER IF EXISTS trg_piano_conti_no_self_parent');
|
||
DB::unprepared('DROP TRIGGER IF EXISTS trg_piano_conti_no_self_parent_update');
|
||
Schema::dropIfExists('piano_conti');
|
||
}
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## 🏗️ **ELOQUENT MODEL**
|
||
|
||
```php
|
||
<?php
|
||
|
||
namespace App\Models;
|
||
|
||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||
use Illuminate\Database\Eloquent\Model;
|
||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||
use App\Traits\MultiTenantTrait;
|
||
|
||
/**
|
||
* Piano dei Conti Model
|
||
*
|
||
* Gestisce la struttura contabile condominiale con supporto per:
|
||
* - Struttura gerarchica ad albero
|
||
* - Ripartizione millesimi automatica
|
||
* - Conformità normativa Codice Civile
|
||
* - Mastrini e chiusure automatiche
|
||
*/
|
||
class PianoConto extends Model
|
||
{
|
||
use HasFactory, MultiTenantTrait;
|
||
|
||
protected $table = 'piano_conti';
|
||
|
||
protected $fillable = [
|
||
'codice_conto',
|
||
'denominazione',
|
||
'padre_id',
|
||
'livello',
|
||
'tipo_conto',
|
||
'sottotipo',
|
||
'gestione_id',
|
||
'natura_saldo',
|
||
'utilizzabile_registrazioni',
|
||
'richiede_dettaglio',
|
||
'ripartizione_default',
|
||
'ripartizione_personalizzata',
|
||
'art_1131_cc',
|
||
'art_1133_cc',
|
||
'art_1134_cc',
|
||
'categoria_tributi',
|
||
'formula_automatica',
|
||
'soglia_attenzione',
|
||
'budget_annuale',
|
||
'codice_importazione',
|
||
'conto_collegato_id',
|
||
'attivo',
|
||
'chiuso_esercizio',
|
||
'note',
|
||
'created_by',
|
||
'updated_by'
|
||
];
|
||
|
||
protected $casts = [
|
||
'attivo' => 'boolean',
|
||
'utilizzabile_registrazioni' => 'boolean',
|
||
'richiede_dettaglio' => 'boolean',
|
||
'chiuso_esercizio' => 'boolean',
|
||
'art_1131_cc' => 'boolean',
|
||
'art_1133_cc' => 'boolean',
|
||
'art_1134_cc' => 'boolean',
|
||
'ripartizione_personalizzata' => 'array',
|
||
'soglia_attenzione' => 'decimal:2',
|
||
'budget_annuale' => 'decimal:2',
|
||
'livello' => 'integer'
|
||
];
|
||
|
||
// ==================== RELATIONSHIPS ====================
|
||
|
||
/**
|
||
* Gestione di appartenenza (Multi-tenant)
|
||
*/
|
||
public function gestione(): BelongsTo
|
||
{
|
||
return $this->belongsTo(Gestione::class);
|
||
}
|
||
|
||
/**
|
||
* Conto padre (struttura gerarchica)
|
||
*/
|
||
public function padre(): BelongsTo
|
||
{
|
||
return $this->belongsTo(PianoConto::class, 'padre_id');
|
||
}
|
||
|
||
/**
|
||
* Conti figli
|
||
*/
|
||
public function figli(): HasMany
|
||
{
|
||
return $this->hasMany(PianoConto::class, 'padre_id');
|
||
}
|
||
|
||
/**
|
||
* Conto collegato per contropartite automatiche
|
||
*/
|
||
public function contoCollegato(): BelongsTo
|
||
{
|
||
return $this->belongsTo(PianoConto::class, 'conto_collegato_id');
|
||
}
|
||
|
||
/**
|
||
* Registrazioni contabili
|
||
*/
|
||
public function registrazioniContabili(): HasMany
|
||
{
|
||
return $this->hasMany(RegistrazioneContabile::class, 'conto_id');
|
||
}
|
||
|
||
/**
|
||
* Movimenti mastrino
|
||
*/
|
||
public function movimentiMastrino(): HasMany
|
||
{
|
||
return $this->hasMany(MovimentoMastrino::class, 'conto_id');
|
||
}
|
||
|
||
/**
|
||
* Utente creatore
|
||
*/
|
||
public function createdBy(): BelongsTo
|
||
{
|
||
return $this->belongsTo(User::class, 'created_by');
|
||
}
|
||
|
||
/**
|
||
* Utente modificatore
|
||
*/
|
||
public function updatedBy(): BelongsTo
|
||
{
|
||
return $this->belongsTo(User::class, 'updated_by');
|
||
}
|
||
|
||
// ==================== SCOPES ====================
|
||
|
||
/**
|
||
* Solo conti attivi
|
||
*/
|
||
public function scopeAttivi($query)
|
||
{
|
||
return $query->where('attivo', true);
|
||
}
|
||
|
||
/**
|
||
* Solo conti utilizzabili per registrazioni
|
||
*/
|
||
public function scopeUtilizzabili($query)
|
||
{
|
||
return $query->where('utilizzabile_registrazioni', true)->where('attivo', true);
|
||
}
|
||
|
||
/**
|
||
* Filtra per tipo conto
|
||
*/
|
||
public function scopeTipoConto($query, string $tipo)
|
||
{
|
||
return $query->where('tipo_conto', $tipo);
|
||
}
|
||
|
||
/**
|
||
* Conti di primo livello (radici)
|
||
*/
|
||
public function scopeRadici($query)
|
||
{
|
||
return $query->whereNull('padre_id');
|
||
}
|
||
|
||
/**
|
||
* Conti foglia (senza figli)
|
||
*/
|
||
public function scopeFoglie($query)
|
||
{
|
||
return $query->whereDoesntHave('figli');
|
||
}
|
||
|
||
/**
|
||
* Filtro per normativa Codice Civile
|
||
*/
|
||
public function scopeNormativa($query, string $articolo)
|
||
{
|
||
return $query->where("art_{$articolo}_cc", true);
|
||
}
|
||
|
||
// ==================== BUSINESS METHODS ====================
|
||
|
||
/**
|
||
* Genera codice conto automatico
|
||
*/
|
||
public static function generaCodiceConto(int $gestioneId, ?int $padreId = null): string
|
||
{
|
||
if (!$padreId) {
|
||
// Primo livello: trova prossimo numero disponibile
|
||
$ultimoCodice = static::where('gestione_id', $gestioneId)
|
||
->whereNull('padre_id')
|
||
->orderBy('codice_conto', 'desc')
|
||
->value('codice_conto');
|
||
|
||
$prossimoNumero = $ultimoCodice ? (int)explode('.', $ultimoCodice)[0] + 1 : 100;
|
||
return sprintf('%03d', $prossimoNumero);
|
||
}
|
||
|
||
// Livelli successivi: estende codice padre
|
||
$padre = static::find($padreId);
|
||
$ultimoFiglio = static::where('padre_id', $padreId)
|
||
->orderBy('codice_conto', 'desc')
|
||
->value('codice_conto');
|
||
|
||
if (!$ultimoFiglio) {
|
||
return $padre->codice_conto . '.001';
|
||
}
|
||
|
||
$partiCodice = explode('.', $ultimoFiglio);
|
||
$ultimoSuffisso = (int)end($partiCodice);
|
||
$nuovoSuffisso = sprintf('%03d', $ultimoSuffisso + 1);
|
||
|
||
return $padre->codice_conto . '.' . $nuovoSuffisso;
|
||
}
|
||
|
||
/**
|
||
* Calcola saldo del conto per periodo
|
||
*/
|
||
public function calcolaSaldo(\Carbon\Carbon $dataInizio, \Carbon\Carbon $dataFine): array
|
||
{
|
||
$registrazioni = $this->registrazioniContabili()
|
||
->whereBetween('data_registrazione', [$dataInizio, $dataFine])
|
||
->get();
|
||
|
||
$totaleDare = $registrazioni->sum('importo_dare');
|
||
$totaleAvere = $registrazioni->sum('importo_avere');
|
||
|
||
$saldo = ($this->natura_saldo === 'DARE')
|
||
? $totaleDare - $totaleAvere
|
||
: $totaleAvere - $totaleDare;
|
||
|
||
return [
|
||
'dare' => $totaleDare,
|
||
'avere' => $totaleAvere,
|
||
'saldo' => $saldo,
|
||
'natura_saldo' => $saldo >= 0 ? $this->natura_saldo :
|
||
($this->natura_saldo === 'DARE' ? 'AVERE' : 'DARE')
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Genera mastrino del conto
|
||
*/
|
||
public function generaMastrino(\Carbon\Carbon $dataInizio, \Carbon\Carbon $dataFine): array
|
||
{
|
||
$movimenti = $this->registrazioniContabili()
|
||
->with(['contabilita', 'contabilita.gestione', 'contabilita.unitaImmobiliare'])
|
||
->whereBetween('data_registrazione', [$dataInizio, $dataFine])
|
||
->orderBy('data_registrazione')
|
||
->orderBy('numero_registrazione')
|
||
->get();
|
||
|
||
$saldoProgressivo = 0;
|
||
$mastrino = [];
|
||
|
||
foreach ($movimenti as $movimento) {
|
||
$importoMovimento = ($this->natura_saldo === 'DARE')
|
||
? $movimento->importo_dare - $movimento->importo_avere
|
||
: $movimento->importo_avere - $movimento->importo_dare;
|
||
|
||
$saldoProgressivo += $importoMovimento;
|
||
|
||
$mastrino[] = [
|
||
'data' => $movimento->data_registrazione,
|
||
'descrizione' => $movimento->contabilita->descrizione,
|
||
'documento' => $movimento->contabilita->numero_documento,
|
||
'dare' => $movimento->importo_dare,
|
||
'avere' => $movimento->importo_avere,
|
||
'saldo_progressivo' => $saldoProgressivo,
|
||
'unita_immobiliare' => $movimento->contabilita->unitaImmobiliare?->denominazione
|
||
];
|
||
}
|
||
|
||
return $mastrino;
|
||
}
|
||
|
||
/**
|
||
* Calcola ripartizione per unità immobiliari
|
||
*/
|
||
public function calcolaRipartizione(float $importoTotale): array
|
||
{
|
||
$gestione = $this->gestione;
|
||
$unitaImmobiliari = $gestione->stabile->unitaImmobiliari()->attive()->get();
|
||
|
||
$ripartizione = [];
|
||
|
||
switch ($this->ripartizione_default) {
|
||
case 'MILLESIMI_GENERALI':
|
||
$totaleMillesimi = $unitaImmobiliari->sum('millesimi_generali');
|
||
foreach ($unitaImmobiliari as $unita) {
|
||
$quota = ($unita->millesimi_generali / $totaleMillesimi) * $importoTotale;
|
||
$ripartizione[$unita->id] = round($quota, 2);
|
||
}
|
||
break;
|
||
|
||
case 'MILLESIMI_SCALE':
|
||
$totaleMillesimi = $unitaImmobiliari->sum('millesimi_scale');
|
||
foreach ($unitaImmobiliari as $unita) {
|
||
$quota = ($unita->millesimi_scale / $totaleMillesimi) * $importoTotale;
|
||
$ripartizione[$unita->id] = round($quota, 2);
|
||
}
|
||
break;
|
||
|
||
case 'MILLESIMI_ASCENSORE':
|
||
$totaleMillesimi = $unitaImmobiliari->sum('millesimi_ascensore');
|
||
foreach ($unitaImmobiliari as $unita) {
|
||
$quota = ($unita->millesimi_ascensore / $totaleMillesimi) * $importoTotale;
|
||
$ripartizione[$unita->id] = round($quota, 2);
|
||
}
|
||
break;
|
||
|
||
case 'MILLESIMI_RISCALDAMENTO':
|
||
$totaleMillesimi = $unitaImmobiliari->sum('millesimi_riscaldamento');
|
||
foreach ($unitaImmobiliari as $unita) {
|
||
$quota = ($unita->millesimi_riscaldamento / $totaleMillesimi) * $importoTotale;
|
||
$ripartizione[$unita->id] = round($quota, 2);
|
||
}
|
||
break;
|
||
|
||
case 'PARTI_UGUALI':
|
||
$numeroUnita = $unitaImmobiliari->count();
|
||
$quotaUguale = $importoTotale / $numeroUnita;
|
||
foreach ($unitaImmobiliari as $unita) {
|
||
$ripartizione[$unita->id] = round($quotaUguale, 2);
|
||
}
|
||
break;
|
||
|
||
case 'PERSONALIZZATA':
|
||
if ($this->ripartizione_personalizzata) {
|
||
foreach ($unitaImmobiliari as $unita) {
|
||
$percentuale = $this->ripartizione_personalizzata[$unita->id] ?? 0;
|
||
$quota = ($percentuale / 100) * $importoTotale;
|
||
$ripartizione[$unita->id] = round($quota, 2);
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
|
||
return $ripartizione;
|
||
}
|
||
|
||
/**
|
||
* Verifica se conto supera soglia attenzione
|
||
*/
|
||
public function verificaSogliaAttenzione(): bool
|
||
{
|
||
if (!$this->soglia_attenzione) {
|
||
return false;
|
||
}
|
||
|
||
$saldoCorrente = $this->calcolaSaldo(
|
||
now()->startOfYear(),
|
||
now()
|
||
)['saldo'];
|
||
|
||
return abs($saldoCorrente) > $this->soglia_attenzione;
|
||
}
|
||
|
||
/**
|
||
* Ottieni gerarchia completa del conto
|
||
*/
|
||
public function getPercorsoCompleto(): string
|
||
{
|
||
$percorso = [$this->denominazione];
|
||
$padre = $this->padre;
|
||
|
||
while ($padre) {
|
||
array_unshift($percorso, $padre->denominazione);
|
||
$padre = $padre->padre;
|
||
}
|
||
|
||
return implode(' > ', $percorso);
|
||
}
|
||
|
||
/**
|
||
* Chiudi conto per esercizio
|
||
*/
|
||
public function chiudiEsercizio(\Carbon\Carbon $dataChiusura, string $nota = null): bool
|
||
{
|
||
if ($this->chiuso_esercizio) {
|
||
return false; // Già chiuso
|
||
}
|
||
|
||
// Calcola saldo finale
|
||
$saldoFinale = $this->calcolaSaldo(
|
||
$this->gestione->data_inizio,
|
||
$dataChiusura
|
||
);
|
||
|
||
// Crea registrazione di chiusura se necessario
|
||
if ($saldoFinale['saldo'] != 0) {
|
||
$this->creaRegistrazioneChiusura($dataChiusura, $saldoFinale['saldo'], $nota);
|
||
}
|
||
|
||
$this->update([
|
||
'chiuso_esercizio' => true,
|
||
'updated_by' => auth()->id()
|
||
]);
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Crea registrazione di chiusura
|
||
*/
|
||
private function creaRegistrazioneChiusura(\Carbon\Carbon $dataChiusura, float $saldo, ?string $nota): void
|
||
{
|
||
// Trova o crea conto "Risultato di gestione"
|
||
$contoRisultato = static::where('gestione_id', $this->gestione_id)
|
||
->where('codice_conto', 'LIKE', '%RISULTATO%')
|
||
->first();
|
||
|
||
if (!$contoRisultato) {
|
||
$contoRisultato = static::create([
|
||
'codice_conto' => '999.001',
|
||
'denominazione' => 'Risultato di gestione',
|
||
'gestione_id' => $this->gestione_id,
|
||
'tipo_conto' => 'PATRIMONIO',
|
||
'natura_saldo' => 'AVERE',
|
||
'utilizzabile_registrazioni' => false,
|
||
'created_by' => auth()->id()
|
||
]);
|
||
}
|
||
|
||
// Crea registrazione di chiusura
|
||
Contabilita::create([
|
||
'gestione_id' => $this->gestione_id,
|
||
'data_registrazione' => $dataChiusura,
|
||
'numero_documento' => 'CHIUSURA-' . $this->codice_conto,
|
||
'descrizione' => "Chiusura esercizio - {$this->denominazione}" . ($nota ? " - {$nota}" : ''),
|
||
'importo_totale' => abs($saldo),
|
||
'tipo_movimento' => 'CHIUSURA_ESERCIZIO',
|
||
'registrazioni' => [
|
||
[
|
||
'conto_id' => $this->id,
|
||
'importo_dare' => $saldo < 0 ? abs($saldo) : 0,
|
||
'importo_avere' => $saldo > 0 ? abs($saldo) : 0,
|
||
],
|
||
[
|
||
'conto_id' => $contoRisultato->id,
|
||
'importo_dare' => $saldo > 0 ? abs($saldo) : 0,
|
||
'importo_avere' => $saldo < 0 ? abs($saldo) : 0,
|
||
]
|
||
],
|
||
'created_by' => auth()->id()
|
||
]);
|
||
}
|
||
|
||
// ==================== VALIDAZIONE ====================
|
||
|
||
/**
|
||
* Valida struttura gerarchica
|
||
*/
|
||
public function validaGerarchia(): array
|
||
{
|
||
$errori = [];
|
||
|
||
// Verifica livello coerente con padre
|
||
if ($this->padre_id) {
|
||
$padre = $this->padre;
|
||
if ($padre && $this->livello !== $padre->livello + 1) {
|
||
$errori[] = "Livello {$this->livello} non coerente con padre (livello {$padre->livello})";
|
||
}
|
||
} else {
|
||
if ($this->livello !== 1) {
|
||
$errori[] = "Conto radice deve essere di livello 1";
|
||
}
|
||
}
|
||
|
||
// Verifica codice conto
|
||
$partiCodice = explode('.', $this->codice_conto);
|
||
if (count($partiCodice) !== $this->livello) {
|
||
$errori[] = "Codice conto non coerente con livello {$this->livello}";
|
||
}
|
||
|
||
return $errori;
|
||
}
|
||
|
||
// ==================== EVENTI MODEL ====================
|
||
|
||
protected static function booted()
|
||
{
|
||
// Auto-genera codice se non fornito
|
||
static::creating(function ($pianoConto) {
|
||
if (!$pianoConto->codice_conto) {
|
||
$pianoConto->codice_conto = static::generaCodiceConto(
|
||
$pianoConto->gestione_id,
|
||
$pianoConto->padre_id
|
||
);
|
||
}
|
||
|
||
// Calcola livello automaticamente
|
||
if ($pianoConto->padre_id) {
|
||
$padre = static::find($pianoConto->padre_id);
|
||
$pianoConto->livello = $padre ? $padre->livello + 1 : 1;
|
||
} else {
|
||
$pianoConto->livello = 1;
|
||
}
|
||
|
||
$pianoConto->created_by = auth()->id();
|
||
});
|
||
|
||
static::updating(function ($pianoConto) {
|
||
$pianoConto->updated_by = auth()->id();
|
||
});
|
||
|
||
// Applica filtro multi-tenant
|
||
static::addGlobalScope(new \App\Scopes\MultiTenantScope);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🎯 **PIANO DEI CONTI STANDARD CONDOMINIALE**
|
||
|
||
### **STRUTTURA PREDEFINITA**
|
||
|
||
```sql
|
||
-- INSERIMENTO PIANO DEI CONTI STANDARD PER CONDOMINI
|
||
-- Conforme alle normative italiane per amministrazione condominiale
|
||
|
||
-- LIVELLO 1: MACRO-CATEGORIE
|
||
INSERT INTO piano_conti (codice_conto, denominazione, tipo_conto, natura_saldo, livello, gestione_id) VALUES
|
||
('100', 'ATTIVITÀ', 'ATTIVO', 'DARE', 1, :gestione_id),
|
||
('200', 'PASSIVITÀ', 'PASSIVO', 'AVERE', 1, :gestione_id),
|
||
('300', 'RICAVI', 'RICAVO', 'AVERE', 1, :gestione_id),
|
||
('400', 'COSTI ORDINARI', 'COSTO', 'DARE', 1, :gestione_id),
|
||
('500', 'COSTI STRAORDINARI', 'COSTO', 'DARE', 1, :gestione_id),
|
||
('900', 'PATRIMONIO E RISULTATO', 'PATRIMONIO', 'AVERE', 1, :gestione_id);
|
||
|
||
-- LIVELLO 2: CATEGORIE PRINCIPALI
|
||
|
||
-- 100 - ATTIVITÀ
|
||
INSERT INTO piano_conti (codice_conto, denominazione, padre_id, tipo_conto, natura_saldo, livello, gestione_id) VALUES
|
||
('100.001', 'Disponibilità Liquide', (SELECT id FROM piano_conti WHERE codice_conto='100'), 'ATTIVO', 'DARE', 2, :gestione_id),
|
||
('100.002', 'Crediti verso Condomini', (SELECT id FROM piano_conti WHERE codice_conto='100'), 'ATTIVO', 'DARE', 2, :gestione_id),
|
||
('100.003', 'Fondi di Cassa', (SELECT id FROM piano_conti WHERE codice_conto='100'), 'ATTIVO', 'DARE', 2, :gestione_id);
|
||
|
||
-- 200 - PASSIVITÀ
|
||
INSERT INTO piano_conti (codice_conto, denominazione, padre_id, tipo_conto, natura_saldo, livello, gestione_id) VALUES
|
||
('200.001', 'Debiti verso Fornitori', (SELECT id FROM piano_conti WHERE codice_conto='200'), 'PASSIVO', 'AVERE', 2, :gestione_id),
|
||
('200.002', 'Fondi per Lavori Futuri', (SELECT id FROM piano_conti WHERE codice_conto='200'), 'PASSIVO', 'AVERE', 2, :gestione_id),
|
||
('200.003', 'Depositi Cauzionali', (SELECT id FROM piano_conti WHERE codice_conto='200'), 'PASSIVO', 'AVERE', 2, :gestione_id);
|
||
|
||
-- 300 - RICAVI
|
||
INSERT INTO piano_conti (codice_conto, denominazione, padre_id, tipo_conto, natura_saldo, livello, gestione_id, ripartizione_default) VALUES
|
||
('300.001', 'Quote Condominiali Ordinarie', (SELECT id FROM piano_conti WHERE codice_conto='300'), 'RICAVO', 'AVERE', 2, :gestione_id, 'MILLESIMI_GENERALI'),
|
||
('300.002', 'Quote Condominiali Straordinarie', (SELECT id FROM piano_conti WHERE codice_conto='300'), 'RICAVO', 'AVERE', 2, :gestione_id, 'MILLESIMI_GENERALI'),
|
||
('300.003', 'Interessi di Mora', (SELECT id FROM piano_conti WHERE codice_conto='300'), 'RICAVO', 'AVERE', 2, :gestione_id, 'PERSONALIZZATA');
|
||
|
||
-- 400 - COSTI ORDINARI
|
||
INSERT INTO piano_conti (codice_conto, denominazione, padre_id, tipo_conto, natura_saldo, livello, gestione_id, art_1133_cc) VALUES
|
||
('400.001', 'Manutenzione Ordinaria', (SELECT id FROM piano_conti WHERE codice_conto='400'), 'COSTO', 'DARE', 2, :gestione_id, TRUE),
|
||
('400.002', 'Pulizie e Igiene', (SELECT id FROM piano_conti WHERE codice_conto='400'), 'COSTO', 'DARE', 2, :gestione_id, TRUE),
|
||
('400.003', 'Servizi Condominiali', (SELECT id FROM piano_conti WHERE codice_conto='400'), 'COSTO', 'DARE', 2, :gestione_id, FALSE);
|
||
|
||
-- 500 - COSTI STRAORDINARI
|
||
INSERT INTO piano_conti (codice_conto, denominazione, padre_id, tipo_conto, natura_saldo, livello, gestione_id, art_1134_cc) VALUES
|
||
('500.001', 'Innovazioni', (SELECT id FROM piano_conti WHERE codice_conto='500'), 'COSTO', 'DARE', 2, :gestione_id, TRUE),
|
||
('500.002', 'Manutenzione Straordinaria', (SELECT id FROM piano_conti WHERE codice_conto='500'), 'COSTO', 'DARE', 2, :gestione_id, FALSE),
|
||
('500.003', 'Lavori di Miglioria', (SELECT id FROM piano_conti WHERE codice_conto='500'), 'COSTO', 'DARE', 2, :gestione_id, TRUE);
|
||
|
||
-- LIVELLO 3: DETTAGLIO OPERATIVO
|
||
|
||
-- Disponibilità Liquide (100.001)
|
||
INSERT INTO piano_conti (codice_conto, denominazione, padre_id, tipo_conto, natura_saldo, livello, gestione_id, utilizzabile_registrazioni) VALUES
|
||
('100.001.001', 'Conto Corrente Ordinario', (SELECT id FROM piano_conti WHERE codice_conto='100.001'), 'ATTIVO', 'DARE', 3, :gestione_id, TRUE),
|
||
('100.001.002', 'Conto Corrente Straordinario', (SELECT id FROM piano_conti WHERE codice_conto='100.001'), 'ATTIVO', 'DARE', 3, :gestione_id, TRUE),
|
||
('100.001.003', 'Cassa Contanti', (SELECT id FROM piano_conti WHERE codice_conto='100.001'), 'ATTIVO', 'DARE', 3, :gestione_id, TRUE);
|
||
|
||
-- Crediti verso Condomini (100.002)
|
||
INSERT INTO piano_conti (codice_conto, denominazione, padre_id, tipo_conto, natura_saldo, livello, gestione_id, utilizzabile_registrazioni, ripartizione_default) VALUES
|
||
('100.002.001', 'Rate Ordinarie da Incassare', (SELECT id FROM piano_conti WHERE codice_conto='100.002'), 'ATTIVO', 'DARE', 3, :gestione_id, TRUE, 'MILLESIMI_GENERALI'),
|
||
('100.002.002', 'Rate Straordinarie da Incassare', (SELECT id FROM piano_conti WHERE codice_conto='100.002'), 'ATTIVO', 'DARE', 3, :gestione_id, TRUE, 'MILLESIMI_GENERALI'),
|
||
('100.002.003', 'Morosità e Interessi', (SELECT id FROM piano_conti WHERE codice_conto='100.002'), 'ATTIVO', 'DARE', 3, :gestione_id, TRUE, 'PERSONALIZZATA');
|
||
|
||
-- Manutenzione Ordinaria (400.001) - Art. 1133 C.C.
|
||
INSERT INTO piano_conti (codice_conto, denominazione, padre_id, tipo_conto, natura_saldo, livello, gestione_id, utilizzabile_registrazioni, art_1133_cc, ripartizione_default) VALUES
|
||
('400.001.001', 'Elettricista', (SELECT id FROM piano_conti WHERE codice_conto='400.001'), 'COSTO', 'DARE', 3, :gestione_id, TRUE, TRUE, 'MILLESIMI_GENERALI'),
|
||
('400.001.002', 'Idraulico', (SELECT id FROM piano_conti WHERE codice_conto='400.001'), 'COSTO', 'DARE', 3, :gestione_id, TRUE, TRUE, 'MILLESIMI_GENERALI'),
|
||
('400.001.003', 'Ascensore', (SELECT id FROM piano_conti WHERE codice_conto='400.001'), 'COSTO', 'DARE', 3, :gestione_id, TRUE, TRUE, 'MILLESIMI_ASCENSORE'),
|
||
('400.001.004', 'Riscaldamento', (SELECT id FROM piano_conti WHERE codice_conto='400.001'), 'COSTO', 'DARE', 3, :gestione_id, TRUE, TRUE, 'MILLESIMI_RISCALDAMENTO');
|
||
|
||
-- Pulizie e Igiene (400.002)
|
||
INSERT INTO piano_conti (codice_conto, denominazione, padre_id, tipo_conto, natura_saldo, livello, gestione_id, utilizzabile_registrazioni, ripartizione_default) VALUES
|
||
('400.002.001', 'Pulizie Scale', (SELECT id FROM piano_conti WHERE codice_conto='400.002'), 'COSTO', 'DARE', 3, :gestione_id, TRUE, 'MILLESIMI_SCALE'),
|
||
('400.002.002', 'Derattizzazione', (SELECT id FROM piano_conti WHERE codice_conto='400.002'), 'COSTO', 'DARE', 3, :gestione_id, TRUE, 'MILLESIMI_GENERALI'),
|
||
('400.002.003', 'Disinfestazione', (SELECT id FROM piano_conti WHERE codice_conto='400.002'), 'COSTO', 'DARE', 3, :gestione_id, TRUE, 'MILLESIMI_GENERALI');
|
||
|
||
-- Servizi Condominiali (400.003) - Art. 1131 C.C.
|
||
INSERT INTO piano_conti (codice_conto, denominazione, padre_id, tipo_conto, natura_saldo, livello, gestione_id, utilizzabile_registrazioni, art_1131_cc, ripartizione_default) VALUES
|
||
('400.003.001', 'Amministratore', (SELECT id FROM piano_conti WHERE codice_conto='400.003'), 'COSTO', 'DARE', 3, :gestione_id, TRUE, TRUE, 'MILLESIMI_GENERALI'),
|
||
('400.003.002', 'Portiere', (SELECT id FROM piano_conti WHERE codice_conto='400.003'), 'COSTO', 'DARE', 3, :gestione_id, TRUE, FALSE, 'MILLESIMI_GENERALI'),
|
||
('400.003.003', 'Assicurazioni', (SELECT id FROM piano_conti WHERE codice_conto='400.003'), 'COSTO', 'DARE', 3, :gestione_id, TRUE, FALSE, 'MILLESIMI_GENERALI');
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 **UTILIZZO E INTEGRAZIONE**
|
||
|
||
### **Query Esempio - Mastrino Completo**
|
||
|
||
```sql
|
||
-- Genera mastrino per conto specifico con saldi progressivi
|
||
SELECT
|
||
c.data_registrazione,
|
||
c.numero_documento,
|
||
c.descrizione,
|
||
rc.importo_dare,
|
||
rc.importo_avere,
|
||
(@saldo := @saldo + (rc.importo_dare - rc.importo_avere)) AS saldo_progressivo,
|
||
ui.denominazione AS unita_immobiliare
|
||
FROM contabilita c
|
||
INNER JOIN registrazioni_contabili rc ON c.id = rc.contabilita_id
|
||
LEFT JOIN unita_immobiliari ui ON c.unita_immobiliare_id = ui.id
|
||
CROSS JOIN (SELECT @saldo := 0) AS init
|
||
WHERE rc.conto_id = :conto_id
|
||
AND c.gestione_id = :gestione_id
|
||
AND c.data_registrazione BETWEEN :data_inizio AND :data_fine
|
||
ORDER BY c.data_registrazione, c.numero_registrazione;
|
||
```
|
||
|
||
### **Business Logic - Ripartizione Automatica**
|
||
|
||
```php
|
||
// Esempio: Ripartire spesa di manutenzione ascensore
|
||
$contoAscensore = PianoConto::where('codice_conto', '400.001.003')->first();
|
||
$ripartizione = $contoAscensore->calcolaRipartizione(1500.00);
|
||
|
||
// Risultato automatico basato su millesimi_ascensore:
|
||
// [
|
||
// '1' => 45.75, // Unità 1: 30,5‰ × 1500€
|
||
// '2' => 38.25, // Unità 2: 25,5‰ × 1500€
|
||
// '3' => 66.00, // Unità 3: 44‰ × 1500€
|
||
// // ...
|
||
// ]
|
||
```
|
||
|
||
---
|
||
|
||
## 🚀 **PROSSIMI STEP**
|
||
|
||
✅ **Completato**: Piano dei Conti con struttura gerarchica e normativa CC
|
||
🔄 **Prossimo**: Movimenti Bancari per riconciliazione automatica
|
||
⏳ **Seguente**: Fatture Elettroniche per integrazione fiscale
|
||
|
||
La struttura Piano dei Conti è ora completa e pronta per integrarsi con il sistema contabile centralizzato. Procediamo con i **Movimenti Bancari**?
|