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 68d940c..0de6882 100644
--- a/core/models.py
+++ b/core/models.py
@@ -754,7 +754,8 @@ class WholesaleOrder(models.Model):
size=2,
)
- fulfilled = models.BooleanField(default=False)
+ 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)
@@ -762,4 +763,5 @@ class WholesaleOrder(models.Model):
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 ea2e5fc..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,6 +119,24 @@ 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
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 1d2db15..e56ac88 100644
--- a/static/styles/main.css
+++ b/static/styles/main.css
@@ -1141,8 +1141,15 @@ footer > section {
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/templates/storefront/wholesale_order.html b/storefront/templates/storefront/wholesale_order.html
index dc97cce..d97ab13 100644
--- a/storefront/templates/storefront/wholesale_order.html
+++ b/storefront/templates/storefront/wholesale_order.html
@@ -6,17 +6,23 @@
Wholesale
- {% if user.is_authenticated %}
+ {% if user.is_authenticated and perms.core.add_wholesaleorder %}
New wholesale order
+ Orders
{% else %}
diff --git a/storefront/templates/storefront/wholesale_order_detail.html b/storefront/templates/storefront/wholesale_order_detail.html
index b6d53a1..afffd10 100644
--- a/storefront/templates/storefront/wholesale_order_detail.html
+++ b/storefront/templates/storefront/wholesale_order_detail.html
@@ -3,65 +3,64 @@
{% block content %}
- ← Back
+ ← Back
Wholesale Order No. {{order.pk}}
Placed on {{order.created_at|date:"M j, Y"}}
-
+
| Product |
- 16 oz |
- 5 lb |
+ 16 oz. |
+ 5 lb. |
- | Brazil |
+ Brazil |
{% for qty in order.brazil %}
{% if qty %}Qty: {{ qty }}{% endif %} |
{% endfor %}
-
-
- | Dante's Tornado |
+
+ | Dante's Tornado |
{% for qty in order.dantes_tornado %}
{% if qty %}Qty: {{ qty }}{% endif %} |
{% endfor %}
- | Decaf |
+ Decaf |
{% for qty in order.decaf %}
{% if qty %}Qty: {{ qty }}{% endif %} |
{% endfor %}
- | Ethiopia |
+ Ethiopia |
{% for qty in order.ethiopia %}
{% if qty %}Qty: {{ qty }}{% endif %} |
{% endfor %}
- | Loop d' Loop |
+ Loop d' Loop |
{% for qty in order.loop_d_loop %}
{% if qty %}Qty: {{ qty }}{% endif %} |
{% endfor %}
- | Moka Java |
+ Moka Java |
{% for qty in order.moka_java %}
{% if qty %}Qty: {{ qty }}{% endif %} |
{% endfor %}
- | Nicaragua |
+ Nicaragua |
{% for qty in order.nicaragua %}
{% if qty %}Qty: {{ qty }}{% endif %} |
{% endfor %}
- | Pantomime |
+ Pantomime |
{% for qty in order.pantomime %}
{% if qty %}Qty: {{ qty }}{% endif %} |
{% endfor %}
- | Sumatra |
+ Sumatra |
{% for qty in order.sumatra %}
{% if qty %}Qty: {{ qty }}{% endif %} |
{% endfor %}
diff --git a/storefront/views.py b/storefront/views.py
index d9df71b..06ff166 100644
--- a/storefront/views.py
+++ b/storefront/views.py
@@ -39,6 +39,7 @@ 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,
@@ -850,9 +851,9 @@ class WholesaleOrderView(TemplateView):
class WholesaleOrderCreateView(
- PermissionRequiredMixin, LoginRequiredMixin, CreateView, SuccessMessageMixin
+ PermissionRequiredMixin, LoginRequiredMixin, SuccessMessageMixin, CreateView
):
- permission_required = "core.create_wholesaleorder"
+ permission_required = "core.add_wholesaleorder"
model = WholesaleOrder
template_name = 'storefront/wholesale_order_create_form.html'
form_class = WholesaleOrderCreateForm
@@ -867,6 +868,22 @@ class WholesaleOrderCreateView(
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)
diff --git a/templates/base.html b/templates/base.html
index e3568f6..f65242d 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -87,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 %}