From c598f805e20936b397218f7db2866263881a5c34 Mon Sep 17 00:00:00 2001 From: Nathan Chapman Date: Sat, 29 Jul 2023 11:48:17 -0600 Subject: [PATCH] Add base for wholesale orders --- accounts/fixtures/accounts.json | 44 ++----- core/fixtures/orders.json | 27 ++++- core/migrations/0009_wholesaleorder.py | 41 +++++++ core/models.py | 75 ++++++++++++ dashboard/forms.py | 1 + static/styles/main.css | 6 + storefront/forms.py | 108 +++++++++++++++++- .../templates/storefront/wholesale_order.html | 35 ++++++ .../wholesale_order_create_form.html | 31 +++++ .../storefront/wholesale_order_detail.html | 73 ++++++++++++ storefront/urls.py | 19 +++ storefront/views.py | 50 +++++++- templates/base.html | 1 + 13 files changed, 469 insertions(+), 42 deletions(-) create mode 100644 core/migrations/0009_wholesaleorder.py create mode 100644 storefront/templates/storefront/wholesale_order.html create mode 100644 storefront/templates/storefront/wholesale_order_create_form.html create mode 100644 storefront/templates/storefront/wholesale_order_detail.html diff --git a/accounts/fixtures/accounts.json b/accounts/fixtures/accounts.json index 7087acb..7b717de 100644 --- a/accounts/fixtures/accounts.json +++ b/accounts/fixtures/accounts.json @@ -1,28 +1,4 @@ [{ - "model": "accounts.address", - "pk": 1, - "fields": { - "first_name": "Nathan", - "last_name": "Chapman", - "street_address_1": "1504 N 230 E", - "street_address_2": "", - "city": "North Logan", - "state": "UT", - "postal_code": "84341" - } -}, { - "model": "accounts.address", - "pk": 2, - "fields": { - "first_name": "John", - "last_name": "Doe", - "street_address_1": "90415 Pollich Skyway", - "street_address_2": "", - "city": "Jaskolskiburgh", - "state": "MS", - "postal_code": "32715" - } -}, { "model": "accounts.user", "pk": 1, "fields": { @@ -36,11 +12,13 @@ "is_staff": true, "is_active": true, "date_joined": "2022-04-28T01:24:47.591Z", - "default_shipping_address": 1, - "default_billing_address": null, + "shipping_street_address_1": "1504 N 230 E", + "shipping_street_address_2": "", + "shipping_city": "North Logan", + "shipping_state": "UT", + "shipping_postal_code": "84341", "groups": [], - "user_permissions": [], - "addresses": [] + "user_permissions": [] } }, { "model": "accounts.user", @@ -56,10 +34,12 @@ "is_staff": false, "is_active": true, "date_joined": "2022-05-04T00:00:11Z", - "default_shipping_address": 2, - "default_billing_address": null, + "shipping_street_address_1": "90415 Pollich Skyway", + "shipping_street_address_2": "", + "shipping_city": "Jaskolskiburgh", + "shipping_state": "MS", + "shipping_postal_code": "32715", "groups": [], - "user_permissions": [], - "addresses": [] + "user_permissions": [] } }] diff --git a/core/fixtures/orders.json b/core/fixtures/orders.json index 1fbc41e..a596e4b 100644 --- a/core/fixtures/orders.json +++ b/core/fixtures/orders.json @@ -5,8 +5,13 @@ "fields": { "customer": 1, "status": "unfulfilled", - "billing_address": null, - "shipping_address": 1, + "shipping_first_name": "Nathan", + "shipping_last_name": "Chapman", + "shipping_street_address_1": "1504 N 230 E", + "shipping_street_address_2": "", + "shipping_city": "North Logan", + "shipping_state": "UT", + "shipping_postal_code": "84341", "coupon": null, "subtotal_amount": "26.80", "coupon_amount": "0.00", @@ -22,8 +27,13 @@ "fields": { "customer": 1, "status": "unfulfilled", - "billing_address": null, - "shipping_address": 1, + "shipping_first_name": "Nathan", + "shipping_last_name": "Chapman", + "shipping_street_address_1": "1504 N 230 E", + "shipping_street_address_2": "", + "shipping_city": "North Logan", + "shipping_state": "UT", + "shipping_postal_code": "84341", "coupon": null, "subtotal_amount": "26.80", "coupon_amount": "0.00", @@ -39,8 +49,13 @@ "fields": { "customer": 2, "status": "unfulfilled", - "billing_address": null, - "shipping_address": 1, + "shipping_first_name": "John", + "shipping_last_name": "Doe", + "shipping_street_address_1": "90415 Pollich Skyway", + "shipping_street_address_2": "", + "shipping_city": "Jaskolskiburgh", + "shipping_state": "MS", + "shipping_postal_code": "32715", "coupon": null, "subtotal_amount": "26.80", "coupon_amount": "0.00", diff --git a/core/migrations/0009_wholesaleorder.py b/core/migrations/0009_wholesaleorder.py new file mode 100644 index 0000000..ee5af24 --- /dev/null +++ b/core/migrations/0009_wholesaleorder.py @@ -0,0 +1,41 @@ +# Generated by Django 4.1.6 on 2023-07-29 02:28 + +from django.conf import settings +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('core', '0008_remove_order_billing_address_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='WholesaleOrder', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('brazil', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50)]), size=2)), + ('dantes_tornado', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50)]), size=2)), + ('decaf', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50)]), size=2)), + ('ethiopia', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50)]), size=2)), + ('loop_d_loop', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50)]), size=2)), + ('moka_java', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50)]), size=2)), + ('nicaragua', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50)]), size=2)), + ('pantomime', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50)]), size=2)), + ('sumatra', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50)]), size=2)), + ('fulfilled', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('customer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='wholesale_orders', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'wholesale order', + 'verbose_name_plural': 'wholesale orders', + }, + ), + ] diff --git a/core/models.py b/core/models.py index f265bd2..68d940c 100644 --- a/core/models.py +++ b/core/models.py @@ -14,6 +14,7 @@ from django.core.validators import MinValueValidator, MaxValueValidator from django.core.serializers.json import DjangoJSONEncoder from django.contrib.postgres.fields import ArrayField, HStoreField from django.forms.models import model_to_dict +from django.contrib.postgres.fields import ArrayField from django_measurement.models import MeasurementField from localflavor.us.us_states import USPS_CHOICES @@ -688,3 +689,77 @@ class SiteSettings(SingletonBase): class Meta: verbose_name = 'Site Settings' verbose_name_plural = 'Site Settings' + + +class WholesaleOrder(models.Model): + customer = models.ForeignKey( + User, + related_name='wholesale_orders', + on_delete=models.SET_NULL, + null=True + ) + + brazil = ArrayField( + models.PositiveSmallIntegerField(default=0, validators=[ + MinValueValidator(0), MaxValueValidator(50) + ]), + size=2, + ) + dantes_tornado = ArrayField( + models.PositiveSmallIntegerField(default=0, validators=[ + MinValueValidator(0), MaxValueValidator(50) + ]), + size=2, + ) + decaf = ArrayField( + models.PositiveSmallIntegerField(default=0, validators=[ + MinValueValidator(0), MaxValueValidator(50) + ]), + size=2, + ) + ethiopia = ArrayField( + models.PositiveSmallIntegerField(default=0, validators=[ + MinValueValidator(0), MaxValueValidator(50) + ]), + size=2, + ) + loop_d_loop = ArrayField( + models.PositiveSmallIntegerField(default=0, validators=[ + MinValueValidator(0), MaxValueValidator(50) + ]), + size=2, + ) + moka_java = ArrayField( + models.PositiveSmallIntegerField(default=0, validators=[ + MinValueValidator(0), MaxValueValidator(50) + ]), + size=2, + ) + nicaragua = ArrayField( + models.PositiveSmallIntegerField(default=0, validators=[ + MinValueValidator(0), MaxValueValidator(50) + ]), + size=2, + ) + pantomime = ArrayField( + models.PositiveSmallIntegerField(default=0, validators=[ + MinValueValidator(0), MaxValueValidator(50) + ]), + size=2, + ) + sumatra = ArrayField( + models.PositiveSmallIntegerField(default=0, validators=[ + MinValueValidator(0), MaxValueValidator(50) + ]), + size=2, + ) + + fulfilled = models.BooleanField(default=False) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = "wholesale order" + verbose_name_plural = "wholesale orders" + diff --git a/dashboard/forms.py b/dashboard/forms.py index ae168ab..ea2e5fc 100644 --- a/dashboard/forms.py +++ b/dashboard/forms.py @@ -122,3 +122,4 @@ class ProductPhotoForm(forms.ModelForm): class Meta: model = ProductPhoto fields = ('image',) + diff --git a/static/styles/main.css b/static/styles/main.css index ebe5083..a58acf0 100644 --- a/static/styles/main.css +++ b/static/styles/main.css @@ -1134,3 +1134,9 @@ footer > section { .show-modal { white-space: unset; } + + +#wholesale-faq { + display: flex; + gap: 3rem; +} diff --git a/storefront/forms.py b/storefront/forms.py index 7b3c377..f3b840a 100644 --- a/storefront/forms.py +++ b/storefront/forms.py @@ -11,12 +11,27 @@ from localflavor.us.us_states import USPS_CHOICES from usps import USPSApi, Address from django_measurement.forms import MeasurementField -from core.models import Order, ProductVariant, Subscription, SiteSettings +from core.models import ( + Order, ProductVariant, Subscription, SiteSettings, WholesaleOrder +) from core import CoffeeGrind, ShippingContainer logger = logging.getLogger(__name__) + +class SplitWidget(forms.MultiWidget): + def decompress(self, value): + if value: + return [value[0], value[1]] + return [0, 0] + + def value_from_datadict(self, data, files, name): + sixteen, five = super().value_from_datadict(data, files, name) + return [sixteen, five] + + + class VariantChoiceField(forms.ModelChoiceField): def label_from_instance(self, obj): return f'{obj.name} | ${obj.price}' @@ -142,3 +157,94 @@ class SubscriptionForm(forms.Form): stripe_price_id = forms.CharField(widget=forms.HiddenInput()) total_quantity = forms.IntegerField(widget=forms.HiddenInput()) total_weight = forms.CharField(widget=forms.HiddenInput()) + + +class WholesaleOrderCreateForm(forms.ModelForm): + class Meta: + model = WholesaleOrder + fields = [ + "brazil", + "dantes_tornado", + "decaf", + "ethiopia", + "loop_d_loop", + "moka_java", + "nicaragua", + "pantomime", + "sumatra", + ] + widgets = { + "brazil": SplitWidget(widgets={ + "16": forms.NumberInput(attrs={ + "size": 5 + }), + "5": forms.NumberInput(attrs={ + "size": 5 + }), + }), + "dantes_tornado": SplitWidget(widgets={ + "16": forms.NumberInput(attrs={ + "size": 5 + }), + "5": forms.NumberInput(attrs={ + "size": 5 + }), + }), + "decaf": SplitWidget(widgets={ + "16": forms.NumberInput(attrs={ + "size": 5 + }), + "5": forms.NumberInput(attrs={ + "size": 5 + }), + }), + "ethiopia": SplitWidget(widgets={ + "16": forms.NumberInput(attrs={ + "size": 5 + }), + "5": forms.NumberInput(attrs={ + "size": 5 + }), + }), + "loop_d_loop": SplitWidget(widgets={ + "16": forms.NumberInput(attrs={ + "size": 5 + }), + "5": forms.NumberInput(attrs={ + "size": 5 + }), + }), + "moka_java": SplitWidget(widgets={ + "16": forms.NumberInput(attrs={ + "size": 5 + }), + "5": forms.NumberInput(attrs={ + "size": 5 + }), + }), + "nicaragua": SplitWidget(widgets={ + "16": forms.NumberInput(attrs={ + "size": 5 + }), + "5": forms.NumberInput(attrs={ + "size": 5 + }), + }), + "pantomime": SplitWidget(widgets={ + "16": forms.NumberInput(attrs={ + "size": 5 + }), + "5": forms.NumberInput(attrs={ + "size": 5 + }), + }), + "sumatra": SplitWidget(widgets={ + "16": forms.NumberInput(attrs={ + "size": 5 + }), + "5": forms.NumberInput(attrs={ + "size": 5 + }), + }), + } + diff --git a/storefront/templates/storefront/wholesale_order.html b/storefront/templates/storefront/wholesale_order.html new file mode 100644 index 0000000..7cdc9f5 --- /dev/null +++ b/storefront/templates/storefront/wholesale_order.html @@ -0,0 +1,35 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+

Wholesale

+
+ + {% if user.is_authenticated %} +
+

New wholesale order

+ + {% for order in user.wholesale_orders.all %} + + + + + {% endfor %} +
{{ order.created_at }} + +
+ {% else %} +
+
+

I'm already a wholesale customer

+

If you are already a wholesale customer, please login to place an order.

+

+
+

I would like to become a wholesale customer

+

You must become a wholesale customer to place an order. Contact us to become a wholesale customer.

+
+
+ {% endif %} +
+{% endblock %} diff --git a/storefront/templates/storefront/wholesale_order_create_form.html b/storefront/templates/storefront/wholesale_order_create_form.html new file mode 100644 index 0000000..99fe83a --- /dev/null +++ b/storefront/templates/storefront/wholesale_order_create_form.html @@ -0,0 +1,31 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+

Place a wholesale order

+
+
+
+ {% csrf_token %} + + + + + + + {{ form.as_table }} + + + + + + + +
Product16 oz Qty / 5 lb Qty
+ +
+
+
+
+{% endblock %} diff --git a/storefront/templates/storefront/wholesale_order_detail.html b/storefront/templates/storefront/wholesale_order_detail.html new file mode 100644 index 0000000..b6d53a1 --- /dev/null +++ b/storefront/templates/storefront/wholesale_order_detail.html @@ -0,0 +1,73 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+

← Back

+
+

Wholesale Order No. {{order.pk}}

+

Placed on {{order.created_at|date:"M j, Y"}}

+
+ +
+ + + + + + + + + + + + {% for qty in order.brazil %} + + {% endfor %} + + + + {% for qty in order.dantes_tornado %} + + {% endfor %} + + + {% for qty in order.decaf %} + + {% endfor %} + + + {% for qty in order.ethiopia %} + + {% endfor %} + + + {% for qty in order.loop_d_loop %} + + {% endfor %} + + + {% for qty in order.moka_java %} + + {% endfor %} + + + {% for qty in order.nicaragua %} + + {% endfor %} + + + {% for qty in order.pantomime %} + + {% endfor %} + + + {% for qty in order.sumatra %} + + {% endfor %} + + +
Product16 oz5 lb
Brazil{% if qty %}Qty: {{ qty }}{% endif %}
Dante's Tornado{% if qty %}Qty: {{ qty }}{% endif %}
Decaf{% if qty %}Qty: {{ qty }}{% endif %}
Ethiopia{% if qty %}Qty: {{ qty }}{% endif %}
Loop d' Loop{% if qty %}Qty: {{ qty }}{% endif %}
Moka Java{% if qty %}Qty: {{ qty }}{% endif %}
Nicaragua{% if qty %}Qty: {{ qty }}{% endif %}
Pantomime{% if qty %}Qty: {{ qty }}{% endif %}
Sumatra{% if qty %}Qty: {{ qty }}{% endif %}
+
+
+{% endblock content %} diff --git a/storefront/urls.py b/storefront/urls.py index bf34d62..9372ca9 100644 --- a/storefront/urls.py +++ b/storefront/urls.py @@ -99,6 +99,13 @@ urlpatterns = [ views.OrderDetailView.as_view(), name='order-detail', ), + path('wholesale-orders//', include([ + path( + '', + views.WholesaleOrderDetailView.as_view(), + name='wholesale-order-detail' + ), + ])), ])), path( @@ -130,4 +137,16 @@ urlpatterns = [ name='subscription-done' ), ])), + + # Wholesale Orders + path( + 'wholesale-orders/', + views.WholesaleOrderView.as_view(), + name='wholesale-order' + ), + path( + 'wholesale-orders/new/', + views.WholesaleOrderCreateView.as_view(), + name='wholesale-order-create' + ), ] diff --git a/storefront/views.py b/storefront/views.py index cd541c3..a3ab0c4 100644 --- a/storefront/views.py +++ b/storefront/views.py @@ -19,7 +19,9 @@ from django.views.generic.edit import ( from django.views.generic.detail import DetailView, SingleObjectMixin from django.views.generic.list import ListView from django.contrib.auth.decorators import login_required -from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin +from django.contrib.auth.mixins import ( + PermissionRequiredMixin, LoginRequiredMixin, UserPassesTestMixin +) from django.contrib.messages.views import SuccessMessageMixin from django.contrib import messages from django.views.decorators.csrf import csrf_exempt @@ -40,7 +42,7 @@ from accounts.forms import CustomerUpdateForm, CustomerShippingAddressUpdateForm from core.models import ( ProductCategory, Product, ProductVariant, ProductOption, Order, Transaction, OrderLine, Coupon, ShippingRate, - Subscription, SiteSettings + Subscription, SiteSettings, WholesaleOrder ) from core.forms import ShippingRateForm from core.shipping import get_shipping_cost @@ -49,7 +51,7 @@ from core import OrderStatus, ShippingContainer, TransactionStatus from .forms import ( AddToCartForm, CartItemUpdateForm, OrderCreateForm, AddressForm, CouponApplyForm, CheckoutShippingForm, - SubscriptionForm + SubscriptionForm, WholesaleOrderCreateForm ) from .cart import CartItem, Cart from .payments import CaptureOrder @@ -842,3 +844,45 @@ def stripe_webhook(request): return JsonResponse({'status': 'success'}) + +class WholesaleOrderView(TemplateView): + template_name = "storefront/wholesale_order.html" + + +class WholesaleOrderCreateView( + PermissionRequiredMixin, LoginRequiredMixin, CreateView +): + permission_required = "core.create_wholesaleorder" + model = WholesaleOrder + template_name = 'storefront/wholesale_order_create_form.html' + form_class = WholesaleOrderCreateForm + + def get_success_url(self): + return reverse( + 'storefront:wholesale-order-detail', kwargs={ + 'pk': self.object.customer.pk, 'order_pk': self.object.pk + } + ) + + def form_valid(self, form): + form.instance.customer = self.request.user + return super().form_valid(form) + + +class WholesaleOrderDetailView( + PermissionRequiredMixin, LoginRequiredMixin, DetailView +): + permission_required = "core.view_wholesaleorder" + model = WholesaleOrder + context_object_name = 'order' + pk_url_kwarg = 'order_pk' + template_name = 'storefront/wholesale_order_detail.html' + + def test_func(self): + return self.request.user.pk == self.get_object().customer.pk + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['customer'] = User.objects.get(pk=self.kwargs['pk']) + return context + diff --git a/templates/base.html b/templates/base.html index dfd0def..e3568f6 100644 --- a/templates/base.html +++ b/templates/base.html @@ -54,6 +54,7 @@
  • {{ category }}
  • {% endfor %}
  • Subscribe
  • +
  • Wholesale
  • Fair trade
  • Reviews
  • About