netgescon-master/SPECIFICHE-SISTEMA-CONTABILE-COMPLETO.md
Michele Windows e68ee85a18 🚀 CHECKPOINT STABILE - Sistema Contabile Avanzato
📋 AGGIUNTE PRINCIPALI:
- Sistema contabile partita doppia con gestioni multiple
- Documentazione implementazione completa
- Models Laravel: GestioneContabile, MovimentoPartitaDoppia
- Controller ContabilitaAvanzataController
- Migration sistema contabile completo
- Scripts automazione e trasferimento
- Manuali utente e checklist implementazione

📊 FILES PRINCIPALI:
- docs/10-IMPLEMENTAZIONE-CONTABILITA-PARTITA-DOPPIA-GESTIONI.md
- SPECIFICHE-SISTEMA-CONTABILE-COMPLETO.md
- netgescon-laravel/database/migrations/2025_07_20_100000_create_complete_accounting_system.php
- netgescon-laravel/app/Models/GestioneContabile.php

 CHECKPOINT SICURO PER ROLLBACK
2025-07-26 15:11:19 +02:00

46 KiB

📋 SPECIFICHE COMPLETE SISTEMA CONTABILE NETGESCON

🗃️ MIGRAZIONI DATABASE

File: database/migrations/2025_07_23_100000_create_sistema_contabile_completo.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;

