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/accounts/utils.py b/accounts/utils.py index 82a0144..8496e6a 100644 --- a/accounts/utils.py +++ b/accounts/utils.py @@ -8,7 +8,6 @@ def get_or_create_customer(request, shipping_address): user, u_created = User.objects.get_or_create( email=shipping_address['email'].lower(), defaults={ - 'username': shipping_address['email'].lower(), 'is_staff': False, 'is_active': True, 'is_superuser': False, 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/migrations/0010_wholesaleorder_is_cancelled.py b/core/migrations/0010_wholesaleorder_is_cancelled.py new file mode 100644 index 0000000..0d8db2a --- /dev/null +++ b/core/migrations/0010_wholesaleorder_is_cancelled.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.6 on 2023-08-06 18:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0009_wholesaleorder'), + ] + + operations = [ + migrations.AddField( + model_name='wholesaleorder', + name='is_cancelled', + field=models.BooleanField(default=False), + ), + ] diff --git a/core/migrations/0011_alter_wholesaleorder_options.py b/core/migrations/0011_alter_wholesaleorder_options.py new file mode 100644 index 0000000..16460f2 --- /dev/null +++ b/core/migrations/0011_alter_wholesaleorder_options.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.6 on 2023-08-06 18:53 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0010_wholesaleorder_is_cancelled'), + ] + + operations = [ + migrations.AlterModelOptions( + name='wholesaleorder', + options={'ordering': ['-created_at'], 'verbose_name': 'wholesale order', 'verbose_name_plural': 'wholesale orders'}, + ), + ] diff --git a/core/migrations/0012_rename_fulfilled_wholesaleorder_is_fulfilled.py b/core/migrations/0012_rename_fulfilled_wholesaleorder_is_fulfilled.py new file mode 100644 index 0000000..d9d9ab5 --- /dev/null +++ b/core/migrations/0012_rename_fulfilled_wholesaleorder_is_fulfilled.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.6 on 2023-08-23 01:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0011_alter_wholesaleorder_options'), + ] + + operations = [ + migrations.RenameField( + model_name='wholesaleorder', + old_name='fulfilled', + new_name='is_fulfilled', + ), + ] diff --git a/core/models.py b/core/models.py index f265bd2..0de6882 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,79 @@ 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, + ) + + is_fulfilled = models.BooleanField(default=False) + is_cancelled = 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" + ordering = ['-created_at'] + diff --git a/core/tasks.py b/core/tasks.py index 02fa9d3..13e60ae 100644 --- a/core/tasks.py +++ b/core/tasks.py @@ -11,8 +11,9 @@ logger = get_task_logger(__name__) CONFIRM_ORDER_TEMPLATE = 'storefront/order_confirmation' SHIP_ORDER_TEMPLATE = 'storefront/order_shipped' -ORDER_CANCEl_TEMPLATE = 'storefront/order_cancel' +ORDER_CANCEL_TEMPLATE = 'storefront/order_cancel' ORDER_REFUND_TEMPLATE = 'storefront/order_refund' +CONFIRM_WHOLESALE_ORDER_TEMPLATE = 'storefront/wholesale_order_confirmation' @shared_task(name='send_order_confirmation_email') @@ -27,6 +28,18 @@ def send_order_confirmation_email(order): logger.info(f"Order confirmation email sent to {order['email']}") +@shared_task(name='send_wholesale_order_confirmation_email') +def send_wholesale_order_confirmation_email(order): + send_templated_mail( + template_name=CONFIRM_WHOLESALE_ORDER_TEMPLATE, + from_email=SiteSettings.load().order_from_email, + recipient_list=[order['email']], + context=order + ) + + logger.info(f"WholesaleOrder confirmation email sent to {order['email']}") + + @shared_task(name='send_order_shipped_email') def send_order_shipped_email(data): send_templated_mail( diff --git a/dashboard/forms.py b/dashboard/forms.py index ae168ab..4f3351e 100644 --- a/dashboard/forms.py +++ b/dashboard/forms.py @@ -4,6 +4,7 @@ from django import forms from core import OrderStatus from core.models import ( ProductVariant, + WholesaleOrder, Order, OrderLine, ShippingRate, @@ -118,7 +119,26 @@ OrderTrackingFormset = forms.inlineformset_factory( ) +class WholesaleOrderCancelForm(forms.ModelForm): + class Meta: + model = WholesaleOrder + fields = ['is_cancelled'] + widgets = { + 'is_cancelled': forms.HiddenInput() + } + + +class WholesaleOrderFulfillForm(forms.ModelForm): + class Meta: + model = WholesaleOrder + fields = ['is_fulfilled'] + widgets = { + 'is_fulfilled': forms.HiddenInput() + } + + class ProductPhotoForm(forms.ModelForm): class Meta: model = ProductPhoto fields = ('image',) + diff --git a/dashboard/templates/dashboard/wholesale_order/_table.html b/dashboard/templates/dashboard/wholesale_order/_table.html new file mode 100644 index 0000000..0545927 --- /dev/null +++ b/dashboard/templates/dashboard/wholesale_order/_table.html @@ -0,0 +1,28 @@ + + + Order No. + Date + Customer + Fulfillment + + + + {% for order in order_list %} + + No. {{ order.pk }} + {{ order.created_at|date:"D, M j Y" }} + {{ order.customer.get_full_name }} + +
+ {% if order.is_fulfilled %} + Fulfilled + {% elif order.is_cancelled %} + Cancelled + {% else %} + Unfulfilled + {% endif %} +
+ + + {% endfor %} + diff --git a/dashboard/templates/dashboard/wholesale_order/cancel_form.html b/dashboard/templates/dashboard/wholesale_order/cancel_form.html new file mode 100644 index 0000000..f8eb1c9 --- /dev/null +++ b/dashboard/templates/dashboard/wholesale_order/cancel_form.html @@ -0,0 +1,26 @@ +{% extends 'dashboard.html' %} + +{% block head_title %}Cancel Wholesale Order No. {{ order.pk }} | {% endblock %} + +{% block content %} +
+

