netgescon-day0/docs/91-database/02-tabelle-collegate/contabilita.md

39 KiB

💰 TABELLA CONTABILITA

Archivio principale centralizzato di tutte le scritture contabili


🔑 CONNESSIONE DATABASE

Credenziali NetGescon:

# Connessione MySQL
mysql -u netgescon_user -p netgescon
# Password: NetGescon2024!

# Test connessione Laravel
cd /var/www/netgescon
php artisan tinker
DB::connection()->getPdo();

🎯 CARATTERISTICHE TABELLA

Scopo:

Archivio centralizzato di TUTTE le scritture contabili con collegamenti completi a gestioni, unità immobiliari, fatture elettroniche, movimenti bancari e piano dei conti

Principio:

Centralizzazione totale - ogni movimento passa da qui → controllo quadratura con banca → mastrini automatici per ogni conto → chiusura per gestione

Quadratura:

Ogni scrittura deve essere quadrata con i movimenti bancari. Sistema di riconciliazione automatica con estratti conto.


🗄️ STRUTTURA TABELLA CONTABILITA

CREATE TABLE contabilita (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    
    -- Collegamento gestione (OBBLIGATORIO per isolamento)
    gestione_id BIGINT UNSIGNED NOT NULL,             -- FK verso gestioni
    
    -- Collegamento piano dei conti (OBBLIGATORIO)
    piano_conti_id BIGINT UNSIGNED NOT NULL,          -- FK verso piano_conti
    
    -- Riferimenti business
    unita_immobiliare_id BIGINT UNSIGNED NULL,        -- FK per quote condominiali
    soggetto_id BIGINT UNSIGNED NULL,                 -- Chi paga/riceve (proprietario/inquilino/fornitore)
    fattura_elettronica_id BIGINT UNSIGNED NULL,      -- FK verso fatture_elettroniche
    movimento_bancario_id BIGINT UNSIGNED NULL,       -- FK verso movimenti_bancari per quadratura
    
    -- Dati contabili principali
    data_registrazione DATE NOT NULL,                 -- Data contabile
    data_competenza DATE NOT NULL,                    -- Data di competenza economica
    numero_registrazione VARCHAR(20) NOT NULL,        -- Numero progressivo per gestione
    
    -- Importi (SEMPRE in EUR con 2 decimali)
    importo_dare DECIMAL(12, 2) DEFAULT 0.00,        -- Importo in DARE
    importo_avere DECIMAL(12, 2) DEFAULT 0.00,       -- Importo in AVERE
    
    -- Descrizioni e causali
    causale VARCHAR(255) NOT NULL,                    -- Descrizione movimento
    descrizione_dettagliata TEXT NULL,               -- Dettagli aggiuntivi
    
    -- Tipo movimento
    tipo_movimento ENUM('entrata', 'uscita', 'girocontabileo', 'storno') NOT NULL,
    categoria_movimento ENUM(
        'quota_ordinaria', 'quota_straordinaria', 'quota_riscaldamento',
        'spesa_amministrativa', 'spesa_manutenzione', 'spesa_servizi',
        'fattura_fornitore', 'bolletta', 'assicurazione',
        'incasso_banca', 'pagamento_banca', 'bonifico_in', 'bonifico_out',
        'fondo_riserva', 'interessi_attivi', 'interessi_passivi',
        'rimborso', 'anticipo', 'storno_precedente', 'altro'
    ) NOT NULL,
    
    -- Riferimenti documento
    numero_documento VARCHAR(100) NULL,               -- Numero fattura/ricevuta/ecc
    data_documento DATE NULL,                         -- Data documento originale
    protocollo_documento VARCHAR(50) NULL,           -- Protocollo interno
    
    -- Stato riconciliazione con banca
    stato_riconciliazione ENUM('da_riconciliare', 'riconciliato', 'discordanza', 'manuale') DEFAULT 'da_riconciliare',
    data_riconciliazione TIMESTAMP NULL,
    note_riconciliazione TEXT NULL,
    
    -- Ripartizione millesimale (per quote condominiali)
    usa_millesimi_generali BOOLEAN DEFAULT TRUE,
    usa_millesimi_riscaldamento BOOLEAN DEFAULT FALSE,
    usa_millesimi_ascensore BOOLEAN DEFAULT FALSE,
    coefficiente_ripartizione DECIMAL(8, 4) NULL,    -- Coefficiente specifico se non millesimi standard
    
    -- Periodo competenza (per ratei/risconti)
    competenza_dal DATE NULL,                         -- Inizio periodo di competenza
    competenza_al DATE NULL,                          -- Fine periodo di competenza
    
    -- Quadratura e controlli
    is_quadrato BOOLEAN DEFAULT FALSE,                -- Se quadra con movimento bancario
    importo_quadratura DECIMAL(12, 2) NULL,          -- Importo del movimento bancario associato
    differenza_quadratura DECIMAL(12, 2) NULL,       -- Eventuale differenza
    
    -- Mastrino e saldi
    saldo_dare_progressivo DECIMAL(12, 2) DEFAULT 0.00,    -- Saldo DARE del conto a questa data
    saldo_avere_progressivo DECIMAL(12, 2) DEFAULT 0.00,   -- Saldo AVERE del conto a questa data
    saldo_finale_conto DECIMAL(12, 2) DEFAULT 0.00,        -- Saldo finale del conto (DARE - AVERE)
    
    -- Stato e workflow
    stato ENUM('bozza', 'registrato', 'contabilizzato', 'chiuso') DEFAULT 'bozza',
    is_stornato BOOLEAN DEFAULT FALSE,
    movimento_storno_id BIGINT UNSIGNED NULL,         -- FK verso movimento di storno
    data_storno TIMESTAMP NULL,
    
    -- Metadati
    user_id BIGINT UNSIGNED NULL,                     -- Chi ha inserito la registrazione
    note_interne TEXT NULL,                           -- Note per uso interno
    tag_personalizzati JSON NULL,                     -- Tag per ricerche/filtri
    allegati_paths JSON NULL,                         -- Path ai documenti allegati
    
    -- Audit completo
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    -- Foreign Keys
    FOREIGN KEY (gestione_id) REFERENCES gestioni(id) ON DELETE RESTRICT,
    FOREIGN KEY (piano_conti_id) REFERENCES piano_conti(id) ON DELETE RESTRICT,
    FOREIGN KEY (unita_immobiliare_id) REFERENCES unita_immobiliari(id) ON DELETE SET NULL,
    FOREIGN KEY (soggetto_id) REFERENCES soggetti(id) ON DELETE SET NULL,
    FOREIGN KEY (fattura_elettronica_id) REFERENCES fatture_elettroniche(id) ON DELETE SET NULL,
    FOREIGN KEY (movimento_bancario_id) REFERENCES movimenti_bancari(id) ON DELETE SET NULL,
    FOREIGN KEY (movimento_storno_id) REFERENCES contabilita(id) ON DELETE SET NULL,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
    
    -- Indici principali
    INDEX idx_gestione_id (gestione_id),
    INDEX idx_piano_conti_id (piano_conti_id),
    INDEX idx_data_registrazione (data_registrazione),
    INDEX idx_data_competenza (data_competenza),
    INDEX idx_numero_registrazione (numero_registrazione),
    INDEX idx_unita_immobiliare_id (unita_immobiliare_id),
    INDEX idx_soggetto_id (soggetto_id),
    INDEX idx_tipo_movimento (tipo_movimento),
    INDEX idx_categoria_movimento (categoria_movimento),
    INDEX idx_stato (stato),
    INDEX idx_stato_riconciliazione (stato_riconciliazione),
    INDEX idx_movimento_bancario_id (movimento_bancario_id),
    INDEX idx_fattura_elettronica_id (fattura_elettronica_id),
    
    -- Indici composti per performance
    INDEX idx_gestione_data (gestione_id, data_registrazione),
    INDEX idx_gestione_piano_conti (gestione_id, piano_conti_id),
    INDEX idx_piano_conti_data (piano_conti_id, data_registrazione),
    INDEX idx_unita_soggetto (unita_immobiliare_id, soggetto_id),
    INDEX idx_gestione_categoria (gestione_id, categoria_movimento),
    INDEX idx_quadratura (is_quadrato, stato_riconciliazione),
    
    -- Indici per mastrini
    INDEX idx_mastrino_conto (piano_conti_id, data_registrazione, stato),
    INDEX idx_mastrino_gestione (gestione_id, piano_conti_id, data_registrazione),
    
    -- Vincoli business
    CONSTRAINT chk_importi_validi CHECK (
        (importo_dare >= 0 AND importo_avere >= 0) AND 
        (importo_dare > 0 OR importo_avere > 0) AND
        NOT (importo_dare > 0 AND importo_avere > 0)
    ),
    
    CONSTRAINT chk_competenza_valida CHECK (
        competenza_dal IS NULL OR competenza_al IS NULL OR competenza_dal <= competenza_al
    ),
    
    -- Vincolo univocità numero registrazione per gestione
    UNIQUE KEY unique_numero_gestione (gestione_id, numero_registrazione)
);

