mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-01-17 05:22:16 +00:00
Merge branch 'master' into search
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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')
|
||||
return self.request.POST.get('next')
|
||||
|
||||
129
RIGS/models.py
129
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)
|
||||
return "%s: %d" % (self.get_method_display(), self.amount)
|
||||
|
||||
@@ -25,9 +25,17 @@
|
||||
class="hidden-xs">Duplicate</span></a>
|
||||
{% if event.is_rig %}
|
||||
{% if perms.RIGS.add_invoice %}
|
||||
<a href="{% url 'invoice_event' event.pk %}" class="btn btn-default" title="Invoice Rig"><span
|
||||
class="glyphicon glyphicon-gbp"></span> <span
|
||||
class="hidden-xs">Invoice</span></a>
|
||||
<a id="invoiceDropdownLabel" href="{% url 'invoice_event' event.pk %}" class="btn
|
||||
{% if event.invoice and event.invoice.is_closed %}
|
||||
btn-success
|
||||
{% elif event.invoice %}
|
||||
btn-warning
|
||||
{% else %}
|
||||
btn-danger
|
||||
{% endif %}
|
||||
" title="Invoice Rig"><span
|
||||
class="glyphicon glyphicon-gbp"></span>
|
||||
<span class="hidden-xs">Invoice</span></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -190,9 +198,17 @@
|
||||
class="hidden-xs">Duplicate</span></a>
|
||||
{% if event.is_rig %}
|
||||
{% if perms.RIGS.add_invoice %}
|
||||
<a href="{% url 'invoice_event' event.pk %}" class="btn btn-default" title="Invoice Rig"><span
|
||||
class="glyphicon glyphicon-gbp"></span> <span
|
||||
class="hidden-xs">Invoice</span></a>
|
||||
<a id="invoiceDropdownLabel" href="{% url 'invoice_event' event.pk %}" class="btn
|
||||
{% if event.invoice and event.invoice.is_closed %}
|
||||
btn-success
|
||||
{% elif event.invoice %}
|
||||
btn-warning
|
||||
{% else %}
|
||||
btn-danger
|
||||
{% endif %}
|
||||
" title="Invoice Rig"><span
|
||||
class="glyphicon glyphicon-gbp"></span>
|
||||
<span class="hidden-xs">Invoice</span></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -227,9 +243,17 @@
|
||||
class="hidden-xs">Duplicate</span></a>
|
||||
{% if event.is_rig %}
|
||||
{% if perms.RIGS.add_invoice %}
|
||||
<a href="{% url 'invoice_event' event.pk %}" class="btn btn-default" title="Invoice Rig"><span
|
||||
class="glyphicon glyphicon-gbp"></span> <span
|
||||
class="hidden-xs">Invoice</span></a>
|
||||
<a id="invoiceDropdownLabel" href="{% url 'invoice_event' event.pk %}" class="btn
|
||||
{% if event.invoice and event.invoice.is_closed %}
|
||||
btn-success
|
||||
{% elif event.invoice %}
|
||||
btn-warning
|
||||
{% else %}
|
||||
btn-danger
|
||||
{% endif %}
|
||||
" title="Invoice Rig"><span
|
||||
class="glyphicon glyphicon-gbp"></span>
|
||||
<span class="hidden-xs">Invoice</span></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -1,68 +1,91 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load paginator from filters %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Events for Invoice{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static "js/tooltip.js" %}"></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-sm-12">
|
||||
<h2>Events for Invoice</h2>
|
||||
<h2>Events for Invoice ({{count}} Events, £ {{ total|floatformat:2 }})</h2>
|
||||
<p>These events have happened, but paperwork has not yet been sent to treasury</p>
|
||||
{% if is_paginated %}
|
||||
<div class="col-md-6 col-md-offset-6 col-sm-12 text-right">
|
||||
{% paginator %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<table class="table table-responsive table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="hiddenx-xs">#</th>
|
||||
<th>Date</th>
|
||||
<th>Event</th>
|
||||
<th>Client</th>
|
||||
<th>Cost</th>
|
||||
<th class="hidden-xs">MIC</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for object in object_list %}
|
||||
<tr class="
|
||||
{% if object.cancelled %}
|
||||
active text-muted
|
||||
{% elif not object.is_rig %}
|
||||
info
|
||||
{% elif object.confirmed and object.mic %}
|
||||
{# interpreated as (booked and mic) #}
|
||||
success
|
||||
{% elif object.mic %}
|
||||
warning
|
||||
{% else %}
|
||||
danger
|
||||
{% endif %}
|
||||
">
|
||||
<td class="hidden-xs"><a href="{% url 'event_detail' object.pk %}" target="_blank">N{{ object.pk|stringformat:"05d" }}</a></td>
|
||||
<td>{{ object.end_date }}</td>
|
||||
<td>{{ object.name }}</td>
|
||||
<td>
|
||||
{% if object.organisation %}
|
||||
{{ object.organisation.name }}
|
||||
{% else %}
|
||||
{{ object.person.name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ object.sum_total|floatformat:2 }}</td>
|
||||
<td class="text-center">
|
||||
{{ object.mic.initials }}<br/>
|
||||
<img src="{{ object.mic.profile_picture }}" class="event-mic-photo"/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url 'invoice_event' object.pk %}" target="_blank" class="btn btn-default">
|
||||
<span class="glyphicon glyphicon-gbp"></span>
|
||||
</a>
|
||||
</td>
|
||||
<div class="table-responsive col-sm-12">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Event #</th>
|
||||
<th>Start Date</th>
|
||||
<th>Event Name</th>
|
||||
<th>Client</th>
|
||||
<th>Cost</th>
|
||||
<th>MIC</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for object in object_list %}
|
||||
<tr class="
|
||||
{% if object.cancelled %}
|
||||
active text-muted
|
||||
{% elif not object.is_rig %}
|
||||
info
|
||||
{% elif object.confirmed and object.mic %}
|
||||
{# interpreated as (booked and mic) #}
|
||||
success
|
||||
{% elif object.mic %}
|
||||
warning
|
||||
{% else %}
|
||||
danger
|
||||
{% endif %}
|
||||
">
|
||||
<td><a href="{% url 'event_detail' object.pk %}">N{{ object.pk|stringformat:"05d" }}</a><br>
|
||||
<span class="text-muted">{{ object.get_status_display }}</span></td>
|
||||
<td>{{ object.start_date }}</td>
|
||||
<td>{{ object.name }}</td>
|
||||
<td>
|
||||
{% if object.organisation %}
|
||||
{{ object.organisation.name }}
|
||||
<br>
|
||||
<span class="text-muted">{{ object.organisation.union_account|yesno:'Internal,External' }}</span>
|
||||
{% else %}
|
||||
{{ object.person.name }}
|
||||
<br>
|
||||
<span class="text-muted">External</span>
|
||||
{% endif %}
|
||||
|
||||
</td>
|
||||
<td>{{ object.sum_total|floatformat:2 }}</td>
|
||||
<td class="text-center">
|
||||
{% if object.mic %}
|
||||
{{ object.mic.initials }}<br>
|
||||
<img src="{{ object.mic.profile_picture }}" class="event-mic-photo"/>
|
||||
{% else %}
|
||||
<span class="glyphicon glyphicon-exclamation-sign"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url 'invoice_event' object.pk %}" class="btn btn-default" data-toggle="tooltip" title="'Invoice' this event - click this when paperwork has been sent to treasury">
|
||||
<span class="glyphicon glyphicon-gbp"></span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if is_paginated %}
|
||||
<div class="col-md-6 col-md-offset-6 col-sm-12 text-right">
|
||||
{% paginator %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<td class="hidden-xs">#</td>
|
||||
<td>#</td>
|
||||
<td>Event Date</td>
|
||||
<td>Event Details</td>
|
||||
<td>Event Timings</td>
|
||||
@@ -23,7 +23,7 @@
|
||||
danger
|
||||
{% endif %}
|
||||
">
|
||||
<td class="hidden-xs">{{ event.pk }}</td>
|
||||
<td>{{ event.pk }}</td>
|
||||
<td>
|
||||
<div><strong>{{ event.start_date|date:"D d/m/Y" }}</strong></div>
|
||||
{% if event.end_date and event.end_date != event.start_date %}
|
||||
|
||||
@@ -38,8 +38,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="panel panel-{% if object.void %}danger{% else %}info{% endif %}">
|
||||
<div class="panel-heading">Event Details</div>
|
||||
<div class="panel panel-{% if object.is_closed %}success{% else %}warning{% endif %}">
|
||||
<div class="panel-heading">Event Details<span class="pull-right">
|
||||
{% if object.void %}(VOID){% elif object.is_closed %}(PAID){% else %}(OUTSTANDING){% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Event Number</dt>
|
||||
@@ -109,6 +112,11 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td class="text-right"><strong>Balance:</strong></td>
|
||||
<td>{{ object.balance|floatformat:2 }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -5,40 +5,71 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="col-sm-12">
|
||||
<h2>Invoices</h2>
|
||||
{% block search %}
|
||||
{% endblock %}
|
||||
<h2>{% block heading %}Invoices{% endblock %}</h2>
|
||||
{% block description %}{% endblock %}
|
||||
{% if is_paginated %}
|
||||
<div class="col-md-6 col-md-offset-6 col-sm-12 text-right">
|
||||
{% paginator %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<table class="table table-responsive table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Event</th>
|
||||
<th>Invoice Date</th>
|
||||
<th>Balance</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for object in object_list %}
|
||||
<tr class="{% if object.void %}danger{% elif object.balance == 0 %}success{% endif %}">
|
||||
<td>{{ object.pk }}</td>
|
||||
<td><a href="{% url 'event_detail' object.event.pk %}" target="_blank">N{{ object.event.pk|stringformat:"05d" }}</a>: {{ object.event.name }}</td>
|
||||
<td>{{ object.invoice_date }}</td>
|
||||
<td>{{ object.balance|floatformat:2 }}</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default">
|
||||
<span class="glyphicon glyphicon-pencil"></span>
|
||||
</a>
|
||||
</td>
|
||||
<div class="table-responsive col-sm-12">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Invoice #</th>
|
||||
<th>Event</th>
|
||||
<th>Client</th>
|
||||
<th>Event Date</th>
|
||||
<th>Invoice Date</th>
|
||||
<th>Balance</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for object in object_list %}
|
||||
<tr>
|
||||
<td class="{% if object.is_closed %}success{% else %}warning{% endif %}">{{ object.pk }}<br>
|
||||
<span class="text-muted">{% if object.void %}(VOID){% elif object.is_closed %}(PAID){% else %}(O/S){% endif %}</span></td>
|
||||
<td class="
|
||||
{% if object.event.cancelled %}
|
||||
active text-muted
|
||||
{% elif not object.event.is_rig %}
|
||||
info
|
||||
{% elif object.event.confirmed and object.event.mic %}
|
||||
{# interpreated as (booked and mic) #}
|
||||
success
|
||||
{% elif object.event.mic %}
|
||||
warning
|
||||
{% else %}
|
||||
danger
|
||||
{% endif %}
|
||||
"><a href="{% url 'event_detail' object.event.pk %}">N{{ object.event.pk|stringformat:"05d" }}</a>: {{ object.event.name }} <br>
|
||||
<span class="text-muted">{{ object.event.get_status_display }}{% if not object.event.mic %}, No MIC{% endif %}
|
||||
</span></td>
|
||||
</td>
|
||||
<td>{% if object.event.organisation %}
|
||||
{{ object.event.organisation.name }}
|
||||
<br>
|
||||
<span class="text-muted">{{ object.event.organisation.union_account|yesno:'Internal,External' }}</span>
|
||||
{% else %}
|
||||
{{ object.event.person.name }}
|
||||
<br>
|
||||
<span class="text-muted">External</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ object.event.start_date }}</td>
|
||||
<td>{{ object.invoice_date }}</td>
|
||||
<td>{{ object.balance|floatformat:2 }}</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default">
|
||||
<span class="glyphicon glyphicon-pencil"></span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if is_paginated %}
|
||||
<div class="col-md-6 col-md-offset-6 col-sm-12 text-right">
|
||||
{% paginator %}
|
||||
|
||||
13
RIGS/templates/RIGS/invoice_list_active.html
Normal file
13
RIGS/templates/RIGS/invoice_list_active.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends 'RIGS/invoice_list.html' %}
|
||||
|
||||
{% block title %}
|
||||
Outstanding Invoices
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
Outstanding Invoices ({{ count }} Events, £ {{ total|floatformat:2 }})
|
||||
{% endblock %}
|
||||
|
||||
{% block description %}
|
||||
<p>Paperwork for these events has been sent to treasury, but the full balance has not yet appeared on a ledger</p>
|
||||
{% endblock %}
|
||||
13
RIGS/templates/RIGS/invoice_list_archive.html
Normal file
13
RIGS/templates/RIGS/invoice_list_archive.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends 'RIGS/invoice_list.html' %}
|
||||
|
||||
{% block title %}
|
||||
Invoice Archive
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
All Invoices
|
||||
{% endblock %}
|
||||
|
||||
{% block description %}
|
||||
<p>This page displays all invoices: outstanding, paid, and void</p>
|
||||
{% endblock %}
|
||||
@@ -45,7 +45,7 @@
|
||||
<td>{{ object.pk }}</td>
|
||||
<td>{{ object.name }}</td>
|
||||
<td>{{ object.email }}</td>
|
||||
<td>{{ object.phone }}</td>
|
||||
<td><a href="tel:{{ object.phone }}">{{ object.phone }}</a></td>
|
||||
<td>{{ object.notes|yesno|capfirst }}</td>
|
||||
<td>{{ object.union_account|yesno|capfirst }}</td>
|
||||
<td>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<td>{{ person.pk }}</td>
|
||||
<td>{{ person.name }}</td>
|
||||
<td>{{ person.email }}</td>
|
||||
<td>{{ person.phone }}</td>
|
||||
<td><a href="tel:{{ person.phone }}">{{ person.phone }}</a></td>
|
||||
<td>{{ person.notes|yesno|capfirst }}</td>
|
||||
<td>
|
||||
<a href="{% url 'person_detail' person.pk %}" class="btn btn-default modal-href">
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<dd>{{object.initials}}</dd>
|
||||
|
||||
<dt>Phone</dt>
|
||||
<dd>{{object.phone}}</dd>
|
||||
<dd><a href="tel:{{ object.phone }}">{{object.phone}}</a></dd>
|
||||
</dl>
|
||||
{% if not request.is_ajax %}
|
||||
{% if object.pk == user.pk %}
|
||||
@@ -126,7 +126,7 @@
|
||||
<dd>
|
||||
{% if user.api_key %}
|
||||
<pre id="cal-url" data-url="http{{ request.is_secure|yesno:"s,"}}://{{ request.get_host }}{% url 'ics_calendar' api_pk=user.pk api_key=user.api_key %}"></pre>
|
||||
<small><a id="gcal-link" data-url="http://www.google.com/calendar/render?cid=http{{ request.is_secure|yesno:"s,"}}://{{ request.get_host }}{% url 'ics_calendar' api_pk=user.pk api_key=user.api_key %}" href="">Click here</a> to add to google calendar.<br/>
|
||||
<small><a id="gcal-link" data-url="https://support.google.com/calendar/answer/37100" href="">Click here</a> for instructions on adding to google calendar.<br/>
|
||||
To sync from google calendar to mobile device, visit <a href="https://www.google.com/calendar/syncselect" target="_blank">this page</a> on your device and tick "RIGS Calendar".</small>
|
||||
{% else %}
|
||||
<pre>No API Key Generated</pre>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<td>{{ object.pk }}</td>
|
||||
<td>{{ object.name }}</td>
|
||||
<td>{{ object.email }}</td>
|
||||
<td>{{ object.phone }}</td>
|
||||
<td><a href="tel:{{ object.phone }}">{{ object.phone }}</a></td>
|
||||
<td>{{ object.notes|yesno|capfirst }}</td>
|
||||
<td>
|
||||
<a href="{% url 'venue_detail' object.pk %}" class="btn btn-default modal-href">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<link rel="icon" type="image/png" href="{% static "imgs/pyrigs-avatar.png" %}">
|
||||
<link rel="apple-touch-icon" href="{% static "imgs/pyrigs-avatar.png" %}">
|
||||
<link href='http://fonts.googleapis.com/css?family=Open+Sans:400italic,700,300,400' rel='stylesheet'
|
||||
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400italic,700,300,400' rel='stylesheet'
|
||||
type='text/css'>
|
||||
|
||||
|
||||
@@ -74,12 +74,12 @@
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Invoices<b class="caret"></b></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'invoice_list' %}"><span class="glyphicon glyphicon-gbp"></span> Active</a>
|
||||
</li>
|
||||
{% if perms.RIGS.add_invoice %}
|
||||
<li><a href="{% url 'invoice_waiting' %}"><span
|
||||
class="glyphicon glyphicon-briefcase"></span> Waiting</a></li>
|
||||
class="glyphicon glyphicon-briefcase text-danger"></span> Waiting</a></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'invoice_list' %}"><span class="glyphicon glyphicon-gbp text-warning"></span> Outstanding</a>
|
||||
</li>
|
||||
<li><a href="{% url 'invoice_archive' %}"><span class="glyphicon glyphicon-book"></span>
|
||||
Archive</a></li>
|
||||
</ul>
|
||||
|
||||
Reference in New Issue
Block a user