📋 AGGIUNTE PRINCIPALI: - Sistema contabile partita doppia con gestioni multiple - Documentazione implementazione completa - Models Laravel: GestioneContabile, MovimentoPartitaDoppia - Controller ContabilitaAvanzataController - Migration sistema contabile completo - Scripts automazione e trasferimento - Manuali utente e checklist implementazione 📊 FILES PRINCIPALI: - docs/10-IMPLEMENTAZIONE-CONTABILITA-PARTITA-DOPPIA-GESTIONI.md - SPECIFICHE-SISTEMA-CONTABILE-COMPLETO.md - netgescon-laravel/database/migrations/2025_07_20_100000_create_complete_accounting_system.php - netgescon-laravel/app/Models/GestioneContabile.php ✅ CHECKPOINT SICURO PER ROLLBACK
46 KiB
46 KiB
📋 SPECIFICHE COMPLETE SISTEMA CONTABILE NETGESCON
🗃️ MIGRAZIONI DATABASE
File: database/migrations/2025_07_23_100000_create_sistema_contabile_completo.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
// 1. PIANO DEI CONTI MASTERPLAN
if (!Schema::hasTable('piano_conti_masterplan')) {
Schema::create('piano_conti_masterplan', function (Blueprint $table) {
$table->id();
$table->string('codice_conto', 10)->unique();
$table->string('descrizione_conto');
$table->enum('tipologia_conto', ['attivo', 'passivo', 'ricavo', 'costo', 'patrimoniale']);
$table->string('categoria_contabile', 50)->nullable();
$table->boolean('ripartibile')->default(true);
$table->json('default_ripartizioni')->nullable();
$table->boolean('attivo')->default(true);
$table->timestamps();
$table->index(['tipologia_conto', 'categoria_contabile']);
});
}
// 2. GESTIONI CONTABILI
if (!Schema::hasTable('gestioni_contabili')) {
Schema::create('gestioni_contabili', function (Blueprint $table) {
$table->id();
$table->char('codice_gestione', 8)->unique();
$table->unsignedBigInteger('stabile_id');
$table->unsignedBigInteger('esercizio_contabile_id');
$table->string('denominazione');
$table->text('descrizione')->nullable();
$table->enum('tipologia', ['ordinaria', 'riscaldamento', 'straordinaria', 'fondo_lavori', 'fondo_riserva']);
$table->enum('stato', ['attiva', 'chiusa', 'sospesa'])->default('attiva');
$table->date('data_apertura');
$table->date('data_chiusura')->nullable();
$table->decimal('budget_previsto', 12, 2)->default(0);
$table->decimal('fondo_cassa_iniziale', 12, 2)->default(0);
$table->json('regole_ripartizione')->nullable();
$table->unsignedBigInteger('tabella_millesimale_id')->nullable();
$table->timestamps();
$table->softDeletes();
$table->foreign('stabile_id')->references('id')->on('stabili')->onDelete('cascade');
$table->foreign('esercizio_contabile_id')->references('id')->on('esercizi_contabili')->onDelete('cascade');
$table->foreign('tabella_millesimale_id')->references('id')->on('tabelle_millesimali')->onDelete('set null');
$table->index(['stabile_id', 'tipologia', 'stato']);
$table->unique(['stabile_id', 'esercizio_contabile_id', 'tipologia'], 'unique_gestione_per_esercizio');
});
}
// 3. MOVIMENTI PARTITA DOPPIA
if (!Schema::hasTable('movimenti_partita_doppia')) {
Schema::create('movimenti_partita_doppia', function (Blueprint $table) {
$table->id();
$table->char('codice_movimento', 12)->unique();
$table->unsignedBigInteger('stabile_id');
$table->unsignedBigInteger('gestione_contabile_id');
$table->unsignedBigInteger('esercizio_contabile_id');
$table->date('data_movimento');
$table->date('data_registrazione')->default(DB::raw('CURRENT_DATE'));
$table->string('descrizione');
$table->text('causale_dettagliata')->nullable();
$table->text('note_interne')->nullable();
$table->string('tipo_documento', 50)->nullable();
$table->string('numero_documento')->nullable();
$table->date('data_documento')->nullable();
$table->unsignedBigInteger('fornitore_id')->nullable();
$table->unsignedBigInteger('documento_id')->nullable();
$table->string('numero_protocollo', 20)->nullable();
$table->integer('progressivo_anno')->nullable();
$table->enum('stato_movimento', ['bozza', 'da_verificare', 'verificato', 'confermato', 'chiuso'])->default('bozza');
$table->enum('tipologia_registrazione', ['ordinaria', 'straordinaria', 'chiusura', 'apertura', 'rettifica'])->default('ordinaria');
$table->decimal('importo_lordo', 12, 2);
$table->decimal('importo_iva', 12, 2)->default(0);
$table->decimal('importo_ritenute', 12, 2)->default(0);
$table->decimal('importo_netto', 12, 2);
$table->json('dettagli_fiscali')->nullable();
$table->boolean('ripartito')->default(false);
$table->json('ripartizione_millesimale')->nullable();
$table->unsignedBigInteger('tabella_millesimale_utilizzata')->nullable();
$table->unsignedBigInteger('creato_da');
$table->unsignedBigInteger('verificato_da')->nullable();
$table->unsignedBigInteger('confermato_da')->nullable();
$table->timestamp('data_verifica')->nullable();
$table->timestamp('data_conferma')->nullable();
$table->timestamps();
$table->softDeletes();
$table->foreign('stabile_id')->references('id')->on('stabili')->onDelete('cascade');
$table->foreign('gestione_contabile_id')->references('id')->on('gestioni_contabili')->onDelete('cascade');
$table->foreign('esercizio_contabile_id')->references('id')->on('esercizi_contabili')->onDelete('cascade');
$table->foreign('fornitore_id')->references('id')->on('fornitori')->onDelete('set null');
$table->foreign('tabella_millesimale_utilizzata')->references('id')->on('tabelle_millesimali')->onDelete('set null');
$table->foreign('creato_da')->references('id')->on('users')->onDelete('cascade');
$table->foreign('verificato_da')->references('id')->on('users')->onDelete('set null');
$table->foreign('confermato_da')->references('id')->on('users')->onDelete('set null');
$table->index(['stabile_id', 'data_movimento']);
$table->index(['gestione_contabile_id', 'stato_movimento']);
$table->index(['esercizio_contabile_id', 'tipologia_registrazione']);
$table->index(['numero_protocollo']);
$table->index(['progressivo_anno', 'stabile_id']);
});
}
// 4. RIGHE CONTABILI (DARE/AVERE)
if (!Schema::hasTable('righe_contabili')) {
Schema::create('righe_contabili', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('movimento_id');
$table->string('codice_conto', 10);
$table->string('descrizione_riga');
$table->enum('dare_avere', ['dare', 'avere']);
$table->decimal('importo', 12, 2);
$table->unsignedBigInteger('unita_immobiliare_id')->nullable();
$table->decimal('quota_millesimale', 10, 4)->nullable();
$table->text('note_riga')->nullable();
$table->timestamps();
$table->foreign('movimento_id')->references('id')->on('movimenti_partita_doppia')->onDelete('cascade');
$table->foreign('codice_conto')->references('codice_conto')->on('piano_conti_masterplan')->onDelete('cascade');
$table->foreign('unita_immobiliare_id')->references('id')->on('unita_immobiliari')->onDelete('set null');
$table->index(['movimento_id', 'dare_avere']);
$table->index(['codice_conto']);
});
}
// 5. RATE CONDOMINIALI
if (!Schema::hasTable('rate_condominiali')) {
Schema::create('rate_condominiali', function (Blueprint $table) {
$table->id();
$table->char('codice_rata', 12)->unique();
$table->unsignedBigInteger('stabile_id');
$table->unsignedBigInteger('gestione_contabile_id');
$table->unsignedBigInteger('unita_immobiliare_id');
$table->unsignedBigInteger('soggetto_id');
$table->string('tipo_rata', 50);
$table->date('data_scadenza');
$table->decimal('importo_dovuto', 10, 2);
$table->decimal('importo_pagato', 10, 2)->default(0);
$table->decimal('importo_residuo', 10, 2);
$table->enum('stato_pagamento', ['da_pagare', 'parzialmente_pagata', 'pagata', 'insoluta', 'stornata'])->default('da_pagare');
$table->date('data_primo_pagamento')->nullable();
$table->date('data_ultimo_pagamento')->nullable();
$table->decimal('millesimi_applicati', 10, 4);
$table->unsignedBigInteger('tabella_millesimale_id');
$table->decimal('interessi_mora', 10, 2)->default(0);
$table->date('data_decorrenza_mora')->nullable();
$table->decimal('percentuale_mora', 5, 2)->default(0);
$table->timestamps();
$table->softDeletes();
$table->foreign('stabile_id')->references('id')->on('stabili')->onDelete('cascade');
$table->foreign('gestione_contabile_id')->references('id')->on('gestioni_contabili')->onDelete('cascade');
$table->foreign('unita_immobiliare_id')->references('id')->on('unita_immobiliari')->onDelete('cascade');
$table->foreign('soggetto_id')->references('id')->on('soggetti')->onDelete('cascade');
$table->foreign('tabella_millesimale_id')->references('id')->on('tabelle_millesimali')->onDelete('cascade');
$table->index(['stabile_id', 'data_scadenza']);
$table->index(['gestione_contabile_id', 'stato_pagamento']);
$table->index(['soggetto_id', 'stato_pagamento']);
});
}
// 6. PAGAMENTI RATE
if (!Schema::hasTable('pagamenti_rate')) {
Schema::create('pagamenti_rate', function (Blueprint $table) {
$table->id();
$table->char('codice_pagamento', 12)->unique();
$table->unsignedBigInteger('rata_id');
$table->date('data_pagamento');
$table->decimal('importo_pagamento', 10, 2);
$table->string('modalita_pagamento', 50);
$table->string('riferimento_pagamento')->nullable();
$table->text('note_pagamento')->nullable();
$table->unsignedBigInteger('movimento_bancario_id')->nullable();
$table->timestamps();
$table->foreign('rata_id')->references('id')->on('rate_condominiali')->onDelete('cascade');
$table->index(['rata_id', 'data_pagamento']);
});
}
// 7. DOCUMENTI CONTABILI
if (!Schema::hasTable('documenti_contabili')) {
Schema::create('documenti_contabili', function (Blueprint $table) {
$table->id();
$table->char('codice_documento', 12)->unique();
$table->unsignedBigInteger('stabile_id');
$table->unsignedBigInteger('movimento_id')->nullable();
$table->string('tipo_documento', 50);
$table->string('numero_documento');
$table->date('data_documento');
$table->string('oggetto');
$table->text('descrizione')->nullable();
$table->string('file_path')->nullable();
$table->string('file_originale')->nullable();
$table->string('mime_type')->nullable();
$table->bigInteger('file_size')->nullable();
$table->string('numero_protocollo')->nullable();
$table->date('data_protocollo')->nullable();
$table->timestamps();
$table->softDeletes();
$table->foreign('stabile_id')->references('id')->on('stabili')->onDelete('cascade');
$table->foreign('movimento_id')->references('id')->on('movimenti_partita_doppia')->onDelete('set null');
$table->index(['stabile_id', 'tipo_documento']);
$table->index(['numero_protocollo']);
});
}
// 8. AUDIT CONTABILITA
if (!Schema::hasTable('audit_contabilita')) {
Schema::create('audit_contabilita', function (Blueprint $table) {
$table->id();
$table->string('tabella_interessata');
$table->unsignedBigInteger('record_id');
$table->string('azione');
$table->json('dati_precedenti')->nullable();
$table->json('dati_nuovi')->nullable();
$table->unsignedBigInteger('user_id');
$table->string('ip_address')->nullable();
$table->string('user_agent')->nullable();
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->index(['tabella_interessata', 'record_id']);
$table->index(['user_id', 'created_at']);
});
}
}
public function down(): void
{
Schema::dropIfExists('audit_contabilita');
Schema::dropIfExists('documenti_contabili');
Schema::dropIfExists('pagamenti_rate');
Schema::dropIfExists('rate_condominiali');
Schema::dropIfExists('righe_contabili');
Schema::dropIfExists('movimenti_partita_doppia');
Schema::dropIfExists('gestioni_contabili');
Schema::dropIfExists('piano_conti_masterplan');
}
};
🏗️ MODELS DA CREARE
File: app/Models/GestioneContabile.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class GestioneContabile extends Model
{
use HasFactory, SoftDeletes;
protected $table = 'gestioni_contabili';
protected $fillable = [
'codice_gestione', 'stabile_id', 'esercizio_contabile_id',
'denominazione', 'descrizione', 'tipologia', 'stato',
'data_apertura', 'data_chiusura', 'budget_previsto',
'fondo_cassa_iniziale', 'regole_ripartizione', 'tabella_millesimale_id'
];
protected $casts = [
'data_apertura' => 'date',
'data_chiusura' => 'date',
'budget_previsto' => 'decimal:2',
'fondo_cassa_iniziale' => 'decimal:2',
'regole_ripartizione' => 'json'
];
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
if (!$model->codice_gestione) {
$model->codice_gestione = static::generateCodiceGestione();
}
});
}
public static function generateCodiceGestione(): string
{
do {
$codice = 'GES' . sprintf('%05d', rand(10000, 99999));
} while (static::where('codice_gestione', $codice)->exists());
return $codice;
}
// Relazioni
public function stabile(): BelongsTo
{
return $this->belongsTo(Stabile::class);
}
public function esercizioContabile(): BelongsTo
{
return $this->belongsTo(EsercizioContabile::class, 'esercizio_contabile_id');
}
public function tabellaMillesimale(): BelongsTo
{
return $this->belongsTo(TabellaMillesimale::class, 'tabella_millesimale_id');
}
public function movimenti(): HasMany
{
return $this->hasMany(MovimentoPartitaDoppia::class, 'gestione_contabile_id');
}
public function rate(): HasMany
{
return $this->hasMany(RataCondominiale::class, 'gestione_contabile_id');
}
// Scopes
public function scopeAttive($query)
{
return $query->where('stato', 'attiva');
}
public function scopeByTipologia($query, $tipologia)
{
return $query->where('tipologia', $tipologia);
}
// Business Logic
public function calcolaSaldoContabile(): float
{
$entrate = $this->movimenti()
->whereHas('righeContabili', function($q) {
$q->where('dare_avere', 'avere');
})
->sum('importo_netto');
$uscite = $this->movimenti()
->whereHas('righeContabili', function($q) {
$q->where('dare_avere', 'dare');
})
->sum('importo_netto');
return $entrate - $uscite;
}
public function isChiudibile(): bool
{
$movimentiNonConfermati = $this->movimenti()
->whereIn('stato_movimento', ['bozza', 'da_verificare'])
->count();
return $movimentiNonConfermati === 0;
}
public function chiudiGestione(): bool
{
if (!$this->isChiudibile()) {
return false;
}
$this->update([
'stato' => 'chiusa',
'data_chiusura' => now(),
]);
return true;
}
}
File: app/Models/MovimentoPartitaDoppia.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class MovimentoPartitaDoppia extends Model
{
use HasFactory, SoftDeletes;
protected $table = 'movimenti_partita_doppia';
protected $fillable = [
'codice_movimento', 'stabile_id', 'gestione_contabile_id', 'esercizio_contabile_id',
'data_movimento', 'data_registrazione', 'descrizione', 'causale_dettagliata',
'note_interne', 'tipo_documento', 'numero_documento', 'data_documento',
'fornitore_id', 'documento_id', 'numero_protocollo', 'progressivo_anno',
'stato_movimento', 'tipologia_registrazione', 'importo_lordo', 'importo_iva',
'importo_ritenute', 'importo_netto', 'dettagli_fiscali', 'ripartito',
'ripartizione_millesimale', 'tabella_millesimale_utilizzata',
'creato_da', 'verificato_da', 'confermato_da', 'data_verifica', 'data_conferma'
];
protected $casts = [
'data_movimento' => 'date',
'data_registrazione' => 'date',
'data_documento' => 'date',
'data_verifica' => 'datetime',
'data_conferma' => 'datetime',
'importo_lordo' => 'decimal:2',
'importo_iva' => 'decimal:2',
'importo_ritenute' => 'decimal:2',
'importo_netto' => 'decimal:2',
'dettagli_fiscali' => 'json',
'ripartito' => 'boolean',
'ripartizione_millesimale' => 'json'
];
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
if (!$model->codice_movimento) {
$model->codice_movimento = static::generateCodiceMovimento();
}
if (!$model->progressivo_anno) {
$model->progressivo_anno = static::getNextProgressivo($model->stabile_id);
}
});
}
public static function generateCodiceMovimento(): string
{
do {
$codice = 'MOV' . sprintf('%09d', rand(100000000, 999999999));
} while (static::where('codice_movimento', $codice)->exists());
return $codice;
}
public static function getNextProgressivo($stabile_id): int
{
$anno = date('Y');
$ultimo = static::where('stabile_id', $stabile_id)
->whereYear('data_registrazione', $anno)
->max('progressivo_anno');
return ($ultimo ?? 0) + 1;
}
// Relazioni
public function stabile(): BelongsTo
{
return $this->belongsTo(Stabile::class);
}
public function gestioneContabile(): BelongsTo
{
return $this->belongsTo(GestioneContabile::class, 'gestione_contabile_id');
}
public function esercizioContabile(): BelongsTo
{
return $this->belongsTo(EsercizioContabile::class, 'esercizio_contabile_id');
}
public function fornitore(): BelongsTo
{
return $this->belongsTo(Fornitore::class);
}
public function righeContabili(): HasMany
{
return $this->hasMany(RigaContabile::class, 'movimento_id');
}
public function creatoBy(): BelongsTo
{
return $this->belongsTo(User::class, 'creato_da');
}
public function verificatoBy(): BelongsTo
{
return $this->belongsTo(User::class, 'verificato_da');
}
public function confermatoBy(): BelongsTo
{
return $this->belongsTo(User::class, 'confermato_da');
}
// Scopes
public function scopeConfermati($query)
{
return $query->where('stato_movimento', 'confermato');
}
public function scopeByGestione($query, $gestione_id)
{
return $query->where('gestione_contabile_id', $gestione_id);
}
public function scopeByPeriodo($query, $data_inizio, $data_fine)
{
return $query->whereBetween('data_movimento', [$data_inizio, $data_fine]);
}
// Business Logic
public function verificaQuadratura(): bool
{
$totaleDare = $this->righeContabili()->where('dare_avere', 'dare')->sum('importo');
$totaleAvere = $this->righeContabili()->where('dare_avere', 'avere')->sum('importo');
return abs($totaleDare - $totaleAvere) < 0.01;
}
public function confermaMovimento($user_id): bool
{
if (!$this->verificaQuadratura()) {
return false;
}
$this->update([
'stato_movimento' => 'confermato',
'confermato_da' => $user_id,
'data_conferma' => now(),
]);
return true;
}
public function creaRigheStandard($conto_dare, $conto_avere): void
{
// Riga in DARE
$this->righeContabili()->create([
'codice_conto' => $conto_dare,
'descrizione_riga' => $this->descrizione,
'dare_avere' => 'dare',
'importo' => $this->importo_netto,
]);
// Riga in AVERE
$this->righeContabili()->create([
'codice_conto' => $conto_avere,
'descrizione_riga' => $this->descrizione,
'dare_avere' => 'avere',
'importo' => $this->importo_netto,
]);
}
}
File: app/Models/RigaContabile.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class RigaContabile extends Model
{
use HasFactory;
protected $table = 'righe_contabili';
protected $fillable = [
'movimento_id', 'codice_conto', 'descrizione_riga',
'dare_avere', 'importo', 'unita_immobiliare_id',
'quota_millesimale', 'note_riga'
];
protected $casts = [
'importo' => 'decimal:2',
'quota_millesimale' => 'decimal:4'
];
// Relazioni
public function movimento(): BelongsTo
{
return $this->belongsTo(MovimentoPartitaDoppia::class, 'movimento_id');
}
public function pianoConti(): BelongsTo
{
return $this->belongsTo(PianoContiMasterplan::class, 'codice_conto', 'codice_conto');
}
public function unitaImmobiliare(): BelongsTo
{
return $this->belongsTo(UnitaImmobiliare::class, 'unita_immobiliare_id');
}
// Scopes
public function scopeDare($query)
{
return $query->where('dare_avere', 'dare');
}
public function scopeAvere($query)
{
return $query->where('dare_avere', 'avere');
}
public function scopeByConto($query, $codice_conto)
{
return $query->where('codice_conto', $codice_conto);
}
}
File: app/Models/PianoContiMasterplan.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class PianoContiMasterplan extends Model
{
use HasFactory;
protected $table = 'piano_conti_masterplan';
protected $fillable = [
'codice_conto', 'descrizione_conto', 'tipologia_conto',
'categoria_contabile', 'ripartibile', 'default_ripartizioni', 'attivo'
];
protected $casts = [
'ripartibile' => 'boolean',
'attivo' => 'boolean',
'default_ripartizioni' => 'json'
];
// Relazioni
public function righeContabili(): HasMany
{
return $this->hasMany(RigaContabile::class, 'codice_conto', 'codice_conto');
}
// Scopes
public function scopeAttivi($query)
{
return $query->where('attivo', true);
}
public function scopeByTipologia($query, $tipologia)
{
return $query->where('tipologia_conto', $tipologia);
}
public function scopeByCategoria($query, $categoria)
{
return $query->where('categoria_contabile', $categoria);
}
public function scopeRipartibili($query)
{
return $query->where('ripartibile', true);
}
// Metodi helper
public static function getContiByCategoria($categoria)
{
return static::attivi()->byCategoria($categoria)->get();
}
public static function getContiCosti()
{
return static::attivi()->byTipologia('costo')->get();
}
public static function getContiRicavi()
{
return static::attivi()->byTipologia('ricavo')->get();
}
public static function getContiPatrimoniali()
{
return static::attivi()->whereIn('tipologia_conto', ['attivo', 'passivo', 'patrimoniale'])->get();
}
}
🎛️ CONTROLLER DA CREARE
File: app/Http/Controllers/Admin/ContabilitaAvanzataController.php
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\GestioneContabile;
use App\Models\MovimentoPartitaDoppia;
use App\Models\RigaContabile;
use App\Models\PianoContiMasterplan;
use App\Models\RataCondominiale;
use App\Models\EsercizioContabile;
use App\Models\Stabile;
use App\Models\Fornitore;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Carbon\Carbon;
class ContabilitaAvanzataController extends Controller
{
public function dashboard()
{
$amministratore_id = Auth::user()->amministratore->id_amministratore ?? null;
$stats = $this->calcolaStatisticheDashboard($amministratore_id);
$ultimiMovimenti = MovimentoPartitaDoppia::with([
'stabile', 'gestioneContabile', 'fornitore', 'righeContabili.pianoConti'
])
->whereHas('stabile', function($q) use ($amministratore_id) {
$q->where('amministratore_id', $amministratore_id);
})
->orderBy('data_registrazione', 'desc')
->limit(10)
->get();
$gestioniAttive = GestioneContabile::with(['stabile', 'esercizioContabile'])
->whereHas('stabile', function($q) use ($amministratore_id) {
$q->where('amministratore_id', $amministratore_id);
})
->where('stato', 'attiva')
->get();
return view('admin.contabilita.dashboard', compact(
'stats', 'ultimiMovimenti', 'gestioniAttive'
));
}
public function movimenti(Request $request)
{
$amministratore_id = Auth::user()->amministratore->id_amministratore ?? null;
$query = MovimentoPartitaDoppia::with([
'stabile', 'gestioneContabile', 'fornitore', 'righeContabili.pianoConti'
])
->whereHas('stabile', function($q) use ($amministratore_id) {
$q->where('amministratore_id', $amministratore_id);
});
// Filtri
if ($request->stabile_id) {
$query->where('stabile_id', $request->stabile_id);
}
if ($request->gestione_id) {
$query->where('gestione_contabile_id', $request->gestione_id);
}
if ($request->stato) {
$query->where('stato_movimento', $request->stato);
}
if ($request->data_da && $request->data_a) {
$query->whereBetween('data_movimento', [$request->data_da, $request->data_a]);
}
$movimenti = $query->orderBy('data_registrazione', 'desc')->paginate(25);
$stabili = Stabile::where('amministratore_id', $amministratore_id)->get();
$gestioni = GestioneContabile::whereIn('stabile_id', $stabili->pluck('id'))->get();
return view('admin.contabilita.movimenti.index', compact('movimenti', 'stabili', 'gestioni'));
}
public function creaMovimento()
{
$amministratore_id = Auth::user()->amministratore->id_amministratore ?? null;
$stabili = Stabile::where('amministratore_id', $amministratore_id)->get();
$fornitori = Fornitore::where('amministratore_id', $amministratore_id)->get();
$pianoConti = PianoContiMasterplan::attivi()->get();
return view('admin.contabilita.movimenti.create', compact('stabili', 'fornitori', 'pianoConti'));
}
public function salvaMovimento(Request $request)
{
$validator = Validator::make($request->all(), [
'stabile_id' => 'required|exists:stabili,id',
'gestione_contabile_id' => 'required|exists:gestioni_contabili,id',
'data_movimento' => 'required|date',
'descrizione' => 'required|string|max:255',
'importo_lordo' => 'required|numeric|min:0.01',
'importo_netto' => 'required|numeric|min:0.01',
'righe' => 'required|array|min:2',
'righe.*.codice_conto' => 'required|exists:piano_conti_masterplan,codice_conto',
'righe.*.dare_avere' => 'required|in:dare,avere',
'righe.*.importo' => 'required|numeric|min:0.01',
'righe.*.descrizione_riga' => 'required|string|max:255',
]);
if ($validator->fails()) {
return response()->json(['errors' => $validator->errors()], 422);
}
DB::beginTransaction();
try {
// Verifica quadratura dare/avere
$totaleDare = collect($request->righe)->where('dare_avere', 'dare')->sum('importo');
$totaleAvere = collect($request->righe)->where('dare_avere', 'avere')->sum('importo');
if (abs($totaleDare - $totaleAvere) > 0.01) {
return response()->json([
'error' => 'Le righe contabili non sono in quadratura. Dare: ' . $totaleDare . ', Avere: ' . $totaleAvere
], 422);
}
// Crea movimento
$movimento = MovimentoPartitaDoppia::create([
'stabile_id' => $request->stabile_id,
'gestione_contabile_id' => $request->gestione_contabile_id,
'esercizio_contabile_id' => $this->getEsercizioAttivo($request->stabile_id),
'data_movimento' => $request->data_movimento,
'descrizione' => $request->descrizione,
'causale_dettagliata' => $request->causale_dettagliata,
'note_interne' => $request->note_interne,
'tipo_documento' => $request->tipo_documento,
'numero_documento' => $request->numero_documento,
'data_documento' => $request->data_documento,
'fornitore_id' => $request->fornitore_id,
'importo_lordo' => $request->importo_lordo,
'importo_iva' => $request->importo_iva ?? 0,
'importo_ritenute' => $request->importo_ritenute ?? 0,
'importo_netto' => $request->importo_netto,
'creato_da' => Auth::id(),
'stato_movimento' => 'bozza',
]);
// Crea righe contabili
foreach ($request->righe as $riga) {
RigaContabile::create([
'movimento_id' => $movimento->id,
'codice_conto' => $riga['codice_conto'],
'descrizione_riga' => $riga['descrizione_riga'],
'dare_avere' => $riga['dare_avere'],
'importo' => $riga['importo'],
'note_riga' => $riga['note_riga'] ?? null,
]);
}
DB::commit();
return response()->json([
'success' => true,
'message' => 'Movimento contabile creato con successo',
'movimento_id' => $movimento->id
]);
} catch (\Exception $e) {
DB::rollback();
return response()->json(['error' => 'Errore nel salvataggio: ' . $e->getMessage()], 500);
}
}
public function confermaMovimento($id)
{
$movimento = MovimentoPartitaDoppia::findOrFail($id);
if (!$movimento->verificaQuadratura()) {
return response()->json(['error' => 'Il movimento non è in quadratura'], 422);
}
if ($movimento->confermaMovimento(Auth::id())) {
return response()->json(['success' => true, 'message' => 'Movimento confermato']);
}
return response()->json(['error' => 'Errore nella conferma'], 500);
}
private function calcolaStatisticheDashboard($amministratore_id)
{
$stabiliIds = Stabile::where('amministratore_id', $amministratore_id)->pluck('id');
$meseCorrente = Carbon::now()->startOfMonth();
return [
'movimenti_mese' => MovimentoPartitaDoppia::whereIn('stabile_id', $stabiliIds)
->where('data_registrazione', '>=', $meseCorrente)
->count(),
'entrate_mese' => MovimentoPartitaDoppia::whereIn('stabile_id', $stabiliIds)
->where('data_registrazione', '>=', $meseCorrente)
->whereHas('righeContabili', function($q) {
$q->where('dare_avere', 'avere')
->whereHas('pianoConti', function($sq) {
$sq->where('tipologia_conto', 'ricavo');
});
})
->sum('importo_netto'),
'uscite_mese' => MovimentoPartitaDoppia::whereIn('stabile_id', $stabiliIds)
->where('data_registrazione', '>=', $meseCorrente)
->whereHas('righeContabili', function($q) {
$q->where('dare_avere', 'dare')
->whereHas('pianoConti', function($sq) {
$sq->where('tipologia_conto', 'costo');
});
})
->sum('importo_netto'),
'saldo_gestioni' => GestioneContabile::whereIn('stabile_id', $stabiliIds)
->where('stato', 'attiva')
->get()
->sum(function($gestione) {
return $gestione->calcolaSaldoContabile();
}),
];
}
private function getEsercizioAttivo($stabile_id)
{
$esercizio = EsercizioContabile::where('stabile_id', $stabile_id)
->where('stato', 'aperto')
->where('tipologia', 'ordinaria')
->first();
return $esercizio ? $esercizio->id : null;
}
public function getGestioniByStabile($stabile_id)
{
$gestioni = GestioneContabile::where('stabile_id', $stabile_id)
->where('stato', 'attiva')
->with('esercizioContabile')
->get();
return response()->json($gestioni);
}
public function verificaQuadratura(Request $request)
{
$righe = $request->righe ?? [];
$totaleDare = collect($righe)->where('dare_avere', 'dare')->sum('importo');
$totaleAvere = collect($righe)->where('dare_avere', 'avere')->sum('importo');
$differenza = abs($totaleDare - $totaleAvere);
return response()->json([
'in_quadratura' => $differenza < 0.01,
'totale_dare' => $totaleDare,
'totale_avere' => $totaleAvere,
'differenza' => $differenza,
]);
}
}
🌱 SEEDER DA CREARE
File: database/seeders/PianoContiSeeder.php
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\PianoContiMasterplan;
class PianoContiSeeder extends Seeder
{
public function run(): void
{
$conti = [
// CONTI PATRIMONIALI - ATTIVO
[
'codice_conto' => '1001',
'descrizione_conto' => 'Cassa',
'tipologia_conto' => 'attivo',
'categoria_contabile' => 'liquidita',
'ripartibile' => false,
],
[
'codice_conto' => '1002',
'descrizione_conto' => 'Banca c/c ordinario',
'tipologia_conto' => 'attivo',
'categoria_contabile' => 'liquidita',
'ripartibile' => false,
],
[
'codice_conto' => '1201',
'descrizione_conto' => 'Crediti vs condòmini per rate',
'tipologia_conto' => 'attivo',
'categoria_contabile' => 'crediti',
'ripartibile' => false,
],
// CONTI PATRIMONIALI - PASSIVO
[
'codice_conto' => '2001',
'descrizione_conto' => 'Debiti vs fornitori',
'tipologia_conto' => 'passivo',
'categoria_contabile' => 'debiti',
'ripartibile' => false,
],
[
'codice_conto' => '2101',
'descrizione_conto' => 'Fondo di riserva',
'tipologia_conto' => 'passivo',
'categoria_contabile' => 'fondi',
'ripartibile' => false,
],
// CONTI ECONOMICI - RICAVI
[
'codice_conto' => '5001',
'descrizione_conto' => 'Quote ordinarie',
'tipologia_conto' => 'ricavo',
'categoria_contabile' => 'quote_condominiali',
'ripartibile' => false,
'default_ripartizioni' => json_encode(['generale' => 100]),
],
[
'codice_conto' => '5002',
'descrizione_conto' => 'Quote straordinarie',
'tipologia_conto' => 'ricavo',
'categoria_contabile' => 'quote_condominiali',
'ripartibile' => false,
'default_ripartizioni' => json_encode(['generale' => 100]),
],
// CONTI ECONOMICI - COSTI AMMINISTRAZIONE
[
'codice_conto' => '6001',
'descrizione_conto' => 'Compenso amministratore',
'tipologia_conto' => 'costo',
'categoria_contabile' => 'amministrazione',
'ripartibile' => true,
'default_ripartizioni' => json_encode(['generale' => 100]),
],
[
'codice_conto' => '6002',
'descrizione_conto' => 'Spese postali e telefoniche',
'tipologia_conto' => 'costo',
'categoria_contabile' => 'amministrazione',
'ripartibile' => true,
'default_ripartizioni' => json_encode(['generale' => 100]),
],
// CONTI ECONOMICI - PULIZIA E IGIENE
[
'codice_conto' => '6101',
'descrizione_conto' => 'Pulizia scale e parti comuni',
'tipologia_conto' => 'costo',
'categoria_contabile' => 'pulizia',
'ripartibile' => true,
'default_ripartizioni' => json_encode(['scale' => 100]),
],
[
'codice_conto' => '6102',
'descrizione_conto' => 'Materiali di pulizia',
'tipologia_conto' => 'costo',
'categoria_contabile' => 'pulizia',
'ripartibile' => true,
'default_ripartizioni' => json_encode(['scale' => 100]),
],
// CONTI ECONOMICI - MANUTENZIONE
[
'codice_conto' => '6201',
'descrizione_conto' => 'Manutenzione ordinaria ascensore',
'tipologia_conto' => 'costo',
'categoria_contabile' => 'manutenzione',
'ripartibile' => true,
'default_ripartizioni' => json_encode(['ascensore' => 100]),
],
[
'codice_conto' => '6202',
'descrizione_conto' => 'Manutenzione impianto elettrico',
'tipologia_conto' => 'costo',
'categoria_contabile' => 'manutenzione',
'ripartibile' => true,
'default_ripartizioni' => json_encode(['generale' => 100]),
],
// CONTI ECONOMICI - UTENZE
[
'codice_conto' => '6301',
'descrizione_conto' => 'Energia elettrica',
'tipologia_conto' => 'costo',
'categoria_contabile' => 'utenze',
'ripartibile' => true,
'default_ripartizioni' => json_encode(['generale' => 100]),
],
[
'codice_conto' => '6302',
'descrizione_conto' => 'Gas',
'tipologia_conto' => 'costo',
'categoria_contabile' => 'utenze',
'ripartibile' => true,
'default_ripartizioni' => json_encode(['riscaldamento' => 100]),
],
[
'codice_conto' => '6303',
'descrizione_conto' => 'Acqua',
'tipologia_conto' => 'costo',
'categoria_contabile' => 'utenze',
'ripartibile' => true,
'default_ripartizioni' => json_encode(['generale' => 100]),
],
// CONTI ECONOMICI - ASSICURAZIONI
[
'codice_conto' => '6501',
'descrizione_conto' => 'Assicurazione globale fabbricati',
'tipologia_conto' => 'costo',
'categoria_contabile' => 'assicurazioni',
'ripartibile' => true,
'default_ripartizioni' => json_encode(['generale' => 100]),
],
];
foreach ($conti as $conto) {
PianoContiMasterplan::updateOrCreate(
['codice_conto' => $conto['codice_conto']],
$conto
);
}
$this->command->info('Piano dei conti popolato con ' . count($conti) . ' voci');
}
}
🛤️ ROUTES DA AGGIUNGERE
File: routes/admin.php (aggiungere alla fine)
// === CONTABILITÀ AVANZATA ===
Route::prefix('contabilita-avanzata')->name('contabilita_avanzata.')->group(function () {
Route::get('/', [ContabilitaAvanzataController::class, 'dashboard'])->name('dashboard');
// Movimenti
Route::get('/movimenti', [ContabilitaAvanzataController::class, 'movimenti'])->name('movimenti.index');
Route::get('/movimenti/create', [ContabilitaAvanzataController::class, 'creaMovimento'])->name('movimenti.create');
Route::post('/movimenti', [ContabilitaAvanzataController::class, 'salvaMovimento'])->name('movimenti.store');
Route::post('/movimenti/{id}/conferma', [ContabilitaAvanzataController::class, 'confermaMovimento'])->name('movimenti.conferma');
// API
Route::get('/api/gestioni/{stabile_id}', [ContabilitaAvanzataController::class, 'getGestioniByStabile'])->name('api.gestioni');
Route::post('/api/verifica-quadratura', [ContabilitaAvanzataController::class, 'verificaQuadratura'])->name('api.quadratura');
});
📜 COMANDI ARTISAN DA ESEGUIRE
# 1. Eseguire la migrazione
php artisan migrate --path=database/migrations/2025_07_23_100000_create_sistema_contabile_completo.php
# 2. Eseguire il seeder
php artisan db:seed --class=PianoContiSeeder
# 3. Clearing cache
php artisan config:clear
php artisan cache:clear
php artisan route:clear
# 4. Ottimizzazioni
php artisan config:cache
php artisan route:cache
🎯 COSA PREFERISCI?
OPZIONE A: Creo l'utente Gitea per me così gestisco tutto direttamente ✅ CONSIGLIATO
OPZIONE B: Tu segui queste specifiche manualmente step-by-step
Fammi sapere quale opzione preferisci! Se scegli l'OPZIONE A ti guido nella creazione dell'utente Gitea, se scegli l'OPZIONE B possiamo procedere step-by-step con l'implementazione manuale.
La partita doppia è già pronta con quadrature automatiche! 💎