Merge pull request #27 from JamesRamm/dashboard

Dashboard
pull/43/head
James Ramm 2017-02-26 22:38:43 +00:00 zatwierdzone przez GitHub
commit 5a4dccf0e3
13 zmienionych plików z 280 dodań i 5 usunięć

Wyświetl plik

@ -30,11 +30,31 @@ Setup a Wagtail+Longclaw project::
Features
--------
* Order admin page for Wagtail
* Variable shipping rates per country, managed from wagtail admin
* Pluggable basket and checkout API, supporting a variety of payment backends
* Designed to be adaptable to the needs of your own product catalogue
* Complete control of your own front end, just like Wagtail.
View and fulfill orders from the Wagtail admin
+++++++++++++++++++++++++++++++++++++++++++++++
.. figure:: docs/_static/images/order_list.png
The orders list can be sorted and filtered by status, date or customer
.. figure:: docs/_static/images/order_detail.png
Variable Shipping Rates
+++++++++++++++++++++++
Manage your shipping destinations and rates from the Wagtail admin.
Pluggable basket and checkout API
++++++++++++++++++++++++++++++++++
Longclaw provides a simple RESTful API for retrieving/updating the shopping basket and for performing a checkout.
Longclaw currently supports Stripe, Braintree and PayPal (v.zero) payments.
Easy project startup and catalogue modelling
++++++++++++++++++++++++++++++++++++++++++++
Longclaw provides a project template to easily setup your Wagtail + Longclaw project. This sets up a basic ``ProductVariant`` model
so you can get started adding your product-specific fields straight away.
Running Tests
-------------

BIN
docs/_static/images/dashboard.png vendored 100644

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 113 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 91 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 132 KiB

Wyświetl plik

@ -39,6 +39,7 @@ class Order(models.Model):
def total_items(self):
return self.items.count()
class OrderItem(models.Model):
product = models.ForeignKey(PRODUCT_VARIANT_MODEL, on_delete=models.DO_NOTHING)
quantity = models.IntegerField(default=1)

Wyświetl plik

@ -0,0 +1,8 @@
from longclaw.longclawsettings.models import LongclawSettings
def currency(request):
settings = LongclawSettings.for_site(request.site)
return {
'currency_html_code': settings.currency_html_code,
'currency': settings.currency
}

Wyświetl plik

@ -0,0 +1,48 @@
'''
Various stats/analysis calculations
'''
import itertools
import calendar
from datetime import datetime
from django.db.models import Q, Sum, F
from longclaw.longclaworders.models import Order, OrderItem
def current_month():
now = datetime.now()
n_days = calendar.monthrange(now.year, now.month)[1]
month_start = datetime.strptime('1{}{}'.format(now.month, now.year), '%d%m%Y')
month_end = datetime.strptime('{}{}{}'.format(n_days, now.month, now.year), '%d%m%Y')
return month_start, month_end
def sales_for_time_period(from_date, to_date):
'''
Get all sales for a given time period
'''
sales = Order.objects.filter(
Q(payment_date__lte=to_date) & Q(payment_date__gte=from_date)
).exclude(status=Order.CANCELLED)
return sales
def daily_sales(from_date, to_date):
sales = sales_for_time_period(from_date, to_date)
grouped = itertools.groupby(sales, lambda order: order.payment_date.strftime("%Y-%m-%d"))
return grouped
def sales_by_product(from_date, to_date):
sales = OrderItem.objects.filter(
Q(order__payment_date__lte=to_date) & Q(order__payment_date__gte=from_date)
).exclude(
order__status=Order.CANCELLED
).annotate(
title=F('product__product__title')
).values(
'title'
).annotate(
quantity=Sum('quantity')
).order_by('-quantity')
return sales

Wyświetl plik

