# 📋 SPECIFICHE COMPLETE SISTEMA CONTABILE NETGESCON ## 🗃️ **MIGRAZIONI DATABASE** ### **File: `database/migrations/2025_07_23_100000_create_sistema_contabile_completo.php`** ```php 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 '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 '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 '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 '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 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 '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)** ```php // === 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** ```bash # 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! 💎