🔗 TABELLE AUSILIARIE PER MASTRINI

Tabella Saldi Progressivi (per performance)

CREATE TABLE contabilita_saldi_progressivi (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    piano_conti_id BIGINT UNSIGNED NOT NULL,
    gestione_id BIGINT UNSIGNED NOT NULL,
    data_saldo DATE NOT NULL,
    
    -- Saldi calcolati
    saldo_dare_cumulato DECIMAL(12, 2) DEFAULT 0.00,
    saldo_avere_cumulato DECIMAL(12, 2) DEFAULT 0.00,
    saldo_finale DECIMAL(12, 2) DEFAULT 0.00,          -- DARE - AVERE
    
    -- Totali movimenti nel giorno
    totale_dare_giorno DECIMAL(12, 2) DEFAULT 0.00,
    totale_avere_giorno DECIMAL(12, 2) DEFAULT 0.00,
    numero_movimenti_giorno INT DEFAULT 0,
    
    -- Audit
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    -- Foreign Keys
    FOREIGN KEY (piano_conti_id) REFERENCES piano_conti(id) ON DELETE CASCADE,
    FOREIGN KEY (gestione_id) REFERENCES gestioni(id) ON DELETE CASCADE,
    
    -- Indici
    INDEX idx_piano_conti_data (piano_conti_id, data_saldo),
    INDEX idx_gestione_data (gestione_id, data_saldo),
    
    -- Vincolo univocità
    UNIQUE KEY unique_conto_gestione_data (piano_conti_id, gestione_id, data_saldo)
);

