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/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}}
+
+
+
+{% 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
+
+
+
+
+
+
+
+
+ | Product |
+ 16 oz. |
+ 5 lb. |
+
+
+
+
+ | Brazil |
+ {% for qty in order.brazil %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+
+ | Dante's Tornado |
+ {% for qty in order.dantes_tornado %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+ | Decaf |
+ {% for qty in order.decaf %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+ | Ethiopia |
+ {% for qty in order.ethiopia %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+ | Loop d' Loop |
+ {% for qty in order.loop_d_loop %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+ | Moka Java |
+ {% for qty in order.moka_java %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+ | Nicaragua |
+ {% for qty in order.nicaragua %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+ | Pantomime |
+ {% for qty in order.pantomime %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+ | Sumatra |
+ {% for qty in order.sumatra %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+
+
+
+
+{% 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}}
+
+
+
+{% 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 %}
+
+
+
+ {% if user.is_authenticated and perms.core.add_wholesaleorder %}
+
+ New wholesale order
+ Orders
+
+ {% 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
+
+
+
+{% 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"}}
+
+
+
+
+
+
+ | Product |
+ 16 oz. |
+ 5 lb. |
+
+
+
+
+ | Brazil |
+ {% for qty in order.brazil %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+ | Dante's Tornado |
+ {% for qty in order.dantes_tornado %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+ | Decaf |
+ {% for qty in order.decaf %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+ | Ethiopia |
+ {% for qty in order.ethiopia %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+ | Loop d' Loop |
+ {% for qty in order.loop_d_loop %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+ | Moka Java |
+ {% for qty in order.moka_java %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+ | Nicaragua |
+ {% for qty in order.nicaragua %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+ | Pantomime |
+ {% for qty in order.pantomime %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+ | Sumatra |
+ {% for qty in order.sumatra %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+
+
+
+
+{% 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
+
+
+
+ | Product |
+ 16 oz. |
+ 5 lb. |
+
+
+
+
+ | Brazil |
+ {% for qty in brazil %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+ | Dante's Tornado |
+ {% for qty in dantes_tornado %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+ | Decaf |
+ {% for qty in decaf %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+ | Ethiopia |
+ {% for qty in ethiopia %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+ | Loop d' Loop |
+ {% for qty in loop_d_loop %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+ | Moka Java |
+ {% for qty in moka_java %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+ | Nicaragua |
+ {% for qty in nicaragua %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+ | Pantomime |
+ {% for qty in pantomime %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+ | Sumatra |
+ {% for qty in sumatra %}
+ {% if qty %}Qty: {{ qty }}{% endif %} |
+ {% endfor %}
+
+
+
+
+ If you have any questions, send us an email at support@ptcoffee.com.
+
+ Thanks,
+ Port Townsend Roasting Co.
+{% endblock %}