+ ← Back +

+
+

Cancel Wholesale Order No. {{order.pk}}

+
+
+
+

Are you sure you want to cancel Wholesale Order No. {{ order.pk }}?

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

+ +

+
+
+
+{% endblock content %} diff --git a/dashboard/templates/dashboard/wholesale_order/detail.html b/dashboard/templates/dashboard/wholesale_order/detail.html new file mode 100644 index 0000000..21c663b --- /dev/null +++ b/dashboard/templates/dashboard/wholesale_order/detail.html @@ -0,0 +1,108 @@ +{% extends 'dashboard.html' %} +{% load static %} + +{% block head_title %}Order No. {{ order.pk }} | {% endblock %} + +{% block content %} +
+

+ ← Back to wholesale +

+
+

Wholesale Order No. {{order.pk}}

+ {% if perms.core.cancel_order and not order.is_cancelled %} + Cancel order + {% endif %} +
+
+
+

Details

+
+
+
Date
+
{{ order.created_at }}
+ +
Customer
+
+ {{order.customer.get_full_name}}  + {{order.customer.email}} ↗ +
+ +
Status
+
+ {% if order.is_fulfilled %} + Fulfilled + {% elif order.is_cancelled %} + Cancelled + {% else %} + Unfulfilled + {% endif %} +
+
+
+
+
+

Items

