diff --git a/src/accounts/models.py b/src/accounts/models.py index 1647c59..bdda95d 100644 --- a/src/accounts/models.py +++ b/src/accounts/models.py @@ -19,10 +19,10 @@ class Address(models.Model): def __str__(self): return f""" - {first_name} {last_name} - {street_address_1} - {street_address_2} - {city}, {state}, {postal_code} + {self.first_name} {self.last_name} + {self.street_address_1} + {self.street_address_2} + {self.city}, {self.state}, {self.postal_code} """ diff --git a/src/core/migrations/0002_shippingrate_is_selectable_and_more.py b/src/core/migrations/0002_shippingrate_is_selectable_and_more.py new file mode 100644 index 0000000..130ee1b --- /dev/null +++ b/src/core/migrations/0002_shippingrate_is_selectable_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.0.2 on 2022-10-29 14:44 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='shippingrate', + name='is_selectable', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='sitesettings', + name='default_shipping_rate', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.shippingrate'), + ), + ] diff --git a/src/core/models.py b/src/core/models.py index 2962905..3e290e7 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -56,17 +56,6 @@ class SingletonBase(models.Model): abstract = True -class SiteSettings(SingletonBase): - usps_user_id = models.CharField(max_length=255) - - def __str__(self): - return 'Site Settings' - - class Meta: - verbose_name = 'Site Settings' - verbose_name_plural = 'Site Settings' - - class ProductCategory(models.Model): name = models.CharField(max_length=255) main_category = models.BooleanField(default=True) @@ -271,6 +260,7 @@ class ShippingRate(models.Model): blank=True, null=True ) + is_selectable = models.BooleanField(default=True) def get_absolute_url(self): return reverse('dashboard:rate-detail', kwargs={'pk': self.pk}) @@ -490,3 +480,21 @@ class Subscription(models.Model): on_delete=models.SET_NULL, null=True ) + + +class SiteSettings(SingletonBase): + usps_user_id = models.CharField(max_length=255) + default_shipping_rate = models.ForeignKey( + ShippingRate, + blank=True, + null=True, + related_name='+', + on_delete=models.SET_NULL + ) + + def __str__(self): + return 'Site Settings' + + class Meta: + verbose_name = 'Site Settings' + verbose_name_plural = 'Site Settings' diff --git a/src/dashboard/templates/dashboard/customer_list.html b/src/dashboard/templates/dashboard/customer_list.html index 5387de4..d7aac40 100644 --- a/src/dashboard/templates/dashboard/customer_list.html +++ b/src/dashboard/templates/dashboard/customer_list.html @@ -22,5 +22,24 @@ No customers {% endfor %} +
+ +
{% endblock content %} diff --git a/src/dashboard/templates/dashboard/stock.html b/src/dashboard/templates/dashboard/stock.html new file mode 100644 index 0000000..fe545ed --- /dev/null +++ b/src/dashboard/templates/dashboard/stock.html @@ -0,0 +1,25 @@ +{% extends "dashboard.html" %} +{% load static %} + +{% block content %} +
+
+

Stock

+
+ +
+
+

Products

+
+
+ {% for variant in variant_list %} +
+

{{ variant }}

+

Variant ID: {{ variant.pk }}

+

Total in warehouse: {{ variant.stock }}