return new class extends Migration
{
    public function up(): void
    {
        // 1. PIANO DEI CONTI MASTERPLAN
        if (!Schema::hasTable('piano_conti_masterplan')) {
            Schema::create('piano_conti_masterplan', function (Blueprint $table) {
                $table->id();
                $table->string('codice_conto', 10)->unique();
                $table->string('descrizione_conto');
                $table->enum('tipologia_conto', ['attivo', 'passivo', 'ricavo', 'costo', 'patrimoniale']);
                $table->string('categoria_contabile', 50)->nullable();
                $table->boolean('ripartibile')->default(true);
                $table->json('default_ripartizioni')->nullable();
                $table->boolean('attivo')->default(true);
                $table->timestamps();
                
                $table->index(['tipologia_conto', 'categoria_contabile']);
            });
        }

        // 2. GESTIONI CONTABILI
        if (!Schema::hasTable('gestioni_contabili')) {
            Schema::create('gestioni_contabili', function (Blueprint $table) {
                $table->id();
                $table->char('codice_gestione', 8)->unique();
                $table->unsignedBigInteger('stabile_id');
                $table->unsignedBigInteger('esercizio_contabile_id');
                $table->string('denominazione');
                $table->text('descrizione')->nullable();
                $table->enum('tipologia', ['ordinaria', 'riscaldamento', 'straordinaria', 'fondo_lavori', 'fondo_riserva']);
                $table->enum('stato', ['attiva', 'chiusa', 'sospesa'])->default('attiva');
                $table->date('data_apertura');
                $table->date('data_chiusura')->nullable();
                $table->decimal('budget_previsto', 12, 2)->default(0);
                $table->decimal('fondo_cassa_iniziale', 12, 2)->default(0);
                $table->json('regole_ripartizione')->nullable();
                $table->unsignedBigInteger('tabella_millesimale_id')->nullable();
                $table->timestamps();
                $table->softDeletes();

                $table->foreign('stabile_id')->references('id')->on('stabili')->onDelete('cascade');
                $table->foreign('esercizio_contabile_id')->references('id')->on('esercizi_contabili')->onDelete('cascade');
                $table->foreign('tabella_millesimale_id')->references('id')->on('tabelle_millesimali')->onDelete('set null');
                
                $table->index(['stabile_id', 'tipologia', 'stato']);
                $table->unique(['stabile_id', 'esercizio_contabile_id', 'tipologia'], 'unique_gestione_per_esercizio');
            });
        }

        // 3. MOVIMENTI PARTITA DOPPIA
        if (!Schema::hasTable('movimenti_partita_doppia')) {
            Schema::create('movimenti_partita_doppia', function (Blueprint $table) {
                $table->id();
                $table->char('codice_movimento', 12)->unique();
                $table->unsignedBigInteger('stabile_id');
                $table->unsignedBigInteger('gestione_contabile_id');
                $table->unsignedBigInteger('esercizio_contabile_id');
                
                $table->date('data_movimento');
                $table->date('data_registrazione')->default(DB::raw('CURRENT_DATE'));
                $table->string('descrizione');
                $table->text('causale_dettagliata')->nullable();
                $table->text('note_interne')->nullable();
                
                $table->string('tipo_documento', 50)->nullable();
                $table->string('numero_documento')->nullable();
                $table->date('data_documento')->nullable();
                $table->unsignedBigInteger('fornitore_id')->nullable();
                $table->unsignedBigInteger('documento_id')->nullable();
                
                $table->string('numero_protocollo', 20)->nullable();
                $table->integer('progressivo_anno')->nullable();
                
                $table->enum('stato_movimento', ['bozza', 'da_verificare', 'verificato', 'confermato', 'chiuso'])->default('bozza');
                $table->enum('tipologia_registrazione', ['ordinaria', 'straordinaria', 'chiusura', 'apertura', 'rettifica'])->default('ordinaria');
                
                $table->decimal('importo_lordo', 12, 2);
                $table->decimal('importo_iva', 12, 2)->default(0);
                $table->decimal('importo_ritenute', 12, 2)->default(0);
                $table->decimal('importo_netto', 12, 2);
                $table->json('dettagli_fiscali')->nullable();
                
                $table->boolean('ripartito')->default(false);
                $table->json('ripartizione_millesimale')->nullable();
                $table->unsignedBigInteger('tabella_millesimale_utilizzata')->nullable();
                
                $table->unsignedBigInteger('creato_da');
                $table->unsignedBigInteger('verificato_da')->nullable();
                $table->unsignedBigInteger('confermato_da')->nullable();
                $table->timestamp('data_verifica')->nullable();
                $table->timestamp('data_conferma')->nullable();
                
                $table->timestamps();
                $table->softDeletes();

                $table->foreign('stabile_id')->references('id')->on('stabili')->onDelete('cascade');
                $table->foreign('gestione_contabile_id')->references('id')->on('gestioni_contabili')->onDelete('cascade');
                $table->foreign('esercizio_contabile_id')->references('id')->on('esercizi_contabili')->onDelete('cascade');
                $table->foreign('fornitore_id')->references('id')->on('fornitori')->onDelete('set null');
                $table->foreign('tabella_millesimale_utilizzata')->references('id')->on('tabelle_millesimali')->onDelete('set null');
                $table->foreign('creato_da')->references('id')->on('users')->onDelete('cascade');
                $table->foreign('verificato_da')->references('id')->on('users')->onDelete('set null');
                $table->foreign('confermato_da')->references('id')->on('users')->onDelete('set null');

                $table->index(['stabile_id', 'data_movimento']);
                $table->index(['gestione_contabile_id', 'stato_movimento']);
                $table->index(['esercizio_contabile_id', 'tipologia_registrazione']);
                $table->index(['numero_protocollo']);
                $table->index(['progressivo_anno', 'stabile_id']);
            });
        }

        // 4. RIGHE CONTABILI (DARE/AVERE)
        if (!Schema::hasTable('righe_contabili')) {
            Schema::create('righe_contabili', function (Blueprint $table) {
                $table->id();
                $table->unsignedBigInteger('movimento_id');
                $table->string('codice_conto', 10);
                $table->string('descrizione_riga');
                $table->enum('dare_avere', ['dare', 'avere']);
                $table->decimal('importo', 12, 2);
                $table->unsignedBigInteger('unita_immobiliare_id')->nullable();
                $table->decimal('quota_millesimale', 10, 4)->nullable();
                $table->text('note_riga')->nullable();
                $table->timestamps();

                $table->foreign('movimento_id')->references('id')->on('movimenti_partita_doppia')->onDelete('cascade');
                $table->foreign('codice_conto')->references('codice_conto')->on('piano_conti_masterplan')->onDelete('cascade');
                $table->foreign('unita_immobiliare_id')->references('id')->on('unita_immobiliari')->onDelete('set null');

                $table->index(['movimento_id', 'dare_avere']);
                $table->index(['codice_conto']);
            });
        }

        // 5. RATE CONDOMINIALI
        if (!Schema::hasTable('rate_condominiali')) {
            Schema::create('rate_condominiali', function (Blueprint $table) {
                $table->id();
                $table->char('codice_rata', 12)->unique();
                $table->unsignedBigInteger('stabile_id');
                $table->unsignedBigInteger('gestione_contabile_id');
                $table->unsignedBigInteger('unita_immobiliare_id');
                $table->unsignedBigInteger('soggetto_id');
                
                $table->string('tipo_rata', 50);
                $table->date('data_scadenza');
                $table->decimal('importo_dovuto', 10, 2);
                $table->decimal('importo_pagato', 10, 2)->default(0);
                $table->decimal('importo_residuo', 10, 2);
                
                $table->enum('stato_pagamento', ['da_pagare', 'parzialmente_pagata', 'pagata', 'insoluta', 'stornata'])->default('da_pagare');
                $table->date('data_primo_pagamento')->nullable();
                $table->date('data_ultimo_pagamento')->nullable();
                
                $table->decimal('millesimi_applicati', 10, 4);
                $table->unsignedBigInteger('tabella_millesimale_id');
                
                $table->decimal('interessi_mora', 10, 2)->default(0);
                $table->date('data_decorrenza_mora')->nullable();
                $table->decimal('percentuale_mora', 5, 2)->default(0);
                
                $table->timestamps();
                $table->softDeletes();

                $table->foreign('stabile_id')->references('id')->on('stabili')->onDelete('cascade');
                $table->foreign('gestione_contabile_id')->references('id')->on('gestioni_contabili')->onDelete('cascade');
                $table->foreign('unita_immobiliare_id')->references('id')->on('unita_immobiliari')->onDelete('cascade');
                $table->foreign('soggetto_id')->references('id')->on('soggetti')->onDelete('cascade');
                $table->foreign('tabella_millesimale_id')->references('id')->on('tabelle_millesimali')->onDelete('cascade');

                $table->index(['stabile_id', 'data_scadenza']);
                $table->index(['gestione_contabile_id', 'stato_pagamento']);
                $table->index(['soggetto_id', 'stato_pagamento']);
            });
        }

        // 6. PAGAMENTI RATE
        if (!Schema::hasTable('pagamenti_rate')) {
            Schema::create('pagamenti_rate', function (Blueprint $table) {
                $table->id();
                $table->char('codice_pagamento', 12)->unique();
                $table->unsignedBigInteger('rata_id');
                $table->date('data_pagamento');
                $table->decimal('importo_pagamento', 10, 2);
                $table->string('modalita_pagamento', 50);
                $table->string('riferimento_pagamento')->nullable();
                $table->text('note_pagamento')->nullable();
                $table->unsignedBigInteger('movimento_bancario_id')->nullable();
                $table->timestamps();

                $table->foreign('rata_id')->references('id')->on('rate_condominiali')->onDelete('cascade');
                $table->index(['rata_id', 'data_pagamento']);
            });
        }

        // 7. DOCUMENTI CONTABILI
        if (!Schema::hasTable('documenti_contabili')) {
            Schema::create('documenti_contabili', function (Blueprint $table) {
                $table->id();
                $table->char('codice_documento', 12)->unique();
                $table->unsignedBigInteger('stabile_id');
                $table->unsignedBigInteger('movimento_id')->nullable();
                
                $table->string('tipo_documento', 50);
                $table->string('numero_documento');
                $table->date('data_documento');
                $table->string('oggetto');
                $table->text('descrizione')->nullable();
                
                $table->string('file_path')->nullable();
                $table->string('file_originale')->nullable();
                $table->string('mime_type')->nullable();
                $table->bigInteger('file_size')->nullable();
                
                $table->string('numero_protocollo')->nullable();
                $table->date('data_protocollo')->nullable();
                
                $table->timestamps();
                $table->softDeletes();

                $table->foreign('stabile_id')->references('id')->on('stabili')->onDelete('cascade');
                $table->foreign('movimento_id')->references('id')->on('movimenti_partita_doppia')->onDelete('set null');
                
                $table->index(['stabile_id', 'tipo_documento']);
                $table->index(['numero_protocollo']);
            });
        }

        // 8. AUDIT CONTABILITA
        if (!Schema::hasTable('audit_contabilita')) {
            Schema::create('audit_contabilita', function (Blueprint $table) {
                $table->id();
                $table->string('tabella_interessata');
                $table->unsignedBigInteger('record_id');
                $table->string('azione');
                $table->json('dati_precedenti')->nullable();
                $table->json('dati_nuovi')->nullable();
                $table->unsignedBigInteger('user_id');
                $table->string('ip_address')->nullable();
                $table->string('user_agent')->nullable();
                $table->timestamps();

                $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
                $table->index(['tabella_interessata', 'record_id']);
                $table->index(['user_id', 'created_at']);
            });
        }
    }

    public function down(): void
    {
        Schema::dropIfExists('audit_contabilita');
        Schema::dropIfExists('documenti_contabili');
        Schema::dropIfExists('pagamenti_rate');
        Schema::dropIfExists('rate_condominiali');
        Schema::dropIfExists('righe_contabili');
        Schema::dropIfExists('movimenti_partita_doppia');
        Schema::dropIfExists('gestioni_contabili');
        Schema::dropIfExists('piano_conti_masterplan');
    }
};

