Compare commits
2 Commits
e9da339a4c
...
a84b9af926
| Author | SHA1 | Date | |
|---|---|---|---|
| a84b9af926 | |||
| f029e11175 |
2
.env
2
.env
@@ -1,5 +1,5 @@
|
|||||||
SECRET_KEY=replace-me
|
SECRET_KEY=replace-me
|
||||||
DEBUG=false
|
DEBUG=true
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
WHITENOISE_USE_MANIFEST=false
|
WHITENOISE_USE_MANIFEST=false
|
||||||
WHITENOISE_MANIFEST_STRICT=false
|
WHITENOISE_MANIFEST_STRICT=false
|
||||||
|
|||||||
BIN
Baymax.otf
Normal file
BIN
Baymax.otf
Normal file
Binary file not shown.
BIN
ST-Nizhegorodsky.otf
Normal file
BIN
ST-Nizhegorodsky.otf
Normal file
Binary file not shown.
BIN
Waterway Display Font.ttf
Normal file
BIN
Waterway Display Font.ttf
Normal file
Binary file not shown.
@@ -381,6 +381,13 @@ class CatalogViewTests(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertNotContains(response, "data-guest-cart-detail-button", html=False)
|
self.assertNotContains(response, "data-guest-cart-detail-button", html=False)
|
||||||
|
|
||||||
|
def test_detail_page_guest_buy_now_link_does_not_require_login(self):
|
||||||
|
response = self.client.get("/catalog/plumbing/kitchen-sink/")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, f'href="/orders/buy/{self.sink.id}/"', html=False)
|
||||||
|
self.assertContains(response, "Купить")
|
||||||
|
|
||||||
def test_header_cart_link_redirects_guest_to_login(self):
|
def test_header_cart_link_redirects_guest_to_login(self):
|
||||||
response = self.client.get("/catalog/")
|
response = self.client.get("/catalog/")
|
||||||
|
|
||||||
|
|||||||
28
orders/migrations/0005_alter_order_user.py
Normal file
28
orders/migrations/0005_alter_order_user.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django on 2026-06-15
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("orders", "0004_alter_order_payment_method"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="order",
|
||||||
|
name="user",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="orders",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="Пользователь",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -79,8 +79,10 @@ class Order(models.Model):
|
|||||||
|
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.SET_NULL,
|
||||||
related_name="orders",
|
related_name="orders",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
verbose_name="Пользователь",
|
verbose_name="Пользователь",
|
||||||
)
|
)
|
||||||
customer_name = models.CharField(max_length=255, verbose_name="ФИО")
|
customer_name = models.CharField(max_length=255, verbose_name="ФИО")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Any
|
from typing import Any, Sequence
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@@ -13,18 +13,26 @@ from items.models import Item
|
|||||||
from orders.models import Cart, CartItem, Order, OrderItem
|
from orders.models import Cart, CartItem, Order, OrderItem
|
||||||
|
|
||||||
|
|
||||||
DELIVERY_PRICE = Decimal("500.00")
|
DELIVERY_PRICE = Decimal("50.00")
|
||||||
FLOOR_LIFT_PRICE = Decimal("45.00")
|
FLOOR_LIFT_PRICE = Decimal("45.00")
|
||||||
ZERO_MONEY = Decimal("0.00")
|
ZERO_MONEY = Decimal("0.00")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CheckoutLine:
|
||||||
|
item: Item
|
||||||
|
quantity: int = 1
|
||||||
|
selected_color: Any = None
|
||||||
|
id: str = "direct"
|
||||||
|
|
||||||
|
|
||||||
def format_currency(value: Decimal) -> str:
|
def format_currency(value: Decimal) -> str:
|
||||||
return f"{formats.number_format(value, 2)} руб."
|
return f"{formats.number_format(value, 2)} руб."
|
||||||
|
|
||||||
|
|
||||||
def calculate_checkout_totals(cart_items: list[CartItem], delivery_method: str) -> dict[str, Decimal]:
|
def calculate_checkout_totals(checkout_items: Sequence[Any], delivery_method: str) -> dict[str, Decimal]:
|
||||||
products_total = sum(
|
products_total = sum(
|
||||||
(cart_item.item.price * cart_item.quantity for cart_item in cart_items),
|
(checkout_item.item.price * checkout_item.quantity for checkout_item in checkout_items),
|
||||||
start=ZERO_MONEY,
|
start=ZERO_MONEY,
|
||||||
)
|
)
|
||||||
discount_total = ZERO_MONEY
|
discount_total = ZERO_MONEY
|
||||||
@@ -114,18 +122,18 @@ def update_cart_item_quantity(user: Any, item_id: int, quantity: int) -> CartIte
|
|||||||
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create_order_from_cart(user: Any, cleaned_data: dict[str, Any]) -> Order:
|
def create_order_from_checkout_items(
|
||||||
cart = get_user_cart(user)
|
user: Any | None,
|
||||||
if not cart:
|
checkout_items: Sequence[Any],
|
||||||
raise ValidationError("Корзина пуста.")
|
cleaned_data: dict[str, Any],
|
||||||
|
) -> Order:
|
||||||
|
if not checkout_items:
|
||||||
|
raise ValidationError("Нет товаров для оформления заказа.")
|
||||||
|
|
||||||
cart_items = list(cart.items.select_related("item"))
|
order_user = user if getattr(user, "is_authenticated", False) else None
|
||||||
if not cart_items:
|
totals = calculate_checkout_totals(checkout_items, cleaned_data["delivery_method"])
|
||||||
raise ValidationError("Корзина пуста.")
|
|
||||||
|
|
||||||
totals = calculate_checkout_totals(cart_items, cleaned_data["delivery_method"])
|
|
||||||
order = Order.objects.create(
|
order = Order.objects.create(
|
||||||
user=user,
|
user=order_user,
|
||||||
customer_name=cleaned_data["customer_name"],
|
customer_name=cleaned_data["customer_name"],
|
||||||
phone=cleaned_data["phone"],
|
phone=cleaned_data["phone"],
|
||||||
email=cleaned_data["email"],
|
email=cleaned_data["email"],
|
||||||
@@ -153,13 +161,34 @@ def create_order_from_cart(user: Any, cleaned_data: dict[str, Any]) -> Order:
|
|||||||
quantity=cart_item.quantity,
|
quantity=cart_item.quantity,
|
||||||
line_total=cart_item.item.price * cart_item.quantity,
|
line_total=cart_item.item.price * cart_item.quantity,
|
||||||
)
|
)
|
||||||
for cart_item in cart_items
|
for cart_item in checkout_items
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
return order
|
||||||
|
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def create_order_from_cart(user: Any, cleaned_data: dict[str, Any]) -> Order:
|
||||||
|
cart = get_user_cart(user)
|
||||||
|
if not cart:
|
||||||
|
raise ValidationError("Корзина пуста.")
|
||||||
|
|
||||||
|
cart_items = list(cart.items.select_related("item"))
|
||||||
|
if not cart_items:
|
||||||
|
raise ValidationError("Корзина пуста.")
|
||||||
|
|
||||||
|
order = create_order_from_checkout_items(user, cart_items, cleaned_data)
|
||||||
cart.items.all().delete()
|
cart.items.all().delete()
|
||||||
return order
|
return order
|
||||||
|
|
||||||
|
|
||||||
|
def create_order_from_item(user: Any | None, item: Item, cleaned_data: dict[str, Any]) -> Order:
|
||||||
|
if not item.is_available:
|
||||||
|
raise ValidationError("Недоступный товар нельзя оформить.")
|
||||||
|
|
||||||
|
return create_order_from_checkout_items(user, [CheckoutLine(item=item)], cleaned_data)
|
||||||
|
|
||||||
|
|
||||||
def send_order_confirmation_email(order: Order) -> None:
|
def send_order_confirmation_email(order: Order) -> None:
|
||||||
order = (
|
order = (
|
||||||
Order.objects.select_related("user")
|
Order.objects.select_related("user")
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ class CartFlowTests(TestCase):
|
|||||||
self.assertContains(response, "Оформление заказа")
|
self.assertContains(response, "Оформление заказа")
|
||||||
self.assertContains(response, "1600,00 руб.")
|
self.assertContains(response, "1600,00 руб.")
|
||||||
self.assertContains(response, "3200,00 руб.")
|
self.assertContains(response, "3200,00 руб.")
|
||||||
self.assertContains(response, "3745,00 руб.")
|
self.assertContains(response, "3295,00 руб.")
|
||||||
self.assertContains(response, "/media/items/cart-item.jpg")
|
self.assertContains(response, "/media/items/cart-item.jpg")
|
||||||
self.assertNotContains(response, 'name="promo_code"', html=False)
|
self.assertNotContains(response, 'name="promo_code"', html=False)
|
||||||
|
|
||||||
@@ -145,9 +145,9 @@ class CartFlowTests(TestCase):
|
|||||||
self.assertEqual(order.status, Order.Status.PAID)
|
self.assertEqual(order.status, Order.Status.PAID)
|
||||||
self.assertEqual(order.products_total, 3200)
|
self.assertEqual(order.products_total, 3200)
|
||||||
self.assertEqual(order.discount_total, 0)
|
self.assertEqual(order.discount_total, 0)
|
||||||
self.assertEqual(order.delivery_total, 500)
|
self.assertEqual(order.delivery_total, 50)
|
||||||
self.assertEqual(order.floor_lift_total, 45)
|
self.assertEqual(order.floor_lift_total, 45)
|
||||||
self.assertEqual(order.total, 3745)
|
self.assertEqual(order.total, 3295)
|
||||||
self.assertEqual(order.promo_code, "SPRING")
|
self.assertEqual(order.promo_code, "SPRING")
|
||||||
self.assertFalse(CartItem.objects.filter(cart=cart).exists())
|
self.assertFalse(CartItem.objects.filter(cart=cart).exists())
|
||||||
|
|
||||||
@@ -202,6 +202,79 @@ class CartFlowTests(TestCase):
|
|||||||
self.assertEqual(order.floor_lift_total, 0)
|
self.assertEqual(order.floor_lift_total, 0)
|
||||||
self.assertEqual(order.total, 1600)
|
self.assertEqual(order.total, 1600)
|
||||||
|
|
||||||
|
def test_buy_now_page_is_available_to_guest(self):
|
||||||
|
response = self.client.get(reverse("orders:buy_now", kwargs={"item_id": self.item.id}))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "Оформление заказа")
|
||||||
|
self.assertContains(response, "Кухонная мойка")
|
||||||
|
self.assertContains(response, f'action="{reverse("orders:buy_now", kwargs={"item_id": self.item.id})}"', html=False)
|
||||||
|
self.assertNotContains(response, "cart-remove-", html=False)
|
||||||
|
|
||||||
|
def test_buy_now_rejects_unavailable_item(self):
|
||||||
|
response = self.client.get(reverse("orders:buy_now", kwargs={"item_id": self.unavailable_item.id}))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_buy_now_creates_guest_order_without_cart_and_sends_email(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("orders:buy_now", kwargs={"item_id": self.item.id}),
|
||||||
|
data=self.checkout_data(),
|
||||||
|
)
|
||||||
|
|
||||||
|
order = Order.objects.get()
|
||||||
|
self.assertRedirects(response, reverse("orders:success", kwargs={"order_id": order.id}))
|
||||||
|
self.assertIsNone(order.user)
|
||||||
|
self.assertEqual(order.products_total, 1600)
|
||||||
|
self.assertEqual(order.delivery_total, 50)
|
||||||
|
self.assertEqual(order.floor_lift_total, 45)
|
||||||
|
self.assertEqual(order.total, 1695)
|
||||||
|
self.assertFalse(Cart.objects.exists())
|
||||||
|
|
||||||
|
order_item = OrderItem.objects.get(order=order)
|
||||||
|
self.assertEqual(order_item.item_name, "Кухонная мойка")
|
||||||
|
self.assertEqual(order_item.quantity, 1)
|
||||||
|
self.assertEqual(order_item.line_total, 1600)
|
||||||
|
|
||||||
|
self.assertIn(order.id, self.client.session["guest_order_ids"])
|
||||||
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
|
self.assertEqual(mail.outbox[0].to, ["buyer@example.com"])
|
||||||
|
|
||||||
|
def test_guest_success_page_requires_session_order_access(self):
|
||||||
|
order = Order.objects.create(
|
||||||
|
user=None,
|
||||||
|
customer_name="Иван Петров",
|
||||||
|
phone="+375296881819",
|
||||||
|
email="buyer@example.com",
|
||||||
|
delivery_method=Order.DeliveryMethod.COURIER,
|
||||||
|
region="Минск",
|
||||||
|
address="ул. Тимирязева, 123",
|
||||||
|
payment_method=Order.PaymentMethod.CARD,
|
||||||
|
products_total=1600,
|
||||||
|
discount_total=0,
|
||||||
|
delivery_total=50,
|
||||||
|
floor_lift_total=45,
|
||||||
|
total=1695,
|
||||||
|
status=Order.Status.PAID,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("orders:success", kwargs={"order_id": order.id}))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_guest_success_page_allows_session_order_access(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("orders:buy_now", kwargs={"item_id": self.item.id}),
|
||||||
|
data=self.checkout_data(),
|
||||||
|
)
|
||||||
|
order = Order.objects.get()
|
||||||
|
self.assertRedirects(response, reverse("orders:success", kwargs={"order_id": order.id}))
|
||||||
|
|
||||||
|
success_response = self.client.get(reverse("orders:success", kwargs={"order_id": order.id}))
|
||||||
|
|
||||||
|
self.assertEqual(success_response.status_code, 200)
|
||||||
|
self.assertContains(success_response, f"Заказ №{order.id} оформлен")
|
||||||
|
|
||||||
def test_success_page_is_only_available_to_order_owner(self):
|
def test_success_page_is_only_available_to_order_owner(self):
|
||||||
other_user = User.objects.create_user(
|
other_user = User.objects.create_user(
|
||||||
username="other-user",
|
username="other-user",
|
||||||
@@ -220,9 +293,9 @@ class CartFlowTests(TestCase):
|
|||||||
payment_method=Order.PaymentMethod.CARD,
|
payment_method=Order.PaymentMethod.CARD,
|
||||||
products_total=1600,
|
products_total=1600,
|
||||||
discount_total=0,
|
discount_total=0,
|
||||||
delivery_total=500,
|
delivery_total=50,
|
||||||
floor_lift_total=45,
|
floor_lift_total=45,
|
||||||
total=2145,
|
total=1695,
|
||||||
status=Order.Status.PAID,
|
status=Order.Status.PAID,
|
||||||
)
|
)
|
||||||
self.client.force_login(other_user)
|
self.client.force_login(other_user)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ urlpatterns = [
|
|||||||
path("cart/add/", views.add, name="add"),
|
path("cart/add/", views.add, name="add"),
|
||||||
path("cart/remove/", views.remove, name="remove"),
|
path("cart/remove/", views.remove, name="remove"),
|
||||||
path("cart/update-quantity/", views.update_quantity, name="update_quantity"),
|
path("cart/update-quantity/", views.update_quantity, name="update_quantity"),
|
||||||
|
path("buy/<int:item_id>/", views.buy_now, name="buy_now"),
|
||||||
path("checkout/", views.checkout, name="checkout"),
|
path("checkout/", views.checkout, name="checkout"),
|
||||||
path("success/<int:order_id>/", views.success, name="success"),
|
path("success/<int:order_id>/", views.success, name="success"),
|
||||||
]
|
]
|
||||||
|
|||||||
145
orders/views.py
145
orders/views.py
@@ -5,14 +5,16 @@ from django.http import Http404, HttpRequest, HttpResponse
|
|||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.http import url_has_allowed_host_and_scheme
|
from django.utils.http import url_has_allowed_host_and_scheme
|
||||||
from django.views.decorators.http import require_POST
|
from django.views.decorators.http import require_POST, require_http_methods
|
||||||
|
|
||||||
from items.models import Color, Item
|
from items.models import Color, Item
|
||||||
from orders.forms import CheckoutForm
|
from orders.forms import CheckoutForm
|
||||||
from orders.models import CartItem, Order
|
from orders.models import CartItem, Order
|
||||||
from orders.services import (
|
from orders.services import (
|
||||||
|
CheckoutLine,
|
||||||
add_item_to_cart,
|
add_item_to_cart,
|
||||||
calculate_checkout_totals,
|
calculate_checkout_totals,
|
||||||
|
create_order_from_item,
|
||||||
create_order_from_cart,
|
create_order_from_cart,
|
||||||
format_currency,
|
format_currency,
|
||||||
get_user_cart_items,
|
get_user_cart_items,
|
||||||
@@ -22,6 +24,9 @@ from orders.services import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
GUEST_ORDER_SESSION_KEY = "guest_order_ids"
|
||||||
|
|
||||||
|
|
||||||
def resolve_next_url(request: HttpRequest, fallback: str) -> str:
|
def resolve_next_url(request: HttpRequest, fallback: str) -> str:
|
||||||
next_url = request.POST.get("next") or fallback
|
next_url = request.POST.get("next") or fallback
|
||||||
if not url_has_allowed_host_and_scheme(
|
if not url_has_allowed_host_and_scheme(
|
||||||
@@ -33,18 +38,19 @@ def resolve_next_url(request: HttpRequest, fallback: str) -> str:
|
|||||||
return next_url
|
return next_url
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
def build_checkout_context(
|
||||||
def cart(request: HttpRequest) -> HttpResponse:
|
checkout_items: list,
|
||||||
cart_items = get_user_cart_items(request.user)
|
form: CheckoutForm,
|
||||||
total_quantity = sum(cart_item.quantity for cart_item in cart_items)
|
*,
|
||||||
totals = calculate_checkout_totals(cart_items, Order.DeliveryMethod.COURIER)
|
direct_checkout: bool = False,
|
||||||
form = CheckoutForm(initial={
|
direct_item: Item | None = None,
|
||||||
"customer_name": request.user.get_full_name() or request.user.get_username(),
|
) -> dict:
|
||||||
"email": request.user.email,
|
total_quantity = sum(checkout_item.quantity for checkout_item in checkout_items)
|
||||||
})
|
delivery_method = form.data.get("delivery_method") if form.is_bound else Order.DeliveryMethod.COURIER
|
||||||
|
totals = calculate_checkout_totals(checkout_items, delivery_method or Order.DeliveryMethod.COURIER)
|
||||||
|
|
||||||
return render(request, "orders/cart.html", {
|
return {
|
||||||
"cart_items": cart_items,
|
"cart_items": checkout_items,
|
||||||
"checkout_form": form,
|
"checkout_form": form,
|
||||||
"total_quantity": total_quantity,
|
"total_quantity": total_quantity,
|
||||||
"products_total_value": totals["products_total"],
|
"products_total_value": totals["products_total"],
|
||||||
@@ -53,7 +59,36 @@ def cart(request: HttpRequest) -> HttpResponse:
|
|||||||
"delivery_total": format_currency(totals["delivery_total"]),
|
"delivery_total": format_currency(totals["delivery_total"]),
|
||||||
"floor_lift_total": format_currency(totals["floor_lift_total"]),
|
"floor_lift_total": format_currency(totals["floor_lift_total"]),
|
||||||
"total_amount": format_currency(totals["total"]),
|
"total_amount": format_currency(totals["total"]),
|
||||||
})
|
"direct_checkout": direct_checkout,
|
||||||
|
"direct_item": direct_item,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_checkout_initial(user) -> dict:
|
||||||
|
if not getattr(user, "is_authenticated", False):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"customer_name": user.get_full_name() or user.get_username(),
|
||||||
|
"email": user.email,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def remember_guest_order(request: HttpRequest, order: Order) -> None:
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
return
|
||||||
|
|
||||||
|
guest_order_ids = request.session.get(GUEST_ORDER_SESSION_KEY, [])
|
||||||
|
guest_order_ids.append(order.id)
|
||||||
|
request.session[GUEST_ORDER_SESSION_KEY] = guest_order_ids
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def cart(request: HttpRequest) -> HttpResponse:
|
||||||
|
cart_items = get_user_cart_items(request.user)
|
||||||
|
form = CheckoutForm(initial=get_checkout_initial(request.user))
|
||||||
|
|
||||||
|
return render(request, "orders/cart.html", build_checkout_context(cart_items, form))
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@@ -66,20 +101,7 @@ def checkout(request: HttpRequest) -> HttpResponse:
|
|||||||
|
|
||||||
form = CheckoutForm(request.POST)
|
form = CheckoutForm(request.POST)
|
||||||
if not form.is_valid():
|
if not form.is_valid():
|
||||||
total_quantity = sum(cart_item.quantity for cart_item in cart_items)
|
return render(request, "orders/cart.html", build_checkout_context(cart_items, form), status=400)
|
||||||
delivery_method = request.POST.get("delivery_method") or Order.DeliveryMethod.COURIER
|
|
||||||
totals = calculate_checkout_totals(cart_items, delivery_method)
|
|
||||||
return render(request, "orders/cart.html", {
|
|
||||||
"cart_items": cart_items,
|
|
||||||
"checkout_form": form,
|
|
||||||
"total_quantity": total_quantity,
|
|
||||||
"products_total_value": totals["products_total"],
|
|
||||||
"products_total": format_currency(totals["products_total"]),
|
|
||||||
"discount_total": format_currency(totals["discount_total"]),
|
|
||||||
"delivery_total": format_currency(totals["delivery_total"]),
|
|
||||||
"floor_lift_total": format_currency(totals["floor_lift_total"]),
|
|
||||||
"total_amount": format_currency(totals["total"]),
|
|
||||||
}, status=400)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
order = create_order_from_cart(request.user, form.cleaned_data)
|
order = create_order_from_cart(request.user, form.cleaned_data)
|
||||||
@@ -97,16 +119,73 @@ def checkout(request: HttpRequest) -> HttpResponse:
|
|||||||
return redirect("orders:success", order_id=order.id)
|
return redirect("orders:success", order_id=order.id)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def success(request: HttpRequest, order_id: int) -> HttpResponse:
|
def success(request: HttpRequest, order_id: int) -> HttpResponse:
|
||||||
order = get_object_or_404(
|
orders = Order.objects.prefetch_related("items")
|
||||||
Order.objects.prefetch_related("items"),
|
if request.user.is_authenticated:
|
||||||
pk=order_id,
|
order = get_object_or_404(orders, pk=order_id, user=request.user)
|
||||||
user=request.user,
|
else:
|
||||||
)
|
guest_order_ids = request.session.get(GUEST_ORDER_SESSION_KEY, [])
|
||||||
|
if order_id not in guest_order_ids:
|
||||||
|
raise Http404()
|
||||||
|
order = get_object_or_404(orders, pk=order_id, user__isnull=True)
|
||||||
|
|
||||||
return render(request, "orders/success.html", {"order": order})
|
return render(request, "orders/success.html", {"order": order})
|
||||||
|
|
||||||
|
|
||||||
|
@require_http_methods(["GET", "POST"])
|
||||||
|
def buy_now(request: HttpRequest, item_id: int) -> HttpResponse:
|
||||||
|
item = get_object_or_404(
|
||||||
|
Item.objects.select_related("category", "subcategory").prefetch_related("images"),
|
||||||
|
pk=item_id,
|
||||||
|
is_available=True,
|
||||||
|
)
|
||||||
|
checkout_items = [CheckoutLine(item=item)]
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = CheckoutForm(request.POST)
|
||||||
|
if not form.is_valid():
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"orders/cart.html",
|
||||||
|
build_checkout_context(
|
||||||
|
checkout_items,
|
||||||
|
form,
|
||||||
|
direct_checkout=True,
|
||||||
|
direct_item=item,
|
||||||
|
),
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
order = create_order_from_item(request.user, item, form.cleaned_data)
|
||||||
|
except ValidationError as error:
|
||||||
|
messages.error(request, error.message)
|
||||||
|
return redirect("detail", category=item.category.slug, slug=item.slug)
|
||||||
|
|
||||||
|
remember_guest_order(request, order)
|
||||||
|
|
||||||
|
try:
|
||||||
|
send_order_confirmation_email(order)
|
||||||
|
except Exception:
|
||||||
|
messages.error(request, "Заказ создан, но письмо с деталями не удалось отправить. Мы свяжемся с вами.")
|
||||||
|
else:
|
||||||
|
messages.success(request, "Заказ успешно оформлен. Детали отправлены на почту.")
|
||||||
|
|
||||||
|
return redirect("orders:success", order_id=order.id)
|
||||||
|
|
||||||
|
form = CheckoutForm(initial=get_checkout_initial(request.user))
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"orders/cart.html",
|
||||||
|
build_checkout_context(
|
||||||
|
checkout_items,
|
||||||
|
form,
|
||||||
|
direct_checkout=True,
|
||||||
|
direct_item=item,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@require_POST
|
@require_POST
|
||||||
def add(request: HttpRequest) -> HttpResponse:
|
def add(request: HttpRequest) -> HttpResponse:
|
||||||
|
|||||||
BIN
static/img/design-project.jpg
Normal file
BIN
static/img/design-project.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
@@ -24,7 +24,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const productsTotal = Number(checkoutForm.dataset.productsTotal || '0');
|
const productsTotal = Number(checkoutForm.dataset.productsTotal || '0');
|
||||||
const deliveryPrice = 500;
|
const deliveryPrice = 50;
|
||||||
const floorLiftPrice = 45;
|
const floorLiftPrice = 45;
|
||||||
const totalNodes = document.querySelectorAll('[data-checkout-total-text]');
|
const totalNodes = document.querySelectorAll('[data-checkout-total-text]');
|
||||||
const deliveryNode = document.querySelector('[data-delivery-total-text]');
|
const deliveryNode = document.querySelector('[data-delivery-total-text]');
|
||||||
|
|||||||
@@ -106,7 +106,13 @@
|
|||||||
В корзину
|
В корзину
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button class="product-summary__button product-summary__button--secondary" type="button">Купить</button>
|
{% if item.is_available %}
|
||||||
|
<a class="product-summary__button product-summary__button--secondary product-summary__button--link" href="{% url 'orders:buy_now' item.id %}">
|
||||||
|
Купить
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<button class="product-summary__button product-summary__button--secondary" type="button" disabled>Купить</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "_layouts/base.html" %}
|
{% extends "_layouts/base.html" %}
|
||||||
{% load static l10n %}
|
{% load static l10n %}
|
||||||
|
|
||||||
{% block title %}Корзина{% endblock %}
|
{% block title %}{% if direct_checkout %}Оформление заказа{% else %}Корзина{% endif %}{% endblock %}
|
||||||
|
|
||||||
{% block add_css %}
|
{% block add_css %}
|
||||||
<link rel="stylesheet" href="{% static "css/cart.css" %}">
|
<link rel="stylesheet" href="{% static "css/cart.css" %}">
|
||||||
@@ -10,7 +10,11 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
{% include "_fragments/header.html" %}
|
{% include "_fragments/header.html" %}
|
||||||
|
|
||||||
|
{% if direct_checkout %}
|
||||||
|
{% include '_fragments/breadcrumbs.html' with url_1='/' text_1='Главная' url_2=request.path text_2='Оформление заказа' %}
|
||||||
|
{% else %}
|
||||||
{% include '_fragments/breadcrumbs.html' with url_1='/' text_1='Главная' url_2='/orders/cart/' text_2='Корзина' %}
|
{% include '_fragments/breadcrumbs.html' with url_1='/' text_1='Главная' url_2='/orders/cart/' text_2='Корзина' %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<main class="container cart-page">
|
<main class="container cart-page">
|
||||||
{% if cart_items %}
|
{% if cart_items %}
|
||||||
@@ -39,6 +43,13 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<p class="cart-item__price">{{ item.price }} руб.</p>
|
<p class="cart-item__price">{{ item.price }} руб.</p>
|
||||||
|
|
||||||
|
{% if direct_checkout %}
|
||||||
|
<div class="cart-item__controls">
|
||||||
|
<div class="cart-quantity" aria-label="Количество товара">
|
||||||
|
<span class="cart-quantity__value">{{ cart_item.quantity }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
<div class="cart-item__controls">
|
<div class="cart-item__controls">
|
||||||
<div class="cart-quantity" aria-label="Количество товара">
|
<div class="cart-quantity" aria-label="Количество товара">
|
||||||
<button
|
<button
|
||||||
@@ -75,6 +86,7 @@
|
|||||||
<button class="cart-item__remove" type="submit" aria-label="Удалить товар">Удалить</button>
|
<button class="cart-item__remove" type="submit" aria-label="Удалить товар">Удалить</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
@@ -94,7 +106,7 @@
|
|||||||
<form
|
<form
|
||||||
class="checkout-form"
|
class="checkout-form"
|
||||||
method="post"
|
method="post"
|
||||||
action="{% url 'orders:checkout' %}"
|
action="{% if direct_checkout %}{% url 'orders:buy_now' direct_item.id %}{% else %}{% url 'orders:checkout' %}{% endif %}"
|
||||||
data-checkout-form
|
data-checkout-form
|
||||||
data-products-total="{{ products_total_value|unlocalize }}"
|
data-products-total="{{ products_total_value|unlocalize }}"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user