aboutsummaryrefslogtreecommitdiffstats
path: root/servo/views
diff options
context:
space:
mode:
authorFilipp Lepalaan <filipp@mac.com>2015-08-04 10:11:24 +0300
committerFilipp Lepalaan <filipp@mac.com>2015-08-04 10:11:24 +0300
commit63b0fc6269b38edf7234b9f151b80d81f614c0a3 (patch)
tree555de3068f33f8dddb4619349bbea7d9b7c822fd /servo/views
downloadServo-63b0fc6269b38edf7234b9f151b80d81f614c0a3.tar.gz
Servo-63b0fc6269b38edf7234b9f151b80d81f614c0a3.tar.bz2
Servo-63b0fc6269b38edf7234b9f151b80d81f614c0a3.zip
Initial commit
First public commit
Diffstat (limited to 'servo/views')
-rw-r--r--servo/views/__init__.py0
-rw-r--r--servo/views/account.py450
-rw-r--r--servo/views/admin.py778
-rw-r--r--servo/views/api.py401
-rw-r--r--servo/views/checkin.py418
-rw-r--r--servo/views/customer.py505
-rw-r--r--servo/views/device.py605
-rw-r--r--servo/views/error.py53
-rw-r--r--servo/views/events.py44
-rw-r--r--servo/views/files.py52
-rw-r--r--servo/views/gsx.py349
-rw-r--r--servo/views/invoices.py199
-rw-r--r--servo/views/note.py435
-rw-r--r--servo/views/order.py990
-rw-r--r--servo/views/product.py474
-rw-r--r--servo/views/purchases.py242
-rw-r--r--servo/views/queue.py40
-rw-r--r--servo/views/rules.py101
-rw-r--r--servo/views/search.py254
-rw-r--r--servo/views/shipments.py392
-rw-r--r--servo/views/stats.py443
-rw-r--r--servo/views/tags.py37
22 files changed, 7262 insertions, 0 deletions
diff --git a/servo/views/__init__.py b/servo/views/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/servo/views/__init__.py
diff --git a/servo/views/account.py b/servo/views/account.py
new file mode 100644
index 0000000..39193b6
--- /dev/null
+++ b/servo/views/account.py
@@ -0,0 +1,450 @@
+# -*- 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 csv
+import pytz
+from datetime import date
+
+from django.contrib import auth
+from django.utils import timezone, translation
+
+from django.contrib import messages
+from django.http import HttpResponse
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect, render
+from dateutil.relativedelta import relativedelta
+from django.utils.translation import ugettext as _
+from django.contrib.auth.decorators import permission_required
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+
+from servo.views.order import prepare_list_view
+
+from servo.models import Order, User, Calendar, CalendarEvent
+from servo.forms.account import ProfileForm, RegistrationForm, LoginForm
+
+
+def settings(request, username):
+ """
+ User editing their profile preferences
+ """
+ title = _("Profile Settings")
+ form = ProfileForm(instance=request.user)
+
+ if request.method == "POST":
+
+ form = ProfileForm(request.POST, request.FILES, instance=request.user)
+
+ if form.is_valid():
+ user = form.save()
+ messages.success(request, _("Settings saved"))
+ User.refresh_nomail()
+
+ if form.cleaned_data['password1']:
+ request.user.set_password(form.cleaned_data['password1'])
+ request.user.save()
+
+ lang = user.activate_locale()
+ translation.activate(lang)
+ request.session[translation.LANGUAGE_SESSION_KEY] = lang
+ request.session['django_timezone'] = user.timezone
+
+ return redirect(settings, username)
+ else:
+ print("Error in user settings: %s" % form.errors)
+ messages.error(request, _("Error in user details"))
+
+ return render(request, "accounts/settings.html", locals())
+
+
+def orders(request, username):
+ """
+ This is basically like orders/index, but limited to the user
+ First, filter by the provided search criteria,
+ then check if we have a saved search filter
+ then default to user id
+ Always update saved search filter
+ """
+ args = request.GET.copy()
+
+ if not args:
+ args = request.session.get("account_search_filter", args)
+
+ if not args:
+ args.update({'state': 1}) # default to open cases
+
+ # Filter by the user, no matter what
+ args.update({'followed_by': request.user.pk})
+ request.session['account_search_filter'] = args
+
+ data = prepare_list_view(request, args)
+ data['title'] = _("My Orders")
+
+ del(data['form'].fields['assigned_to'])
+
+ return render(request, "accounts/orders.html", data)
+
+
+def login(request):
+ """
+ User trying to log in
+ """
+ title = _("Sign In")
+ form = LoginForm()
+
+ if 'username' in request.POST:
+
+ form = LoginForm(request.POST)
+
+ if form.is_valid():
+ user = auth.authenticate(
+ username=form.cleaned_data['username'],
+ password=form.cleaned_data['password']
+ )
+
+ if user is None:
+ messages.error(request, _("Incorrect username or password"))
+ elif not user.is_active:
+ messages.error(request, _("Your account has been deactivated"))
+ else:
+ auth.login(request, user)
+
+ if user.location:
+ lang = user.activate_locale()
+ request.session['django_language'] = lang
+ request.session['django_timezone'] = user.timezone
+
+ messages.success(request, _(u"%s logged in") % user.get_full_name())
+
+ if request.GET.get('next'):
+ return redirect(request.GET['next'])
+ else:
+ return redirect(orders, username=user.username)
+ else:
+ messages.error(request, _("Invalid input for login"))
+
+ return render(request, "accounts/login.html", locals())
+
+
+def logout(request):
+ if request.method == 'POST':
+ auth.logout(request)
+ messages.info(request, _("You have logged out"))
+
+ return redirect(login)
+
+ return render(request, "accounts/logout.html")
+
+
+@permission_required("servo.add_calendar")
+def calendars(request, username=None):
+ data = {'title': _('Calendars')}
+ data['calendars'] = Calendar.objects.filter(user=request.user)
+
+ if data['calendars'].count() > 0:
+ cal = data['calendars'][0]
+ return redirect(view_calendar, username, cal.pk)
+
+ return render(request, "accounts/calendars.html", data)
+
+
+@permission_required("servo.add_calendar")
+def prepare_calendar_view(request, pk, view, start_date):
+ """
+ Prepares a calendar detail view for other views to use
+ """
+ calendar = Calendar.objects.get(user=request.user, pk=pk)
+
+ if start_date is not None:
+ year, month, day = start_date.split("-")
+ start_date = date(int(year), int(month), int(day))
+ else:
+ start_date = timezone.now().date()
+
+ start = start_date
+ finish = start_date + relativedelta(days=+1)
+
+ if view == "week":
+ start = start_date + relativedelta(day=1)
+ finish = start_date + relativedelta(weeks=+1)
+
+ if view == "month":
+ start = start_date + relativedelta(day=1)
+ finish = start_date + relativedelta(day=1, months=+1, days=-1)
+
+ data = {'title': "%s %s - %s" % (calendar.title, start.strftime("%x"),
+ finish.strftime("%x"))}
+
+ data['view'] = view
+ data['start'] = start
+ data['finish'] = finish
+
+ data['next'] = finish + relativedelta(days=+1)
+ data['previous'] = start + relativedelta(days=-1)
+
+ data['calendars'] = Calendar.objects.filter(user=request.user)
+ data['events'] = calendar.calendarevent_set.filter(
+ started_at__range=(start, finish)
+ )
+
+ data['calendar'] = calendar
+ data['subtitle'] = calendar.subtitle(start, finish)
+
+ return data
+
+
+@permission_required("servo.add_calendar")
+def download_calendar(request, username, pk, view):
+ calendar = Calendar.objects.get(pk=pk)
+
+ response = HttpResponse(content_type="text/csv")
+ response['Content-Disposition'] = 'attachment; filename="%s.csv"' % calendar.title
+ writer = csv.writer(response)
+ writer.writerow(['START', 'FINISH', 'HOURS', 'NOTES'])
+
+ for e in calendar.calendarevent_set.all():
+ writer.writerow([e.started_at, e.finished_at, e.get_hours(), e.notes])
+
+ return response
+
+
+@permission_required("servo.add_calendar")
+def print_calendar(request, username, pk, view, start_date):
+ data = prepare_calendar_view(request, pk, view, start_date)
+ calendar = data['calendar']
+
+ data['location'] = request.user.location
+ # Don't show unfinished events in the report
+ data['events'] = data['events'].exclude(finished_at=None)
+ data['subtitle'] = calendar.subtitle(data['start'], data['finish'])
+ return render(request, "accounts/print_calendar.html", data)
+
+
+@permission_required("servo.add_calendar")
+def view_calendar(request, username, pk, view, start_date=None):
+ data = prepare_calendar_view(request, pk, view, start_date)
+ data['base_url'] = reverse(view_calendar, args=[username, pk, view])
+
+ return render(request, "accounts/view_calendar.html", data)
+
+
+@permission_required("servo.delete_calendar")
+def delete_calendar(request, username, pk):
+ calendar = Calendar.objects.get(pk=pk)
+
+ if calendar.user != request.user:
+ messages.error(request, _("Users can only delete their own calendars!"))
+
+ return redirect(calendars, username=username)
+
+ if request.method == "POST":
+ calendar.delete()
+ messages.success(request, _('Calendar deleted'))
+ return redirect(calendars, username=request.user.username)
+
+ data = {'title': _("Really delete this calendar?")}
+ data['action'] = request.path
+
+ return render(request, "accounts/delete_calendar.html", data)
+
+
+@permission_required("servo.change_calendar")
+def edit_calendar(request, username, pk=None, view="week"):
+ from servo.models.calendar import CalendarForm
+ calendar = Calendar(user=request.user)
+
+ if pk is not None:
+ calendar = Calendar.objects.get(pk=pk)
+
+ if request.method == "POST":
+ form = CalendarForm(request.POST, instance=calendar)
+
+ if form.is_valid():
+ calendar = form.save()
+ messages.success(request, _("Calendar saved"))
+ return redirect(view_calendar, username, calendar.pk, 'week')
+
+ form = CalendarForm(instance=calendar)
+
+ data = {'title': calendar.title}
+ data['form'] = form
+ data['action'] = request.path
+
+ return render(request, "accounts/calendar_form.html", data)
+
+
+@permission_required('servo.change_calendar')
+def edit_calendar_event(request, username, cal_pk, pk=None):
+ from servo.models.calendar import CalendarEventForm
+
+ calendar = Calendar.objects.get(pk=cal_pk)
+ event = CalendarEvent(calendar=calendar)
+
+ if pk:
+ event = CalendarEvent.objects.get(pk=pk)
+ else:
+ event.save()
+ messages.success(request, _(u'Calendar event created'))
+ return redirect(event.calendar)
+
+ form = CalendarEventForm(instance=event)
+
+ if request.method == 'POST':
+ form = CalendarEventForm(request.POST, instance=event)
+
+ if form.is_valid():
+ event = form.save()
+ messages.success(request, _(u'Event saved'))
+ return redirect(event.calendar)
+
+ data = {'title': _(u'Edit Event')}
+ data['form'] = form
+ data['calendars'] = Calendar.objects.filter(user=request.user)
+
+ return render(request, 'accounts/edit_calendar_event.html', data)
+
+
+@permission_required("servo.change_calendar")
+def finish_calendar_event(request, username, cal_pk, pk):
+ event = CalendarEvent.objects.get(pk=pk)
+ event.set_finished()
+ messages.success(request, _(u'Calendar event updated'))
+
+ return redirect(view_calendar, username, cal_pk, 'week')
+
+
+def delete_calendar_event(request, username, cal_pk, pk):
+ if username != request.user.username:
+ messages.error(request, _(u'Users can only delete their own events!'))
+
+ return redirect(calendars, username=request.user.username)
+
+ event = CalendarEvent.objects.get(pk=pk)
+
+ if request.method == 'POST':
+ event.delete()
+ messages.success(request, _('Calendar event deleted'))
+ return redirect(event.calendar)
+
+ data = {'title': _(u'Really delete this event?')}
+ data['action'] = request.path
+ return render(request, 'accounts/delete_calendar_event.html', data)
+
+
+def register(request):
+ """
+ New user applying for access
+ """
+ form = RegistrationForm()
+ data = {'title': _("Register")}
+
+ if request.method == 'POST':
+
+ form = RegistrationForm(request.POST)
+
+ if form.is_valid():
+ user = User(is_active=False)
+ user.email = form.cleaned_data['email']
+ user.last_name = form.cleaned_data['last_name']
+ user.first_name = form.cleaned_data['first_name']
+ user.set_password(form.cleaned_data['password'])
+ user.save()
+
+ messages.success(request, _(u'Your registration is now pending approval.'))
+
+ return redirect(login)
+
+ data['form'] = form
+ return render(request, 'accounts/register.html', data)
+
+
+def clear_notifications(request, username):
+ from datetime import datetime
+ ts = [int(x) for x in request.GET.get('t').split('/')]
+ ts = datetime(*ts, tzinfo=timezone.get_current_timezone())
+ notif = request.user.notifications.filter(handled_at=None)
+ notif.filter(triggered_at__lt=ts).update(handled_at=timezone.now())
+ messages.success(request, _('All notifications cleared'))
+ return redirect(request.META['HTTP_REFERER'])
+
+
+def search(request, username):
+ """
+ User searching for something from their homepage
+ """
+ query = request.GET.get("q")
+
+ if not query or len(query) < 3:
+ messages.error(request, _('Search query is too short'))
+ return redirect('accounts-list_orders', username)
+
+ request.session['search_query'] = query
+
+ # Redirect Order ID:s to the order
+ try:
+ order = Order.objects.get(code__iexact=query)
+ return redirect(order)
+ except Order.DoesNotExist:
+ pass
+
+ kwargs = request.GET.copy()
+ kwargs.update({'followed_by': request.user.pk})
+ data = prepare_list_view(request, kwargs)
+
+ data['title'] = _("Search results")
+ orders = data['queryset']
+ data['orders'] = orders.filter(customer__fullname__icontains=query)
+
+ return render(request, "accounts/orders.html", data)
+
+
+def stats(request, username):
+ from servo.views.stats import prep_view, BasicStatsForm
+ data = prep_view(request)
+ form = BasicStatsForm(initial=data['initial'])
+ if request.method == 'POST':
+ form = BasicStatsForm(request.POST, initial=data['initial'])
+ if form.is_valid():
+ request.session['stats_filter'] = form.cleaned_data
+ data['form'] = form
+ return render(request, "accounts/stats.html", data)
+
+
+def updates(request, username):
+ title = _('Updates')
+ kind = request.GET.get('kind', 'note_added')
+ events = request.user.notifications.filter(action=kind)
+
+ page = request.GET.get("page")
+ paginator = Paginator(events, 100)
+
+ try:
+ events = paginator.page(page)
+ except PageNotAnInteger:
+ events = paginator.page(1)
+ except EmptyPage:
+ events = paginator.page(paginator.num_pages)
+
+ return render(request, "accounts/updates.html", locals())
diff --git a/servo/views/admin.py b/servo/views/admin.py
new file mode 100644
index 0000000..91f73a9
--- /dev/null
+++ b/servo/views/admin.py
@@ -0,0 +1,778 @@
+# -*- 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 gsxws
+
+from django.db import IntegrityError, transaction
+
+from django.core.cache import cache
+from django.contrib import messages
+from django.shortcuts import render, redirect, get_object_or_404
+
+from django.conf import settings as app_settings
+from django.utils.translation import ugettext as _
+
+from django.contrib.admin.views.decorators import staff_member_required
+from django.contrib.auth.models import Group
+
+from django.forms.models import (inlineformset_factory,
+ modelform_factory,
+ modelformset_factory,)
+
+from servo.forms.admin import *
+from servo.models.common import *
+from servo.models.repair import Checklist, ChecklistItem
+from servo.models.account import User, Group
+from servo.models.product import ShippingMethod
+
+
+def prep_list_view(model):
+ title = model._meta.verbose_name_plural
+ object_list = model.objects.all()
+ return locals()
+
+
+@staff_member_required
+def list_gsx_accounts(request):
+ object_list = GsxAccount.objects.all()
+ title = GsxAccount._meta.verbose_name_plural
+
+ if object_list.count() > 0:
+ return redirect(object_list[0].get_admin_url())
+
+ return render(request, 'admin/gsx/index.html', locals())
+
+
+@staff_member_required
+def edit_gsx_account(request, pk=None):
+ object_list = GsxAccount.objects.all()
+ title = GsxAccount._meta.verbose_name_plural
+
+ if pk is None:
+ act = GsxAccount()
+ else:
+ act = GsxAccount.objects.get(pk=pk)
+
+ form = GsxAccountForm(instance=act)
+
+ if request.method == 'POST':
+ form = GsxAccountForm(request.POST, instance=act)
+ if form.is_valid():
+ try:
+ act = form.save()
+ cache.delete('gsx_session')
+ try:
+ act.test()
+ messages.success(request, _(u'%s saved') % act.title)
+ return redirect(list_gsx_accounts)
+ except gsxws.GsxError, e:
+ messages.warning(request, e)
+ except IntegrityError:
+ transaction.rollback()
+ msg = _('GSX account for this sold-to and environment already exists')
+ messages.error(request, msg)
+
+ return render(request, 'admin/gsx/form.html', locals())
+
+
+@staff_member_required
+def delete_gsx_account(request, pk=None):
+ act = GsxAccount.objects.get(pk=pk)
+ if request.method == 'POST':
+ try:
+ act.delete()
+ messages.success(request, _("GSX account deleted"))
+ except Exception, e:
+ messages.error(request, e)
+
+ return redirect(list_gsx_accounts)
+
+ return render(request, 'admin/gsx/remove.html', {'action': request.path})
+
+
+@staff_member_required
+def checklists(request):
+ object_list = Checklist.objects.all()
+ title = Checklist._meta.verbose_name_plural
+
+ if object_list.count() > 0:
+ return redirect(object_list[0].get_admin_url())
+
+ return render(request, 'admin/checklist/index.html', locals())
+
+
+@staff_member_required
+def edit_checklist(request, pk=None):
+ object_list = Checklist.objects.all()
+ title = Checklist._meta.verbose_name_plural
+ ChecklistItemFormset = inlineformset_factory(Checklist, ChecklistItem, exclude=[])
+
+ if pk is None:
+ checklist = Checklist()
+ else:
+ checklist = Checklist.objects.get(pk=pk)
+
+ form = ChecklistForm(instance=checklist)
+ formset = ChecklistItemFormset(instance=checklist)
+
+ if request.method == 'POST':
+ form = ChecklistForm(request.POST, instance=checklist)
+
+ if form.is_valid():
+ checklist = form.save()
+ formset = ChecklistItemFormset(request.POST, instance=checklist)
+
+ if formset.is_valid():
+ formset.save()
+ messages.success(request, _('Checklist saved'))
+ return redirect(checklist.get_admin_url())
+
+ return render(request, 'admin/checklist/form.html', locals())
+
+
+@staff_member_required
+def delete_checklist(request, pk):
+ checklist = Checklist.objects.get(pk=pk)
+
+ if request.method == 'POST':
+ checklist.delete()
+ messages.success(request, _('Checklist deleted'))
+ return redirect(checklists)
+
+ action = str(request.path)
+ title = _('Really delete this checklist?')
+ explanation = _('This will also delete all checklist values.')
+
+ return render(request, 'generic/delete.html', locals())
+
+
+@staff_member_required
+def tags(request, type=None):
+ if type is None:
+ type = Tag.TYPES[0][0]
+
+ title = Checklist._meta.verbose_name_plural
+ object_list = Tag.objects.filter(type=type)
+
+ if object_list.count() > 0:
+ return redirect(object_list[0].get_admin_url())
+
+ types = Tag.TYPES
+
+ return render(request, 'admin/tags/index.html', locals())
+
+
+@staff_member_required
+def edit_tag(request, type, pk=None):
+ if pk is None:
+ tag = Tag(type=type)
+ else:
+ tag = Tag.objects.get(pk=pk)
+
+ TagForm = modelform_factory(Tag, exclude=[])
+ form = TagForm(instance=tag)
+
+ if request.method == 'POST':
+ form = TagForm(request.POST, instance=tag)
+
+ if form.is_valid():
+ tag = form.save()
+ messages.success(request, _(u'Tag %s saved') % tag.title)
+ return redirect(edit_tag, tag.type, tag.pk)
+
+ types = Tag.TYPES
+ title = Tag._meta.verbose_name_plural
+ object_list = Tag.objects.filter(type=type)
+ return render(request, 'admin/tags/form.html', locals())
+
+
+@staff_member_required
+def delete_tag(request, pk):
+ tag = Tag.objects.get(pk=pk)
+
+ if request.method == 'POST':
+ tag.delete()
+ messages.success(request, _('Tag deleted'))
+ return redirect(tags, type=tag.type)
+
+ title = _('Really delete this tag?')
+ action = str(request.path)
+
+ return render(request, 'generic/delete.html', locals())
+
+
+@staff_member_required
+def settings(request):
+ title = _('System Settings')
+ ShippingMethodFormset = modelformset_factory(ShippingMethod,
+ can_delete=True,
+ extra=0,
+ exclude=[])
+ formset = ShippingMethodFormset(queryset=ShippingMethod.objects.all())
+
+ if request.method == 'POST':
+ form = SettingsForm(request.POST, request.FILES)
+
+ if not form.is_valid():
+ messages.error(request, _('Check your settings'))
+ return render(request, 'admin/settings.html', locals())
+
+ config = form.save()
+
+ if request.POST.get('update_prices'):
+ from servo.models import Product
+ for p in Product.objects.filter(fixed_price=False):
+ p.set_stock_sales_price()
+ p.set_exchange_sales_price()
+ p.save()
+
+ # formset = ShippingMethodFormset(request.POST)
+
+ # if not formset.is_valid():
+ # messages.error(request, _('Error in shipping method settings'))
+ # return render(request, 'admin/settings.html', locals())
+
+ # formset.save()
+
+ messages.success(request, _('Settings saved'))
+ return redirect(settings)
+
+ config = Configuration.conf()
+ form = SettingsForm(initial=config)
+
+ return render(request, 'admin/settings.html', locals())
+
+
+@staff_member_required
+def statuses(request):
+ object_list = Status.objects.all()
+ title = Status._meta.verbose_name_plural
+ if object_list.count() > 0:
+ return redirect(edit_status, object_list[0].pk)
+
+ return render(request, 'admin/statuses/index.html', locals())
+
+
+@staff_member_required
+def edit_status(request, pk=None):
+ if pk is None:
+ status = Status()
+ else:
+ status = Status.objects.get(pk=pk)
+
+ header = _(u'Statuses')
+ object_list = Status.objects.all()
+ form = StatusForm(instance=status)
+ title = Status._meta.verbose_name_plural
+
+ if request.method == 'POST':
+ form = StatusForm(request.POST, instance=status)
+ if form.is_valid():
+ status = form.save()
+ messages.success(request, _(u'%s saved') % status.title)
+ return redirect(edit_status, status.pk)
+
+ return render(request, 'admin/statuses/form.html', locals())
+
+
+@staff_member_required
+def remove_status(request, pk):
+ status = Status.objects.get(pk=pk)
+ action = request.path
+
+ if request.method == 'POST':
+ status.delete()
+ messages.success(request, _(u'%s deleted') % status.title)
+ return redirect(statuses)
+
+ return render(request, 'admin/statuses/remove.html', locals())
+
+
+@staff_member_required
+def fields(request, type='customer'):
+ data = prep_list_view(Property)
+ data['type'] = type
+ data['types'] = Property.TYPES
+ data['object_list'] = Property.objects.filter(type=type)
+
+ if data['object_list'].count() > 0:
+ field = data['object_list'][0]
+ return redirect(edit_field, field.type, field.pk)
+
+ return render(request, 'admin/fields/index.html', data)
+
+
+@staff_member_required
+def edit_field(request, type, pk=None):
+ if pk is None:
+ field = Property(type=type)
+ else:
+ field = Property.objects.get(pk=pk)
+
+ FieldForm = modelform_factory(Property, exclude=[])
+
+ types = Property.TYPES
+ title = Property._meta.verbose_name_plural
+ object_list = Property.objects.filter(type=type)
+ form = FieldForm(instance=field)
+
+ if request.method == 'POST':
+ form = FieldForm(request.POST, instance=field)
+
+ if form.is_valid():
+ field = form.save()
+ messages.success(request, _(u'Field saved'))
+ return redirect(field.get_admin_url())
+
+ return render(request, 'admin/fields/form.html', locals())
+
+
+@staff_member_required
+def delete_field(request, pk=None):
+ field = Property.objects.get(pk=pk)
+
+ if request.method == 'POST':
+ field.delete()
+ messages.success(request, _(u'Field deleted'))
+ return redirect(fields, type=field.type)
+
+ data = {'title': _('Really delete this field?')}
+ data['action'] = request.path
+
+ return render(request, 'generic/delete.html', data)
+
+
+@staff_member_required
+def list_templates(request):
+ object_list = Template.objects.all()
+ title = Template._meta.verbose_name_plural
+ if object_list.count() > 0:
+ return redirect(object_list[0].get_admin_url())
+ return render(request, "admin/templates/list_templates.html", locals())
+
+
+@staff_member_required
+def edit_template(request, pk=None):
+
+ if pk is None:
+ template = Template()
+ else:
+ template = Template.objects.get(pk=pk)
+
+ form = TemplateForm(instance=template)
+
+ if request.method == 'POST':
+ form = TemplateForm(request.POST, instance=template)
+
+ if form.is_valid():
+ template = form.save()
+ messages.success(request, _(u'Template %s saved') % template.title)
+ # generic view...
+ return redirect(template.get_admin_url())
+
+ form = form
+ object_list = Template.objects.all()
+ title = Template._meta.verbose_name_plural
+ return render(request, 'admin/templates/form.html', locals())
+
+
+@staff_member_required
+def delete_template(request, pk):
+ template = Template.objects.get(pk=pk)
+
+ if request.method == 'POST':
+ template.delete()
+ messages.success(request, _(u'Template %s deleted') % template.title)
+ return redirect(list_templates)
+
+ title = _('Really delete this template?')
+ action = str(request.path)
+ return render(request, 'generic/delete.html', locals())
+
+
+@staff_member_required
+def list_users(request):
+ object_list = User.objects.filter(is_visible=True)
+ title = User._meta.verbose_name_plural
+ locations = Location.objects.all()
+
+ if object_list.count() > 0:
+ return redirect(object_list[0].get_admin_url())
+
+ return render(request, 'admin/users/index.html', locals())
+
+
+@staff_member_required
+def list_groups(request):
+ object_list = Group.objects.all()
+ title = _('Users & Groups')
+
+ return render(request, 'admin/users/groups.html', locals())
+
+
+@staff_member_required
+def edit_group(request, pk=None):
+ title = _(u'Edit Group')
+ object_list = Group.objects.all()
+
+ if pk is None:
+ group = Group()
+ form = GroupForm(instance=group)
+ else:
+ group = Group.objects.get(pk=pk)
+ title = group.name
+ form = GroupForm(instance=group)
+
+ if request.method == 'POST':
+ form = GroupForm(request.POST, instance=group)
+ if form.is_valid():
+ form.save()
+ messages.success(request, _(u'Group saved'))
+ return redirect(list_groups)
+
+ return render(request, 'admin/users/group_form.html', locals())
+
+
+@staff_member_required
+def delete_group(request, pk):
+ group = Group.objects.get(pk=pk)
+
+ if request.method == "POST":
+ group.delete()
+ messages.success(request, _("Group deleted"))
+ return redirect(list_groups)
+
+ data = {'action': request.path}
+
+ return render(request, "admin/users/delete_group.html", data)
+
+
+@staff_member_required
+def delete_user(request, user_id):
+ user = User.objects.get(pk=user_id)
+
+ if request.method == "POST":
+ try:
+ user.delete()
+ messages.success(request, _("User deleted"))
+ except Exception, e:
+ messages.error(request, e)
+
+ return redirect(list_users)
+
+ return render(request, "admin/users/remove.html", locals())
+
+
+@staff_member_required
+def delete_user_token(request, user_id):
+ user = User.objects.get(pk=user_id)
+ user.delete_tokens()
+ messages.success(request, _('API tokens deleted'))
+ return redirect(edit_user, user.pk)
+
+
+@staff_member_required
+def create_user_token(request, user_id):
+ user = User.objects.get(pk=user_id)
+ token = user.create_token()
+ messages.success(request, _('API token created'))
+ return redirect(edit_user, user.pk)
+
+
+@staff_member_required
+def edit_user(request, pk=None):
+ if pk is None:
+ user = User(site_id=app_settings.SITE_ID)
+ user.location = request.user.location
+ user.locale = request.user.locale
+ user.region = request.user.region
+ user.timezone = request.user.timezone
+ else:
+ user = User.objects.get(pk=pk)
+
+ form = UserForm(instance=user)
+
+ if request.method == "POST":
+ form = UserForm(request.POST, instance=user)
+ if form.is_valid():
+ user = form.save()
+ User.refresh_nomail()
+ if request.POST.get('password1'):
+ user.set_password(request.POST['password1'])
+ user.save()
+ messages.success(request, _(u"User %s saved") % user.get_name())
+ return redirect(edit_user, user.pk)
+ else:
+ messages.error(request, _("Error in user profile data"))
+
+ object_list = User.objects.filter(is_visible=True)
+
+ if request.GET.get('l'):
+ object_list = object_list.filter(locations__pk=request.GET['l'])
+
+ title = User._meta.verbose_name_plural
+ locations = Location.objects.all()
+
+ if len(object_list) > 0:
+ header = _(u'%d users') % len(object_list)
+
+ return render(request, "admin/users/form.html", locals())
+
+
+@staff_member_required
+def locations(request):
+ object_list = Location.objects.all()
+ title = Location._meta.verbose_name_plural
+
+ if object_list.count() > 0:
+ return redirect(object_list[0].get_admin_url())
+
+ return render(request, 'admin/locations/index.html', locals())
+
+
+@staff_member_required
+def edit_location(request, pk=None):
+ header = _('Locations')
+ object_list = Location.objects.all()
+ title = Location._meta.verbose_name_plural
+
+ if pk is None:
+ location = Location()
+ location.timezone = request.user.timezone
+ else:
+ location = Location.objects.get(pk=pk)
+
+ form = LocationForm(instance=location)
+
+ if request.method == 'POST':
+ form = LocationForm(request.POST, request.FILES, instance=location)
+ if form.is_valid():
+ try:
+ location = form.save()
+ messages.success(request, _(u'Location %s saved') % location.title)
+ return redirect(location.get_admin_url())
+ except Exception:
+ pass # just show the form with the error
+
+ return render(request, 'admin/locations/form.html', locals())
+
+
+@staff_member_required
+def delete_location(request, pk):
+ location = Location.objects.get(pk=pk)
+
+ if request.method == 'POST':
+ try:
+ location.delete()
+ messages.success(request, _(u'%s deleted') % location.title)
+ except Exception, e:
+ messages.error(request, e)
+
+ return redirect(locations)
+
+ title = _(u'Really delete this location?')
+ explanation = _(u'This will not delete the orders at this location')
+ action = request.path
+
+ return render(request, 'generic/delete.html', locals())
+
+
+@staff_member_required
+def queues(request):
+ data = prep_list_view(Queue)
+ if data['object_list'].count() > 0:
+ return redirect(data['object_list'][0].get_admin_url())
+ data['subtitle'] = _('Create, edit and delete service queues')
+ return render(request, 'admin/queues/index.html', data)
+
+
+@staff_member_required
+def edit_queue(request, pk=None):
+
+ StatusFormSet = inlineformset_factory(Queue, QueueStatus, extra=1, exclude=[])
+
+ if pk is None:
+ queue = Queue()
+ locations = request.user.locations.all()
+ form = QueueForm(initial={'locations': locations})
+ else:
+ queue = Queue.objects.get(pk=pk)
+ form = QueueForm(instance=queue, initial={'users': queue.user_set.all()})
+
+ title = _(u'Queues')
+ object_list = Queue.objects.all()
+ formset = StatusFormSet(instance=queue)
+
+ if request.method == 'POST':
+ form = QueueForm(request.POST, request.FILES, instance=queue)
+
+ if form.is_valid():
+ try:
+ queue = form.save()
+ queue.user_set = form.cleaned_data['users']
+ queue.save()
+ except Exception as e:
+ messages.error(request, _('Failed to save queue'))
+ return render(request, 'admin/queues/form.html', locals())
+
+ formset = StatusFormSet(request.POST, instance=queue)
+
+ if formset.is_valid():
+ formset.save()
+ messages.success(request, _(u'%s queue saved') % queue.title)
+ return redirect(queue.get_admin_url())
+ else:
+ messages.error(request, formset.errors)
+ else:
+ messages.error(request, form.errors)
+
+ return render(request, 'admin/queues/form.html', locals())
+
+
+@staff_member_required
+def delete_queue(request, pk=None):
+ queue = Queue.objects.get(pk=pk)
+
+ if request.method == 'POST':
+ try:
+ queue.delete()
+ messages.success(request, _("Queue deleted"))
+ except Queue.ProtectedError:
+ messages.error(request, _("Cannot delete queue"))
+
+ return redirect(queues)
+
+ return render(request, 'admin/queues/remove.html', locals())
+
+
+@staff_member_required
+def notifications(request):
+ data = {'title': _(u'Notifications')}
+ return render(request, 'admin/notifications/index.html', data)
+
+
+@staff_member_required
+def edit_notification(request, nid):
+ return render(request, 'admin/notifications/form.html')
+
+
+def list_sites(request):
+ if not request.user.is_superuser:
+ messages.error(request, _(u"Access denied"))
+ return redirect('/login/')
+
+ data = {'sites': Site.objects.all()}
+ data['title'] = _(u"Manage Sites")
+
+ return render(request, "admin/sites/index.html", data)
+
+
+def edit_site(request, pk=None):
+ if not request.user.is_superuser:
+ messages.add_message(request, messages.ERROR, _(u"Access denied"))
+ return redirect('/login/')
+
+ site = Site()
+ data = {'title': _(u"New Site")}
+
+ if pk is not None:
+ site = Site.objects.get(pk=pk)
+ data['title'] = site.name
+
+ SiteForm = modelform_factory(Site, exclude=[])
+ form = SiteForm(instance=site)
+
+ if request.method == "POST":
+
+ form = SiteForm(request.POST, instance=site)
+
+ if form.is_valid():
+ form.save()
+ messages.add_message(request, messages.SUCCESS, _(u"Site saved"))
+ return redirect(list_sites)
+
+ data['form'] = form
+ data['sites'] = Site.objects.all()
+
+ return render(request, "admin/sites/edit_site.html", data)
+
+
+def upload_users(request):
+ """
+ """
+ action = request.path
+ form = UserUploadForm()
+ title = _('Upload Users')
+
+ if request.method == 'POST':
+ form = UserUploadForm(request.POST, request.FILES)
+ if form.is_valid():
+ try:
+ users = form.save()
+ messages.success(request, _('%d users imported') % len(users))
+ except Exception, e:
+ messages.error(request, e)
+ else:
+ messages.error(request, form.errors)
+
+ return redirect(list_users)
+
+ return render(request, "admin/users/upload_users.html", locals())
+
+
+class Backup(object):
+ @classmethod
+ def all(cls):
+ from glob import glob
+ return [cls(s) for s in glob("backups/*.gz")]
+
+ def __init__(self, path):
+ import os
+ self.path = path
+ self.filename = os.path.basename(path)
+ self.filesize = os.path.getsize(path)
+
+ def get_wrapper(self):
+ from django.core.servers.basehttp import FileWrapper
+ return FileWrapper(file(self.path))
+
+ def get_response(self):
+ from django.http import HttpResponse
+ wrapper = self.get_wrapper()
+ response = HttpResponse(wrapper, content_type='application/force-download')
+ response['Content-Disposition'] = 'attachment; filename=%s' % self.filename
+ response['Content-Length'] = self.filesize
+ return response
+
+def backups(request):
+
+ if request.GET.get('dl'):
+ backup = Backup("backups/%s" % request.GET['dl'])
+ return backup.get_response()
+
+ title = _('Backups')
+ backups = Backup.all()
+ return render(request, "admin/backups.html", locals())
diff --git a/servo/views/api.py b/servo/views/api.py
new file mode 100644
index 0000000..87234f3
--- /dev/null
+++ b/servo/views/api.py
@@ -0,0 +1,401 @@
+# -*- 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 json
+from django.utils import timezone
+from django.http import HttpResponse
+from django.core.exceptions import FieldError
+from django.core.serializers import serialize
+from django.shortcuts import get_object_or_404
+from django.views.generic.detail import DetailView
+
+from rest_framework.response import Response
+
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.authentication import TokenAuthentication, SessionAuthentication
+from rest_framework.decorators import (api_view, authentication_classes, permission_classes)
+
+from servo.api.serializers import *
+
+from servo.models import *
+
+
+def dumps(obj):
+ import datetime
+ data = {}
+ for f in obj.api_fields:
+ value = getattr(obj, f)
+ if type(value) in (datetime.datetime, datetime.date,):
+ value = value.isoformat()
+ data[f] = value
+ return json.dumps(data)
+
+
+class OrderStatusView(DetailView):
+
+ model = Order
+
+ def get(self, *args):
+ args = self.request.GET
+ if not args.get('q'):
+ error = {'error': 'Need parameter for query'}
+ return HttpResponse(json.dumps(error),
+ status=400,
+ content_type='application/json')
+
+ self.code = args.get('q')
+ self.object = get_object_or_404(Order, code=self.code)
+ context = self.get_context_data(object=self.object)
+ return self.render_to_response(context)
+
+ def render_to_response(self, context, **response_kwargs):
+ out = {
+ 'order': self.object.code,
+ 'status': self.object.get_status_name(),
+ 'status_description': self.object.get_status_description(),
+ }
+
+ if Configuration.conf('checkin_timeline'):
+ timeline = []
+ for i in self.object.orderstatus_set.exclude(status=None):
+ status = {'badge': i.get_badge()}
+ status['status'] = i.status.title
+ status['started_at'] = i.started_at.isoformat()
+ status['description'] = i.status.description
+ timeline.append(status)
+
+ out['timeline'] = timeline
+
+ return HttpResponse(json.dumps(out), content_type='application/json')
+
+
+def tags(request):
+ results = Tag.objects.filter(**request.GET.dict())
+ data = results.distinct().values_list("title", flat=True)
+ return HttpResponse(json.dumps(list(data)), content_type='application/json')
+
+
+def statuses(request):
+ from servo.models import Status
+ results = Status.objects.all()
+ data = serialize('json', results)
+ return HttpResponse(data, content_type='application/json')
+
+
+def locations(request):
+ queryset = Location.objects.all()
+ serializer = 'json'
+ if request.META['HTTP_USER_AGENT'].startswith('curl'):
+ serializer = 'yaml'
+ data = serialize(serializer, queryset)
+ return HttpResponse(data)
+
+
+@api_view(['GET'])
+@authentication_classes((SessionAuthentication, TokenAuthentication,))
+@permission_classes((IsAuthenticated,))
+def users(request):
+ query = request.GET.dict()
+ queryset = User.active.filter(**query)
+ data = list(queryset.values_list("full_name", flat=True))
+ return HttpResponse(json.dumps(data), content_type='application/json')
+
+
+def places(request):
+ places = Order.objects.exclude(place=None)
+ places = places.order_by("place").distinct("place").values_list('place', flat=True)
+ return HttpResponse(json.dumps(list(places)), content_type='application/json')
+
+
+def queues(request):
+ queryset = Queue.objects.all()
+ data = serialize('json', queryset, fields=('pk', 'title'))
+ return HttpResponse(data, content_type='application/json')
+
+
+def json_response(data):
+ return HttpResponse(json.dumps(data), content_type='application/json')
+
+
+def ok(message):
+ msg = json.dumps(dict(ok=message))
+ return HttpResponse(msg, content_type='application/json')
+
+
+def error(message):
+ msg = json.dumps(dict(error=str(message)))
+ return HttpResponse(msg, content_type='application/json')
+
+
+def client_error(message):
+ msg = json.dumps(dict(error=str(message)))
+ return HttpResponse(msg, content_type='application/json', status=400)
+
+
+def create_order(request):
+ try:
+ data = json.loads(request.body)
+ except ValueError as e:
+ return client_error('Malformed request: %s' % e)
+
+ cdata = data.get('customer')
+ problem = data.get('problem')
+
+ if not cdata:
+ return client_error('Cannot create order without customer info')
+
+ if not problem:
+ return client_error('Cannot create order without problem description')
+
+ try:
+ customer, created = Customer.objects.get_or_create(
+ name=cdata['name'],
+ email=cdata['email']
+ )
+ except Exception as e:
+ return client_error('Invalid customer details: %s' % e)
+
+ if request.user.customer:
+ customer.parent = request.user.customer
+
+ if cdata.get('city'):
+ customer.city = cdata.get('city')
+
+ if cdata.get('phone'):
+ customer.phone = cdata.get('phone')
+
+ if cdata.get('zip_code'):
+ customer.zip_code = cdata.get('zip_code')
+
+ if cdata.get('street_address'):
+ customer.street_address = cdata.get('street_address')
+
+ customer.save()
+
+ order = Order(created_by=request.user, customer=customer)
+ order.save()
+
+ note = Note(created_by=request.user, body=problem, is_reported=True)
+ note.order = order
+ note.save()
+
+ if data.get('attachment'):
+ import base64
+ from servo.models import Attachment
+ from django.core.files.base import ContentFile
+
+ attachment = data.get('attachment')
+
+ try:
+ filename = attachment.get('name')
+ content = base64.b64decode(attachment.get('data'))
+ except Exception as e:
+ return client_error('Invalid file data: %s' %e)
+
+ content = ContentFile(content, filename)
+ attachment = Attachment(content=content, content_object=note)
+ attachment.save()
+ attachment.content.save(filename, content)
+ note.attachments.add(attachment)
+
+ if data.get('device'):
+
+ try:
+ GsxAccount.default(request.user)
+ except Exception as e:
+ pass
+
+ ddata = data.get('device')
+
+ try:
+ device = order.add_device_sn(ddata.get('sn'), request.user)
+ except Exception as e:
+ device = Device(sn=ddata.get('sn', ''))
+ device.description = ddata.get('description', '')
+ device.save()
+ order.add_device(device)
+
+ for a in ddata.get('accessories', []):
+ a = Accessory(name=a, order=order, device=device)
+ a.save()
+
+ return ok(order.code)
+
+
+@api_view(['GET', 'POST', 'PUT'])
+@authentication_classes((TokenAuthentication,))
+@permission_classes((IsAuthenticated,))
+def orders(request, code=None, pk=None):
+ """
+ This is the orders API
+ """
+ from servo.api.serializers import OrderSerializer
+
+ if request.method == 'POST':
+ return create_order(request)
+
+ if request.method == 'PUT':
+ return error('Method not yet implemented')
+
+ if request.GET.get('q'):
+ results = Order.objects.filter(**request.GET)
+
+ if pk:
+ order = Order.objects.get(pk=pk)
+ serializer = OrderSerializer(order, context={'request': request})
+ return Response(serializer.data)
+
+ if code:
+ order = Order.objects.get(code=code)
+ if order.status:
+ order.status_description = order.status.status.description
+ serializer = OrderSerializer(order, context={'request': request})
+ return Response(serializer.data)
+
+ orders = Order.objects.none()
+ serializer = OrderSerializer(orders, many=True, context={'request': request})
+ return Response(serializer.data)
+
+
+def messages(request):
+ """
+ Responds to SMS status updates
+ """
+ from servo.messaging.sms import SMSJazzProvider, HQSMSProvider
+
+ if not request.GET.get('id'):
+ return HttpResponse('Thanks, but no thanks')
+
+ m = get_object_or_404(Message, code=request.GET['id'])
+ gw = Configuration.conf('sms_gateway')
+ statusmap = HQSMSProvider.STATUSES
+
+ if gw == 'jazz':
+ statusmap = SMSJazzProvider.STATUSES
+
+ status = statusmap[request.GET['status']]
+ m.status = status[0]
+ m.error = status[1]
+
+ if m.status == 'DELIVERED':
+ m.received_at = timezone.now()
+
+ if m.status == 'FAILED':
+ if m.note.order:
+ uid = Configuration.conf('imap_act')
+ if uid:
+ user = User.objects.get(pk=uid)
+ m.note.order.notify('sms_failed', m.error, user)
+
+ m.save()
+
+ return HttpResponse('OK')
+
+
+def device_models(request):
+ data = Device.objects.order_by("description").distinct("description")
+ return json_response(list(data.values_list("description", flat=True)))
+
+
+@api_view(['GET'])
+@authentication_classes((TokenAuthentication,))
+@permission_classes((IsAuthenticated,))
+def warranty(request):
+ from servo.api.serializers import DeviceSerializer
+ sn = request.GET.get('sn')
+
+ if not sn:
+ return error('Need query parameter for warranty lookup')
+
+ try:
+ GsxAccount.default(request.user)
+ except Exception as e:
+ return error('Cannot connect to GSX (check user name and password)')
+
+ try:
+ result = Device.from_gsx(sn, cached=False)
+ serializer = DeviceSerializer(result, context={'request': request})
+ return Response(serializer.data)
+ except Exception as e:
+ return error(e)
+
+
+@api_view(['GET'])
+def order_status(request):
+ from servo.api.serializers import OrderStatusSerializer
+ code = request.GET.get('q')
+ try:
+ result = Order.objects.get(code=code)
+ #serializer = OrderStatusSerializer(result)
+ return Response(serializer.data)
+ except Exception as e:
+ return (error(e))
+
+
+@api_view(['GET'])
+@authentication_classes((TokenAuthentication,))
+@permission_classes((IsAuthenticated,))
+def notes(request, pk=None):
+ if pk:
+ note = Note.objects.get(pk=pk)
+ serializer = NoteSerializer(note, context={'request': request})
+ return Response(serializer.data)
+
+
+@api_view(['GET'])
+@authentication_classes((TokenAuthentication,))
+@permission_classes((IsAuthenticated,))
+def order_items(request, pk):
+ item = ServiceOrderItem.objects.get(pk=pk)
+ serializer = ServiceOrderItemSerializer(item, context={'request': request})
+ return Response(serializer.data)
+
+
+@api_view(['GET'])
+@authentication_classes((TokenAuthentication,))
+@permission_classes((IsAuthenticated,))
+def user_detail(request, pk):
+ user = User.objects.get(pk=pk)
+ serializer = UserSerializer(user, context={'request': request})
+ return Response(serializer.data)
+
+
+@api_view(['GET'])
+@authentication_classes((TokenAuthentication,))
+@permission_classes((IsAuthenticated,))
+def customers(request, pk=None):
+ customer = Customer.objects.get(pk=pk)
+ serializer = CustomerSerializer(customer, context={'request': request})
+ return Response(serializer.data)
+
+
+@api_view(['GET'])
+@authentication_classes((TokenAuthentication,))
+@permission_classes((IsAuthenticated,))
+def devices(request, pk=None):
+ device = Device.objects.get(pk=pk)
+ serializer = DeviceSerializer(device, context={'request': request})
+ return Response(serializer.data)
diff --git a/servo/views/checkin.py b/servo/views/checkin.py
new file mode 100644
index 0000000..7b6787a
--- /dev/null
+++ b/servo/views/checkin.py
@@ -0,0 +1,418 @@
+# -*- 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 json
+import locale
+
+from gsxws import products, GsxError
+
+from django.conf import settings
+from django.http import HttpResponse
+from django.contrib import messages
+from django.core.cache import cache
+
+from django.utils import translation
+from django.core.exceptions import ValidationError
+from django.utils.translation import ugettext as _
+from django.shortcuts import render, redirect, get_object_or_404
+
+from servo.views.order import put_on_paper
+from servo.validators import apple_sn_validator
+from servo.models import (User, Device, GsxAccount, Order,
+ Customer, Location, Note, Attachment,
+ Configuration, ChecklistItem, Tag,)
+from servo.forms import (SerialNumberForm, AppleSerialNumberForm,
+ DeviceForm, IssueForm, CustomerForm,
+ QuestionForm, AttachmentForm, StatusCheckForm,)
+
+
+def init_locale(request):
+ lc = settings.INSTALL_LOCALE.split('.')
+ locale.setlocale(locale.LC_TIME, lc)
+ locale.setlocale(locale.LC_NUMERIC, lc)
+ locale.setlocale(locale.LC_MESSAGES, lc)
+ locale.setlocale(locale.LC_MONETARY, lc)
+
+ translation.activate(settings.INSTALL_LANGUAGE)
+ request.session[translation.LANGUAGE_SESSION_KEY] = settings.INSTALL_LANGUAGE
+
+
+def set_cache_device(device):
+ key = 'checkin-device-%s' % device.sn
+ cache.set(key, device)
+
+
+def get_gsx_connection(request):
+ act = GsxAccount.get_default_account()
+ user = User.objects.get(pk=request.session['checkin_user'])
+ location = Location.objects.get(pk=request.session['checkin_location'])
+ return act.connect(user, location)
+
+
+def get_remote_device(request, sn):
+ try:
+ apple_sn_validator(sn)
+ except ValidationError:
+ return Device(sn=sn, image_url='https://static.servoapp.com/images/na.gif')
+
+ get_gsx_connection(request)
+
+ return Device.from_gsx(sn)
+
+
+def get_local_device(request, sn):
+ try:
+ device = Device.objects.filter(sn=sn)[0]
+ except IndexError:
+ device = get_remote_device(request, sn)
+
+ return device
+
+
+def get_device(request, sn):
+ if len(sn) < 1:
+ return Device(sn=sn)
+
+ key = 'checkin-device-%s' % sn
+ device = cache.get(key, get_local_device(request, sn))
+ set_cache_device(device)
+ return device
+
+
+def reset_session(request):
+
+ # initialize some basic vars
+ if not request.user.is_authenticated():
+ request.session.flush()
+
+ # initialize locale
+ init_locale(request)
+
+ request.session['checkin_device'] = None
+ request.session['checkin_customer'] = None
+
+ if not request.session.get('company_name'):
+ request.session['company_name'] = Configuration.conf('company_name')
+
+ if request.user.is_authenticated():
+
+ if request.GET.get('u'):
+ user = User.objects.get(pk=request.GET['u'])
+ else:
+ user = request.user
+
+ if request.GET.get('l'):
+ location = Location.objects.get(pk=request.GET['l'])
+ else:
+ location = user.location
+
+ checkin_users = User.get_checkin_group()
+ request.session['checkin_users'] = User.get_checkin_group_list()
+ request.session['checkin_locations'] = request.user.get_location_list()
+
+ queryset = checkin_users.filter(location=location)
+ request.session['checkin_users'] = User.serialize(queryset)
+
+ else:
+ user = User.get_checkin_user()
+ location = user.location
+
+ request.session['checkin_user'] = user.pk
+ request.session['checkin_location'] = location.pk
+ request.session['checkin_user_name'] = user.get_name()
+ request.session['checkin_location_name'] = location.title
+
+
+def reset(request):
+ reset_session(request)
+ return redirect(index)
+
+
+def thanks(request, order):
+ """
+ Final step/confirmation
+ """
+ title = _('Done!')
+
+ try:
+ request.session.delete_test_cookie()
+ except KeyError:
+ pass # ignore spurious KeyError at /checkin/thanks/RJTPS/
+
+ try:
+ order = Order.objects.get(url_code=order)
+ except Order.DoesNotExist:
+ messages.error(request, _('Order does not exist'))
+ return redirect(reset)
+
+ return render(request, "checkin/thanks.html", locals())
+
+
+def get_customer(request):
+ if not request.user.is_authenticated():
+ return
+
+ if not request.GET.get('c'):
+ return
+
+ customer = Customer.objects.get(pk=request.GET['c'])
+ request.session['checkin_customer'] = customer.pk
+
+ fdata = {'fname': customer.firstname}
+ fdata['lname'] = customer.lastname
+ fdata['email'] = customer.email
+ fdata['city'] = customer.city
+ fdata['phone'] = customer.phone
+ fdata['country'] = customer.country
+ fdata['address'] = customer.street_address
+ fdata['postal_code'] = customer.zip_code
+
+ return HttpResponse(json.dumps(fdata), content_type='application/json')
+
+
+def status(request):
+ """
+ Status checking through the checkin
+ """
+ title = _('Repair Status')
+
+ if request.GET.get('code'):
+ timeline = []
+ form = StatusCheckForm(request.GET)
+ if form.is_valid():
+ code = form.cleaned_data['code']
+ try:
+ order = Order.objects.get(code=code)
+ if Configuration.conf('checkin_timeline'):
+ timeline = order.orderstatus_set.all()
+ if order.status is None:
+ order.status_name = _(u'Waiting to be processed')
+ except Order.DoesNotExist:
+ messages.error(request, _(u'Order %s not found') % code)
+ return render(request, "checkin/status-show.html", locals())
+ else:
+ form = StatusCheckForm()
+
+ return render(request, "checkin/status.html", locals())
+
+
+def print_confirmation(request, code):
+ order = Order.objects.get(url_code=code)
+ return put_on_paper(request, order.pk)
+
+
+def terms(request):
+ conf = Configuration.conf()
+ return render(request, 'checkin/terms.html', locals())
+
+
+def index(request):
+
+ if request.method == 'GET':
+ reset_session(request)
+
+ title = _('Service Order Check-In')
+
+ dcat = request.GET.get('d', 'mac')
+ dmap = {
+ 'mac' : _('Mac'),
+ 'iphone' : _('iPhone'),
+ 'ipad' : _('iPad'),
+ 'ipod' : _('iPod'),
+ 'acc' : _('Apple Accessory'),
+ 'beats' : _('Beats Products'),
+ 'other' : _('Other Devices'),
+ }
+
+ issue_form = IssueForm()
+ device = Device(description=dmap[dcat])
+
+ if dcat in ('mac', 'iphone', 'ipad', 'ipod'):
+ sn_form = AppleSerialNumberForm()
+ else:
+ sn_form = SerialNumberForm()
+
+ tags = Tag.objects.filter(type="order")
+ device_form = DeviceForm(instance=device)
+ customer_form = CustomerForm(request)
+
+ if request.method == 'POST':
+
+ sn_form = SerialNumberForm(request.POST)
+ issue_form = IssueForm(request.POST, request.FILES)
+ customer_form = CustomerForm(request, request.POST)
+ device_form = DeviceForm(request.POST, request.FILES)
+
+ if device_form.is_valid() and issue_form.is_valid() and customer_form.is_valid():
+
+ user = User.objects.get(pk=request.session['checkin_user'])
+
+ idata = issue_form.cleaned_data
+ ddata = device_form.cleaned_data
+ cdata = customer_form.cleaned_data
+
+ customer_id = request.session.get('checkin_customer')
+ if customer_id:
+ customer = Customer.objects.get(pk=customer_id)
+ else:
+ customer = Customer()
+
+ name = u'{0} {1}'.format(cdata['fname'], cdata['lname'])
+
+ if len(cdata['company']):
+ name += ', ' + cdata['company']
+
+ customer.name = name
+ customer.city = cdata['city']
+ customer.phone = cdata['phone']
+ customer.email = cdata['email']
+ customer.phone = cdata['phone']
+ customer.zip_code = cdata['postal_code']
+ customer.street_address = cdata['address']
+ customer.save()
+
+ order = Order(customer=customer, created_by=user)
+ order.location_id = request.session['checkin_location']
+ order.checkin_location = cdata['checkin_location']
+ order.checkout_location = cdata['checkout_location']
+
+ order.save()
+ order.check_in(user)
+
+ try:
+ device = get_device(request, ddata['sn'])
+ except GsxError as e:
+ pass
+
+ device.username = ddata['username']
+ device.password = ddata['password']
+ device.description = ddata['description']
+ device.purchased_on = ddata['purchased_on']
+ device.purchase_country = ddata['purchase_country']
+ device.save()
+
+ order.add_device(device, user)
+
+ note = Note(created_by=user, body=idata['issue_description'])
+ note.is_reported = True
+ note.order = order
+ note.save()
+
+ # Proof of purchase was supplied
+ if ddata.get('pop'):
+ f = {'content_type': Attachment.get_content_type('note').pk}
+ f['object_id'] = note.pk
+ a = AttachmentForm(f, {'content': ddata['pop']})
+ a.save()
+
+ if request.POST.get('tags'):
+ order.set_tags(request.POST.getlist('tags'), request.user)
+
+ # Check checklists early for validation
+ answers = []
+
+ # @FIXME: should try to move this to a formset...
+ for k, v in request.POST.items():
+ if k.startswith('__cl__'):
+ answers.append('- **' + k[6:] + '**: ' + v)
+
+ if len(answers) > 0:
+ note = Note(created_by=user, body="\r\n".join(answers))
+
+ if Configuration.true('checkin_report_checklist'):
+ note.is_reported = True
+
+ note.order = order
+ note.save()
+
+ # mark down internal notes (only if logged in)
+ if len(idata.get('notes')):
+ note = Note(created_by=user, body=idata['notes'])
+ note.is_reported = False
+ note.order = order
+ note.save()
+
+ # mark down condition of device
+ if len(ddata.get('condition')):
+ note = Note(created_by=user, body=ddata['condition'])
+ note.is_reported = True
+ note.order = order
+ note.save()
+
+ # mark down supplied accessories
+ if len(ddata.get('accessories')):
+ accs = ddata['accessories'].strip().split("\n")
+ order.set_accessories(accs, device)
+
+ redirect_to = thanks
+
+ """
+ if request.user.is_authenticated():
+ if request.user.autoprint:
+ redirect_to = print_confirmation
+ """
+ return redirect(redirect_to, order.url_code)
+
+ try:
+ pk = Configuration.conf('checkin_checklist')
+ questions = ChecklistItem.objects.filter(checklist_id=pk)
+ except ValueError:
+ # Checklists probably not configured
+ pass
+
+ if request.GET.get('phone'):
+
+ if not request.user.is_authenticated():
+ return
+
+ results = []
+
+ for c in Customer.objects.filter(phone=request.GET['phone']):
+ title = '%s - %s' % (c.phone, c.name)
+ results.append({'id': c.pk, 'name': c.name, 'title': title})
+
+ return HttpResponse(json.dumps(results), content_type='application/json')
+
+ if request.GET.get('sn'):
+
+ device = Device(sn=request.GET['sn'])
+ device.description = _('Other Device')
+ device_form = DeviceForm(instance=device)
+
+ try:
+ apple_sn_validator(device.sn)
+ except Exception as e: # not an Apple serial number
+ return render(request, "checkin/device_form.html", locals())
+
+ try:
+ device = get_device(request, device.sn)
+ device_form = DeviceForm(instance=device)
+ except GsxError as e:
+ error = e
+
+ return render(request, "checkin/device_form.html", locals())
+
+ return render(request, "checkin/newindex.html", locals())
diff --git a/servo/views/customer.py b/servo/views/customer.py
new file mode 100644
index 0000000..455126e
--- /dev/null
+++ b/servo/views/customer.py
@@ -0,0 +1,505 @@
+# -*- 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 __future__ import absolute_import
+
+from django.db.models import Q
+from django.contrib import messages
+from django.http import HttpResponse
+
+from django.forms.models import modelform_factory
+from django.utils.translation import ugettext as _
+from django.contrib.auth.decorators import permission_required
+from django.shortcuts import render, redirect, get_object_or_404
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+
+from servo.models.note import Note
+from servo.models.order import Order
+from servo.models.common import Property
+from servo.models.customer import Customer, CustomerGroup, ContactInfo
+
+from servo.forms.customer import (CustomerForm,
+ CustomerSearchForm,
+ CustomerUploadForm)
+
+GroupForm = modelform_factory(CustomerGroup, exclude=[])
+
+
+def prepare_view(request, group='all'):
+
+ title = _("Customers")
+
+ customer_list = []
+ all_customers = Customer.objects.all().order_by('name')
+ customer_count = all_customers.count()
+
+ if request.session.get("return_to"):
+ del(request.session['return_to'])
+
+ if request.method == 'POST':
+ q = request.POST.get('query')
+ if q is not None:
+ try:
+ (key, value) = q.split('=')
+ # allow searching customers by arbitrary key/values
+ customer_list = Customer.objects.filter(**{key: value.strip()})
+ except Exception:
+ customer_list = Customer.objects.filter(name__icontains=q)
+ else:
+ if group == 'all':
+ customer_list = all_customers
+ else:
+ g = CustomerGroup.objects.get(slug=group)
+ customer_list = all_customers.filter(groups=g)
+ title = g.name
+
+ page = request.GET.get('page')
+ paginator = Paginator(customer_list, 40)
+
+ try:
+ customers = paginator.page(page)
+ except PageNotAnInteger:
+ customers = paginator.page(1)
+ except EmptyPage:
+ customers = paginator.page(paginator.num_pages)
+
+ groups = CustomerGroup.objects.all()
+
+ return locals()
+
+
+def index(request, group='all'):
+ data = prepare_view(request, group)
+ request.session['customer_query'] = None
+
+ if data['customer_list']:
+ customer = data['customer_list'][0]
+ return redirect(view, pk=customer.pk, group=group)
+
+ return render(request, "customers/index.html", data)
+
+
+@permission_required("servo.change_order")
+def add_order(request, customer_id, order_id):
+ order = Order.objects.get(pk=order_id)
+ customer = Customer.objects.get(pk=customer_id)
+ order.customer = customer
+ order.save()
+
+ for d in order.devices.all():
+ customer.devices.add(d)
+
+ customer.save()
+ messages.success(request, _('Customer added'))
+ return redirect(order)
+
+
+def notes(request, pk, note_id=None):
+ from servo.forms.note import NoteForm
+ customer = Customer.objects.get(pk=pk)
+ form = NoteForm(initial={'recipient': customer.name})
+
+ return render(request, "notes/form.html", {'form': form})
+
+
+def view(request, pk, group='all'):
+ try:
+ c = Customer.objects.get(pk=pk)
+ except Customer.DoesNotExist:
+ messages.error(request, _('Customer not found'))
+ return redirect(index)
+
+ data = prepare_view(request, group)
+
+ data['title'] = c.name
+ data['orders'] = Order.objects.filter(
+ customer__lft__gte=c.lft,
+ customer__rght__lte=c.rght,
+ customer__tree_id=c.tree_id
+ )
+
+ if c.email:
+ data['notes'] = Note.objects.filter(recipient=c.email)
+
+ data['customer'] = c
+ request.session['return_to'] = request.path
+
+ return render(request, 'customers/view.html', data)
+
+
+@permission_required("servo.change_customer")
+def edit_group(request, group='all'):
+ if group == 'all':
+ group = CustomerGroup()
+ else:
+ group = CustomerGroup.objects.get(slug=group)
+
+ title = group.name
+ form = GroupForm(instance=group)
+
+ if request.method == "POST":
+ form = GroupForm(request.POST, instance=group)
+ if form.is_valid():
+ group = form.save()
+ messages.success(request, _(u'%s saved') % group.name)
+ return redirect(index, group.slug)
+ messages.error(request, form.errors['name'][0])
+ return redirect(index)
+
+ return render(request, "customers/edit_group.html", locals())
+
+
+@permission_required("servo.change_customer")
+def delete_group(request, group):
+ group = CustomerGroup.objects.get(slug=group)
+
+ if request.method == "POST":
+ group.delete()
+ messages.success(request, _(u'%s deleted') % group.name)
+ return redirect(index)
+
+ return render(request, "customers/delete_group.html", locals())
+
+
+@permission_required("servo.change_customer")
+def edit(request, pk=None, parent_id=None, group='all'):
+
+ data = prepare_view(request, group)
+
+ customer = Customer()
+ form = CustomerForm(instance=customer)
+
+ if group != 'all':
+ g = CustomerGroup.objects.get(slug=group)
+ form.initial = {'groups': [g]}
+
+ name = request.GET.get('name')
+
+ if name:
+ form = CustomerForm(initial={'name': name})
+
+ if pk is not None:
+ customer = Customer.objects.get(pk=pk)
+ form = CustomerForm(instance=customer)
+
+ if parent_id is not None:
+ customer.parent = Customer.objects.get(pk=parent_id)
+ form = CustomerForm(initial={'parent': parent_id})
+
+ if request.method == 'POST':
+ props = dict()
+ keys = request.POST.getlist('keys')
+ values = request.POST.getlist('values')
+
+ form = CustomerForm(request.POST, request.FILES, instance=customer)
+
+ if form.is_valid():
+ ContactInfo.objects.filter(customer=customer).delete()
+
+ for k, v in enumerate(values):
+ if v != '':
+ key = keys[k]
+ props[key] = v
+
+ if form.is_valid():
+ try:
+ customer = form.save()
+ except Exception as e:
+ messages.error(request, e)
+ return redirect(edit, group, pk)
+
+ for k, v in props.items():
+ if v != '':
+ ContactInfo.objects.create(key=k, value=v, customer=customer)
+
+ messages.success(request, _('Customer saved'))
+
+ if request.session.get('return_to'):
+ return_to = request.session['return_to']
+ if hasattr(return_to, 'set_customer'):
+ return_to.set_customer(customer)
+ del request.session['return_to']
+ return redirect(return_to)
+
+ return redirect(view, pk=customer.pk, group=group)
+
+ data['form'] = form
+ data['customer'] = customer
+ data['title'] = customer.name
+ data['fields'] = Property.objects.filter(type='customer')
+
+ return render(request, 'customers/form.html', data)
+
+
+@permission_required("servo.delete_customer")
+def delete(request, pk=None, group='all'):
+
+ customer = Customer.objects.get(pk=pk)
+
+ if request.method == "POST":
+ customer.delete()
+ messages.success(request, _("Customer deleted"))
+ return redirect(index, group=group)
+ else:
+ data = {'action': request.path, 'customer': customer}
+ return render(request, "customers/remove.html", data)
+
+
+@permission_required("servo.change_customer")
+def merge(request, pk, target=None):
+ """
+ Merges customer PK with customer TARGET
+ Re-links everything from customer PK to TARGET:
+ - orders
+ - devices
+ - invoices
+ Deletes the source customer
+ """
+ customer = Customer.objects.get(pk=pk)
+ title = _('Merge %s with') % customer.name
+
+ if request.method == 'POST':
+ name = request.POST.get('name')
+ results = Customer.objects.filter(name__icontains=name)
+ return render(request, 'customers/results-merge.html', locals())
+
+ if pk and target:
+ target_customer = Customer.objects.get(pk=target)
+ target_customer.orders.add(*customer.orders.all())
+ target_customer.devices.add(*customer.devices.all())
+ target_customer.note_set.add(*customer.note_set.all())
+ target_customer.invoice_set.add(*customer.invoice_set.all())
+ target_customer.save()
+ customer.delete()
+ messages.success(request, _('Customer records merged succesfully'))
+ return redirect(target_customer)
+
+ return render(request, "customers/merge.html", locals())
+
+
+@permission_required("servo.change_customer")
+def move(request, pk, new_parent=None):
+ """
+ Moves a customer under another customer
+ """
+ customer = Customer.objects.get(pk=pk)
+
+ if new_parent is not None:
+ if int(new_parent) == 0:
+ new_parent = None
+ msg = _(u"Customer %s moved to top level") % customer
+ else:
+ new_parent = Customer.objects.get(pk=new_parent)
+ d = {'customer': customer, 'target': new_parent}
+ msg = _(u"Customer %(customer)s moved to %(target)s") % d
+
+ try:
+ customer.move_to(new_parent)
+ customer.save() # To update fullname
+ messages.success(request, msg)
+ except Exception, e:
+ messages.error(request, e)
+
+ return redirect(customer)
+
+ return render(request, "customers/move.html", locals())
+
+
+def search(request):
+ """
+ Searches for customers from "spotlight"
+ """
+ query = request.GET.get("q")
+ kind = request.GET.get('kind')
+ request.session['search_query'] = query
+
+ customers = Customer.objects.filter(
+ Q(fullname__icontains=query) | Q(email__icontains=query) | Q(phone__contains=query)
+ )
+
+ if kind == 'company':
+ customers = customers.filter(is_company=True)
+
+ if kind == 'contact':
+ customers = customers.filter(is_company=False)
+
+ title = _('Search results for "%s"') % query
+ return render(request, "customers/search.html", locals())
+
+
+def filter(request):
+ """
+ Search for customers by name
+ May return JSON for ajax requests
+ or a rendered list
+ """
+ import json
+ from django.http import HttpResponse
+
+ if request.method == "GET":
+ results = list()
+ query = request.GET.get("query")
+ customers = Customer.objects.filter(fullname__icontains=query)
+
+ for c in customers:
+ results.append(u"%s <%s>" % (c.name, c.email))
+ results.append(u"%s <%s>" % (c.name, c.phone))
+ else:
+ query = request.POST.get("name")
+ results = Customer.objects.filter(fullname__icontains=query)
+ data = {'results': results, 'id': request.POST['id']}
+
+ return render(request, "customers/search-results.html", data)
+
+ return HttpResponse(json.dumps(results), content_type="application/json")
+
+
+def find(request):
+ """
+ Search from customer advanced search
+ """
+ results = list()
+ request.session['customer_list'] = list()
+
+ if request.method == 'POST':
+ form = CustomerSearchForm(request.POST)
+
+ if form.is_valid():
+ d = form.cleaned_data
+ checkin_start = d.pop('checked_in_start')
+ checkin_end = d.pop('checked_in_end')
+
+ if checkin_start and checkin_end:
+ d['orders__created_at__range'] = [checkin_start.isoformat(),
+ checkin_end.isoformat()]
+
+ results = Customer.objects.filter(**d).distinct()
+ request.session['customer_query'] = d
+ else:
+ form = CustomerSearchForm()
+
+ title = _('Search for customers')
+
+ page = request.GET.get('page')
+ paginator = Paginator(results, 50)
+
+ try:
+ customers = paginator.page(page)
+ except PageNotAnInteger:
+ customers = paginator.page(1)
+ except EmptyPage:
+ customers = paginator.page(paginator.num_pages)
+
+ return render(request, "customers/find.html", locals())
+
+
+def download(request, format='csv', group='all'):
+ """
+ Downloads all customers or search results
+ """
+ filename = 'customers'
+ results = Customer.objects.all()
+ query = request.session.get('customer_query')
+
+ response = HttpResponse(content_type="text/plain; charset=utf-8")
+ response['Content-Disposition'] = 'attachment; filename="%s.txt"' % filename
+ response.write(u"ID\tNAME\tEMAIL\tPHONE\tADDRESS\tPOSTAL CODE\tCITY\tCOUNTRY\tNOTES\n")
+
+ if group != 'all':
+ results = results.filter(groups__slug=group)
+
+ if query:
+ results = Customer.objects.filter(**query).distinct()
+
+ for c in results:
+ row = u"%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" % (c.pk,
+ c.name,
+ c.email,
+ c.phone,
+ c.street_address,
+ c.zip_code,
+ c.city,
+ c.country,
+ c.notes,)
+ response.write(row)
+
+ return response
+
+
+def create_message(request, pk):
+ return redirect("servo.views.note.edit", customer=pk)
+
+
+def upload(request, group='all'):
+
+ action = request.path
+ form = CustomerUploadForm()
+
+ if request.method == 'POST':
+ form = CustomerUploadForm(request.POST, request.FILES)
+
+ if not form.is_valid():
+ messages.error(request, form.errors)
+ return redirect(index)
+
+ i, df = 0, form.cleaned_data['datafile'].read()
+
+ for l in df.split("\r"):
+ row = force_decode(l).strip().split("\t")
+
+ if len(row) < 5:
+ messages.error(request, _("Invalid upload data"))
+ return redirect(index)
+
+ if form.cleaned_data.get('skip_dups'):
+ if Customer.objects.filter(email=row[1]).exists():
+ continue
+
+ c = Customer(name=row[0], email=row[1])
+ c.street_address = row[2]
+ c.zip_code = row[3]
+ c.city = row[4]
+ c.notes = row[5]
+ c.save()
+
+ if group != 'all':
+ g = CustomerGroup.objects.get(slug=group)
+ c.groups.add(g)
+
+ i += 1
+
+ messages.success(request, _("%d customer(s) imported") % i)
+ return redirect(index, group=group)
+
+ return render(request, "customers/upload.html", locals())
+
+
+def force_decode(s, codecs=['mac_roman', 'utf-8', 'latin-1']):
+ for i in codecs:
+ try:
+ return s.decode(i)
+ except UnicodeDecodeError:
+ pass
diff --git a/servo/views/device.py b/servo/views/device.py
new file mode 100644
index 0000000..f35bd99
--- /dev/null
+++ b/servo/views/device.py
@@ -0,0 +1,605 @@
+# -*- 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 gsxws
+
+from django.db.models import Q
+from django.contrib import messages
+
+from django.core.cache import cache
+from django.shortcuts import render, redirect, get_object_or_404
+
+from django.utils.translation import ugettext as _
+from django.template.defaultfilters import slugify
+from django.views.decorators.cache import cache_page
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+
+from servo.models import Device, Product, GsxAccount, ServiceOrderItem
+from servo.forms.devices import DeviceForm, DeviceUploadForm, DeviceSearchForm
+
+class RepairDiagnosticResults:
+ pass
+
+class DiagnosticResults(object):
+ def __init__(self, diags):
+ if not diags.diagnosticTestData:
+ raise gsxws.GsxError('Missing diagnostic data')
+
+ self.diags = dict(result={}, profile={}, report={})
+
+ for r in diags.diagnosticTestData.testResult.result:
+ self.diags['result'][r.name] = r.value
+
+ for r in diags.diagnosticProfileData.profile.unit.key:
+ self.diags['profile'][r.name] = r.value
+
+ for r in diags.diagnosticProfileData.report.reportData.key:
+ self.diags['report'][r.name] = r.value
+
+ def __iter__(self):
+ return iter(self.diags)
+
+
+def model_from_slug(product_line, model=None):
+ """
+ Returns product description for model slug or models dict for
+ the specified product line
+ """
+ if not cache.get("slugmap"):
+ slugmap = {} # Map model slug to corresponding product description
+ product_lines = gsxws.products.models()
+
+ for k, v in product_lines.items():
+ d = {}
+ for p in v['models']:
+ slug = slugify(p)
+ d[slug] = p
+
+ slugmap[k] = d
+
+ cache.set("slugmap", slugmap)
+
+ models = cache.get("slugmap").get(product_line)
+
+ if model is not None:
+ return models.get(model)
+
+ return models
+
+
+def prep_list_view(request, product_line=None, model=None):
+ title = _('Devices')
+ all_devices = Device.objects.all()
+ product_lines = gsxws.products.models()
+
+ if product_line is None:
+ product_line = product_lines.keys()[0]
+
+ models = model_from_slug(product_line)
+
+ if model is None:
+ model = models.keys()[0]
+ title = product_lines[product_line]['name']
+ else:
+ title = models.get(model)
+
+ if product_line == "OTHER":
+ all_devices = all_devices.filter(product_line=product_line)
+ else:
+ all_devices = all_devices.filter(slug=model)
+
+ page = request.GET.get('page')
+ paginator = Paginator(all_devices, 50)
+
+ try:
+ devices = paginator.page(page)
+ except PageNotAnInteger:
+ devices = paginator.page(1)
+ except EmptyPage:
+ devices = paginator.page(paginator.num_pages)
+
+ return locals()
+
+
+def prep_detail_view(request, pk, product_line=None, model=None):
+ if pk is None:
+ device = Device()
+ else:
+ device = Device.objects.get(pk=pk)
+
+ data = prep_list_view(request, product_line, model)
+
+ data['device'] = device
+ data['title'] = device.description
+
+ return data
+
+
+def index(request, product_line=None, model=None):
+ if request.session.get('return_to'):
+ del(request.session['return_to'])
+
+ data = prep_list_view(request, product_line, model)
+
+ if data['all_devices'].count() > 0:
+ return redirect(data['all_devices'].latest())
+
+ return render(request, "devices/index.html", data)
+
+
+def delete_device(request, product_line, model, pk):
+ dev = Device.objects.get(pk=pk)
+
+ if request.method == 'POST':
+ from django.db.models import ProtectedError
+ try:
+ dev.delete()
+ messages.success(request, _("Device deleted"))
+ except ProtectedError:
+ messages.error(request, _("Cannot delete device with GSX repairs"))
+ return redirect(dev)
+
+ return redirect(index)
+
+ data = {'action': request.path}
+ data['device'] = dev
+
+ return render(request, "devices/remove.html", data)
+
+
+def edit_device(request, pk=None, product_line=None, model=None):
+ """
+ Edits an existing device or adds a new one
+ """
+ device = Device()
+ device.sn = request.GET.get('sn', '')
+
+ if product_line is not None:
+ device.product_line = product_line
+
+ if model is not None:
+ device.product_line = product_line
+ device.description = model_from_slug(product_line, model)
+
+ if pk is not None:
+ device = Device.objects.get(pk=pk)
+
+ form = DeviceForm(instance=device)
+
+ if request.method == "POST":
+
+ form = DeviceForm(request.POST, request.FILES, instance=device)
+
+ if form.is_valid():
+ device = form.save()
+ messages.success(request, _(u"%s saved") % device.description)
+ device.add_tags(request.POST.getlist('tag'))
+
+ return redirect(view_device,
+ pk=device.pk,
+ product_line=device.product_line,
+ model=device.slug)
+
+ data = prep_detail_view(request, pk, product_line, model)
+ data['form'] = form
+
+ return render(request, 'devices/form.html', data)
+
+
+def view_device(request, pk, product_line=None, model=None):
+ data = prep_detail_view(request, pk, product_line, model)
+ return render(request, "devices/view.html", data)
+
+
+def diagnostics(request, pk):
+ """
+ Fetches MRI diagnostics or initiates iOS diags from GSX
+ """
+ device = get_object_or_404(Device, pk=pk)
+
+ if request.GET.get('a') == 'init':
+ if request.method == 'POST':
+ from gsxws import diagnostics
+ order = request.POST.get('order')
+ order = device.order_set.get(pk=order)
+ email = request.POST.get('email')
+ diag = diagnostics.Diagnostics(serialNumber=device.sn)
+ diag.emailAddress = email
+ diag.shipTo = order.location.gsx_shipto
+
+ try:
+ GsxAccount.default(request.user)
+ res = diag.initiate()
+ msg = _('Diagnostics initiated - diags://%s') % res
+ order.notify("init_diags", msg, request.user)
+ messages.success(request, msg)
+ except gsxws.GsxError, e:
+ messages.error(request, e)
+
+ return redirect(order)
+
+ order = request.GET.get('order')
+ order = device.order_set.get(pk=order)
+ customer = order.customer
+ url = request.path
+ return render(request, "devices/diagnostic_init.html", locals())
+
+ if request.GET.get('a') == 'get':
+ try:
+ diagnostics = device.get_diagnostics(request.user)
+ if device.is_ios():
+ diagnostics = DiagnosticResults(diagnostics)
+ return render(request, "devices/diagnostic_ios.html", locals())
+ return render(request, "devices/diagnostic_results.html", locals())
+ except gsxws.GsxError, e:
+ return render(request, "devices/diagnostic_error.html", {'error': e})
+
+ return render(request, "devices/diagnostics.html", locals())
+
+
+def get_gsx_search_results(request, what, param, query):
+ """
+ The second phase of a GSX search.
+ There should be an active GSX session open at this stage.
+ """
+ data = {}
+ results = []
+ query = query.upper()
+ device = Device(sn=query)
+ error_template = "search/results/gsx_error.html"
+
+ # @TODO: this isn't a GSX search. Move it somewhere else.
+ if what == "orders":
+ try:
+ if param == 'serialNumber':
+ device = Device.objects.get(sn__exact=query)
+ if param == 'alternateDeviceId':
+ device = Device.objects.get(imei__exact=query)
+ except (Device.DoesNotExist, ValueError,):
+ return render(request, "search/results/gsx_notfound.html")
+
+ orders = device.order_set.all()
+ return render(request, "orders/list.html", locals())
+
+ if what == "warranty":
+ # Update wty info if been here before
+ try:
+ device = Device.objects.get(sn__exact=query)
+ device.update_gsx_details()
+ except Exception:
+ try:
+ device = Device.from_gsx(query)
+ except Exception, e:
+ return render(request, error_template, {'message': e})
+
+ results.append(device)
+
+ # maybe it's a device we've already replaced...
+ try:
+ soi = ServiceOrderItem.objects.get(sn__iexact=query)
+ results[0].repeat_service = soi.order
+ except ServiceOrderItem.DoesNotExist:
+ pass
+
+ if what == "parts":
+ # looking for parts
+ if param == "partNumber":
+ # ... with a part number
+ part = gsxws.Part(partNumber=query)
+
+ try:
+ partinfo = part.lookup()
+ except gsxws.GsxError, e:
+ return render(request, error_template, {'message': e})
+
+ product = Product.from_gsx(partinfo)
+ cache.set(query, product)
+ results.append(product)
+
+ if param == "serialNumber":
+ # ... with a serial number
+ try:
+ results = device.get_parts()
+ data['device'] = device
+ except Exception, e:
+ return render(request, error_template, {'message': e})
+
+ if param == "productName":
+ product = gsxws.Product(productName=query)
+ parts = product.parts()
+ for p in parts:
+ results.append(Product.from_gsx(p))
+
+ if what == "repairs":
+ # Looking for GSX repairs
+ if param == "serialNumber":
+ # ... with a serial number
+ try:
+ device = gsxws.Product(query)
+ #results = device.repairs()
+ # @TODO: move the encoding hack to py-gsxws
+ for i, p in enumerate(device.repairs()):
+ d = {'purchaseOrderNumber': p.purchaseOrderNumber}
+ d['repairConfirmationNumber'] = p.repairConfirmationNumber
+ d['createdOn'] = p.createdOn
+ d['customerName'] = p.customerName.encode('utf-8')
+ d['repairStatus'] = p.repairStatus
+ results.append(d)
+ except gsxws.GsxError, e:
+ return render(request, "search/results/gsx_notfound.html")
+
+ elif param == "dispatchId":
+ # ... with a repair confirmation number
+ repair = gsxws.Repair(number=query)
+ try:
+ results = repair.lookup()
+ except gsxws.GsxError, message:
+ return render(request, error_template, locals())
+
+ return render(request, "devices/search_gsx_%s.html" % what, locals())
+
+
+def search_gsx(request, what, param, query):
+ """
+ The first phase of a GSX search
+ """
+ title = _(u'Search results for "%s"') % query
+
+ try:
+ act = request.session.get("gsx_account")
+ act = None
+ if act is None:
+ GsxAccount.default(user=request.user)
+ else:
+ act.connect(request.user)
+ except gsxws.GsxError, message:
+ return render(request, "devices/search_gsx_error.html", locals())
+
+ if request.is_ajax():
+ if what == "parts":
+ try:
+ dev = Device.from_gsx(query)
+ products = dev.get_parts()
+ return render(request, "devices/parts.html", locals())
+ except gsxws.GsxError, message:
+ return render(request, "search/results/gsx_error.html", locals())
+
+ return get_gsx_search_results(request, what, param, query)
+
+ return render(request, "devices/search_gsx.html", locals())
+
+
+def search(request):
+ """
+ Searching for devices from the main navbar
+ """
+ query = request.GET.get("q", '').strip()
+ request.session['search_query'] = query
+
+ query = query.upper()
+ valid_arg = gsxws.validate(query)
+
+ if valid_arg in ('serialNumber', 'alternateDeviceId',):
+ return redirect(search_gsx, "warranty", valid_arg, query)
+
+ devices = Device.objects.filter(
+ Q(sn__icontains=query) | Q(description__icontains=query)
+ )
+
+ title = _(u'Devices matching "%s"') % query
+
+ return render(request, "devices/search.html", locals())
+
+
+def find(request):
+ """
+ Searching for device from devices/find
+ """
+ title = _("Device search")
+ form = DeviceSearchForm()
+ results = Device.objects.none()
+
+ if request.method == 'POST':
+ form = DeviceSearchForm(request.POST)
+ if form.is_valid():
+ fdata = form.cleaned_data
+ results = Device.objects.all()
+
+ if fdata.get("product_line"):
+ results = results.filter(product_line__in=fdata['product_line'])
+ if fdata.get("warranty_status"):
+ results = results.filter(warranty_status__in=fdata['warranty_status'])
+ if fdata.get("description"):
+ results = results.filter(description__icontains=fdata['description'])
+ if fdata.get("sn"):
+ results = results.filter(sn__icontains=fdata['sn'])
+ if fdata.get("date_start"):
+ results = results.filter(created_at__range=[fdata['date_start'],
+ fdata['date_end']])
+
+ paginator = Paginator(results, 100)
+ page = request.GET.get("page")
+
+ try:
+ devices = paginator.page(page)
+ except PageNotAnInteger:
+ devices = paginator.page(1)
+ except EmptyPage:
+ devices = paginator.page(paginator.num_pages)
+
+ return render(request, "devices/find.html", locals())
+
+
+#@cache_page(60*5)
+def parts(request, pk, order_id, queue_id):
+ """
+ Lists available parts for this device/order
+ taking into account the order's queues GSX Sold-To
+ and the Location's corresponding GSX account
+ """
+ from decimal import InvalidOperation
+
+ device = Device.objects.get(pk=pk)
+ order = device.order_set.get(pk=order_id)
+
+ try:
+ # remember the right GSX account
+ act = GsxAccount.default(request.user, order.queue)
+ request.session['gsx_account'] = act.pk
+ products = device.get_parts()
+ except gsxws.GsxError as message:
+ return render(request, "search/results/gsx_error.html", locals())
+ except AttributeError:
+ message = _('Invalid serial number for parts lookup')
+ return render(request, "search/results/gsx_error.html", locals())
+ except InvalidOperation:
+ message = _('Error calculating prices. Please check your system settings.')
+ return render(request, "search/results/gsx_error.html", locals())
+
+ return render(request, "devices/parts.html", locals())
+
+
+def model_parts(request, product_line=None, model=None):
+ """
+ Shows parts for this device model
+ """
+ data = prep_list_view(request, product_line, model)
+
+ if cache.get("slugmap") and model:
+ models = cache.get("slugmap")[product_line]
+ data['what'] = "parts"
+ data['param'] = "productName"
+ data['query'] = models[model]
+ data['products'] = Product.objects.filter(tags__tag=data['query'])
+
+ return render(request, "devices/index.html", data)
+
+
+def choose(request, order_id):
+ """
+ Choosing a device from within an SRO
+ Does GSX lookup in case device is not found locally
+ """
+ context = {'order': order_id}
+
+ if request.method == "POST":
+
+ query = request.POST.get('q').upper()
+ results = Device.objects.filter(Q(sn__iexact=query) | Q(imei=query))
+
+ if len(results) < 1:
+ try:
+ current_order = request.session.get("current_order_id")
+ current_order = Order.objects.get(pk=current_order)
+ if current_order and current_order.queue:
+ GsxAccount.default(request.user, current_order.queue)
+ else:
+ GsxAccount.default(request.user)
+ results = [Device.from_gsx(query)]
+ except Exception as e:
+ context['error'] = e
+ return render(request, "devices/choose-error.html", context)
+
+ context['results'] = results
+ return render(request, "devices/choose-list.html", context)
+
+ return render(request, "devices/choose.html", context)
+
+
+def upload_devices(request):
+ """
+ User uploads device DB as tab-delimited CSV file
+ SN USERNAME PASSWORD NOTES
+ """
+ gsx_account = None
+ form = DeviceUploadForm()
+
+ if request.method == "POST":
+ form = DeviceUploadForm(request.POST, request.FILES)
+
+ if form.is_valid():
+ i = 0
+ df = form.cleaned_data['datafile'].read()
+
+ if form.cleaned_data.get('do_warranty_check'):
+ gsx_account = GsxAccount.default(request.user)
+
+ for l in df.split("\r"):
+ l = l.decode("latin-1").encode("utf-8")
+ row = l.strip().split("\t")
+
+ if gsx_account:
+ try:
+ device = Device.from_gsx(row[0])
+ except Exception, e:
+ messages.error(request, e)
+ break
+ else:
+ device = Device.objects.get_or_create(sn=row[0])[0]
+
+ try:
+ device.username = row[1]
+ device.password = row[2]
+ device.notes = row[3]
+ except IndexError:
+ pass
+
+ device.save()
+ i += 1
+
+ if form.cleaned_data.get("customer"):
+ customer = form.cleaned_data['customer']
+ customer.devices.add(device)
+
+ messages.success(request, _("%d devices imported") % i)
+
+ return redirect(index)
+
+ data = {'form': form, 'action': request.path}
+ return render(request, "devices/upload_devices.html", data)
+
+
+def update_gsx_details(request, pk):
+ """
+ Updates devices GSX warranty details
+ """
+ device = get_object_or_404(Device, pk=pk)
+ try:
+ GsxAccount.default(request.user)
+ device.update_gsx_details()
+ messages.success(request, _("Warranty status updated successfully"))
+ except Exception, e:
+ messages.error(request, e)
+
+ if request.session.get('return_to'):
+ return redirect(request.session['return_to'])
+
+ return redirect(device)
+
+
+def get_info(request, pk):
+ device = get_object_or_404(Device, pk=pk)
+ return render(request, "devices/get_info.html", locals())
diff --git a/servo/views/error.py b/servo/views/error.py
new file mode 100644
index 0000000..b05367d
--- /dev/null
+++ b/servo/views/error.py
@@ -0,0 +1,53 @@
+# -*- 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 django.conf import settings
+from django.core.mail import send_mail
+from servo.lib.shorturl import from_time
+from servo.forms.base import FullTextArea
+from django.shortcuts import render
+from django.utils.translation import ugettext as _
+
+
+class ErrorForm(forms.Form):
+ description = FullTextArea(max_length=512, min_length=10)
+
+
+def report(request):
+ crashed = True
+ if request.method == 'POST':
+ form = ErrorForm(request.POST)
+ if form.is_valid():
+ ref = 'Error %s' % from_time()
+ recipient = settings.ADMINS[0][1]
+ send_mail(ref, form.cleaned_data['description'], request.user.email, [recipient])
+ crashed = False
+ else:
+ initial = _('Browser: %s') % request.META['HTTP_USER_AGENT']
+ form = ErrorForm(initial={'description': initial})
+
+ return render(request, 'error.html', locals())
diff --git a/servo/views/events.py b/servo/views/events.py
new file mode 100644
index 0000000..66ded84
--- /dev/null
+++ b/servo/views/events.py
@@ -0,0 +1,44 @@
+# -*- 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.utils import timezone
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+
+from servo.models.common import Event
+
+
+def acknowledge(request, pk):
+ e = Event.objects.get(pk=pk)
+ e.handled_at = timezone.now()
+ e.save()
+
+ referer = request.META.get('HTTP_REFERER')
+
+ if request.GET.get('return') == '0'and referer:
+ return redirect(referer)
+
+ return redirect(e.content_object.get_absolute_url())
diff --git a/servo/views/files.py b/servo/views/files.py
new file mode 100644
index 0000000..6c3002f
--- /dev/null
+++ b/servo/views/files.py
@@ -0,0 +1,52 @@
+# -*- 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 os
+import mimetypes
+from django.conf import settings
+from django.http import HttpResponse, Http404
+
+from servo.models.common import Attachment
+
+
+def view_file(request, pk):
+ doc = Attachment.objects.get(pk=pk)
+ return HttpResponse(doc.content.read(), content_type=doc.mime_type)
+
+
+def get_file(request, path):
+ """
+ Returns a file from the upload directory
+ """
+ try:
+ f = open(os.path.join(settings.MEDIA_ROOT, path), 'r')
+ except IOError:
+ raise Http404
+
+ mimetypes.init()
+ t, e = mimetypes.guess_type(f.name)
+
+ return HttpResponse(f.read(), t)
diff --git a/servo/views/gsx.py b/servo/views/gsx.py
new file mode 100644
index 0000000..c9af1a1
--- /dev/null
+++ b/servo/views/gsx.py
@@ -0,0 +1,349 @@
+# -*- 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 gsxws
+import logging
+
+from django.contrib import messages
+from django.http import HttpResponse
+from django.utils.translation import ugettext as _
+from django.shortcuts import render, redirect, get_object_or_404
+from django.contrib.auth.decorators import permission_required
+
+from servo.models import Order, GsxAccount, Repair, ServicePart
+from servo.forms import GsxCustomerForm, GsxRepairForm, GsxComponentForm
+
+
+class RepairDetails(object):
+ def __init__(self, confirmation):
+ repair = gsxws.Repair(confirmation).details()
+ self.dispatch_id = repair.dispatchId
+ self.po_number = repair.purchaseOrderNumber
+ self.cs_code = repair.csCode
+ self.tracking_number = repair.deliveryTrackingNumber
+ self.notes = repair.notes
+ self.status = repair.repairStatus
+ self.status_description = repair.coverageStatusDescription
+ self.parts = repair.partsInfo
+
+
+@permission_required("servo.change_order")
+def register_return(request, part_id):
+ part = ServicePart.objects.get(pk=part_id)
+ try:
+ part.register_for_return(request.user)
+ messages.success(request, _(u"Part %s updated") % part.order_item.code)
+ except Exception, e:
+ messages.error(request, e)
+
+ return redirect(part.repair.order)
+
+
+@permission_required("servo.change_repair")
+def import_repair(request, pk):
+ pass
+
+
+@permission_required("servo.change_order")
+def return_label(request, repair, part):
+ """
+ Returns the return label PDF for this repair and part
+ """
+ repair = Repair.objects.get(pk=repair)
+
+ try:
+ repair.connect_gsx(request.user)
+ label_data = repair.get_return_label(part)
+ return HttpResponse(label_data, content_type="application/pdf")
+ except gsxws.GsxError, e:
+ messages.error(request, e)
+ return redirect(repair.order)
+
+
+@permission_required("servo.change_repair")
+def add_part(request, repair, part):
+ """
+ Adds this part to this GSX repair
+ """
+ rep = Repair.objects.get(pk=repair)
+ soi = rep.order.serviceorderitem_set.get(pk=part)
+
+ if request.method == "POST":
+ try:
+ part = rep.add_part(soi, request.user)
+ data = {'part': part.part_number, 'repair': rep.confirmation}
+ msg = _("Part %(part)s added to repair %(repair)s") % data
+ messages.success(request, msg)
+ except gsxws.GsxError, e:
+ messages.error(request, e)
+
+ return redirect(rep.order)
+
+ context = {'item': soi}
+ context['repair'] = rep
+ context['action'] = request.path
+
+ return render(request, "repairs/add_part.html", context)
+
+
+def remove_part(request, repair, part):
+ rep = Repair.objects.get(pk=repair)
+ part = ServicePart.objects.get(pk=part)
+
+ if request.method == "POST":
+
+ rep.connect_gsx(request.user)
+ gsx_rep = rep.get_gsx_repair()
+ orderline = part.get_repair_order_line()
+ orderline.toDelete = True
+ orderline.orderLineNumber = part.line_number
+
+ try:
+ gsx_rep.update({'orderLines': [orderline]})
+ data = {'part': part.code, 'repair': rep.confirmation}
+ msg = _(u"Part %(part)s removed from %(repair)s") % data
+ messages.success(request, msg)
+ except gsxws.GsxError, e:
+ messages.error(request, e)
+
+ return redirect(rep.order)
+
+ data = {'action': request.path}
+ return render(request, "repairs/delete_part.html", data)
+
+
+def delete_repair(request, repair_id):
+ repair = get_object_or_404(Repair, pk=repair_id)
+ if repair.submitted_at:
+ messages.error(request, _('Submitted repairs cannot be deleted'))
+ return redirect(repair.order)
+
+ if request.method == 'POST':
+ order = repair.order
+ repair.delete()
+ messages.success(request, _('GSX repair deleted'))
+ return redirect(order)
+
+ context = {'action': request.path}
+ return render(request, 'repairs/delete_repair.html', context)
+
+
+def check_parts_warranty(request, repair):
+ """
+ Checks this (new) repair warranty status
+ with the included device and parts
+ """
+ repair = Repair.objects.get(pk=repair)
+ parts = repair.order.get_parts()
+
+ try:
+ wty = repair.warranty_status()
+ wty_parts = wty.parts
+ except Exception, e:
+ return render(request, 'search/results/gsx_error.html', {'message': e})
+
+ try:
+ for k, v in enumerate(parts):
+ try:
+ parts[k].warranty_status = wty_parts[k].partWarranty
+ except TypeError:
+ parts[k].warranty_status = _('Unknown')
+ except KeyError:
+ parts[0].warranty_status = wty_parts.partWarranty
+
+ context = {'parts': parts}
+ context['checked_parts'] = [p.pk for p in repair.parts.all()]
+ return render(request, 'repairs/check_parts.html', context)
+
+
+def prep_edit_view(request, repair, order=None, device=None):
+ """
+ Prepares edit view for GSX repair
+ """
+ context = {'order': order}
+
+ if repair.submitted_at:
+ raise ValueError(_("Submitted repairs cannot be edited"))
+
+ if not order.has_parts:
+ raise ValueError(_("Please add some parts before creating repair"))
+
+ if not order.customer:
+ raise ValueError(_("Cannot create GSX repair without valid customer data"))
+
+ customer = order.customer.gsx_address(request.user.location)
+ customer_form = GsxCustomerForm(initial=customer)
+
+ context['repair'] = repair
+ context['customer'] = customer
+ context['title'] = repair.get_number()
+ context['customer_form'] = customer_form
+ context['device'] = device or repair.device
+ context['repair_form'] = GsxRepairForm(instance=repair)
+
+ if len(repair.component_data):
+ context['component_form'] = GsxComponentForm(components=repair.component_data)
+
+ return context
+
+
+def edit_repair(request, order_id, repair_id):
+ """
+ Edits existing (non-submitted) GSX repair
+ """
+ order = Order.objects.get(pk=order_id)
+ repair = Repair.objects.get(pk=repair_id)
+ repair.set_parts(order.get_parts())
+
+ try:
+ repair.connect_gsx(request.user)
+ repair.check_components()
+ data = prep_edit_view(request, repair, order)
+ except (ValueError, gsxws.GsxError) as e:
+ messages.error(request, e)
+ return redirect(order)
+
+ if request.method == "POST":
+ try:
+ data = save_repair(request, data)
+ msg = _('GSX repair saved')
+ if 'confirm' in request.POST.keys():
+ repair.submit(data['customer_data'])
+ msg = _(u"GSX repair %s created") % repair.confirmation
+ messages.success(request, msg)
+ return redirect("repairs-view_repair", order.pk, repair.pk)
+ messages.success(request, msg)
+ return redirect(order)
+ except Exception, e:
+ messages.error(request, e)
+
+ return render(request, "orders/gsx_repair_form.html", data)
+
+
+def save_repair(request, context):
+ """
+ Saves this GSX repair
+ """
+ repair = context['repair']
+ customer = context['customer']
+
+ if len(repair.component_data):
+ component_form = GsxComponentForm(request.POST, components=repair.component_data)
+ if component_form.is_valid():
+ repair.component_data = component_form.json_data
+ else:
+ raise ValueError(_("Invalid component data"))
+
+ customer_form = GsxCustomerForm(request.POST, initial=customer)
+ repair_form = GsxRepairForm(request.POST, request.FILES, instance=repair)
+
+ if customer_form.is_valid():
+ context['customer_data'] = customer_form.cleaned_data
+ if repair_form.is_valid():
+ parts = repair_form.cleaned_data['parts']
+ repair.save()
+ repair.set_parts(parts)
+ else:
+ logging.debug(repair_form.errors)
+ raise ValueError(repair_form.errors)
+ else:
+ raise ValueError(_("Invalid customer info"))
+
+ context['repair_form'] = repair_form
+ context['customer_form'] = customer_form
+
+ return context
+
+
+def create_repair(request, order_id, device_id, type):
+ """
+ Creates a GSX repair for the specified SRO and device
+ and redirects to the repair's edit page.
+ """
+ from datetime import timedelta
+ from django.utils import timezone
+
+ order = Order.objects.get(pk=order_id)
+ device = order.devices.get(pk=device_id)
+
+ repair = Repair(order=order, created_by=request.user, device=device)
+ timediff = timezone.now() - order.created_at
+
+ if timediff.seconds <= 3600:
+ repair.unit_received_at = order.created_at - timedelta(hours=1)
+ else:
+ repair.unit_received_at = order.created_at
+
+ repair.reference = request.user.gsx_poprefix + order.code
+
+ try:
+ repair.gsx_account = GsxAccount.default(request.user, order.queue)
+ except Exception, e:
+ messages.error(request, e)
+ return redirect(order)
+
+ repair.repair_type = type
+ repair.tech_id = request.user.tech_id
+ repair.save()
+
+ return redirect(edit_repair, order.pk, repair.pk)
+
+
+def repair_details(request, confirmation):
+ """
+ Returns GSX repair details for confirmation number
+ """
+ repair = RepairDetails(confirmation)
+ data = {'repair': repair}
+ if request.method == "POST":
+ data = save_repair(request, data)
+ return render(request, "repairs/get_details.html", data)
+
+
+def copy_repair(request, pk):
+ """
+ Duplicates a local GSX repair
+ """
+ repair = Repair.objects.get(pk=pk)
+ new_repair = repair.duplicate(request.user)
+ return redirect(edit_repair, new_repair.order_id, new_repair.pk)
+
+
+def update_sn(request, pk, part):
+ """
+ Updates the parts serial number
+ """
+ part = ServicePart.objects.get(pk=part)
+
+ try:
+ part.repair.connect_gsx(request.user)
+ part.update_sn()
+ msg = _(u'%s serial numbers updated') % part.part_number
+ messages.success(request, msg)
+ except Exception, e:
+ messages.error(request, e)
+
+ return redirect(part.repair.order)
diff --git a/servo/views/invoices.py b/servo/views/invoices.py
new file mode 100644
index 0000000..cc48c60
--- /dev/null
+++ b/servo/views/invoices.py
@@ -0,0 +1,199 @@
+# -*- 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 django.utils import timezone
+from django.contrib import messages
+from django.utils.translation import ugettext as _
+from django.forms.models import inlineformset_factory
+from django.contrib.auth.decorators import permission_required
+from django.shortcuts import render, redirect, get_object_or_404
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+
+from servo.forms.invoices import *
+from servo.models import Order, Invoice, Payment, PurchaseOrder
+
+
+def invoices(request):
+ """
+ Lists invoices, optionally with a search filter
+ """
+ from datetime import timedelta
+ from django.db.models import Sum
+
+ data = {'title': _("Invoices")}
+ now = timezone.now()
+
+ start_date, end_date = now - timedelta(days=30), now
+ initial = {'start_date': start_date, 'end_date': end_date}
+
+ invoices = Invoice.objects.filter(created_at__range=(start_date, end_date))
+ form = InvoiceSearchForm(initial=initial)
+
+ if request.method == 'POST':
+ invoices = Invoice.objects.all()
+ form = InvoiceSearchForm(request.POST, initial=initial)
+
+ if form.is_valid():
+ fdata = form.cleaned_data
+ if fdata.get('state') == 'OPEN':
+ invoices = invoices.filter(paid_at=None)
+ if fdata.get('state') == 'PAID':
+ invoices = invoices.exclude(paid_at=None)
+
+ payment_method = fdata.get('payment_method')
+ if len(payment_method):
+ invoices = invoices.filter(payment__method=payment_method)
+
+ start_date = fdata.get('start_date', start_date)
+ end_date = fdata.get('end_date', end_date)
+ invoices = invoices.filter(created_at__range=(start_date, end_date))
+
+ if fdata.get('status_isnot'):
+ invoices = invoices.exclude(order__status__status=fdata['status_isnot'])
+
+ if fdata.get('customer_name'):
+ invoices = invoices.filter(customer_name__icontains=fdata['customer_name'])
+
+ if fdata.get('service_order'):
+ invoices = invoices.filter(order__code__exact=fdata['service_order'])
+
+ page = request.GET.get('page')
+ data['total'] = invoices.aggregate(Sum('total_net'))
+ data['total_paid'] = invoices.exclude(paid_at=None).aggregate(Sum('total_net'))
+ pos = PurchaseOrder.objects.filter(created_at__range=[start_date, end_date])
+ data['total_purchases'] = pos.aggregate(Sum('total'))
+
+ paginator = Paginator(invoices, 50)
+
+ try:
+ invoices = paginator.page(page)
+ except PageNotAnInteger:
+ invoices = paginator.page(1)
+ except EmptyPage:
+ invoices = paginator.page(paginator.num_pages)
+
+ data['form'] = form
+ data['invoices'] = invoices
+
+ return render(request, "invoices/index.html", data)
+
+
+def gsx_invoices(request):
+ pass
+
+
+def print_invoice(request, pk):
+ from servo.models import Configuration
+
+ invoice = get_object_or_404(Invoice, pk=pk)
+ template = invoice.order.get_print_template("receipt")
+
+ title = _("Receipt #%d") % invoice.pk
+ conf = Configuration.conf()
+ order = invoice.order
+
+ return render(request, template, locals())
+
+
+def view_invoice(request, pk):
+ title = _("Invoice %s") % pk
+ invoice = get_object_or_404(Invoice, pk=pk)
+ return render(request, "invoices/view_invoice.html", locals())
+
+
+@permission_required('servo.change_order')
+def create_invoice(request, order_id=None, numbers=None):
+ """
+ Dispatches Sales Order
+ """
+ order = get_object_or_404(Order, pk=order_id)
+ title = _(u'Dispatch Order %s') % order.code
+ products = order.products.filter(dispatched=False)
+
+ initial = {
+ 'order': order,
+ 'products': products,
+ 'total_tax': order.total_tax(),
+ 'total_net': order.net_total(),
+ 'total_gross': order.gross_total(),
+ }
+
+ total_margin = order.total_margin()
+
+ invoice = Invoice(order=order)
+ invoice.created_by = request.user
+ invoice.customer = order.customer
+ invoice.total_margin = total_margin
+
+ if order.customer:
+ customer = order.customer
+ initial['customer_name'] = customer.name
+ initial['customer_phone'] = customer.phone
+ initial['customer_email'] = customer.email
+ initial['customer_address'] = customer.street_address
+ else:
+ initial['customer_name'] = _(u'Walk-In Customer')
+
+ form = InvoiceForm(initial=initial, instance=invoice, prefix='invoice')
+
+ PaymentFormset = inlineformset_factory(Invoice, Payment, extra=1, form=PaymentForm, exclude=[])
+ initial = [{'amount': order.gross_total, 'created_by': request.user}]
+ formset = PaymentFormset(initial=initial, prefix='payment')
+
+ if request.method == 'POST':
+ form = InvoiceForm(request.POST, instance=invoice, prefix='invoice')
+ if form.is_valid():
+ invoice = form.save()
+ formset = PaymentFormset(request.POST, instance=invoice, prefix='payment')
+
+ if formset.is_valid():
+ payments = formset.save()
+ else:
+ messages.error(request, formset.errors)
+ return render(request, "orders/dispatch.html", locals())
+
+ products = request.POST.getlist('items')
+
+ try:
+ order.dispatch(invoice=invoice, products=products)
+ messages.success(request, _(u'Order %s dispatched') % order.code)
+ except Exception, e:
+ messages.error(request, e)
+ return redirect(order)
+ else:
+ messages.error(request, form.errors)
+
+ return render(request, "orders/dispatch.html", locals())
+
+
+@permission_required('servo.change_order')
+def add_payment(request, pk):
+ invoice = get_object_or_404(Invoice, pk=pk)
+ payment = Payment(invoice=invoice)
+ payment.created_by = request.user
+ payment.amount = request.POST.get('amount')
+ payment.save()
diff --git a/servo/views/note.py b/servo/views/note.py
new file mode 100644
index 0000000..29c74e1
--- /dev/null
+++ b/servo/views/note.py
@@ -0,0 +1,435 @@
+# -*- 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 json
+import StringIO
+from gsxws import escalations
+
+from django import template
+from django.contrib import messages
+from django.http import HttpResponse
+from django.utils.translation import ugettext as _
+from django.forms.models import modelformset_factory
+from django.views.decorators.csrf import csrf_exempt
+from django.views.decorators.cache import cache_page
+from django.shortcuts import render, redirect, get_object_or_404
+from django.contrib.auth.decorators import permission_required
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+
+from reportlab.lib.units import mm
+from reportlab.graphics.shapes import Drawing
+from reportlab.graphics.barcode import createBarcodeDrawing
+
+from servo.models import Order, Template, Tag, Customer, Note, Attachment, Escalation
+from servo.forms import NoteForm, NoteSearchForm, EscalationForm
+
+
+class BarcodeDrawing(Drawing):
+ def __init__(self, text_value, *args, **kwargs):
+ barcode = createBarcodeDrawing(
+ "Code128",
+ value=text_value.encode("utf-8"),
+ barHeight=10*mm,
+ width=80*mm
+ )
+
+ Drawing.__init__(self, barcode.width, barcode.height, *args, **kwargs)
+ self.add(barcode, name="barcode")
+
+
+def show_barcode(request, text):
+ """
+ Returns text as a barcode
+ """
+ if request.GET.get('f') == 'svg':
+ import barcode
+ output = StringIO.StringIO()
+ code = barcode.Code39(text, add_checksum=False)
+ code.write(output)
+ contents = output.getvalue()
+ output.close()
+ return HttpResponse(contents, content_type="image/svg+xml")
+
+ d = BarcodeDrawing(text)
+ return HttpResponse(d.asString("png"), content_type="image/png")
+
+
+def prep_list_view(request, kind):
+ """
+ Prepares the view for listing notes/messages
+ """
+ data = {'title': _("Messages")}
+ all_notes = Note.objects.all().order_by("-created_at")
+
+ if kind == "inbox":
+ all_notes = all_notes.filter(order=None).order_by("is_read", "-created_at")
+ if kind == "sent":
+ all_notes = all_notes.filter(created_by=request.user)
+ if kind == "flagged":
+ all_notes = all_notes.filter(is_flagged=True)
+ if kind == "escalations":
+ all_notes = Note.objects.all().exclude(escalation=None)
+
+ page = request.GET.get("page")
+ paginator = Paginator(all_notes, 20)
+
+ try:
+ notes = paginator.page(page)
+ except PageNotAnInteger:
+ notes = paginator.page(1)
+ except EmptyPage:
+ notes = paginator.page(paginator.num_pages)
+
+ data['kind'] = kind
+ data['notes'] = notes
+ data['inbox_count'] = Note.objects.filter(order=None).count()
+
+ return data
+
+
+@permission_required('servo.change_note')
+def copy(request, pk):
+ """
+ Copies a note with its attachments and labels
+ """
+ from servo.lib.shorturl import from_time
+ note = get_object_or_404(Note, pk=pk)
+
+ new_note = Note(created_by=request.user)
+ new_note.body = note.body
+ new_note.order = note.order
+ new_note.subject = note.subject
+ new_note.save()
+
+ new_note.labels = note.labels.all()
+
+ for a in note.attachments.all():
+ a.pk = None
+ a.content_object = new_note
+ a.save()
+ new_note.attachments.add(a)
+
+ return redirect(edit, pk=new_note.pk, order_id=note.order_id)
+
+
+@permission_required('servo.change_note')
+def edit(request, pk=None, order_id=None, parent=None, recipient=None, customer=None):
+ """
+ Edits a note
+ """
+ to = []
+ order = None
+ note = Note(order_id=order_id)
+ excluded_emails = note.get_excluded_emails()
+
+ if recipient is not None:
+ to.append(recipient)
+
+ if order_id is not None:
+ order = get_object_or_404(Order, pk=order_id)
+
+ if order.user and (order.user != request.user):
+ note.is_read = False
+ if order.user.email not in excluded_emails:
+ to.append(order.user.email)
+
+ if order.customer is not None:
+ customer = order.customer_id
+
+ if customer is not None:
+ customer = Customer.objects.get(pk=customer)
+ note.customer = customer
+
+ if order_id is None:
+ to.append(customer.email)
+
+ tpl = template.Template(note.subject)
+ note.subject = tpl.render(template.Context({'note': note}))
+
+ note.recipient = ', '.join(to)
+ note.created_by = request.user
+ note.sender = note.get_default_sender()
+
+ fields = escalations.CONTEXTS
+
+ try:
+ note.escalation = Escalation(created_by=request.user)
+ except Exception, e:
+ messages.error(request, e)
+ return redirect(request.META['HTTP_REFERER'])
+
+ AttachmentFormset = modelformset_factory(Attachment,
+ fields=('content',),
+ can_delete=True,
+ extra=3,
+ exclude=[])
+ formset = AttachmentFormset(queryset=Attachment.objects.none())
+
+ if pk is not None:
+ note = get_object_or_404(Note, pk=pk)
+ formset = AttachmentFormset(queryset=note.attachments.all())
+
+ if parent is not None:
+ parent = Note.objects.get(pk=parent)
+ note.parent = parent
+ note.body = parent.quote()
+
+ if parent.subject:
+ note.subject = _(u'Re: %s') % parent.clean_subject()
+ if parent.sender not in excluded_emails:
+ note.recipient = parent.sender
+ if parent.order:
+ order = parent.order
+ note.order = parent.order
+
+ note.customer = parent.customer
+ note.escalation = parent.escalation
+ note.is_reported = parent.is_reported
+
+ title = note.subject
+ form = NoteForm(instance=note)
+
+ if note.escalation:
+ contexts = json.loads(note.escalation.contexts)
+
+ escalation_form = EscalationForm(prefix='escalation', instance=note.escalation)
+
+ if request.method == "POST":
+ escalation_form = EscalationForm(request.POST,
+ prefix='escalation',
+ instance=note.escalation)
+
+ if escalation_form.is_valid():
+ note.escalation = escalation_form.save()
+
+ form = NoteForm(request.POST, instance=note)
+
+ if form.is_valid():
+
+ note = form.save()
+ formset = AttachmentFormset(request.POST, request.FILES)
+
+ if formset.is_valid():
+
+ files = formset.save(commit=False)
+
+ for f in files:
+ f.content_object = note
+ try:
+ f.save()
+ except ValueError, e:
+ messages.error(request, e)
+ return redirect(note)
+
+ note.attachments.add(*files)
+ note.save()
+
+ try:
+ msg = note.send_and_save(request.user)
+ messages.success(request, msg)
+ except ValueError, e:
+ messages.error(request, e)
+
+ return redirect(note)
+
+ return render(request, "notes/form.html", locals())
+
+
+def delete_note(request, pk):
+ """
+ Deletes a note
+ """
+ note = get_object_or_404(Note, pk=pk)
+
+ if request.method == 'POST':
+ note.delete()
+ messages.success(request, _("Note deleted"))
+
+ if request.session.get('return_to'):
+ url = request.session.get('return_to')
+ del(request.session['return_to'])
+ elif note.order_id:
+ url = note.order.get_absolute_url()
+
+ return redirect(url)
+
+ return render(request, 'notes/remove.html', {'note': note})
+
+
+@csrf_exempt
+def render_template(request):
+ """
+ Renders the template with this title with the current
+ Service Order as the context
+ """
+ content = ''
+ title = request.POST.get('title')
+ tpl = Template.objects.get(title=title)
+
+ if request.session.get('current_order_id'):
+ tpl = template.Template(tpl.content)
+ order = Order.objects.get(pk=request.session['current_order_id'])
+ content = tpl.render(template.Context({'order': order}))
+
+ return HttpResponse(content)
+
+
+def templates(request, template_id=None):
+ if template_id is not None:
+ tpl = Template.objects.get(pk=template_id)
+ content = tpl.content
+ if request.session.get('current_order_id'):
+ tpl = template.Template(content)
+ order = Order.objects.get(pk=request.session['current_order_id'])
+ content = tpl.render(template.Context({'order': order}))
+
+ return HttpResponse(content)
+
+ templates = Template.objects.all()
+ return render(request, 'notes/templates.html', {'templates': templates})
+
+
+def toggle_flag(request, pk, flag):
+ field = 'is_%s' % flag
+ note = Note.objects.get(pk=pk)
+ attr = getattr(note, field)
+ setattr(note, field, not attr)
+ note.save()
+
+ return HttpResponse(getattr(note, 'get_%s_title' % flag)())
+
+
+def toggle_tag(request, pk, tag_id):
+ note = Note.objects.get(pk=pk)
+ tag = Tag.objects.get(pk=tag_id)
+
+ if tag in note.labels.all():
+ note.labels.remove(tag)
+ else:
+ note.labels.add(tag)
+
+ if note.order:
+ return redirect(note.order)
+
+ return HttpResponse(_('OK'))
+
+def list_notes(request, kind="inbox"):
+ data = prep_list_view(request, kind)
+ request.session['return_to'] = request.path
+ return render(request, "notes/list_notes.html", data)
+
+
+def view_note(request, kind, pk):
+ note = Note.objects.get(pk=pk)
+ data = prep_list_view(request, kind)
+ data['title'] = note.subject
+ data['note'] = note
+
+ if kind == 'escalations':
+ return render(request, "notes/view_escalation.html", data)
+ else:
+ return render(request, "notes/view_note.html", data)
+
+
+def search(request):
+ query = request.GET.get("q")
+ request.session['search_query'] = query
+
+ title = _(u'Notes containing "%s"') % query
+ results = Note.objects.filter(body__icontains=query).order_by('-created_at')
+ paginator = Paginator(results, 10)
+
+ page = request.GET.get("page")
+
+ try:
+ notes = paginator.page(page)
+ except PageNotAnInteger:
+ notes = paginator.page(1)
+ except EmptyPage:
+ notes = paginator.page(paginator.num_pages)
+
+ return render(request, "notes/search.html", locals())
+
+
+def find(request):
+ form = NoteSearchForm(request.GET)
+ results = Note.objects.none()
+
+ if request.GET and form.is_valid():
+
+ fdata = form.cleaned_data
+ results = Note.objects.all()
+
+ if fdata.get('body'):
+ results = results.filter(body__icontains=fdata['body'])
+ if fdata.get('recipient'):
+ results = results.filter(recipient__icontains=fdata['recipient'])
+ if fdata.get('sender'):
+ results = results.filter(sender__icontains=fdata['sender'])
+ if fdata.get('order_code'):
+ results = results.filter(order__code__icontains=fdata['order_code'])
+
+ results = results.order_by('-created_at')
+
+ paginator = Paginator(results, 10)
+ page = request.GET.get("page")
+
+ try:
+ notes = paginator.page(page)
+ except PageNotAnInteger:
+ notes = paginator.page(1)
+ except EmptyPage:
+ notes = paginator.page(paginator.num_pages)
+
+ title = _('Message search')
+ return render(request, "notes/find.html", locals())
+
+
+def edit_escalation(request):
+ pass
+
+
+def create_escalation(request):
+ esc = Escalation()
+ form = EscalationForm()
+ title = _('Edit Escalation')
+
+ if request.method == 'POST':
+ data = request.POST.copy()
+ data['created_by'] = request.user
+ form = EscalationForm(data, request.FILES, instance=esc)
+ if form.is_valid():
+ note = form.save()
+ #esc.submit(request.user)
+ return redirect(view_note, 'escalations', note.pk)
+
+ return render(request, 'notes/edit_escalation.html', locals())
+
+
+def list_messages(request, pk):
+ note = get_object_or_404(Note, pk=pk)
+ messages = note.message_set.all()
+ return render(request, "notes/messages.html", locals())
diff --git a/servo/views/order.py b/servo/views/order.py
new file mode 100644
index 0000000..00767ab
--- /dev/null
+++ b/servo/views/order.py
@@ -0,0 +1,990 @@
+# -*- 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 json
+
+from gsxws.core import GsxError
+from django.http import QueryDict
+
+from django.db.models import Q
+from django.utils import timezone
+from django.contrib import messages
+from django.core.cache import cache
+from django.http import HttpResponse
+
+from django.db import DatabaseError
+
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext as _
+from django.views.decorators.cache import cache_page
+from django.shortcuts import render, redirect, get_object_or_404
+
+from django.views.decorators.csrf import csrf_exempt
+from django.contrib.auth.decorators import permission_required
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+
+from servo.models.order import *
+from servo.forms.orders import *
+
+from servo.models import Note, User, Device, Customer
+from servo.models.common import (Tag,
+ Configuration,
+ FlaggedItem,
+ GsxAccount,)
+from servo.models.repair import (Checklist,
+ ChecklistItem,
+ Repair,
+ ChecklistItemValue,)
+
+
+def prepare_list_view(request, args):
+ """
+ Lists service orders matching specified criteria
+ """
+ data = {'title': _("Orders")}
+
+ form = OrderSearchForm(args)
+ form.fields['location'].queryset = request.user.locations
+
+ if request.session.get("current_queue"):
+ del(request.session['current_queue'])
+
+ if request.session.get("return_to"):
+ del(request.session['return_to'])
+
+ if request.user.customer:
+ orders = Order.objects.filter(customer=request.user.customer)
+ else:
+ orders = Order.objects.filter(location__in=request.user.locations.all())
+
+ if args.get("state"):
+ orders = orders.filter(state__in=args.getlist("state"))
+
+ start_date = args.get("start_date")
+ if start_date:
+ end_date = args.get('end_date') or timezone.now()
+ orders = orders.filter(created_at__range=[start_date, end_date])
+
+ if args.get("assigned_to"):
+ users = args.getlist("assigned_to")
+ orders = orders.filter(user__in=users)
+
+ if args.get("followed_by"):
+ users = args.getlist("followed_by")
+ orders = orders.filter(followed_by__in=users)
+
+ if args.get("created_by"):
+ users = args.getlist("created_by")
+ orders = orders.filter(created_by__in=users)
+
+ if args.get("customer"):
+ customer = int(args['customer'][0])
+ if customer == 0:
+ orders = orders.filter(customer__pk=None)
+ else:
+ orders = orders.filter(customer__tree_id=customer)
+
+ if args.get("spec"):
+ spec = args['spec'][0]
+ if spec is "None":
+ orders = orders.filter(devices=None)
+ else:
+ orders = orders.filter(devices__slug=spec)
+
+ if args.get("device"):
+ orders = orders.filter(devices__pk=args['device'])
+
+ if args.get("queue"):
+ queue = args.getlist("queue")
+ orders = orders.filter(queue__in=queue)
+
+ if args.get("checkin_location"):
+ ci_location = args.getlist("checkin_location")
+ orders = orders.filter(checkin_location__in=ci_location)
+
+ if args.get("location"):
+ location = args.getlist("location")
+ orders = orders.filter(location__in=location)
+
+ if args.get("label"):
+ orders = orders.filter(tags__in=args.getlist("label"))
+
+ if args.get("status"):
+ status = args.getlist("status")
+
+ if args['status'][0] == 'None':
+ orders = orders.filter(status__pk=None)
+ else:
+ orders = orders.filter(status__status__in=status)
+
+ if args.get("color"):
+ color = args.getlist("color")
+ now = timezone.now()
+
+ if "grey" in color:
+ orders = orders.filter(status=None)
+ if "green" in color:
+ orders = orders.filter(status_limit_green__gte=now)
+ if "yellow" in color:
+ orders = orders.filter(status_limit_yellow__gte=now,
+ status_limit_green__lte=now)
+ if "red" in color:
+ orders = orders.filter(status_limit_yellow__lte=now)
+
+ page = request.GET.get("page")
+ paginator = Paginator(orders.distinct(), 100)
+
+ try:
+ order_pages = paginator.page(page)
+ except PageNotAnInteger:
+ order_pages = paginator.page(1)
+ except EmptyPage:
+ order_pages = paginator.page(paginator.num_pages)
+
+ data['form'] = form
+ data['queryset'] = orders
+ data['orders'] = order_pages
+ data['subtitle'] = _("%d search results") % orders.count()
+
+ # @FIXME!!! how to handle this with jsonserializer???
+ #request.session['order_queryset'] = orders
+
+ return data
+
+
+def prepare_detail_view(request, pk):
+ """
+ Prepares the view for whenever we're dealing with a specific order
+ """
+ order = get_object_or_404(Order, pk=pk)
+
+ request.session['current_order_id'] = None
+ request.session['current_order_code'] = None
+ request.session['current_order_customer'] = None
+
+ title = _(u'Order %s') % order.code
+ priorities = Queue.PRIORITIES
+ followers = order.followed_by.all()
+ locations = Location.objects.filter(enabled=True)
+ queues = request.user.queues.all()
+ users = order.get_available_users(request.user)
+
+ # wrap the customer in a list for easier recursetree
+ if order.customer is not None:
+ customer = order.customer.get_ancestors(include_self=True)
+ title = u'%s | %s' % (title, order.customer.name)
+ else:
+ customer = []
+
+ statuses = []
+ checklists = []
+
+ if order.queue is not None:
+ checklists = Checklist.objects.filter(queues=order.queue)
+ statuses = order.queue.queuestatus_set.all()
+
+ if order.is_editable:
+ request.session['current_order_id'] = order.pk
+ request.session['current_order_code'] = order.code
+ request.session['return_to'] = order.get_absolute_url()
+ if order.customer:
+ request.session['current_order_customer'] = order.customer.pk
+
+ return locals()
+
+
+@permission_required("servo.change_order")
+def close(request, pk):
+ """
+ Closes this Service Order
+ """
+ order = Order.objects.get(pk=pk)
+
+ if request.method == 'POST':
+ try:
+ order.close(request.user)
+ except Exception as e:
+ messages.error(request, e)
+ return redirect(order)
+
+ if request.session.get("current_order_id"):
+ del(request.session['current_order_id'])
+ del(request.session['current_order_code'])
+ del(request.session['current_order_customer'])
+
+ messages.success(request, _('Order %s closed') % order.code)
+
+ return redirect(order)
+
+ data = {'order': order, 'action': request.path}
+ return render(request, "orders/close.html", data)
+
+
+@permission_required("servo.delete_order")
+def reopen_order(request, pk):
+ order = Order.objects.get(pk=pk)
+ msg = order.reopen(request.user)
+ messages.success(request, msg)
+ return redirect(order)
+
+
+@permission_required("servo.add_order")
+def create(request, sn=None, device_id=None, product_id=None, note_id=None, customer_id=None):
+ """
+ Creates a new Service Order
+ """
+ order = Order(created_by=request.user)
+
+ if customer_id is not None:
+ order.customer_id = customer_id
+
+ try:
+ order.save()
+ except Exception as e:
+ messages.error(request, e)
+ return redirect(list_orders)
+
+ messages.success(request, _("Order %s created") % order.code)
+
+ # create service order from a new device
+ if sn is not None:
+ order.add_device_sn(sn, request.user)
+
+ if device_id is not None:
+ device = Device.objects.get(pk=device_id)
+ order.add_device(device, request.user)
+
+ # creating an order from a product
+ if product_id is not None:
+ return redirect(add_product, order.pk, product_id)
+
+ # creating an order from a note
+ if note_id is not None:
+ note = Note.objects.get(pk=note_id)
+ note.order = order
+ note.save()
+ # try to match a customer
+ if note.sender:
+ try:
+ customer = Customer.objects.get(email=note.sender)
+ order.customer = customer
+ order.save()
+ except Customer.DoesNotExist:
+ pass
+
+ return redirect(order)
+
+
+def list_orders(request):
+ """
+ orders/index
+ """
+ args = request.GET.copy()
+ default = QueryDict('state={0}'.format(Order.STATE_QUEUED))
+
+ if len(args) < 2: # search form not submitted
+ args = request.session.get("order_search_filter", default)
+
+ request.session['order_search_filter'] = args
+ data = prepare_list_view(request, args)
+
+ return render(request, "orders/index.html", data)
+
+
+@permission_required("servo.change_order")
+def toggle_tag(request, order_id, tag_id):
+ tag = Tag.objects.get(pk=tag_id)
+ order = Order.objects.get(pk=order_id)
+
+ if tag not in order.tags.all():
+ order.add_tag(tag)
+ else:
+ order.tags.remove(tag)
+
+ return HttpResponse(tag.title)
+
+
+@permission_required("servo.change_order")
+def toggle_task(request, order_id, item_id):
+ """
+ Toggles a given Check List item in this order
+ """
+ checklist_item = ChecklistItem.objects.get(pk=item_id)
+
+ try:
+ item = ChecklistItemValue.objects.get(order_id=order_id, item=checklist_item)
+ item.delete()
+ except ChecklistItemValue.DoesNotExist:
+ item = ChecklistItemValue()
+ item.item = checklist_item
+ item.order_id = order_id
+ item.checked_by = request.user
+ item.save()
+
+ return HttpResponse(checklist_item.title)
+
+
+def repair(request, order_id, repair_id):
+ """
+ Show the corresponding GSX Repair for this Service Order
+ """
+ repair = get_object_or_404(Repair, pk=repair_id)
+ data = prepare_detail_view(request, order_id)
+ data['repair'] = repair
+
+ try:
+ repair.connect_gsx(request.user)
+ details = repair.get_details()
+ try:
+ data['notes'] = details.notes.encode('utf-8')
+ except AttributeError:
+ pass
+ data['status'] = repair.update_status(request.user)
+ except Exception as e:
+ messages.error(request, e)
+
+ data['parts'] = repair.servicepart_set.all()
+ return render(request, "orders/repair.html", data)
+
+
+@permission_required("servo.change_order")
+def complete_repair(request, order_id, repair_id):
+ repair = Repair.objects.get(pk=repair_id)
+ if request.method == 'POST':
+ try:
+ repair.close(request.user)
+ msg = _(u"Repair %s marked complete.") % repair.confirmation
+ messages.success(request, msg)
+ except GsxError, e:
+ messages.error(request, e)
+
+ return redirect(repair.order)
+
+ return render(request, 'orders/close_repair.html', locals())
+
+
+@csrf_exempt
+@permission_required("servo.change_order")
+def accessories(request, pk, device_id):
+ from django.utils import safestring
+
+ if request.POST.get('name'):
+ a = Accessory(name=request.POST['name'])
+ a.order_id = pk
+ a.device_id = device_id
+ a.save()
+
+ choice_list = []
+ choices = Accessory.objects.distinct('name')
+
+ for c in choices:
+ choice_list.append(c.name)
+
+ action = reverse('orders-accessories', args=[pk, device_id])
+ selected = Accessory.objects.filter(order_id=pk, device_id=device_id)
+ choices_json = safestring.mark_safe(json.dumps(choice_list))
+
+ return render(request, 'devices/accessories_edit.html', locals())
+
+
+@permission_required('servo.change_order')
+def delete_accessory(request, order_id, device_id, pk):
+ Accessory.objects.filter(pk=pk).delete()
+ return accessories(request, order_id, device_id)
+
+
+@permission_required("servo.change_order")
+def edit(request, pk):
+ data = prepare_detail_view(request, pk)
+ data['note_tags'] = Tag.objects.filter(type='note')
+ return render(request, "orders/edit.html", data)
+
+
+@permission_required('servo.delete_order')
+def delete(request, pk):
+
+ order = get_object_or_404(Order, pk=pk)
+
+ if request.method == "POST":
+ return_to = order.get_queue_url()
+ try:
+ order.delete()
+ del(request.session['current_order_id'])
+ del(request.session['current_order_code'])
+ del(request.session['current_order_customer'])
+ messages.success(request, _(u'Order %s deleted') % order.code)
+ return redirect(return_to)
+ except Exception as e:
+ ed = {'order': order.code, 'error': e}
+ messages.error(request, _(u'Cannot delete order %(order)s: %(error)s') % ed)
+ return redirect(order)
+
+ action = request.path
+ return render(request, "orders/delete_order.html", locals())
+
+
+@permission_required('servo.change_order')
+def toggle_follow(request, order_id):
+ order = Order.objects.get(pk=order_id)
+ data = {'icon': "open", 'action': _("Follow")}
+
+ if request.user in order.followed_by.all():
+ order.followed_by.remove(request.user)
+ else:
+ order.followed_by.add(request.user)
+ data = {'icon': "close", 'action': _("Unfollow")}
+
+ if request.is_ajax():
+ return render(request, "orders/toggle_follow.html", data)
+
+ return redirect(order)
+
+
+def toggle_flagged(request, pk):
+ order = Order.objects.get(pk=pk)
+ t = FlaggedItem(content_object=order, flagged_by=request.user)
+ t.save()
+
+
+@permission_required("servo.change_order")
+def remove_user(request, pk, user_id):
+ """
+ Removes this user from the follower list, unsets assignee
+ """
+ order = get_object_or_404(Order, pk=pk)
+ user = User.objects.get(pk=user_id)
+
+ try:
+ order.remove_follower(user)
+ if user == order.user:
+ order.set_user(None, request.user)
+ order.notify("unset_user", _('User %s removed from followers') % user, request.user)
+ except Exception, e:
+ messages.error(request, e)
+
+ return redirect(order)
+
+
+@permission_required("servo.change_order")
+def update_order(request, pk, what, what_id):
+ """
+ Updates some things about an order
+ """
+ order = get_object_or_404(Order, pk=pk)
+ what_id = int(what_id)
+
+ if order.state is Order.STATE_CLOSED:
+ messages.error(request, _("Closed orders cannot be modified"))
+ return redirect(order)
+
+ if what == "user":
+ if request.method == "POST":
+ fullname = request.POST.get("user")
+ try:
+ user = User.active.get(full_name=fullname)
+ if order.user is None:
+ order.set_user(user, request.user)
+ else:
+ order.add_follower(user)
+ order.save()
+ except User.DoesNotExist:
+ messages.error(request, _(u"User %s not found") % fullname)
+ elif what_id > 0:
+ user = User.objects.get(pk=what_id)
+ order.set_user(user, request.user)
+
+ if what == "queue":
+ order.set_queue(what_id, request.user)
+
+ if what == "status":
+ order.set_status(what_id, request.user)
+
+ if what == "priority":
+ order.priority = what_id
+ order.save()
+
+ if what == "place" and request.method == "POST":
+ place = request.POST.get("place")
+ order.notify("set_place", place, request.user)
+ order.place = place
+ order.save()
+
+ if what == "label" and request.method == "POST":
+ label = request.POST.get("label")
+
+ try:
+ tag = Tag.objects.get(title=label, type="order")
+ order.add_tag(tag, request.user)
+ except Tag.DoesNotExist:
+ messages.error(request, _(u"Label %s does not exist") % label)
+
+ if what == "checkin":
+ location = Location.objects.get(pk=what_id)
+ order.checkin_location = location
+ messages.success(request, _('Order updated'))
+ order.save()
+
+ if what == "checkout":
+ location = Location.objects.get(pk=what_id)
+ order.checkout_location = location
+ messages.success(request, _('Order updated'))
+ order.save()
+
+ if what == "location":
+ location = Location.objects.get(pk=what_id)
+ msg = order.set_location(location, request.user)
+ messages.success(request, msg)
+
+ request.session['current_order_id'] = order.pk
+ request.session['current_order_code'] = order.code
+ if order.queue:
+ request.session['current_order_queue'] = order.queue.pk
+
+ return redirect(order)
+
+
+def put_on_paper(request, pk, kind="confirmation"):
+ """
+ 'Print' was taken?
+ """
+ conf = Configuration.conf()
+ order = get_object_or_404(Order, pk=pk)
+
+ title = _(u"Service Order #%s") % order.code
+ notes = order.note_set.filter(is_reported=True)
+
+ template = order.get_print_template(kind)
+
+ if kind == "receipt":
+ try:
+ invoice = order.invoice_set.latest()
+ except Exception as e:
+ pass
+
+ return render(request, template, locals())
+
+
+@permission_required("servo.change_order")
+def add_device(request, pk, device_id=None, sn=None):
+ """
+ Adds a device to a service order
+ using device_id with existing devices or
+ sn for new devices (which should have gone through GSX search)
+ """
+ order = get_object_or_404(Order, pk=pk)
+
+ if device_id is not None:
+ device = Device.objects.get(pk=device_id)
+
+ if sn is not None:
+ sn = sn.upper()
+ # not using get() since SNs are not unique
+ device = Device.objects.filter(sn=sn).first()
+
+ if device is None:
+ try:
+ device = Device.from_gsx(sn)
+ device.save()
+ except Exception, e:
+ messages.error(request, e)
+ return redirect(order)
+
+ try:
+ event = order.add_device(device, request.user)
+ messages.success(request, event)
+ except Exception, e:
+ messages.error(request, e)
+ return redirect(order)
+
+ if order.customer:
+ order.customer.devices.add(device)
+
+ return redirect(order)
+
+
+@permission_required("servo.change_order")
+def remove_device(request, order_id, device_id):
+ action = request.path
+ order = Order.objects.get(pk=order_id)
+ device = Device.objects.get(pk=device_id)
+
+ if request.method == "POST":
+ msg = order.remove_device(device, request.user)
+ messages.info(request, msg)
+ return redirect(order)
+
+ return render(request, "orders/remove_device.html", locals())
+
+
+def events(request, order_id):
+ data = prepare_detail_view(request, order_id)
+ return render(request, "orders/events.html", data)
+
+
+def device_from_product(request, pk, item_id):
+ """
+ Turns a SOI into a device and attaches it to this order
+ """
+ order = Order.objects.get(pk=pk)
+ soi = ServiceOrderItem.objects.get(pk=item_id)
+
+ try:
+ GsxAccount.default(request.user, order.queue)
+ device = Device.from_gsx(soi.sn)
+ device.save()
+ event = order.add_device(device, request.user)
+ messages.success(request, event)
+ except Exception, e:
+ messages.error(request, e)
+
+ return redirect(order)
+
+
+@permission_required('servo.change_order')
+def reserve_products(request, pk):
+ order = Order.objects.get(pk=pk)
+ location = request.user.get_location()
+
+ if request.method == 'POST':
+
+ for p in order.serviceorderitem_set.all():
+ p.reserve_product()
+
+ msg = _(u"Products of order %s reserved") % order.code
+ order.notify("products_reserved", msg, request.user)
+ messages.info(request, msg)
+
+ return redirect(order)
+
+ data = {'order': order, 'action': request.path}
+ return render(request, "orders/reserve_products.html", data)
+
+
+@permission_required("servo.change_order")
+def edit_product(request, pk, item_id):
+ """
+ Edits a product added to an order
+ """
+ order = Order.objects.get(pk=pk)
+ item = ServiceOrderItem.objects.get(pk=item_id)
+
+ if not item.kbb_sn and item.product.part_type == "REPLACEMENT":
+ try:
+ device = order.devices.all()[0]
+ item.kbb_sn = device.sn
+ except IndexError:
+ pass # Probably no device in the order
+
+ if item.product.component_code:
+ try:
+ GsxAccount.default(request.user, order.queue)
+ except Exception, e:
+ return render(request, "snippets/error_modal.html", {'error': e})
+
+ form = OrderItemForm(instance=item)
+
+ if request.method == "POST":
+ form = OrderItemForm(request.POST, instance=item)
+ if form.is_valid():
+ try:
+ item = form.save()
+ # Set whoever set the KBB sn as the one who replaced the part
+ if item.kbb_sn and not item.replaced_by:
+ item.replaced_by = request.user
+ item.save()
+
+ messages.success(request, _(u"Product %s saved") % item.code)
+
+ return redirect(order)
+ except Exception, e:
+ messages.error(request, e)
+
+ product = item.product
+ title = product.code
+ prices = json.dumps(item.product.get_price())
+
+ return render(request, "orders/edit_product.html", locals())
+
+
+@permission_required("servo.change_order")
+def add_product(request, pk, product_id):
+ "Adds this product to this Sales Order"
+ order = Order.objects.get(pk=pk)
+ product = Product.objects.get(pk=product_id)
+ order.add_product(product, 1, request.user)
+ messages.success(request, _(u'Product %s added') % product.code)
+
+ return redirect(order)
+
+
+@permission_required("servo.change_order")
+def add_part(request, pk, device, code):
+ """
+ Adds a part for this device to this order
+ """
+ gsx_product = cache.get(code)
+ order = Order.objects.get(pk=pk)
+ device = Device.objects.get(pk=device)
+
+ try:
+ product = Product.objects.get(code=code)
+ if not product.fixed_price:
+ product.update_price(gsx_product)
+ except Product.DoesNotExist:
+ product = gsx_product
+
+ product.save()
+
+ try:
+ tag, created = TaggedItem.objects.get_or_create(
+ content_type__model="product",
+ object_id=product.pk,
+ tag=device.description
+ )
+ tag.save()
+ except DatabaseError:
+ pass
+
+ order.add_product(product, 1, request.user)
+
+ return render(request, "orders/list_products.html", locals())
+
+
+def choose_product(request, order_id):
+ pass
+
+
+@permission_required("servo.change_order")
+def report_product(request, pk, item_id):
+ product = ServiceOrderItem.objects.get(pk=item_id)
+ product.should_report = not product.should_report
+ product.save()
+
+ if product.should_report:
+ return HttpResponse('<i class="icon-ok"></i>')
+
+ return HttpResponse('<i class="icon-ok icon-white"></i>')
+
+
+@permission_required("servo.change_order")
+def report_device(request, pk, device_id):
+ device = OrderDevice.objects.get(pk=device_id)
+ device.should_report = not device.should_report
+ device.save()
+
+ if device.should_report:
+ return HttpResponse('<i class="icon-ok"></i>')
+
+ return HttpResponse('<i class="icon-ok icon-white"></i>')
+
+
+@permission_required('servo.change_order')
+def remove_product(request, pk, item_id):
+ order = Order.objects.get(pk=pk)
+
+ # The following is to help those who hit Back after removing a product
+ try:
+ item = ServiceOrderItem.objects.get(pk=item_id)
+ except ServiceOrderItem.DoesNotExist:
+ messages.error(request, _("Order item does not exist"))
+ return redirect(order)
+
+ if request.method == 'POST':
+ msg = order.remove_product(item, request.user)
+ messages.info(request, msg)
+ return redirect(order)
+
+ return render(request, 'orders/remove_product.html', locals())
+
+
+@permission_required('servo.change_order')
+def products(request, pk, item_id=None, action='list'):
+ order = Order.objects.get(pk=pk)
+ if action == 'list':
+ return render(request, 'orders/products.html', {'order': order})
+
+
+@permission_required('servo.change_order')
+def list_products(request, pk):
+ order = Order.objects.get(pk=pk)
+ return render(request, "orders/list_products.html", locals())
+
+
+def parts(request, order_id, device_id, queue_id):
+ """
+ Selects parts for this device in this order
+ """
+ order = Order.objects.get(pk=order_id)
+ device = Device.objects.get(pk=device_id)
+ title = device.description
+ url = reverse('devices-parts', args=[device_id, order_id, queue_id])
+
+ if order.queue is not None:
+ request.session['current_queue'] = order.queue.pk
+
+ return render(request, "orders/parts.html", locals())
+
+
+@permission_required("servo.change_order")
+def select_customer(request, pk, customer_id):
+ """
+ Selects a specific customer for this order
+ """
+ order = Order.objects.get(pk=pk)
+ order.customer_id = customer_id
+ order.save()
+
+ return redirect(order)
+
+
+@permission_required("servo.change_order")
+def choose_customer(request, pk):
+ """
+ Lets the user search for a customer for this order
+ """
+ if request.method == "POST":
+ customers = Customer.objects.none()
+ kind = request.POST.get('kind')
+ query = request.POST.get('name')
+
+ if len(query) > 2:
+ customers = Customer.objects.filter(
+ Q(fullname__icontains=query)
+ | Q(email__icontains=query)
+ | Q(phone__contains=query)
+ )
+
+ if kind == 'companies':
+ customers = customers.filter(is_company=True)
+
+ if kind == 'contacts':
+ customers = customers.filter(is_company=False)
+
+ data = {'customers': customers, 'order_id': pk}
+ return render(request, "customers/choose-list.html", data)
+
+ data = {'action': request.path}
+ return render(request, 'customers/choose.html', data)
+
+
+@permission_required("servo.change_order")
+def remove_customer(request, pk, customer_id):
+ if request.method == "POST":
+ order = Order.objects.get(pk=pk)
+ customer = Customer.objects.get(pk=customer_id)
+ order.customer = None
+ order.save()
+ msg = _(u"Customer %s removed") % customer.name
+ order.notify("customer_removed", msg, request.user)
+ messages.success(request, msg)
+ return redirect(order)
+
+ data = {'action': request.path}
+ return render(request, "orders/remove_customer.html", data)
+
+
+def search(request):
+ query = request.GET.get("q")
+
+ if not query or len(query) < 3:
+ messages.error(request, _('Search query is too short'))
+ return redirect(list_orders)
+
+ request.session['search_query'] = query
+
+ # Redirect Order ID:s to the order
+ try:
+ order = Order.objects.get(code__iexact=query)
+ return redirect(order)
+ except Order.DoesNotExist:
+ pass
+
+ orders = Order.objects.filter(
+ Q(code=query) | Q(devices__sn__contains=query) |
+ Q(customer__fullname__icontains=query) |
+ Q(customer__phone__contains=query) |
+ Q(repair__confirmation=query) |
+ Q(repair__reference=query)
+ )
+
+ data = {'title': _(u'Search results for "%s"') % query}
+ data['orders'] = orders.distinct()
+
+ return render(request, "orders/index.html", data)
+
+
+@permission_required("servo.add_order")
+def copy_order(request, pk):
+ order = Order.objects.get(pk=pk)
+ new_order = order.duplicate(request.user)
+ return redirect(new_order)
+
+
+def history(request, pk, device):
+ device = get_object_or_404(Device, pk=device)
+ orders = device.order_set.exclude(pk=pk)
+ return render(request, "orders/history.html", locals())
+
+
+@permission_required("servo.batch_process")
+def batch_process(request):
+ form = BatchProcessForm()
+ title = _('Batch Processing')
+
+ if request.method == 'POST':
+ form = BatchProcessForm(request.POST)
+ if form.is_valid():
+ from servo.tasks import batch_process
+ batch_process.delay(request.user, form.cleaned_data)
+ messages.success(request, _('Request accepted for batch processing'))
+
+ return render(request, "orders/batch_process.html", locals())
+
+
+def download_results(request):
+ import csv
+ response = HttpResponse(content_type='text/csv')
+ response['Content-Disposition'] = 'attachment; filename="orders.csv"'
+
+ writer = csv.writer(response)
+ header = [
+ 'CODE',
+ 'CUSTOMER',
+ 'CREATED_AT',
+ 'ASSIGNED_TO',
+ 'CHECKED_IN',
+ 'LOCATION'
+ ]
+ writer.writerow(header)
+
+ for o in request.session['order_queryset']:
+ row = [o.code, o.customer, o.created_at,
+ o.user, o.checkin_location, o.location]
+ coded = [unicode(s).encode('utf-8') for s in row]
+
+ writer.writerow(coded)
+
+ return response
diff --git a/servo/views/product.py b/servo/views/product.py
new file mode 100644
index 0000000..3dd2202
--- /dev/null
+++ b/servo/views/product.py
@@ -0,0 +1,474 @@
+# -*- 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 json
+from decimal import *
+
+from django.db.models import Q
+from django.db import IntegrityError
+
+from django.contrib import messages
+from django.core.cache import cache
+from django.http import HttpResponse
+from django.shortcuts import render, redirect
+from django.utils.translation import ugettext as _
+from django.forms.models import inlineformset_factory
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.auth.decorators import permission_required
+
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+
+from servo.models import (Attachment, TaggedItem,
+ Product, ProductCategory,
+ Inventory, Location, inventory_totals,
+ GsxAccount)
+from servo.forms.product import ProductForm, CategoryForm, ProductSearchForm
+
+
+def prep_list_view(request, group='all'):
+ """
+ Prepares the product list view
+ """
+ title = _("Products")
+ all_products = Product.objects.all()
+ categories = ProductCategory.objects.all()
+
+ if group == 'all':
+ group = ProductCategory(title=_('All'), slug='all')
+ else:
+ group = categories.get(slug=group)
+ all_products = group.get_products()
+
+ if request.method == 'POST':
+ form = ProductSearchForm(request.POST)
+ if form.is_valid():
+ fdata = form.cleaned_data
+
+ description = fdata.get('description')
+ if description:
+ all_products = all_products.filter(description__icontains=description)
+
+ title = fdata.get('title')
+ if title:
+ all_products = all_products.filter(title__icontains=title)
+
+ code = fdata.get('code')
+ if code:
+ all_products = all_products.filter(code__icontains=code)
+
+ tag = fdata.get('tag')
+ if tag:
+ tag = tag.tag
+ title += u" / %s" % tag
+ all_products = all_products.filter(tags__tag=tag)
+ else:
+ form = ProductSearchForm()
+
+ title += u" / %s" % group.title
+ page = request.GET.get("page")
+ paginator = Paginator(all_products.distinct(), 25)
+
+ try:
+ products = paginator.page(page)
+ except PageNotAnInteger:
+ products = paginator.page(1)
+ except EmptyPage:
+ products = paginator.page(paginator.num_pages)
+
+ return locals()
+
+
+def tags(request):
+ """
+ Returns all product tags
+ """
+ tags = TaggedItem.objects.filter(content_type__model="product")
+ tags = tags.distinct("tag").values_list("tag", flat=True)
+ return HttpResponse(json.dumps(list(tags)), content_type='application/json')
+
+
+def list_products(request, group='all'):
+ data = prep_list_view(request, group)
+ p, s = inventory_totals()
+ data['total_sales_value'] = s
+ data['total_purchase_value'] = p
+
+ return render(request, "products/index.html", data)
+
+
+@permission_required("servo.change_product")
+def upload_gsx_parts(request, group=None):
+ from servo.forms.product import PartsImportForm
+ form = PartsImportForm()
+
+ data = {'action': request.path}
+
+ if request.method == "POST":
+
+ form = PartsImportForm(request.POST, request.FILES)
+
+ if form.is_valid():
+ data = form.cleaned_data
+ filename = "servo/uploads/products/partsdb.csv"
+ destination = open(filename, "wb+")
+
+ for chunk in data['partsdb'].chunks():
+ destination.write(chunk)
+
+ messages.success(request, _("Parts database uploaded for processing"))
+ return redirect(list_products)
+
+ data['form'] = form
+ return render(request, "products/upload_gsx_parts.html", data)
+
+
+@permission_required("servo.change_product")
+def download_products(request, group="all"):
+ filename = "products"
+
+ if group == "all":
+ products = Product.objects.all()
+ else:
+ category = ProductCategory.objects.get(slug=group)
+ products = category.get_products()
+ filename = group
+
+ response = HttpResponse(content_type="text/plain; charset=utf-8")
+ response['Content-Disposition'] = 'attachment; filename="%s.txt"' % filename
+
+ response.write(u"ID\tCODE\tTITLE\tPURCHASE_PRICE\tSALES_PRICE\tSTOCKED\n")
+
+ for p in products:
+ row = u"%s\t%s\t%s\t%s\t%s\t%s\n" % (p.pk,
+ p.code,
+ p.title,
+ p.price_purchase_stock,
+ p.price_sales_stock, 0)
+ response.write(row)
+
+ return response
+
+
+@permission_required("servo.change_product")
+def upload_products(request, group=None):
+ """"
+ Format should be the same as from download_products
+ """
+ import io
+ from servo.forms import ProductUploadForm
+ location = request.user.get_location()
+ form = ProductUploadForm()
+
+ if request.method == "POST":
+ form = ProductUploadForm(request.POST, request.FILES)
+
+ if form.is_valid():
+ string = u''
+ category = form.cleaned_data['category']
+ data = form.cleaned_data['datafile'].read()
+
+ for i in ('utf-8', 'latin-1',):
+ try:
+ string = data.decode(i)
+ except:
+ pass
+
+ if not string:
+ raise ValueError(_('Unsupported file encoding'))
+
+ i = 0
+ sio = io.StringIO(string, newline=None)
+
+ for l in sio.readlines():
+ cols = l.strip().split("\t")
+
+ if cols[0] == "ID":
+ continue # Skip header row
+
+ if len(cols) < 2:
+ continue # Skip empty rows
+
+ if len(cols) < 6: # No ID row, pad it
+ cols.insert(0, "")
+
+ product, created = Product.objects.get_or_create(code=cols[1])
+
+ product.title = cols[2].strip(' "').replace('""', '"') # Remove Excel escapes
+ product.price_purchase_stock = cols[3].replace(',', '.')
+ product.price_sales_stock = cols[4].replace(',', '.')
+ product.save()
+
+ if category:
+ product.categories.add(category)
+
+ inventory, created = Inventory.objects.get_or_create(
+ product=product, location=location
+ )
+ inventory.amount_stocked = cols[5]
+ inventory.save()
+ i += 1
+
+ messages.success(request, _(u"%d products imported") % i)
+
+ return redirect(list_products)
+
+ action = request.path
+ title = _("Upload products")
+ return render(request, "products/upload_products.html", locals())
+
+
+@permission_required("servo.change_product")
+def edit_product(request, pk=None, code=None, group='all'):
+
+ initial = {}
+ product = Product()
+
+ data = prep_list_view(request, group)
+
+ if pk is not None:
+ product = Product.objects.get(pk=pk)
+ form = ProductForm(instance=product)
+
+ if not group == 'all':
+ cat = ProductCategory.objects.get(slug=group)
+ initial = {'categories': [cat]}
+ data['group'] = cat
+
+ product.update_photo()
+
+ if code is not None:
+ product = cache.get(code)
+
+ form = ProductForm(instance=product, initial=initial)
+ InventoryFormset = inlineformset_factory(
+ Product,
+ Inventory,
+ extra=1,
+ max_num=1,
+ exclude=[]
+ )
+
+ formset = InventoryFormset(
+ instance=product,
+ initial=[{'location': request.user.location}]
+ )
+
+ if request.method == "POST":
+
+ form = ProductForm(request.POST, request.FILES, instance=product)
+
+ if form.is_valid():
+
+ product = form.save()
+ content_type = ContentType.objects.get(model="product")
+
+ for a in request.POST.getlist("attachments"):
+ doc = Attachment.objects.get(pk=a)
+ product.attachments.add(doc)
+
+ tags = [x for x in request.POST.getlist('tag') if x != '']
+
+ for t in tags:
+ tag, created = TaggedItem.objects.get_or_create(
+ content_type=content_type,
+ object_id=product.pk,
+ tag=t)
+ tag.save()
+
+ formset = InventoryFormset(request.POST, instance=product)
+
+ if formset.is_valid():
+ formset.save()
+ messages.success(request, _(u"Product %s saved") % product.code)
+ return redirect(product)
+ else:
+ messages.error(request, _('Error in inventory details'))
+ else:
+ messages.error(request, _('Error in product info'))
+
+ data['form'] = form
+ data['product'] = product
+ data['formset'] = formset
+ data['title'] = product.title
+
+ return render(request, "products/form.html", data)
+
+
+@permission_required("servo.delete_product")
+def delete_product(request, pk, group):
+ from django.db.models import ProtectedError
+
+ product = Product.objects.get(pk=pk)
+
+ if request.method == 'POST':
+ try:
+ product.delete()
+ Inventory.objects.filter(product=product).delete()
+ messages.success(request, _("Product deleted"))
+ except ProtectedError:
+ messages.error(request, _('Cannot delete product'))
+
+ return redirect(list_products, group)
+
+ action = request.path
+ return render(request, 'products/remove.html', locals())
+
+
+def search(request):
+
+ query = request.GET.get("q")
+ request.session['search_query'] = query
+
+ results = Product.objects.filter(
+ Q(code__icontains=query) | Q(title__icontains=query) | Q(eee_code__icontains=query)
+ )
+
+ paginator = Paginator(results, 100)
+ page = request.GET.get("page")
+
+ try:
+ products = paginator.page(page)
+ except PageNotAnInteger:
+ products = paginator.page(1)
+ except EmptyPage:
+ products = paginator.page(paginator.num_pages)
+
+ title = _(u'Search results for "%s"') % query
+ group = ProductCategory(title=_('All'), slug='all')
+
+ return render(request, 'products/search.html', locals())
+
+
+def view_product(request, pk=None, code=None, group=None):
+
+ product = Product()
+ inventory = Inventory.objects.none()
+
+ try:
+ product = Product.objects.get(pk=pk)
+ inventory = Inventory.objects.filter(product=product)
+ except Product.DoesNotExist:
+ product = cache.get(code)
+
+ data = prep_list_view(request, group)
+
+ data['product'] = product
+ data['title'] = product.title
+ data['inventory'] = inventory
+
+ return render(request, "products/view.html", data)
+
+
+@permission_required("servo.change_productcategory")
+def edit_category(request, slug=None, parent_slug=None):
+
+ form = CategoryForm()
+ category = ProductCategory()
+
+ if slug is not None:
+ category = ProductCategory.objects.get(slug=slug)
+ form = CategoryForm(instance=category)
+
+ if parent_slug is not None:
+ parent = ProductCategory.objects.get(slug=parent_slug)
+ form = CategoryForm(initial={'parent': parent.pk})
+
+ if request.method == "POST":
+ form = CategoryForm(request.POST, instance=category)
+ if form.is_valid():
+ try:
+ category = form.save()
+ except IntegrityError:
+ messages.error(request, _(u'Category %s already exists') % category.title)
+ return redirect(list_products)
+ messages.success(request, _(u"Category %s saved") % category.title)
+ return redirect(category)
+ else:
+ messages.error(request, form.errors)
+ return redirect(list_products)
+
+ return render(request, "products/category_form.html", locals())
+
+
+@permission_required("servo.delete_productcategory")
+def delete_category(request, slug):
+
+ category = ProductCategory.objects.get(slug=slug)
+
+ if request.method == "POST":
+ category.delete()
+ messages.success(request, _("Category deleted"))
+
+ return redirect(list_products)
+
+ data = {'category': category}
+ data['action'] = request.path
+ return render(request, 'products/delete_category.html', data)
+
+
+@permission_required("servo.change_order")
+def choose_product(request, order_id, product_id=None, target_url="orders-add_product"):
+ """
+ order_id can be either Service Order or Purchase Order
+ """
+ data = {'order': order_id}
+ data['action'] = request.path
+ data['target_url'] = target_url
+
+ if request.method == "POST":
+ query = request.POST.get('q')
+
+ if len(query) > 2:
+ products = Product.objects.filter(
+ Q(code__icontains=query) | Q(title__icontains=query)
+ )
+ data['products'] = products
+
+ return render(request, 'products/choose-list.html', data)
+
+ return render(request, 'products/choose.html', data)
+
+
+def get_info(request, location, code):
+ try:
+ product = Product.objects.get(code=code)
+ inventory = Inventory.objects.filter(product=product)
+ except Product.DoesNotExist:
+ product = cache.get(code)
+
+ return render(request, 'products/get_info.html', locals())
+
+
+def update_price(request, pk):
+ product = Product.objects.get(pk=pk)
+ try:
+ GsxAccount.default(request.user)
+ product.update_price()
+ messages.success(request, _('Price info updated from GSX'))
+ except Exception, e:
+ messages.error(request, _('Failed to update price from GSX'))
+
+ return redirect(product)
diff --git a/servo/views/purchases.py b/servo/views/purchases.py
new file mode 100644
index 0000000..66e7075
--- /dev/null
+++ b/servo/views/purchases.py
@@ -0,0 +1,242 @@
+# -*- 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 gsxws
+
+from django.forms.models import modelform_factory
+from django.forms.models import inlineformset_factory
+
+from django.utils.translation import ugettext as _
+
+from django.shortcuts import render, redirect
+from servo.models.order import ServiceOrderItem
+from django.contrib.auth.decorators import permission_required
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+
+from django.contrib import messages
+
+from servo.models import Product, GsxAccount, PurchaseOrder, PurchaseOrderItem
+from servo.forms import PurchaseOrderItemEditForm, PurchaseOrderSearchForm
+
+
+@permission_required("servo.change_purchaseorder")
+def list_pos(request):
+ from datetime import timedelta
+ from django.utils import timezone
+ from django.db.models import Sum
+
+ now = timezone.now()
+ data = {'title': _("Purchase Orders")}
+
+ initial = {'start_date': now - timedelta(days=30), 'end_date': now}
+ all_orders = PurchaseOrder.objects.filter(
+ created_at__range=(initial['start_date'], initial['end_date'])
+ )
+
+ form = PurchaseOrderSearchForm(initial=initial)
+
+ if request.method == 'POST':
+ all_orders = PurchaseOrder.objects.all()
+ form = PurchaseOrderSearchForm(request.POST, initial=initial)
+
+ if form.is_valid():
+ fdata = form.cleaned_data
+ reference = fdata.get('reference')
+ if reference:
+ all_orders = all_orders.filter(reference__contains=reference)
+ if fdata.get('state') == 'open':
+ all_orders = all_orders.filter(submitted_at=None)
+ if fdata.get('state') == 'submitted':
+ all_orders = all_orders.exclude(submitted_at=None)
+ if fdata.get('state') == 'received':
+ all_orders = all_orders.exclude(has_arrived=True)
+ s, e = (fdata.get('start_date'), fdata.get('end_date'))
+ if s and e:
+ all_orders = all_orders.filter(created_at__range=(s, e))
+ created_by = fdata.get('created_by')
+ if created_by:
+ all_orders = all_orders.filter(created_by=created_by)
+
+ page = request.GET.get("page")
+ paginator = Paginator(all_orders, 50)
+
+ try:
+ orders = paginator.page(page)
+ except PageNotAnInteger:
+ orders = paginator.page(1)
+ except EmptyPage:
+ orders = paginator.page(paginator.num_pages)
+
+ data['orders'] = orders
+ data['form'] = form
+ data['total'] = all_orders.aggregate(Sum('total'))
+ return render(request, "purchases/list_pos.html", data)
+
+
+@permission_required("servo.change_purchaseorder")
+def delete_from_po(request, pk, item_id):
+ # @TODO - decrement amount_ordered?
+ po = PurchaseOrder.objects.get(pk=pk)
+ poi = PurchaseOrderItem.objects.get(pk=item_id)
+ poi.delete()
+ messages.success(request, _(u'Product %s removed' % poi.product.code))
+ return redirect(po)
+
+
+@permission_required("servo.change_purchaseorder")
+def add_to_po(request, pk, product_id):
+ po = PurchaseOrder.objects.get(pk=pk)
+ product = Product.objects.get(pk=product_id)
+ po.add_product(product, 1, request.user)
+ messages.success(request, _(u"Product %s added" % product.code))
+ return redirect(edit_po, po.pk)
+
+
+def view_po(request, pk):
+ po = PurchaseOrder.objects.get(pk=pk)
+ title = _('Purchase Order %d' % po.pk)
+ return render(request, "purchases/view_po.html", locals())
+
+
+@permission_required("servo.change_purchaseorder")
+def edit_po(request, pk, item_id=None):
+
+ if pk is not None:
+ po = PurchaseOrder.objects.get(pk=pk)
+ else:
+ po = PurchaseOrder(created_by=request.user)
+
+ PurchaseOrderForm = modelform_factory(PurchaseOrder, exclude=[])
+ form = PurchaseOrderForm(instance=po)
+
+ ItemFormset = inlineformset_factory(
+ PurchaseOrder,
+ PurchaseOrderItem,
+ extra=0,
+ form=PurchaseOrderItemEditForm,
+ exclude=[]
+ )
+
+ formset = ItemFormset(instance=po)
+
+ if request.method == "POST":
+
+ form = PurchaseOrderForm(request.POST, instance=po)
+
+ if form.is_valid():
+
+ po = form.save()
+ formset = ItemFormset(request.POST, instance=po)
+
+ if formset.is_valid():
+
+ formset.save()
+ msg = _("Purchase Order %d saved" % po.pk)
+
+ if "confirm" in request.POST.keys():
+ po.submit(request.user)
+ msg = _("Purchase Order %d submitted") % po.pk
+
+ messages.success(request, msg)
+ return redirect(list_pos)
+
+ request.session['current_po'] = po
+ data = {'order': po, 'form': form}
+ data['formset'] = formset
+ data['title'] = _('Purchase Order #%d' % po.pk)
+
+ return render(request, "purchases/edit_po.html", data)
+
+
+@permission_required("servo.change_purchaseorder")
+def order_stock(request, po_id):
+ """
+ Submits the PO as a GSX Stocking Order
+ Using the default GSX account.
+ """
+ po = PurchaseOrder.objects.get(pk=po_id)
+
+ if request.method == "POST":
+ if po.submitted_at:
+ messages.error(request, _(u'Purchase Order %s has already been submitted') % po.pk)
+ return list_pos(request)
+
+ act = GsxAccount.default(request.user)
+
+ stock_order = gsxws.StockingOrder(
+ shipToCode=act.ship_to,
+ purchaseOrderNumber=po.id
+ )
+
+ for i in po.purchaseorderitem_set.all():
+ stock_order.add_part(i.code, i.amount)
+
+ try:
+ result = stock_order.submit()
+ po.supplier = "Apple"
+ po.confirmation = result.confirmationNumber
+ po.submit(request.user)
+ msg = _("Products ordered with confirmation %s" % po.confirmation)
+ messages.success(request, msg)
+ except gsxws.GsxError, e:
+ messages.error(request, e)
+
+ return redirect(list_pos)
+
+ data = {'action': request.path}
+ return render(request, "purchases/order_stock.html", data)
+
+
+@permission_required('servo.delete_purchaseorder')
+def delete_po(request, po_id):
+ po = PurchaseOrder.objects.get(pk=po_id)
+ try:
+ po.delete()
+ messages.success(request, _("Purchase Order %s deleted" % po_id))
+ except Exception, e:
+ messages.error(request, e)
+ return redirect(list_pos)
+
+
+@permission_required('servo.add_purchaseorder')
+def create_po(request, product_id=None, order_id=None):
+ po = PurchaseOrder(created_by=request.user)
+ location = request.user.get_location()
+ po.location = location
+ po.save()
+
+ if order_id is not None:
+ po.sales_order_id = order_id
+ for i in ServiceOrderItem.objects.filter(order_id=order_id):
+ po.add_product(i, amount=1, user=request.user)
+
+ if product_id is not None:
+ product = Product.objects.get(pk=product_id)
+ po.add_product(product, amount=1, user=request.user)
+
+ messages.success(request, _("Purchase Order %d created" % po.pk))
+
+ return redirect(edit_po, po.pk)
diff --git a/servo/views/queue.py b/servo/views/queue.py
new file mode 100644
index 0000000..e7e0914
--- /dev/null
+++ b/servo/views/queue.py
@@ -0,0 +1,40 @@
+# -*- 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 servo.models import Queue
+from django.http import HttpResponse
+
+
+def statuses(request, queue_id):
+ """Lists available statuses for this queue"""
+ queue = Queue.objects.get(pk=queue_id)
+
+ class StatusForm(forms.Form):
+ status = forms.ModelChoiceField(queryset=queue.queuestatus_set.all())
+
+ form = StatusForm()
+ return HttpResponse(str(form['status']))
diff --git a/servo/views/rules.py b/servo/views/rules.py
new file mode 100644
index 0000000..ec193a8
--- /dev/null
+++ b/servo/views/rules.py
@@ -0,0 +1,101 @@
+# -*- 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.conf import settings
+from django.contrib import messages
+from django.utils.translation import ugettext as _
+from django.shortcuts import render, redirect, get_object_or_404
+
+from servo.models.rules import *
+
+
+def get_data(request):
+ pass
+
+
+def list_rules(request):
+ title = _('Rules')
+ object_list = Rule.objects.all()
+ return render(request, "rules/list_rules.html", locals())
+
+
+def edit_rule(request, pk=None):
+ title = _('Rules')
+ object_list = Rule.objects.all()
+
+ if pk:
+ rule = get_object_or_404(Rule, pk=pk)
+
+ if request.method == 'POST':
+ if pk:
+ rule = Rule.objects.get(pk=pk)
+ else:
+ rule = Rule()
+
+ rule.description = request.POST.get('description')
+ #rule.match = request.POST.get('description')
+ rule.save()
+
+ rule.condition_set.all().delete()
+ rule.action_set.all().delete()
+
+ keys = request.POST.getlist('condition-key')
+ values = request.POST.getlist('condition-value')
+
+ for k, v in enumerate(keys):
+ cond = Condition(rule=rule)
+ cond.key = v
+ cond.value = values[k]
+ cond.save()
+
+ keys = request.POST.getlist('action-key')
+ values = request.POST.getlist('action-value')
+
+ for k, v in enumerate(keys):
+ action = Action(rule=rule)
+ action.key = v
+ action.value = values[k]
+ action.save()
+
+
+ return render(request, "rules/form.html", locals())
+
+
+def view_rule(request, pk):
+ pass
+
+
+def delete_rule(request, pk):
+ action = request.path
+ title = _('Delete rule')
+ rule = get_object_or_404(Rule, pk=pk)
+
+ if request.method == 'POST':
+ rule.delete()
+ messages.error(request, _('Rule deleted'))
+ return redirect(list_rules)
+
+ return render(request, "generic/delete.html", locals())
diff --git a/servo/views/search.py b/servo/views/search.py
new file mode 100644
index 0000000..c61eca6
--- /dev/null
+++ b/servo/views/search.py
@@ -0,0 +1,254 @@
+# -*- 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 re
+import gsxws
+
+from django.db.models import Q
+from django.core.cache import cache
+from django.shortcuts import render, redirect
+from django.utils.translation import ugettext as _
+from django.core.urlresolvers import reverse
+
+from django.http import QueryDict, HttpResponseRedirect
+
+from servo.models.note import Note
+from servo.models.wiki import Article
+from servo.models.device import Device
+from servo.models.product import Product
+from servo.models.common import GsxAccount
+from servo.models.purchases import PurchaseOrder
+from servo.models.order import Order, ServiceOrderItem
+
+
+def results_redirect(view, data):
+ q = QueryDict('', mutable=True)
+ q['q'] = data['query']
+ query_str = q.urlencode()
+ url = reverse(view, args=[data['what']])
+ return HttpResponseRedirect("%s?%s" % (url, query_str))
+
+
+def prepare_result_view(request):
+
+ query = request.GET.get('q')
+
+ data = {'title': _('Search results for "%s"' % query)}
+
+ data['gsx_type'] = gsxws.validate(query.upper())
+ data['query'] = query
+ data['tag_id'] = None
+ data['cat_id'] = None # Product category
+ data['group'] = 'all' # customer group
+
+ return data, query
+
+
+def list_gsx(request, what="warranty"):
+ data, query = prepare_result_view(request)
+ data['what'] = what
+ return render(request, "search/results/gsx.html", data)
+
+
+def search_gsx(request, what, arg, value):
+ if request.is_ajax():
+
+ if what == "parts" and value != "None":
+ results = []
+ GsxAccount.default(user=request.user)
+
+ try:
+ product = gsxws.Product(productName=value)
+ parts = product.parts()
+ for p in parts:
+ results.append(Product.from_gsx(p))
+ except gsxws.GsxError, e:
+ data = {'message': e}
+ return render(request, "search/results/gsx_error.html", data)
+
+ data = {'results': results}
+
+ return render(request, "search/results/gsx_%s.html" % what, data)
+
+ data = {arg: value}
+ return render(request, "search/gsx_results.html", data)
+
+
+def view_gsx_results(request, what="warranty"):
+ """
+ Searches for something from GSX. Defaults to warranty lookup.
+ GSX search strings are always UPPERCASE.
+ """
+ results = list()
+ data, query = prepare_result_view(request)
+ query = query.upper()
+
+ error_template = "search/results/gsx_error.html"
+
+ if data['gsx_type'] == "dispatchId":
+ what = "repairs"
+
+ if data['gsx_type'] == "partNumber":
+ what = "parts"
+
+ data['what'] = what
+ gsx_type = data['gsx_type']
+
+ try:
+ if request.session.get("current_queue"):
+ queue = request.session['current_queue']
+ GsxAccount.default(request.user, queue)
+ else:
+ GsxAccount.default(request.user)
+ except gsxws.GsxError, e:
+ error = {'message': e}
+ return render(request, error_template, error)
+
+ if gsx_type == "serialNumber" or "alternateDeviceId":
+ try:
+ device = Device.objects.get(sn=query)
+ except Device.DoesNotExist:
+ device = Device(sn=query)
+
+ if what == "warranty":
+ if cache.get(query):
+ result = cache.get(query)
+ else:
+ try:
+ result = Device.from_gsx(query)
+ except gsxws.GsxError, e:
+ error = {'message': e}
+ return render(request, error_template, error)
+
+ if re.match(r'iPhone', result.description):
+ result.activation = device.get_activation()
+
+ results.append(result)
+
+ if what == "parts":
+ # looking for parts
+ if gsx_type == "partNumber":
+ # ... with a part number
+ part = gsxws.Part(partNumber=query)
+
+ try:
+ partinfo = part.lookup()
+ except gsxws.GsxError, e:
+ error = {'message': e}
+ return render(request, error_template, error)
+
+ product = Product.from_gsx(partinfo)
+ cache.set(query, product)
+ results.append(product)
+ else:
+ # ... with a serial number
+ try:
+ results = device.get_parts()
+ data['device'] = device
+ except Exception, e:
+ error = {'message': e}
+ return render(request, error_template, error)
+
+ if what == "repairs":
+ # Looking for GSX repairs
+ if gsx_type == "serialNumber":
+ # ... with a serial number
+ try:
+ device = gsxws.Product(query)
+ results = device.repairs()
+ except gsxws.GsxError, e:
+ return render(request, "search/results/gsx_notfound.html")
+
+ elif gsx_type == "dispatchId":
+ # ... with a repair confirmation number
+ repair = gsxws.Repair(number=query)
+ try:
+ results = repair.lookup()
+ except gsxws.GsxError, e:
+ error = {'message': e}
+ return render(request, error_template, error)
+
+ if what == "repair_details":
+ repair = gsxws.Repair(number=query)
+ results = repair.details()
+ return render(request, "search/results/gsx_repair_details.html", results)
+
+ # Cache the results for quicker access later
+ cache.set('%s-%s' % (what, query), results)
+ data['results'] = results
+
+ return render(request, "search/results/gsx_%s.html" % what, data)
+
+
+def list_products(request):
+ data, query = prepare_result_view(request)
+ data['products'] = Product.objects.filter(
+ Q(code__icontains=query) | Q(title__icontains=query)
+ )
+
+ return render(request, "search/results/products.html", data)
+
+
+def list_notes(request):
+ data, query = prepare_result_view(request)
+ data['notes'] = Note.objects.filter(body__icontains=query)
+ return render(request, "search/results/notes.html", data)
+
+
+def spotlight(request):
+ """
+ Searches for anything and redirects to the "closest" result view.
+ GSX searches are done separately.
+ """
+ data, query = prepare_result_view(request)
+ data['what'] = "warranty"
+
+ if Order.objects.filter(customer__name__icontains=query).exists():
+ return list_orders(request)
+
+ if data['gsx_type'] == "serialNumber":
+ try:
+ device = Device.objects.get(sn=query)
+ return redirect(device)
+ except Device.DoesNotExist:
+ return results_redirect("search-gsx", data)
+
+ data['parts'] = ServiceOrderItem.objects.filter(sn__icontains=query)
+
+ if gsxws.validate(query, "dispatchId"):
+ try:
+ po = PurchaseOrder.objects.get(confirmation=query)
+ data['orders'] = [po.sales_order]
+ except PurchaseOrder.DoesNotExist:
+ pass
+
+ data['products'] = Product.objects.filter(
+ Q(code__icontains=query) | Q(title__icontains=query)
+ )
+
+ data['articles'] = Article.objects.filter(content__contains=query)
+
+ return render(request, "search/spotlight.html", data)
diff --git a/servo/views/shipments.py b/servo/views/shipments.py
new file mode 100644
index 0000000..9b31e93
--- /dev/null
+++ b/servo/views/shipments.py
@@ -0,0 +1,392 @@
+# -*- 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 gsxws
+from decimal import *
+
+from django.utils import timezone
+from django.contrib import messages
+from django.http import HttpResponse
+from django.shortcuts import render, redirect
+from django.utils.translation import ugettext as _
+from django.forms.models import inlineformset_factory
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+
+from servo.models import GsxAccount, ServicePart, Shipment, PurchaseOrderItem
+from servo.forms.product import PurchaseOrderItemForm, IncomingSearchForm
+from servo.forms.returns import *
+
+
+def prep_counts():
+ incoming = PurchaseOrderItem.objects.filter(received_at=None)
+ incoming = incoming.exclude(purchase_order__submitted_at=None).count()
+ pending_return = ''
+ returns = Shipment.objects.exclude(dispatched_at=None).count()
+ return locals()
+
+
+def prep_list_view(request):
+
+ from datetime import timedelta
+ now = timezone.now()
+
+ data = {'can_receive': True}
+ data['counts'] = prep_counts()
+ location = request.user.get_location()
+
+ ordered_date_range = [now - timedelta(days=30), timezone.now()]
+ received_date_range = [now - timedelta(days=30), timezone.now()]
+
+ initial = {
+ 'location': location,
+ 'ordered_start_date': ordered_date_range[0],
+ 'ordered_end_date': ordered_date_range[1],
+ }
+
+ if request.method == 'POST':
+ data['form'] = IncomingSearchForm(request.POST, initial=initial)
+ else:
+ data['form'] = IncomingSearchForm(initial=initial)
+
+ inventory = PurchaseOrderItem.objects.filter(received_at=None)
+ inventory = inventory.exclude(purchase_order__submitted_at=None)
+
+ if request.method == 'POST':
+ fdata = request.POST
+
+ loc = fdata.get('location')
+ if loc:
+ inventory = PurchaseOrderItem.objects.filter(purchase_order__location=loc)
+
+ ordered_sd = fdata.get('ordered_start_date')
+ if ordered_sd:
+ ordered_date_range[0] = ordered_sd
+ inventory = inventory.filter(purchase_order__submitted_at__range=ordered_date_range)
+
+ received_sd = fdata.get('received_start_date')
+ if received_sd:
+ received_date_range[0] = received_sd
+ inventory = inventory.filter(received_at__range=received_date_range)
+
+ conf = fdata.get('confirmation')
+ if conf:
+ inventory = PurchaseOrderItem.objects.filter(purchase_order__confirmation=conf)
+
+ service_order = fdata.get('service_order')
+ if service_order:
+ inventory = PurchaseOrderItem.objects.filter(purchase_order__sales_order__code=service_order)
+
+ page = request.GET.get("page")
+ data['count'] = inventory.count()
+ inventory = inventory.order_by('-id')
+
+ paginator = Paginator(inventory, 200)
+ data['title'] = _(u"%d incoming products") % data['count']
+
+ try:
+ inventory = paginator.page(page)
+ except PageNotAnInteger:
+ inventory = paginator.page(1)
+ except EmptyPage:
+ inventory = paginator.page(paginator.num_pages)
+
+ data['inventory'] = inventory
+
+ return data
+
+
+def list_incoming(request, shipment=None, status=""):
+ """
+ Lists purchase order items that have not arrived yet
+ """
+ data = prep_list_view(request)
+
+ if request.POST.getlist("id"):
+ count = len(request.POST.getlist("id"))
+ for i in request.POST.getlist("id"):
+ item = PurchaseOrderItem.objects.get(pk=i)
+ try:
+ item.receive(request.user)
+ except ValueError, e:
+ messages.error(request, e)
+ return redirect(list_incoming)
+
+ messages.success(request, _("%d products received") % count)
+
+ return redirect(list_incoming)
+
+ return render(request, "shipments/list_incoming.html", data)
+
+
+def view_incoming(request, pk):
+ """
+ Shows an incoming part
+ """
+ next = False
+ item = PurchaseOrderItem.objects.get(pk=pk)
+
+ data = prep_list_view(request)
+
+ data['next'] = ""
+ data['subtitle'] = item.code
+
+ try:
+ next = item.get_next_by_created_at(received_at=None)
+ data['next'] = next.pk
+ except PurchaseOrderItem.DoesNotExist:
+ pass # That was the last of them...
+
+ if request.method == "POST":
+
+ item.received_by = request.user
+ item.received_at = timezone.now()
+
+ form = PurchaseOrderItemForm(request.POST, instance=item)
+
+ if form.is_valid():
+ try:
+ item = form.save()
+ except gsxws.GsxError, e:
+ messages.error(request, e)
+ return redirect(view_incoming, date, pk)
+
+ messages.success(request, _(u"Product %s received") % item.code)
+
+ if next:
+ return redirect(view_incoming, next.pk)
+ else:
+ return redirect(list_incoming)
+ else:
+ form = PurchaseOrderItemForm(instance=item)
+
+ data['form'] = form
+ data['item'] = item
+ data['url'] = request.path
+
+ return render(request, "products/receive_item.html", data)
+
+
+def list_returns(request, shipment=None, date=None):
+ return render(request, "shipments/list_returns.html", locals())
+
+
+def return_label(request, code, return_order):
+
+ GsxAccount.default(request.user)
+
+ try:
+ label = gsxws.Returns(return_order)
+ return HttpResponse(label.returnLabelFileData, content_type="application/pdf")
+ except Exception, e:
+ messages.add_message(request, messages.ERROR, e)
+ return redirect('products-list')
+
+
+def list_bulk_returns(request):
+ from django.db.models import Count
+ title = _("Browse Bulk Returns")
+ returns = Shipment.objects.exclude(dispatched_at=None).annotate(num_parts=Count('servicepart'))
+
+ page = request.GET.get("page")
+ paginator = Paginator(returns, 50)
+
+ try:
+ returns = paginator.page(page)
+ except PageNotAnInteger:
+ returns = paginator.page(1)
+ except EmptyPage:
+ returns = paginator.page(paginator.num_pages)
+
+ counts = prep_counts()
+ return render(request, "shipments/list_bulk_returns.html", locals())
+
+
+def view_packing_list(request, pk):
+ shipment = Shipment.objects.get(pk=pk)
+ pdf = shipment.packing_list.read()
+ return HttpResponse(pdf, content_type="application/pdf")
+
+
+def view_bulk_return(request, pk):
+ title = _("View bulk return")
+ shipment = Shipment.objects.get(pk=pk)
+ return render(request, "shipments/view_bulk_return.html", locals())
+
+
+def edit_bulk_return(request, pk=None, ship_to=None):
+ """
+ Edits the bulk return shipment before it's submitted
+ """
+ location = request.user.get_location()
+ accounts = location.get_shipto_choices()
+
+ if len(accounts) < 1:
+ messages.error(request, _(u'Location %s has no Ship-To') % location.title)
+ return redirect('products-list_products')
+
+ if not ship_to:
+ ship_to = accounts[0][0]
+ return redirect(edit_bulk_return, ship_to=ship_to)
+
+ shipment = Shipment.get_current(request.user, location, ship_to)
+
+ part_count = shipment.servicepart_set.all().count()
+ PartFormSet = inlineformset_factory(Shipment,
+ ServicePart,
+ form=BulkReturnPartForm,
+ extra=0,
+ exclude=[])
+ form = BulkReturnForm(instance=shipment)
+ formset = PartFormSet(instance=shipment)
+
+ if request.method == "POST":
+ form = BulkReturnForm(request.POST, instance=shipment)
+ if form.is_valid():
+ formset = PartFormSet(request.POST, instance=shipment)
+ if formset.is_valid():
+ shipment = form.save()
+ msg = _("Bulk return saved")
+ formset.save()
+ if "confirm" in request.POST.keys():
+ try:
+ shipment.register_bulk_return(request.user)
+ msg = _(u"Bulk return %s submitted") % shipment.return_id
+ messages.success(request, msg)
+ return redirect(view_bulk_return, shipment.pk)
+ except Exception, e:
+ messages.error(request, e)
+ return redirect(edit_bulk_return, ship_to=ship_to)
+ messages.success(request, msg)
+ return redirect(edit_bulk_return, ship_to=ship_to)
+ else:
+ messages.error(request, formset.errors)
+ else:
+ messages.error(request, form.errors)
+
+ counts = prep_counts()
+ counts['pending_return'] = len(formset)
+ title = _(u"%d parts pending return") % part_count
+ return render(request, "shipments/edit_bulk_return.html", locals())
+
+
+def remove_from_return(request, pk, part_pk):
+ """
+ Removes a part from a bulk return
+ """
+ shipment = Shipment.objects.get(pk=pk)
+ part = ServicePart.objects.get(pk=part_pk)
+
+ try:
+ shipment.toggle_part(part)
+ messages.success(request, _(u"Part %s removed from bulk return") % part.part_number)
+ except Exception, e:
+ messages.error(request, e)
+
+ return redirect(edit_bulk_return)
+
+
+def add_to_return(request, pk, part=None):
+ """
+ Adds a part to a bulk return
+ """
+ data = {'action': request.path}
+
+ if pk and part:
+ shipment = Shipment.objects.get(pk=pk)
+ part = ServicePart.objects.get(pk=part)
+ shipment.servicepart_set.add(part)
+ messages.success(request, _(u"Part %s added to return") % part.part_number)
+
+ return redirect(edit_bulk_return)
+
+ if request.method == "POST":
+ query = request.POST.get('q')
+ results = ServicePart.objects.filter(return_order=query)
+ data = {'shipment': pk, 'results': results}
+
+ return render(request, "shipments/add_to_return-results.html", data)
+
+ return render(request, "shipments/add_to_return.html", data)
+
+
+def update_part(request, part, return_type):
+ """
+ Update part status to GSX
+ """
+ return_type = int(return_type)
+ part = ServicePart.objects.get(pk=part)
+
+ msg = ""
+ form = ""
+ title = ""
+
+ if return_type == Shipment.RETURN_DOA:
+ title = _("Return DOA Part")
+ form = DoaPartReturnForm(part=part)
+
+ if return_type == Shipment.RETURN_GPR:
+ title = _("Return Good Part")
+ form = GoodPartReturnForm()
+
+ if return_type == Shipment.RETURN_CTS:
+ title = _("Convert to Stock")
+ msg = _("This part will be converted to regular inventory")
+ form = ConvertToStockForm(initial={'partNumber': part.part_number})
+
+ if request.method == "POST":
+
+ if return_type == Shipment.RETURN_DOA:
+ form = DoaPartReturnForm(part=part, data=request.POST)
+ if return_type == Shipment.RETURN_GPR:
+ form = GoodPartReturnForm(request.POST)
+ if return_type == Shipment.RETURN_CTS:
+ form = ConvertToStockForm(request.POST)
+
+ if form.is_valid():
+ try:
+ part.update_part(form.cleaned_data, return_type, request.user)
+ messages.success(request, _("Part updated"))
+ except Exception, e:
+ messages.error(request, e)
+ else:
+ messages.error(request, form.errors)
+
+ return redirect(part.order_item.order)
+
+ action = request.path
+ return render(request, "shipments/update_part.html", locals())
+
+
+def parts_pending_return(request, ship_to):
+ """
+ Returns the part pending return for this GSX Account
+ """
+ pass
+
+
+def verify(request, pk):
+ shipment = Shipment.objects.get(pk=pk)
+ return redirect(shipment)
diff --git a/servo/views/stats.py b/servo/views/stats.py
new file mode 100644
index 0000000..5ca66f8
--- /dev/null
+++ b/servo/views/stats.py
@@ -0,0 +1,443 @@
+# -*- 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 json
+import datetime
+from datetime import timedelta
+from django.utils import timezone
+
+from django import forms
+from django.db import connection
+from django.shortcuts import render
+from django.http import HttpResponse
+from django.utils.translation import ugettext as _
+from django.views.decorators.cache import cache_page
+
+from servo.stats.forms import *
+from servo.stats.queries import *
+
+from servo.models import User, Order
+
+
+class ServoTimeDelta:
+ def __init__(self, td):
+ self.td = td
+
+ def days(self):
+ return self.td.days
+
+ def workdays(self):
+ pass
+
+ def hours(self):
+ return self.td.seconds//3600
+
+ def nonzero(self):
+ return self.hours() > 0
+
+
+def prep_view(request):
+ """
+ Prepares the stats view
+ """
+ title = _('Statistics')
+ profile = request.user
+ location = request.user.location
+
+ initial = {
+ 'location' : location.pk,
+ 'end_date' : str(default_end_date),
+ 'start_date': str(default_start_date),
+ 'timescale' : default_timescale,
+ }
+
+ group = request.user.get_group()
+
+ if group:
+ initial['group'] = group.pk
+
+ # Remember the previous stats filter
+ if request.session.get('stats_filter'):
+ initial.update(request.session['stats_filter'])
+
+ request.session['stats_filter'] = initial
+
+ return locals()
+
+
+def index(request):
+ """
+ /stats/
+ """
+ data = prep_view(request)
+ form = TechieStatsForm(initial=data['initial'])
+
+ if request.method == 'POST':
+ form = TechieStatsForm(request.POST, initial=data['initial'])
+ if form.is_valid():
+ request.session['stats_filter'] = form.cleaned_data
+
+ data['form'] = form
+ return render(request, "stats/index.html", data)
+
+
+#@cache_page(15*60)
+def data(request, query):
+ result = []
+ stats = StatsManager()
+ cursor = connection.cursor()
+ report, what = query.split('/')
+
+ locations = request.user.locations
+ params = request.session['stats_filter']
+ timescale = params.get('timescale', default_timescale)
+ location = params.get('location', request.user.location)
+
+ if params.get('location'):
+ location = Location.objects.get(pk=params['location'])
+ else:
+ location = request.user.location
+
+ try:
+ location_id = location.pk
+ except AttributeError:
+ location_id = 0
+
+ start_date = params.get('start_date', default_start_date)
+ end_date = params.get('end_date', default_end_date)
+ queues = request.user.queues.all()
+
+ try:
+ users = params.get('group').user_set
+ except AttributeError:
+ users = User.objects.filter(location=location)
+
+ if report == "sales":
+ if what == "invoices":
+ for i in queues:
+ data = stats.sales_invoices(timescale, i.pk, start_date, end_date)
+ result.append({'label': i.title, 'data': data})
+
+ if what == "purchases":
+ for i in queues:
+ data = stats.sales_purchases(timescale, i.pk, start_date, end_date)
+ result.append({'label': i.title, 'data': data})
+
+ if what == "parts":
+ i = 0
+ data = []
+ labels = []
+ results = stats.sales_parts_per_labtier(start_date, end_date)
+ for r in results:
+ data.append([i, r[1]])
+ labels.append([i, r[0]])
+ i += 1
+
+ result.append({'label': labels, 'data': data})
+
+ if what == "personal":
+ location_id = request.user.get_location().id
+ users = User.objects.filter(pk=request.user.pk)
+
+ for i in users.filter(is_active=True):
+ data = stats.order_runrate(timescale, location_id, i.pk, start_date, end_date)
+ result.append({'label': i.get_full_name(), 'data': data})
+
+ if what == "runrate":
+ for i in users.filter(is_active=True):
+ data = stats.order_runrate(timescale, location_id, i.pk, start_date, end_date)
+ result.append({'label': i.get_full_name(), 'data': data})
+
+ if report == "created":
+ if what == "user":
+ for i in location.user_set.all():
+ data = stats.orders_created_by(timescale,
+ location_id,
+ i.pk,
+ start_date,
+ end_date)
+ result.append({'label': i.get_full_name(), 'data': data})
+
+ if what == "location":
+ for i in locations.all():
+ data = stats.orders_created_at(timescale, i.pk, start_date, end_date)
+ result.append({'label': i.title, 'data': data})
+
+ if report == "closed":
+ if what == "location":
+ for i in locations.all():
+ data = stats.orders_closed_at(timescale, i.pk, start_date, end_date)
+ result.append({'label': i.title, 'data': data})
+ if what == "queue":
+ for i in queues:
+ data = stats.orders_closed_in(
+ timescale,
+ location.pk,
+ i.pk,
+ start_date,
+ end_date)
+ result.append({'label': i.title, 'data': data})
+
+ if what == "count":
+ for i in queues:
+ data = stats.order_count(timescale, location_id, i.pk, start_date, end_date)
+ result.append({'label': i.title, 'data': data})
+
+ if report == "status":
+ try:
+ status = params.get('status').title
+ except AttributeError:
+ return HttpResponse(json.dumps(result))
+
+ if what == "location":
+ for i in locations.all():
+ data = stats.statuses_per_location(
+ timescale,
+ i.pk,
+ status,
+ start_date,
+ end_date)
+ result.append({'label': i.title, 'data': data})
+ if what == "tech":
+ for i in User.objects.filter(location=location, is_active=True):
+ data = stats.statuses_per_user(
+ timescale,
+ i.pk,
+ status,
+ start_date,
+ end_date)
+ result.append({'label': i.get_name(), 'data': data})
+
+ if report == "turnaround":
+ if what == "location":
+ for i in locations.all():
+ data = stats.turnaround_per_location(
+ timescale,
+ i.pk,
+ start_date,
+ end_date)
+ result.append({'label': i.title, 'data': data})
+
+ if report == "runrate":
+ if what == "location":
+ for i in locations.all():
+ data = stats.runrate_per_location(
+ timescale,
+ i.pk,
+ start_date,
+ end_date)
+ result.append({'label': i.title, 'data': data})
+
+ if report == "distribution":
+ if what == "location":
+ result = stats.distribution_per_location(start_date, end_date)
+
+ if what == "turnaround":
+ for i in queues:
+ data = stats.order_turnaround(
+ timescale,
+ location_id,
+ i.pk,
+ start_date,
+ end_date
+ )
+ result.append({'label': i.title, 'data': data})
+
+ if what == "queues":
+ cursor.execute("""SELECT q.title, COUNT(*)
+ FROM servo_order o LEFT OUTER JOIN servo_queue q on (o.queue_id = q.id)
+ WHERE (o.created_at, o.created_at) OVERLAPS (%s, %s)
+ GROUP BY q.title""", [start_date, end_date])
+
+ for k, v in cursor.fetchall():
+ k = k or _('No Queue')
+ result.append({'label': k, 'data': v})
+
+ if what == "techs":
+ for i in users.filter(is_active=True):
+ cursor.execute("""SELECT COUNT(*) as p
+ FROM servo_order o
+ WHERE user_id = %s
+ AND location_id = %s
+ AND (created_at, created_at) OVERLAPS (%s, %s)
+ GROUP BY user_id""", [i.pk, location_id, start_date, end_date])
+
+ for v in cursor.fetchall():
+ result.append({'label': i.username, 'data': v})
+
+ return HttpResponse(json.dumps(result))
+
+
+def sales(request):
+ data = prep_view(request)
+ form = InvoiceStatsForm(initial=data['initial'])
+
+ if request.method == 'POST':
+ form = InvoiceStatsForm(request.POST, initial=data['initial'])
+ if form.is_valid():
+ request.session['stats_filter'] = form.cleaned_data
+
+ data['form'] = form
+ return render(request, "stats/sales.html", data)
+
+
+def queues(request):
+ data = prep_view(request)
+ form = OrderStatsForm(initial=data['initial'])
+ if request.method == 'POST':
+ form = OrderStatsForm(request.POST, initial=data['initial'])
+ if form.is_valid():
+ request.session['stats_filter'] = form.cleaned_data
+
+ data['form'] = form
+ return render(request, "stats/queues.html", data)
+
+
+def locations(request):
+ data = prep_view(request)
+ form = BasicStatsForm(initial=data['initial'])
+ if request.method == 'POST':
+ form = BasicStatsForm(request.POST, initial=data['initial'])
+ if form.is_valid():
+ request.session['stats_filter'] = form.cleaned_data
+ data['form'] = form
+ return render(request, "stats/locations.html", data)
+
+
+def statuses(request):
+ data = prep_view(request)
+ form = StatusStatsForm(initial=data['initial'])
+ if request.method == 'POST':
+ form = StatusStatsForm(request.POST, initial=data['initial'])
+ if form.is_valid():
+ request.session['stats_filter'] = form.cleaned_data
+
+ data['form'] = form
+ return render(request, "stats/statuses.html", data)
+
+
+def repairs(request):
+ title = _('Repair statistics')
+ form = NewStatsForm(initial={
+ 'location': [request.user.location],
+ 'queue': request.user.queues.all()
+ })
+
+ if request.GET.get('location'):
+ results = []
+ form = NewStatsForm(request.GET)
+ totals = {
+ 'created' : 0,
+ 'assigned' : 0,
+ 'repairs' : 0,
+ 'dispatched' : 0,
+ 'tmp_orders' : [],
+ 'turnaround' : timedelta(),
+ }
+
+ if not form.is_valid():
+ return render(request, "stats/newstats.html", locals())
+
+ cdata = form.cleaned_data
+ date_range = (cdata['start_date'], cdata['end_date'])
+
+ for u in User.active.filter(location=cdata['location']):
+ r = {'name': u.get_full_name()}
+
+ # Look at invoices first because that data may be different from
+ # assignment info (tech A startx, tech B finishes)
+ dispatched = u.invoice_set.filter(
+ order__queue=cdata['queue'],
+ order__location=cdata['location'],
+ created_at__range=date_range
+ )
+
+ if len(cdata.get('label')):
+ dispatched = dispatched.filter(order__tags=cdata['label'])
+
+ # Count each case's dispatch only once
+ r['dispatched'] = dispatched.values('order_id').distinct().count()
+
+ created = u.created_orders.filter(
+ queue=cdata['queue'],
+ location=cdata['location'],
+ created_at__range=date_range
+ )
+
+ if len(cdata.get('label')):
+ created = created.filter(tags=cdata['label'])
+
+ r['created'] = created.count()
+ totals['created'] += r['created'] # add amount to totals
+
+ assigned = u.order_set.filter(
+ queue=cdata['queue'],
+ location=cdata['location'],
+ started_at__range=date_range
+ )
+
+ if len(cdata.get('label')):
+ assigned = assigned.filter(tags=cdata['label'])
+
+ r['assigned'] = assigned.count()
+
+ if (r['assigned'] < 1) and (r['dispatched'] < 1):
+ continue # ... only continue with actual techs
+
+ repairs = u.created_repairs.filter(
+ order__queue=cdata['queue'],
+ order__location=cdata['location'],
+ submitted_at__range=date_range
+ )
+
+ if len(cdata.get('label')):
+ repairs = repairs.filter(order__tags=cdata['label'])
+
+ # Only count each case's GSX repair once
+ r['repairs'] = repairs.values('order_id').distinct().count()
+
+ totals['repairs'] += r['repairs']
+ totals['assigned'] += r['assigned']
+ totals['dispatched'] += r['dispatched']
+
+ results.append(r)
+ turnaround = timedelta()
+
+ # calculate turnaround time of dispatched cases
+ for o in dispatched:
+ totals['tmp_orders'].append(o.order)
+ for s in o.order.orderstatus_set.filter(status=cdata['status']):
+ if s.finished_at is None:
+ s.finished_at = s.order.closed_at or timezone.now()
+
+ totals['turnaround'] += (s.finished_at - s.started_at)
+
+ totals['diff'] = totals['dispatched'] - totals['assigned']
+
+ if totals['dispatched'] > 0:
+ totals['turnaround'] = ServoTimeDelta(totals['turnaround']/totals['dispatched'])
+
+ return render(request, "stats/newstats.html", locals())
diff --git a/servo/views/tags.py b/servo/views/tags.py
new file mode 100644
index 0000000..3dea6d4
--- /dev/null
+++ b/servo/views/tags.py
@@ -0,0 +1,37 @@
+# -*- 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.http import HttpResponse
+from servo.models import TaggedItem
+
+
+def clear(request, pk):
+ TaggedItem.objects.get(pk=pk).delete()
+ return HttpResponse("")
+
+
+def add(request, content_type, pk, tag):
+ pass