🏗️ MODELS DA CREARE

File: app/Models/GestioneContabile.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class GestioneContabile extends Model
{
    use HasFactory, SoftDeletes;

    protected $table = 'gestioni_contabili';

    protected $fillable = [
        'codice_gestione', 'stabile_id', 'esercizio_contabile_id',
        'denominazione', 'descrizione', 'tipologia', 'stato',
        'data_apertura', 'data_chiusura', 'budget_previsto',
        'fondo_cassa_iniziale', 'regole_ripartizione', 'tabella_millesimale_id'
    ];

    protected $casts = [
        'data_apertura' => 'date',
        'data_chiusura' => 'date',
        'budget_previsto' => 'decimal:2',
        'fondo_cassa_iniziale' => 'decimal:2',
        'regole_ripartizione' => 'json'
    ];

    protected static function boot()
    {
        parent::boot();
        
        static::creating(function ($model) {
            if (!$model->codice_gestione) {
                $model->codice_gestione = static::generateCodiceGestione();
            }
        });
    }

    public static function generateCodiceGestione(): string
    {
        do {
            $codice = 'GES' . sprintf('%05d', rand(10000, 99999));
        } while (static::where('codice_gestione', $codice)->exists());
        
        return $codice;
    }

    // Relazioni
    public function stabile(): BelongsTo
    {
        return $this->belongsTo(Stabile::class);
    }

    public function esercizioContabile(): BelongsTo
    {
        return $this->belongsTo(EsercizioContabile::class, 'esercizio_contabile_id');
    }

    public function tabellaMillesimale(): BelongsTo
    {
        return $this->belongsTo(TabellaMillesimale::class, 'tabella_millesimale_id');
    }

    public function movimenti(): HasMany
    {
        return $this->hasMany(MovimentoPartitaDoppia::class, 'gestione_contabile_id');
    }

    public function rate(): HasMany
    {
        return $this->hasMany(RataCondominiale::class, 'gestione_contabile_id');
    }

    // Scopes
    public function scopeAttive($query)
    {
        return $query->where('stato', 'attiva');
    }

    public function scopeByTipologia($query, $tipologia)
    {
        return $query->where('tipologia', $tipologia);
    }

    // Business Logic
    public function calcolaSaldoContabile(): float
    {
        $entrate = $this->movimenti()
            ->whereHas('righeContabili', function($q) {
                $q->where('dare_avere', 'avere');
            })
            ->sum('importo_netto');
            
        $uscite = $this->movimenti()
            ->whereHas('righeContabili', function($q) {
                $q->where('dare_avere', 'dare');
            })
            ->sum('importo_netto');
            
        return $entrate - $uscite;
    }

    public function isChiudibile(): bool
    {
        $movimentiNonConfermati = $this->movimenti()
            ->whereIn('stato_movimento', ['bozza', 'da_verificare'])
            ->count();
            
        return $movimentiNonConfermati === 0;
    }

    public function chiudiGestione(): bool
    {
        if (!$this->isChiudibile()) {
            return false;
        }
        
        $this->update([
            'stato' => 'chiusa',
            'data_chiusura' => now(),
        ]);
        
        return true;
    }
}