Tabella Riconciliazioni Bancarie

CREATE TABLE contabilita_riconciliazioni (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    gestione_id BIGINT UNSIGNED NOT NULL,
    movimento_bancario_id BIGINT UNSIGNED NOT NULL,
    contabilita_id BIGINT UNSIGNED NOT NULL,
    
    -- Dati riconciliazione
    data_riconciliazione TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    tipo_riconciliazione ENUM('automatica', 'manuale', 'forzata') DEFAULT 'automatica',
    
    -- Importi
    importo_contabile DECIMAL(12, 2) NOT NULL,
    importo_bancario DECIMAL(12, 2) NOT NULL,
    differenza DECIMAL(12, 2) GENERATED ALWAYS AS (importo_contabile - importo_bancario) STORED,
    
    -- Stato
    stato ENUM('ok', 'differenza', 'errore') NOT NULL,
    note TEXT NULL,
    
    -- Audit
    user_id BIGINT UNSIGNED NULL,                      -- Chi ha fatto la riconciliazione
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    -- Foreign Keys
    FOREIGN KEY (gestione_id) REFERENCES gestioni(id) ON DELETE CASCADE,
    FOREIGN KEY (movimento_bancario_id) REFERENCES movimenti_bancari(id) ON DELETE CASCADE,
    FOREIGN KEY (contabilita_id) REFERENCES contabilita(id) ON DELETE CASCADE,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
    
    -- Indici
    INDEX idx_gestione_id (gestione_id),
    INDEX idx_movimento_bancario_id (movimento_bancario_id),
    INDEX idx_contabilita_id (contabilita_id),
    INDEX idx_data_riconciliazione (data_riconciliazione),
    INDEX idx_stato (stato),
    
    -- Vincolo univocità movimento bancario
    UNIQUE KEY unique_movimento_bancario (movimento_bancario_id)
);

🏗️ MIGRATION LARAVEL

