Laravel 7

Shopping : les commandes

Nous allons aborder dans cet article la partie la plus importante de la boutique : la gestion des commandes. Il faut pouvoir entrer un numéro de bon de commande en cas de commande par mandat administratif, changer l’état, et générer la facture si le paiement a eu lieu. On doit de plus avoir accès à toutes les informations nécessaires.

Vous pouvez télécharger un ZIP du projet ici.

Les données

Les données des commandes sont dans la table orders :

D’autre part on va aller chercher des renseignements dans les tables state et user.

La liste des commandes

Contrôleur et routes

On crée un contrôleur :

php artisan make:controller Back\OrderController --resource --model=Models\Order

On n’utilisera que les méthodes index, show et update, on va ajouter quelques autres méthodes.

On ajoute les routes :

Route::prefix('admin')->middleware('admin')->namespace('Back')->group(function () {
    ...
    Route::resource('commandes', 'OrderController')->only(['index', 'show', 'update'])->names([
        'index' => 'orders.index',
        'show' => 'orders.show',
        'update' => 'orders.update',
    ]);
});

DataTable

On va utiliser un dataTable pour la gestion des commandes :

php artisan datatables:make OrdersDataTable

<?php

namespace App\DataTables;

use App\Models\Order;
use Yajra\DataTables\Html\Column;
use Yajra\DataTables\Services\DataTable;

class OrdersDataTable extends DataTable
{
    /**
     * Build DataTable class.
     *
     * @param mixed $query Results from query() method.
     * @return \Yajra\DataTables\DataTableAbstract
     */
    public function dataTable($query)
    {
        return datatables()
            ->eloquent($query)
            ->editColumn('created_at', function ($order) {
                return $order->created_at->format('d/m/Y');
            })
            ->editColumn('updated_at', function ($order) {
                return $order->updated_at->format('d/m/Y');
            })
            ->editColumn('total', function ($order) {
                return number_format($order->totalOrder, 2, ',', ' ') . ' €';
            })
            ->editColumn('payment', function ($order) {
                return $order->payment_text;
            })
            ->editColumn('state_id', function ($order) {
                return '<span class="badge badge-' . config('colors.' . $order->state->color) . '">' . $order->state->name . '</span>';
            })
            ->orderColumn('state_id', '-state_id $1')
            ->addColumn('client', function ($order) {
                return '<a href="' . route('clients.show', $order->user->id) . '">' . $order->user->name . ' ' . $order->user->firstname . '</a>';
            })
            ->editColumn('invoice_id', function ($order) {
                return $order->invoice_id ? '<i class="fas fa-check text-success"></i>' : '';
            })
            ->addColumn('action', function ($order) {
                return '<a href="' . route('orders.show', $order->id) . '" class="btn btn-xs btn-info btn-block">Voir</a>';
            })
            ->rawColumns(['client', 'state_id', 'invoice_id', 'action']);
    }

    /**
     * Get query source of dataTable.
     *
     * @param \App\Models\Order $model
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function query(Order $model)
    {
        return $model->with('state', 'user')->newQuery();
    }

    /**
     * Optional method if you want to use html builder.
     *
     * @return \Yajra\DataTables\Html\Builder
     */
    public function html()
    {
        return $this->builder()
                    ->setTableId('orders-table')
                    ->columns($this->getColumns())
                    ->minifiedAjax()
                    ->dom('Blfrtip')
                    ->orderBy(1)
                    ->lengthMenu()
                    ->language('//cdn.datatables.net/plug-ins/1.10.20/i18n/French.json');
    }

    /**
     * Get columns.
     *
     * @return array
     */
    protected function getColumns()
    {
        return [
            Column::make('id'),
            Column::make('reference')->title('Référence'),
            Column::computed('client')->title('Client'),
            Column::make('total')->title('Total'),
            Column::make('payment')->title('Paiement'),
            Column::make('state_id')->title('Etat'),
            Column::make('invoice_id')->title('Facture')->addClass('text-center'),
            Column::make('created_at')->title('Date'),
            Column::make('updated_at')->title('Changement'),
            Column::computed('action')->title('')->width(60)->addClass('text-center'),
        ];
    }

    /**
     * Get filename for export.
     *
     * @return string
     */
    protected function filename()
    {
        return 'Orders_' . date('YmdHis');
    }
}

Le tableau

Pour afficher la liste des commandes on code la méthode index du contrôleur OrderController :

use App\DataTables\OrdersDataTable;

...

public function index(OrdersDataTable $dataTable)
{
    return $dataTable->render('back.shared.index');
}