+
+ {% endfor %} +
+
+
+{% endblock %} diff --git a/src/dashboard/urls.py b/src/dashboard/urls.py index b054de5..c2b6fdf 100644 --- a/src/dashboard/urls.py +++ b/src/dashboard/urls.py @@ -17,6 +17,11 @@ urlpatterns = [ views.CatalogView.as_view(), name='catalog' ), + path( + 'stock/', + views.StockView.as_view(), + name='stock' + ), path( 'shipping-rates/new/', diff --git a/src/dashboard/views.py b/src/dashboard/views.py index de64a44..7e5814e 100644 --- a/src/dashboard/views.py +++ b/src/dashboard/views.py @@ -101,6 +101,20 @@ class CatalogView(ListView): return context +class StockView(ListView): + model = ProductVariant + context_object_name = 'variant_list' + template_name = 'dashboard/stock.html' + + def get_queryset(self): + object_list = ProductVariant.objects.filter( + track_inventory=True + ).order_by('product') + # quantity + # quantity_fulfilled + return object_list + + class ShippingRateDetailView(LoginRequiredMixin, DetailView): model = ShippingRate context_object_name = 'rate' @@ -487,6 +501,7 @@ class ProductOptionDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteVie class CustomerListView(LoginRequiredMixin, ListView): model = User template_name = 'dashboard/customer_list.html' + paginate_by = 100 def get_queryset(self): object_list = User.objects.filter( diff --git a/src/static/images/warehouse.png b/src/static/images/warehouse.png new file mode 100644 index 0000000..bbede28 Binary files /dev/null and b/src/static/images/warehouse.png differ diff --git a/src/storefront/cart.py b/src/storefront/cart.py index 65d0f28..8e6d381 100644 --- a/src/storefront/cart.py +++ b/src/storefront/cart.py @@ -72,7 +72,7 @@ class Cart: if update_quantity: self.cart[item['variant']]['quantity'] = item['quantity'] else: - self.cart.append(item) + self.add_or_update_item(item) # TODO: abstract this to a function that will check the max amount of item in the cart if len(self) <= 20: @@ -80,6 +80,17 @@ class Cart: else: messages.warning(request, "Cart is full: 20 items or less.") + def add_or_update_item(self, new_item): + new_item_pk = int(new_item['variant']) + for item in self: + if new_item_pk == item['variant'].pk: + if new_item['options'] == item['options']: + item['quantity'] += new_item['quantity'] + return + else: + continue + self.cart.append(new_item) + def save(self): self.session[settings.CART_SESSION_ID] = self.cart self.session.modified = True @@ -123,6 +134,9 @@ class Cart: return 0 def get_shipping_container_choices(self): + is_selectable = Q( + is_selectable=True + ) min_weight_matched = Q( min_order_weight__lte=self.get_total_weight()) | Q( min_order_weight__isnull=True @@ -132,7 +146,7 @@ class Cart: max_order_weight__isnull=True ) containers = ShippingRate.objects.filter( - min_weight_matched & max_weight_matched + is_selectable & min_weight_matched & max_weight_matched ) return containers diff --git a/src/storefront/views.py b/src/storefront/views.py index 5994b41..a332159 100644 --- a/src/storefront/views.py +++ b/src/storefront/views.py @@ -7,6 +7,7 @@ from django.utils import timezone from django.shortcuts import render, reverse, redirect, get_object_or_404 from django.urls import reverse_lazy from django.core.mail import EmailMessage +from django.core.cache import cache from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.http import JsonResponse, HttpResponseRedirect from django.views.generic.base import View, RedirectView, TemplateView @@ -36,7 +37,8 @@ from accounts.forms import ( ) from core.models import ( ProductCategory, Product, ProductVariant, ProductOption, - Order, Transaction, OrderLine, Coupon, ShippingRate + Order, Transaction, OrderLine, Coupon, ShippingRate, + SiteSettings ) from core.forms import ShippingRateForm from core import OrderStatus, ShippingContainer @@ -222,7 +224,7 @@ class CheckoutAddressView(FormView): if user.is_authenticated and user.default_shipping_address: address = user.default_shipping_address initial = { - 'full_name': address.first_name+' '+address.last_name, + 'full_name': address.first_name + ' ' + address.last_name, 'email': user.email, 'street_address_1': address.street_address_1, 'street_address_2': address.street_address_2, @@ -233,7 +235,7 @@ class CheckoutAddressView(FormView): elif self.request.session.get('shipping_address'): address = self.request.session.get('shipping_address') initial = { - 'full_name': address['first_name']+' '+address['last_name'], + 'full_name': address['first_name'] + ' ' + address['last_name'], 'email': address['email'], 'street_address_1': address['street_address_1'], 'street_address_2': address['street_address_2'], @@ -267,6 +269,13 @@ class CheckoutShippingView(FormView): template_name = 'storefront/checkout_shipping_form.html' form_class = CheckoutShippingForm success_url = reverse_lazy('storefront:order-create') + containers = None + + def get_containers(self, request): + if self.containers is None: + cart = Cart(request) + self.containers = cart.get_shipping_container_choices() + return self.containers def get(self, request, *args, **kwargs): if not self.request.session.get('shipping_address'): @@ -274,16 +283,22 @@ class CheckoutShippingView(FormView): return HttpResponseRedirect( reverse('storefront:checkout-address') ) + site_settings = cache.get('SiteSettings') + cart = Cart(self.request) + if len(self.get_containers(request)) == 0: + self.request.session['shipping_container'] = site_settings.default_shipping_rate + return HttpResponseRedirect( + reverse('storefront:order-create') + ) return super().get(request, *args, **kwargs) def get_form(self, form_class=None): cart = Cart(self.request) - containers = cart.get_shipping_container_choices() - for container in containers: + for container in self.get_containers(self.request): 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()) + return form_class(self.get_containers(self.request), **self.get_form_kwargs()) def form_valid(self, form): shipping_container = ShippingRate.objects.get( @@ -400,7 +415,6 @@ def paypal_order_transaction_capture(request, transaction_id): transaction.save() cart.clear() logger.debug(f'\nPayPal Response data: {data}\n') - return JsonResponse(data) else: return JsonResponse({'details': 'invalid request'}) diff --git a/src/templates/dashboard.html b/src/templates/dashboard.html index 690da0b..6409bb3 100644 --- a/src/templates/dashboard.html +++ b/src/templates/dashboard.html @@ -33,6 +33,12 @@ Catalog + Orders