File: app/Models/MovimentoPartitaDoppia.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class MovimentoPartitaDoppia extends Model
{
    use HasFactory, SoftDeletes;

    protected $table = 'movimenti_partita_doppia';

    protected $fillable = [
        'codice_movimento', 'stabile_id', 'gestione_contabile_id', 'esercizio_contabile_id',
        'data_movimento', 'data_registrazione', 'descrizione', 'causale_dettagliata',
        'note_interne', 'tipo_documento', 'numero_documento', 'data_documento',
        'fornitore_id', 'documento_id', 'numero_protocollo', 'progressivo_anno',
        'stato_movimento', 'tipologia_registrazione', 'importo_lordo', 'importo_iva',
        'importo_ritenute', 'importo_netto', 'dettagli_fiscali', 'ripartito',
        'ripartizione_millesimale', 'tabella_millesimale_utilizzata',
        'creato_da', 'verificato_da', 'confermato_da', 'data_verifica', 'data_conferma'
    ];

    protected $casts = [
        'data_movimento' => 'date',
        'data_registrazione' => 'date',
        'data_documento' => 'date',
        'data_verifica' => 'datetime',
        'data_conferma' => 'datetime',
        'importo_lordo' => 'decimal:2',
        'importo_iva' => 'decimal:2',
        'importo_ritenute' => 'decimal:2',
        'importo_netto' => 'decimal:2',
        'dettagli_fiscali' => 'json',
        'ripartito' => 'boolean',
        'ripartizione_millesimale' => 'json'
    ];

    protected static function boot()
    {
        parent::boot();
        
        static::creating(function ($model) {
            if (!$model->codice_movimento) {
                $model->codice_movimento = static::generateCodiceMovimento();
            }
            
            if (!$model->progressivo_anno) {
                $model->progressivo_anno = static::getNextProgressivo($model->stabile_id);
            }
        });
    }

    public static function generateCodiceMovimento(): string
    {
        do {
            $codice = 'MOV' . sprintf('%09d', rand(100000000, 999999999));
        } while (static::where('codice_movimento', $codice)->exists());
        
        return $codice;
    }

    public static function getNextProgressivo($stabile_id): int
    {
        $anno = date('Y');
        $ultimo = static::where('stabile_id', $stabile_id)
            ->whereYear('data_registrazione', $anno)
            ->max('progressivo_anno');
            
        return ($ultimo ?? 0) + 1;
    }

    // Relazioni
    public function stabile(): BelongsTo
    {
        return $this->belongsTo(Stabile::class);
    }

    public function gestioneContabile(): BelongsTo
    {
        return $this->belongsTo(GestioneContabile::class, 'gestione_contabile_id');
    }

    public function esercizioContabile(): BelongsTo
    {
        return $this->belongsTo(EsercizioContabile::class, 'esercizio_contabile_id');
    }

    public function fornitore(): BelongsTo
    {
        return $this->belongsTo(Fornitore::class);
    }

    public function righeContabili(): HasMany
    {
        return $this->hasMany(RigaContabile::class, 'movimento_id');
    }

    public function creatoBy(): BelongsTo
    {
        return $this->belongsTo(User::class, 'creato_da');
    }

    public function verificatoBy(): BelongsTo
    {
        return $this->belongsTo(User::class, 'verificato_da');
    }

    public function confermatoBy(): BelongsTo
    {
        return $this->belongsTo(User::class, 'confermato_da');
    }

    // Scopes
    public function scopeConfermati($query)
    {
        return $query->where('stato_movimento', 'confermato');
    }

    public function scopeByGestione($query, $gestione_id)
    {
        return $query->where('gestione_contabile_id', $gestione_id);
    }

    public function scopeByPeriodo($query, $data_inizio, $data_fine)
    {
        return $query->whereBetween('data_movimento', [$data_inizio, $data_fine]);
    }

    // Business Logic
    public function verificaQuadratura(): bool
    {
        $totaleDare = $this->righeContabili()->where('dare_avere', 'dare')->sum('importo');
        $totaleAvere = $this->righeContabili()->where('dare_avere', 'avere')->sum('importo');
        
        return abs($totaleDare - $totaleAvere) < 0.01;
    }

    public function confermaMovimento($user_id): bool
    {
        if (!$this->verificaQuadratura()) {
            return false;
        }
        
        $this->update([
            'stato_movimento' => 'confermato',
            'confermato_da' => $user_id,
            'data_conferma' => now(),
        ]);
        
        return true;
    }

    public function creaRigheStandard($conto_dare, $conto_avere): void
    {
        // Riga in DARE
        $this->righeContabili()->create([
            'codice_conto' => $conto_dare,
            'descrizione_riga' => $this->descrizione,
            'dare_avere' => 'dare',
            'importo' => $this->importo_netto,
        ]);
        
        // Riga in AVERE
        $this->righeContabili()->create([
            'codice_conto' => $conto_avere,
            'descrizione_riga' => $this->descrizione,
            'dare_avere' => 'avere',
            'importo' => $this->importo_netto,
        ]);
    }
}

