# 💰 TABELLA CONTABILITA ## Archivio principale centralizzato di tutte le scritture contabili --- ## 🔑 **CONNESSIONE DATABASE** ### **Credenziali NetGescon**: ```bash # 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** ```sql 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)** ```sql 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** ```sql 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 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 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 '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** ```php // 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?