From 34b6eb6bfdf9efa37571e61032158172d914ebdd Mon Sep 17 00:00:00 2001 From: Nathan Chapman Date: Mon, 3 Oct 2022 17:12:45 -0600 Subject: [PATCH] Add basic product variations and categories --- src/accounts/models.py | 7 +- src/core/__init__.py | 17 ++ src/core/admin.py | 2 + src/core/fixtures/orders.json | 6 +- ...t_amount_order_subtotal_amount_and_more.py | 38 ++++ src/core/models.py | 54 +++--- .../templates/dashboard/customer_detail.html | 2 +- .../templates/dashboard/order_detail.html | 6 +- .../templates/dashboard/product_detail.html | 24 ++- .../dashboard/productvariant_create_form.html | 18 ++ .../dashboard/productvariant_detail.html | 0 .../dashboard/productvariant_form.html | 0 src/dashboard/urls.py | 26 +++ src/dashboard/views.py | 56 +++++- src/fixtures/db.json | 144 +++++++------- src/static/styles/main.css | 6 + src/storefront/cart.py | 177 +++++++++--------- src/storefront/forms.py | 31 +-- src/storefront/payments.py | 14 +- .../templates/storefront/cart_detail.html | 30 +-- .../storefront/checkout_shipping_form.html | 15 +- .../templates/storefront/order_detail.html | 6 +- .../templates/storefront/order_form.html | 18 +- .../templates/storefront/product_detail.html | 3 - .../templates/storefront/product_list.html | 2 +- src/storefront/tests/test_cart.py | 2 +- src/storefront/tests/test_views.py | 2 +- src/storefront/urls.py | 9 +- src/storefront/views.py | 117 ++++++------ 29 files changed, 487 insertions(+), 345 deletions(-) create mode 100644 src/core/migrations/0013_rename_total_net_amount_order_subtotal_amount_and_more.py create mode 100644 src/dashboard/templates/dashboard/productvariant_create_form.html create mode 100644 src/dashboard/templates/dashboard/productvariant_detail.html create mode 100644 src/dashboard/templates/dashboard/productvariant_form.html diff --git a/src/accounts/models.py b/src/accounts/models.py index 70b657d..1647c59 100644 --- a/src/accounts/models.py +++ b/src/accounts/models.py @@ -18,7 +18,12 @@ class Address(models.Model): postal_code = models.CharField(max_length=20, blank=True) def __str__(self): - return f'{self.street_address_1} — {self.city}' + return f""" + {first_name} {last_name} + {street_address_1} + {street_address_2} + {city}, {state}, {postal_code} + """ class User(AbstractUser): diff --git a/src/core/__init__.py b/src/core/__init__.py index cf1d22b..f8e65f7 100644 --- a/src/core/__init__.py +++ b/src/core/__init__.py @@ -127,3 +127,20 @@ class CoffeeGrind: (PERCOLATOR, 'Percolator'), (CAFE_STYLE, 'BLTC cafe pour over') ] + + +def build_usps_rate_request(weight, container, zip_destination): + return \ + { + 'service': ShippingService.PRIORITY_COMMERCIAL, + 'zip_origination': settings.DEFAULT_ZIP_ORIGINATION, + 'zip_destination': zip_destination, + 'pounds': '0', + 'ounces': weight, + 'container': container, + 'width': '', + 'length': '', + 'height': '', + 'girth': '', + 'machinable': 'TRUE' + } diff --git a/src/core/admin.py b/src/core/admin.py index 6e41d90..324c630 100644 --- a/src/core/admin.py +++ b/src/core/admin.py @@ -1,6 +1,7 @@ from django.contrib import admin from .models import ( + SiteSettings, ProductCategory, Product, ProductPhoto, @@ -13,6 +14,7 @@ from .models import ( OrderLine, ) +admin.site.register(SiteSettings) admin.site.register(ProductCategory) admin.site.register(Product) admin.site.register(ProductPhoto) diff --git a/src/core/fixtures/orders.json b/src/core/fixtures/orders.json index f0c17c2..5b40378 100644 --- a/src/core/fixtures/orders.json +++ b/src/core/fixtures/orders.json @@ -10,7 +10,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "9.55", - "total_net_amount": "13.40", + "subtotal_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-03-15T17:18:59.584Z", "updated_at": "2022-03-15T17:18:59.584Z" @@ -26,7 +26,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "9.55", - "total_net_amount": "13.40", + "subtotal_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-03-15T17:22:18.440Z", "updated_at": "2022-03-15T17:22:18.440Z" @@ -42,7 +42,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "9.55", - "total_net_amount": "13.40", + "subtotal_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-03-15T17:26:27.869Z", "updated_at": "2022-03-15T17:26:27.869Z" diff --git a/src/core/migrations/0013_rename_total_net_amount_order_subtotal_amount_and_more.py b/src/core/migrations/0013_rename_total_net_amount_order_subtotal_amount_and_more.py new file mode 100644 index 0000000..975ad85 --- /dev/null +++ b/src/core/migrations/0013_rename_total_net_amount_order_subtotal_amount_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 4.0.2 on 2022-10-01 18:45 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0012_alter_productvariant_product'), + ] + + operations = [ + migrations.RenameField( + model_name='order', + old_name='total_net_amount', + new_name='subtotal_amount', + ), + migrations.RemoveField( + model_name='orderline', + name='product', + ), + migrations.AddField( + model_name='order', + name='coupon_amount', + field=models.CharField(blank=True, max_length=255), + ), + migrations.AddField( + model_name='order', + name='total_amount', + field=models.DecimalField(decimal_places=2, default=0, max_digits=10), + ), + migrations.AddField( + model_name='orderline', + name='variant', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order_lines', to='core.productvariant'), + ), + ] diff --git a/src/core/models.py b/src/core/models.py index e1ed7bd..aeff19c 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -24,7 +24,8 @@ from . import ( TransactionStatus, OrderStatus, ShippingProvider, - ShippingContainer + ShippingContainer, + build_usps_rate_request ) from .weight import WeightUnits, zero_weight @@ -77,13 +78,6 @@ class ProductCategory(models.Model): verbose_name_plural = 'Product Categories' -class ProductManager(models.Manager): - def get_queryset(self): - return super().get_queryset().annotate( - num_ordered=models.Sum('order_lines__quantity') - ) - - class Product(models.Model): category = models.ForeignKey( ProductCategory, @@ -105,8 +99,6 @@ class Product(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) - objects = ProductManager() - def __str__(self): return self.name @@ -126,6 +118,13 @@ class Product(models.Model): ordering = ['sorting', 'name'] +class ProductVariantManager(models.Manager): + def get_queryset(self): + return super().get_queryset().annotate( + num_ordered=models.Sum('order_lines__quantity') + ) + + class ProductVariant(models.Model): product = models.ForeignKey( Product, @@ -157,6 +156,8 @@ class ProductVariant(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + objects = ProductVariantManager() + def __str__(self): return f'{self.product}: {self.name}' @@ -296,7 +297,7 @@ class OrderManager(models.Manager): class Order(models.Model): customer = models.ForeignKey( User, - related_name="orders", + related_name='orders', on_delete=models.SET_NULL, null=True ) @@ -319,7 +320,6 @@ class Order(models.Model): null=True, on_delete=models.SET_NULL ) - coupon = models.ForeignKey( Coupon, related_name='orders', @@ -327,19 +327,22 @@ class Order(models.Model): blank=True, null=True ) - + subtotal_amount = models.DecimalField( + max_digits=10, + decimal_places=2, + default=0 + ) + coupon_amount = models.CharField(max_length=255, blank=True) shipping_total = models.DecimalField( max_digits=5, decimal_places=2, default=0 ) - - total_net_amount = models.DecimalField( + total_amount = models.DecimalField( max_digits=10, decimal_places=2, default=0 ) - weight = MeasurementField( measurement=Weight, unit_choices=WeightUnits.CHOICES, @@ -361,11 +364,11 @@ class Order(models.Model): if self.coupon.discount_value_type == DiscountValueType.FIXED: return self.coupon.discount_value elif self.coupon.discount_value_type == DiscountValueType.PERCENTAGE: - return (self.coupon.discount_value / Decimal('100')) * self.total_net_amount + return (self.coupon.discount_value / Decimal('100')) * self.subtotal_amount return Decimal('0') def get_total_price_after_discount(self): - return round((self.total_net_amount - self.get_discount()) + self.shipping_total, 2) + return round((self.subtotal_amount - self.get_discount()) + self.shipping_total, 2) def get_absolute_url(self): return reverse('dashboard:order-detail', kwargs={'pk': self.pk}) @@ -395,13 +398,13 @@ class Transaction(models.Model): class OrderLine(models.Model): order = models.ForeignKey( Order, - related_name="lines", + related_name='lines', editable=False, on_delete=models.CASCADE ) - product = models.ForeignKey( - Product, - related_name="order_lines", + variant = models.ForeignKey( + ProductVariant, + related_name='order_lines', on_delete=models.SET_NULL, blank=True, null=True, @@ -410,20 +413,17 @@ class OrderLine(models.Model): quantity_fulfilled = models.IntegerField( validators=[MinValueValidator(0)], default=0 ) - customer_note = models.TextField(blank=True, default="") - + customer_note = models.TextField(blank=True, default='') currency = models.CharField( max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH, default=settings.DEFAULT_CURRENCY, ) - unit_price = models.DecimalField( max_digits=settings.DEFAULT_MAX_DIGITS, decimal_places=settings.DEFAULT_DECIMAL_PLACES, ) - tax_rate = models.DecimalField( - max_digits=5, decimal_places=2, default=Decimal("0.0") + max_digits=5, decimal_places=2, default=Decimal('0.0') ) def get_total(self): diff --git a/src/dashboard/templates/dashboard/customer_detail.html b/src/dashboard/templates/dashboard/customer_detail.html index aa00d24..3fefbbf 100644 --- a/src/dashboard/templates/dashboard/customer_detail.html +++ b/src/dashboard/templates/dashboard/customer_detail.html @@ -71,7 +71,7 @@
{{order.get_status_display}}
- ${{order.total_net_amount}} + ${{order.total_amount}} {% empty %} No orders diff --git a/src/dashboard/templates/dashboard/order_detail.html b/src/dashboard/templates/dashboard/order_detail.html index ac5f882..a457290 100644 --- a/src/dashboard/templates/dashboard/order_detail.html +++ b/src/dashboard/templates/dashboard/order_detail.html @@ -26,10 +26,10 @@ {% for item in order.lines.all %}
- {% with product=item.product %} + {% with product=item.variant.product %}
{{product.get_first_img.image}} -
{{product.name}}
Grind: {{item.customer_note}}
+
{{item.variant}}
{{item.customer_note}}
{{product.sku}} {{item.quantity}} @@ -103,7 +103,7 @@

- Subtotal: ${{order.total_net_amount}}
+ Subtotal: ${{order.subtotal_amount}}
{% if order.coupon %} Discount: {{order.coupon.discount_value}} {{order.coupon.get_discount_value_type_display}}
{% endif %} diff --git a/src/dashboard/templates/dashboard/product_detail.html b/src/dashboard/templates/dashboard/product_detail.html index 9fae9a5..e8a2ce5 100644 --- a/src/dashboard/templates/dashboard/product_detail.html +++ b/src/dashboard/templates/dashboard/product_detail.html @@ -15,15 +15,33 @@ {{product.get_first_img.image}}

+

Category: {{ product.category }}

{{product.name}}

+
{{ product.subtitle }}

{{product.description}}

-

${{product.price}}

-

{{product.weight.oz}} oz

+

Checkout limit: {{ product.checkout_limit }}

Visible in listings: {{product.visible_in_listings|yesno:"Yes,No"}}

-

Stripe ID: {{ product.stripe_id }}

+

Sorting: {{ product.sorting }}

Ordered {{product.num_ordered|default_if_none:"0"}} time{{ product.num_ordered|default_if_none:"0"|pluralize }}.

+
+
+

Variants

+ + New variant +
+ {% for variant in product.variants.all %} +
+

name: {{ variant.name }}

+

sku: {{ variant.sku }}

+

stripe_id: {{ variant.stripe_id }}

+

price: ${{ variant.price }}

+

weight: {{ variant.weight }}

+

track_inventory: {{ variant.track_inventory }}

+

stock: {{ variant.stock }}

+
+ {% endfor %} +

Photos

diff --git a/src/dashboard/templates/dashboard/productvariant_create_form.html b/src/dashboard/templates/dashboard/productvariant_create_form.html new file mode 100644 index 0000000..ae94ae7 --- /dev/null +++ b/src/dashboard/templates/dashboard/productvariant_create_form.html @@ -0,0 +1,18 @@ +{% extends "dashboard.html" %} + +{% block content %} +
+
+

Create variant

+
+
+
+ {% csrf_token %} + {{form.as_p}} +

+ or cancel +

+
+
+
+{% endblock %} diff --git a/src/dashboard/templates/dashboard/productvariant_detail.html b/src/dashboard/templates/dashboard/productvariant_detail.html new file mode 100644 index 0000000..e69de29 diff --git a/src/dashboard/templates/dashboard/productvariant_form.html b/src/dashboard/templates/dashboard/productvariant_form.html new file mode 100644 index 0000000..e69de29 diff --git a/src/dashboard/urls.py b/src/dashboard/urls.py index df2d7f4..2fc3d1f 100644 --- a/src/dashboard/urls.py +++ b/src/dashboard/urls.py @@ -121,6 +121,32 @@ urlpatterns = [ name='prodphoto-delete' ), ])), + + # ProductVariants + path('variants/', include([ + path( + 'new/', + views.ProductVariantCreateView.as_view(), + name='variant-create' + ), + path('/', include([ + path( + '', + views.ProductVariantDetailView.as_view(), + name='variant-detail' + ), + path( + 'update/', + views.ProductVariantUpdateView.as_view(), + name='variant-update' + ), + path( + 'delete/', + views.ProductVariantDeleteView.as_view(), + name='variant-delete' + ), + ])), + ])), ])), path( diff --git a/src/dashboard/views.py b/src/dashboard/views.py index 85f1939..4d23580 100644 --- a/src/dashboard/views.py +++ b/src/dashboard/views.py @@ -28,6 +28,7 @@ from accounts.forms import AddressForm from core.models import ( Product, ProductPhoto, + ProductVariant, Order, OrderLine, ShippingRate, @@ -71,7 +72,7 @@ class DashboardHomeView(LoginRequiredMixin, TemplateView): status=OrderStatus.DRAFT ).filter( created_at__date=today - ).aggregate(total=Sum('total_net_amount'))['total'] + ).aggregate(total=Sum('total_amount'))['total'] return context @@ -167,10 +168,9 @@ class OrderDetailView(LoginRequiredMixin, DetailView): ).select_related( 'customer', 'billing_address', - 'shipping_address', - 'shipping_method' + 'shipping_address' ).prefetch_related( - 'lines__product__productphoto_set' + 'lines__variant__product__productphoto_set' ) obj = queryset.get() return obj @@ -291,6 +291,54 @@ class ProductPhotoDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']}) +class ProductVariantCreateView(SuccessMessageMixin, CreateView): + model = ProductVariant + success_message = 'ProductVariant created.' + template_name = 'dashboard/productvariant_create_form.html' + fields = [ + 'name', + 'sku', + 'price', + 'weight', + 'track_inventory', + 'stock', + ] + success_message = 'Variant created.' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['product'] = Product.objects.get(pk=self.kwargs['pk']) + return context + + def form_valid(self, form): + form.instance.product = Product.objects.get(pk=self.kwargs['pk']) + return super().form_valid(form) + + def get_success_url(self): + return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']}) + + +class ProductVariantDetailView(DetailView): + model = ProductVariant + pk_url_kwarg = 'variant_pk' + + +class ProductVariantUpdateView(SuccessMessageMixin, UpdateView): + model = ProductVariant + pk_url_kwarg = 'variant_pk' + success_message = 'ProductVariant saved.' + fields = '__all__' + + +class ProductVariantDeleteView(SuccessMessageMixin, DeleteView): + model = ProductVariant + pk_url_kwarg = 'variant_pk' + success_message = 'ProductVariant deleted.' + + def get_success_url(self): + return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']}) + + class CustomerListView(LoginRequiredMixin, ListView): model = User template_name = 'dashboard/customer_list.html' diff --git a/src/fixtures/db.json b/src/fixtures/db.json index f563747..50d5606 100644 --- a/src/fixtures/db.json +++ b/src/fixtures/db.json @@ -1881,7 +1881,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-03-15T17:18:59.584Z", "updated_at": "2022-03-15T17:18:59.584Z" @@ -1897,7 +1897,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-03-15T17:22:18.440Z", "updated_at": "2022-03-15T17:22:18.440Z" @@ -1913,7 +1913,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-03-15T17:26:27.869Z", "updated_at": "2022-03-15T17:26:27.869Z" @@ -1929,7 +1929,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-03-15T18:14:16.587Z", "updated_at": "2022-03-15T18:14:16.587Z" @@ -1945,7 +1945,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-03-15T18:16:59.460Z", "updated_at": "2022-03-15T18:16:59.460Z" @@ -1961,7 +1961,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-03-15T18:23:13.283Z", "updated_at": "2022-03-15T18:23:13.283Z" @@ -1977,7 +1977,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "40.20", + "total_amount": "40.20", "weight": "0.0:oz", "created_at": "2022-03-15T18:29:02.632Z", "updated_at": "2022-03-15T18:29:02.632Z" @@ -1993,7 +1993,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "40.20", + "total_amount": "40.20", "weight": "0.0:oz", "created_at": "2022-03-15T19:13:50.050Z", "updated_at": "2022-03-15T19:13:50.050Z" @@ -2009,7 +2009,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "40.20", + "total_amount": "40.20", "weight": "0.0:oz", "created_at": "2022-03-15T19:15:18.843Z", "updated_at": "2022-03-15T19:15:18.843Z" @@ -2025,7 +2025,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "40.20", + "total_amount": "40.20", "weight": "0.0:oz", "created_at": "2022-03-15T19:17:21.952Z", "updated_at": "2022-03-15T19:17:21.952Z" @@ -2041,7 +2041,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-03-15T19:22:34.503Z", "updated_at": "2022-03-15T19:22:34.503Z" @@ -2057,7 +2057,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-03-15T19:25:35.313Z", "updated_at": "2022-03-15T19:25:35.313Z" @@ -2073,7 +2073,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-03-15T19:26:51.478Z", "updated_at": "2022-03-15T19:26:51.478Z" @@ -2089,7 +2089,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-03-15T19:30:28.497Z", "updated_at": "2022-03-15T19:30:28.497Z" @@ -2105,7 +2105,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-03-15T19:36:30.561Z", "updated_at": "2022-03-15T19:36:30.561Z" @@ -2121,7 +2121,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-03-15T19:54:38.099Z", "updated_at": "2022-03-15T19:54:38.099Z" @@ -2137,7 +2137,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-03-15T19:56:49.477Z", "updated_at": "2022-03-15T19:56:49.477Z" @@ -2153,7 +2153,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-03-15T20:01:53.848Z", "updated_at": "2022-03-15T20:01:53.848Z" @@ -2169,7 +2169,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-03-15T20:09:31.510Z", "updated_at": "2022-03-15T20:09:31.510Z" @@ -2185,7 +2185,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-03-15T20:13:16.927Z", "updated_at": "2022-03-15T20:13:16.927Z" @@ -2201,7 +2201,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-03-15T20:14:43.333Z", "updated_at": "2022-03-15T20:14:43.333Z" @@ -2217,7 +2217,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-03-15T20:16:03.299Z", "updated_at": "2022-03-15T20:16:03.299Z" @@ -2233,7 +2233,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-03-15T20:17:32.842Z", "updated_at": "2022-03-15T20:17:32.842Z" @@ -2249,7 +2249,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "26.80", + "total_amount": "26.80", "weight": "0.0:oz", "created_at": "2022-03-15T20:21:35.974Z", "updated_at": "2022-03-15T20:21:35.974Z" @@ -2265,7 +2265,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "40.20", + "total_amount": "40.20", "weight": "0.0:oz", "created_at": "2022-03-15T20:22:11.717Z", "updated_at": "2022-03-15T20:22:11.717Z" @@ -2281,7 +2281,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "53.60", + "total_amount": "53.60", "weight": "0.0:oz", "created_at": "2022-03-15T20:23:49.392Z", "updated_at": "2022-03-15T20:23:49.392Z" @@ -2297,7 +2297,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "53.60", + "total_amount": "53.60", "weight": "0.0:oz", "created_at": "2022-03-15T20:25:04.787Z", "updated_at": "2022-03-15T20:25:04.787Z" @@ -2313,7 +2313,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "53.60", + "total_amount": "53.60", "weight": "0.0:oz", "created_at": "2022-03-15T20:27:47.933Z", "updated_at": "2022-03-15T20:27:47.933Z" @@ -2329,7 +2329,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "40.20", + "total_amount": "40.20", "weight": "0.0:oz", "created_at": "2022-03-15T20:30:40.141Z", "updated_at": "2022-03-15T20:30:40.141Z" @@ -2345,7 +2345,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "40.20", + "total_amount": "40.20", "weight": "0.0:oz", "created_at": "2022-03-15T20:32:09.015Z", "updated_at": "2022-03-23T16:02:59.305Z" @@ -2361,7 +2361,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "26.80", + "total_amount": "26.80", "weight": "0.0:oz", "created_at": "2022-03-23T16:59:10.471Z", "updated_at": "2022-03-23T17:00:17.128Z" @@ -2377,7 +2377,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "25.46", + "total_amount": "25.46", "weight": "0.0:oz", "created_at": "2022-03-23T21:22:54.950Z", "updated_at": "2022-03-23T21:22:54.950Z" @@ -2393,7 +2393,7 @@ "shipping_method": null, "coupon": 1, "shipping_total": "0.00", - "total_net_amount": "12.73", + "total_amount": "12.73", "weight": "0.0:oz", "created_at": "2022-03-23T21:30:54.290Z", "updated_at": "2022-03-23T21:30:54.290Z" @@ -2409,7 +2409,7 @@ "shipping_method": null, "coupon": 1, "shipping_total": "0.00", - "total_net_amount": "26.80", + "total_amount": "26.80", "weight": "0.0:oz", "created_at": "2022-03-23T21:45:57.399Z", "updated_at": "2022-03-23T21:45:57.399Z" @@ -2425,7 +2425,7 @@ "shipping_method": null, "coupon": 1, "shipping_total": "0.00", - "total_net_amount": "26.80", + "total_amount": "26.80", "weight": "0.0:oz", "created_at": "2022-03-23T21:52:22.463Z", "updated_at": "2022-03-25T16:51:04.837Z" @@ -2441,7 +2441,7 @@ "shipping_method": null, "coupon": 1, "shipping_total": "0.00", - "total_net_amount": "67.00", + "total_amount": "67.00", "weight": "0.0:oz", "created_at": "2022-04-01T17:09:34.892Z", "updated_at": "2022-04-01T17:09:34.892Z" @@ -2457,7 +2457,7 @@ "shipping_method": null, "coupon": 2, "shipping_total": "0.00", - "total_net_amount": "40.20", + "total_amount": "40.20", "weight": "0.0:oz", "created_at": "2022-04-04T00:02:12.247Z", "updated_at": "2022-04-04T00:02:12.247Z" @@ -2473,7 +2473,7 @@ "shipping_method": null, "coupon": 2, "shipping_total": "0.00", - "total_net_amount": "40.20", + "total_amount": "40.20", "weight": "0.0:oz", "created_at": "2022-04-04T00:03:44.789Z", "updated_at": "2022-04-04T00:03:44.789Z" @@ -2489,7 +2489,7 @@ "shipping_method": null, "coupon": 2, "shipping_total": "0.00", - "total_net_amount": "26.80", + "total_amount": "26.80", "weight": "0.0:oz", "created_at": "2022-04-06T01:18:18.633Z", "updated_at": "2022-04-06T01:18:18.633Z" @@ -2505,7 +2505,7 @@ "shipping_method": null, "coupon": 2, "shipping_total": "0.00", - "total_net_amount": "67.00", + "total_amount": "67.00", "weight": "0.0:oz", "created_at": "2022-04-06T17:48:39.005Z", "updated_at": "2022-04-06T18:04:31.040Z" @@ -2521,7 +2521,7 @@ "shipping_method": null, "coupon": 2, "shipping_total": "0.00", - "total_net_amount": "26.80", + "total_amount": "26.80", "weight": "0.0:oz", "created_at": "2022-04-06T18:00:15.976Z", "updated_at": "2022-04-06T18:00:15.976Z" @@ -2537,7 +2537,7 @@ "shipping_method": null, "coupon": 2, "shipping_total": "0.00", - "total_net_amount": "53.60", + "total_amount": "53.60", "weight": "0.0:oz", "created_at": "2022-04-06T18:01:51.206Z", "updated_at": "2022-04-06T18:01:51.206Z" @@ -2553,7 +2553,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-04-15T03:18:58.958Z", "updated_at": "2022-04-15T03:18:58.958Z" @@ -2569,7 +2569,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-04-15T03:19:14.980Z", "updated_at": "2022-04-15T03:19:14.980Z" @@ -2585,7 +2585,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-04-15T03:21:45.918Z", "updated_at": "2022-04-15T03:21:45.918Z" @@ -2601,7 +2601,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-04-15T03:22:58.009Z", "updated_at": "2022-04-15T03:22:58.009Z" @@ -2617,7 +2617,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-04-15T03:24:22.731Z", "updated_at": "2022-04-15T03:24:22.731Z" @@ -2633,7 +2633,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-04-15T03:24:38.585Z", "updated_at": "2022-04-15T03:24:38.585Z" @@ -2649,7 +2649,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-04-15T03:26:19.552Z", "updated_at": "2022-04-15T03:26:19.552Z" @@ -2665,7 +2665,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "26.80", + "total_amount": "26.80", "weight": "0.0:oz", "created_at": "2022-04-23T20:51:39.679Z", "updated_at": "2022-04-23T20:51:39.679Z" @@ -2681,7 +2681,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "26.80", + "total_amount": "26.80", "weight": "0.0:oz", "created_at": "2022-04-23T20:55:39.285Z", "updated_at": "2022-04-23T20:55:39.285Z" @@ -2697,7 +2697,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "26.80", + "total_amount": "26.80", "weight": "0.0:oz", "created_at": "2022-04-23T21:00:39.249Z", "updated_at": "2022-04-24T03:38:54.039Z" @@ -2713,7 +2713,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "40.20", + "total_amount": "40.20", "weight": "0.0:oz", "created_at": "2022-04-24T16:34:28.911Z", "updated_at": "2022-04-24T16:34:28.911Z" @@ -2729,7 +2729,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "40.20", + "total_amount": "40.20", "weight": "0.0:oz", "created_at": "2022-04-24T16:37:32.671Z", "updated_at": "2022-04-24T16:37:32.671Z" @@ -2745,7 +2745,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "40.20", + "total_amount": "40.20", "weight": "0.0:oz", "created_at": "2022-04-24T16:41:55.368Z", "updated_at": "2022-04-24T16:41:55.368Z" @@ -2761,7 +2761,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "40.20", + "total_amount": "40.20", "weight": "0.0:oz", "created_at": "2022-04-24T16:47:43.438Z", "updated_at": "2022-04-24T16:47:43.438Z" @@ -2777,7 +2777,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "40.20", + "total_amount": "40.20", "weight": "0.0:oz", "created_at": "2022-04-24T16:49:10.526Z", "updated_at": "2022-04-24T16:49:10.526Z" @@ -2793,7 +2793,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "40.20", + "total_amount": "40.20", "weight": "0.0:oz", "created_at": "2022-04-24T16:49:18.644Z", "updated_at": "2022-04-24T16:49:18.645Z" @@ -2809,7 +2809,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "53.60", + "total_amount": "53.60", "weight": "0.0:oz", "created_at": "2022-04-24T17:01:14.133Z", "updated_at": "2022-04-24T17:01:14.133Z" @@ -2825,7 +2825,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "53.60", + "total_amount": "53.60", "weight": "0.0:oz", "created_at": "2022-04-24T17:03:50.880Z", "updated_at": "2022-04-24T17:03:50.880Z" @@ -2841,7 +2841,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "53.60", + "total_amount": "53.60", "weight": "0.0:oz", "created_at": "2022-04-24T17:19:22.528Z", "updated_at": "2022-04-24T17:19:22.528Z" @@ -2857,7 +2857,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "53.60", + "total_amount": "53.60", "weight": "0.0:oz", "created_at": "2022-04-24T17:23:48.946Z", "updated_at": "2022-04-24T17:23:48.946Z" @@ -2873,7 +2873,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "40.20", + "total_amount": "40.20", "weight": "0.0:oz", "created_at": "2022-04-24T17:35:04.209Z", "updated_at": "2022-04-24T17:35:04.209Z" @@ -2889,7 +2889,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "40.20", + "total_amount": "40.20", "weight": "0.0:oz", "created_at": "2022-04-24T17:35:40.334Z", "updated_at": "2022-04-24T17:35:40.334Z" @@ -2905,7 +2905,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "40.20", + "total_amount": "40.20", "weight": "0.0:oz", "created_at": "2022-04-24T17:36:27.559Z", "updated_at": "2022-04-24T17:36:46.155Z" @@ -2921,7 +2921,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "9.55", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-04-24T17:52:07.802Z", "updated_at": "2022-04-24T17:52:07.802Z" @@ -2937,7 +2937,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "12.47", - "total_net_amount": "40.20", + "total_amount": "40.20", "weight": "0.0:oz", "created_at": "2022-04-24T17:52:59.926Z", "updated_at": "2022-04-24T17:53:38.188Z" @@ -2953,7 +2953,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "9.55", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-04-24T17:57:18.399Z", "updated_at": "2022-04-24T17:57:18.399Z" @@ -2969,7 +2969,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "9.55", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-04-24T18:36:43.689Z", "updated_at": "2022-04-24T18:37:06.954Z" @@ -2985,7 +2985,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "0.00", - "total_net_amount": "0.00", + "total_amount": "0.00", "weight": "0.0:oz", "created_at": "2022-04-24T20:44:10.464Z", "updated_at": "2022-04-24T20:44:10.464Z" @@ -3001,7 +3001,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "9.55", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-04-24T20:44:28.234Z", "updated_at": "2022-04-24T20:44:44.522Z" @@ -3017,7 +3017,7 @@ "shipping_method": null, "coupon": null, "shipping_total": "9.55", - "total_net_amount": "13.40", + "total_amount": "13.40", "weight": "0.0:oz", "created_at": "2022-04-24T21:06:59.696Z", "updated_at": "2022-04-24T21:07:17.313Z" diff --git a/src/static/styles/main.css b/src/static/styles/main.css index 7b4d259..f8d1853 100644 --- a/src/static/styles/main.css +++ b/src/static/styles/main.css @@ -773,6 +773,7 @@ article + article { .item__price { justify-self: end; + text-align: right; } .item__form, @@ -911,3 +912,8 @@ footer > section { text-align: center; } + + +.show-modal { + white-space: unset; +} diff --git a/src/storefront/cart.py b/src/storefront/cart.py index 99d0f4d..633453a 100644 --- a/src/storefront/cart.py +++ b/src/storefront/cart.py @@ -6,8 +6,11 @@ from django.conf import settings from django.contrib import messages from django.shortcuts import redirect, reverse from django.urls import reverse_lazy +from django.db.models import OuterRef, Q, Subquery -from core.models import Product, OrderLine, Coupon +from core.models import ( + Product, ProductVariant, OrderLine, Coupon, ShippingRate +) from core.usps import USPSApi from core import ( DiscountValueType, @@ -16,7 +19,8 @@ from core import ( OrderStatus, ShippingService, ShippingContainer, - CoffeeGrind + CoffeeGrind, + build_usps_rate_request ) from .payments import CreateOrder @@ -29,32 +33,18 @@ class Cart: self.request = request self.session = request.session self.coupon_code = self.session.get('coupon_code') - self.container = self.session.get('shipping_container') cart = self.session.get(settings.CART_SESSION_ID) if not cart: - cart = self.session[settings.CART_SESSION_ID] = {} + cart = self.session[settings.CART_SESSION_ID] = [] self.cart = cart - def add( - self, request, product, quantity=1, grind='', update_quantity=False - ): - product_id = str(product.id) - if product_id not in self.cart: - self.cart[product_id] = { - 'variations': {}, - 'price': str(product.price) - } - self.cart[product_id]['variations'][grind] = {'quantity': 0} - + def add(self, request, item, update_quantity=False): if update_quantity: - self.cart[product_id]['variations'][grind]['quantity'] = quantity + self.cart[item['variant']]['quantity'] = item['quantity'] else: - if not grind in self.cart[product_id]['variations']: - # create it - self.cart[product_id]['variations'][grind] = {'quantity': quantity} - else: - # add to it - self.cart[product_id]['variations'][grind]['quantity'] += quantity + self.cart.append(item) + + # TODO: abstract this to a function that will check the max amount of item in the cart if len(self) <= 20: self.save() else: @@ -65,39 +55,32 @@ class Cart: self.session.modified = True logger.info(f'\nCart:\n{self.cart}\n') - def remove(self, product, grind): - product_id = str(product.id) - if product_id in self.cart: - del self.cart[product_id]['variations'][grind] - if not self.cart[product_id]['variations']: - del self.cart[product_id] - self.save() + def remove(self, pk): + self.cart.pop(pk) + self.save() def __iter__(self): - product_ids = self.cart.keys() - products = Product.objects.filter(id__in=product_ids) - for product in products: - self.cart[str(product.id)]['product'] = product - - for item in self.cart.values(): - item['price'] = Decimal(item['price']) - item['total_price'] = Decimal(sum(self.get_item_prices())) - item['quantity'] = self.get_single_item_total_quantity(item) + for item in self.cart: + pk = item['variant'].pk if isinstance(item['variant'], ProductVariant) else item['variant'] + item['variant'] = ProductVariant.objects.get(pk=pk) + item['price_total'] = item['variant'].price * item['quantity'] yield item def __len__(self): - return sum(self.get_all_item_quantities()) + return sum([item['quantity'] for item in self.cart]) def get_all_item_quantities(self): - for item in self.cart.values(): - yield sum([value['quantity'] for value in item['variations'].values()]) + for item in self.cart: + yield item['quantity'] def get_single_item_total_quantity(self, item): return sum([value['quantity'] for value in item['variations'].values()]) def get_item_prices(self): - for item in self.cart.values(): - yield Decimal(item['price']) * sum([value['quantity'] for value in item['variations'].values()]) + for item in self: + yield item['price_total'] + # for item in self.cart.values(): + # yield Decimal(item['price']) * sum([value['quantity'] for value in item['variations'].values()]) def get_total_price(self): return sum(self.get_item_prices()) @@ -105,30 +88,36 @@ class Cart: def get_total_weight(self): if len(self) > 0: for item in self: - return item['product'].weight.value * sum(self.get_all_item_quantities()) + return item['variant'].weight.value * sum(self.get_all_item_quantities()) else: return 0 - def get_shipping_box(self, container=None): - if container: - return container - - if self.container: - return self.container - - if len(self) > 6 and len(self) <= 10: - return ShippingContainer.LG_FLAT_RATE_BOX - elif len(self) > 3 and len(self) <= 6: - return ShippingContainer.REGIONAL_RATE_BOX_B - elif len(self) <= 3: - return ShippingContainer.REGIONAL_RATE_BOX_A - else: - return ShippingContainer.VARIABLE + def get_shipping_container_choices(self): + min_weight_matched = Q( + min_order_weight__lte=self.get_total_weight()) | Q( + min_order_weight__isnull=True + ) + max_weight_matched = Q( + max_order_weight__gte=self.get_total_weight()) | Q( + max_order_weight__isnull=True + ) + containers = ShippingRate.objects.filter( + min_weight_matched & max_weight_matched + ) + return containers def get_shipping_cost(self, container=None): - if len(self) > 0 and self.session.get("shipping_address"): + if container is None: + container = self.session.get('shipping_container').container + + if len(self) > 0 and self.session.get('shipping_address'): + usps_rate_request = build_usps_rate_request( + str(self.get_total_weight()), + container, + str(self.session.get('shipping_address')['postal_code']) + ) try: - usps_rate_request = self.build_usps_rate_request(container) + logger.info('wafd') except TypeError as e: return Decimal('0.00') usps = USPSApi(settings.USPS_USER_ID, test=True) @@ -140,9 +129,10 @@ class Cart: 'Could not connect to USPS, try again.' ) - logger.info(validation.result) - if 'Error' not in validation.result['RateV4Response']['Package']: - rate = validation.result['RateV4Response']['Package']['Postage']['CommercialRate'] + logger.error(validation.result) + package = dict(validation.result['RateV4Response']['Package']) + if 'Error' not in package: + rate = package['Postage']['CommercialRate'] else: logger.error("USPS Rate error") rate = '0.00' @@ -158,22 +148,6 @@ class Cart: pass self.session.modified = True - def build_usps_rate_request(self, container=None): - return \ - { - 'service': ShippingService.PRIORITY_COMMERCIAL, - 'zip_origination': settings.DEFAULT_ZIP_ORIGINATION, - 'zip_destination': f'{self.session.get("shipping_address")["postal_code"]}', - 'pounds': '0', - 'ounces': f'{self.get_total_weight()}', - 'container': f'{self.get_shipping_box(container)}', - 'width': '', - 'length': '', - 'height': '', - 'girth': '', - 'machinable': 'TRUE' - } - def build_order_params(self, container=None): return \ { @@ -186,7 +160,9 @@ class Cart: 'shipping_method': 'US POSTAL SERVICE ' + ( container if container else '' ), - 'shipping_address': self.build_shipping_address(self.session.get('shipping_address')), + 'shipping_address': self.build_shipping_address( + self.session.get('shipping_address') + ), } def create_order(self, container=None): @@ -198,20 +174,22 @@ class Cart: response = CreateOrder().create_order(params) return response + def get_line_options(self, options_dict): + options = '' + for key, value in options_dict.items(): + options += f'{key}: {value}; ' + return options + def build_bulk_list(self, order): bulk_list = [] - for item in self: - for key, value in item['variations'].items(): - bulk_list.append(OrderLine( - order=order, - product=item['product'], - customer_note=next((v[1] for i, v in enumerate(CoffeeGrind.GRIND_CHOICES) if v[0] == key), None), - unit_price=item['price'], - quantity=value['quantity'], - tax_rate=2, - )) - + bulk_list.append(OrderLine( + order=order, + variant=item['variant'], + customer_note=self.get_line_options(item['options']), + unit_price=item['variant'].price, + quantity=item['quantity'] + )) return bulk_list def build_shipping_address(self, address): @@ -231,12 +209,25 @@ class Cart: return Coupon.objects.get(code=self.coupon_code) return None + def get_coupon_total_for_specific_products(self): + for item in self.cart: + if item['variant'].product in self.coupon.products.all(): + yield item['price_total'] + def get_discount(self): + # SHIPPING + # ENTIRE_ORDER + # SPECIFIC_PRODUCT if self.coupon: if self.coupon.discount_value_type == DiscountValueType.FIXED: return round(self.coupon.discount_value, 2) elif self.coupon.discount_value_type == DiscountValueType.PERCENTAGE: - return round((self.coupon.discount_value / Decimal('100')) * self.get_total_price(), 2) + if self.coupon.type == VoucherType.ENTIRE_ORDER: + return round((self.coupon.discount_value / Decimal('100')) * self.get_total_price(), 2) + elif self.coupon.type == VoucherType.SPECIFIC_PRODUCT: + # Get the product in cart quantity + total = sum(self.get_coupon_total_for_specific_products()) + return round((self.coupon.discount_value / Decimal('100')) * total, 2) return Decimal('0') def get_subtotal_price_after_discount(self): diff --git a/src/storefront/forms.py b/src/storefront/forms.py index e7b9dc0..940fced 100644 --- a/src/storefront/forms.py +++ b/src/storefront/forms.py @@ -18,27 +18,29 @@ logger = logging.getLogger(__name__) class AddToCartForm(forms.Form): - # grind = forms.ChoiceField(choices=CoffeeGrind.GRIND_CHOICES) - variants = forms.ChoiceField(widget=forms.RadioSelect()) quantity = forms.IntegerField(min_value=1, max_value=20, initial=1) def __init__(self, variants, options, *args, **kwargs): super().__init__(*args, **kwargs) + self.fields['variant'] = forms.ChoiceField( + label='', + choices=[(variant.pk, f'{variant.name} | ${variant.price}') for variant in variants] + ) + for option in options: self.fields[option.name] = forms.ChoiceField( choices=[(opt, opt) for opt in option.options] ) - self.fields['variants'].widget.choices = [(variant.pk, variant.name) for variant in variants] - class UpdateCartItemForm(forms.Form): + item_pk = forms.IntegerField(widget=forms.HiddenInput()) quantity = forms.IntegerField(min_value=1, max_value=20, initial=1) update = forms.BooleanField( required=False, initial=True, - widget=forms.HiddenInput + widget=forms.HiddenInput() ) @@ -117,15 +119,14 @@ class AddressForm(forms.Form): class CheckoutShippingForm(forms.Form): - SHIPPING_CHOICES = [ - (ShippingContainer.MD_FLAT_RATE_BOX, 'Flate Rate Box - Medium'), - (ShippingContainer.REGIONAL_RATE_BOX_B, 'Regional Rate Box B'), - ] + def __init__(self, containers, *args, **kwargs): + super().__init__(*args, **kwargs) - shipping_method = forms.ChoiceField( - widget=forms.RadioSelect, - choices=SHIPPING_CHOICES - ) + self.fields['shipping_method'] = forms.ChoiceField( + label='', + widget=forms.RadioSelect, + choices=[(container.pk, f'{container.name} ${container.s_cost}') for container in containers] + ) class OrderCreateForm(forms.ModelForm): @@ -136,11 +137,11 @@ class OrderCreateForm(forms.ModelForm): class Meta: model = Order fields = ( - 'total_net_amount', + 'total_amount', 'shipping_total', ) widgets = { - 'total_net_amount': forms.HiddenInput(), + 'total_amount': forms.HiddenInput(), 'shipping_total': forms.HiddenInput() } diff --git a/src/storefront/payments.py b/src/storefront/payments.py index 1d806ae..c55bdf8 100644 --- a/src/storefront/payments.py +++ b/src/storefront/payments.py @@ -93,21 +93,13 @@ class CreateOrder(PayPalClient): processed_items = [ { # Shows within upper-right dropdown during payment approval - "name": f'{item["product"]}: ' + ', '.join([ - next(( - f"{value['quantity']} x {v[1]}" - for i, v in enumerate(CoffeeGrind.GRIND_CHOICES) - if v[0] == key - ), - None, - ) for key, value in item["variations"].items()] - )[:100], + "name": str(item["variant"]), # Item details will also be in the completed paypal.com # transaction view - "description": item["product"].subtitle, + "description": item["variant"].product.subtitle, "unit_amount": { "currency_code": settings.DEFAULT_CURRENCY, - "value": f'{item["price"]}', + "value": f'{item["variant"].price}', }, "quantity": f'{item["quantity"]}', } diff --git a/src/storefront/templates/storefront/cart_detail.html b/src/storefront/templates/storefront/cart_detail.html index 418d1dd..9ff2cc8 100644 --- a/src/storefront/templates/storefront/cart_detail.html +++ b/src/storefront/templates/storefront/cart_detail.html @@ -11,26 +11,30 @@
{% for item in cart %}
- {% with product=item.product %} + {% with product=item.variant.product %}
{{product.get_first_img.image}}

{{product.name}}

-
Grind:
- {% for key, value in item.variations.items %} -

{{ key|get_grind_display }}
-

- {% csrf_token %} - {{ value.update_quantity_form }} - - Remove item -
-

+

{{ item.variant.name }}

+ {% for key, value in item.options.items %} +

{{ key }}: {{ value }}

{% endfor %} +
+ {% csrf_token %} + {{ item.update_quantity_form }} + +
+

Remove item

-

${{item.price}}

+

+ ${{ item.variant.price }} + {% if cart.coupon and cart.coupon.type == 'specific_product' and product in cart.coupon.products.all %} +
Coupon: {{ cart.coupon.name }} ({{cart.coupon.discount_value}} {{cart.coupon.get_discount_value_type_display}}) + {% endif %} +

{% endwith %}
@@ -56,7 +60,7 @@ Subtotal ${{ cart.get_total_price|floatformat:"2" }} - {% if cart.coupon %} + {% if cart.coupon and cart.coupon.type == 'entire_order' %} Coupon {{cart.coupon.discount_value}} {{cart.coupon.get_discount_value_type_display}} diff --git a/src/storefront/templates/storefront/checkout_shipping_form.html b/src/storefront/templates/storefront/checkout_shipping_form.html index cd71ba3..e2d9c66 100644 --- a/src/storefront/templates/storefront/checkout_shipping_form.html +++ b/src/storefront/templates/storefront/checkout_shipping_form.html @@ -14,20 +14,7 @@ {% csrf_token %} {{ form.non_field_errors }}
- {{ form.shipping_method.label }} - {% for radio in form.shipping_method %} -

- - {{ radio.tag }} -

- {% endfor %} + {{form.as_p}}

diff --git a/src/storefront/templates/storefront/order_detail.html b/src/storefront/templates/storefront/order_detail.html index 7c0d48c..d5eb3ca 100644 --- a/src/storefront/templates/storefront/order_detail.html +++ b/src/storefront/templates/storefront/order_detail.html @@ -21,12 +21,12 @@ {% for item in order.lines.all %} - {% with product=item.product %} + {% with product=item.variant.product %} {{product.get_first_img.image}} - {{product.name}}
+ {{ item.variant }}
{{item.customer_note}} {{item.quantity}} @@ -48,7 +48,7 @@ - + {% if order.coupon %} diff --git a/src/storefront/templates/storefront/order_form.html b/src/storefront/templates/storefront/order_form.html index 3e50ac8..8614c54 100644 --- a/src/storefront/templates/storefront/order_form.html +++ b/src/storefront/templates/storefront/order_form.html @@ -32,18 +32,24 @@

Review items

{% for item in cart %}
- {% with product=item.product %} + {% with product=item.variant.product %}
{{product.get_first_img.image}}
-

{{product.name}}

- {% for key, value in item.variations.items %} -

Grind: {{ key|get_grind_display }}, Qty: {{value.quantity}}

+

{{product.name}}

+

{{ item.variant.name }}

+ {% for key, value in item.options.items %} +

{{ key }}: {{ value }}

{% endfor %}
-

${{item.price}}

+

+ {{ item.quantity }} × ${{ item.variant.price }} + {% if cart.coupon and cart.coupon.type == 'specific_product' and product in cart.coupon.products.all %} +
Coupon: {{ cart.coupon.name }} ({{cart.coupon.discount_value}} {{cart.coupon.get_discount_value_type_display}}) + {% endif %} +

{% endwith %}
@@ -61,7 +67,7 @@ - {% if cart.coupon %} + {% if cart.coupon and cart.coupon.type == 'entire_order' %} diff --git a/src/storefront/templates/storefront/product_detail.html b/src/storefront/templates/storefront/product_detail.html index 3594102..d88686f 100644 --- a/src/storefront/templates/storefront/product_detail.html +++ b/src/storefront/templates/storefront/product_detail.html @@ -21,9 +21,6 @@

{{product.name}}

{{product.subtitle}}

{{product.description}}

-

Fair trade

-

${{product.price}}

-

{{product.weight.oz|floatformat}}oz

{% csrf_token %} {{ form.as_p }} diff --git a/src/storefront/templates/storefront/product_list.html b/src/storefront/templates/storefront/product_list.html index e0eb0c5..5368c8b 100644 --- a/src/storefront/templates/storefront/product_list.html +++ b/src/storefront/templates/storefront/product_list.html @@ -21,7 +21,7 @@

{{ product.name }}

{{ product.subtitle }}

{{product.description|truncatewords:20}}

-

${{product.price}} | {{product.weight.oz|floatformat}}oz

+

${{product.variants.first.price}}

{% endfor %} diff --git a/src/storefront/tests/test_cart.py b/src/storefront/tests/test_cart.py index 5ea93de..2aecbd1 100644 --- a/src/storefront/tests/test_cart.py +++ b/src/storefront/tests/test_cart.py @@ -35,7 +35,7 @@ class CartTest(TestCase): ) cls.order = Order.objects.create( customer=cls.customer, - total_net_amount=13.4 + total_amount=13.4 ) def setUp(self): diff --git a/src/storefront/tests/test_views.py b/src/storefront/tests/test_views.py index dbade8b..a680c05 100644 --- a/src/storefront/tests/test_views.py +++ b/src/storefront/tests/test_views.py @@ -78,7 +78,7 @@ class OrderCreateViewTest(TestCase): ) cls.order = Order.objects.create( customer=cls.customer, - total_net_amount=13.4 + total_amount=13.4 ) def setUp(self): diff --git a/src/storefront/urls.py b/src/storefront/urls.py index 9c758be..9c95e0a 100644 --- a/src/storefront/urls.py +++ b/src/storefront/urls.py @@ -20,12 +20,12 @@ urlpatterns = [ name='cart-add' ), path( - 'cart//update//', + 'cart//update/', views.CartUpdateProductView.as_view(), name='cart-update', ), path( - 'cart//remove//', + 'cart//remove/', views.cart_remove_product_view, name='cart-remove', ), @@ -39,11 +39,6 @@ urlpatterns = [ views.paypal_order_transaction_capture, name='paypal-capture', ), - path( - 'paypal/webhooks/', - views.paypal_webhook_endpoint, - name='paypal-webhook' - ), path( 'checkout/address/', views.CheckoutAddressView.as_view(), diff --git a/src/storefront/views.py b/src/storefront/views.py index 085c1d9..2388792 100644 --- a/src/storefront/views.py +++ b/src/storefront/views.py @@ -32,7 +32,7 @@ from accounts.forms import ( AddressForm as AccountAddressForm, CustomerUpdateForm ) from core.models import ( - Product, ProductOption, Order, Transaction, OrderLine, Coupon + Product, ProductOption, Order, Transaction, OrderLine, Coupon, ShippingRate ) from core.forms import ShippingRateForm from core import OrderStatus, ShippingContainer @@ -54,13 +54,13 @@ class CartView(TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) cart = Cart(self.request) - for item in cart: - for variation in item['variations'].values(): - variation['update_quantity_form'] = UpdateCartItemForm( - initial={ - 'quantity': variation['quantity'] - } - ) + for i, item in enumerate(cart): + item['update_quantity_form'] = UpdateCartItemForm( + initial={ + 'item_pk': i, + 'quantity': item['quantity'] + } + ) context['cart'] = cart context['coupon_apply_form'] = CouponApplyForm() return context @@ -74,23 +74,30 @@ class CartAddProductView(SingleObjectMixin, FormView): def get_success_url(self): return reverse('storefront:cart-detail') + def get_form(self, form_class=None): + variants = self.get_object().variants.all() + options = ProductOption.objects.filter(product__pk=self.get_object().pk) + if form_class is None: + form_class = self.get_form_class() + return form_class(variants, options, **self.get_form_kwargs()) + def post(self, request, *args, **kwargs): cart = Cart(request) form = self.get_form() if form.is_valid(): + cleaned_data = form.cleaned_data cart.add( request=request, - product=self.get_object(), - grind=form.cleaned_data['grind'], - quantity=form.cleaned_data['quantity'] + item={ + 'variant': cleaned_data.pop('variant'), + 'quantity': cleaned_data.pop('quantity'), + 'options': cleaned_data + } ) return self.form_valid(form) else: return self.form_invalid(form) - def form_valid(self, form): - return super().form_valid(form) - class CartUpdateProductView(SingleObjectMixin, FormView): model = Product @@ -106,9 +113,10 @@ class CartUpdateProductView(SingleObjectMixin, FormView): if form.is_valid(): cart.add( request=request, - product=self.get_object(), - grind=kwargs['grind'], - quantity=form.cleaned_data['quantity'], + item={ + 'variant': form.cleaned_data['item_pk'], + 'quantity': form.cleaned_data['quantity'] + }, update_quantity=form.cleaned_data['update'] ) return self.form_valid(form) @@ -119,10 +127,9 @@ class CartUpdateProductView(SingleObjectMixin, FormView): return super().form_valid(form) -def cart_remove_product_view(request, pk, grind): +def cart_remove_product_view(request, pk): cart = Cart(request) - product = get_object_or_404(Product, id=pk) - cart.remove(product, grind) + cart.remove(pk) return redirect('storefront:cart-detail') @@ -147,10 +154,10 @@ class CouponApplyView(FormView): return super().form_valid(form) -class ProductListView(FormMixin, ListView): +class ProductListView(ListView): model = Product template_name = 'storefront/product_list.html' - form_class = AddToCartForm + # form_class = AddToCartForm ordering = 'sorting' queryset = Product.objects.filter( @@ -229,38 +236,27 @@ class CheckoutShippingView(FormView): success_url = reverse_lazy('storefront:order-create') def get(self, request, *args, **kwargs): - cart = Cart(request) - if len(cart) != 6: - if 'shipping_container' in self.request.session: - del self.request.session['shipping_container'] - return HttpResponseRedirect( - reverse('storefront:order-create') - ) - - if not self.request.session.get("shipping_address"): + if not self.request.session.get('shipping_address'): messages.warning(request, 'Please add a shipping address.') return HttpResponseRedirect( reverse('storefront:checkout-address') ) - return super().get(request, *args, **kwargs) - def get_context_data(self, **kwargs): + def get_form(self, form_class=None): cart = Cart(self.request) - context = super().get_context_data(**kwargs) - context['MD_FLAT_RATE_BOX'] = cart.get_shipping_cost( - ShippingContainer.MD_FLAT_RATE_BOX - ) - context['REGIONAL_RATE_BOX_B'] = cart.get_shipping_cost( - ShippingContainer.REGIONAL_RATE_BOX_B - ) - return context + containers = cart.get_shipping_container_choices() + for container in containers: + container.s_cost = cart.get_shipping_cost(container.container) + if form_class is None: + form_class = self.get_form_class() + return form_class(containers, **self.get_form_kwargs()) def form_valid(self, form): - cleaned_data = form.cleaned_data - self.request.session['shipping_container'] = cleaned_data.get( - 'shipping_method' + shipping_container = ShippingRate.objects.get( + pk=form.cleaned_data.get('shipping_method') ) + self.request.session['shipping_container'] = shipping_container return super().form_valid(form) @@ -271,17 +267,13 @@ class OrderCreateView(CreateView): success_url = reverse_lazy('storefront:payment-done') def get(self, request, *args, **kwargs): - cart = Cart(request) - if len(cart) != 6 and 'shipping_container' in self.request.session: - del self.request.session['shipping_container'] - - if not self.request.session.get("shipping_address"): + if not self.request.session.get('shipping_address'): messages.warning(request, 'Please add a shipping address.') return HttpResponseRedirect( reverse('storefront:checkout-address') ) elif self.request.session.get('coupon_code'): - address = self.request.session.get("shipping_address") + address = self.request.session.get('shipping_address') coupon = Coupon.objects.get( code=self.request.session.get('coupon_code') ) @@ -292,19 +284,22 @@ class OrderCreateView(CreateView): if user in coupon.users.all(): del self.request.session['coupon_code'] messages.warning(request, 'Coupon already used.') - return super().get(request, *args, **kwargs) def get_initial(self): cart = Cart(self.request) + shipping_container = self.request.session.get( + 'shipping_container' + ).container try: - shipping_cost = cart.get_shipping_cost() + shipping_cost = cart.get_shipping_cost(shipping_container) except Exception as e: - raise e('Could not get shipping information') + logger.error('Could not get shipping information') + raise shipping_cost = Decimal('0.00') initial = { - 'total_net_amount': cart.get_total_price(), + 'total_amount': cart.get_total_price(), 'shipping_total': shipping_cost } if self.request.session.get('shipping_address'): @@ -326,8 +321,12 @@ class OrderCreateView(CreateView): def form_valid(self, form): cart = Cart(self.request) + form.instance.subtotal_amount = cart.get_subtotal_price_after_discount() + form.instance.coupon_amount = cart.get_discount() + form.instance.total_amount = cart.get_total_price_after_discount() + form.instance.weight = cart.get_total_weight() shipping_address = self.request.session.get('shipping_address') - shipping_container = self.request.session.get('shipping_container') + shipping_container = self.request.session.get('shipping_container').container form.instance.customer, form.instance.shipping_address = get_or_create_customer(self.request, form, shipping_address) form.instance.status = OrderStatus.DRAFT self.object = form.save() @@ -373,14 +372,6 @@ def paypal_order_transaction_capture(request, transaction_id): return JsonResponse({'details': 'invalid request'}) -@csrf_exempt -@require_POST -def paypal_webhook_endpoint(request): - data = json.loads(request.body) - logger.info(data) - return JsonResponse(data) - - class PaymentDoneView(TemplateView): template_name = 'storefront/payment_done.html'
Subtotal${{order.total_net_amount}}${{order.subtotal}}
Subtotal ${{cart.get_total_price|floatformat:"2"}}
Coupon {{cart.coupon.discount_value}} {{cart.coupon.get_discount_value_type_display}}