File: app/Models/RigaContabile.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class RigaContabile extends Model
{
    use HasFactory;

    protected $table = 'righe_contabili';

    protected $fillable = [
        'movimento_id', 'codice_conto', 'descrizione_riga',
        'dare_avere', 'importo', 'unita_immobiliare_id',
        'quota_millesimale', 'note_riga'
    ];

    protected $casts = [
        'importo' => 'decimal:2',
        'quota_millesimale' => 'decimal:4'
    ];

    // Relazioni
    public function movimento(): BelongsTo
    {
        return $this->belongsTo(MovimentoPartitaDoppia::class, 'movimento_id');
    }

    public function pianoConti(): BelongsTo
    {
        return $this->belongsTo(PianoContiMasterplan::class, 'codice_conto', 'codice_conto');
    }

    public function unitaImmobiliare(): BelongsTo
    {
        return $this->belongsTo(UnitaImmobiliare::class, 'unita_immobiliare_id');
    }

    // Scopes
    public function scopeDare($query)
    {
        return $query->where('dare_avere', 'dare');
    }

    public function scopeAvere($query)
    {
        return $query->where('dare_avere', 'avere');
    }

    public function scopeByConto($query, $codice_conto)
    {
        return $query->where('codice_conto', $codice_conto);
    }
}

File: app/Models/PianoContiMasterplan.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class PianoContiMasterplan extends Model
{
    use HasFactory;

    protected $table = 'piano_conti_masterplan';

    protected $fillable = [
        'codice_conto', 'descrizione_conto', 'tipologia_conto',
        'categoria_contabile', 'ripartibile', 'default_ripartizioni', 'attivo'
    ];

    protected $casts = [
        'ripartibile' => 'boolean',
        'attivo' => 'boolean',
        'default_ripartizioni' => 'json'
    ];

    // Relazioni
    public function righeContabili(): HasMany
    {
        return $this->hasMany(RigaContabile::class, 'codice_conto', 'codice_conto');
    }

    // Scopes
    public function scopeAttivi($query)
    {
        return $query->where('attivo', true);
    }

    public function scopeByTipologia($query, $tipologia)
    {
        return $query->where('tipologia_conto', $tipologia);
    }

    public function scopeByCategoria($query, $categoria)
    {
        return $query->where('categoria_contabile', $categoria);
    }

    public function scopeRipartibili($query)
    {
        return $query->where('ripartibile', true);
    }

    // Metodi helper
    public static function getContiByCategoria($categoria)
    {
        return static::attivi()->byCategoria($categoria)->get();
    }

    public static function getContiCosti()
    {
        return static::attivi()->byTipologia('costo')->get();
    }

    public static function getContiRicavi()
    {
        return static::attivi()->byTipologia('ricavo')->get();
    }

    public static function getContiPatrimoniali()
    {
        return static::attivi()->whereIn('tipologia_conto', ['attivo', 'passivo', 'patrimoniale'])->get();
    }
}

🎛️ CONTROLLER DA CREARE

File: app/Http/Controllers/Admin/ContabilitaAvanzataController.php

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\GestioneContabile;
use App\Models\MovimentoPartitaDoppia;
use App\Models\RigaContabile;
use App\Models\PianoContiMasterplan;
use App\Models\RataCondominiale;
use App\Models\EsercizioContabile;
use App\Models\Stabile;
use App\Models\Fornitore;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Carbon\Carbon;

class ContabilitaAvanzataController extends Controller
{
    public function dashboard()
    {
        $amministratore_id = Auth::user()->amministratore->id_amministratore ?? null;
        
        $stats = $this->calcolaStatisticheDashboard($amministratore_id);
        
        $ultimiMovimenti = MovimentoPartitaDoppia::with([
            'stabile', 'gestioneContabile', 'fornitore', 'righeContabili.pianoConti'
        ])
        ->whereHas('stabile', function($q) use ($amministratore_id) {
            $q->where('amministratore_id', $amministratore_id);
        })
        ->orderBy('data_registrazione', 'desc')
        ->limit(10)
        ->get();
        
        $gestioniAttive = GestioneContabile::with(['stabile', 'esercizioContabile'])
            ->whereHas('stabile', function($q) use ($amministratore_id) {
                $q->where('amministratore_id', $amministratore_id);
            })
            ->where('stato', 'attiva')
            ->get();

        return view('admin.contabilita.dashboard', compact(
            'stats', 'ultimiMovimenti', 'gestioniAttive'
        ));
    }

