python: Add Supporter pseudo-model.
This commit is contained in:
parent
b32e72b1e8
commit
bc21b83951
1 changed files with 131 additions and 1 deletions
|
@ -1,10 +1,140 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import datetime
|
||||
import operator
|
||||
|
||||
from django.db import models
|
||||
|
||||
class Date(datetime.date):
|
||||
MONTH_MAXDAY = {
|
||||
1: 31,
|
||||
2: 28,
|
||||
3: 31,
|
||||
4: 30,
|
||||
5: 31,
|
||||
6: 30,
|
||||
7: 31,
|
||||
8: 31,
|
||||
9: 30,
|
||||
10: 31,
|
||||
11: 30,
|
||||
12: 31,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_pydate(cls, date):
|
||||
return cls(date.year, date.month, date.day)
|
||||
|
||||
def adjust_month(self, delta, day=None):
|
||||
if day is None:
|
||||
day = self.day
|
||||
year_delta, month_delta = divmod(abs(delta), 12)
|
||||
op_func = operator.sub if delta < 0 else operator.add
|
||||
month = op_func(self.month, month_delta)
|
||||
if (month < 1) or (month > 12):
|
||||
year_delta += 1
|
||||
month = op_func(month, -12)
|
||||
year = op_func(self.year, year_delta)
|
||||
day = min(day, self.MONTH_MAXDAY[month])
|
||||
return type(self)(year, month, day)
|
||||
|
||||
def next_month(self, day=None):
|
||||
return self.adjust_month(1, day)
|
||||
|
||||
def next_year(self):
|
||||
return self.adjust_month(12)
|
||||
|
||||
def round_month_up(self):
|
||||
return self.adjust_month(1, day=1)
|
||||
|
||||
|
||||
class DateField(models.DateField, metaclass=models.SubfieldBase):
|
||||
def to_python(self, value):
|
||||
retval = super().to_python(value)
|
||||
if retval is not None:
|
||||
retval = Date.from_pydate(retval)
|
||||
return retval
|
||||
|
||||
|
||||
class Payment(models.Model):
|
||||
date = models.DateField()
|
||||
date = DateField()
|
||||
entity = models.TextField()
|
||||
payee = models.TextField()
|
||||
program = models.TextField()
|
||||
amount = models.TextField()
|
||||
|
||||
|
||||
class Supporter:
|
||||
STATUS_NEW = 'New'
|
||||
STATUS_ACTIVE = 'Active'
|
||||
STATUS_LAPSED = 'Lapsed'
|
||||
STATUS_LOST = 'Lost'
|
||||
|
||||
LOST_THRESHOLD = datetime.timedelta(days=365)
|
||||
LAPSED_THRESHOLD = datetime.timedelta()
|
||||
|
||||
def __init__(self, entity):
|
||||
self.entity = entity
|
||||
|
||||
@classmethod
|
||||
def iter_entities(cls, supporter_types=['Annual', 'Monthly']):
|
||||
qset = Payment.objects.only('entity')
|
||||
if supporter_types is None:
|
||||
pass
|
||||
elif not supporter_types:
|
||||
qset = qset.none()
|
||||
else:
|
||||
condition = models.Q()
|
||||
for suffix in supporter_types:
|
||||
condition |= models.Q(program__endswith=':' + suffix)
|
||||
qset = qset.filter(condition)
|
||||
seen = set()
|
||||
for payment in qset:
|
||||
if payment.entity not in seen:
|
||||
seen.add(payment.entity)
|
||||
yield payment.entity
|
||||
|
||||
def payments(self, as_of_date=None):
|
||||
pset = Payment.objects.order_by('date').filter(entity=self.entity)
|
||||
if as_of_date is not None:
|
||||
pset = pset.filter(date__lte=as_of_date)
|
||||
return pset
|
||||
|
||||
def _expose(internal_method):
|
||||
def expose_wrapper(self, as_of_date=None, *args, **kwargs):
|
||||
return internal_method(self, self.payments(as_of_date), *args, **kwargs)
|
||||
return expose_wrapper
|
||||
|
||||
def _supporter_type(self, payments):
|
||||
return payments.last().program.rsplit(':', 1)[-1]
|
||||
supporter_type = _expose(_supporter_type)
|
||||
|
||||
def _calculate_lapse_date(self, last_payment_date, supporter_type):
|
||||
if supporter_type == 'Monthly':
|
||||
lapse_date = last_payment_date.next_month()
|
||||
else:
|
||||
lapse_date = last_payment_date.next_year()
|
||||
return lapse_date.round_month_up()
|
||||
|
||||
def _lapse_date(self, payments):
|
||||
return self._calculate_lapse_date(payments.last().date,
|
||||
self._supporter_type(payments))
|
||||
lapse_date = _expose(_lapse_date)
|
||||
|
||||
def status(self, as_of_date=None):
|
||||
if as_of_date is None:
|
||||
as_of_date = Date.today()
|
||||
payments = self.payments(as_of_date)
|
||||
payments_count = payments.count()
|
||||
if payments_count == 0:
|
||||
return None
|
||||
lapse_date = self._lapse_date(payments)
|
||||
days_past_due = as_of_date - lapse_date
|
||||
if days_past_due >= self.LOST_THRESHOLD:
|
||||
return self.STATUS_LOST
|
||||
elif days_past_due >= self.LAPSED_THRESHOLD:
|
||||
return self.STATUS_LAPSED
|
||||
elif as_of_date.adjust_month(-1, 1) < payments.first().date <= as_of_date:
|
||||
return self.STATUS_NEW
|
||||
else:
|
||||
return self.STATUS_ACTIVE
|
||||
|
|
Loading…
Reference in a new issue