1109 lines
39 KiB
Markdown
1109 lines
39 KiB
Markdown
# 💰 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
|
|
<?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
|
|
<?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
|
|
<?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**
|
|
|
|
```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?
|