Add functionality to cancel an order from dashboard

This commit is contained in:
Nathan Chapman 2022-05-12 18:43:55 -06:00
parent d93f266861
commit 06d809f7f4
11 changed files with 4715 additions and 23 deletions

View File

@ -1,5 +1,6 @@
from django.conf import settings from django.conf import settings
class DiscountValueType: class DiscountValueType:
FIXED = "fixed" FIXED = "fixed"
PERCENTAGE = "percentage" PERCENTAGE = "percentage"
@ -46,6 +47,7 @@ class OrderStatus:
(CANCELED, "Canceled"), (CANCELED, "Canceled"),
] ]
class TransactionStatus: class TransactionStatus:
CREATED = "CREATED" # The order was created with the specified context. CREATED = "CREATED" # The order was created with the specified context.
SAVED = "SAVED" # The order was saved and persisted. The order status continues to be in progress until a capture is made with final_capture = true for all purchase units within the order. SAVED = "SAVED" # The order was saved and persisted. The order status continues to be in progress until a capture is made with final_capture = true for all purchase units within the order.
@ -63,6 +65,7 @@ class TransactionStatus:
(PAYER_ACTION_REQUIRED, "Payer action required") (PAYER_ACTION_REQUIRED, "Payer action required")
] ]
class ShippingMethodType: class ShippingMethodType:
PRICE_BASED = "price" PRICE_BASED = "price"
WEIGHT_BASED = "weight" WEIGHT_BASED = "weight"
@ -72,6 +75,7 @@ class ShippingMethodType:
(WEIGHT_BASED, "Weight based shipping"), (WEIGHT_BASED, "Weight based shipping"),
] ]
class ShippingService: class ShippingService:
FIRST_CLASS = "FIRST CLASS" FIRST_CLASS = "FIRST CLASS"
PRIORITY = "PRIORITY" PRIORITY = "PRIORITY"
@ -83,6 +87,7 @@ class ShippingService:
(PRIORITY_COMMERCIAL, "Priority Commercial") (PRIORITY_COMMERCIAL, "Priority Commercial")
] ]
class ShippingContainer: class ShippingContainer:
LG_FLAT_RATE_BOX = "LG FLAT RATE BOX" LG_FLAT_RATE_BOX = "LG FLAT RATE BOX"
REGIONAL_RATE_BOX_A = "REGIONALRATEBOXA" REGIONAL_RATE_BOX_A = "REGIONALRATEBOXA"

View File

@ -0,0 +1,129 @@
[
{
"model": "core.order",
"pk": 1,
"fields": {
"customer": null,
"status": "unfulfilled",
"billing_address": null,
"shipping_address": 1,
"shipping_method": null,
"coupon": null,
"shipping_total": "9.55",
"total_net_amount": "13.40",
"weight": "0.0:oz",
"created_at": "2022-03-15T17:18:59.584Z",
"updated_at": "2022-03-15T17:18:59.584Z"
}
}, {
"model": "core.order",
"pk": 2,
"fields": {
"customer": null,
"status": "unfulfilled",
"billing_address": null,
"shipping_address": 1,
"shipping_method": null,
"coupon": null,
"shipping_total": "9.55",
"total_net_amount": "13.40",
"weight": "0.0:oz",
"created_at": "2022-03-15T17:22:18.440Z",
"updated_at": "2022-03-15T17:22:18.440Z"
}
}, {
"model": "core.order",
"pk": 3,
"fields": {
"customer": null,
"status": "unfulfilled",
"billing_address": null,
"shipping_address": 1,
"shipping_method": null,
"coupon": null,
"shipping_total": "9.55",
"total_net_amount": "13.40",
"weight": "0.0:oz",
"created_at": "2022-03-15T17:26:27.869Z",
"updated_at": "2022-03-15T17:26:27.869Z"
}
}, {
"model": "core.orderline",
"pk": 1,
"fields": {
"order": 1,
"product": 1,
"quantity": 2,
"quantity_fulfilled": 0,
"customer_note": "Whole Beans",
"currency": "USD",
"unit_price": "13.40",
"tax_rate": "2.00"
}
}, {
"model": "core.orderline",
"pk": 2,
"fields": {
"order": 1,
"product": 1,
"quantity": 1,
"quantity_fulfilled": 1,
"customer_note": "Espresso",
"currency": "USD",
"unit_price": "13.40",
"tax_rate": "2.00"
}
}, {
"model": "core.orderline",
"pk": 3,
"fields": {
"order": 2,
"product": 8,
"quantity": 1,
"quantity_fulfilled": 1,
"customer_note": "Whole Beans",
"currency": "USD",
"unit_price": "13.40",
"tax_rate": "2.00"
}
}, {
"model": "core.orderline",
"pk": 4,
"fields": {
"order": 2,
"product": 7,
"quantity": 1,
"quantity_fulfilled": 1,
"customer_note": "Cone Drip",
"currency": "USD",
"unit_price": "13.40",
"tax_rate": "2.00"
}
}, {
"model": "core.orderline",
"pk": 5,
"fields": {
"order": 3,
"product": 4,
"quantity": 1,
"quantity_fulfilled": 0,
"customer_note": "Percolator",
"currency": "USD",
"unit_price": "13.40",
"tax_rate": "2.00"
}
}, {
"model": "core.orderline",
"pk": 6,
"fields": {
"order": 3,
"product": 6,
"quantity": 1,
"quantity_fulfilled": 0,
"customer_note": "Whole Beans",
"currency": "USD",
"unit_price": "13.40",
"tax_rate": "2.00"
}
}
]

