diff options
Diffstat (limited to 'servo/stats')
-rw-r--r-- | servo/stats/__init__.py | 0 | ||||
-rw-r--r-- | servo/stats/forms.py | 109 | ||||
-rw-r--r-- | servo/stats/queries.py | 247 |
3 files changed, 356 insertions, 0 deletions
diff --git a/servo/stats/__init__.py b/servo/stats/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/servo/stats/__init__.py diff --git a/servo/stats/forms.py b/servo/stats/forms.py new file mode 100644 index 0000000..d1ec2aa --- /dev/null +++ b/servo/stats/forms.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2013, First Party Software +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: + +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +from django import forms +from datetime import timedelta +from django.utils import timezone +from servo.forms import DatepickerInput +from django.utils.translation import ugettext as _ + +from servo.models import Location, Group, Status, Tag, Queue + +default_timescale = 'WEEK' +default_end_date = timezone.now() +default_start_date = default_end_date - timedelta(days=30) + + +class BasicStatsForm(forms.Form): + timescale = forms.ChoiceField( + label=_('Time Scale'), + choices=( + ('DAY', _('Day')), + ('WEEK', _('Week')), + ('MONTH', _('Month')) + ) + ) + start_date = forms.DateField( + label=_('Start date'), + widget=DatepickerInput(attrs={'class': "input-small"}) + ) + end_date = forms.DateField( + label=_('End date'), + widget=DatepickerInput(attrs={'class': "input-small"}) + ) + + +class OrderStatsForm(BasicStatsForm): + location = forms.ModelChoiceField( + required=False, + label=_('Location'), + queryset=Location.objects.all() + ) + + +class TechieStatsForm(OrderStatsForm): + group = forms.ModelChoiceField( + required=False, + label=_('Group'), + queryset=Group.objects.all() + ) + + +class StatusStatsForm(OrderStatsForm): + status = forms.ModelChoiceField( + label=_('Status'), + queryset=Status.objects.all() + ) + + +class InvoiceStatsForm(BasicStatsForm): + pass + + +class NewStatsForm(forms.Form): + location = forms.ModelMultipleChoiceField( + queryset=Location.objects.all() + ) + queue = forms.ModelMultipleChoiceField( + queryset=Queue.objects.all(), + widget=forms.SelectMultiple + ) + label = forms.ModelMultipleChoiceField( + queryset=Tag.objects.filter(type="order"), + required=False + ) + status = forms.ModelMultipleChoiceField( + queryset=Status.objects.all(), + required=False + ) + start_date = forms.DateField( + widget=DatepickerInput(attrs={'class': "input-small"}), + initial=default_start_date + ) + end_date = forms.DateField( + widget=DatepickerInput(attrs={'class': "input-small"}), + initial=default_end_date + ) diff --git a/servo/stats/queries.py b/servo/stats/queries.py new file mode 100644 index 0000000..1b892fe --- /dev/null +++ b/servo/stats/queries.py @@ -0,0 +1,247 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2013, First Party Software +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: + +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +import decimal +from django.db import connection + + +class StatsManager: + def __init__(self): + self.cursor = connection.cursor() + + def _result(self, args): + result = [] + self.cursor.execute(self.sql, args) + for k, v in self.cursor.fetchall(): + if isinstance(v, decimal.Decimal): + v = float(v) + result.append((k, v,)) + + return result + + def cases_per_tech(self, location, queues, labels, start, end): + users = User.object.filter(location=location) + + + def statuses_per_location(self, timescale, location, status, start, end): + self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, se.triggered_at))*1000 as p, + COUNT(*) AS v + FROM servo_order so, servo_event se + WHERE (se.triggered_at, se.triggered_at) OVERLAPS (%s, %s) + AND se.action = 'set_status' + AND se.object_id = so.id + AND so.location_id = %s + AND se.description = %s + GROUP BY p + ORDER BY p ASC""" + + return self._result([timescale, start, end, location, status]) + + def statuses_per_user(self, timescale, user, status, start, end): + self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, se.triggered_at))*1000 as p, + COUNT(*) AS v + FROM servo_order so, servo_event se + WHERE (se.triggered_at, se.triggered_at) OVERLAPS (%s, %s) + AND se.action = 'set_status' + AND se.object_id = so.id + AND so.user_id = %s + AND se.description = %s + GROUP BY p + ORDER BY p ASC""" + + return self._result([timescale, start, end, user, status]) + + def sales_invoices(self, timescale, queue, start, end): + self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, so.created_at))*1000 as p, + SUM(total_gross) AS v + FROM servo_invoice si, servo_order so + WHERE (si.created_at, si.created_at) OVERLAPS (%s, %s) + AND si.order_id = so.id + AND so.queue_id = %s + GROUP BY p + ORDER BY p ASC""" + + return self._result([timescale, start, end, queue]) + + def sales_purchases(self, timescale, queue, start, end): + self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, po.created_at))*1000 as p, + SUM(total) AS v + FROM servo_purchaseorder po, servo_order so + WHERE (po.created_at, po.created_at) OVERLAPS (%s, %s) + AND po.sales_order_id = so.id + AND so.queue_id = %s + GROUP BY p + ORDER BY p ASC""" + + return self._result([timescale, start, end, queue]) + + def sales_parts_per_labtier(self, start, end): + self.sql = """SELECT labour_tier, count(*) + FROM servo_product p, servo_servicepart sp, servo_serviceorderitem soi + WHERE soi.product_id = p.id + AND sp.order_item_id = soi.id + AND (soi.created_at, soi.created_at) OVERLAPS (%s, %s) + AND char_length(labour_tier) = 4 + GROUP BY labour_tier + ORDER BY labour_tier""" + + return self._result([start, end]) + + def order_runrate(self, timescale, location, user, start, end): + self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, started_at))*1000 as p, + COUNT(*) AS v + FROM servo_order + WHERE user_id = %s + AND location_id = %s + AND (started_at, started_at) OVERLAPS (%s, %s) + GROUP BY p + ORDER BY p ASC""" + + return self._result([timescale, user, location, start, end]) + + def turnaround_per_location(self, timescale, location, start, end): + self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, created_at))*1000 as p, + EXTRACT(HOUR FROM AVG(closed_at - created_at)) as v + FROM servo_order + WHERE closed_at IS NOT NULL + AND location_id = %s + AND queue_id IS NOT NULL + AND (created_at, created_at) OVERLAPS (%s, %s) + GROUP BY p + ORDER BY p ASC""" + + return self._result([timescale, location, start, end]) + + def runrate_per_location(self, timescale, location, start, end): + self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, created_at))*1000 as p, + COUNT(*) AS v + FROM servo_order + WHERE location_id = %s + AND closed_at IS NOT NULL + AND (created_at, created_at) OVERLAPS (%s, %s) + GROUP BY p + ORDER BY p ASC""" + + return self._result([timescale, location, start, end]) + + def distribution_per_location(self, start, end): + result = [] + self.sql = """SELECT l.title, COUNT(*) + FROM servo_order o LEFT OUTER JOIN servo_location l on (o.location_id = l.id) + WHERE (o.created_at, o.created_at) OVERLAPS (%s, %s) + GROUP BY l.title""" + self.cursor.execute(self.sql, [start, end]) + + for k, v in self.cursor.fetchall(): + result.append({'label': k, 'data': v}) + + return result + + def orders_created_by(self, timescale, location, user, start, end): + self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, created_at))*1000 as p, + COUNT(*) AS v + FROM servo_order + WHERE location_id = %s + AND created_by_id = %s + AND (created_at, created_at) OVERLAPS (%s, %s) + GROUP BY p + ORDER BY p ASC""" + + return self._result([timescale, location, user, start, end]) + + def orders_created_at(self, timescale, location, start, end): + self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, created_at))*1000 as p, + COUNT(*) AS v + FROM servo_order + WHERE location_id = %s + AND (created_at, created_at) OVERLAPS (%s, %s) + GROUP BY p + ORDER BY p ASC""" + + return self._result([timescale, location, start, end]) + + def orders_closed_at(self, timescale, location, start, end): + self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, created_at))*1000 as p, + COUNT(*) AS v + FROM servo_order + WHERE location_id = %s + AND (closed_at, closed_at) OVERLAPS (%s, %s) + GROUP BY p + ORDER BY p ASC""" + + return self._result([timescale, location, start, end]) + + def orders_closed_in(self, timescale, location, queue, start, end): + self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, created_at))*1000 as p, + COUNT(*) AS v + FROM servo_order + WHERE location_id = %s + AND queue_id = %s + AND (closed_at, closed_at) OVERLAPS (%s, %s) + GROUP BY p + ORDER BY p ASC""" + + return self._result([timescale, location, queue, start, end]) + + def order_count(self, timescale, location, queue, start, end): + self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, created_at))*1000 as p, + COUNT(*) AS v + FROM servo_order + WHERE location_id = %s + AND queue_id = %s + AND (created_at, created_at) OVERLAPS (%s, %s) + GROUP BY p + ORDER BY p ASC""" + + return self._result([timescale, location, queue, start, end]) + + def order_turnaround(self, timescale, location, queue, start, end): + self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, created_at))*1000 as p, + EXTRACT(HOUR FROM AVG(closed_at - created_at)) as v + FROM servo_order + WHERE closed_at IS NOT NULL + AND location_id = %s + AND queue_id = %s + AND queue_id IS NOT NULL + AND (created_at, created_at) OVERLAPS (%s, %s) + GROUP BY p + ORDER BY p ASC""" + + return self._result([timescale, location, queue, start, end]) + + def order_turnaround(self, timescale, location, queue, start, end): + self.sql = """SELECT EXTRACT(EPOCH FROM date_trunc(%s, created_at))*1000 as p, + EXTRACT(HOUR FROM AVG(closed_at - created_at)) as v + FROM servo_order + WHERE closed_at IS NOT NULL + AND location_id = %s + AND queue_id = %s + AND queue_id IS NOT NULL + AND (created_at, created_at) OVERLAPS (%s, %s) + GROUP BY p + ORDER BY p ASC""" + + return self._result([timescale, location, queue, start, end]) |