+ Fulfill order → +
+ + + + + + + + + + + + {% 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 oz.5 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/dashboard/templates/dashboard/wholesale_order/fulfill_form.html b/dashboard/templates/dashboard/wholesale_order/fulfill_form.html new file mode 100644 index 0000000..db2c147 --- /dev/null +++ b/dashboard/templates/dashboard/wholesale_order/fulfill_form.html @@ -0,0 +1,26 @@ +{% extends 'dashboard.html' %} + +{% block head_title %}Fulfill Wholesale Order No. {{ order.pk }} | {% endblock %} + +{% block content %} +
+

+ ← Back +

+
+

Fulfill Wholesale Order No. {{order.pk}}

+
+
+
+

Are you sure you want to fulfill Wholesale Order No. {{ order.pk }}?

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

+ +

+
+
+
+{% endblock content %} diff --git a/dashboard/templates/dashboard/wholesale_order/list.html b/dashboard/templates/dashboard/wholesale_order/list.html new file mode 100644 index 0000000..ee8a9f0 --- /dev/null +++ b/dashboard/templates/dashboard/wholesale_order/list.html @@ -0,0 +1,40 @@ +{% extends 'dashboard.html' %} +{% load static %} + +{% block head_title %}Wholesale Orders | {% endblock %} + +{% block content %} +
+
+

Wholesale Orders

+
+
+
+
+ + {% include 'dashboard/wholesale_order/_table.html' with order_list=order_list %} + + + + + +
+ +
+
+
+{% endblock content %} diff --git a/dashboard/urls.py b/dashboard/urls.py index f2ab9d6..11813c2 100644 --- a/dashboard/urls.py +++ b/dashboard/urls.py @@ -254,4 +254,28 @@ urlpatterns = [ name='customer-update' ), ])), + + # Wholesale Orders + path( + 'wholesale-orders/', + views.WholesaleOrderListView.as_view(), + name='wholesale-order-list' + ), + path('wholesale-orders//', include([ + path( + '', + views.WholesaleOrderDetailView.as_view(), + name='wholesale-order-detail' + ), + path( + 'fulfill/', + views.WholesaleOrderFulfillView.as_view(), + name='wholesale-order-fulfill' + ), + path( + 'cancel/', + views.WholesaleOrderCancelView.as_view(), + name='wholesale-order-cancel' + ), + ])), ] diff --git a/dashboard/views.py b/dashboard/views.py index ffffee5..b589202 100644 --- a/dashboard/views.py +++ b/dashboard/views.py @@ -43,6 +43,7 @@ from core.models import ( Transaction, TrackingNumber, Coupon, + WholesaleOrder, SiteSettings ) @@ -57,6 +58,8 @@ from .forms import ( OrderLineFormset, OrderCancelForm, OrderTrackingFormset, + WholesaleOrderFulfillForm, + WholesaleOrderCancelForm, CouponForm, ProductPhotoForm ) @@ -723,3 +726,53 @@ class CustomerUpdateView( def get_success_url(self): return reverse('dashboard:customer-detail', kwargs={'pk': self.object.pk}) + + +class WholesaleOrderListView(LoginRequiredMixin, ListView): + model = WholesaleOrder + template_name = 'dashboard/wholesale_order/list.html' + context_object_name = 'order_list' + paginate_by = 50 + + +class WholesaleOrderDetailView( + LoginRequiredMixin, DetailView +): + model = WholesaleOrder + context_object_name = 'order' + template_name = 'dashboard/wholesale_order/detail.html' + + +class WholesaleOrderCancelView( + LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView +): + permission_required = 'core.cancel_order' + model = WholesaleOrder + context_object_name = 'order' + template_name = 'dashboard/wholesale_order/cancel_form.html' + form_class = WholesaleOrderCancelForm + success_message = 'Wholesale Order canceled.' + initial = { + 'is_cancelled': True + } + + def get_success_url(self): + return reverse('dashboard:wholesale-order-detail', kwargs={'pk': self.object.pk}) + + +class WholesaleOrderFulfillView( + LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView +): + permission_required = 'core.change_wholesaleorder' + model = WholesaleOrder + context_object_name = 'order' + template_name = 'dashboard/wholesale_order/fulfill_form.html' + form_class = WholesaleOrderFulfillForm + success_message = 'Wholesale Order fulfilled.' + initial = { + 'is_fulfilled': True + } + + def get_success_url(self): + return reverse('dashboard:wholesale-order-detail', kwargs={'pk': self.object.pk}) + diff --git a/static/images/pallet.png b/static/images/pallet.png new file mode 100644 index 0000000..8bc9599 Binary files /dev/null and b/static/images/pallet.png differ diff --git a/static/styles/main.css b/static/styles/main.css index ebe5083..e56ac88 100644 --- a/static/styles/main.css +++ b/static/styles/main.css @@ -1134,3 +1134,22 @@ footer > section { .show-modal { white-space: unset; } + + +#wholesale-faq { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 3rem; +} + +.wholesale-detail, +.wholesale-fields { + width: fit-content; +} +.wholesale-field { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 0.5rem; + margin-bottom: 1rem; +} + 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..d97ab13 --- /dev/null +++ b/storefront/templates/storefront/wholesale_order.html @@ -0,0 +1,41 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+

Wholesale

+
+ + {% if user.is_authenticated and perms.core.add_wholesaleorder %} +
+

New wholesale order

+

Orders

+ + {% for order in user.wholesale_orders.all %} + + + + {% empty %} + + + + {% endfor %} +
+ + No. {{ order.pk }} | {{ order.created_at }} + +
No wholesale order placed yet.
+ {% else %} +
+
+

I would like to become a wholesale customer

+

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

+
+
+

I'm already a wholesale customer

+

If you are already a wholesale customer, please login and return to this page to place an order.

