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