File: database/migrations/2024_08_05_000050_create_contabilita_table.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
    {
        Schema::create('contabilita', function (Blueprint $table) {
            $table->id();
            
            // Collegamento gestione (OBBLIGATORIO)
            $table->foreignId('gestione_id')
                  ->constrained('gestioni')
                  ->onDelete('restrict');
                  
            // Collegamento piano dei conti (OBBLIGATORIO)
            $table->foreignId('piano_conti_id')
                  ->constrained('piano_conti')
                  ->onDelete('restrict');
                  
            // Riferimenti business (opzionali)
            $table->foreignId('unita_immobiliare_id')
                  ->nullable()
                  ->constrained('unita_immobiliari')
                  ->onDelete('set null');
            $table->foreignId('soggetto_id')
                  ->nullable()
                  ->constrained('soggetti')
                  ->onDelete('set null');
            $table->foreignId('fattura_elettronica_id')
                  ->nullable()
                  ->constrained('fatture_elettroniche')
                  ->onDelete('set null');
            $table->foreignId('movimento_bancario_id')
                  ->nullable()
                  ->constrained('movimenti_bancari')
                  ->onDelete('set null');
                  
            // Dati contabili principali
            $table->date('data_registrazione');
            $table->date('data_competenza');
            $table->string('numero_registrazione', 20);
            
            // Importi
            $table->decimal('importo_dare', 12, 2)->default(0.00);
            $table->decimal('importo_avere', 12, 2)->default(0.00);
            
            // Descrizioni
            $table->string('causale');
            $table->text('descrizione_dettagliata')->nullable();
            
            // Tipo movimento
            $table->enum('tipo_movimento', ['entrata', 'uscita', 'girocontabile', 'storno']);
            $table->enum('categoria_movimento', [
                'quota_ordinaria', 'quota_straordinaria', 'quota_riscaldamento',
                'spesa_amministrativa', 'spesa_manutenzione', 'spesa_servizi',
                'fattura_fornitore', 'bolletta', 'assicurazione',
                'incasso_banca', 'pagamento_banca', 'bonifico_in', 'bonifico_out',
                'fondo_riserva', 'interessi_attivi', 'interessi_passivi',
                'rimborso', 'anticipo', 'storno_precedente', 'altro'
            ]);
            
            // Riferimenti documento
            $table->string('numero_documento', 100)->nullable();
            $table->date('data_documento')->nullable();
            $table->string('protocollo_documento', 50)->nullable();
            
            // Stato riconciliazione
            $table->enum('stato_riconciliazione', ['da_riconciliare', 'riconciliato', 'discordanza', 'manuale'])
                  ->default('da_riconciliare');
            $table->timestamp('data_riconciliazione')->nullable();
            $table->text('note_riconciliazione')->nullable();
            
            // Ripartizione millesimale
            $table->boolean('usa_millesimi_generali')->default(true);
            $table->boolean('usa_millesimi_riscaldamento')->default(false);
            $table->boolean('usa_millesimi_ascensore')->default(false);
            $table->decimal('coefficiente_ripartizione', 8, 4)->nullable();
            
            // Periodo competenza
            $table->date('competenza_dal')->nullable();
            $table->date('competenza_al')->nullable();
            
            // Quadratura
            $table->boolean('is_quadrato')->default(false);
            $table->decimal('importo_quadratura', 12, 2)->nullable();
            $table->decimal('differenza_quadratura', 12, 2)->nullable();
            
            // Saldi progressivi
            $table->decimal('saldo_dare_progressivo', 12, 2)->default(0.00);
            $table->decimal('saldo_avere_progressivo', 12, 2)->default(0.00);
            $table->decimal('saldo_finale_conto', 12, 2)->default(0.00);
            
            // Stato e workflow
            $table->enum('stato', ['bozza', 'registrato', 'contabilizzato', 'chiuso'])
                  ->default('bozza');
            $table->boolean('is_stornato')->default(false);
            $table->foreignId('movimento_storno_id')
                  ->nullable()
                  ->constrained('contabilita')
                  ->onDelete('set null');
            $table->timestamp('data_storno')->nullable();
            
            // Metadati
            $table->foreignId('user_id')
                  ->nullable()
                  ->constrained('users')
                  ->onDelete('set null');
            $table->text('note_interne')->nullable();
            $table->json('tag_personalizzati')->nullable();
            $table->json('allegati_paths')->nullable();
            
            $table->timestamps();
            
            // Indici principali
            $table->index(['gestione_id']);
            $table->index(['piano_conti_id']);
            $table->index(['data_registrazione']);
            $table->index(['data_competenza']);
            $table->index(['numero_registrazione']);
            $table->index(['tipo_movimento']);
            $table->index(['categoria_movimento']);
            $table->index(['stato']);
            $table->index(['stato_riconciliazione']);
            
            // Indici composti
            $table->index(['gestione_id', 'data_registrazione'], 'idx_gestione_data');
            $table->index(['gestione_id', 'piano_conti_id'], 'idx_gestione_piano_conti');
            $table->index(['piano_conti_id', 'data_registrazione'], 'idx_piano_conti_data');
            $table->index(['piano_conti_id', 'data_registrazione', 'stato'], 'idx_mastrino_conto');
            
            // Vincolo univocità numero registrazione
            $table->unique(['gestione_id', 'numero_registrazione'], 'unique_numero_gestione');
        });
        
        // Aggiungi vincoli custom con SQL raw
        DB::statement('
            ALTER TABLE contabilita 
            ADD CONSTRAINT chk_importi_validi 
            CHECK (
                (importo_dare >= 0 AND importo_avere >= 0) AND 
                (importo_dare > 0 OR importo_avere > 0) AND
                NOT (importo_dare > 0 AND importo_avere > 0)
            )
        ');
        
        DB::statement('
            ALTER TABLE contabilita 
            ADD CONSTRAINT chk_competenza_valida 
            CHECK (
                competenza_dal IS NULL OR competenza_al IS NULL OR competenza_dal <= competenza_al
            )
        ');
    }

    public function down(): void
    {
        Schema::dropIfExists('contabilita');
    }
};

File: database/migrations/2024_08_05_000051_create_contabilita_saldi_progressivi_table.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(): void
    {
        Schema::create('contabilita_saldi_progressivi', function (Blueprint $table) {
            $table->id();
            $table->foreignId('piano_conti_id')
                  ->constrained('piano_conti')
                  ->onDelete('cascade');
            $table->foreignId('gestione_id')
                  ->constrained('gestioni')
                  ->onDelete('cascade');
            $table->date('data_saldo');
            
            // Saldi calcolati
            $table->decimal('saldo_dare_cumulato', 12, 2)->default(0.00);
            $table->decimal('saldo_avere_cumulato', 12, 2)->default(0.00);
            $table->decimal('saldo_finale', 12, 2)->default(0.00);
            
            // Totali giornalieri
            $table->decimal('totale_dare_giorno', 12, 2)->default(0.00);
            $table->decimal('totale_avere_giorno', 12, 2)->default(0.00);
            $table->integer('numero_movimenti_giorno')->default(0);
            
            $table->timestamps();
            
            // Indici
            $table->index(['piano_conti_id', 'data_saldo'], 'idx_piano_conti_data');
            $table->index(['gestione_id', 'data_saldo'], 'idx_gestione_data');
            
            // Vincolo univocità
            $table->unique(['piano_conti_id', 'gestione_id', 'data_saldo'], 'unique_conto_gestione_data');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('contabilita_saldi_progressivi');
    }
};

📊 MODEL ELOQUENT

File: app/Models/Contabilita.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Builder;
use Carbon\Carbon;

class Contabilita extends Model
{
    protected $table = 'contabilita';
    
    protected $fillable = [
        'gestione_id',
        'piano_conti_id',
        'unita_immobiliare_id',
        'soggetto_id',
        'fattura_elettronica_id',
        'movimento_bancario_id',
        'data_registrazione',
        'data_competenza',
        'numero_registrazione',
        'importo_dare',
        'importo_avere',
        'causale',
        'descrizione_dettagliata',
        'tipo_movimento',
        'categoria_movimento',
        'numero_documento',
        'data_documento',
        'protocollo_documento',
        'stato_riconciliazione',
        'data_riconciliazione',
        'note_riconciliazione',
        'usa_millesimi_generali',
        'usa_millesimi_riscaldamento',
        'usa_millesimi_ascensore',
        'coefficiente_ripartizione',
        'competenza_dal',
        'competenza_al',
        'is_quadrato',
        'importo_quadratura',
        'differenza_quadratura',
        'saldo_dare_progressivo',
        'saldo_avere_progressivo',
        'saldo_finale_conto',
        'stato',
        'is_stornato',
        'movimento_storno_id',
        'data_storno',
        'user_id',
        'note_interne',
        'tag_personalizzati',
        'allegati_paths',
    ];

    protected $casts = [
        'data_registrazione' => 'date',
        'data_competenza' => 'date',
        'data_documento' => 'date',
        'data_riconciliazione' => 'datetime',
        'data_storno' => 'datetime',
        'competenza_dal' => 'date',
        'competenza_al' => 'date',
        'importo_dare' => 'decimal:2',
        'importo_avere' => 'decimal:2',
        'importo_quadratura' => 'decimal:2',
        'differenza_quadratura' => 'decimal:2',
        'saldo_dare_progressivo' => 'decimal:2',
        'saldo_avere_progressivo' => 'decimal:2',
        'saldo_finale_conto' => 'decimal:2',
        'coefficiente_ripartizione' => 'decimal:4',
        'is_quadrato' => 'boolean',
        'is_stornato' => 'boolean',
        'usa_millesimi_generali' => 'boolean',
        'usa_millesimi_riscaldamento' => 'boolean',
        'usa_millesimi_ascensore' => 'boolean',
        'tag_personalizzati' => 'array',
        'allegati_paths' => 'array',
    ];

    // Relazioni principali
    public function gestione(): BelongsTo
    {
        return $this->belongsTo(Gestione::class);
    }

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

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

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

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

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

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

    // Relazioni storni
    public function movimentoStorno(): BelongsTo
    {
        return $this->belongsTo(Contabilita::class, 'movimento_storno_id');
    }

    public function movimentiStornoDi(): HasMany
    {
        return $this->hasMany(Contabilita::class, 'movimento_storno_id');
    }

    // Relazione riconciliazione
    public function riconciliazione(): HasOne
    {
        return $this->hasOne(ContabilitaRiconciliazione::class);
    }

    // Scope automatico per amministratore via gestione
    protected static function booted(): void
    {
        static::addGlobalScope('amministratore', function (Builder $builder) {
            $user = auth()->user();
            if ($user && $user->amministratore_id) {
                $builder->whereHas('gestione.stabile', function ($query) use ($user) {
                    $query->where('amministratore_id', $user->amministratore_id);
                });
            }
        });
    }

    // Scope filtri
    public function scopeByGestione(Builder $query, int $gestioneId): Builder
    {
        return $query->where('gestione_id', $gestioneId);
    }

    public function scopeByPianoConti(Builder $query, int $pianoContiId): Builder
    {
        return $query->where('piano_conti_id', $pianoContiId);
    }

    public function scopeByTipoMovimento(Builder $query, string $tipo): Builder
    {
        return $query->where('tipo_movimento', $tipo);
    }

    public function scopeByCategoriaMovimento(Builder $query, string $categoria): Builder
    {
        return $query->where('categoria_movimento', $categoria);
    }

    public function scopeByStato(Builder $query, string $stato): Builder
    {
        return $query->where('stato', $stato);
    }

    public function scopeByUnitaImmobiliare(Builder $query, int $unitaId): Builder
    {
        return $query->where('unita_immobiliare_id', $unitaId);
    }

    public function scopeBySoggetto(Builder $query, int $soggettoId): Builder
    {
        return $query->where('soggetto_id', $soggettoId);
    }

    public function scopeEntrate(Builder $query): Builder
    {
        return $query->where('tipo_movimento', 'entrata');
    }

    public function scopeUscite(Builder $query): Builder
    {
        return $query->where('tipo_movimento', 'uscita');
    }

    public function scopeQuadrate(Builder $query): Builder
    {
        return $query->where('is_quadrato', true);
    }

    public function scopeDaRiconciliare(Builder $query): Builder
    {
        return $query->where('stato_riconciliazione', 'da_riconciliare');
    }

    public function scopeByPeriodo(Builder $query, Carbon $dataInizio, Carbon $dataFine): Builder
    {
        return $query->whereBetween('data_registrazione', [$dataInizio, $dataFine]);
    }

    public function scopeByCompetenza(Builder $query, Carbon $dataInizio, Carbon $dataFine): Builder
    {
        return $query->whereBetween('data_competenza', [$dataInizio, $dataFine]);
    }

    public function scopeNonStornate(Builder $query): Builder
    {
        return $query->where('is_stornato', false);
    }

    public function scopePerMastrino(Builder $query, int $pianoContiId, int $gestioneId): Builder
    {
        return $query->where('piano_conti_id', $pianoContiId)
                    ->where('gestione_id', $gestioneId)
                    ->where('stato', '!=', 'bozza')
                    ->orderBy('data_registrazione')
                    ->orderBy('id');
    }

    // Accessors
    public function getImportoMovimentoAttribute(): float
    {
        return $this->importo_dare > 0 ? $this->importo_dare : $this->importo_avere;
    }

    public function getSegnoMovimentoAttribute(): string
    {
        return $this->importo_dare > 0 ? 'DARE' : 'AVERE';
    }

    public function getIsEntrataAttribute(): bool
    {
        return $this->tipo_movimento === 'entrata';
    }

    public function getIsUscitaAttribute(): bool
    {
        return $this->tipo_movimento === 'uscita';
    }

    public function getStatoColorAttribute(): string
    {
        return match($this->stato) {
            'bozza' => 'gray',
            'registrato' => 'blue',
            'contabilizzato' => 'green',
            'chiuso' => 'purple',
            default => 'gray'
        };
    }

    public function getRiconciliazioneColorAttribute(): string
    {
        return match($this->stato_riconciliazione) {
            'da_riconciliare' => 'orange',
            'riconciliato' => 'green',
            'discordanza' => 'red',
            'manuale' => 'blue',
            default => 'gray'
        };
    }

    public function getHasAllegatiAttribute(): bool
    {
        return !empty($this->allegati_paths);
    }

    public function getIsDareAttribute(): bool
    {
        return $this->importo_dare > 0;
    }

    public function getIsAvereAttribute(): bool
    {
        return $this->importo_avere > 0;
    }

    // Helper Methods

    public function generaNumeroRegistrazione(): string
    {
        $ultimoNumero = static::byGestione($this->gestione_id)
            ->where('id', '!=', $this->id ?? 0)
            ->max('numero_registrazione');
            
        $numeroProgressivo = $ultimoNumero ? (int)substr($ultimoNumero, -6) + 1 : 1;
        
        return sprintf(
            '%04d%06d', 
            $this->gestione->anno_gestione, 
            $numeroProgressivo
        );
    }

    public function calcolaSaldoProgressivo(): void
    {
        // Calcola saldo progressivo per questo conto fino a questa data
        $saldoPrecedente = static::perMastrino($this->piano_conti_id, $this->gestione_id)
            ->where('data_registrazione', '<', $this->data_registrazione)
            ->orWhere(function($query) {
                $query->where('data_registrazione', '=', $this->data_registrazione)
                      ->where('id', '<', $this->id);
            })
            ->selectRaw('
                COALESCE(SUM(importo_dare), 0) as totale_dare,
                COALESCE(SUM(importo_avere), 0) as totale_avere
            ')
            ->first();
            
        $this->saldo_dare_progressivo = ($saldoPrecedente->totale_dare ?? 0) + $this->importo_dare;
        $this->saldo_avere_progressivo = ($saldoPrecedente->totale_avere ?? 0) + $this->importo_avere;
        $this->saldo_finale_conto = $this->saldo_dare_progressivo - $this->saldo_avere_progressivo;
    }

    public function aggiornaStato(string $nuovoStato): bool
    {
        $statiConsentiti = ['bozza', 'registrato', 'contabilizzato', 'chiuso'];
        
        if (!in_array($nuovoStato, $statiConsentiti)) {
            return false;
        }
        
        // Verifica progressione stati
        $progressione = [
            'bozza' => ['registrato'],
            'registrato' => ['contabilizzato', 'bozza'],
            'contabilizzato' => ['chiuso', 'registrato'],
            'chiuso' => [] // Non si può tornare indietro da chiuso
        ];
        
        if (!in_array($nuovoStato, $progressione[$this->stato] ?? [])) {
            return false;
        }
        
        $this->update(['stato' => $nuovoStato]);
        
        // Aggiorna saldi se passa a contabilizzato
        if ($nuovoStato === 'contabilizzato') {
            $this->aggiornaStato(Contabilita::costo());
        }
        
        return true;
    }

    public function storna(string $motivazione = null): ?Contabilita
    {
        if ($this->is_stornato || $this->stato === 'chiuso') {
            return null;
        }
        
        // Crea movimento di storno
        $storno = static::create([
            'gestione_id' => $this->gestione_id,
            'piano_conti_id' => $this->piano_conti_id,
            'unita_immobiliare_id' => $this->unita_immobiliare_id,
            'soggetto_id' => $this->soggetto_id,
            'data_registrazione' => now()->toDateString(),
            'data_competenza' => $this->data_competenza,
            'numero_registrazione' => $this->generaNumeroRegistrazione(),
            
            // Inverti importi
            'importo_dare' => $this->importo_avere,
            'importo_avere' => $this->importo_dare,
            
            'causale' => "STORNO: {$this->causale}",
            'descrizione_dettagliata' => $motivazione ? "Storno per: {$motivazione}" : null,
            'tipo_movimento' => 'storno',
            'categoria_movimento' => 'storno_precedente',
            
            'movimento_storno_id' => $this->id,
            'stato' => 'registrato',
            'user_id' => auth()->id(),
        ]);
        
        // Marca questo movimento come stornato
        $this->update([
            'is_stornato' => true,
            'data_storno' => now(),
        ]);
        
        return $storno;
    }

    public function riconciliaConBanca(MovimentoBancario $movimentoBancario, string $tipo = 'automatica'): bool
    {
        $differenza = $this->importo_movimento - $movimentoBancario->importo;
        $tolleranza = 0.01; // 1 centesimo di tolleranza
        
        $statoRiconciliazione = abs($differenza) <= $tolleranza ? 'riconciliato' : 'discordanza';
        
        $this->update([
            'movimento_bancario_id' => $movimentoBancario->id,
            'stato_riconciliazione' => $statoRiconciliazione,
            'data_riconciliazione' => now(),
            'is_quadrato' => abs($differenza) <= $tolleranza,
            'importo_quadratura' => $movimentoBancario->importo,
            'differenza_quadratura' => $differenza,
        ]);
        
        // Crea record riconciliazione
        ContabilitaRiconciliazione::create([
            'gestione_id' => $this->gestione_id,
            'movimento_bancario_id' => $movimentoBancario->id,
            'contabilita_id' => $this->id,
            'importo_contabile' => $this->importo_movimento,
            'importo_bancario' => $movimentoBancario->importo,
            'tipo_riconciliazione' => $tipo,
            'stato' => abs($differenza) <= $tolleranza ? 'ok' : 'differenza',
            'user_id' => auth()->id(),
        ]);
        
        return abs($differenza) <= $tolleranza;
    }

    public function calcolaRipartizione(): array
    {
        if (!$this->unita_immobiliare_id) {
            return [];
        }
        
        $unita = $this->unitaImmobiliare;
        $stabile = $unita->stabile;
        
        // Determina tipo millesimi da usare
        $millesimi = 0;
        if ($this->usa_millesimi_generali) {
            $millesimi = $unita->millesimi_generali;
        } elseif ($this->usa_millesimi_riscaldamento) {
            $millesimi = $unita->millesimi_riscaldamento;
        } elseif ($this->usa_millesimi_ascensore) {
            $millesimi = $unita->millesimi_ascensore;
        } elseif ($this->coefficiente_ripartizione) {
            $millesimi = $this->coefficiente_ripartizione * 1000; // Converti in millesimi
        }
        
        $quotaUnita = ($millesimi / 1000) * $this->importo_movimento;
        
        return [
            'millesimi_utilizzati' => $millesimi,
            'tipo_millesimi' => $this->getTipoMillesimi(),
            'quota_unita' => $quotaUnita,
            'percentuale' => $millesimi / 10, // Converti in percentuale
        ];
    }

    public function getTipoMillesimi(): string
    {
        if ($this->usa_millesimi_generali) return 'generali';
        if ($this->usa_millesimi_riscaldamento) return 'riscaldamento';
        if ($this->usa_millesimi_ascensore) return 'ascensore';
        if ($this->coefficiente_ripartizione) return 'personalizzato';
        return 'nessuno';
    }

    public function canDelete(): bool
    {
        return $this->stato === 'bozza' && !$this->is_stornato;
    }

    public function canEdit(): bool
    {
        return in_array($this->stato, ['bozza', 'registrato']) && !$this->is_stornato;
    }

    public function canStorno(): bool
    {
        return $this->stato === 'contabilizzato' && !$this->is_stornato;
    }

    // Observer Events
    protected static function boot(): void
    {
        parent::boot();

        static::creating(function ($contabilita) {
            // Genera numero registrazione automatico
            if (!$contabilita->numero_registrazione) {
                $contabilita->numero_registrazione = $contabilita->generaNumeroRegistrazione();
            }
            
            // Data competenza default = data registrazione
            if (!$contabilita->data_competenza) {
                $contabilita->data_competenza = $contabilita->data_registrazione;
            }
        });

        static::created(function ($contabilita) {
            // Calcola saldo progressivo
            $contabilita->calcolaSaldoProgressivo();
            $contabilita->saveQuietly(); // Evita loop infinito
            
            // Aggiorna saldi successivi per questo conto
            $contabilita->aggiornasSaldiSuccessivi();
        });

        static::updated(function ($contabilita) {
            if ($contabilita->wasChanged(['importo_dare', 'importo_avere', 'data_registrazione'])) {
                $contabilita->calcolaSaldoProgressivo();
                $contabilita->aggiornasSaldiSuccessivi();
            }
        });

        static::deleted(function ($contabilita) {
            // Aggiorna saldi dopo eliminazione
            $contabilita->aggiornasSaldiSuccessivi();
        });
    }

    public function aggiornasSaldiSuccessivi(): void
    {
        // Aggiorna tutti i saldi successivi per questo conto
        $movimentiSuccessivi = static::perMastrino($this->piano_conti_id, $this->gestione_id)
            ->where('data_registrazione', '>', $this->data_registrazione)
            ->orWhere(function($query) {
                $query->where('data_registrazione', '=', $this->data_registrazione)
                      ->where('id', '>', $this->id);
            })
            ->orderBy('data_registrazione')
            ->orderBy('id')
            ->get();
            
        foreach ($movimentiSuccessivi as $movimento) {
            $movimento->calcolaSaldoProgressivo();
            $movimento->saveQuietly();
        }
    }
}

🔍 MASTRINI E SALDI

Metodi per Generazione Mastrini

// Nel Model Contabilita - Metodi statici per mastrini

public static function generaMastrino(int $pianoContiId, int $gestioneId): array
{
    $movimenti = static::perMastrino($pianoContiId, $gestioneId)->get();
    
    $mastrino = [
        'piano_conti_id' => $pianoContiId,
        'gestione_id' => $gestioneId,
        'movimenti' => $movimenti,
        'totale_dare' => $movimenti->sum('importo_dare'),
        'totale_avere' => $movimenti->sum('importo_avere'),
        'saldo_finale' => 0,
    ];
    
    $mastrino['saldo_finale'] = $mastrino['totale_dare'] - $mastrino['totale_avere'];
    
    return $mastrino;
}

public static function generaMastriniGestione(int $gestioneId): array
{
    $contiUtilizzati = static::byGestione($gestioneId)
        ->distinct()
        ->pluck('piano_conti_id');
        
    $mastrini = [];
    foreach ($contiUtilizzati as $contoId) {
        $mastrini[$contoId] = static::generaMastrino($contoId, $gestioneId);
    }
    
    return $mastrini;
}

public static function calcolaChiusuraConti(int $gestioneId): array
{
    $mastrini = static::generaMastriniGestione($gestioneId);
    
    $chiusura = [
        'totale_attivo' => 0,
        'totale_passivo' => 0,
        'utile_perdita' => 0,
        'conti_dettaglio' => [],
    ];
    
    foreach ($mastrini as $contoId => $mastrino) {
        $saldo = $mastrino['saldo_finale'];
        
        if ($saldo > 0) {
            $chiusura['totale_attivo'] += $saldo;
        } else {
            $chiusura['totale_passivo'] += abs($saldo);
        }
        
        $chiusura['conti_dettaglio'][$contoId] = $saldo;
    }
    
    $chiusura['utile_perdita'] = $chiusura['totale_attivo'] - $chiusura['totale_passivo'];
    
    return $chiusura;
}

🎯 CONTABILITÀ CENTRALIZZATA COMPLETATA
Con mastrini automatici, quadratura bancaria e collegamento completo alle gestioni, unità immobiliari, soggetti, fatture elettroniche e piano dei conti

📖 Ora hai il cuore contabile del sistema: ogni movimento centralizzato, saldi progressivi automatici, riconciliazione bancaria e chiusura per gestione tramite mastrini del piano dei conti collegato alle tabelle millesimali

Vuoi procedere con le fatture elettroniche o i movimenti bancari per completare l'ecosistema contabile?