Files
dsi-commander/app/Models/Commande.php

268 lines
9.0 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Builder;
use Carbon\Carbon;
class Commande extends Model
{
use SoftDeletes;
protected $fillable = [
'numero_commande', 'service_id', 'fournisseur_id', 'user_id',
'validateur_id', 'acheteur_id', 'objet', 'description', 'justification',
'statut', 'priorite', 'reference_fournisseur', 'imputation_budgetaire',
'montant_ht', 'montant_ttc',
'date_demande', 'date_souhaitee', 'date_validation', 'date_commande',
'date_livraison_prevue', 'date_reception', 'date_reception_complete', 'date_cloture',
'notes', 'notes_fournisseur',
];
protected $casts = [
'montant_ht' => 'decimal:2',
'montant_ttc' => 'decimal:2',
'date_demande' => 'date',
'date_souhaitee' => 'date',
'date_validation' => 'datetime',
'date_commande' => 'datetime',
'date_livraison_prevue' => 'date',
'date_reception' => 'datetime',
'date_reception_complete' => 'datetime',
'date_cloture' => 'datetime',
];
// -----------------------------------------------------------------------
// Constantes
// -----------------------------------------------------------------------
const STATUTS_LABELS = [
'brouillon' => 'Brouillon',
'en_attente_validation' => 'En attente de validation',
'validee' => 'Validée',
'commandee' => 'Commandée',
'partiellement_recue' => 'Partiellement reçue',
'recue_complete' => 'Reçue complète',
'cloturee' => 'Clôturée',
'annulee' => 'Annulée',
];
const STATUTS_COULEURS = [
'brouillon' => 'gray',
'en_attente_validation' => 'yellow',
'validee' => 'blue',
'commandee' => 'indigo',
'partiellement_recue' => 'orange',
'recue_complete' => 'green',
'cloturee' => 'slate',
'annulee' => 'red',
];
const PRIORITES_LABELS = [
'normale' => 'Normale',
'haute' => 'Haute',
'urgente' => 'Urgente',
];
const STATUT_TRANSITIONS = [
'brouillon' => ['en_attente_validation', 'annulee'],
'en_attente_validation' => ['validee', 'brouillon', 'annulee'],
'validee' => ['commandee', 'annulee'],
'commandee' => ['partiellement_recue', 'recue_complete', 'annulee'],
'partiellement_recue' => ['recue_complete', 'annulee'],
'recue_complete' => ['cloturee'],
'cloturee' => [],
'annulee' => [],
];
// -----------------------------------------------------------------------
// Relations
// -----------------------------------------------------------------------
public function service(): BelongsTo
{
return $this->belongsTo(Service::class);
}
public function fournisseur(): BelongsTo
{
return $this->belongsTo(Fournisseur::class);
}
public function demandeur(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
public function validateur(): BelongsTo
{
return $this->belongsTo(User::class, 'validateur_id');
}
public function acheteur(): BelongsTo
{
return $this->belongsTo(User::class, 'acheteur_id');
}
public function lignes(): HasMany
{
return $this->hasMany(LigneCommande::class)->orderBy('ordre');
}
public function historique(): HasMany
{
return $this->hasMany(HistoriqueCommande::class)->orderBy('created_at', 'desc');
}
public function piecesJointes(): HasMany
{
return $this->hasMany(PieceJointe::class)->orderBy('created_at', 'desc');
}
// -----------------------------------------------------------------------
// Scopes
// -----------------------------------------------------------------------
public function scopeEnCours(Builder $query): Builder
{
return $query->whereNotIn('statut', ['cloturee', 'annulee']);
}
public function scopeEnRetard(Builder $query): Builder
{
return $query->enCours()->whereNotNull('date_souhaitee')->where('date_souhaitee', '<', now()->toDateString());
}
public function scopeUrgentes(Builder $query): Builder
{
return $query->where('priorite', 'urgente');
}
public function scopeParService(Builder $query, int $serviceId): Builder
{
return $query->where('service_id', $serviceId);
}
public function scopeParStatut(Builder $query, string $statut): Builder
{
return $query->where('statut', $statut);
}
public function scopeParFournisseur(Builder $query, int $fournisseurId): Builder
{
return $query->where('fournisseur_id', $fournisseurId);
}
// -----------------------------------------------------------------------
// Accesseurs
// -----------------------------------------------------------------------
public function getStatutLabelAttribute(): string
{
return self::STATUTS_LABELS[$this->statut] ?? $this->statut;
}
public function getStatutCouleurAttribute(): string
{
return self::STATUTS_COULEURS[$this->statut] ?? 'gray';
}
public function getEstEnRetardAttribute(): bool
{
return !in_array($this->statut, ['cloturee', 'annulee'])
&& $this->date_souhaitee !== null
&& $this->date_souhaitee->isPast();
}
public function getTauxReceptionAttribute(): float
{
$total = $this->lignes->sum(fn ($l) => (float) $l->quantite);
if ($total <= 0) {
return 0.0;
}
$recu = $this->lignes->sum(fn ($l) => (float) $l->quantite_recue);
return round(min($recu / $total * 100, 100), 1);
}
// -----------------------------------------------------------------------
// Numérotation automatique
// -----------------------------------------------------------------------
public static function genererNumero(): string
{
$annee = now()->year;
$derniere = static::withTrashed()
->where('numero_commande', 'like', "CMD-IT-{$annee}-%")
->orderByDesc('id')
->value('numero_commande');
$sequence = 1;
if ($derniere) {
$sequence = (int) substr($derniere, strrpos($derniere, '-') + 1) + 1;
}
return sprintf('CMD-IT-%d-%04d', $annee, $sequence);
}
// -----------------------------------------------------------------------
// Moteur de transitions
// -----------------------------------------------------------------------
public function peutTransitionnerVers(string $statut): bool
{
return in_array($statut, self::STATUT_TRANSITIONS[$this->statut] ?? []);
}
public function transitionnerVers(string $statut, User $user, ?string $commentaire = null): bool
{
if (!$this->peutTransitionnerVers($statut)) {
return false;
}
$ancienStatut = $this->statut;
$this->statut = $statut;
// Mettre à jour les dates et les acteurs selon le nouveau statut
match ($statut) {
'validee' => $this->fill(['date_validation' => now(), 'validateur_id' => $user->id]),
'commandee' => $this->fill(['date_commande' => now(), 'acheteur_id' => $user->id]),
'partiellement_recue' => $this->fill(['date_reception' => $this->date_reception ?? now()]),
'recue_complete' => $this->fill(['date_reception_complete' => now(), 'date_reception' => $this->date_reception ?? now()]),
'cloturee' => $this->fill(['date_cloture' => now()]),
default => null,
};
$this->save();
HistoriqueCommande::create([
'commande_id' => $this->id,
'user_id' => $user->id,
'ancien_statut' => $ancienStatut,
'nouveau_statut' => $statut,
'commentaire' => $commentaire,
]);
return true;
}
// -----------------------------------------------------------------------
// Recalcul des montants (appelé depuis LigneCommande)
// -----------------------------------------------------------------------
public function recalculerMontants(): void
{
$this->lignes()->get()->tap(function ($lignes) {
$ht = $lignes->sum(fn ($l) => (float) $l->montant_ht);
$ttc = $lignes->sum(fn ($l) => (float) $l->montant_ttc);
$this->withoutEvents(function () use ($ht, $ttc) {
$this->update(['montant_ht' => $ht, 'montant_ttc' => $ttc]);
});
});
}
}