On renseigne le titre dans config.titles :

return [

   ...

    'orders' => [
        'index' => 'Commandes',
        'show' => 'Gestion d\'une commande',
    ],
];

Avec l’url …/admin/commandes on a le tableau :

On peut trier par colonne (sauf les clients parce que le contenu de la colonne est calculé) et normalement la pagination fonctionne automatiquement, de même que le champ de recherche.

La fiche de la commande

Quand on clique sur le bouton Voir on doit afficher la fiche de la commande.

Le contrôleur

Pour afficher la fiche de la commande on va coder la méthode show du contrôleur :

use App\Models\{ Order, State };

...

public function show($id)
{
    $order = Order::with('adresses', 'products', 'state', 'user', 'user.orders', 'payment_infos')->findOrFail($id);

    // Cas du mandat administratif
    $annule_indice = State::whereSlug('annule')->first()->indice;
    $states = $order->payment === 'mandat' && !$order->purchase_order ?
      State::where('indice', '<=', $annule_indice)
          ->where('indice', '>', 0)
          ->get() :
      State::where('indice', '>=', $order->state->indice)->get();    

    return view('back.orders.show', compact('order', 'states', 'shop', 'annule_indice'));
}

On charge toutes les données nécessaires à l’affichage.

On a le cas particulier du mandat administratif parce qu’il faut gérer correctement les états disponibles.

La vue

On crée la vue pour la fiche :

@extends('back.layout')

