/
models.py
313 lines (228 loc) · 9.92 KB
/
models.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# -*- coding: utf-8 -*-
from django.utils.translation import ugettext_lazy as _
from django.db import models
from django.db.models import Sum
from django.db.models.signals import pre_save, post_save, post_init, pre_init
from django.contrib.auth.models import User, UserManager
from django.core.exceptions import ValidationError
import datetime, calendar
# Create your models here.
def get_image_path(instance, filename):
return os.path.join('users', instance.id, filename)
class User(User):
"""Custom User model, extending Django's default User"""
USER_TYPES = (
('MEM', 'Member'),
('SPO', 'Sponsor'),
('DON', 'Donation')
)
profile_image = models.ImageField(upload_to=get_image_path, blank=True)
utype = models.CharField(max_length=3, choices=USER_TYPES, default='MEM')
objects = UserManager()
@property
def most_recent_payment(self):
p = self.payments_made.all().order_by('-date_paid')
return p[0] if p else None
def total_paid(self, ptype=None):
'''Returns the total amount the User has paid either in total, or for a specified Contract type'''
# Construct the appropriate Queryset
if ptype is not None:
payments = self.payments_made.filter(contract__ctype__desc=ptype)
else:
payments = self.payments_made
return payments.aggregate(Sum('amount'))['amount__sum'] or 0.0
def membership_status(self, pretty=False):
'''Returns string (see Contract::CONTRACT_STATUSES) indicating latest Membership status of this User'''
try:
if not hasattr(self, '__latest_membership'):
lm = self.contracts.filter(ctype__desc='Membership').latest('start')
if len(lm) > 0:
self.__latest_membership = lm.pop()
else:
self.__latest_membership = None
return self.__latest_membership.get_status_display() if pretty else self.__latest_membership.status
except Contract.DoesNotExist:
return None
def member_since(self):
'''Returns datetime object representing start date of earliest Membership Contract if found, None otherwise'''
try:
if not hasattr(self, '__member_since'):
ms = self.contracts.filter(ctype__desc='Membership').order_by('start')[0:1]
if len(ms) > 0:
self.__member_since = ms[0].start
else:
self.__member_since = None
return self.__member_since
except Contract.DoesNotExist:
return None
def __unicode__(self):
if self.first_name and self.last_name:
return self.get_full_name()
else:
return self.username
class ContractType(models.Model):
desc = models.CharField(max_length=128, blank=False, null=True)
def __unicode__(self):
return self.desc
class Contract(models.Model):
CONTRACT_STATUSES = (
('ACT', 'Active'),
('LAP', 'Lapsed'),
('TER', 'Terminated'),
('PEN', 'Pending')
)
start = models.DateField()
end = models.DateField(blank=True, null=True)
valid_till = models.DateField(editable=False)
ctype = models.ForeignKey(ContractType, blank=False, null=True, verbose_name="Contract type", help_text="Locker and Address Use Contracts must use their respective Tiers. Membership contracts can accept all other Tiers")
tier = models.ForeignKey("Tier", blank=False, null=True)
user = models.ForeignKey(User, blank=False, null=True, related_name="contracts")
status = models.CharField(max_length=3, choices=CONTRACT_STATUSES)
desc = models.CharField(max_length=1024, blank=True, help_text="Enter company name if Contract is for Address Use. May use for general remarks for other Contract types")
def __extend_by(self, num_months):
'''Extends the validity of this Contract by specified number of months (Assume 1 month = 28 days). THIS METHOD DOES NOT save() AUTOMATICALLY'''
# year = s.year + ((s.month + num_months) / 12)
# month = ((s.month + num_months) % 12)
# last_day = calendar.monthrange(self.end.year, self.end.month)[1]
#self.end = datetime.date(year, month, last_day)
self.valid_till = self.valid_till + datetime.timedelta(days=(28*num_months))
# Normalise date to end of that month
self.valid_till = datetime.date(self.valid_till.year, self.valid_till.month, calendar.monthrange(self.valid_till.year, self.valid_till.month)[1])
@property
def total_paid(self):
'''Returns total amount paid due to this Contract'''
return self.payments.aggregate(Sum('amount'))['amount__sum'] or 0.0
def sync(self):
'''Looks at the total amount paid to this Contract and recalculates its proper expiry (end) date, taking a month's deposit into account'''
# Reset the clock
self.valid_till = self.start
months_paid = self.total_paid / self.tier.fee
if months_paid > 0:
# Take into account 1 month's deposit
months_paid -= 1
self.__extend_by(int(months_paid))
self.save()
def balance(self, in_months=False):
'''Looks at how much has been paid for this Contract and determines if there is any balance owed by (-ve) / owed to (+ve) the Member'''
balance = 0
duration_in_months = 1
# Calculate number of months Contract has been in effect, ie. not Terminated
if self.status == 'TER':
duration_in_months = (self.end - self.start).days / 28 # Naive month calculation
else:
duration_in_months = (datetime.date.today() - self.start).days / 28 # Naive month calculation
balance = self.total_paid - (self.tier.fee * duration_in_months)
if in_months:
return balance / self.tier.fee
else:
return balance
def update_with_payment(self, p):
# Takes a Payment object, calculates how many month's worth it is, and extends the contract end date accordingly
if isinstance(p, Payment):
# Get number of multiples of Contract for this Payment
multiples = int(p.amount / self.tier.fee)
self.__extend_by(multiples)
self.save()
# sync() the Contract if this is the first Payment being made on this Contract
if self.payments.count() == 1:
self.sync()
else:
return False
def save(self):
# Overridden save() forces the date of self.end to be the last day of that given month.
# Eg. if self.end is initially declared as 5 May 2010, we now force it to become 31 May 2010 before actually save()'ing the object.
# But first, is self.end even specified?
if not self.valid_till:
self.valid_till = self.start
last_day = calendar.monthrange(self.valid_till.year, self.valid_till.month)[1]
self.valid_till = datetime.date(self.valid_till.year, self.valid_till.month, last_day)
#force start date to be normalised as 1st day of the month
self.start = datetime.date(self.start.year, self.start.month, 1)
# If we notice the Contract is now Terminated, and the end date has not been set, set the end date
if self.status == 'TER' and self.end is None:
today = datetime.date.today()
self.end = datetime.date(today.year, today.month, calendar.monthrange(today.year, today.month)[1])
super(Contract, self).save()
def clean(self):
# Model validation to ensure that validates that contract and tier are allowed
if self.ctype != self.tier.ctype:
raise ValidationError(_("Contract type and tier mismatched"))
def __unicode__(self):
return "%s %s | %s to %s" % (self.tier, self.ctype, self.start.strftime('%b %Y'), self.valid_till.strftime('%b %Y'))
class Tier(models.Model):
fee = models.FloatField(default=0.0)
desc = models.CharField(max_length=255)
ctype = models.ForeignKey("ContractType", blank=False, null=True)
def __unicode__(self):
return self.desc
class Payment(models.Model):
PAYMENT_METHODS = (
('EFT', 'Electronic Fund Transfer'),
('CHK', 'Cheque'),
('CSH', 'Cash'),
('OTH', 'Others')
)
# PAYMENT_TYPES = (
# ('DPT', 'Deposit'),
# ('FEE', 'Membership Fees'),
# ('DNT', 'Donation')
# )
# MONTHS = (
# ('1', 'Jan'),
# ('2', 'Feb'),
# ('3', 'Mar'),
# ('4', 'Apr'),
# ('5', 'May'),
# ('6', 'Jun'),
# ('7', 'Jul'),
# ('8', 'Aug'),
# ('9', 'Sep'),
# ('10', 'Oct'),
# ('11', 'Nov'),
# ('12', 'Dec')
# )
# @property
# def year_range(self):
# this_year = datetime.today().year
# years = ( (unicode(this_year), unicode(this_year)) )
#
# for i in xrange(0, 10):
# years.insert(0, (unicode(this_year-i), unicode(this_year-i)))
# years.append((unicode(this_year+i), unicode(this_year+i)))
#
# return years
# YEARS = (
# ('2010', '2010'),
# )
date_paid = models.DateField()
amount = models.FloatField(default=0.0)
method = models.CharField(max_length=3, choices=PAYMENT_METHODS, default='EFT')
contract = models.ForeignKey(Contract, blank=False, null=True, related_name="payments")
desc = models.CharField(max_length=255, blank=True, help_text="Eg. Cheque or transaction number, if applicable")
user = models.ForeignKey(User, blank=False, null=True, related_name="payments_made")
verified = models.BooleanField(default=False, blank=False, help_text="Has this Payment been verified/approved by an Admin?")
def __unicode__(self):
return u"%s | %s %s | %s, %s" % (self.user, self.contract.tier, self.contract.ctype, self.amount, self.date_paid.strftime('%d %b %Y'))
class Locker(models.Model):
user = models.ForeignKey(User, blank=False, null=True, related_name="locker")
num = models.IntegerField()
# Attaching a post_save signal handler to the Payment model to update the appropriate Contract
def update_contract_with_payments(sender, **kwargs):
payment = kwargs['instance']
c = payment.contract
c.update_with_payment(payment)
post_save.connect(update_contract_with_payments, sender=Payment)
def lapsed_check(sender, **kwargs):
'''Checks the end date of active contract and compares it with today. If contract is lapsed, update the contract status to lapsed.'''
contract = kwargs['instance']
# If this is a new Contract, check if we have a valid_till date set
if not contract.id and not contract.valid_till:
contract.valid_till = contract.start
if contract.status == u'ACT':
if contract.valid_till < datetime.date.today():
contract.status = u'LAP'
contract.save()
elif contract.status == u'LAP' and contract.valid_till > datetime.date.today():
contract.status = u'ACT'
contract.save()
post_init.connect(lapsed_check, sender=Contract)