    public function movimenti(Request $request)
    {
        $amministratore_id = Auth::user()->amministratore->id_amministratore ?? null;
        
        $query = MovimentoPartitaDoppia::with([
            'stabile', 'gestioneContabile', 'fornitore', 'righeContabili.pianoConti'
        ])
        ->whereHas('stabile', function($q) use ($amministratore_id) {
            $q->where('amministratore_id', $amministratore_id);
        });

        // Filtri
        if ($request->stabile_id) {
            $query->where('stabile_id', $request->stabile_id);
        }
        
        if ($request->gestione_id) {
            $query->where('gestione_contabile_id', $request->gestione_id);
        }
        
        if ($request->stato) {
            $query->where('stato_movimento', $request->stato);
        }
        
        if ($request->data_da && $request->data_a) {
            $query->whereBetween('data_movimento', [$request->data_da, $request->data_a]);
        }

        $movimenti = $query->orderBy('data_registrazione', 'desc')->paginate(25);
        
        $stabili = Stabile::where('amministratore_id', $amministratore_id)->get();
        $gestioni = GestioneContabile::whereIn('stabile_id', $stabili->pluck('id'))->get();

        return view('admin.contabilita.movimenti.index', compact('movimenti', 'stabili', 'gestioni'));
    }

    public function creaMovimento()
    {
        $amministratore_id = Auth::user()->amministratore->id_amministratore ?? null;
        
        $stabili = Stabile::where('amministratore_id', $amministratore_id)->get();
        $fornitori = Fornitore::where('amministratore_id', $amministratore_id)->get();
        $pianoConti = PianoContiMasterplan::attivi()->get();
        
        return view('admin.contabilita.movimenti.create', compact('stabili', 'fornitori', 'pianoConti'));
    }

    public function salvaMovimento(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'stabile_id' => 'required|exists:stabili,id',
            'gestione_contabile_id' => 'required|exists:gestioni_contabili,id',
            'data_movimento' => 'required|date',
            'descrizione' => 'required|string|max:255',
            'importo_lordo' => 'required|numeric|min:0.01',
            'importo_netto' => 'required|numeric|min:0.01',
            'righe' => 'required|array|min:2',
            'righe.*.codice_conto' => 'required|exists:piano_conti_masterplan,codice_conto',
            'righe.*.dare_avere' => 'required|in:dare,avere',
            'righe.*.importo' => 'required|numeric|min:0.01',
            'righe.*.descrizione_riga' => 'required|string|max:255',
        ]);

        if ($validator->fails()) {
            return response()->json(['errors' => $validator->errors()], 422);
        }

        DB::beginTransaction();
        try {
            // Verifica quadratura dare/avere
            $totaleDare = collect($request->righe)->where('dare_avere', 'dare')->sum('importo');
            $totaleAvere = collect($request->righe)->where('dare_avere', 'avere')->sum('importo');
            
            if (abs($totaleDare - $totaleAvere) > 0.01) {
                return response()->json([
                    'error' => 'Le righe contabili non sono in quadratura. Dare: ' . $totaleDare . ', Avere: ' . $totaleAvere
                ], 422);
            }

            // Crea movimento
            $movimento = MovimentoPartitaDoppia::create([
                'stabile_id' => $request->stabile_id,
                'gestione_contabile_id' => $request->gestione_contabile_id,
                'esercizio_contabile_id' => $this->getEsercizioAttivo($request->stabile_id),
                'data_movimento' => $request->data_movimento,
                'descrizione' => $request->descrizione,
                'causale_dettagliata' => $request->causale_dettagliata,
                'note_interne' => $request->note_interne,
                'tipo_documento' => $request->tipo_documento,
                'numero_documento' => $request->numero_documento,
                'data_documento' => $request->data_documento,
                'fornitore_id' => $request->fornitore_id,
                'importo_lordo' => $request->importo_lordo,
                'importo_iva' => $request->importo_iva ?? 0,
                'importo_ritenute' => $request->importo_ritenute ?? 0,
                'importo_netto' => $request->importo_netto,
                'creato_da' => Auth::id(),
                'stato_movimento' => 'bozza',
            ]);

            // Crea righe contabili
            foreach ($request->righe as $riga) {
                RigaContabile::create([
                    'movimento_id' => $movimento->id,
                    'codice_conto' => $riga['codice_conto'],
                    'descrizione_riga' => $riga['descrizione_riga'],
                    'dare_avere' => $riga['dare_avere'],
                    'importo' => $riga['importo'],
                    'note_riga' => $riga['note_riga'] ?? null,
                ]);
            }

            DB::commit();
            
            return response()->json([
                'success' => true,
                'message' => 'Movimento contabile creato con successo',
                'movimento_id' => $movimento->id
            ]);

        } catch (\Exception $e) {
            DB::rollback();
            return response()->json(['error' => 'Errore nel salvataggio: ' . $e->getMessage()], 500);
        }
    }

    public function confermaMovimento($id)
    {
        $movimento = MovimentoPartitaDoppia::findOrFail($id);
        
        if (!$movimento->verificaQuadratura()) {
            return response()->json(['error' => 'Il movimento non è in quadratura'], 422);
        }
        
        if ($movimento->confermaMovimento(Auth::id())) {
            return response()->json(['success' => true, 'message' => 'Movimento confermato']);
        }
        
        return response()->json(['error' => 'Errore nella conferma'], 500);
    }

    private function calcolaStatisticheDashboard($amministratore_id)
    {
        $stabiliIds = Stabile::where('amministratore_id', $amministratore_id)->pluck('id');
        
        $meseCorrente = Carbon::now()->startOfMonth();
        
        return [
            'movimenti_mese' => MovimentoPartitaDoppia::whereIn('stabile_id', $stabiliIds)
                ->where('data_registrazione', '>=', $meseCorrente)
                ->count(),
                
            'entrate_mese' => MovimentoPartitaDoppia::whereIn('stabile_id', $stabiliIds)
                ->where('data_registrazione', '>=', $meseCorrente)
                ->whereHas('righeContabili', function($q) {
                    $q->where('dare_avere', 'avere')
                      ->whereHas('pianoConti', function($sq) {
                          $sq->where('tipologia_conto', 'ricavo');
                      });
                })
                ->sum('importo_netto'),
                
            'uscite_mese' => MovimentoPartitaDoppia::whereIn('stabile_id', $stabiliIds)
                ->where('data_registrazione', '>=', $meseCorrente)
                ->whereHas('righeContabili', function($q) {
                    $q->where('dare_avere', 'dare')
                      ->whereHas('pianoConti', function($sq) {
                          $sq->where('tipologia_conto', 'costo');
                      });
                })
                ->sum('importo_netto'),
                
            'saldo_gestioni' => GestioneContabile::whereIn('stabile_id', $stabiliIds)
                ->where('stato', 'attiva')
                ->get()
                ->sum(function($gestione) {
                    return $gestione->calcolaSaldoContabile();
                }),
        ];
    }

    private function getEsercizioAttivo($stabile_id)
    {
        $esercizio = EsercizioContabile::where('stabile_id', $stabile_id)
            ->where('stato', 'aperto')
            ->where('tipologia', 'ordinaria')
            ->first();
            
        return $esercizio ? $esercizio->id : null;
    }

    public function getGestioniByStabile($stabile_id)
    {
        $gestioni = GestioneContabile::where('stabile_id', $stabile_id)
            ->where('stato', 'attiva')
            ->with('esercizioContabile')
            ->get();
            
        return response()->json($gestioni);
    }

    public function verificaQuadratura(Request $request)
    {
        $righe = $request->righe ?? [];
        
        $totaleDare = collect($righe)->where('dare_avere', 'dare')->sum('importo');
        $totaleAvere = collect($righe)->where('dare_avere', 'avere')->sum('importo');
        $differenza = abs($totaleDare - $totaleAvere);
        
        return response()->json([
            'in_quadratura' => $differenza < 0.01,
            'totale_dare' => $totaleDare,
            'totale_avere' => $totaleAvere,
            'differenza' => $differenza,
        ]);
    }
}