@section('main') 
  <div class="card">
    <h5 class="card-header">Commande 
      <span class="badge badge-secondary">{{ $order->reference }}</span> 
      <span class="badge badge-secondary">N° {{ $order->id }}</span>
    </h5>
    <div class="card-body">
      <div class="card">
        <h5 class="card-header">Mode de paiement</span></h5>
        <div class="card-body">
          <p>{{ $order->payment_text }}</p>
          @if($order->payment_infos)
            ID de paiement : <span class="badge badge-secondary">{{ $order->payment_infos->payment_id }}</span>
          @endif
        </div>
      </div>
      @if($shop->invoice && ($order->invoice_id || $order->state->indice > $annule_indice))
        <div class="card">        
          <h5 class="card-header">Facture</h5>
          <div class="card-body">  
            @if(session('invoice'))
              <div class="alert alert-danger" role="alert">
                  {{ session('invoice') }}
                  <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                  </button>
              </div>
            @endif
            @if($order->invoice_id)
              <p>La facture a été générée avec l'id <strong>{{ $order->invoice_id }}</strong> et le numéro <strong>{{ $order->invoice_number }}</strong>.</p>
            @else
              <form method="POST" action="#">
                @csrf
                <x-checkbox
                  name="paid"
                  label="Le paiement a été effectué"
                  :value="$order->state->indice > 3"
                ></x-checkbox>
                <button type="submit" class="btn btn-primary">Générer la facture</button>
              </form>
            @endif
          </div>
        </div>
      @endif
      @if($order->payment === 'mandat')
        <div class="card">
          <h5 class="card-header">Bon de commande</h5>
          <div class="card-body">
            <form method="POST" action="#">
              @method('PUT')
              @csrf   
              <x-inputbs4
                name="purchase_order"
                type="text"
                label="N° du bon de commande"
                :value="$order->purchase_order"
              ></x-inputbs4>
              <button type="submit" class="btn btn-primary"  @if($order->state->indice >= 3) disabled @endif>Mettre à jour le numéro du bon de commande</button>
            </form>
          </div>
        </div>
      @endif
      <div class="card">
        <h5 class="card-header">Etat : <span class="badge badge-{{ config('colors.' . $order->state->color) }}"> {{ $order->state->name }}</span></h5>
        <div class="card-body">
          <form method="POST" action="{{ route('orders.update', $order->id) }}">
            @method('PUT')
            @csrf   
            <select id="state_id" name="state_id" class="custom-select custom-select-md mb-3">
              @foreach($states as $state)
                <option data-slug="{{ $state->slug }}" value="{{ $state->id }}" @if($order->state->id === $state->id) selected @endif>{{ $state->name }}</option>
              @endforeach
            </select>
            <button type="submit" class="btn btn-primary">Mettre à jour l'état</button>
          </form>
        </div>
      </div>
      <div class="card">
        <h5 class="card-header">Produits</h5>
        <div class="card-body">
          @foreach ($order->products as $item)
            <br>
            <div class="row">
              <div class="col m6 s12">
                {{ $item->name }} ({{ $item->quantity }} @if($item->quantity > 1) exemplaires) @else exemplaire) @endif
              </div>
              <div class="col m6 s12"><strong>{{ number_format($item->total_price_gross, 2, ',', ' ') }} €</strong></div>
            </div>
          @endforeach
          <hr><br>
          <div class="row" style="background-color: lightgrey">
            <div class="col s6">
              Total HT
            </div>
            <div class="col s6">
              <strong>{{ number_format($order->ht, 2, ',', ' ') }} €</strong>
            </div>
          </div>
          <br>
          <div class="row" style="background-color: lightgrey">
            <div class="col s6">
              Livraison en Colissimo
            </div>
            <div class="col s6">
              <strong>{{ number_format($order->shipping, 2, ',', ' ') }} €</strong>
            </div>
          </div>
          <br>
          @if($order->tax > 0)
            <div class="row" style="background-color: lightgrey">
              <div class="col s6">
                TVA à {{ $order->tax * 100 }} %
              </div>
              <div class="col s6">
                <strong>{{ number_format($order->tva, 2, ',', ' ') }} €</strong>
              </div>
            </div>
            <br>
          @endif
          <div class="row" style="background-color: lightgrey">
            <div class="col s6">
              Total TTC
            </div>
            <div class="col s6">
              <strong>{{ number_format($order->totalOrder, 2, ',', ' ') }} €</strong>
            </div>
          </div>
        </div>
      </div>
      <div class="card">
        <h5 class="card-header">Livraison</h5>
        <div class="card-body">
          @if($order->pick)
            Le client a choisi de venir chercher sa commande sur place
          @else
            Livraison normale en Colissimo
          @endif
        </div>
      </div>
    </div>
  </div>

  <div class="card">
    <h5 class="card-header">Client : 
    <a href="{{ route('clients.show', $order->user->id) }}"><span class="badge badge-primary">{{ $order->user->firstname . ' ' . $order->user->name }}</span></a>  
      <span class="badge badge-secondary">N° {{ $order->user->id }}</span>
    </h5>
    <div class="card-body">
      <div class="card">
        <div class="card-body">
          <dl class="row">  
            <dt class="col-sm-3">Email</dt>
            <dd class="col-sm-9"><a href="mailto:{{ $order->user->email }}">{{ $order->user->email }}</a></dd>      
            <dt class="col-sm-3 text-truncate">Date d'inscription</dt>
            <dd class="col-sm-9">{{ $order->user->created_at->format('d/m/Y') }}</dd>
            <dt class="col-sm-3 text-truncate">Commandes validées</dt>
            <dd class="col-sm-9"><span class="badge badge-primary">{{ $order->user->orders->where('state_id', '>', 5)->count() }}</span></dd>
          </dl>
        </div>
      </div>
    </div>
  </div>

  <div class="card">
    <h5 class="card-header">Adresses</h5>
    <div class="card-body">
      <div class="card">
        <div class="card-body">
          <ul class="nav nav-tabs" id="myTab" role="tablist">
            <li class="nav-item">
              <a class="nav-link active" id="home-tab" data-toggle="tab" href="#home" role="tab" aria-controls="home" aria-selected="true"><i class="fas fa-truck"></i> Adresse d'expédition</a>
            </li>
            <li class="nav-item">
              <a class="nav-link" id="profile-tab" data-toggle="tab" href="#profile" role="tab" aria-controls="profile" aria-selected="false"><i class="fas fa-euro-sign"></i> Adresse de facturation</a>
            </li>
          </ul>
          <div class="tab-content" id="myTabContent">
            <div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab"><br>
              @if($order->adresses->count() === 1)
                @include('account.addresses.partials.address', ['address' => $order->adresses->first()])
              @else
                @include('account.addresses.partials.address', ['address' => $order->adresses->get(1)])
              @endif
            </div>
            <div class="tab-pane fade" id="profile" role="tabpanel" aria-labelledby="profile-tab"><br>
              @include('account.addresses.partials.address', ['address' => $order->adresses->first()])
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>

@endsection

On obtient la fiche en cliquant sur le bouton Voir. On a plusieurs zones.

La référence et l’indice de la commande en partie supérieure :

Puis le mode de paiement :

Si le paiement a été effectué un zone pour la facture (si la facture a déjà été génére le numro apparaît, sinon on peut la générer) :

En cas de paiement par mandat administratif le numéro du bon de commande :

L’état actuel et tous les états disponibles dans une liste de choix :

Le détail de la commande :