+

+
+ {% 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..2d39111 --- /dev/null +++ b/storefront/templates/storefront/wholesale_order_create_form.html @@ -0,0 +1,35 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+

Place a wholesale order

+
+
+
+ {% csrf_token %} +
+
+ Product + 16 oz. + 5 lb. + +
+ {% for field in form %} +
+ {{ field.label_tag }} + {{ field }} +
+ {{ field.errors }} + {% if field.help_text %} +

{{ field.help_text|safe }}

+ {% endif %} +
+
+ {% endfor %} +
+ +
+
+
+{% 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..afffd10 --- /dev/null +++ b/storefront/templates/storefront/wholesale_order_detail.html @@ -0,0 +1,72 @@ +{% 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 oz.5 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..06ff166 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 @@ -37,10 +39,11 @@ from moneyed import Money, USD from accounts.models import User from accounts.utils import get_or_create_customer from accounts.forms import CustomerUpdateForm, CustomerShippingAddressUpdateForm +from core.tasks import send_wholesale_order_confirmation_email 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 +52,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 +845,62 @@ def stripe_webhook(request): return JsonResponse({'status': 'success'}) + +class WholesaleOrderView(TemplateView): + template_name = "storefront/wholesale_order.html" + + +class WholesaleOrderCreateView( + PermissionRequiredMixin, LoginRequiredMixin, SuccessMessageMixin, CreateView +): + permission_required = "core.add_wholesaleorder" + model = WholesaleOrder + template_name = 'storefront/wholesale_order_create_form.html' + form_class = WholesaleOrderCreateForm + success_message = "Wholesale order placed. Thank you." + + 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 + self.object = form.save() + order = { + 'order_id': self.object.pk, + 'full_name': self.object.customer.get_full_name(), + 'email': self.object.customer.email, + 'brazil': self.object.brazil, + 'dantes_tornado': self.object.dantes_tornado, + 'decaf': self.object.decaf, + 'ethiopia': self.object.ethiopia, + 'loop_d_loop': self.object.loop_d_loop, + 'moka_java': self.object.moka_java, + 'nicaragua': self.object.nicaragua, + 'pantomime': self.object.pantomime, + 'sumatra': self.object.sumatra + } + send_wholesale_order_confirmation_email.delay(order) + 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..f65242d 100644 --- a/templates/base.html +++ b/templates/base.html @@ -54,6 +54,7 @@
  • {{ category }}
  • {% endfor %}
  • Subscribe
  • +
  • Wholesale
  • Fair trade
  • Reviews
  • About
  • @@ -86,11 +87,11 @@
    {% if messages %} -
    - {% for message in messages %} -

    {{ message }}

    - {% endfor %} -
    +
    + {% for message in messages %} +

    {{ message }}

    + {% endfor %} +
    {% endif %} {% block content %} {% endblock content %} diff --git a/templates/dashboard.html b/templates/dashboard.html index 0ed2c7f..5b7d2c6 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -48,6 +48,10 @@ Orders + + + Wholesale + Customers diff --git a/templates/templated_email/storefront/wholesale_order_confirmation.email b/templates/templated_email/storefront/wholesale_order_confirmation.email new file mode 100644 index 0000000..e85a4e5 --- /dev/null +++ b/templates/templated_email/storefront/wholesale_order_confirmation.email @@ -0,0 +1,84 @@ +{% block subject %}PT Coffee: Confirmation for Wholesale Order No. {{ order_id }}{% endblock %} +{% block html %} + + + +

    Thank you for your order!

    + +

    Hi {{ full_name }}, your order details are below.

    + +

    Order Summary

    + + + + + + + + + + + + {% for qty in brazil %} + + {% endfor %} + + + {% for qty in dantes_tornado %} + + {% endfor %} + + + {% for qty in decaf %} + + {% endfor %} + + + {% for qty in ethiopia %} + + {% endfor %} + + + {% for qty in loop_d_loop %} + + {% endfor %} + + + {% for qty in moka_java %} + + {% endfor %} + + + {% for qty in nicaragua %} + + {% endfor %} + + + {% for qty in pantomime %} + + {% endfor %} + + + {% for qty in sumatra %} + + {% endfor %} + + +
    Product16 oz.5 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 %}
    + +

    If you have any questions, send us an email at support@ptcoffee.com.

    + +

    Thanks,
    + Port Townsend Roasting Co.

    +{% endblock %}