Création et soumission de commandes dans un projet e-commerce avec Django

Affichage de la page de soumission

Lorsque l'utilisateur clique sur "Passer commande" dans le panier, il est redirigé vers la page /order/place/. Les éléments sélectionnés sont regroupés dans un fromulaire HTML utilisant des cases à cocher. Seules les valeurs des cases cochées sont soumises via une liste d'identifiants produits.

<form method="post" action="{% url 'order:place' %}">
    {% csrf_token %}
    {% for produit in liste_produits %}
    <ul class="liste_produit">
        <li><input type="checkbox" name="produit_ids" value="{{ produit.id }}" checked></li>
        <!-- Détails du produit -->
    </ul>
    {% endfor %}
    <ul class="actions">
        <li><input type="submit" value='Valider la commande'/></li>
    </ul>
</form>

Configuration des URLs et vue

# urls.py
path('place/', VueCommande.as_view(), name='place'),

# views.py
class VueCommande(LoginRequiredMixin, View):
    def post(self, request):
        identifiants_produits = request.POST.getlist('produit_ids')
        if not identifiants_produits:
            return redirect('panier:index')
        
        total = 0
        articles = []
        connexion_redis = get_redis_connection('default')
        cle_panier = f'panier_{request.user.id}'
        
        for pid in identifiants_produits:
            try:
                article = Produit.objects.get(id=pid)
                quantite = int(connexion_redis.hget(cle_panier, pid))
                montant = article.prix * quantite
                article.quantite = quantite
                article.montant = montant
                articles.append(article)
                total += montant
            except Produit.DoesNotExist:
                return redirect('panier:index')
        
        context = {
            'articles': articles,
            'total': total,
            'frais_livraison': 10
        }
        return render(request, 'commande/validation.html', context)

Template de validation de commaned

{% extends 'base.html' %}
{% block contenu %}
<h3>Adresse de livraison</h3>
<!-- Sélection d'adresse -->

<h3>Méthode de paiement</h3>
<!-- Options de paiement -->

<h3>Détails des articles</h3>
{% for article in articles %}
<div class="detail_article">
    <img src="{{ article.image.url }}">
    <span>{{ article.nom }} - {{ article.quantite }} x {{ article.prix }}</span>
</div>
{% endfor %}

<script>
$('#btn_commander').click(function() {
    $.post('/commande/creer/', {
        'adresse_id': $('input[name="adresse"]:checked').val(),
        'mode_paiement': $('input[name="paiement"]:checked').val(),
        'articles_ids': "{{ chaine_ids }}",
        'csrfmiddlewaretoken': "{{ csrf_token }}"
    }, function(reponse) {
        if (reponse.succes) {
            // Redirection après succès
        }
    })
});
</script>
{% endblock %}

Création de la commande

class CreationCommande(View):
    @transaction.atomic
    def post(self, request):
        reponse = {'succes': False, 'erreur': ''}
        utilisateur = request.user
        
        # Validation des données
        adresse_id = request.POST.get('adresse_id')
        mode_paiement = request.POST.get('mode_paiement')
        chaine_ids = request.POST.get('articles_ids')
        
        if not all([adresse_id, mode_paiement, chaine_ids]):
            reponse['erreur'] = 'Données manquantes'
            return JsonResponse(reponse)
        
        # Génération du numéro de commande
        sequence = get_next_value('commande')
        numero_commande = f"{datetime.now().strftime('%Y%m%d%H%M%S')}{sequence}"
        
        point_sauvegarde = transaction.savepoint()
        try:
            # Création entête commande
            commande = Commande.objects.create(
                numero=numero_commande,
                utilisateur=utilisateur,
                adresse_id=adresse_id,
                mode_paiement=mode_paiement
            )
            
            connexion_redis = get_redis_connection('default')
            cle_panier = f'panier_{utilisateur.id}'
            ids_produits = chaine_ids.split(',')
            
            for pid in ids_produits:
                tentatives = 0
                while tentatives < 3:
                    produit = Produit.objects.get(id=pid)
                    quantite = int(connexion_redis.hget(cle_panier, pid))
                    
                    if quantite > produit.stock:
                        reponse['erreur'] = 'Stock insuffisant'
                        raise Exception()
                    
                    nouveau_stock = produit.stock - quantite
                    lignes_affectees = Produit.objects.filter(
                        id=pid, 
                        stock=produit.stock
                    ).update(stock=nouveau_stock)
                    
                    if lignes_affectees:
                        LigneCommande.objects.create(
                            commande=commande,
                            produit=produit,
                            quantite=quantite,
                            prix=produit.prix
                        )
                        break
                        
                    tentatives += 1
            
            connexion_redis.hdel(cle_panier, *ids_produits)
            reponse['succes'] = True
            
        except Exception as e:
            transaction.savepoint_rollback(point_sauvegarde)
            reponse['erreur'] = str(e) or 'Erreur système'
        
        return JsonResponse(reponse)

Gestion des transactions et concurrence

L'annotation @transaction.atomic assure l'intégrité des opérations base de données. Deux stratégies gèrent les accès concurrents :

Verrouillage pesimiste :

produit = Produit.objects.select_for_update().get(id=pid)

Verrouillage optimiste :

lignes_affectees = Produit.objects.filter(
    id=pid, 
    stock=ancien_stock
).update(stock=nouveau_stock)

Le verrouillage optimiste est préférable pour les systèmes à forte lecture, tandis que le verrouillage pessimiste convient mieux aux environnements à écriture intensive.

Étiquettes: Django e-commerce transaction verrouillage commande

Publié le 10 juillet à 16h54