- Added maximum discount settings configurable by the administrator. - Implemented discount calculations in the quote confirmation process. - Introduced transport options with associated costs, including handling for exceeding maximum piece limits. - Updated frontend to display discounts and transport information in the quote summary. - Created new database migrations for storing discount and transport settings. - Developed a new Filament page for managing global discount settings. - Enhanced email templates to include discount and transport details. - Added tests to ensure proper functionality of discounts and transport calculations.
150 lines
5.0 KiB
PHP
150 lines
5.0 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
class Preventivo extends Model
|
|
{
|
|
protected $table = 'preventivi';
|
|
|
|
protected $fillable = [
|
|
'cliente_id', 'token', 'numero', 'stato', 'subtotale', 'sconto_1', 'sconto_2', 'trasporto', 'trasporto_importo', 'totale', 'pdf_path', 'confirmed_at',
|
|
];
|
|
|
|
protected $casts = [
|
|
'trasporto_importo' => 'decimal:2',
|
|
'subtotale' => 'decimal:2',
|
|
'sconto_1' => 'decimal:2',
|
|
'sconto_2' => 'decimal:2',
|
|
'totale' => 'decimal:2',
|
|
'confirmed_at' => 'datetime',
|
|
];
|
|
|
|
/** Trasporto oltre il numero massimo di pezzi della tariffa cadauno: da quantificare. */
|
|
public const TRASPORTO_OLTRE_MAX = 'oltre_max_pezzi';
|
|
|
|
public const STATI = [
|
|
'bozza' => 'Bozza',
|
|
'inviato' => 'Inviato',
|
|
'in_lavorazione' => 'In lavorazione',
|
|
'chiuso' => 'Chiuso',
|
|
];
|
|
|
|
protected static function booted(): void
|
|
{
|
|
static::deleting(function (Preventivo $preventivo): void {
|
|
// Elimina immagini degli item (disk 'public') e PDF (disk 'local') prima della cancellazione.
|
|
foreach ($preventivo->items()->get() as $item) {
|
|
if ($item->immagine_path && Storage::disk('public')->exists($item->immagine_path)) {
|
|
Storage::disk('public')->delete($item->immagine_path);
|
|
}
|
|
|
|
$item->delete();
|
|
}
|
|
|
|
if ($preventivo->pdf_path && Storage::disk('local')->exists($preventivo->pdf_path)) {
|
|
Storage::disk('local')->delete($preventivo->pdf_path);
|
|
}
|
|
});
|
|
}
|
|
|
|
public function cliente(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Cliente::class);
|
|
}
|
|
|
|
public function items(): HasMany
|
|
{
|
|
return $this->hasMany(PreventivoItem::class);
|
|
}
|
|
|
|
/**
|
|
* Ricalcola e salva subtotale (somma degli item), contributo trasporto e totale:
|
|
* imponibile scontato + trasporto (che non entra negli sconti).
|
|
*/
|
|
public function ricalcolaTotale(): void
|
|
{
|
|
$this->subtotale = $this->items()->sum('prezzo_totale');
|
|
|
|
$pezzi = (int) $this->items()->sum('quantita');
|
|
if ($this->trasporto !== null && static::oltreMaxPezzi($pezzi)) {
|
|
$this->trasporto = self::TRASPORTO_OLTRE_MAX;
|
|
}
|
|
|
|
$perPezzo = config("preventivi.trasporto.zone.{$this->trasporto}.importo");
|
|
$this->trasporto_importo = $perPezzo === null ? null : round($perPezzo * $pezzi, 2);
|
|
|
|
$this->totale = $this->importiSconto()['imponibile'] + (float) $this->trasporto_importo;
|
|
$this->save();
|
|
}
|
|
|
|
public function getTrasportoEtichettaAttribute(): ?string
|
|
{
|
|
if ($this->trasporto === self::TRASPORTO_OLTRE_MAX) {
|
|
return config('preventivi.trasporto.etichetta_oltre_max');
|
|
}
|
|
|
|
return $this->trasporto ? config("preventivi.trasporto.zone.{$this->trasporto}.etichetta", $this->trasporto) : null;
|
|
}
|
|
|
|
/** Oltre il massimo di pezzi il trasporto non si sceglie: è da quantificare. */
|
|
public static function oltreMaxPezzi(int $pezzi): bool
|
|
{
|
|
return $pezzi > (int) config('preventivi.trasporto.max_pezzi');
|
|
}
|
|
|
|
/** Avviso per il cliente quando il trasporto è da quantificare. */
|
|
public function notaTrasporto(): ?string
|
|
{
|
|
if (! $this->trasportoDaQuantificare()) {
|
|
return null;
|
|
}
|
|
|
|
$nota = config('preventivi.trasporto.nota_da_quantificare');
|
|
|
|
return $this->trasporto === self::TRASPORTO_OLTRE_MAX
|
|
? sprintf('%s: il preventivo supera i %d pezzi. %s', config('preventivi.trasporto.etichetta_oltre_max'), config('preventivi.trasporto.max_pezzi'), $nota)
|
|
: $nota;
|
|
}
|
|
|
|
/** Zona scelta senza un importo fisso ("Altro"): il costo va chiesto all'azienda. */
|
|
public function trasportoDaQuantificare(): bool
|
|
{
|
|
return $this->trasporto !== null && $this->trasporto_importo === null;
|
|
}
|
|
|
|
/**
|
|
* Sconti a cascata sul totale parziale (IVA esclusa): il 1° sul subtotale, il 2° extra
|
|
* sul totale già scontato. Es. 1000 -50% = 500, -10% = 450.
|
|
*
|
|
* @return array{sconto_1:float,sconto_2:float,imponibile:float}
|
|
*/
|
|
public function importiSconto(): array
|
|
{
|
|
$subtotale = (float) $this->subtotale;
|
|
$sconto1 = round($subtotale * (float) $this->sconto_1 / 100, 2);
|
|
$sconto2 = round(($subtotale - $sconto1) * (float) $this->sconto_2 / 100, 2);
|
|
|
|
return [
|
|
'sconto_1' => $sconto1,
|
|
'sconto_2' => $sconto2,
|
|
'imponibile' => round($subtotale - $sconto1 - $sconto2, 2),
|
|
];
|
|
}
|
|
|
|
public function haSconti(): bool
|
|
{
|
|
return (float) $this->sconto_1 > 0 || (float) $this->sconto_2 > 0;
|
|
}
|
|
|
|
/** Numero progressivo assegnato alla conferma (max+1). */
|
|
public static function prossimoNumero(): int
|
|
{
|
|
return (int) static::max('numero') + 1;
|
|
}
|
|
}
|