View File

@ -1,10 +1,19 @@
import logging import logging
from django import forms from django import forms
from core.models import Order, OrderLine, ShippingMethod, TrackingNumber, Coupon, ProductPhoto from core import OrderStatus
from core.models import (
Order,
OrderLine,
ShippingMethod,
TrackingNumber,
Coupon,
ProductPhoto
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CouponForm(forms.ModelForm): class CouponForm(forms.ModelForm):
class Meta: class Meta:
model = Coupon model = Coupon
@ -19,10 +28,10 @@ class CouponForm(forms.ModelForm):
'products', 'products',
) )
widgets = { widgets = {
'valid_from': forms.DateInput(attrs = { 'valid_from': forms.DateInput(attrs={
'type': 'date' 'type': 'date'
}), }),
'valid_to': forms.DateInput(attrs = { 'valid_to': forms.DateInput(attrs={
'type': 'date' 'type': 'date'
}), }),
} }
@ -35,7 +44,7 @@ class OrderLineFulfillForm(forms.ModelForm):
model = OrderLine model = OrderLine
fields = ('quantity_fulfilled',) fields = ('quantity_fulfilled',)
widgets = { widgets = {
'quantity_fulfilled': forms.NumberInput(attrs = { 'quantity_fulfilled': forms.NumberInput(attrs={
'min': 0, 'min': 0,
}) })
} }
@ -44,12 +53,22 @@ class OrderLineFulfillForm(forms.ModelForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['quantity_fulfilled'].widget.attrs['max'] = self.instance.quantity self.fields['quantity_fulfilled'].widget.attrs['max'] = self.instance.quantity
OrderLineFormset = forms.inlineformset_factory( OrderLineFormset = forms.inlineformset_factory(
Order, OrderLine, form=OrderLineFulfillForm, Order, OrderLine, form=OrderLineFulfillForm,
extra=0, can_delete=False extra=0, can_delete=False
) )
class OrderCancelForm(forms.ModelForm):
class Meta:
model = Order
fields = ('status',)
widgets = {
'status': forms.HiddenInput()
}
class OrderTrackingForm(forms.ModelForm): class OrderTrackingForm(forms.ModelForm):
# send_shipment_details_to_customer = forms.BooleanField(initial=True) # send_shipment_details_to_customer = forms.BooleanField(initial=True)
@ -57,6 +76,7 @@ class OrderTrackingForm(forms.ModelForm):
model = TrackingNumber model = TrackingNumber
fields = ('tracking_id',) fields = ('tracking_id',)
OrderTrackingFormset = forms.inlineformset_factory( OrderTrackingFormset = forms.inlineformset_factory(
Order, TrackingNumber, form=OrderTrackingForm, Order, TrackingNumber, form=OrderTrackingForm,
extra=1, can_delete=False extra=1, can_delete=False

View File

@ -0,0 +1,17 @@
{% extends "dashboard.html" %}
{% block content %}
<article>
<header class="object__header">
<h1>Cancel Order #{{order.pk}}</h1>
</header>
<section class="object__panel">
<form method="POST" action="{% url 'dashboard:order-cancel' order.pk %}">
{% csrf_token %}
{{ form.as_p }}
<input class="action-button action-button--warning order__fulfill" type="submit" value="Cancel order"> or <a href="{% url 'dashboard:order-detail' order.pk %}">cancel</a>
</section>
</form>
</section>
</article>
{% endblock content %}

View File

@ -9,7 +9,7 @@
<div class="dropdown"> <div class="dropdown">
<span class="dropdown__menu">Options &darr;</span> <span class="dropdown__menu">Options &darr;</span>
<div class="dropdown__child"> <div class="dropdown__child">
<a href="">Cancel order</a> <a href="{% url 'dashboard:order-cancel' order.pk %}">Cancel order</a>
<a href="">Return order</a> <a href="">Return order</a>
</div> </div>
</div> </div>

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

View File

@ -0,0 +1,80 @@
import logging
from decimal import Decimal
from django.test import TestCase, Client, RequestFactory
from django.urls import reverse
from django.conf import settings
from measurement.measures import Weight
from paypalcheckoutsdk.orders import OrdersCreateRequest, OrdersCaptureRequest
from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment
from accounts.models import User, Address
from core.models import Product, Order, Coupon
from core import CoffeeGrind
from dashboard.forms import (
CouponForm,
OrderLineFulfillForm,
OrderTrackingForm,
ProductPhotoForm,
)
from dashboard.views import (
DashboardHomeView,
DashboardConfigView,
ShippingMethodCreateView,
ShippingMethodDetailView,
CouponListView,
CouponCreateView,
CouponDetailView,
CouponUpdateView,
CouponDeleteView,
OrderListView,
OrderDetailView,
OrderFulfillView,
OrderTrackingView,
ProductListView,
ProductDetailView,
ProductUpdateView,
ProductCreateView,
ProductDeleteView,
ProductPhotoCreateView,
ProductPhotoDeleteView,
CustomerListView,
CustomerDetailView,
CustomerUpdateView
)
logger = logging.getLogger(__name__)
class OrderCancelViewTests(TestCase):
fixtures = [
'accounts.json',
'coupons.json',
'products.json',
'orders.json'
]
@classmethod
def setUpTestData(cls):
cls.admin_user = User.objects.get(pk=1)
def setUp(self):
self.client = Client()
self.client.force_login(self.admin_user)
def test_view_url_exists_at_desired_location(self):
response = self.client.get('/dashboard/orders/1/cancel/')
self.assertEqual(response.status_code, 200)
def test_view_url_accesible_by_name(self):
response = self.client.get(
reverse('dashboard:order-cancel', kwargs={'pk': 1})
)
self.assertEqual(response.status_code, 200)
def test_view_uses_correct_template(self):
response = self.client.get(
reverse('dashboard:order-cancel', kwargs={'pk': 1})
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'dashboard/order_cancel_form.html')

View File

@ -21,9 +21,8 @@ urlpatterns = [
path('orders/', views.OrderListView.as_view(), name='order-list'), path('orders/', views.OrderListView.as_view(), name='order-list'),
path('orders/<int:pk>/', include([ path('orders/<int:pk>/', include([
path('', views.OrderDetailView.as_view(), name='order-detail'), path('', views.OrderDetailView.as_view(), name='order-detail'),
# 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('fulfill/', views.OrderFulfillView.as_view(), name='order-fulfill'),
path('cancel/', views.OrderCancelView.as_view(), name='order-cancel'),
path('ship/', views.OrderTrackingView.as_view(), name='order-ship'), path('ship/', views.OrderTrackingView.as_view(), name='order-ship'),
])), ])),

View File

@ -6,7 +6,9 @@ from django.shortcuts import render, reverse, redirect, get_object_or_404
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.views.generic.base import RedirectView, TemplateView from django.views.generic.base import RedirectView, TemplateView
from django.views.generic.edit import FormView, CreateView, UpdateView, DeleteView, FormMixin from django.views.generic.edit import (
FormView, CreateView, UpdateView, DeleteView, FormMixin
)
from django.views.generic.detail import DetailView, SingleObjectMixin from django.views.generic.detail import DetailView, SingleObjectMixin
from django.views.generic.list import ListView from django.views.generic.list import ListView
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
@ -34,11 +36,24 @@ from core.models import (
Coupon Coupon
) )
from core import DiscountValueType, VoucherType, OrderStatus, ShippingMethodType from core import (
from .forms import OrderLineFulfillForm, OrderLineFormset, OrderTrackingFormset, CouponForm, ProductPhotoForm DiscountValueType,
VoucherType,
OrderStatus,
ShippingMethodType
)
from .forms import (
OrderLineFulfillForm,
OrderLineFormset,
OrderCancelForm,
OrderTrackingFormset,
CouponForm,
ProductPhotoForm
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class DashboardHomeView(LoginRequiredMixin, TemplateView): class DashboardHomeView(LoginRequiredMixin, TemplateView):
template_name = 'dashboard/dashboard_detail.html' template_name = 'dashboard/dashboard_detail.html'
@ -56,6 +71,7 @@ class DashboardHomeView(LoginRequiredMixin, TemplateView):
).aggregate(total=Sum('total_net_amount'))['total'] ).aggregate(total=Sum('total_net_amount'))['total']
return context return context
class DashboardConfigView(TemplateView): class DashboardConfigView(TemplateView):
template_name = 'dashboard/config.html' template_name = 'dashboard/config.html'
@ -68,40 +84,42 @@ class DashboardConfigView(TemplateView):
return context return context
class ShippingMethodCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): class ShippingMethodCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
model = ShippingMethod model = ShippingMethod
template_name = 'dashboard/shipmeth_create_form.html' template_name = 'dashboard/shipmeth_create_form.html'
fields = '__all__' fields = '__all__'
success_message = '%(name)s created.' success_message = '%(name)s created.'
class ShippingMethodDetailView(LoginRequiredMixin, DetailView): class ShippingMethodDetailView(LoginRequiredMixin, DetailView):
model = ShippingMethod model = ShippingMethod
template_name = 'dashboard/shipmeth_detail.html' template_name = 'dashboard/shipmeth_detail.html'
class CouponListView(LoginRequiredMixin, ListView): class CouponListView(LoginRequiredMixin, ListView):
model = Coupon model = Coupon
template_name = 'dashboard/coupon_list.html' template_name = 'dashboard/coupon_list.html'
class CouponCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): class CouponCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
model = Coupon model = Coupon
template_name = 'dashboard/coupon_create_form.html' template_name = 'dashboard/coupon_create_form.html'
form_class = CouponForm form_class = CouponForm
success_message = '%(name)s created.' success_message = '%(name)s created.'
class CouponDetailView(LoginRequiredMixin, DetailView): class CouponDetailView(LoginRequiredMixin, DetailView):
model = Coupon model = Coupon
template_name = 'dashboard/coupon_detail.html' template_name = 'dashboard/coupon_detail.html'
class CouponUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): class CouponUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
model = Coupon model = Coupon
template_name = 'dashboard/coupon_form.html' template_name = 'dashboard/coupon_form.html'
success_message = '%(name)s saved.' success_message = '%(name)s saved.'
form_class = CouponForm form_class = CouponForm
class CouponDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView): class CouponDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
model = Coupon model = Coupon
template_name = 'dashboard/coupon_confirm_delete.html' template_name = 'dashboard/coupon_confirm_delete.html'
@ -109,7 +127,6 @@ class CouponDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
success_message = 'Coupon deleted.' success_message = 'Coupon deleted.'
class OrderListView(LoginRequiredMixin, ListView): class OrderListView(LoginRequiredMixin, ListView):
model = Order model = Order
template_name = 'dashboard/order_list.html' template_name = 'dashboard/order_list.html'
@ -135,6 +152,7 @@ class OrderListView(LoginRequiredMixin, ListView):
return object_list return object_list
class OrderDetailView(LoginRequiredMixin, DetailView): class OrderDetailView(LoginRequiredMixin, DetailView):
model = Order model = Order
template_name = 'dashboard/order_detail.html' template_name = 'dashboard/order_detail.html'
@ -171,6 +189,20 @@ class OrderFulfillView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
def get_success_url(self): def get_success_url(self):
return reverse('dashboard:order-detail', kwargs={'pk': self.object.pk}) return reverse('dashboard:order-detail', kwargs={'pk': self.object.pk})
class OrderCancelView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
model = Order
template_name = "dashboard/order_cancel_form.html"
form_class = OrderCancelForm
success_message = "Order canceled."
initial = {
'status': OrderStatus.CANCELED
}
def get_success_url(self):
return reverse('dashboard:order-detail', kwargs={'pk': self.object.pk})
class OrderTrackingView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): class OrderTrackingView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
model = Order model = Order
template_name = "dashboard/order_tracking_form.html" template_name = "dashboard/order_tracking_form.html"
@ -198,22 +230,26 @@ class ProductListView(LoginRequiredMixin, ListView):
# ) # )
# return object_list # return object_list
class ProductDetailView(LoginRequiredMixin, DetailView): class ProductDetailView(LoginRequiredMixin, DetailView):
model = Product model = Product
template_name = 'dashboard/product_detail.html' template_name = 'dashboard/product_detail.html'
class ProductUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): class ProductUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
model = Product model = Product
template_name = 'dashboard/product_update_form.html' template_name = 'dashboard/product_update_form.html'
fields = '__all__' fields = '__all__'
success_message = '%(name)s saved.' success_message = '%(name)s saved.'
class ProductCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): class ProductCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
model = Product model = Product
template_name = 'dashboard/product_create_form.html' template_name = 'dashboard/product_create_form.html'
fields = '__all__' fields = '__all__'
success_message = '%(name)s created.' success_message = '%(name)s created.'
class ProductDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView): class ProductDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
model = Product model = Product
template_name = 'dashboard/product_confirm_delete.html' template_name = 'dashboard/product_confirm_delete.html'
@ -240,6 +276,7 @@ class ProductPhotoCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView
def get_success_url(self): def get_success_url(self):
return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']}) return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']})
class ProductPhotoDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView): class ProductPhotoDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
model = ProductPhoto model = ProductPhoto
pk_url_kwarg = 'photo_pk' pk_url_kwarg = 'photo_pk'
@ -250,10 +287,6 @@ class ProductPhotoDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView
return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']}) return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']})
class CustomerListView(LoginRequiredMixin, ListView): class CustomerListView(LoginRequiredMixin, ListView):
model = User model = User
template_name = 'dashboard/customer_list.html' template_name = 'dashboard/customer_list.html'
@ -271,11 +304,13 @@ class CustomerListView(LoginRequiredMixin, ListView):
return object_list return object_list
class CustomerDetailView(LoginRequiredMixin, DetailView): class CustomerDetailView(LoginRequiredMixin, DetailView):
model = User model = User
template_name = 'dashboard/customer_detail.html' template_name = 'dashboard/customer_detail.html'
context_object_name = 'customer' context_object_name = 'customer'
class CustomerUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): class CustomerUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
model = User model = User
template_name = 'dashboard/customer_form.html' template_name = 'dashboard/customer_form.html'
@ -292,4 +327,3 @@ class CustomerUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
def get_success_url(self): def get_success_url(self):
return reverse('dashboard:customer-detail', kwargs={'pk': self.object.pk}) return reverse('dashboard:customer-detail', kwargs={'pk': self.object.pk})

File diff suppressed because one or more lines are too long