sforkowany z mtyton/comfy
added delivery feature and payment method
rodzic
3580a3b1e1
commit
783e04a134
|
@ -1,6 +1,9 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
from abc import (
|
from abc import (
|
||||||
ABC,
|
ABC,
|
||||||
abstractmethod
|
abstractmethod,
|
||||||
|
abstractproperty
|
||||||
)
|
)
|
||||||
from typing import (
|
from typing import (
|
||||||
List,
|
List,
|
||||||
|
@ -13,9 +16,12 @@ from django.core import signing
|
||||||
|
|
||||||
from store.models import (
|
from store.models import (
|
||||||
Product,
|
Product,
|
||||||
ProductAuthor
|
ProductAuthor,
|
||||||
|
DeliveryMethod
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger("cart_logger")
|
||||||
|
|
||||||
|
|
||||||
class BaseCart(ABC):
|
class BaseCart(ABC):
|
||||||
|
|
||||||
|
@ -34,22 +40,54 @@ class BaseCart(ABC):
|
||||||
def update_item_quantity(self, item_id, change):
|
def update_item_quantity(self, item_id, change):
|
||||||
...
|
...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractproperty
|
||||||
def get_items(self):
|
def display_items(self):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
class SessionCart(BaseCart):
|
class SessionCart(BaseCart):
|
||||||
|
|
||||||
def __init__(self, request: HttpRequest) -> None:
|
|
||||||
|
def _get_author_total_price(self, author_id: int):
|
||||||
|
author_cart = self._cart[str(author_id)]
|
||||||
|
author_price = 0
|
||||||
|
product_ids = list(int(pk) for pk in author_cart.keys())
|
||||||
|
queryset = Product.objects.filter(id__in=product_ids)
|
||||||
|
for product in queryset:
|
||||||
|
author_price += product.price * author_cart[str(product.id)]
|
||||||
|
|
||||||
|
if self._delivery_info:
|
||||||
|
author_price += self._delivery_info.price
|
||||||
|
|
||||||
|
return author_price
|
||||||
|
|
||||||
|
def _prepare_display_items(self)-> List[dict[str, dict|str]]:
|
||||||
|
items: List[dict[str, dict|str]] = []
|
||||||
|
for author_id, cart_items in self._cart.items():
|
||||||
|
author = ProductAuthor.objects.get(id=int(author_id))
|
||||||
|
products = []
|
||||||
|
for item_id, quantity in cart_items.items():
|
||||||
|
product=Product.objects.get(id=int(item_id))
|
||||||
|
products.append({"product": product, "quantity": quantity})
|
||||||
|
items.append({
|
||||||
|
"author": author,
|
||||||
|
"products": products,
|
||||||
|
"group_price": self._get_author_total_price(author_id)
|
||||||
|
})
|
||||||
|
return items
|
||||||
|
|
||||||
|
def __init__(self, request: HttpRequest, delivery: DeliveryMethod=None) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.session = request.session
|
self.session = request.session
|
||||||
self._cart = self.session.get(settings.CART_SESSION_ID, None)
|
self._cart = self.session.get(settings.CART_SESSION_ID, None)
|
||||||
if not self._cart:
|
if not self._cart:
|
||||||
self._cart = {}
|
self._cart = {}
|
||||||
self.session[settings.CART_SESSION_ID] = self._cart
|
self.session[settings.CART_SESSION_ID] = self._cart
|
||||||
|
self._delivery_info = delivery
|
||||||
|
self._display_items = self._prepare_display_items()
|
||||||
|
|
||||||
def save_cart(self):
|
def save_cart(self):
|
||||||
|
self._display_items = self._prepare_display_items()
|
||||||
self.session[settings.CART_SESSION_ID] = self._cart
|
self.session[settings.CART_SESSION_ID] = self._cart
|
||||||
self.session.modified = True
|
self.session.modified = True
|
||||||
|
|
||||||
|
@ -76,8 +114,7 @@ class SessionCart(BaseCart):
|
||||||
self._cart[str(author.id)].pop(str(item_id))
|
self._cart[str(author.id)].pop(str(item_id))
|
||||||
self.save_cart()
|
self.save_cart()
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# TODO - add logging
|
logger.exception(f"Item {item_id} not found in cart")
|
||||||
...
|
|
||||||
|
|
||||||
def update_item_quantity(self, item_id: int, new_quantity: int) -> None:
|
def update_item_quantity(self, item_id: int, new_quantity: int) -> None:
|
||||||
product = self.validate_and_get_product(item_id)
|
product = self.validate_and_get_product(item_id)
|
||||||
|
@ -91,17 +128,14 @@ class SessionCart(BaseCart):
|
||||||
self._cart[str(author.id)][str(product.id)] = new_quantity
|
self._cart[str(author.id)][str(product.id)] = new_quantity
|
||||||
self.save_cart()
|
self.save_cart()
|
||||||
|
|
||||||
def get_items(self) -> List[dict[str, dict|str]]:
|
@property
|
||||||
items: List[dict[str, dict|str]] = []
|
def delivery_info(self):
|
||||||
for author_id, cart_items in self._cart.items():
|
return self._delivery_info
|
||||||
author = ProductAuthor.objects.get(id=int(author_id))
|
|
||||||
products = []
|
|
||||||
for item_id, quantity in cart_items.items():
|
|
||||||
product=Product.objects.get(id=int(item_id))
|
|
||||||
products.append({"product": product, "quantity": quantity})
|
|
||||||
items.append({"author": author, "products": products})
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_items(self) -> List[dict[str, dict|str]]:
|
||||||
|
return self._display_items
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_price(self):
|
def total_price(self):
|
||||||
total = 0
|
total = 0
|
||||||
|
@ -109,6 +143,8 @@ class SessionCart(BaseCart):
|
||||||
for item_id, quantity in cart_items.items():
|
for item_id, quantity in cart_items.items():
|
||||||
product = Product.objects.get(id=int(item_id))
|
product = Product.objects.get(id=int(item_id))
|
||||||
total += product.price * quantity
|
total += product.price * quantity
|
||||||
|
if self._delivery_info:
|
||||||
|
total += self._delivery_info.price * len(self._cart.keys())
|
||||||
return total
|
return total
|
||||||
|
|
||||||
def is_empty(self) -> bool:
|
def is_empty(self) -> bool:
|
||||||
|
|
|
@ -51,11 +51,10 @@ class CustomerDataForm(forms.Form):
|
||||||
widget=forms.Select(attrs={"class": "form-control"})
|
widget=forms.Select(attrs={"class": "form-control"})
|
||||||
)
|
)
|
||||||
|
|
||||||
def clean(self):
|
def serialize(self):
|
||||||
"""Clean method should return JSON serializable"""
|
"""Clean method should return JSON serializable"""
|
||||||
cleaned_data = super().clean()
|
|
||||||
new_cleaned_data = {}
|
new_cleaned_data = {}
|
||||||
for key, value in cleaned_data.items():
|
for key, value in self.cleaned_data.items():
|
||||||
if isinstance(value, PhoneNumber):
|
if isinstance(value, PhoneNumber):
|
||||||
new_cleaned_data[key] = str(value)
|
new_cleaned_data[key] = str(value)
|
||||||
elif isinstance(value, Model):
|
elif isinstance(value, Model):
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-truck" viewBox="0 0 16 16">
|
||||||
|
<path d="M0 3.5A1.5 1.5 0 0 1 1.5 2h9A1.5 1.5 0 0 1 12 3.5V5h1.02a1.5 1.5 0 0 1 1.17.563l1.481 1.85a1.5 1.5 0 0 1 .329.938V10.5a1.5 1.5 0 0 1-1.5 1.5H14a2 2 0 1 1-4 0H5a2 2 0 1 1-3.998-.085A1.5 1.5 0 0 1 0 10.5v-7zm1.294 7.456A1.999 1.999 0 0 1 4.732 11h5.536a2.01 2.01 0 0 1 .732-.732V3.5a.5.5 0 0 0-.5-.5h-9a.5.5 0 0 0-.5.5v7a.5.5 0 0 0 .294.456zM12 10a2 2 0 0 1 1.732 1h.768a.5.5 0 0 0 .5-.5V8.35a.5.5 0 0 0-.11-.312l-1.48-1.85A.5.5 0 0 0 13.02 6H12v4zm-9 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm9 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/>
|
||||||
|
</svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 658 B |
|
@ -9,7 +9,7 @@
|
||||||
<h3 class="fw-normal mb-0 text-black">Koszyk</h3>
|
<h3 class="fw-normal mb-0 text-black">Koszyk</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% for group in cart.get_items %}
|
{% for group in cart.display_items %}
|
||||||
{% if group.products %}
|
{% if group.products %}
|
||||||
<h4>Wykonawca: {{group.author.display_name}}</h4>
|
<h4>Wykonawca: {{group.author.display_name}}</h4>
|
||||||
{% for item in group.products %}
|
{% for item in group.products %}
|
||||||
|
|
|
@ -57,20 +57,28 @@
|
||||||
<h3 class="fw-normal mb-0 text-black">Zamówione przedmioty</h3>
|
<h3 class="fw-normal mb-0 text-black">Zamówione przedmioty</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% for group in cart.get_items %}
|
{% for group in cart.display_items %}
|
||||||
{% if group.products %}
|
{% if group.products %}
|
||||||
<h4>Wykonawca: {{group.author.display_name}}</h4>
|
<h4>Wykonawca: {{group.author.display_name}}</h4>
|
||||||
{% for item in group.products %}
|
{% for item in group.products %}
|
||||||
{% include 'store/partials/summary_cart_item.html' %}
|
{% include 'store/partials/summary_cart_item.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if cart.delivery_info %}
|
||||||
|
{% with delivery=cart.delivery_info %}
|
||||||
|
{% include 'store/partials/delivery_cart_item.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
<div class="col-sm-11 text-end">
|
||||||
|
<h5 class="fw-normal mb-0 pr-3text-black">W sumie: {{group.group_price}} zł</h5>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<div class="card ">
|
<div class="card mt-5">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<h5 class="fw-normal mb-0 text-black">Do zapłaty: {{cart.total_price}}</h5>
|
<h5 class="fw-normal mb-0 text-black">Do zapłaty: {{cart.total_price}} zł</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6 text-end">
|
<div class="col-sm-6 text-end">
|
||||||
<form action="" method="POST">
|
<form action="" method="POST">
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<div class="card rounded-3 mb-1">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="row d-flex justify-content-between align-items-center">
|
||||||
|
<div class="col-md-2 col-lg-2 col-xl-2">
|
||||||
|
<img
|
||||||
|
src="{% static 'images/icons/truck.svg'%}"
|
||||||
|
class="rounded mx-auto d-block">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 col-lg-3 col-xl-3">
|
||||||
|
<p class="lead fw-normal mb-2">{{delivery.name}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 col-lg-3 col-xl-2 d-flex">
|
||||||
|
1
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 col-lg-2 col-xl-2 offset-lg-1">
|
||||||
|
<h5 class="mb-0">{{delivery.price}} zł</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
{{item.quantity}}
|
{{item.quantity}}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 col-lg-2 col-xl-2 offset-lg-1">
|
<div class="col-md-3 col-lg-2 col-xl-2 offset-lg-1">
|
||||||
<h5 class="mb-0">{{item.product.price}} ZŁ</h5>
|
<h5 class="mb-0">{{item.product.price}} zł</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -126,7 +126,7 @@ class ProductTestCase(TestCase):
|
||||||
self.assertIsNotNone(prod)
|
self.assertIsNotNone(prod)
|
||||||
self.assertNotEqual(prod.pk, product.pk)
|
self.assertNotEqual(prod.pk, product.pk)
|
||||||
self.assertFalse(prod.available)
|
self.assertFalse(prod.available)
|
||||||
self.assertEqual(prod.price, 13.0)
|
self.assertEqual(prod.price, 0)
|
||||||
|
|
||||||
def test_get_or_create_by_params_success_not_existing_product_no_other_products(self):
|
def test_get_or_create_by_params_success_not_existing_product_no_other_products(self):
|
||||||
template = factories.ProductTemplateFactory()
|
template = factories.ProductTemplateFactory()
|
||||||
|
|
|
@ -58,7 +58,7 @@ class CartActionView(ViewSet):
|
||||||
def list_products(self, request):
|
def list_products(self, request):
|
||||||
# get cart items
|
# get cart items
|
||||||
cart = SessionCart(self.request)
|
cart = SessionCart(self.request)
|
||||||
items = cart.get_items()
|
items = cart.display_items
|
||||||
serializer = CartSerializer(instance=items, many=True)
|
serializer = CartSerializer(instance=items, many=True)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ class CartActionView(ViewSet):
|
||||||
if not serializer.is_valid():
|
if not serializer.is_valid():
|
||||||
return Response(serializer.errors, status=400)
|
return Response(serializer.errors, status=400)
|
||||||
serializer.save(cart)
|
serializer.save(cart)
|
||||||
items = cart.get_items()
|
items = cart.display_items
|
||||||
serializer = CartSerializer(instance=items, many=True)
|
serializer = CartSerializer(instance=items, many=True)
|
||||||
return Response(serializer.data, status=201)
|
return Response(serializer.data, status=201)
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@ class CartActionView(ViewSet):
|
||||||
except Product.DoesNotExist:
|
except Product.DoesNotExist:
|
||||||
return Response({"error": "Product does not exist"}, status=400)
|
return Response({"error": "Product does not exist"}, status=400)
|
||||||
|
|
||||||
items = cart.get_items()
|
items = cart.display_items
|
||||||
serializer = CartSerializer(instance=items, many=True)
|
serializer = CartSerializer(instance=items, many=True)
|
||||||
return Response(serializer.data, status=201)
|
return Response(serializer.data, status=201)
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ class CartActionView(ViewSet):
|
||||||
cart.update_item_quantity(pk, int(request.data["quantity"]))
|
cart.update_item_quantity(pk, int(request.data["quantity"]))
|
||||||
except Product.DoesNotExist:
|
except Product.DoesNotExist:
|
||||||
return Response({"error": "Product does not exist"}, status=404)
|
return Response({"error": "Product does not exist"}, status=404)
|
||||||
items = cart.get_items()
|
items = cart.display_items
|
||||||
serializer = CartSerializer(instance=items, many=True)
|
serializer = CartSerializer(instance=items, many=True)
|
||||||
return Response(serializer.data, status=201)
|
return Response(serializer.data, status=201)
|
||||||
|
|
||||||
|
@ -176,7 +176,7 @@ class OrderView(View):
|
||||||
context = self.get_context_data()
|
context = self.get_context_data()
|
||||||
context["form"] = form
|
context["form"] = form
|
||||||
return render(request, self.template_name, context)
|
return render(request, self.template_name, context)
|
||||||
customer_data = CustomerData(data=form.cleaned_data)
|
customer_data = CustomerData(data=form.serialize())
|
||||||
request.session["customer_data"] = customer_data.data
|
request.session["customer_data"] = customer_data.data
|
||||||
return HttpResponseRedirect(reverse("order-confirm"))
|
return HttpResponseRedirect(reverse("order-confirm"))
|
||||||
|
|
||||||
|
@ -185,12 +185,19 @@ class OrderConfirmView(View):
|
||||||
template_name = "store/order_confirm.html"
|
template_name = "store/order_confirm.html"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||||
customer_data = CustomerData(
|
|
||||||
encrypted_data=self.request.session["customer_data"]
|
form = CustomerDataForm(
|
||||||
).decrypted_data
|
data=CustomerData(
|
||||||
|
encrypted_data=self.request.session["customer_data"]
|
||||||
|
).decrypted_data
|
||||||
|
)
|
||||||
|
if not form.is_valid():
|
||||||
|
raise Exception("Customer data is not valid")
|
||||||
|
|
||||||
|
customer_data = form.cleaned_data
|
||||||
return {
|
return {
|
||||||
"cart": SessionCart(self.request),
|
"cart": SessionCart(self.request, delivery=customer_data["delivery_method"]),
|
||||||
"customer_data": customer_data
|
"customer_data": customer_data
|
||||||
}
|
}
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
@ -201,10 +208,12 @@ class OrderConfirmView(View):
|
||||||
return render(request, self.template_name, self.get_context_data())
|
return render(request, self.template_name, self.get_context_data())
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
customer_data = request.session["customer_data"]
|
customer_data = CustomerData(
|
||||||
|
encrypted_data=self.request.session["customer_data"]
|
||||||
|
).decrypted_data
|
||||||
cart = SessionCart(self.request)
|
cart = SessionCart(self.request)
|
||||||
order = Order.objects.create_from_cart(
|
order = Order.objects.create_from_cart(
|
||||||
cart.get_items(),
|
cart.display_items,
|
||||||
None, customer_data
|
None, customer_data
|
||||||
)
|
)
|
||||||
request.session.pop("customer_data")
|
request.session.pop("customer_data")
|
||||||
|
|
Ładowanie…
Reference in New Issue