Le mode de livraison :

Des données sur le client avec un lien vers sa fiche :

La ou les adresses de la commande :

Le menu

On va ajouter les commandes dans le menu (back.layout) :

<x-menu-item 
  :href="route('orders.index')" 
  icon="shopping-basket"
  :active="currentRouteActive('orders.index', 'orders.show')">
  Commandes
</x-menu-item>

Changement de l’état

En fonction de l’évolution de la commande (paiement, expédition, annulation…) on doit mettre à jour l’état. selon la situation seuls les états disponibles apparaissent dans la liste de choix (ainsi que l’état actuel) :

On dispose d’un bouton pour valider le changement de l’état.

On code la méthode update du contrôleur :

public function update(Request $request, Order $commande)
{
    $commande->load('state');

    $states = State::all();

    if($request->state_id !== $commande->state_id) {
        // En cas de changement de type de paiement
        $indice_payment = $states->firstWhere('slug', 'cheque')->indice;
        $state_new = $states->firstWhere('id', $request->state_id);
        if($commande->state->indice ===  $indice_payment && $state_new->indice ===  $indice_payment){
            $commande->payment = $states->firstWhere('id', $request->state_id)->slug;
        }

        $commande->state_id = $request->state_id;                      
        $commande->save();          
    }
    
    return back();
}

On vérifie qu’on a vraiment changé d’état sinon on ne fait rien. En cas de changement de type de paiement il faut aussi mettre à jour la colonne payment. On retourne sur la fiche de la commande avec le nouvel état :

Génération de la facture

Lorsque la commande est réglée par carte bancaire on a vu que la génération de la facture était automatique. Par contre lorsqu’on change d’état parce que la commande a été réglée (chèque, virement…) il va falloir valider ce paiement et générer la facture :

On pourrait aussi créer un automatisme à partir du changement d’état mais je trouve que c’est toujours mieux de gérer les opération de façon manuelle.

On crée une méthode invoice dans le contrôleur :

use App\Services\Facture;

...

public function invoice(Request $request, Facture $facture,  Order $commande)
{
    $response = $facture->create($commande, $request->has('paid'));       

    if($response->successful()) {

        $data = json_decode($response->body());
        $commande->invoice_id = $data->id;
        $commande->invoice_number = $data->number;
        $commande->save();

      } else {
        $request->session()->flash('invoice', 'La création de facture n\'a pas abouti');
    }

    return back();
}

On utilise le service qu’on a déjà mis en place. On mémorise l’identifiant et le numéro de la facture.

On ajoute la route :

Route::prefix('admin')->middleware('admin')->namespace('Back')->group(function () {
    ...
    Route::name('orders.invoice')->post('commandes/invoice/{commande}', 'OrderController@invoice');
});

Ainsi que la route dans le formulaire :

<form method="POST" action="{{ route('orders.invoice', $order->id) }}">

On peut maintenant générer la facture :

Numéro du bon de commande

Lorsqu’une commande a été passée avec comme paiement un mandat administratif on obtient le numéro qu’après coup lorsqu’on reçoit ce bon de commande. Il faut alors le préciser parce qu’on en a besoin pour la facture :

On ajoute une méthode dans le contrôleur :

public function updateNumber(Request $request, Order $commande)
{
    $request->validate([
        'purchase_order' => 'required|string|max:100'
    ]);

    $commande->purchase_order = $request->purchase_order;
    $commande->save();            

    return back();
}

On crée la route :

Route::prefix('admin')->middleware('admin')->namespace('Back')->group(function () {
    ...
    Route::name('orders.updateNumber')->put('commandes/updateNumber/{commande}', 'OrderController@updateNumber');
});

On l’ajoute dans le formulaire :

<form method="POST" action="{{ route('orders.updateNumber', $order->id) }}">

Et maintenant ça doit fonctionner !

Les commandes d’un client

Lorqu’on a vu la gestion des clients on a laissé quelque chose en suspend. Dans la liste de ses commande on a prévu un bouton Voir mais on n’avait pas mis de lien :

On va maintenant pouvoir coder ce bouton (back.users.show) :

<td style="text-align: center"><a href="{{ route('orders.show', $order->id) }}" class="btn btn-primary btn-sm">Voir</a></td>

Maintenant le clic ouvre la fiche de la commande.

Conclusion

Nous avons codé les parties essentielles de la boutique. Dans le prochain article on verra le mode maintenance et les mises en cache pour améliorer les performances.

Print Friendly, PDF & Email

4 commentaires

Laisser un commentaire