🌱 SEEDER DA CREARE

File: database/seeders/PianoContiSeeder.php

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\PianoContiMasterplan;

class PianoContiSeeder extends Seeder
{
    public function run(): void
    {
        $conti = [
            // CONTI PATRIMONIALI - ATTIVO
            [
                'codice_conto' => '1001',
                'descrizione_conto' => 'Cassa',
                'tipologia_conto' => 'attivo',
                'categoria_contabile' => 'liquidita',
                'ripartibile' => false,
            ],
            [
                'codice_conto' => '1002',
                'descrizione_conto' => 'Banca c/c ordinario',
                'tipologia_conto' => 'attivo',
                'categoria_contabile' => 'liquidita',
                'ripartibile' => false,
            ],
            [
                'codice_conto' => '1201',
                'descrizione_conto' => 'Crediti vs condòmini per rate',
                'tipologia_conto' => 'attivo',
                'categoria_contabile' => 'crediti',
                'ripartibile' => false,
            ],
            
            // CONTI PATRIMONIALI - PASSIVO
            [
                'codice_conto' => '2001',
                'descrizione_conto' => 'Debiti vs fornitori',
                'tipologia_conto' => 'passivo',
                'categoria_contabile' => 'debiti',
                'ripartibile' => false,
            ],
            [
                'codice_conto' => '2101',
                'descrizione_conto' => 'Fondo di riserva',
                'tipologia_conto' => 'passivo',
                'categoria_contabile' => 'fondi',
                'ripartibile' => false,
            ],
            
            // CONTI ECONOMICI - RICAVI
            [
                'codice_conto' => '5001',
                'descrizione_conto' => 'Quote ordinarie',
                'tipologia_conto' => 'ricavo',
                'categoria_contabile' => 'quote_condominiali',
                'ripartibile' => false,
                'default_ripartizioni' => json_encode(['generale' => 100]),
            ],
            [
                'codice_conto' => '5002',
                'descrizione_conto' => 'Quote straordinarie',
                'tipologia_conto' => 'ricavo',
                'categoria_contabile' => 'quote_condominiali',
                'ripartibile' => false,
                'default_ripartizioni' => json_encode(['generale' => 100]),
            ],
            
            // CONTI ECONOMICI - COSTI AMMINISTRAZIONE
            [
                'codice_conto' => '6001',
                'descrizione_conto' => 'Compenso amministratore',
                'tipologia_conto' => 'costo',
                'categoria_contabile' => 'amministrazione',
                'ripartibile' => true,
                'default_ripartizioni' => json_encode(['generale' => 100]),
            ],
            [
                'codice_conto' => '6002',
                'descrizione_conto' => 'Spese postali e telefoniche',
                'tipologia_conto' => 'costo',
                'categoria_contabile' => 'amministrazione',
                'ripartibile' => true,
                'default_ripartizioni' => json_encode(['generale' => 100]),
            ],
            
            // CONTI ECONOMICI - PULIZIA E IGIENE
            [
                'codice_conto' => '6101',
                'descrizione_conto' => 'Pulizia scale e parti comuni',
                'tipologia_conto' => 'costo',
                'categoria_contabile' => 'pulizia',
                'ripartibile' => true,
                'default_ripartizioni' => json_encode(['scale' => 100]),
            ],
            [
                'codice_conto' => '6102',
                'descrizione_conto' => 'Materiali di pulizia',
                'tipologia_conto' => 'costo',
                'categoria_contabile' => 'pulizia',
                'ripartibile' => true,
                'default_ripartizioni' => json_encode(['scale' => 100]),
            ],
            
            // CONTI ECONOMICI - MANUTENZIONE
            [
                'codice_conto' => '6201',
                'descrizione_conto' => 'Manutenzione ordinaria ascensore',
                'tipologia_conto' => 'costo',
                'categoria_contabile' => 'manutenzione',
                'ripartibile' => true,
                'default_ripartizioni' => json_encode(['ascensore' => 100]),
            ],
            [
                'codice_conto' => '6202',
                'descrizione_conto' => 'Manutenzione impianto elettrico',
                'tipologia_conto' => 'costo',
                'categoria_contabile' => 'manutenzione',
                'ripartibile' => true,
                'default_ripartizioni' => json_encode(['generale' => 100]),
            ],
            
            // CONTI ECONOMICI - UTENZE
            [
                'codice_conto' => '6301',
                'descrizione_conto' => 'Energia elettrica',
                'tipologia_conto' => 'costo',
                'categoria_contabile' => 'utenze',
                'ripartibile' => true,
                'default_ripartizioni' => json_encode(['generale' => 100]),
            ],
            [
                'codice_conto' => '6302',
                'descrizione_conto' => 'Gas',
                'tipologia_conto' => 'costo',
                'categoria_contabile' => 'utenze',
                'ripartibile' => true,
                'default_ripartizioni' => json_encode(['riscaldamento' => 100]),
            ],
            [
                'codice_conto' => '6303',
                'descrizione_conto' => 'Acqua',
                'tipologia_conto' => 'costo',
                'categoria_contabile' => 'utenze',
                'ripartibile' => true,
                'default_ripartizioni' => json_encode(['generale' => 100]),
            ],
            
            // CONTI ECONOMICI - ASSICURAZIONI
            [
                'codice_conto' => '6501',
                'descrizione_conto' => 'Assicurazione globale fabbricati',
                'tipologia_conto' => 'costo',
                'categoria_contabile' => 'assicurazioni',
                'ripartibile' => true,
                'default_ripartizioni' => json_encode(['generale' => 100]),
            ],
        ];

        foreach ($conti as $conto) {
            PianoContiMasterplan::updateOrCreate(
                ['codice_conto' => $conto['codice_conto']],
                $conto
            );
        }

        $this->command->info('Piano dei conti popolato con ' . count($conti) . ' voci');
    }
}

