Add shipping tracking functionality
This commit is contained in:
parent
89e0f32d23
commit
194fb8d655
@ -6,4 +6,9 @@ class CoreConfig(AppConfig):
|
||||
name = 'core'
|
||||
|
||||
def ready(self):
|
||||
from .signals import order_created, transaction_created, order_line_post_save
|
||||
from .signals import (
|
||||
order_created,
|
||||
transaction_created,
|
||||
order_line_post_save,
|
||||
trackingnumber_postsave
|
||||
)
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
# Generated by Django 4.0.2 on 2022-03-23 16:25
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='shippingmethod',
|
||||
name='price',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=12),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('draft', 'Draft'), ('unfulfilled', 'Unfulfilled'), ('partially_fulfilled', 'Partially fulfilled'), ('partially_returned', 'Partially returned'), ('returned', 'Returned'), ('fulfilled', 'Fulfilled'), ('canceled', 'Canceled')], default='unfulfilled', max_length=32),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TrackingNumber',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('tracking_id', models.CharField(max_length=256)),
|
||||
('order', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='tracking_numbers', to='core.order')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Tracking Number',
|
||||
'verbose_name_plural': 'Tracking Numbers',
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,25 @@
|
||||
# Generated by Django 4.0.2 on 2022-03-23 17:04
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0002_shippingmethod_price_alter_order_status_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='trackingnumber',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trackingnumber',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
]
|
||||
@ -116,6 +116,14 @@ class Coupon(models.Model):
|
||||
class ShippingMethod(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
type = models.CharField(max_length=30, choices=ShippingMethodType.CHOICES)
|
||||
price = models.DecimalField(
|
||||
max_digits=settings.DEFAULT_MAX_DIGITS,
|
||||
decimal_places=settings.DEFAULT_DECIMAL_PLACES,
|
||||
default=0,
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dashboard:shipmeth-detail', kwargs={'pk': self.pk})
|
||||
|
||||
|
||||
class OrderManager(models.Manager):
|
||||
@ -251,3 +259,24 @@ class OrderLine(models.Model):
|
||||
@property
|
||||
def quantity_unfulfilled(self):
|
||||
return self.quantity - self.quantity_fulfilled
|
||||
|
||||
|
||||
class TrackingNumber(models.Model):
|
||||
order = models.ForeignKey(
|
||||
Order,
|
||||
related_name="tracking_numbers",
|
||||
editable=False,
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
tracking_id = models.CharField(max_length=256)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Tracking Number'
|
||||
verbose_name_plural = 'Tracking Numbers'
|
||||
|
||||
def __str__(self):
|
||||
return self.tracking_id
|
||||
|
||||
|
||||
@ -6,8 +6,11 @@ from django.dispatch import receiver
|
||||
from django.db import models
|
||||
|
||||
from . import OrderStatus, TransactionStatus
|
||||
from .models import Order, OrderLine, Transaction
|
||||
from .tasks import send_order_confirmation_email
|
||||
from .models import Order, OrderLine, Transaction, TrackingNumber
|
||||
from .tasks import (
|
||||
send_order_confirmation_email,
|
||||
send_order_shipped_email
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -34,6 +37,20 @@ def transaction_created(sender, instance, created, **kwargs):
|
||||
instance.confirmation_email_sent = True
|
||||
instance.save()
|
||||
|
||||
@receiver(post_save, sender=TrackingNumber, dispatch_uid="trackingnumber_postsave")
|
||||
def trackingnumber_postsave(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
logger.info("TrackingNumber was created")
|
||||
|
||||
data = {
|
||||
'order_id': instance.order.pk,
|
||||
'email': instance.order.customer.email,
|
||||
'full_name': instance.order.customer.get_full_name(),
|
||||
'tracking_id': instance.tracking_id
|
||||
}
|
||||
send_order_shipped_email.delay(data)
|
||||
|
||||
|
||||
def get_order_status(total_quantity_fulfilled, total_quantity_ordered):
|
||||
if total_quantity_fulfilled >= total_quantity_ordered:
|
||||
return OrderStatus.FULFILLED
|
||||
|
||||
@ -11,6 +11,7 @@ logger = get_task_logger(__name__)
|
||||
|
||||
|
||||
CONFIRM_ORDER_TEMPLATE = 'storefront/order_confirmation'
|
||||
SHIP_ORDER_TEMPLATE = 'storefront/order_shipped'
|
||||
ORDER_CANCEl_TEMPLATE = 'storefront/order_cancel'
|
||||
ORDER_REFUND_TEMPLATE = 'storefront/order_refund'
|
||||
|
||||
@ -24,3 +25,14 @@ def send_order_confirmation_email(order):
|
||||
)
|
||||
|
||||
logger.info(f"Order confirmation email sent to {order['email']}")
|
||||
|
||||
@shared_task(name='send_order_shipped_email')
|
||||
def send_order_shipped_email(data):
|
||||
send_templated_mail(
|
||||
template_name=SHIP_ORDER_TEMPLATE,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
recipient_list=[data['email']],
|
||||
context=data
|
||||
)
|
||||
|
||||
logger.info(f"Order shipped email sent to {data['email']}")
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from django import forms
|
||||
|
||||
from core.models import Order, OrderLine, ShippingMethod
|
||||
from core.models import Order, OrderLine, ShippingMethod, TrackingNumber
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -25,3 +25,16 @@ OrderLineFormset = forms.inlineformset_factory(
|
||||
Order, OrderLine, form=OrderLineFulfillForm,
|
||||
extra=0, can_delete=False
|
||||
)
|
||||
|
||||
|
||||
class OrderTrackingForm(forms.ModelForm):
|
||||
# send_shipment_details_to_customer = forms.BooleanField(initial=True)
|
||||
|
||||
class Meta:
|
||||
model = TrackingNumber
|
||||
fields = ('tracking_id',)
|
||||
|
||||
OrderTrackingFormset = forms.inlineformset_factory(
|
||||
Order, TrackingNumber, form=OrderTrackingForm,
|
||||
extra=1, can_delete=False
|
||||
)
|
||||
|
||||
36
src/dashboard/templates/dashboard/config.html
Normal file
36
src/dashboard/templates/dashboard/config.html
Normal file
@ -0,0 +1,36 @@
|
||||
{% extends "dashboard.html" %}
|
||||
{% load static %}
|
||||
{% load tz %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<header class="object__header">
|
||||
<h1><img src="{% static 'images/gear.png' %}" alt=""> Site configuration</h1>
|
||||
</header>
|
||||
|
||||
<section class="object__panel">
|
||||
<div class="object__item object__item--header">
|
||||
<h4>Shipping methods</h4>
|
||||
<a href="{% url 'dashboard:shipmeth-create' %}" class="action-button order__fulfill">+ New method</a>
|
||||
</div>
|
||||
<div class="panel__item">
|
||||
{% for method in shipping_method_list %}
|
||||
<p>
|
||||
<a href="{% url 'dashboard:shipmeth-detail' method.pk %}">{{method.name}} | {{method.type}} | {{method.price}}</a>
|
||||
</p>
|
||||
{% empty %}
|
||||
<p>No shipping methods yet.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="object__panel">
|
||||
<div class="object__item object__item--header">
|
||||
<h4>Staff</h4>
|
||||
<a href="" class="action-button order__fulfill">+ New staff</a>
|
||||
</div>
|
||||
<div class="panel__item">
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock %}
|
||||
@ -4,7 +4,7 @@
|
||||
{% block content %}
|
||||
<article>
|
||||
<header class="object__header">
|
||||
<h1><img src="{% static "images/box.png" %}" alt=""> Order #{{order.pk}}</h1>
|
||||
<h1><img src="{% static 'images/box.png' %}" alt=""> Order #{{order.pk}}</h1>
|
||||
<div class="object__menu">
|
||||
<div class="dropdown">
|
||||
<span class="dropdown__menu">Options ↓</span>
|
||||
@ -17,13 +17,13 @@
|
||||
</div>
|
||||
</header>
|
||||
<section class="object__list">
|
||||
<div class="object__item object__item--header">
|
||||
<span>Product</span>
|
||||
<span>SKU</span>
|
||||
<span>Quantity</span>
|
||||
<span>Price</span>
|
||||
<span>Total</span>
|
||||
</div>
|
||||
<div class="object__item object__item--header">
|
||||
<span>Product</span>
|
||||
<span>SKU</span>
|
||||
<span>Quantity</span>
|
||||
<span>Price</span>
|
||||
<span>Total</span>
|
||||
</div>
|
||||
{% for item in order.lines.all %}
|
||||
<div class="object__item">
|
||||
{% with product=item.product %}
|
||||
@ -40,18 +40,29 @@
|
||||
{% empty %}
|
||||
<p>No items in order yet.</p>
|
||||
{% endfor %}
|
||||
<div class="object__item">
|
||||
<a href="{% url 'dashboard:order-fulfill' order.pk %}" class="action-button order__fulfill">Fulfill →</a>
|
||||
</div>
|
||||
<div class="object__item">
|
||||
<a href="{% url 'dashboard:order-fulfill' order.pk %}" class="action-button order__fulfill">Fulfill →</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="object__panel">
|
||||
<div class="object__item object__item--header">
|
||||
<h4>Shipping</h4>
|
||||
<a href="{% url 'dashboard:order-ship' order.pk %}" class="action-button order__fulfill">Ship order →</a>
|
||||
</div>
|
||||
<div class="panel__item">
|
||||
<a href="{% url 'dashboard:order-fulfill' order.pk %}" class="action-button order__fulfill">Ship order →</a>
|
||||
</div>
|
||||
{% for number in order.tracking_numbers.all %}
|
||||
<div class="panel__item">
|
||||
<p>
|
||||
<strong>Shipment</strong><br>
|
||||
Date: {{number.created_at|date:"SHORT_DATE_FORMAT" }}<br>
|
||||
Tracking number: {{number.tracking_id}}
|
||||
</p>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="panel__item">
|
||||
<p>No tracking information.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<section class="object__panel">
|
||||
|
||||
37
src/dashboard/templates/dashboard/order_tracking_form.html
Normal file
37
src/dashboard/templates/dashboard/order_tracking_form.html
Normal file
@ -0,0 +1,37 @@
|
||||
{% extends "dashboard.html" %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<h1>Fulfill Order #{{order.pk}}</h1>
|
||||
<section>
|
||||
<form method="POST" action="">
|
||||
{% csrf_token %}
|
||||
{{ form.management_form }}
|
||||
|
||||
<section class="object__list">
|
||||
{% for dict in form.errors %}
|
||||
{% for error in dict.values %}
|
||||
<div class="object__item">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<div class="object__item object__item--header">
|
||||
<span>Product</span>
|
||||
<span>SKU</span>
|
||||
<span>Quantity to fulfill</span>
|
||||
<span>Grind</span>
|
||||
</div>
|
||||
{% for formitem in form %}
|
||||
<div class="object__item">
|
||||
{{formitem}}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="object__item">
|
||||
<a href="{% url 'dashboard:order-detail' order.pk %}">cancel</a> <input class="action-button order__fulfill" type="submit" value="Ship order and send tracking info to customer">
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock content %}
|
||||
@ -0,0 +1,22 @@
|
||||
{% extends "dashboard.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<header class="object__header">
|
||||
<h1><img src="{% static 'images/cubes.png' %}" alt=""> Product</h1>
|
||||
</header>
|
||||
<section class="product__detail object__panel">
|
||||
<figure class="product__figure">
|
||||
<img class="" src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
|
||||
</figure>
|
||||
<form method="post">{% csrf_token %}
|
||||
<p>Are you sure you want to delete "{{ object }}"?</p>
|
||||
{{ form.as_p }}
|
||||
<p>
|
||||
<input class="action-button action-button--warning" type="submit" value="Confirm"> or <a href="{% url 'dashboard:product-detail' product.pk %}">cancel</a>
|
||||
</p>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock content %}
|
||||
@ -5,7 +5,10 @@
|
||||
<article>
|
||||
<header class="object__header">
|
||||
<h1><img src="{% static "images/cubes.png" %}" alt=""> Product</h1>
|
||||
<a href="{% url 'dashboard:product-update' product.pk %}" class="action-button">Edit</a>
|
||||
<div class="object__menu">
|
||||
<a href="{% url 'dashboard:product-delete' product.pk %}" class="action-button action-button--warning">Delete</a>
|
||||
<a href="{% url 'dashboard:product-update' product.pk %}" class="action-button">Edit</a>
|
||||
</div>
|
||||
</header>
|
||||
<section class="product__detail object__panel">
|
||||
<figure class="product__figure">
|
||||
@ -17,7 +20,7 @@
|
||||
<p>$<strong>{{product.price}}</strong></p>
|
||||
<p>{{product.weight.oz}} oz</p>
|
||||
<p>Visible in listings: <strong>{{product.visible_in_listings|yesno:"Yes,No"}}</strong></p>
|
||||
<p>Ordered {{product.num_ordered|default_if_none:"0"}} time{{ product.num_ordered|pluralize }}.</p>
|
||||
<p>Ordered {{product.num_ordered|default_if_none:"0"}} time{{ product.num_ordered|default_if_none:"0"|pluralize }}.</p>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
16
src/dashboard/templates/dashboard/shipmeth_create_form.html
Normal file
16
src/dashboard/templates/dashboard/shipmeth_create_form.html
Normal file
@ -0,0 +1,16 @@
|
||||
{% extends "dashboard.html" %}
|
||||
|
||||
{% block content %}
|
||||
<article class="product">
|
||||
<h1>Create shipmeth</h1>
|
||||
<section>
|
||||
<form method="POST" action="{% url 'dashboard:shipmeth-create' %}">
|
||||
{% csrf_token %}
|
||||
{{form.as_p}}
|
||||
<p class="form__submit">
|
||||
<input class="action-button" type="submit" value="Create method"> or <a href="{% url 'dashboard:config' %}">cancel</a>
|
||||
</p>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock %}
|
||||
21
src/dashboard/templates/dashboard/shipmeth_detail.html
Normal file
21
src/dashboard/templates/dashboard/shipmeth_detail.html
Normal file
@ -0,0 +1,21 @@
|
||||
{% extends "dashboard.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<header class="object__header">
|
||||
<h1><img src="{% static 'images/gear.png' %}" alt=""> Shipping Method</h1>
|
||||
<div class="object__menu">
|
||||
<a href="" class="action-button action-button--warning">Delete</a>
|
||||
<a href="" class="action-button">Edit</a>
|
||||
</div>
|
||||
</header>
|
||||
<section class="product__detail object__panel">
|
||||
<div>
|
||||
<h1>{{shippingmethod.name}}</h1>
|
||||
<p>{{shippingmethod.get_type_display}}</p>
|
||||
<p>$<strong>{{shippingmethod.price}}</strong></p>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock content %}
|
||||
@ -3,6 +3,12 @@ from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.DashboardHomeView.as_view(), name='home'),
|
||||
path('config/', views.DashboardConfigView.as_view(), name='config'),
|
||||
|
||||
path('shipping-methods/new/', views.ShippingMethodCreateView.as_view(), name='shipmeth-create'),
|
||||
path('shipping-methods/<int:pk>/', include([
|
||||
path('', views.ShippingMethodDetailView.as_view(), name='shipmeth-detail'),
|
||||
])),
|
||||
|
||||
path('orders/', views.OrderListView.as_view(), name='order-list'),
|
||||
path('orders/<int:pk>/', include([
|
||||
@ -10,14 +16,15 @@ urlpatterns = [
|
||||
# path('update/', views.OrderUpdateView.as_view(), name='product-update'),
|
||||
# path('delete/', views.OrderDeleteView.as_view(), name='product-delete'),
|
||||
path('fulfill/', views.OrderFulfillView.as_view(), name='order-fulfill'),
|
||||
path('ship/', views.OrderTrackingView.as_view(), name='order-ship'),
|
||||
])),
|
||||
|
||||
path('products/', views.ProductListView.as_view(), name='product-list'),
|
||||
path('products/new/', views.ProductCreateView.as_view(), name='product-create'),
|
||||
path('<int:pk>/', include([
|
||||
path('products/<int:pk>/', include([
|
||||
path('', views.ProductDetailView.as_view(), name='product-detail'),
|
||||
path('update/', views.ProductUpdateView.as_view(), name='product-update'),
|
||||
# path('delete/', views.ProductDeleteView.as_view(), name='product-delete'),
|
||||
path('delete/', views.ProductDeleteView.as_view(), name='product-delete'),
|
||||
])),
|
||||
|
||||
path('customers/', views.CustomerListView.as_view(), name='customer-list'),
|
||||
|
||||
@ -12,6 +12,8 @@ from django.views.generic.list import ListView
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.forms import inlineformset_factory
|
||||
from django.contrib import messages
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
|
||||
from django.db.models import (
|
||||
Exists, OuterRef, Prefetch, Subquery, Count, Sum, Avg, F, Q, Value
|
||||
@ -21,14 +23,14 @@ from django.db.models.functions import Coalesce
|
||||
from accounts.models import User
|
||||
from accounts.utils import get_or_create_customer
|
||||
from accounts.forms import AddressForm
|
||||
from core.models import Product, Order, OrderLine
|
||||
from core.models import Product, Order, OrderLine, ShippingMethod, Transaction, TrackingNumber
|
||||
|
||||
from core import DiscountValueType, VoucherType, OrderStatus, ShippingMethodType
|
||||
from .forms import OrderLineFulfillForm, OrderLineFormset
|
||||
from .forms import OrderLineFulfillForm, OrderLineFormset, OrderTrackingFormset
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class DashboardHomeView(TemplateView):
|
||||
class DashboardHomeView(LoginRequiredMixin, TemplateView):
|
||||
template_name = 'dashboard/dashboard_detail.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@ -45,13 +47,35 @@ class DashboardHomeView(TemplateView):
|
||||
).aggregate(total=Sum('total_net_amount'))['total']
|
||||
return context
|
||||
|
||||
class OrderListView(ListView):
|
||||
class DashboardConfigView(TemplateView):
|
||||
template_name = 'dashboard/config.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
today = timezone.localtime(timezone.now()).date()
|
||||
|
||||
context['shipping_method_list'] = ShippingMethod.objects.all()
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class ShippingMethodCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
|
||||
model = ShippingMethod
|
||||
template_name = 'dashboard/shipmeth_create_form.html'
|
||||
fields = '__all__'
|
||||
success_message = '%(name)s created.'
|
||||
|
||||
class ShippingMethodDetailView(LoginRequiredMixin, DetailView):
|
||||
model = ShippingMethod
|
||||
template_name = 'dashboard/shipmeth_detail.html'
|
||||
|
||||
|
||||
class OrderListView(LoginRequiredMixin, ListView):
|
||||
model = Order
|
||||
template_name = 'dashboard/order_list.html'
|
||||
|
||||
def get_queryset(self):
|
||||
query = self.request.GET.get('status')
|
||||
order = self.request.GET.get('order')
|
||||
if query:
|
||||
object_list = Order.objects.filter(
|
||||
status=query
|
||||
@ -70,7 +94,7 @@ class OrderListView(ListView):
|
||||
|
||||
return object_list
|
||||
|
||||
class OrderDetailView(DetailView):
|
||||
class OrderDetailView(LoginRequiredMixin, DetailView):
|
||||
model = Order
|
||||
template_name = 'dashboard/order_detail.html'
|
||||
|
||||
@ -93,10 +117,24 @@ class OrderDetailView(DetailView):
|
||||
return context
|
||||
|
||||
|
||||
class OrderFulfillView(UpdateView):
|
||||
class OrderFulfillView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||
model = Order
|
||||
template_name = "dashboard/order_fulfill.html"
|
||||
form_class = OrderLineFormset
|
||||
success_message = "Order saved."
|
||||
|
||||
def form_valid(self, form):
|
||||
form.save()
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('dashboard:order-detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
class OrderTrackingView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||
model = Order
|
||||
template_name = "dashboard/order_tracking_form.html"
|
||||
form_class = OrderTrackingFormset
|
||||
success_message = "Order saved."
|
||||
|
||||
def form_valid(self, form):
|
||||
form.save()
|
||||
@ -106,7 +144,7 @@ class OrderFulfillView(UpdateView):
|
||||
return reverse('dashboard:order-detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
|
||||
class ProductListView(ListView):
|
||||
class ProductListView(LoginRequiredMixin, ListView):
|
||||
model = Product
|
||||
template_name = 'dashboard/product_list.html'
|
||||
|
||||
@ -118,22 +156,30 @@ class ProductListView(ListView):
|
||||
# )
|
||||
# return object_list
|
||||
|
||||
class ProductDetailView(DetailView):
|
||||
class ProductDetailView(LoginRequiredMixin, DetailView):
|
||||
model = Product
|
||||
template_name = 'dashboard/product_detail.html'
|
||||
|
||||
class ProductUpdateView(UpdateView):
|
||||
class ProductUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||
model = Product
|
||||
template_name = 'dashboard/product_update_form.html'
|
||||
fields = '__all__'
|
||||
success_message = "%(name)s saved."
|
||||
|
||||
class ProductCreateView(CreateView):
|
||||
class ProductDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
|
||||
model = Product
|
||||
template_name = 'dashboard/product_confirm_delete.html'
|
||||
success_url = reverse_lazy('dashboard:product-list')
|
||||
success_message = "Product deleted."
|
||||
|
||||
class ProductCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
|
||||
model = Product
|
||||
template_name = "dashboard/product_create_form.html"
|
||||
fields = '__all__'
|
||||
success_message = "%(name)s created."
|
||||
|
||||
|
||||
class CustomerListView(ListView):
|
||||
class CustomerListView(LoginRequiredMixin, ListView):
|
||||
model = User
|
||||
template_name = 'dashboard/customer_list.html'
|
||||
|
||||
@ -150,15 +196,16 @@ class CustomerListView(ListView):
|
||||
|
||||
return object_list
|
||||
|
||||
class CustomerDetailView(DetailView):
|
||||
class CustomerDetailView(LoginRequiredMixin, DetailView):
|
||||
model = User
|
||||
template_name = 'dashboard/customer_detail.html'
|
||||
context_object_name = 'customer'
|
||||
|
||||
class CustomerUpdateView(UpdateView):
|
||||
class CustomerUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||
model = User
|
||||
template_name = 'dashboard/customer_form.html'
|
||||
context_object_name = 'customer'
|
||||
success_message = '%(name)s saved.'
|
||||
fields = (
|
||||
'first_name',
|
||||
'last_name',
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
--red-color: #ff4d44;
|
||||
|
||||
--default-border: 2px solid var(--gray-color);
|
||||
--default-shadow: 0 1rem 3rem var(--gray-color);
|
||||
}
|
||||
|
||||
html {
|
||||
@ -261,6 +262,41 @@ main article {
|
||||
}
|
||||
|
||||
|
||||
.site__messages {
|
||||
text-align: left;
|
||||
white-space: normal;
|
||||
background-color: var(--fg-color);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: var(--default-shadow);
|
||||
|
||||
margin: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
position: fixed;
|
||||
|
||||
left: auto;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: auto;
|
||||
z-index: 990;
|
||||
}
|
||||
|
||||
.messages__message.debug {
|
||||
color: white;
|
||||
}
|
||||
.messages__message.info {
|
||||
color: white;
|
||||
}
|
||||
.messages__message.success {
|
||||
color: var(--green-color);
|
||||
}
|
||||
.messages__message.warning {
|
||||
color: var(--yellow-color);
|
||||
}
|
||||
.messages__message.error {
|
||||
color: var(--red-color);
|
||||
}
|
||||
|
||||
|
||||
.object__header {
|
||||
display: flex;
|
||||
@ -328,6 +364,10 @@ main article {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.object__menu > a:not(:last-child) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.order__fulfill {
|
||||
grid-column: 8;
|
||||
}
|
||||
@ -356,7 +396,7 @@ main article {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0 3rem var(--gray-color);
|
||||
box-shadow: var(--default-shadow);
|
||||
}
|
||||
|
||||
.dropdown__child a {
|
||||
|
||||
@ -45,9 +45,9 @@
|
||||
<img src="{% static 'images/coupon.png' %}" alt="">
|
||||
Coupons
|
||||
</a>
|
||||
<a href="">
|
||||
<a href="{% url 'dashboard:config' %}">
|
||||
<img src="{% static 'images/gear.png' %}" alt="">
|
||||
Staff
|
||||
Config
|
||||
</a>
|
||||
</nav>
|
||||
<div class="dashboard__user">
|
||||
@ -67,8 +67,24 @@
|
||||
{% endblock content %}
|
||||
</main>
|
||||
</div>
|
||||
{% if messages %}
|
||||
<div class="site__messages">
|
||||
{% for message in messages %}
|
||||
<span class="messages__message {% if message.tags %} {{ message.tags }} {% endif %}">{{ message }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<footer>
|
||||
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const messageEl = document.querySelector('.site__messages')
|
||||
if (messageEl) {
|
||||
setTimeout(function () {
|
||||
messageEl.style.display = 'none'
|
||||
}, 5000)
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
18
src/templates/templated_email/storefront/order_shipped.email
Normal file
18
src/templates/templated_email/storefront/order_shipped.email
Normal file
@ -0,0 +1,18 @@
|
||||
{% block subject %}Your PT Coffee order #{{order_id}} has shipped{% endblock %}
|
||||
{% block plain %}
|
||||
Great news! Your recent order #{{order_id}} has shipped
|
||||
|
||||
{{tracking_id}}
|
||||
|
||||
Thanks,
|
||||
Port Townsend Coffee
|
||||
{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<p>Great news! Your recent order #{{order_id}} has shipped</p>
|
||||
|
||||
<p>{{tracking_id}}</p>
|
||||
|
||||
<p>Thanks,<br>
|
||||
Port Townsend Coffee</p>
|
||||
{% endblock %}
|
||||
Loading…
x
Reference in New Issue
Block a user