PZ: prevent generationg orders for cancelled debits.
[wolnelektury.git] / src / pz / models.py
1 from django.db import models
2 from django.utils.timezone import now
3 from django.utils.translation import ugettext_lazy as _
4 from .bank import parse_export_feedback
5
6
7 class Campaign(models.Model):
8     name = models.CharField(_('name'), max_length=255, unique=True)
9     description = models.TextField(_('description'), blank=True)
10
11     class Meta:
12         verbose_name = _('campaign')
13         verbose_name_plural = _('campaigns')
14
15     def __str__(self):
16         return self.name
17
18
19 class Fundraiser(models.Model):
20     name = models.CharField(_('name'), max_length=255, unique=True)
21
22     class Meta:
23         verbose_name = _('fundraiser')
24         verbose_name_plural = _('fundraisers')
25
26     def __str__(self):
27         return self.name
28
29
30 class DirectDebit(models.Model):
31     first_name = models.CharField(_('first name'), max_length=255, blank=True)
32     last_name = models.CharField(_('last name'), max_length=255, blank=True)
33     sex = models.CharField(_('sex'), max_length=1, blank=True, choices=[
34         ('M', _('M')),
35         ('F', _('F')),
36     ])
37     date_of_birth = models.DateField(_('date of birth'), null=True, blank=True)
38     street = models.CharField(_('street'), max_length=255, blank=True)
39     building = models.CharField(_('building'), max_length=255, blank=True)
40     flat = models.CharField(_('flat'), max_length=255, blank=True)
41     town = models.CharField(_('town'), max_length=255, blank=True)
42     postal_code = models.CharField(_('postal code'),  max_length=255, blank=True)
43     phone = models.CharField(_('phone'), max_length=255, blank=True)
44     email = models.CharField(_('e-mail'), max_length=255, blank=True)
45     iban = models.CharField(_('IBAN'), max_length=255, blank=True)
46     iban_valid = models.NullBooleanField(_('IBAN valid'), default=False)
47     is_consumer = models.BooleanField(_('is a consumer'), default=True)
48     payment_id = models.CharField(_('payment identifier'), max_length=255, blank=True, unique=True)
49     agree_fundraising = models.BooleanField(_('agree fundraising'), default=False)
50     agree_newsletter = models.BooleanField(_('agree newsletter'), default=False)
51
52     acquisition_date = models.DateField(_('acquisition date'), help_text=_('Date from the form'), null=True, blank=True)
53     submission_date = models.DateField(_('submission date'), null=True, blank=True, help_text=_('Date the fundaiser submitted the form'))
54     bank_submission_date = models.DateField(_('bank submission date'), null=True, blank=True, help_text=_('Date when the form data is submitted to the bank'))
55     bank_acceptance_date = models.DateField(_('bank accepted date'), null=True, blank=True, help_text=_('Date when bank accepted the form'))
56
57     fundraiser = models.ForeignKey(Fundraiser, models.PROTECT, blank=True, null=True, verbose_name=_('fundraiser'))
58     fundraiser_commission = models.IntegerField(_('fundraiser commission'), null=True, blank=True)
59     fundraiser_bill = models.CharField(_('fundaiser bill number'), max_length=255, blank=True)
60
61     amount = models.IntegerField(_('amount'), null=True, blank=True)
62
63     notes = models.TextField(_('notes'), blank=True)
64
65     needs_redo = models.BooleanField(_('needs redo'), default=False)
66     cancelled_at = models.DateTimeField(_('cancelled at'), null=True, blank=True)
67     optout = models.BooleanField(_('optout'), default=False)
68
69     campaign = models.ForeignKey(Campaign, models.PROTECT, null=True, blank=True, verbose_name=_('campaign'))
70
71     class Meta:
72         verbose_name = _('direct debit')
73         verbose_name_plural = _('direct debits')
74
75     def __str__(self):
76         return self.payment_id
77
78     def save(self, **kwargs):
79         self.iban_valid = not self.iban_warning() if self.iban else None
80         super().save(**kwargs)
81
82     @classmethod
83     def get_next_payment_id(cls):
84         # Find the last object added.
85         last = cls.objects.order_by('-id').first()
86         if last is None:
87             return ''
88         match = re.match(r'^(.*?)(\d+)$', last.payment_id)
89         if match is None:
90             return ''
91         prefix = match.group(1)
92         number = int(match.group(2))
93         number_length = len(match.group(2))
94         while True:
95             number += 1
96             payment_id = f'{prefix}{number:0{number_length}}'
97             if not cls.objects.filter(payment_id=payment_id).exists():
98                 break
99         return payment_id
100
101     @property
102     def full_name(self):
103         return ' '.join((self.first_name, self.last_name)).strip()
104
105     @property
106     def street_address(self):
107         street_addr = self.street
108         if self.building:
109             street_addr += ' ' + self.building
110         if self.flat:
111             street_addr += ' m. ' + self.flat
112         street_addr = street_addr.strip()
113         return street_addr
114
115     def iban_warning(self):
116         if not self.iban:
117             return 'No IBAN'
118         if len(self.iban) != 26:
119             return 'Bad IBAN length'
120         if int(self.iban[2:] + '2521' + self.iban[:2]) % 97 != 1:
121             return 'This IBAN number looks invalid'
122         return ''
123     iban_warning.short_description = ''
124
125
126 class BankExportFeedback(models.Model):
127     created_at = models.DateTimeField(auto_now_add=True)
128     csv = models.FileField(upload_to='pz/feedback/')
129
130     def save(self, **kwargs):
131         super().save(**kwargs)
132         for payment_id, status, comment in parse_export_feedback(self.csv):
133             debit = DirectDebit.objects.get(payment_id = payment_id)
134             b, created = self.bankexportfeedbackline_set.get_or_create(
135                 debit=debit,
136                 defaults={
137                     "status": status,
138                     "comment": comment,
139                 }
140             )
141             if not created:
142                 b.status = status
143                 b.comment = comment
144                 b.save()
145             if status == 1 and not debit.bank_acceptance_date:
146                 debit.bank_acceptance_date = now().date()
147                 debit.save()
148
149
150 class BankExportFeedbackLine(models.Model):
151     feedback = models.ForeignKey(BankExportFeedback, models.CASCADE)
152     debit = models.ForeignKey(DirectDebit, models.CASCADE)
153     status = models.SmallIntegerField()
154     comment = models.CharField(max_length=255)
155
156
157
158 class BankOrder(models.Model):
159     payment_date = models.DateField(null=True, blank=True)
160     sent = models.DateTimeField(null=True, blank=True)
161     debits = models.ManyToManyField(DirectDebit, blank=True)