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
180 lines
5.8 KiB
Python
180 lines
5.8 KiB
Python
from decimal import Decimal
|
||
from typing import Any
|
||
|
||
from django.conf import settings
|
||
from django.core.exceptions import ValidationError
|
||
from django.core.mail import EmailMultiAlternatives
|
||
from django.db import transaction
|
||
from django.template.loader import render_to_string
|
||
from django.utils import formats
|
||
|
||
from items.models import Item
|
||
from orders.models import Cart, CartItem, Order, OrderItem
|
||
|
||
|
||
DELIVERY_PRICE = Decimal("500.00")
|
||
FLOOR_LIFT_PRICE = Decimal("45.00")
|
||
ZERO_MONEY = Decimal("0.00")
|
||
|
||
|
||
def format_currency(value: Decimal) -> str:
|
||
return f"{formats.number_format(value, 2)} руб."
|
||
|
||
|
||
def calculate_checkout_totals(cart_items: list[CartItem], delivery_method: str) -> dict[str, Decimal]:
|
||
products_total = sum(
|
||
(cart_item.item.price * cart_item.quantity for cart_item in cart_items),
|
||
start=ZERO_MONEY,
|
||
)
|
||
discount_total = ZERO_MONEY
|
||
delivery_total = DELIVERY_PRICE if delivery_method == Order.DeliveryMethod.COURIER else ZERO_MONEY
|
||
floor_lift_total = FLOOR_LIFT_PRICE if delivery_method == Order.DeliveryMethod.COURIER else ZERO_MONEY
|
||
total = products_total - discount_total + delivery_total + floor_lift_total
|
||
|
||
return {
|
||
"products_total": products_total,
|
||
"discount_total": discount_total,
|
||
"delivery_total": delivery_total,
|
||
"floor_lift_total": floor_lift_total,
|
||
"total": total,
|
||
}
|
||
|
||
|
||
def get_or_create_cart(user: Any) -> Cart:
|
||
cart, _ = Cart.objects.get_or_create(user=user)
|
||
return cart
|
||
|
||
|
||
def get_user_cart_item_ids(user: Any) -> set[int]:
|
||
if not getattr(user, "is_authenticated", False):
|
||
return set()
|
||
|
||
return set(
|
||
CartItem.objects.filter(cart__user=user).values_list("item_id", flat=True)
|
||
)
|
||
|
||
|
||
def get_user_cart(user: Any) -> Cart | None:
|
||
if not getattr(user, "is_authenticated", False):
|
||
return None
|
||
|
||
return (
|
||
Cart.objects.filter(user=user)
|
||
.prefetch_related("items__item__images")
|
||
.select_related("user")
|
||
.first()
|
||
)
|
||
|
||
|
||
def get_user_cart_items(user: Any) -> list[CartItem]:
|
||
cart = get_user_cart(user)
|
||
if not cart:
|
||
return []
|
||
|
||
return list(
|
||
cart.items.select_related("item__category", "item__subcategory").prefetch_related("item__images")
|
||
)
|
||
|
||
|
||
def add_item_to_cart(user: Any, item: Item, quantity: int = 1) -> tuple[CartItem, bool]:
|
||
if quantity < 1:
|
||
raise ValidationError("Количество товара должно быть не меньше 1.")
|
||
|
||
if not item.is_available:
|
||
raise ValidationError("Недоступный товар нельзя добавить в корзину.")
|
||
|
||
cart = get_or_create_cart(user)
|
||
cart_item, created = CartItem.objects.get_or_create(
|
||
cart=cart,
|
||
item=item,
|
||
defaults={"quantity": quantity},
|
||
)
|
||
return cart_item, created
|
||
|
||
|
||
def remove_item_from_cart(user: Any, item_id: int) -> bool:
|
||
deleted, _ = CartItem.objects.filter(cart__user=user, item_id=item_id).delete()
|
||
return bool(deleted)
|
||
|
||
|
||
def update_cart_item_quantity(user: Any, item_id: int, quantity: int) -> CartItem:
|
||
if quantity < 1:
|
||
raise ValidationError("Количество товара должно быть не меньше 1.")
|
||
|
||
cart_item = CartItem.objects.select_related("item").get(cart__user=user, item_id=item_id)
|
||
cart_item.quantity = quantity
|
||
cart_item.save(update_fields=["quantity", "updated_at"])
|
||
return cart_item
|
||
|
||
|
||
@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("Корзина пуста.")
|
||
|
||
totals = calculate_checkout_totals(cart_items, cleaned_data["delivery_method"])
|
||
order = Order.objects.create(
|
||
user=user,
|
||
customer_name=cleaned_data["customer_name"],
|
||
phone=cleaned_data["phone"],
|
||
email=cleaned_data["email"],
|
||
delivery_method=cleaned_data["delivery_method"],
|
||
region=cleaned_data.get("region", ""),
|
||
address=cleaned_data.get("address", ""),
|
||
payment_method=cleaned_data["payment_method"],
|
||
promo_code=cleaned_data.get("promo_code", ""),
|
||
products_total=totals["products_total"],
|
||
discount_total=totals["discount_total"],
|
||
delivery_total=totals["delivery_total"],
|
||
floor_lift_total=totals["floor_lift_total"],
|
||
total=totals["total"],
|
||
status=Order.Status.PAID,
|
||
)
|
||
|
||
OrderItem.objects.bulk_create(
|
||
[
|
||
OrderItem(
|
||
order=order,
|
||
item=cart_item.item,
|
||
item_name=cart_item.item.name,
|
||
item_slug=cart_item.item.slug,
|
||
item_price=cart_item.item.price,
|
||
quantity=cart_item.quantity,
|
||
line_total=cart_item.item.price * cart_item.quantity,
|
||
)
|
||
for cart_item in cart_items
|
||
]
|
||
)
|
||
cart.items.all().delete()
|
||
return order
|
||
|
||
|
||
def send_order_confirmation_email(order: Order) -> None:
|
||
order = (
|
||
Order.objects.select_related("user")
|
||
.prefetch_related("items")
|
||
.get(pk=order.pk)
|
||
)
|
||
context = {
|
||
"order": order,
|
||
"site_name": getattr(settings, "SITE_NAME", "SantehRay"),
|
||
}
|
||
subject = render_to_string("orders/emails/order_confirmation_subject.txt", context).strip()
|
||
text_body = render_to_string("orders/emails/order_confirmation_email.txt", context)
|
||
html_body = render_to_string("orders/emails/order_confirmation_email.html", context)
|
||
|
||
message = EmailMultiAlternatives(
|
||
subject=subject,
|
||
body=text_body,
|
||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||
to=[order.email],
|
||
bcc=[settings.ADMINISTRATOR_EMAIL],
|
||
)
|
||
message.attach_alternative(html_body, "text/html")
|
||
message.send()
|