kopia lustrzana https://github.com/longclawshop/longclaw
commit
5a4dccf0e3
30
README.rst
30
README.rst
|
@ -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
|
||||
-------------
|
||||
|
|
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 |
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
|||
<li class="icon {{ icon }}">
|
||||
<a href="{{ url }}">
|
||||
<span>{{ total|safe }}</span> {{ text }}
|
||||
</a>
|
||||
</li>
|
|
@ -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))
|
|
@ -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',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
Ładowanie…
Reference in New Issue