'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 commune(): BelongsTo { return $this->belongsTo(Commune::class); } public function validateur(): BelongsTo { return $this->belongsTo(User::class, 'validateur_id'); } public function assets(): HasMany { return $this->hasMany(Asset::class); } 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]); }); }); } }