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?