diff --git a/app/Models/AnagraficaCondominiale.php b/app/Models/AnagraficaCondominiale.php index 40aca63c..d6be125f 100644 --- a/app/Models/AnagraficaCondominiale.php +++ b/app/Models/AnagraficaCondominiale.php @@ -100,6 +100,22 @@ class AnagraficaCondominiale extends Model return $this->hasMany(ContrattoLocazione::class); } + /** + * Relazione con DettaglioRipartizioneSpese + */ + public function dettagliRipartizioneSpese() + { + return $this->hasMany(DettaglioRipartizioneSpese::class); + } + + /** + * Relazione con Rate + */ + public function rate() + { + return $this->hasMany(Rata::class); + } + /** * Scope per anagrafica attiva */ @@ -270,4 +286,39 @@ class AnagraficaCondominiale extends Model { return $this->belongsTo(Amministratore::class); } + + /** + * Metodo per ottenere il bilancio delle spese per questa anagrafica + */ + public function getBilancioSpese($annoGestione = null) + { + $anno = $annoGestione ?? date('Y'); + + $dettagliRipartizione = $this->dettagliRipartizioneSpese() + ->whereHas('ripartizioneSpese', function($q) use ($anno) { + $q->whereYear('data_ripartizione', $anno) + ->where('stato', 'confermata'); + }) + ->with('ripartizioneSpese.voceSpesa') + ->get(); + + return [ + 'totale_spese' => $dettagliRipartizione->sum('quota_finale'), + 'totale_proprietario' => $dettagliRipartizione->sum('importo_proprietario'), + 'totale_inquilino' => $dettagliRipartizione->sum('importo_inquilino'), + 'numero_voci' => $dettagliRipartizione->count(), + 'dettagli' => $dettagliRipartizione + ]; + } + + /** + * Metodo per ottenere le rate in scadenza + */ + public function getRateInScadenza($giorni = 30) + { + return $this->rate() + ->inScadenza($giorni) + ->with(['pianoRateizzazione', 'unitaImmobiliare']) + ->get(); + } } diff --git a/app/Models/DettaglioRipartizioneSpese.php b/app/Models/DettaglioRipartizioneSpese.php new file mode 100644 index 00000000..e5f13adf --- /dev/null +++ b/app/Models/DettaglioRipartizioneSpese.php @@ -0,0 +1,227 @@ + 'decimal:3', + 'percentuale_applicata' => 'decimal:2', + 'quota_calcolata' => 'decimal:2', + 'quota_personalizzata' => 'decimal:2', + 'quota_finale' => 'decimal:2', + 'percentuale_inquilino' => 'decimal:2', + 'importo_inquilino' => 'decimal:2', + 'importo_proprietario' => 'decimal:2', + 'esente' => 'boolean' + ]; + + /** + * Relazione con RipartizioneSpese + */ + public function ripartizioneSpese() + { + return $this->belongsTo(RipartizioneSpese::class); + } + + /** + * Relazione con UnitaImmobiliare + */ + public function unitaImmobiliare() + { + return $this->belongsTo(UnitaImmobiliare::class); + } + + /** + * Relazione con AnagraficaCondominiale + */ + public function anagraficaCondominiale() + { + return $this->belongsTo(AnagraficaCondominiale::class); + } + + /** + * Scope per dettagli non esenti + */ + public function scopeNonEsenti($query) + { + return $query->where('esente', false); + } + + /** + * Scope per dettagli esenti + */ + public function scopeEsenti($query) + { + return $query->where('esente', true); + } + + /** + * Scope per tipo soggetto + */ + public function scopePerTipoSoggetto($query, $tipo) + { + return $query->where('tipo_soggetto', $tipo); + } + + /** + * Metodo per calcolare la quota finale + */ + public function calcolaQuotaFinale() + { + if ($this->esente) { + $this->quota_finale = 0; + } else { + $this->quota_finale = $this->quota_personalizzata ?? $this->quota_calcolata; + } + + $this->save(); + return $this; + } + + /** + * Metodo per applicare esenzione + */ + public function applicaEsenzione($motivo = null) + { + $this->update([ + 'esente' => true, + 'motivo_esenzione' => $motivo, + 'quota_finale' => 0, + 'importo_inquilino' => 0, + 'importo_proprietario' => 0 + ]); + + return $this; + } + + /** + * Metodo per rimuovere esenzione + */ + public function rimuoviEsenzione() + { + $this->update([ + 'esente' => false, + 'motivo_esenzione' => null + ]); + + $this->calcolaQuotaFinale(); + return $this; + } + + /** + * Metodo per personalizzare la quota + */ + public function personalizzaQuota($nuovaQuota, $motivo = null) + { + $this->update([ + 'quota_personalizzata' => $nuovaQuota, + 'note_dettaglio' => $motivo + ]); + + $this->calcolaQuotaFinale(); + return $this; + } + + /** + * Metodo per calcolare ripartizione inquilino/proprietario + */ + public function calcolaRipartizioneInquilinoProprietario() + { + if ($this->percentuale_inquilino > 0) { + $this->importo_inquilino = $this->quota_finale * ($this->percentuale_inquilino / 100); + $this->importo_proprietario = $this->quota_finale - $this->importo_inquilino; + } else { + $this->importo_inquilino = 0; + $this->importo_proprietario = $this->quota_finale; + } + + $this->save(); + return $this; + } + + /** + * Verifica se la quota è stata personalizzata + */ + public function isPersonalizzata() + { + return !is_null($this->quota_personalizzata); + } + + /** + * Ottieni la differenza tra quota calcolata e personalizzata + */ + public function getDifferenzaQuote() + { + if (!$this->isPersonalizzata()) { + return 0; + } + + return $this->quota_personalizzata - $this->quota_calcolata; + } + + /** + * Accessor per tipo soggetto formattato + */ + public function getTipoSoggettoFormattatoAttribute() + { + return match ($this->tipo_soggetto) { + 'proprietario' => 'Proprietario', + 'inquilino' => 'Inquilino', + 'usufruttuario' => 'Usufruttuario', + 'altro' => 'Altro', + default => ucfirst($this->tipo_soggetto) + }; + } + + /** + * Accessor per percentuale millesimi + */ + public function getPercentualeMillesimiAttribute() + { + if ($this->millesimi_applicati) { + return ($this->millesimi_applicati / 1000) * 100; + } + return 0; + } + + /** + * Accessor per stato esenzione formattato + */ + public function getStatoEsenzioneAttribute() + { + return $this->esente ? 'Esente' : 'Non Esente'; + } +} diff --git a/app/Models/PianoRateizzazione.php b/app/Models/PianoRateizzazione.php new file mode 100644 index 00000000..302a9388 --- /dev/null +++ b/app/Models/PianoRateizzazione.php @@ -0,0 +1,397 @@ + 'decimal:2', + 'numero_rate' => 'integer', + 'data_prima_rata' => 'date', + 'configurazione_rate' => 'array', + 'attivato_at' => 'datetime' + ]; + + protected $dates = [ + 'data_prima_rata', + 'attivato_at', + 'created_at', + 'updated_at', + 'deleted_at' + ]; + + /** + * Boot method per generare automaticamente il codice piano + */ + protected static function boot() + { + parent::boot(); + + static::creating(function ($model) { + if (empty($model->codice_piano)) { + $model->codice_piano = self::generaCodicePiano(); + } + }); + } + + /** + * Genera un codice univoco per il piano + */ + public static function generaCodicePiano() + { + do { + $codice = 'PR' . strtoupper(Str::random(6)); + } while (self::where('codice_piano', $codice)->exists()); + + return $codice; + } + + /** + * Relazione con Stabile + */ + public function stabile() + { + return $this->belongsTo(Stabile::class); + } + + /** + * Relazione con User (creato da) + */ + public function creatoDa() + { + return $this->belongsTo(User::class, 'creato_da'); + } + + /** + * Relazione con User (attivato da) + */ + public function attivatoDa() + { + return $this->belongsTo(User::class, 'attivato_da'); + } + + /** + * Relazione con Rate + */ + public function rate() + { + return $this->hasMany(Rata::class, 'piano_rateizzazione_id'); + } + + /** + * Scope per piani per stabile + */ + public function scopePerStabile($query, $stabileId) + { + return $query->where('stabile_id', $stabileId); + } + + /** + * Scope per stato + */ + public function scopeConStato($query, $stato) + { + return $query->where('stato', $stato); + } + + /** + * Scope per tipo piano + */ + public function scopePerTipo($query, $tipo) + { + return $query->where('tipo_piano', $tipo); + } + + /** + * Scope per frequenza + */ + public function scopePerFrequenza($query, $frequenza) + { + return $query->where('frequenza', $frequenza); + } + + /** + * Metodo per attivare il piano + */ + public function attiva($userId = null) + { + $this->update([ + 'stato' => 'attivo', + 'attivato_at' => now(), + 'attivato_da' => $userId ?? auth()->id() + ]); + + // Genera le rate se non esistono + if ($this->rate()->count() == 0) { + $this->generaRate(); + } + + return $this; + } + + /** + * Metodo per sospendere il piano + */ + public function sospendi() + { + $this->update(['stato' => 'sospeso']); + return $this; + } + + /** + * Metodo per annullare il piano + */ + public function annulla() + { + $this->update(['stato' => 'annullato']); + + // Annulla tutte le rate non pagate + $this->rate()->whereIn('stato', ['emessa', 'scaduta'])->update(['stato' => 'annullata']); + + return $this; + } + + /** + * Metodo per completare il piano + */ + public function completa() + { + $this->update(['stato' => 'completato']); + return $this; + } + + /** + * Verifica se può essere modificato + */ + public function puoEssereModificato() + { + return in_array($this->stato, ['bozza']); + } + + /** + * Verifica se può essere eliminato + */ + public function puoEssereEliminato() + { + return in_array($this->stato, ['bozza', 'annullato']); + } + + /** + * Genera le rate del piano + */ + public function generaRate() + { + if ($this->numero_rate <= 0) { + throw new \Exception('Numero rate deve essere maggiore di 0'); + } + + // Elimina le rate esistenti se ci sono + $this->rate()->delete(); + + $unitaImmobiliari = $this->stabile->unitaImmobiliari; + $importoPerRata = $this->importo_totale / $this->numero_rate; + + for ($numeroRata = 1; $numeroRata <= $this->numero_rate; $numeroRata++) { + $dataScadenza = $this->calcolaDataScadenzaRata($numeroRata); + + foreach ($unitaImmobiliari as $unita) { + $this->rate()->create([ + 'codice_rata' => Rata::generaCodiceRata(), + 'unita_immobiliare_id' => $unita->id, + 'anagrafica_condominiale_id' => $unita->anagrafiche()->where('tipo_diritto', 'proprietario')->first()?->id, + 'numero_rata' => $numeroRata, + 'descrizione' => "{$this->descrizione} - Rata {$numeroRata}/{$this->numero_rate}", + 'importo_rata' => $importoPerRata, + 'data_scadenza' => $dataScadenza, + 'importo_residuo' => $importoPerRata, + 'stato' => 'emessa' + ]); + } + } + + return $this; + } + + /** + * Calcola la data di scadenza per una rata specifica + */ + public function calcolaDataScadenzaRata($numeroRata) + { + $dataBase = Carbon::parse($this->data_prima_rata); + + $mesiDaAggiungere = match ($this->frequenza) { + 'mensile' => ($numeroRata - 1) * 1, + 'bimestrale' => ($numeroRata - 1) * 2, + 'trimestrale' => ($numeroRata - 1) * 3, + 'semestrale' => ($numeroRata - 1) * 6, + 'annuale' => ($numeroRata - 1) * 12, + 'personalizzata' => $this->calcolaIntervalloPersonalizzato($numeroRata), + default => ($numeroRata - 1) * 1 + }; + + return $dataBase->addMonths($mesiDaAggiungere); + } + + /** + * Calcola intervallo personalizzato (da implementare in base alla configurazione) + */ + private function calcolaIntervalloPersonalizzato($numeroRata) + { + $config = $this->configurazione_rate; + + if (isset($config['intervalli']) && is_array($config['intervalli'])) { + return $config['intervalli'][$numeroRata - 1] ?? 0; + } + + return ($numeroRata - 1) * 1; // Default mensile + } + + /** + * Ottieni statistiche del piano + */ + public function getStatistiche() + { + $rateQuery = $this->rate(); + + return [ + 'totale_rate' => $rateQuery->count(), + 'rate_pagate' => $rateQuery->where('stato', 'pagata')->count(), + 'rate_scadute' => $rateQuery->where('stato', 'scaduta')->count(), + 'importo_pagato' => $rateQuery->sum('importo_pagato'), + 'importo_residuo' => $rateQuery->sum('importo_residuo'), + 'percentuale_completamento' => $this->getPercentualeCompletamento(), + 'prossima_scadenza' => $rateQuery->where('stato', 'emessa')->min('data_scadenza') + ]; + } + + /** + * Calcola percentuale di completamento + */ + public function getPercentualeCompletamento() + { + if ($this->importo_totale == 0) { + return 0; + } + + $importoPagato = $this->rate()->sum('importo_pagato'); + return round(($importoPagato / $this->importo_totale) * 100, 2); + } + + /** + * Verifica se il piano è completato + */ + public function isCompletato() + { + return $this->rate()->where('stato', '!=', 'pagata')->count() == 0; + } + + /** + * Verifica se ci sono rate scadute + */ + public function hasRateScadute() + { + return $this->rate() + ->where('stato', 'emessa') + ->where('data_scadenza', '<', now()->toDateString()) + ->exists(); + } + + /** + * Aggiorna automaticamente lo stato del piano + */ + public function aggiornaStato() + { + if ($this->stato === 'attivo') { + if ($this->isCompletato()) { + $this->completa(); + } elseif ($this->hasRateScadute()) { + // Aggiorna le rate scadute + $this->rate() + ->where('stato', 'emessa') + ->where('data_scadenza', '<', now()->toDateString()) + ->update(['stato' => 'scaduta']); + } + } + + return $this; + } + + /** + * Accessor per tipo piano formattato + */ + public function getTipoPianoFormattatoAttribute() + { + return match ($this->tipo_piano) { + 'ordinario' => 'Piano Ordinario', + 'straordinario' => 'Piano Straordinario', + 'conguaglio' => 'Piano Conguaglio', + 'personalizzato' => 'Piano Personalizzato', + default => ucfirst($this->tipo_piano) + }; + } + + /** + * Accessor per frequenza formattata + */ + public function getFrequenzaFormattataAttribute() + { + return match ($this->frequenza) { + 'mensile' => 'Mensile', + 'bimestrale' => 'Bimestrale', + 'trimestrale' => 'Trimestrale', + 'semestrale' => 'Semestrale', + 'annuale' => 'Annuale', + 'personalizzata' => 'Personalizzata', + default => ucfirst($this->frequenza) + }; + } + + /** + * Accessor per stato formattato + */ + public function getStatoFormattatoAttribute() + { + return match ($this->stato) { + 'bozza' => 'Bozza', + 'attivo' => 'Attivo', + 'completato' => 'Completato', + 'sospeso' => 'Sospeso', + 'annullato' => 'Annullato', + default => ucfirst($this->stato) + }; + } +} diff --git a/app/Models/Rata.php b/app/Models/Rata.php index 06cb6b5f..4038592d 100644 --- a/app/Models/Rata.php +++ b/app/Models/Rata.php @@ -4,40 +4,134 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Str; +/** + * Class Rata + * + * Gestisce le rate singole dei piani di rateizzazione + * + * @package App\Models + */ class Rata extends Model { - use HasFactory; + use HasFactory, SoftDeletes; protected $table = 'rate'; protected $fillable = [ - 'gestione_id', + 'codice_rata', + 'piano_rateizzazione_id', 'unita_immobiliare_id', - 'soggetto_id', + 'anagrafica_condominiale_id', 'numero_rata', 'descrizione', - 'importo', + 'importo_rata', 'data_scadenza', - 'data_pagamento', - 'importo_pagato', 'stato', + 'importo_pagato', + 'importo_residuo', + 'data_pagamento', + 'note_pagamento', + 'dettagli_pagamento', + 'metodo_pagamento', + 'riferimento_pagamento', + 'registrato_da', + // Compatibilità con vecchia struttura + 'gestione_id', + 'soggetto_id', + 'importo', 'tipo_rata', - 'note', + 'note' ]; protected $casts = [ - 'importo' => 'decimal:2', + 'importo_rata' => 'decimal:2', 'importo_pagato' => 'decimal:2', + 'importo_residuo' => 'decimal:2', 'data_scadenza' => 'date', 'data_pagamento' => 'date', + 'dettagli_pagamento' => 'array', 'numero_rata' => 'integer', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + // Compatibilità + 'importo' => 'decimal:2' + ]; + + protected $dates = [ + 'data_scadenza', + 'data_pagamento', + 'created_at', + 'updated_at', + 'deleted_at' ]; /** - * Relazione con Gestione + * Boot method per generare automaticamente il codice rata + */ + protected static function boot() + { + parent::boot(); + + static::creating(function ($model) { + if (empty($model->codice_rata)) { + $model->codice_rata = self::generaCodiceRata(); + } + + // Inizializza importo_residuo se non impostato + if (is_null($model->importo_residuo)) { + $model->importo_residuo = $model->importo_rata ?? $model->importo ?? 0; + } + }); + } + + /** + * Genera un codice univoco per la rata + */ + public static function generaCodiceRata() + { + do { + $codice = 'RT' . strtoupper(Str::random(6)); + } while (self::where('codice_rata', $codice)->exists()); + + return $codice; + } + + /** + * Relazione con PianoRateizzazione + */ + public function pianoRateizzazione() + { + return $this->belongsTo(PianoRateizzazione::class); + } + + /** + * Relazione con UnitaImmobiliare + */ + public function unitaImmobiliare() + { + return $this->belongsTo(UnitaImmobiliare::class); + } + + /** + * Relazione con AnagraficaCondominiale + */ + public function anagraficaCondominiale() + { + return $this->belongsTo(AnagraficaCondominiale::class); + } + + /** + * Relazione con User (registrato da) + */ + public function registratoDa() + { + return $this->belongsTo(User::class, 'registrato_da'); + } + + // Relazioni di compatibilità con vecchia struttura + /** + * Relazione con Gestione (compatibilità) */ public function gestione() { @@ -45,15 +139,7 @@ class Rata extends Model } /** - * Relazione con Unità Immobiliare - */ - public function unitaImmobiliare() - { - return $this->belongsTo(UnitaImmobiliare::class, 'unita_immobiliare_id', 'id_unita'); - } - - /** - * Relazione con Soggetto + * Relazione con Soggetto (compatibilità) */ public function soggetto() { @@ -63,34 +149,226 @@ class Rata extends Model /** * Scope per stato */ - public function scopeStato($query, $stato) + public function scopeConStato($query, $stato) { return $query->where('stato', $stato); } /** - * Scope per scadute + * Scope per rate scadute */ public function scopeScadute($query) { - return $query->where('data_scadenza', '<', now()) - ->where('stato', '!=', 'pagata'); + return $query->where('data_scadenza', '<', now()->toDateString()) + ->whereIn('stato', ['emessa']); } /** - * Scope per in scadenza + * Scope per rate in scadenza */ public function scopeInScadenza($query, $giorni = 30) { - return $query->whereBetween('data_scadenza', [now(), now()->addDays($giorni)]) - ->where('stato', '!=', 'pagata'); + return $query->whereBetween('data_scadenza', [now()->toDateString(), now()->addDays($giorni)->toDateString()]) + ->whereIn('stato', ['emessa']); } /** - * Accessor per importo residuo + * Scope per rate pagate + */ + public function scopePagate($query) + { + return $query->where('stato', 'pagata'); + } + + /** + * Scope per rate emesse + */ + public function scopeEmesse($query) + { + return $query->where('stato', 'emessa'); + } + + /** + * Scope per piano rateizzazione + */ + public function scopePerPiano($query, $pianoId) + { + return $query->where('piano_rateizzazione_id', $pianoId); + } + + /** + * Scope per unità immobiliare + */ + public function scopePerUnita($query, $unitaId) + { + return $query->where('unita_immobiliare_id', $unitaId); + } + + /** + * Metodo per registrare pagamento completo + */ + public function registraPagamento($importo, $metodo = null, $riferimento = null, $note = null, $userId = null) + { + $importoPagamento = min($importo, $this->importo_residuo); + $nuovoImportoPagato = $this->importo_pagato + $importoPagamento; + $nuovoImportoResiduo = $this->importo_rata - $nuovoImportoPagato; + + $nuovoStato = $nuovoImportoResiduo <= 0 ? 'pagata' : 'parzialmente_pagata'; + + $this->update([ + 'importo_pagato' => $nuovoImportoPagato, + 'importo_residuo' => $nuovoImportoResiduo, + 'stato' => $nuovoStato, + 'data_pagamento' => now(), + 'metodo_pagamento' => $metodo, + 'riferimento_pagamento' => $riferimento, + 'note_pagamento' => $note, + 'registrato_da' => $userId ?? auth()->id() + ]); + + return $this; + } + + /** + * Metodo per registrare pagamento parziale + */ + public function registraPagamentoParziale($importo, $metodo = null, $riferimento = null, $note = null, $userId = null) + { + return $this->registraPagamento($importo, $metodo, $riferimento, $note, $userId); + } + + /** + * Metodo per annullare la rata + */ + public function annulla($motivo = null) + { + $this->update([ + 'stato' => 'annullata', + 'note_pagamento' => $motivo + ]); + + return $this; + } + + /** + * Metodo per verificare se è scaduta + */ + public function isScaduta() + { + return $this->data_scadenza < now()->toDateString() && + in_array($this->stato, ['emessa']); + } + + /** + * Metodo per verificare se è in scadenza + */ + public function isInScadenza($giorni = 30) + { + $dataLimite = now()->addDays($giorni)->toDateString(); + return $this->data_scadenza <= $dataLimite && + $this->data_scadenza >= now()->toDateString() && + in_array($this->stato, ['emessa']); + } + + /** + * Metodo per aggiornare automaticamente lo stato + */ + public function aggiornaStato() + { + if ($this->stato === 'emessa' && $this->isScaduta()) { + $this->update(['stato' => 'scaduta']); + } + + return $this; + } + + /** + * Verifica se può essere modificata + */ + public function puoEssereModificata() + { + return in_array($this->stato, ['emessa', 'scaduta']); + } + + /** + * Verifica se può essere eliminata + */ + public function puoEssereEliminata() + { + return in_array($this->stato, ['emessa', 'annullata']); + } + + /** + * Accessor per importo residuo calcolato + */ + public function getImportoResiduoCalcolatoAttribute() + { + return ($this->importo_rata ?? $this->importo ?? 0) - ($this->importo_pagato ?? 0); + } + + /** + * Accessor per stato formattato + */ + public function getStatoFormattatoAttribute() + { + return match ($this->stato) { + 'emessa' => 'Emessa', + 'pagata' => 'Pagata', + 'parzialmente_pagata' => 'Parzialmente Pagata', + 'scaduta' => 'Scaduta', + 'annullata' => 'Annullata', + default => ucfirst($this->stato) + }; + } + + /** + * Accessor per metodo pagamento formattato + */ + public function getMetodoPagamentoFormattatoAttribute() + { + return match ($this->metodo_pagamento) { + 'bonifico' => 'Bonifico Bancario', + 'bollettino' => 'Bollettino Postale', + 'contanti' => 'Contanti', + 'assegno' => 'Assegno', + 'pos' => 'POS/Carta', + 'altro' => 'Altro', + default => $this->metodo_pagamento ? ucfirst($this->metodo_pagamento) : 'Non Specificato' + }; + } + + /** + * Accessor per giorni alla scadenza + */ + public function getGiorniAllaScadenzaAttribute() + { + return now()->diffInDays($this->data_scadenza, false); + } + + /** + * Accessor per percentuale pagata + */ + public function getPercentualePagataAttribute() + { + $importoTotale = $this->importo_rata ?? $this->importo ?? 0; + if ($importoTotale == 0) { + return 0; + } + + return round((($this->importo_pagato ?? 0) / $importoTotale) * 100, 2); + } + + /** + * Accessor compatibilità con vecchia struttura */ public function getImportoResiduoAttribute() { - return $this->importo - $this->importo_pagato; + // Se esiste la nuova colonna, usala + if (isset($this->attributes['importo_residuo'])) { + return $this->attributes['importo_residuo']; + } + + // Altrimenti calcola + return $this->getImportoResiduoCalcolatoAttribute(); } } \ No newline at end of file diff --git a/app/Models/RipartizioneSpese.php b/app/Models/RipartizioneSpese.php new file mode 100644 index 00000000..f5d7fb89 --- /dev/null +++ b/app/Models/RipartizioneSpese.php @@ -0,0 +1,328 @@ + 'decimal:2', + 'data_ripartizione' => 'date', + 'parametri_speciali' => 'array', + 'confermata_at' => 'datetime' + ]; + + protected $dates = [ + 'data_ripartizione', + 'confermata_at', + 'created_at', + 'updated_at', + 'deleted_at' + ]; + + /** + * Boot method per generare automaticamente il codice ripartizione + */ + protected static function boot() + { + parent::boot(); + + static::creating(function ($model) { + if (empty($model->codice_ripartizione)) { + $model->codice_ripartizione = self::generaCodiceRipartizione(); + } + }); + } + + /** + * Genera un codice univoco per la ripartizione + */ + public static function generaCodiceRipartizione() + { + do { + $codice = 'RS' . strtoupper(Str::random(6)); + } while (self::where('codice_ripartizione', $codice)->exists()); + + return $codice; + } + + /** + * Relazione con VoceSpesa + */ + public function voceSpesa() + { + return $this->belongsTo(VoceSpesa::class); + } + + /** + * Relazione con Stabile + */ + public function stabile() + { + return $this->belongsTo(Stabile::class); + } + + /** + * Relazione con TabellaMillesimale + */ + public function tabellaMillesimale() + { + return $this->belongsTo(TabellaMillesimale::class); + } + + /** + * Relazione con User (creato da) + */ + public function creatoDa() + { + return $this->belongsTo(User::class, 'creato_da'); + } + + /** + * Relazione con User (confermata da) + */ + public function confermataDa() + { + return $this->belongsTo(User::class, 'confermata_da'); + } + + /** + * Relazione con DettaglioRipartizioneSpese + */ + public function dettagli() + { + return $this->hasMany(DettaglioRipartizioneSpese::class); + } + + /** + * Scope per ripartizioni per stabile + */ + public function scopePerStabile($query, $stabileId) + { + return $query->where('stabile_id', $stabileId); + } + + /** + * Scope per stato + */ + public function scopeConStato($query, $stato) + { + return $query->where('stato', $stato); + } + + /** + * Scope per tipo ripartizione + */ + public function scopePerTipo($query, $tipo) + { + return $query->where('tipo_ripartizione', $tipo); + } + + /** + * Scope per periodo + */ + public function scopePerPeriodo($query, $dataInizio, $dataFine) + { + return $query->whereBetween('data_ripartizione', [$dataInizio, $dataFine]); + } + + /** + * Metodo per confermare la ripartizione + */ + public function conferma($userId = null) + { + $this->update([ + 'stato' => 'confermata', + 'confermata_at' => now(), + 'confermata_da' => $userId ?? auth()->id() + ]); + + return $this; + } + + /** + * Metodo per annullare la ripartizione + */ + public function annulla() + { + $this->update(['stato' => 'annullata']); + return $this; + } + + /** + * Metodo per contabilizzare la ripartizione + */ + public function contabilizza() + { + $this->update(['stato' => 'contabilizzata']); + return $this; + } + + /** + * Verifica se può essere modificata + */ + public function puoEssereModificata() + { + return in_array($this->stato, ['bozza']); + } + + /** + * Verifica se può essere eliminata + */ + public function puoEssereEliminata() + { + return in_array($this->stato, ['bozza', 'annullata']); + } + + /** + * Calcola automaticamente la ripartizione per tutte le unità + */ + public function calcolaRipartizione() + { + if (!$this->tabellaMillesimale) { + throw new \Exception('Tabella millesimale non specificata'); + } + + $unitaImmobiliari = $this->stabile->unitaImmobiliari; + $dettagliMillesimali = $this->tabellaMillesimale->dettagli; + + $this->dettagli()->delete(); // Pulisce i dettagli esistenti + + foreach ($unitaImmobiliari as $unita) { + $dettaglioMillesimale = $dettagliMillesimali->where('unita_immobiliare_id', $unita->id)->first(); + + if (!$dettaglioMillesimale) { + continue; // Salta se non ha millesimi + } + + $quotaCalcolata = ($this->importo_totale * $dettaglioMillesimale->valore_millesimi) / 1000; + + $this->dettagli()->create([ + 'unita_immobiliare_id' => $unita->id, + 'anagrafica_condominiale_id' => $unita->anagrafiche()->where('tipo_diritto', 'proprietario')->first()?->id, + 'millesimi_applicati' => $dettaglioMillesimale->valore_millesimi, + 'quota_calcolata' => $quotaCalcolata, + 'quota_finale' => $quotaCalcolata, + 'tipo_soggetto' => 'proprietario' + ]); + } + + return $this; + } + + /** + * Applica ripartizione inquilini/proprietari + */ + public function applicaRipartizioneInquilini() + { + foreach ($this->dettagli as $dettaglio) { + $ripartizioneInquilini = $dettaglio->unitaImmobiliare + ->ripartizioniSpeseInquilini() + ->attive() + ->byTipoSpesa($this->voceSpesa->categoria) + ->first(); + + if ($ripartizioneInquilini && $ripartizioneInquilini->percentuale_inquilino > 0) { + $importoInquilino = $dettaglio->quota_finale * ($ripartizioneInquilini->percentuale_inquilino / 100); + $importoProprietario = $dettaglio->quota_finale - $importoInquilino; + + $dettaglio->update([ + 'percentuale_inquilino' => $ripartizioneInquilini->percentuale_inquilino, + 'importo_inquilino' => $importoInquilino, + 'importo_proprietario' => $importoProprietario + ]); + } + } + + return $this; + } + + /** + * Ricalcola i totali + */ + public function ricalcolaTotali() + { + $importoTotaleCalcolato = $this->dettagli()->sum('quota_finale'); + + $this->update([ + 'importo_totale' => $importoTotaleCalcolato + ]); + + return $this; + } + + /** + * Ottieni riepilogo ripartizione + */ + public function getRiepilogo() + { + return [ + 'totale_ripartito' => $this->dettagli()->sum('quota_finale'), + 'numero_unita' => $this->dettagli()->count(), + 'numero_unita_esenti' => $this->dettagli()->where('esente', true)->count(), + 'importo_proprietari' => $this->dettagli()->sum('importo_proprietario'), + 'importo_inquilini' => $this->dettagli()->sum('importo_inquilino'), + 'media_per_unita' => $this->dettagli()->count() > 0 ? $this->dettagli()->avg('quota_finale') : 0 + ]; + } + + /** + * Accessor per il tipo ripartizione formattato + */ + public function getTipoRipartizioneFormattatoAttribute() + { + return match ($this->tipo_ripartizione) { + 'millesimale' => 'Ripartizione Millesimale', + 'uguale' => 'Ripartizione Uguale', + 'personalizzata' => 'Ripartizione Personalizzata', + 'speciale' => 'Ripartizione Speciale', + default => ucfirst($this->tipo_ripartizione) + }; + } + + /** + * Accessor per stato formattato + */ + public function getStatoFormattatoAttribute() + { + return match ($this->stato) { + 'bozza' => 'Bozza', + 'confermata' => 'Confermata', + 'contabilizzata' => 'Contabilizzata', + 'annullata' => 'Annullata', + default => ucfirst($this->stato) + }; + } +} diff --git a/app/Models/Stabile.php b/app/Models/Stabile.php index b3131d9b..7cd51a53 100644 --- a/app/Models/Stabile.php +++ b/app/Models/Stabile.php @@ -102,6 +102,22 @@ class Stabile extends Model return $this->hasMany(Ticket::class, 'stabile_id', 'id'); } + /** + * Relazione con RipartizioneSpese + */ + public function ripartizioniSpese() + { + return $this->hasMany(RipartizioneSpese::class); + } + + /** + * Relazione con PianoRateizzazione + */ + public function pianiRateizzazione() + { + return $this->hasMany(PianoRateizzazione::class); + } + /** * Scope per stabili attivi */ diff --git a/app/Models/TabellaMillesimale.php b/app/Models/TabellaMillesimale.php index 111a56e9..6b163868 100644 --- a/app/Models/TabellaMillesimale.php +++ b/app/Models/TabellaMillesimale.php @@ -45,6 +45,14 @@ class TabellaMillesimale extends Model return $this->hasMany(DettaglioTabellaMillesimale::class, 'tabella_millesimale_id'); } + /** + * Relazione con RipartizioneSpese + */ + public function ripartizioniSpese() + { + return $this->hasMany(RipartizioneSpese::class); + } + /** * Scope per tabelle attive */ @@ -165,4 +173,21 @@ class TabellaMillesimale extends Model 'personalizzata' => 'Personalizzata' ]; } + + /** + * Metodo per creare ripartizione spese automatica + */ + public function creaRipartizioneSpese($voceSpesaId, $importoTotale, $dataRipartizione = null) + { + return RipartizioneSpese::create([ + 'voce_spesa_id' => $voceSpesaId, + 'stabile_id' => $this->id, + 'tabella_millesimale_id' => $this->tabelleMillesimali()->where('tipo', 'generale')->first()?->id, + 'tipo_ripartizione' => 'millesimale', + 'importo_totale' => $importoTotale, + 'data_ripartizione' => $dataRipartizione ?? now(), + 'stato' => 'bozza', + 'creato_da' => auth()->id() ?? 1 + ]); + } } \ No newline at end of file diff --git a/app/Models/UnitaImmobiliare.php b/app/Models/UnitaImmobiliare.php index fd82bd7d..779f5b49 100644 --- a/app/Models/UnitaImmobiliare.php +++ b/app/Models/UnitaImmobiliare.php @@ -134,6 +134,54 @@ class UnitaImmobiliare extends Model return $this->hasMany(Proprieta::class, 'unita_immobiliare_id', 'id'); } + /** + * Relazione con DettaglioRipartizioneSpese + */ + public function dettagliRipartizioneSpese() + { + return $this->hasMany(DettaglioRipartizioneSpese::class); + } + + /** + * Relazione con Rate + */ + public function rate() + { + return $this->hasMany(Rata::class); + } + + /** + * Rate attive (emesse o parzialmente pagate) + */ + public function rateAttive() + { + return $this->hasMany(Rata::class)->whereIn('stato', ['emessa', 'parzialmente_pagata', 'scaduta']); + } + + /** + * Rate scadute + */ + public function rateScadute() + { + return $this->hasMany(Rata::class)->scadute(); + } + + /** + * Metodo per ottenere il totale delle rate da pagare + */ + public function getTotaleRateDaPagare() + { + return $this->rateAttive()->sum('importo_residuo'); + } + + /** + * Metodo per ottenere il numero di rate scadute + */ + public function getNumeroRateScadute() + { + return $this->rateScadute()->count(); + } + /** * Scope per unità attive */ diff --git a/app/Models/VoceSpesa.php b/app/Models/VoceSpesa.php index 380bcc6d..3e65e788 100644 --- a/app/Models/VoceSpesa.php +++ b/app/Models/VoceSpesa.php @@ -47,6 +47,14 @@ class VoceSpesa extends Model return $this->belongsTo(TabellaMillesimale::class, 'tabella_millesimale_default_id'); } + /** + * Relazione con RipartizioneSpese + */ + public function ripartizioniSpese() + { + return $this->hasMany(RipartizioneSpese::class); + } + /** * Scope per voci attive */ @@ -70,4 +78,145 @@ class VoceSpesa extends Model { return $query->orderBy('ordinamento')->orderBy('descrizione'); } + + /** + * Boot del modello per generazione automatica codice + */ + protected static function boot() + { + parent::boot(); + + static::creating(function ($model) { + if (empty($model->codice)) { + $model->codice = self::generateUniqueCode($model->stabile_id); + } + }); + } + + /** + * Genera un codice univoco per la voce di spesa + */ + private static function generateUniqueCode($stabileId) + { + do { + $code = 'VS' . str_pad(rand(1, 9999), 4, '0', STR_PAD_LEFT); + } while (self::where('stabile_id', $stabileId)->where('codice', $code)->exists()); + + return $code; + } + + /** + * Scope per categoria specifica + */ + public function scopeByCategoria($query, $categoria) + { + return $query->where('categoria', $categoria); + } + + /** + * Ottieni tutte le categorie standard + */ + public static function getCategorieStandard() + { + return [ + 'manutenzione_ordinaria' => 'Manutenzione Ordinaria', + 'manutenzione_straordinaria' => 'Manutenzione Straordinaria', + 'pulizia' => 'Pulizia', + 'illuminazione' => 'Illuminazione', + 'riscaldamento' => 'Riscaldamento', + 'acqua' => 'Acqua', + 'energia_elettrica' => 'Energia Elettrica', + 'gas' => 'Gas', + 'ascensore' => 'Ascensore', + 'giardino_verde' => 'Giardino e Verde', + 'sicurezza' => 'Sicurezza', + 'amministrazione' => 'Amministrazione', + 'assicurazioni' => 'Assicurazioni', + 'tasse_tributi' => 'Tasse e Tributi', + 'spese_legali' => 'Spese Legali', + 'lavori_miglioramento' => 'Lavori di Miglioramento', + 'altre_spese' => 'Altre Spese' + ]; + } + + /** + * Ottieni i tipi di gestione + */ + public static function getTipiGestione() + { + return [ + 'ordinaria' => 'Gestione Ordinaria', + 'straordinaria' => 'Gestione Straordinaria', + 'speciale' => 'Gestione Speciale' + ]; + } + + /** + * Accessor per descrizione completa + */ + public function getDescrizioneCompletaAttribute() + { + return $this->codice . ' - ' . $this->descrizione; + } + + /** + * Accessor per categoria formattata + */ + public function getCategoriaFormattataAttribute() + { + $categorie = self::getCategorieStandard(); + return $categorie[$this->categoria] ?? $this->categoria; + } + + /** + * Accessor per riassunto voce + */ + public function getRiassuntoAttribute() + { + return [ + 'codice' => $this->codice, + 'descrizione' => $this->descrizione, + 'categoria' => $this->categoria_formattata, + 'tipo_gestione' => $this->tipo_gestione, + 'tabella_default' => $this->tabellaMillesimaleDefault->nome_tabella_millesimale ?? 'Nessuna', + 'ritenuta_default' => $this->ritenuta_acconto_default . '%', + 'attiva' => $this->attiva ? 'Sì' : 'No' + ]; + } + + /** + * Metodo per duplicare voce di spesa + */ + public function duplica($nuovaDescrizione = null) + { + $nuovaVoce = $this->replicate(); + $nuovaVoce->codice = null; // Verrà rigenerato automaticamente + $nuovaVoce->descrizione = $nuovaDescrizione ?? ($this->descrizione . ' (Copia)'); + $nuovaVoce->save(); + + return $nuovaVoce; + } + + /** + * Scope per ricerca testuale + */ + public function scopeRicerca($query, $termine) + { + return $query->where(function($q) use ($termine) { + $q->where('codice', 'LIKE', "%{$termine}%") + ->orWhere('descrizione', 'LIKE', "%{$termine}%") + ->orWhere('categoria', 'LIKE', "%{$termine}%"); + }); + } + + /** + * Metodo per ottenere ripartizioni spese che utilizzano questa voce + */ + public function getRipartizioniSpese() + { + return $this->ripartizioniSpese() + ->with(['stabile', 'tabellaMillesimale', 'dettagli']) + ->orderBy('data_ripartizione', 'desc') + ->get(); + } } \ No newline at end of file diff --git a/database/migrations/2025_07_08_150614_enhance_voci_spesa_table_structure.php b/database/migrations/2025_07_08_150614_enhance_voci_spesa_table_structure.php new file mode 100644 index 00000000..92c4764f --- /dev/null +++ b/database/migrations/2025_07_08_150614_enhance_voci_spesa_table_structure.php @@ -0,0 +1,86 @@ +renameColumn('id_voce', 'id'); + } else if (!Schema::hasColumn('voci_spesa', 'id')) { + $table->id()->first(); + } + + // Aggiungi campi mancanti + if (!Schema::hasColumn('voci_spesa', 'stabile_id')) { + $table->unsignedBigInteger('stabile_id')->after('id'); + $table->foreign('stabile_id')->references('id')->on('stabili')->onDelete('cascade'); + } + + if (!Schema::hasColumn('voci_spesa', 'categoria')) { + $table->string('categoria')->after('descrizione'); + } + + if (!Schema::hasColumn('voci_spesa', 'tipo_gestione')) { + $table->string('tipo_gestione')->default('ordinaria')->after('categoria'); + } + + if (!Schema::hasColumn('voci_spesa', 'tabella_millesimale_default_id')) { + $table->unsignedBigInteger('tabella_millesimale_default_id')->nullable()->after('tipo_gestione'); + $table->foreign('tabella_millesimale_default_id')->references('id')->on('tabelle_millesimali')->onDelete('set null'); + } + + if (!Schema::hasColumn('voci_spesa', 'ritenuta_acconto_default')) { + $table->decimal('ritenuta_acconto_default', 5, 2)->default(0.00)->after('tabella_millesimale_default_id'); + } + + if (!Schema::hasColumn('voci_spesa', 'attiva')) { + $table->boolean('attiva')->default(true)->after('ritenuta_acconto_default'); + } + + if (!Schema::hasColumn('voci_spesa', 'ordinamento')) { + $table->integer('ordinamento')->default(0)->after('attiva'); + } + + // Aggiungi indici per performance + $table->index(['stabile_id', 'categoria']); + $table->index(['stabile_id', 'attiva']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('voci_spesa', function (Blueprint $table) { + // Rimuovi foreign keys + $table->dropForeign(['stabile_id']); + $table->dropForeign(['tabella_millesimale_default_id']); + + // Rimuovi colonne aggiunte + $table->dropColumn([ + 'stabile_id', + 'categoria', + 'tipo_gestione', + 'tabella_millesimale_default_id', + 'ritenuta_acconto_default', + 'attiva', + 'ordinamento' + ]); + + // Ripristina nome colonna originale se era stata rinominata + if (Schema::hasColumn('voci_spesa', 'id')) { + $table->renameColumn('id', 'id_voce'); + } + }); + } +}; diff --git a/database/migrations/2025_07_08_151345_create_ripartizione_spese_table.php b/database/migrations/2025_07_08_151345_create_ripartizione_spese_table.php new file mode 100644 index 00000000..ab068435 --- /dev/null +++ b/database/migrations/2025_07_08_151345_create_ripartizione_spese_table.php @@ -0,0 +1,27 @@ +id(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('ripartizione_spese'); + } +}; diff --git a/database/migrations/2025_07_08_151547_create_dettaglio_ripartizione_spese_table.php b/database/migrations/2025_07_08_151547_create_dettaglio_ripartizione_spese_table.php new file mode 100644 index 00000000..61aeff5f --- /dev/null +++ b/database/migrations/2025_07_08_151547_create_dettaglio_ripartizione_spese_table.php @@ -0,0 +1,27 @@ +id(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('dettaglio_ripartizione_spese'); + } +}; diff --git a/database/migrations/2025_07_08_151654_create_rate_table.php b/database/migrations/2025_07_08_151654_create_rate_table.php new file mode 100644 index 00000000..fa0afe45 --- /dev/null +++ b/database/migrations/2025_07_08_151654_create_rate_table.php @@ -0,0 +1,27 @@ +id(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('rate'); + } +}; diff --git a/database/migrations/2025_07_08_151838_create_piano_rateizzazione_table.php b/database/migrations/2025_07_08_151838_create_piano_rateizzazione_table.php new file mode 100644 index 00000000..5d566655 --- /dev/null +++ b/database/migrations/2025_07_08_151838_create_piano_rateizzazione_table.php @@ -0,0 +1,27 @@ +id(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('piano_rateizzazione'); + } +};