🛤️ ROUTES DA AGGIUNGERE

File: routes/admin.php (aggiungere alla fine)

// === CONTABILITÀ AVANZATA ===
Route::prefix('contabilita-avanzata')->name('contabilita_avanzata.')->group(function () {
    Route::get('/', [ContabilitaAvanzataController::class, 'dashboard'])->name('dashboard');
    
    // Movimenti
    Route::get('/movimenti', [ContabilitaAvanzataController::class, 'movimenti'])->name('movimenti.index');
    Route::get('/movimenti/create', [ContabilitaAvanzataController::class, 'creaMovimento'])->name('movimenti.create');
    Route::post('/movimenti', [ContabilitaAvanzataController::class, 'salvaMovimento'])->name('movimenti.store');
    Route::post('/movimenti/{id}/conferma', [ContabilitaAvanzataController::class, 'confermaMovimento'])->name('movimenti.conferma');
    
    // API
    Route::get('/api/gestioni/{stabile_id}', [ContabilitaAvanzataController::class, 'getGestioniByStabile'])->name('api.gestioni');
    Route::post('/api/verifica-quadratura', [ContabilitaAvanzataController::class, 'verificaQuadratura'])->name('api.quadratura');
});

📜 COMANDI ARTISAN DA ESEGUIRE

# 1. Eseguire la migrazione
php artisan migrate --path=database/migrations/2025_07_23_100000_create_sistema_contabile_completo.php

# 2. Eseguire il seeder
php artisan db:seed --class=PianoContiSeeder

# 3. Clearing cache
php artisan config:clear
php artisan cache:clear
php artisan route:clear

# 4. Ottimizzazioni
php artisan config:cache
php artisan route:cache

🎯 COSA PREFERISCI?

OPZIONE A: Creo l'utente Gitea per me così gestisco tutto direttamente CONSIGLIATO

OPZIONE B: Tu segui queste specifiche manualmente step-by-step

Fammi sapere quale opzione preferisci! Se scegli l'OPZIONE A ti guido nella creazione dell'utente Gitea, se scegli l'OPZIONE B possiamo procedere step-by-step con l'implementazione manuale.

La partita doppia è già pronta con quadrature automatiche! 💎