feat: implement order management and checkout functionality
All checks were successful
Build and Push Docker Image / build (push) Successful in 50s
All checks were successful
Build and Push Docker Image / build (push) Successful in 50s
- Added `Order` and `OrderItem` models with migrations for managing orders and their details - Created `CheckoutForm` for validating order data and customer information - Built templates for order confirmation, checkout success, and emails (HTML and plain text) - Added logic for email notifications upon successful order placement - Enhanced shopping cart template with checkout panel integration - Updated styles and JavaScript for streamlined checkout UX - Refactored cart view to support item summaries and final totals during checkout
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{% extends "_layouts/base.html" %}
|
||||
{% load static %}
|
||||
{% load static l10n %}
|
||||
|
||||
{% block title %}Корзина{% endblock %}
|
||||
|
||||
@@ -13,100 +13,211 @@
|
||||
{% include '_fragments/breadcrumbs.html' with url_1='/' text_1='Главная' url_2='/orders/cart/' text_2='Корзина' %}
|
||||
|
||||
<main class="container cart-page">
|
||||
<section class="cart-hero">
|
||||
<p class="cart-kicker">Покупки</p>
|
||||
<div class="cart-hero__row">
|
||||
<div>
|
||||
<h1 class="cart-title">Корзина</h1>
|
||||
<p class="cart-description">
|
||||
Проверьте выбранные позиции, измените количество или удалите лишнее перед оформлением.
|
||||
</p>
|
||||
</div>
|
||||
<div class="cart-summary-card">
|
||||
<span class="cart-summary-card__value" data-cart-total-quantity>{{ total_quantity }}</span>
|
||||
<span class="cart-summary-card__label">товаров в корзине</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% if cart_items %}
|
||||
<div class="checkout-layout">
|
||||
<section class="cart-products" aria-label="Товары в корзине">
|
||||
<h1 class="cart-section-title">ТОВАРЫ</h1>
|
||||
|
||||
<section class="cart-content" aria-label="Содержимое корзины">
|
||||
{% if cart_items %}
|
||||
<div class="cart-items">
|
||||
{% for cart_item in cart_items %}
|
||||
{% with item=cart_item.item %}
|
||||
<article class="cart-item" data-auth-cart-item data-current-quantity="{{ cart_item.quantity }}">
|
||||
<a class="cart-item__media" href="{% url 'detail' item.category.slug item.slug %}">
|
||||
{% with primary_image=item.primary_image %}
|
||||
{% if primary_image %}
|
||||
<img src="{{ primary_image.image.url }}" alt="{{ primary_image.alt|default:item.name }}">
|
||||
{% else %}
|
||||
<div class="cart-item__placeholder">Нет фото</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</a>
|
||||
<div class="cart-items">
|
||||
{% for cart_item in cart_items %}
|
||||
{% with item=cart_item.item %}
|
||||
<article class="cart-item" data-auth-cart-item data-current-quantity="{{ cart_item.quantity }}">
|
||||
<a class="cart-item__media" href="{% url 'detail' item.category.slug item.slug %}">
|
||||
{% with primary_image=item.primary_image %}
|
||||
{% if primary_image %}
|
||||
<img src="{{ primary_image.image.url }}" alt="{{ primary_image.alt|default:item.name }}">
|
||||
{% else %}
|
||||
<span class="cart-item__placeholder">Нет фото</span>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</a>
|
||||
|
||||
<div class="cart-item__body">
|
||||
<div class="cart-item__main">
|
||||
<p class="cart-item__category">
|
||||
{{ item.category.name }}{% if item.subcategory %} / {{ item.subcategory.name }}{% endif %}
|
||||
</p>
|
||||
<div class="cart-item__info">
|
||||
<a class="cart-item__title" href="{% url 'detail' item.category.slug item.slug %}">{{ item.name }}</a>
|
||||
<p class="cart-item__price">{{ item.price }} руб.</p>
|
||||
</div>
|
||||
|
||||
<div class="cart-item__controls">
|
||||
<div class="cart-quantity">
|
||||
<button
|
||||
class="cart-quantity__button"
|
||||
type="button"
|
||||
data-cart-decrement
|
||||
data-remove-form-id="cart-remove-{{ cart_item.id }}"
|
||||
data-update-form-id="cart-decrement-{{ cart_item.id }}"
|
||||
data-current-quantity="{{ cart_item.quantity }}"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span class="cart-quantity__value">{{ cart_item.quantity }}</span>
|
||||
<form id="cart-increment-{{ cart_item.id }}" method="post" action="{% url 'orders:update_quantity' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="item_id" value="{{ item.id }}">
|
||||
<input type="hidden" name="quantity" value="{{ cart_item.quantity|add:1 }}">
|
||||
<input type="hidden" name="next" value="{% url 'orders:cart' %}">
|
||||
<button class="cart-quantity__button" type="submit">+</button>
|
||||
</form>
|
||||
<form id="cart-decrement-{{ cart_item.id }}" method="post" action="{% url 'orders:update_quantity' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="item_id" value="{{ item.id }}">
|
||||
<input type="hidden" name="quantity" value="{{ cart_item.quantity|add:-1 }}">
|
||||
<input type="hidden" name="next" value="{% url 'orders:cart' %}">
|
||||
</form>
|
||||
</div>
|
||||
<div class="cart-item__controls">
|
||||
<div class="cart-quantity" aria-label="Количество товара">
|
||||
<button
|
||||
class="cart-quantity__button"
|
||||
type="button"
|
||||
data-cart-decrement
|
||||
data-remove-form-id="cart-remove-{{ cart_item.id }}"
|
||||
data-update-form-id="cart-decrement-{{ cart_item.id }}"
|
||||
data-current-quantity="{{ cart_item.quantity }}"
|
||||
aria-label="Уменьшить количество"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span class="cart-quantity__value">{{ cart_item.quantity }}</span>
|
||||
<form id="cart-increment-{{ cart_item.id }}" method="post" action="{% url 'orders:update_quantity' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="item_id" value="{{ item.id }}">
|
||||
<input type="hidden" name="quantity" value="{{ cart_item.quantity|add:1 }}">
|
||||
<input type="hidden" name="next" value="{% url 'orders:cart' %}">
|
||||
<button class="cart-quantity__button" type="submit" aria-label="Увеличить количество">+</button>
|
||||
</form>
|
||||
<form id="cart-decrement-{{ cart_item.id }}" method="post" action="{% url 'orders:update_quantity' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="item_id" value="{{ item.id }}">
|
||||
<input type="hidden" name="quantity" value="{{ cart_item.quantity|add:-1 }}">
|
||||
<input type="hidden" name="next" value="{% url 'orders:cart' %}">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="cart-item__aside">
|
||||
<span class="cart-item__subtotal">{{ cart_item.subtotal }} руб.</span>
|
||||
<form id="cart-remove-{{ cart_item.id }}" method="post" action="{% url 'orders:remove' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="item_id" value="{{ item.id }}">
|
||||
<input type="hidden" name="next" value="{% url 'orders:cart' %}">
|
||||
<button class="cart-item__remove" type="submit">Удалить</button>
|
||||
<button class="cart-item__remove" type="submit" aria-label="Удалить товар">Удалить</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</article>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<aside class="cart-totals">
|
||||
<p class="cart-totals__label">Итого</p>
|
||||
<p class="cart-totals__value" data-cart-total-amount>{{ total_amount }}</p>
|
||||
<p class="cart-totals__meta">Позиции можно изменить прямо на этой странице.</p>
|
||||
</aside>
|
||||
{% else %}
|
||||
<p class="cart-empty">Корзина пока пуста.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
<div class="cart-products-summary">
|
||||
<p><span>Товары, {{ total_quantity }} шт.</span><strong>{{ products_total }}</strong></p>
|
||||
<p><span>Скидка</span><strong>{{ discount_total }}</strong></p>
|
||||
<p><span>Итого к оплате</span><strong data-checkout-total-text>{{ total_amount }}</strong></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="checkout-panel" aria-label="Оформление заказа">
|
||||
<h2 class="cart-section-title">Оформление заказа</h2>
|
||||
|
||||
<form
|
||||
class="checkout-form"
|
||||
method="post"
|
||||
action="{% url 'orders:checkout' %}"
|
||||
data-checkout-form
|
||||
data-products-total="{{ products_total_value|unlocalize }}"
|
||||
>
|
||||
{% csrf_token %}
|
||||
|
||||
{% if checkout_form.non_field_errors %}
|
||||
<div class="checkout-errors">
|
||||
{% for error in checkout_form.non_field_errors %}
|
||||
<p>{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<fieldset class="checkout-fieldset">
|
||||
<legend>СПОСОБ ДОСТАВКИ</legend>
|
||||
<div class="checkout-options checkout-options--two">
|
||||
<label class="checkout-radio">
|
||||
<input type="radio" name="delivery_method" value="courier" {% if checkout_form.delivery_method.value == "courier" or not checkout_form.is_bound %}checked{% endif %}>
|
||||
<span>Курьерская доставка</span>
|
||||
</label>
|
||||
<label class="checkout-radio">
|
||||
<input type="radio" name="delivery_method" value="pickup" {% if checkout_form.delivery_method.value == "pickup" %}checked{% endif %}>
|
||||
<span>Самовывоз</span>
|
||||
</label>
|
||||
</div>
|
||||
{% for error in checkout_form.delivery_method.errors %}<p class="field-error">{{ error }}</p>{% endfor %}
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="checkout-fieldset">
|
||||
<legend>АДРЕС ДОСТАВКИ</legend>
|
||||
<div class="checkout-grid">
|
||||
<label class="checkout-input checkout-input--short">
|
||||
<span>Регион*</span>
|
||||
<input type="text" name="region" value="{{ checkout_form.region.value|default:'' }}" placeholder="Регион*" autocomplete="address-level1">
|
||||
</label>
|
||||
<label class="checkout-input checkout-input--wide">
|
||||
<span>Улица, дом, квартира*</span>
|
||||
<input type="text" name="address" value="{{ checkout_form.address.value|default:'' }}" placeholder="Улица, дом, квартира*" autocomplete="street-address">
|
||||
</label>
|
||||
</div>
|
||||
{% for error in checkout_form.region.errors %}<p class="field-error">{{ error }}</p>{% endfor %}
|
||||
{% for error in checkout_form.address.errors %}<p class="field-error">{{ error }}</p>{% endfor %}
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="checkout-fieldset">
|
||||
<legend>СПОСОБ ОПЛАТЫ</legend>
|
||||
<div class="checkout-options checkout-options--three">
|
||||
<label class="checkout-radio">
|
||||
<input type="radio" name="payment_method" value="card" {% if checkout_form.payment_method.value == "card" or not checkout_form.is_bound %}checked{% endif %}>
|
||||
<span>Банковская карта</span>
|
||||
</label>
|
||||
<label class="checkout-radio">
|
||||
<input type="radio" name="payment_method" value="credit" {% if checkout_form.payment_method.value == "credit" %}checked{% endif %}>
|
||||
<span>Кредит</span>
|
||||
</label>
|
||||
<label class="checkout-radio">
|
||||
<input type="radio" name="payment_method" value="installment" {% if checkout_form.payment_method.value == "installment" %}checked{% endif %}>
|
||||
<span>Рассрочка</span>
|
||||
</label>
|
||||
</div>
|
||||
{% for error in checkout_form.payment_method.errors %}<p class="field-error">{{ error }}</p>{% endfor %}
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="checkout-fieldset">
|
||||
<legend>КОНТАКТЫ</legend>
|
||||
<div class="checkout-grid checkout-grid--contacts">
|
||||
<label class="checkout-input checkout-input--wide">
|
||||
<span>ФИО*</span>
|
||||
<input type="text" name="customer_name" value="{{ checkout_form.customer_name.value|default:'' }}" placeholder="ФИО*" autocomplete="name">
|
||||
</label>
|
||||
<label class="checkout-input">
|
||||
<span>Телефон*</span>
|
||||
<input type="tel" name="phone" value="{{ checkout_form.phone.value|default:'' }}" placeholder="Телефон*" autocomplete="tel">
|
||||
</label>
|
||||
<label class="checkout-input">
|
||||
<span>E-mail*</span>
|
||||
<input type="email" name="email" value="{{ checkout_form.email.value|default:'' }}" placeholder="E-mail*" autocomplete="email">
|
||||
</label>
|
||||
</div>
|
||||
{% for error in checkout_form.customer_name.errors %}<p class="field-error">{{ error }}</p>{% endfor %}
|
||||
{% for error in checkout_form.phone.errors %}<p class="field-error">{{ error }}</p>{% endfor %}
|
||||
{% for error in checkout_form.email.errors %}<p class="field-error">{{ error }}</p>{% endfor %}
|
||||
|
||||
<label class="checkout-checkbox">
|
||||
<input type="checkbox" name="recipient_matches_customer" value="on" {% if checkout_form.recipient_matches_customer.value or not checkout_form.is_bound %}checked{% endif %}>
|
||||
<span>ФИО получателя совпадает с ФИО покупателя</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<div class="promo-row">
|
||||
<label class="checkout-input">
|
||||
<span>Промокод</span>
|
||||
<input type="text" name="promo_code" value="{{ checkout_form.promo_code.value|default:'' }}" placeholder="Промокод">
|
||||
</label>
|
||||
<button type="button">ПРИМЕНИТЬ</button>
|
||||
</div>
|
||||
|
||||
<div class="checkout-summary">
|
||||
<p><strong>Товары, {{ total_quantity }} шт.</strong><span>{{ products_total }}</span></p>
|
||||
<p><strong>Скидка</strong><span>{{ discount_total }}</span></p>
|
||||
<p><strong>Итого к оплате</strong><span data-checkout-total-text>{{ total_amount }}</span></p>
|
||||
</div>
|
||||
|
||||
<button class="checkout-submit" type="submit">ОФОРМИТЬ ЗАКАЗ</button>
|
||||
|
||||
<label class="checkout-checkbox checkout-checkbox--terms">
|
||||
<input type="checkbox" name="terms_accepted" value="on" {% if checkout_form.terms_accepted.value %}checked{% endif %}>
|
||||
<span>Согласен с условиями продажи, оплаты и возврата</span>
|
||||
</label>
|
||||
{% for error in checkout_form.terms_accepted.errors %}<p class="field-error">{{ error }}</p>{% endfor %}
|
||||
|
||||
<div class="checkout-services">
|
||||
<h3>Услуги</h3>
|
||||
<p><span>Доставка</span><strong data-delivery-total-text>{{ delivery_total }}</strong></p>
|
||||
<p><span>Подъём на этаж</span><strong data-floor-lift-total-text>{{ floor_lift_total }}</strong></p>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
{% else %}
|
||||
<section class="cart-empty">
|
||||
<h1>Корзина пуста</h1>
|
||||
<p>Добавьте товары из каталога, чтобы оформить заказ.</p>
|
||||
<a href="/catalog/">Перейти в каталог</a>
|
||||
</section>
|
||||
{% endif %}
|
||||
</main>
|
||||
|
||||
{% include "_fragments/footer.html" %}
|
||||
|
||||
38
templates/orders/emails/order_confirmation_email.html
Normal file
38
templates/orders/emails/order_confirmation_email.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<div style="max-width:640px;margin:0 auto;background:#ffffff;border:1px solid #d6dde6;padding:32px;font-family:Arial,sans-serif;color:#36213e;">
|
||||
<h1 style="margin:0 0 16px;font-size:28px;">Заказ №{{ order.id }} оплачен</h1>
|
||||
<p>Здравствуйте, {{ order.customer_name }}!</p>
|
||||
<p>Ваш заказ успешно оформлен и оплачен.</p>
|
||||
|
||||
<table style="width:100%;border-collapse:collapse;margin:24px 0;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:left;border-bottom:1px solid #d6dde6;padding:8px;">Товар</th>
|
||||
<th style="text-align:right;border-bottom:1px solid #d6dde6;padding:8px;">Кол-во</th>
|
||||
<th style="text-align:right;border-bottom:1px solid #d6dde6;padding:8px;">Сумма</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in order.items.all %}
|
||||
<tr>
|
||||
<td style="padding:8px;border-bottom:1px solid #eef2f6;">{{ item.item_name }}</td>
|
||||
<td style="padding:8px;border-bottom:1px solid #eef2f6;text-align:right;">{{ item.quantity }}</td>
|
||||
<td style="padding:8px;border-bottom:1px solid #eef2f6;text-align:right;">{{ item.line_total }} руб.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p><strong>Товары:</strong> {{ order.products_total }} руб.</p>
|
||||
<p><strong>Скидка:</strong> {{ order.discount_total }} руб.</p>
|
||||
<p><strong>Доставка:</strong> {{ order.delivery_total }} руб.</p>
|
||||
<p><strong>Подъём на этаж:</strong> {{ order.floor_lift_total }} руб.</p>
|
||||
<p style="font-size:18px;"><strong>Итого к оплате:</strong> {{ order.total }} руб.</p>
|
||||
|
||||
<p><strong>Доставка:</strong> {{ order.get_delivery_method_display }}</p>
|
||||
{% if order.region or order.address %}
|
||||
<p><strong>Адрес:</strong> {{ order.region }}{% if order.address %}, {{ order.address }}{% endif %}</p>
|
||||
{% endif %}
|
||||
<p><strong>Оплата:</strong> {{ order.get_payment_method_display }}</p>
|
||||
|
||||
<p style="margin-top:24px;color:#554971;">Спасибо за покупку в {{ site_name }}.</p>
|
||||
</div>
|
||||
25
templates/orders/emails/order_confirmation_email.txt
Normal file
25
templates/orders/emails/order_confirmation_email.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
Здравствуйте, {{ order.customer_name }}!
|
||||
|
||||
Ваш заказ №{{ order.id }} успешно оплачен.
|
||||
|
||||
Состав заказа:
|
||||
{% for item in order.items.all %}
|
||||
- {{ item.item_name }} — {{ item.quantity }} шт. × {{ item.item_price }} руб. = {{ item.line_total }} руб.
|
||||
{% endfor %}
|
||||
|
||||
Товары: {{ order.products_total }} руб.
|
||||
Скидка: {{ order.discount_total }} руб.
|
||||
Доставка: {{ order.delivery_total }} руб.
|
||||
Подъём на этаж: {{ order.floor_lift_total }} руб.
|
||||
Итого к оплате: {{ order.total }} руб.
|
||||
|
||||
Доставка: {{ order.get_delivery_method_display }}
|
||||
{% if order.region or order.address %}Адрес: {{ order.region }}{% if order.address %}, {{ order.address }}{% endif %}
|
||||
{% endif %}Оплата: {{ order.get_payment_method_display }}
|
||||
|
||||
Контакты:
|
||||
ФИО: {{ order.customer_name }}
|
||||
Телефон: {{ order.phone }}
|
||||
E-mail: {{ order.email }}
|
||||
|
||||
Спасибо за покупку в {{ site_name }}.
|
||||
1
templates/orders/emails/order_confirmation_subject.txt
Normal file
1
templates/orders/emails/order_confirmation_subject.txt
Normal file
@@ -0,0 +1 @@
|
||||
{{ site_name }}: заказ №{{ order.id }} оплачен
|
||||
53
templates/orders/success.html
Normal file
53
templates/orders/success.html
Normal file
@@ -0,0 +1,53 @@
|
||||
{% extends "_layouts/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Оплата прошла успешно{% endblock %}
|
||||
|
||||
{% block add_css %}
|
||||
<link rel="stylesheet" href="{% static "css/cart.css" %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "_fragments/header.html" %}
|
||||
|
||||
{% include '_fragments/breadcrumbs.html' with url_1='/' text_1='Главная' url_2='/orders/cart/' text_2='Корзина' url_3=request.path text_3='Заказ оплачен' %}
|
||||
|
||||
<main class="container cart-page">
|
||||
<section class="order-success">
|
||||
<p class="order-success__eyebrow">Оплата прошла успешно</p>
|
||||
<h1>Заказ №{{ order.id }} оформлен</h1>
|
||||
<p class="order-success__lead">
|
||||
Мы отправили письмо с деталями заказа на {{ order.email }}. Менеджер свяжется с вами при необходимости.
|
||||
</p>
|
||||
|
||||
<div class="order-success__summary">
|
||||
<div>
|
||||
<span>Получатель</span>
|
||||
<strong>{{ order.customer_name }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Способ доставки</span>
|
||||
<strong>{{ order.get_delivery_method_display }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Способ оплаты</span>
|
||||
<strong>{{ order.get_payment_method_display }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Итого</span>
|
||||
<strong>{{ order.total }} руб.</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="order-success__items">
|
||||
{% for item in order.items.all %}
|
||||
<p><span>{{ item.item_name }} × {{ item.quantity }}</span><strong>{{ item.line_total }} руб.</strong></p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<a class="order-success__link" href="/catalog/">Вернуться в каталог</a>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{% include "_fragments/footer.html" %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user