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
|
Features
|
||||||
--------
|
--------
|
||||||
|
|
||||||
* Order admin page for Wagtail
|
View and fulfill orders from the Wagtail admin
|
||||||
* 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
|
.. figure:: docs/_static/images/order_list.png
|
||||||
* Complete control of your own front end, just like Wagtail.
|
|
||||||
|
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
|
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):
|
def total_items(self):
|
||||||
return self.items.count()
|
return self.items.count()
|
||||||
|
|
||||||
|
|
||||||
class OrderItem(models.Model):
|
class OrderItem(models.Model):
|
||||||
product = models.ForeignKey(PRODUCT_VARIANT_MODEL, on_delete=models.DO_NOTHING)
|
product = models.ForeignKey(PRODUCT_VARIANT_MODEL, on_delete=models.DO_NOTHING)
|
||||||
quantity = models.IntegerField(default=1)
|
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.longclaworders',
|
||||||
'longclaw.longclawcheckout',
|
'longclaw.longclawcheckout',
|
||||||
'longclaw.longclawbasket',
|
'longclaw.longclawbasket',
|
||||||
|
'longclaw.longclawstats',
|
||||||
|
|
||||||
'home',
|
'home',
|
||||||
'search',
|
'search',
|
||||||
|
@ -92,6 +93,7 @@ TEMPLATES = [
|
||||||
'django.template.context_processors.request',
|
'django.template.context_processors.request',
|
||||||
'django.contrib.auth.context_processors.auth',
|
'django.contrib.auth.context_processors.auth',
|
||||||
'django.contrib.messages.context_processors.messages',
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
'longclaw.longclawsettings.context_processors.currency',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
Ładowanie…
Reference in New Issue