Update post_save for OrderLine to auto-update Order status
This commit is contained in:
parent
f603b268a2
commit
ab155d835c
@ -26,7 +26,7 @@ class OrderStatus:
|
|||||||
DRAFT = "draft" # fully editable, not finalized order created by staff users
|
DRAFT = "draft" # fully editable, not finalized order created by staff users
|
||||||
UNFULFILLED = "unfulfilled" # order with no items marked as fulfilled
|
UNFULFILLED = "unfulfilled" # order with no items marked as fulfilled
|
||||||
PARTIALLY_FULFILLED = (
|
PARTIALLY_FULFILLED = (
|
||||||
"partially fulfilled" # order with some items marked as fulfilled
|
"partially_fulfilled" # order with some items marked as fulfilled
|
||||||
)
|
)
|
||||||
FULFILLED = "fulfilled" # order with all items marked as fulfilled
|
FULFILLED = "fulfilled" # order with all items marked as fulfilled
|
||||||
|
|
||||||
|
|||||||
@ -6,4 +6,4 @@ class CoreConfig(AppConfig):
|
|||||||
name = 'core'
|
name = 'core'
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from .signals import order_created, transaction_created
|
from .signals import order_created, transaction_created, order_line_post_save
|
||||||
|
|||||||
@ -122,6 +122,7 @@ class OrderManager(models.Manager):
|
|||||||
def with_fulfillment(self):
|
def with_fulfillment(self):
|
||||||
return self.annotate(
|
return self.annotate(
|
||||||
total_quantity_fulfilled=models.Sum('lines__quantity_fulfilled'),
|
total_quantity_fulfilled=models.Sum('lines__quantity_fulfilled'),
|
||||||
|
total_quantity_ordered=models.Sum('lines__quantity')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -182,6 +183,7 @@ class Order(models.Model):
|
|||||||
return reverse('dashboard:order-detail', kwargs={'pk': self.pk})
|
return reverse('dashboard:order-detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Transaction(models.Model):
|
class Transaction(models.Model):
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
max_length=32,
|
max_length=32,
|
||||||
|
|||||||
@ -3,9 +3,10 @@ from io import BytesIO
|
|||||||
|
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
from . import TransactionStatus
|
from . import OrderStatus, TransactionStatus
|
||||||
from .models import Order, Transaction
|
from .models import Order, OrderLine, Transaction
|
||||||
from .tasks import send_order_confirmation_email
|
from .tasks import send_order_confirmation_email
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -32,3 +33,31 @@ def transaction_created(sender, instance, created, **kwargs):
|
|||||||
send_order_confirmation_email.delay(order)
|
send_order_confirmation_email.delay(order)
|
||||||
instance.confirmation_email_sent = True
|
instance.confirmation_email_sent = True
|
||||||
instance.save()
|
instance.save()
|
||||||
|
|
||||||
|
def get_order_status(total_quantity_fulfilled, total_quantity_ordered):
|
||||||
|
if total_quantity_fulfilled >= total_quantity_ordered:
|
||||||
|
return OrderStatus.FULFILLED
|
||||||
|
elif total_quantity_fulfilled > 0:
|
||||||
|
return OrderStatus.PARTIALLY_FULFILLED
|
||||||
|
else:
|
||||||
|
return OrderStatus.UNFULFILLED
|
||||||
|
|
||||||
|
@receiver(post_save, sender=OrderLine, dispatch_uid="order_line_post_save")
|
||||||
|
def order_line_post_save(sender, instance, created, **kwargs):
|
||||||
|
if not created:
|
||||||
|
order = Order.objects.with_fulfillment().filter(
|
||||||
|
pk=instance.order.pk
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
order.status = get_order_status(order.total_quantity_fulfilled, order.total_quantity_ordered)
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
# order.update(
|
||||||
|
# status=models.Case(
|
||||||
|
# models.When(models.lookups.GreaterThan(models.F('total_quantity_fulfilled'), models.F('total_quantity_ordered')),
|
||||||
|
# then=models.Value(OrderStatus.FULFILLED)),
|
||||||
|
# models.When(models.lookups.GreaterThan(models.F('total_quantity_fulfilled'), 0),
|
||||||
|
# then=models.Value(OrderStatus.PARTIALLY_FULFILLED)),
|
||||||
|
# default=models.Value(OrderStatus.UNFULFILLED)
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
|||||||
@ -32,6 +32,7 @@
|
|||||||
<p>No items in order yet.</p>
|
<p>No items in order yet.</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="object__item">
|
<div class="object__item">
|
||||||
|
<span>Total fulfilled: {{order.total_quantity_fulfilled}}</span>
|
||||||
<a href="{% url 'dashboard:order-fulfill' order.pk %}" class="action-button order__fulfill">Fulfill →</a>
|
<a href="{% url 'dashboard:order-fulfill' order.pk %}" class="action-button order__fulfill">Fulfill →</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -41,6 +42,7 @@
|
|||||||
<h4>Shipping</h4>
|
<h4>Shipping</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel__item">
|
<div class="panel__item">
|
||||||
|
<a href="" class="action-button action-button--warning">Cancel order</a>
|
||||||
<a href="{% url 'dashboard:order-fulfill' order.pk %}" class="action-button order__fulfill">Ship order →</a>
|
<a href="{% url 'dashboard:order-fulfill' order.pk %}" class="action-button order__fulfill">Ship order →</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -77,6 +77,8 @@ class OrderDetailView(DetailView):
|
|||||||
def get_object(self):
|
def get_object(self):
|
||||||
queryset = Order.objects.filter(
|
queryset = Order.objects.filter(
|
||||||
pk=self.kwargs.get(self.pk_url_kwarg)
|
pk=self.kwargs.get(self.pk_url_kwarg)
|
||||||
|
).annotate(
|
||||||
|
total_quantity_fulfilled=Sum('lines__quantity_fulfilled'),
|
||||||
).select_related(
|
).select_related(
|
||||||
'customer',
|
'customer',
|
||||||
'billing_address',
|
'billing_address',
|
||||||
@ -106,21 +108,6 @@ class OrderFulfillView(UpdateView):
|
|||||||
return reverse('dashboard:order-detail', kwargs={'pk': self.object.pk})
|
return reverse('dashboard:order-detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
def order_fulfill(request, pk):
|
|
||||||
order = Order.objects.get(pk=pk)
|
|
||||||
OrderLineFormset = inlineformset_factory(Order, OrderLine, form=OrderLineFulfillForm, extra=0, can_delete=False)
|
|
||||||
if request.method == "POST":
|
|
||||||
formset = OrderLineFormset(request.POST, request.FILES, instance=order)
|
|
||||||
if formset.is_valid():
|
|
||||||
formset.save()
|
|
||||||
# Do something. Should generally end with a redirect. For example:
|
|
||||||
return HttpResponseRedirect(order.get_absolute_url())
|
|
||||||
|
|
||||||
else:
|
|
||||||
formset = OrderLineFormset(instance=order)
|
|
||||||
return render(request, 'dashboard/order_fulfill.html', {'formset': formset, 'order': order})
|
|
||||||
|
|
||||||
|
|
||||||
class ProductListView(ListView):
|
class ProductListView(ListView):
|
||||||
model = Product
|
model = Product
|
||||||
template_name = 'dashboard/product_list.html'
|
template_name = 'dashboard/product_list.html'
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import os
|
|||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
DEBUG = os.environ.get('DEBUG', True)
|
DEBUG = os.environ.get('DEBUG', 'True') == 'True'
|
||||||
|
|
||||||
DATABASE_CONFIG = {
|
DATABASE_CONFIG = {
|
||||||
'ENGINE' : 'django.db.backends.postgresql',
|
'ENGINE' : 'django.db.backends.postgresql',
|
||||||
|
|||||||
@ -146,9 +146,12 @@ USE_TZ = True
|
|||||||
# https://docs.djangoproject.com/en/3.2/howto/static-files/
|
# https://docs.djangoproject.com/en/3.2/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = '/static/'
|
||||||
STATIC_ROOT = BASE_DIR / 'public'
|
if DEBUG:
|
||||||
|
STATIC_ROOT = BASE_DIR / 'public'
|
||||||
|
else:
|
||||||
|
STATIC_ROOT = '/var/www/ptcoffee-dev/static/'
|
||||||
STATICFILES_DIRS = [BASE_DIR / 'static']
|
STATICFILES_DIRS = [BASE_DIR / 'static']
|
||||||
MEDIA_URL = '/images/'
|
MEDIA_URL = '/media/'
|
||||||
MEDIA_ROOT = BASE_DIR / 'media'
|
MEDIA_ROOT = BASE_DIR / 'media'
|
||||||
|
|
||||||
STATICFILES_FINDERS = (
|
STATICFILES_FINDERS = (
|
||||||
|
|||||||
@ -13,5 +13,7 @@ urlpatterns = [
|
|||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('__debug__/', include('debug_toolbar.urls')),
|
path('__debug__/', include('debug_toolbar.urls')),
|
||||||
]
|
]
|
||||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
|
||||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
if settings.DEBUG:
|
||||||
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|||||||
@ -1,60 +0,0 @@
|
|||||||
import { getCookie, setCookie } from "../lib/cookie.js"
|
|
||||||
import { Controller } from "../stimulus.js"
|
|
||||||
|
|
||||||
export default class extends Controller {
|
|
||||||
static get targets() {
|
|
||||||
return [ "cartList" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
console.log(getCookie('cart'))
|
|
||||||
this.cart = this.getOrSetCart()
|
|
||||||
this.element.addEventListener('addToCart', this.addItem.bind(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
getOrSetCart() {
|
|
||||||
let cart = JSON.parse(getCookie('cart'))
|
|
||||||
if (cart === null) {
|
|
||||||
console.log('created cart cookie')
|
|
||||||
setCookie('cart', '{}')
|
|
||||||
cart = {}
|
|
||||||
} else {
|
|
||||||
Object.keys(cart).forEach((item_id) => {
|
|
||||||
fetch(`/${item_id}/`)
|
|
||||||
.then((response) => response.text())
|
|
||||||
.then((html) => {
|
|
||||||
this.cartListTarget.innerHTML += html;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return cart
|
|
||||||
}
|
|
||||||
|
|
||||||
decodeCartItems(cart) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
addItem(event) {
|
|
||||||
if (this.cart[event.detail.item] === undefined) {
|
|
||||||
this.cart[event.detail.item] = {'quantity': 1}
|
|
||||||
} else {
|
|
||||||
this.cart[event.detail.item]['quantity'] += 1
|
|
||||||
}
|
|
||||||
setCookie('cart', JSON.stringify(this.cart))
|
|
||||||
console.log(this.cart)
|
|
||||||
|
|
||||||
fetch(`/${event.detail.item}/`)
|
|
||||||
.then((response) => response.text())
|
|
||||||
.then((html) => {
|
|
||||||
this.cartListTarget.innerHTML += html;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
removeItem() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import { getCookie, setCookie } from "../lib/cookie.js"
|
|
||||||
import { Controller } from "../stimulus.js"
|
|
||||||
|
|
||||||
export default class extends Controller {
|
|
||||||
static get targets() {
|
|
||||||
return [ "qty" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
console.log(this.qtyTarget)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
// import getCookie from "../get_cookie.js"
|
|
||||||
import { Controller } from "../stimulus.js"
|
|
||||||
|
|
||||||
export default class extends Controller {
|
|
||||||
static values = { url: String }
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
this.event = new CustomEvent('addToCart', {
|
|
||||||
detail: {
|
|
||||||
item: this.urlValue
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
addToCart(event) {
|
|
||||||
event.preventDefault()
|
|
||||||
window.dispatchEvent(this.event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import { Application } from "./stimulus.js"
|
|
||||||
|
|
||||||
import CartController from "./controllers/cart_controller.js"
|
|
||||||
import CartitemController from "./controllers/cartitem_controller.js"
|
|
||||||
import ProductController from "./controllers/product_controller.js"
|
|
||||||
|
|
||||||
const application = Application.start()
|
|
||||||
application.register("cart", CartController)
|
|
||||||
application.register("cartitem", CartitemController)
|
|
||||||
application.register("product", ProductController)
|
|
||||||
@ -60,99 +60,3 @@ paypal.Buttons({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).render('#paypal-button-container');
|
}).render('#paypal-button-container');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// RETURNED DATA
|
|
||||||
// {
|
|
||||||
// "id": "1WM83684A69628456",
|
|
||||||
// "status": "COMPLETED",
|
|
||||||
// "purchase_units": [
|
|
||||||
// {
|
|
||||||
// "reference_id": "default",
|
|
||||||
// "shipping": {
|
|
||||||
// "name": {
|
|
||||||
// "full_name": "John Doe"
|
|
||||||
// },
|
|
||||||
// "address": {
|
|
||||||
// "address_line_1": "1 Main St",
|
|
||||||
// "admin_area_2": "San Jose",
|
|
||||||
// "admin_area_1": "CA",
|
|
||||||
// "postal_code": "95131",
|
|
||||||
// "country_code": "US"
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// "payments": {
|
|
||||||
// "captures": [
|
|
||||||
// {
|
|
||||||
// "id": "9AU23265T5630860D",
|
|
||||||
// "status": "COMPLETED",
|
|
||||||
// "amount": {
|
|
||||||
// "currency_code": "USD",
|
|
||||||
// "value": "13.40"
|
|
||||||
// },
|
|
||||||
// "final_capture": true,
|
|
||||||
// "seller_protection": {
|
|
||||||
// "status": "ELIGIBLE",
|
|
||||||
// "dispute_categories": [
|
|
||||||
// "ITEM_NOT_RECEIVED",
|
|
||||||
// "UNAUTHORIZED_TRANSACTION"
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
// "seller_receivable_breakdown": {
|
|
||||||
// "gross_amount": {
|
|
||||||
// "currency_code": "USD",
|
|
||||||
// "value": "13.40"
|
|
||||||
// },
|
|
||||||
// "paypal_fee": {
|
|
||||||
// "currency_code": "USD",
|
|
||||||
// "value": "0.96"
|
|
||||||
// },
|
|
||||||
// "net_amount": {
|
|
||||||
// "currency_code": "USD",
|
|
||||||
// "value": "12.44"
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// "links": [
|
|
||||||
// {
|
|
||||||
// "href": "https://api.sandbox.paypal.com/v2/payments/captures/9AU23265T5630860D",
|
|
||||||
// "rel": "self",
|
|
||||||
// "method": "GET"
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// "href": "https://api.sandbox.paypal.com/v2/payments/captures/9AU23265T5630860D/refund",
|
|
||||||
// "rel": "refund",
|
|
||||||
// "method": "POST"
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// "href": "https://api.sandbox.paypal.com/v2/checkout/orders/1WM83684A69628456",
|
|
||||||
// "rel": "up",
|
|
||||||
// "method": "GET"
|
|
||||||
// }
|
|
||||||
// ],
|
|
||||||
// "create_time": "2022-02-28T15:33:07Z",
|
|
||||||
// "update_time": "2022-02-28T15:33:07Z"
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// ],
|
|
||||||
// "payer": {
|
|
||||||
// "name": {
|
|
||||||
// "given_name": "John",
|
|
||||||
// "surname": "Doe"
|
|
||||||
// },
|
|
||||||
// "email_address": "sb-rlst914027742@personal.example.com",
|
|
||||||
// "payer_id": "G9RGHQ72CGKF6",
|
|
||||||
// "address": {
|
|
||||||
// "country_code": "US"
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// "links": [
|
|
||||||
// {
|
|
||||||
// "href": "https://api.sandbox.paypal.com/v2/checkout/orders/1WM83684A69628456",
|
|
||||||
// "rel": "self",
|
|
||||||
// "method": "GET"
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -6,6 +6,7 @@
|
|||||||
--yellow-color: #f8a911;
|
--yellow-color: #f8a911;
|
||||||
--yellow-alt-color: #f6c463;
|
--yellow-alt-color: #f6c463;
|
||||||
--green-color: #13ce65;
|
--green-color: #13ce65;
|
||||||
|
--red-color: #ff4d44;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
@ -93,6 +94,23 @@ button:hover {
|
|||||||
background-color: var(--yellow-alt-color);
|
background-color: var(--yellow-alt-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-button--warning {
|
||||||
|
background-color: var(--red-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-link {
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--yellow-color);
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-link--warning {
|
||||||
|
color: var(--red-color);
|
||||||
|
}
|
||||||
|
|
||||||
figure {
|
figure {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -276,13 +294,13 @@ main article {
|
|||||||
background-color: var(--gray-color);
|
background-color: var(--gray-color);
|
||||||
}
|
}
|
||||||
.order__status--unfulfilled {
|
.order__status--unfulfilled {
|
||||||
background-color: var(--yellow-alt-color);
|
background-color: var(--red-color);
|
||||||
}
|
}
|
||||||
.order__status--partially_returned {
|
.order__status--partially_returned {
|
||||||
background-color: var(--gray-color);
|
background-color: var(--yellow-alt-color);
|
||||||
}
|
}
|
||||||
.order__status--partially_fulfilled {
|
.order__status--partially_fulfilled {
|
||||||
background-color: var(--gray-color);
|
background-color: var(--yellow-alt-color);
|
||||||
}
|
}
|
||||||
.order__status--returned {
|
.order__status--returned {
|
||||||
background-color: var(--gray-color);
|
background-color: var(--gray-color);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user