@ -0,0 +1,97 @@
{% load i18n wagtailadmin_tags %}
<section class="panel nice-padding">
<div class="col6">
<canvas id="daily-sales" width="100" height="75"></canvas>
<br>
</div>
<div class="col6">
<canvas id="popular-products" width="100" height="75"></canvas>
<br>
</div>
</section>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.bundle.min.js"></script>
<script>
var ctx = document.getElementById("daily-sales");
var data = {
labels: {{ labels|safe }},
datasets: [
{
data: {{ daily_income }}
}
]
};
var dailySalesChart = new Chart(ctx, {
type: 'line',
data: data,
options: {
title: {
display: true,
text: 'Revenue this month'
},
legend: {
display: false
},
scales: {
xAxes: [{
scaleLabel: {
display: true,
labelString: 'Date'
}
}],
yAxes: [{
scaleLabel: {
display: true,
labelString: 'Revenue ({{currency}})'
}
}]
}
}
});
var ctx_products = document.getElementById("popular-products");
var data = {
labels: {{ product_labels|safe }},
datasets: [
{
data: {{ sales_volume }}
}
]
};
var popProductsChart = new Chart(ctx_products, {
type: 'bar',
data: data,
options: {
title: {
display: true,
text: 'Popular products this month'
},
scales: {
yAxes: [{
ticks: {
beginAtZero:true
},
scaleLabel: {
display: true,
labelString: 'No. Sales'
}
}],
xAxes: [{
scaleLabel: {
display: true,
labelString: 'Product Title'
}
}]
},
legend: {
display: false
}
}
})
</script>

Wyświetl plik

@ -0,0 +1,5 @@
<li class="icon {{ icon }}">
<a href="{{ url }}">
<span>{{ total|safe }}</span> {{ text }}
</a>
</li>

Wyświetl plik

@ -0,0 +1,94 @@
import datetime
from wagtail.wagtailcore import hooks
from wagtail.wagtailadmin.site_summary import SummaryItem
from longclaw.longclaworders.models import Order
from longclaw.longclawproducts.models import Product
from longclaw.longclawstats import stats
from longclaw.longclawsettings.models import LongclawSettings
class LongclawSummaryItem(SummaryItem):
order = 10
template = 'longclawstats/summary_item.html'
def get_context(self):
return {
'total': 0,
'text': '',
'url': '',
'icon': 'icon-doc-empty-inverse'
}
class OutstandingOrders(LongclawSummaryItem):
order = 10
def get_context(self):
orders = Order.objects.filter(status=Order.SUBMITTED)
return {
'total': orders.count(),
'text': 'Outstanding Orders',
'url': '/admin/longclaworders/order/',
'icon': 'icon-warning'
}
class ProductCount(LongclawSummaryItem):
order = 20
def get_context(self):
return {
'total': Product.objects.all().count(),
'text': 'Products',
'url': '',
'icon': 'icon-list-ul'
}
class MonthlySales(LongclawSummaryItem):
order = 30
def get_context(self):
settings = LongclawSettings.for_site(self.request.site)
sales = stats.sales_for_time_period(*stats.current_month())
return {
'total': "{}{}".format(settings.currency_html_code,
sum(order.total for order in sales)),
'text': 'In sales this month',
'url': '/admin/longclaworders/order/',
'icon': 'icon-tick'
}
class LongclawStatsPanel(SummaryItem):
order = 110
template = 'longclawstats/stats_panel.html'
def get_context(self):
month_start, month_end = stats.current_month()
daily_sales = stats.daily_sales(month_start, month_end)
labels = [(month_start + datetime.timedelta(days=x)).strftime('%Y-%m-%d')
for x in range(0, datetime.datetime.now().day)]
daily_income = [0] * len(labels)
for k, order_group in daily_sales:
i = labels.index(k)
daily_income[i] = float(sum(order.total for order in order_group))
popular_products = stats.sales_by_product(month_start, month_end)[:5]
print(popular_products)
return {
"daily_income": daily_income,
"labels": labels,
"product_labels": list(popular_products.values_list('title', flat=True)),
"sales_volume": list(popular_products.values_list('quantity', flat=True))
}
@hooks.register('construct_homepage_summary_items')
def add_longclaw_summary_items(request, items):
# We are going to replace everything with our own items
items[:] = []
items.extend([
OutstandingOrders(request),
ProductCount(request),
MonthlySales(request)
])
@hooks.register('construct_homepage_panels')
def add_stats_panel(request, panels):
return panels.append(LongclawStatsPanel(request))

Wyświetl plik

@ -56,6 +56,7 @@ INSTALLED_APPS = [
'longclaw.longclaworders',
'longclaw.longclawcheckout',
'longclaw.longclawbasket',
'longclaw.longclawstats',
'home',
'search',
@ -92,6 +93,7 @@ TEMPLATES = [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'longclaw.longclawsettings.context_processors.currency',
],
},
},