diff --git a/PyRIGS/decorators.py b/PyRIGS/decorators.py index a7c1db90..67d8964c 100644 --- a/PyRIGS/decorators.py +++ b/PyRIGS/decorators.py @@ -58,7 +58,7 @@ def api_key_required(function): try: user_object = models.Profile.objects.get(pk=userid) - except Profile.DoesNotExist: + except models.Profile.DoesNotExist: return error_resp if user_object.api_key != key: diff --git a/PyRIGS/settings.py b/PyRIGS/settings.py index d85ff887..3098c3fb 100644 --- a/PyRIGS/settings.py +++ b/PyRIGS/settings.py @@ -12,8 +12,6 @@ https://docs.djangoproject.com/en/1.7/ref/settings/ import os BASE_DIR = os.path.dirname(os.path.dirname(__file__)) -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') - # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ @@ -27,6 +25,10 @@ TEMPLATE_DEBUG = True ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com'] +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +if not DEBUG: + SECURE_SSL_REDIRECT = True # Redirect all http requests to https + INTERNAL_IPS = ['127.0.0.1'] ADMINS = ( @@ -55,6 +57,7 @@ INSTALLED_APPS = ( MIDDLEWARE_CLASSES = ( 'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware', + 'django.middleware.security.SecurityMiddleware', 'reversion.middleware.RevisionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', diff --git a/RIGS/finance.py b/RIGS/finance.py index 4674e5dd..b945dfa2 100644 --- a/RIGS/finance.py +++ b/RIGS/finance.py @@ -1,33 +1,42 @@ import cStringIO as StringIO +import datetime +import re +from django.contrib import messages from django.core.urlresolvers import reverse_lazy -from django.db import connection from django.http import Http404, HttpResponseRedirect -from django.views import generic -from django.template import RequestContext -from django.template.loader import get_template from django.http import HttpResponse from django.shortcuts import get_object_or_404 -from django.contrib import messages -import datetime +from django.template import RequestContext +from django.template.loader import get_template +from django.views import generic +from django.db.models import Q from z3c.rml import rml2pdf from django.db.models import Q from RIGS import models -import re class InvoiceIndex(generic.ListView): model = models.Invoice - template_name = 'RIGS/invoice_list.html' + template_name = 'RIGS/invoice_list_active.html' + + def get_context_data(self, **kwargs): + context = super(InvoiceIndex, self).get_context_data(**kwargs) + total = 0 + for i in context['object_list']: + total += i.balance + context['total'] = total + context['count'] = len(list(context['object_list'])) + return context def get_queryset(self): # Manual query is the only way I have found to do this efficiently. Not ideal but needs must sql = "SELECT * FROM " \ "(SELECT " \ - "(SELECT COUNT(p.amount) FROM \"RIGS_payment\" as p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \ + "(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \ "(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \ - "(SELECT SUM(p.amount) FROM \"RIGS_payment\" as p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \ + "(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \ "\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \ "AS sub " \ "WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \ @@ -41,6 +50,7 @@ class InvoiceIndex(generic.ListView): class InvoiceDetail(generic.DetailView): model = models.Invoice + class InvoicePrint(generic.View): def get(self, request, pk): invoice = get_object_or_404(models.Invoice, pk=pk) @@ -55,8 +65,8 @@ class InvoicePrint(generic.View): 'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF', } }, - 'invoice':invoice, - 'current_user':request.user, + 'invoice': invoice, + 'current_user': request.user, }) rml = template.render(context) @@ -73,6 +83,7 @@ class InvoicePrint(generic.View): response.write(pdfData) return response + class InvoiceVoid(generic.View): def get(self, *args, **kwargs): pk = kwargs.get('pk') @@ -87,6 +98,7 @@ class InvoiceVoid(generic.View): class InvoiceArchive(generic.ListView): model = models.Invoice + template_name = 'RIGS/invoice_list_archive.html' paginate_by = 25 template_name="RIGS/invoice_archive.html" @@ -122,17 +134,36 @@ class InvoiceArchive(generic.ListView): class InvoiceWaiting(generic.ListView): model = models.Event - paginate_by = 25 + # paginate_by = 25 template_name = 'RIGS/event_invoice.html' + def get_context_data(self, **kwargs): + context = super(InvoiceWaiting, self).get_context_data(**kwargs) + total = 0 + for obj in self.get_objects(): + total += obj.sum_total + context['total'] = total + context['count'] = len(self.get_objects()) + return context + def get_queryset(self): + return self.get_objects() + + def get_objects(self): # @todo find a way to select items - events = self.model.objects.filter(is_rig=True, end_date__lt=datetime.date.today(), - invoice__isnull=True) \ - .order_by('start_date') \ + events = self.model.objects.filter( + ( + Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end + Q(end_date__lte=datetime.date.today()) # Has end date, finishes before + ) & Q(invoice__isnull=True) # Has not already been invoiced + & Q(is_rig=True) # Is a rig (not non-rig) + + ).order_by('start_date') \ .select_related('person', 'organisation', - 'venue', 'mic') + 'venue', 'mic') \ + .prefetch_related('items') + return events @@ -144,13 +175,14 @@ class InvoiceEvent(generic.View): if created: invoice.invoice_date = datetime.date.today() + messages.success(self.request, 'Invoice created successfully') return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': invoice.pk})) class PaymentCreate(generic.CreateView): model = models.Payment - fields = ['invoice','date','amount','method'] + fields = ['invoice', 'date', 'amount', 'method'] def get_initial(self): initial = super(generic.CreateView, self).get_initial() @@ -170,4 +202,4 @@ class PaymentDelete(generic.DeleteView): model = models.Payment def get_success_url(self): - return self.request.POST.get('next') \ No newline at end of file + return self.request.POST.get('next') diff --git a/RIGS/models.py b/RIGS/models.py index fc06911f..0650d81c 100644 --- a/RIGS/models.py +++ b/RIGS/models.py @@ -1,31 +1,32 @@ +import datetime import hashlib -import datetime, pytz - -from django.db import models, connection -from django.contrib.auth.models import AbstractUser -from django.conf import settings -from django.utils.functional import cached_property -from django.utils.encoding import python_2_unicode_compatible -import reversion -import string +import pytz import random +import string from collections import Counter -from django.core.urlresolvers import reverse_lazy -from django.core.exceptions import ValidationError - from decimal import Decimal +import reversion +from django.conf import settings +from django.contrib.auth.models import AbstractUser +from django.core.exceptions import ValidationError +from django.core.urlresolvers import reverse_lazy +from django.db import models +from django.utils.encoding import python_2_unicode_compatible +from django.utils.functional import cached_property + + # Create your models here. @python_2_unicode_compatible class Profile(AbstractUser): initials = models.CharField(max_length=5, unique=True, null=True, blank=False) phone = models.CharField(max_length=13, null=True, blank=True) - api_key = models.CharField(max_length=40,blank=True,editable=False, null=True) + api_key = models.CharField(max_length=40, blank=True, editable=False, null=True) @classmethod def make_api_key(cls): - size=20 - chars=string.ascii_letters + string.digits + size = 20 + chars = string.ascii_letters + string.digits new_api_key = ''.join(random.choice(chars) for x in range(size)) return new_api_key; @@ -55,6 +56,7 @@ class Profile(AbstractUser): ('view_profile', 'Can view Profile'), ) + class RevisionMixin(object): @property def last_edited_at(self): @@ -79,10 +81,11 @@ class RevisionMixin(object): versions = reversion.get_for_object(self) if versions: version = reversion.get_for_object(self)[0] - return "V{0} | R{1}".format(version.pk,version.revision.pk) + return "V{0} | R{1}".format(version.pk, version.revision.pk) else: return None + @reversion.register @python_2_unicode_compatible class Person(models.Model, RevisionMixin): @@ -97,7 +100,7 @@ class Person(models.Model, RevisionMixin): def __str__(self): string = self.name if self.notes is not None: - if len(self.notes) > 0: + if len(self.notes) > 0: string += "*" return string @@ -108,7 +111,7 @@ class Person(models.Model, RevisionMixin): if e.organisation: o.append(e.organisation) - #Count up occurances and put them in descending order + # Count up occurances and put them in descending order c = Counter(o) stats = c.most_common() return stats @@ -141,7 +144,7 @@ class Organisation(models.Model, RevisionMixin): def __str__(self): string = self.name if self.notes is not None: - if len(self.notes) > 0: + if len(self.notes) > 0: string += "*" return string @@ -151,8 +154,8 @@ class Organisation(models.Model, RevisionMixin): for e in Event.objects.filter(organisation=self).select_related('person'): if e.person: p.append(e.person) - - #Count up occurances and put them in descending order + + # Count up occurances and put them in descending order c = Counter(p) stats = c.most_common() return stats @@ -238,12 +241,18 @@ class Venue(models.Model, RevisionMixin): class EventManager(models.Manager): def current_events(self): events = self.filter( - (models.Q(start_date__gte=datetime.date.today(), end_date__isnull=True, dry_hire=False) & ~models.Q(status=Event.CANCELLED)) | # Starts after with no end - (models.Q(end_date__gte=datetime.date.today(), dry_hire=False) & ~models.Q(status=Event.CANCELLED)) | # Ends after - (models.Q(dry_hire=True, start_date__gte=datetime.date.today()) & ~models.Q(status=Event.CANCELLED)) | # Active dry hire - (models.Q(dry_hire=True, checked_in_by__isnull=True) & (models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT + (models.Q(start_date__gte=datetime.date.today(), end_date__isnull=True, dry_hire=False) & ~models.Q( + status=Event.CANCELLED)) | # Starts after with no end + (models.Q(end_date__gte=datetime.date.today(), dry_hire=False) & ~models.Q( + status=Event.CANCELLED)) | # Ends after + (models.Q(dry_hire=True, start_date__gte=datetime.date.today()) & ~models.Q( + status=Event.CANCELLED)) | # Active dry hire + (models.Q(dry_hire=True, checked_in_by__isnull=True) & ( + models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT models.Q(status=Event.CANCELLED, start_date__gte=datetime.date.today()) # Canceled but not started - ).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic') + ).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', + 'organisation', + 'venue', 'mic') return events def events_in_bounds(self, start, end): @@ -251,15 +260,17 @@ class EventManager(models.Manager): (models.Q(start_date__gte=start.date(), start_date__lte=end.date())) | # Start date in bounds (models.Q(end_date__gte=start.date(), end_date__lte=end.date())) | # End date in bounds (models.Q(access_at__gte=start, access_at__lte=end)) | # Access at in bounds - (models.Q(meet_at__gte=start, meet_at__lte=end)) | # Meet at in bounds + (models.Q(meet_at__gte=start, meet_at__lte=end)) | # Meet at in bounds - (models.Q(start_date__lte=start, end_date__gte=end)) | # Start before, end after - (models.Q(access_at__lte=start, start_date__gte=end)) | # Access before, start after - (models.Q(access_at__lte=start, end_date__gte=end)) | # Access before, end after - (models.Q(meet_at__lte=start, start_date__gte=end)) | # Meet before, start after - (models.Q(meet_at__lte=start, end_date__gte=end)) # Meet before, end after + (models.Q(start_date__lte=start, end_date__gte=end)) | # Start before, end after + (models.Q(access_at__lte=start, start_date__gte=end)) | # Access before, start after + (models.Q(access_at__lte=start, end_date__gte=end)) | # Access before, end after + (models.Q(meet_at__lte=start, start_date__gte=end)) | # Meet before, start after + (models.Q(meet_at__lte=start, end_date__gte=end)) # Meet before, end after - ).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic') + ).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', + 'organisation', + 'venue', 'mic') return events def rig_count(self): @@ -301,7 +312,8 @@ class Event(models.Model, RevisionMixin): status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL) dry_hire = models.BooleanField(default=False) is_rig = models.BooleanField(default=True) - based_on = models.ForeignKey('Event', on_delete=models.SET_NULL, related_name='future_events', blank=True, null=True) + based_on = models.ForeignKey('Event', on_delete=models.SET_NULL, related_name='future_events', blank=True, + null=True) # Timing start_date = models.DateField() @@ -327,6 +339,7 @@ class Event(models.Model, RevisionMixin): """ EX Vat """ + @property def sum_total(self): # Manual querying is required for efficiency whilst maintaining floating point arithmetic @@ -334,14 +347,15 @@ class Event(models.Model, RevisionMixin): # sql = "SELECT SUM(quantity * cost) AS sum_total FROM \"RIGS_eventitem\" WHERE event_id=%i" % self.id # else: # sql = "SELECT id, SUM(quantity * cost) AS sum_total FROM RIGS_eventitem WHERE event_id=%i" % self.id - #total = self.items.raw(sql)[0] - #if total.sum_total: + # total = self.items.raw(sql)[0] + # if total.sum_total: # return total.sum_total - #total = 0.0 - #for item in self.items.filter(cost__gt=0).extra(select="SUM(cost * quantity) AS sum"): + # total = 0.0 + # for item in self.items.filter(cost__gt=0).extra(select="SUM(cost * quantity) AS sum"): # total += item.sum total = EventItem.objects.filter(event=self).aggregate( - sum_total=models.Sum(models.F('cost')*models.F('quantity'), output_field=models.DecimalField(max_digits=10, decimal_places=2)) + sum_total=models.Sum(models.F('cost') * models.F('quantity'), + output_field=models.DecimalField(max_digits=10, decimal_places=2)) )['sum_total'] if total: return total @@ -358,6 +372,7 @@ class Event(models.Model, RevisionMixin): """ Inc VAT """ + @property def total(self): return self.sum_total + self.vat @@ -382,7 +397,7 @@ class Event(models.Model, RevisionMixin): def earliest_time(self): """Finds the earliest time defined in the event - this function could return either a tzaware datetime, or a naiive date object""" - #Put all the datetimes in a list + # Put all the datetimes in a list datetime_list = [] if self.access_at: @@ -394,22 +409,22 @@ class Event(models.Model, RevisionMixin): # If there is no start time defined, pretend it's midnight startTimeFaked = False if self.has_start_time: - startDateTime = datetime.datetime.combine(self.start_date,self.start_time) + startDateTime = datetime.datetime.combine(self.start_date, self.start_time) else: - startDateTime = datetime.datetime.combine(self.start_date,datetime.time(00,00)) + startDateTime = datetime.datetime.combine(self.start_date, datetime.time(00, 00)) startTimeFaked = True - #timezoneIssues - apply the default timezone to the naiive datetime + # timezoneIssues - apply the default timezone to the naiive datetime tz = pytz.timezone(settings.TIME_ZONE) startDateTime = tz.localize(startDateTime) - datetime_list.append(startDateTime) # then add it to the list + datetime_list.append(startDateTime) # then add it to the list - earliest = min(datetime_list).astimezone(tz) #find the earliest datetime in the list + earliest = min(datetime_list).astimezone(tz) # find the earliest datetime in the list # if we faked it & it's the earliest, better own up - if startTimeFaked and earliest==startDateTime: + if startTimeFaked and earliest == startDateTime: return self.start_date - + return earliest @property @@ -421,7 +436,7 @@ class Event(models.Model, RevisionMixin): endDate = self.start_date if self.has_end_time: - endDateTime = datetime.datetime.combine(endDate,self.end_time) + endDateTime = datetime.datetime.combine(endDate, self.end_time) tz = pytz.timezone(settings.TIME_ZONE) endDateTime = tz.localize(endDateTime) @@ -430,7 +445,6 @@ class Event(models.Model, RevisionMixin): else: return endDate - objects = EventManager() def get_absolute_url(self): @@ -446,7 +460,7 @@ class Event(models.Model, RevisionMixin): startEndSameDay = not self.end_date or self.end_date == self.start_date hasStartAndEnd = self.has_start_time and self.has_end_time if startEndSameDay and hasStartAndEnd and self.start_time > self.end_time: - raise ValidationError('Unless you\'ve invented time travel, the event can\'t finish before it has started.') + raise ValidationError('Unless you\'ve invented time travel, the event can\'t finish before it has started.') def save(self, *args, **kwargs): """Call :meth:`full_clean` before saving.""" @@ -503,15 +517,6 @@ class Invoice(models.Model): @property def payment_total(self): - # Manual querying is required for efficiency whilst maintaining floating point arithmetic - #if connection.vendor == 'postgresql': - # sql = "SELECT SUM(amount) AS total FROM \"RIGS_payment\" WHERE invoice_id=%i" % self.id - #else: - # sql = "SELECT id, SUM(amount) AS total FROM RIGS_payment WHERE invoice_id=%i" % self.id - #total = self.payment_set.raw(sql)[0] - #if total.total: - # return total.total - #return 0.0 total = self.payment_set.aggregate(total=models.Sum('amount'))['total'] if total: return total @@ -521,6 +526,10 @@ class Invoice(models.Model): def balance(self): return self.sum_total - self.payment_total + @property + def is_closed(self): + return self.balance == 0 or self.void + def __str__(self): return "%i: %s (%.2f)" % (self.pk, self.event, self.balance) @@ -552,4 +561,4 @@ class Payment(models.Model): method = models.CharField(max_length=2, choices=METHODS, null=True, blank=True) def __str__(self): - return "%s: %d" % (self.get_method_display(), self.amount) \ No newline at end of file + return "%s: %d" % (self.get_method_display(), self.amount) diff --git a/RIGS/templates/RIGS/event_detail.html b/RIGS/templates/RIGS/event_detail.html index 81658663..d4b089a9 100644 --- a/RIGS/templates/RIGS/event_detail.html +++ b/RIGS/templates/RIGS/event_detail.html @@ -25,9 +25,17 @@ class="hidden-xs">Duplicate {% if event.is_rig %} {% if perms.RIGS.add_invoice %} - Invoice + + Invoice {% endif %} {% endif %} @@ -190,9 +198,17 @@ class="hidden-xs">Duplicate {% if event.is_rig %} {% if perms.RIGS.add_invoice %} - Invoice + + Invoice {% endif %} {% endif %} @@ -227,9 +243,17 @@ class="hidden-xs">Duplicate {% if event.is_rig %} {% if perms.RIGS.add_invoice %} - Invoice + + Invoice {% endif %} {% endif %} diff --git a/RIGS/templates/RIGS/event_invoice.html b/RIGS/templates/RIGS/event_invoice.html index 805eb3d9..fcbe5e87 100644 --- a/RIGS/templates/RIGS/event_invoice.html +++ b/RIGS/templates/RIGS/event_invoice.html @@ -1,68 +1,91 @@ {% extends 'base.html' %} {% load paginator from filters %} +{% load static %} {% block title %}Events for Invoice{% endblock %} +{% block js %} + + +{% endblock %} + {% block content %}
These events have happened, but paperwork has not yet been sent to treasury
{% if is_paginated %}| # | -Date | -Event | -Client | -Cost | -MIC | -- | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| N{{ object.pk|stringformat:"05d" }} | -{{ object.end_date }} | -{{ object.name }} | -- {% if object.organisation %} - {{ object.organisation.name }} - {% else %} - {{ object.person.name }} - {% endif %} - | -{{ object.sum_total|floatformat:2 }} | -
- {{ object.mic.initials }} - |
- - - - - | +
| Event # | +Start Date | +Event Name | +Client | +Cost | +MIC | +
|---|
| # | +# | Event Date | Event Details | Event Timings | @@ -23,7 +23,7 @@ danger {% endif %} "> -{{ event.pk }} | +{{ event.pk }} |
{{ event.start_date|date:"D d/m/Y" }}
{% if event.end_date and event.end_date != event.start_date %}
diff --git a/RIGS/templates/RIGS/invoice_detail.html b/RIGS/templates/RIGS/invoice_detail.html
index a3be5320..af26ad21 100644
--- a/RIGS/templates/RIGS/invoice_detail.html
+++ b/RIGS/templates/RIGS/invoice_detail.html
@@ -38,8 +38,11 @@
-
- Event Details
+
+ Event Details
+ {% if object.void %}(VOID){% elif object.is_closed %}(PAID){% else %}(OUTSTANDING){% endif %}
+
+
|
{% endfor %}
+
| Balance: | +{{ object.balance|floatformat:2 }} | ++ |
| # | -Event | -Invoice Date | -Balance | -- | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| {{ object.pk }} | -N{{ object.event.pk|stringformat:"05d" }}: {{ object.event.name }} | -{{ object.invoice_date }} | -{{ object.balance|floatformat:2 }} | -- - - - | +
| Invoice # | +Event | +Client | +Event Date | +Invoice Date | +Balance | +
|---|
Paperwork for these events has been sent to treasury, but the full balance has not yet appeared on a ledger
+{% endblock %} \ No newline at end of file diff --git a/RIGS/templates/RIGS/invoice_list_archive.html b/RIGS/templates/RIGS/invoice_list_archive.html new file mode 100644 index 00000000..77bab204 --- /dev/null +++ b/RIGS/templates/RIGS/invoice_list_archive.html @@ -0,0 +1,13 @@ +{% extends 'RIGS/invoice_list.html' %} + +{% block title %} +Invoice Archive +{% endblock %} + +{% block heading %} +All Invoices +{% endblock %} + +{% block description %} +This page displays all invoices: outstanding, paid, and void
+{% endblock %} \ No newline at end of file diff --git a/RIGS/templates/RIGS/organisation_list.html b/RIGS/templates/RIGS/organisation_list.html index 2c88408a..c8856886 100644 --- a/RIGS/templates/RIGS/organisation_list.html +++ b/RIGS/templates/RIGS/organisation_list.html @@ -45,7 +45,7 @@No API Key Generateddiff --git a/RIGS/templates/RIGS/venue_list.html b/RIGS/templates/RIGS/venue_list.html index cf89686c..88ae61ac 100644 --- a/RIGS/templates/RIGS/venue_list.html +++ b/RIGS/templates/RIGS/venue_list.html @@ -45,7 +45,7 @@