PZ: prevent generationg orders for cancelled debits.
authorRadek Czajka <rczajka@rczajka.pl>
Thu, 14 Oct 2021 09:21:52 +0000 (11:21 +0200)
committerRadek Czajka <rczajka@rczajka.pl>
Thu, 14 Oct 2021 09:21:52 +0000 (11:21 +0200)
src/pz/admin.py
src/pz/bank.py
src/pz/migrations/0007_auto_20211014_1100.py [new file with mode: 0644]
src/pz/migrations/0008_fill_cancelled_date.py [new file with mode: 0644]
src/pz/migrations/0009_remove_directdebit_is_cancelled.py [new file with mode: 0644]
src/pz/models.py

index 25c700b..9687948 100644 (file)
@@ -1,5 +1,7 @@
 from django.contrib import admin
 from django.contrib import admin
+from django.contrib.admin.filters import FieldListFilter
 from django.contrib import messages
 from django.contrib import messages
+from django.db.models import Q
 from django.shortcuts import get_object_or_404, redirect
 from django.urls import path, reverse
 from django.utils.safestring import mark_safe
 from django.shortcuts import get_object_or_404, redirect
 from django.urls import path, reverse
 from django.utils.safestring import mark_safe
@@ -14,6 +16,40 @@ admin.site.register(models.Fundraiser)
 admin.site.register(models.Campaign)
 
 
 admin.site.register(models.Campaign)
 
 
+# Backport from Django 3.1.
+class EmptyFieldListFilter(FieldListFilter):
+    def __init__(self, field, request, params, model, model_admin, field_path):
+        self.lookup_kwarg = '%s__isempty' % field_path
+        self.lookup_val = params.get(self.lookup_kwarg)
+        super().__init__(field, request, params, model, model_admin, field_path)
+
+    def queryset(self, request, queryset):
+        if self.lookup_kwarg not in self.used_parameters:
+            return queryset
+        if self.lookup_val not in ('0', '1'):
+            raise IncorrectLookupParameters
+
+        lookup_condition = Q(**{'%s__isnull' % self.field_path: True})
+        if self.lookup_val == '1':
+            return queryset.filter(lookup_condition)
+        return queryset.exclude(lookup_condition)
+
+    def expected_parameters(self):
+        return [self.lookup_kwarg]
+
+    def choices(self, changelist):
+        for lookup, title in (
+            (None, _('All')),
+            ('1', _('Empty')),
+            ('0', _('Not empty')),
+        ):
+            yield {
+                'selected': self.lookup_val == lookup,
+                'query_string': changelist.get_query_string({self.lookup_kwarg: lookup}),
+                'display': title,
+            }
+
+
 class BankExportFeedbackLineInline(admin.TabularInline):
     model = models.BankExportFeedbackLine
     extra = 0
 class BankExportFeedbackLineInline(admin.TabularInline):
     model = models.BankExportFeedbackLine
     extra = 0
@@ -41,7 +77,7 @@ class DirectDebitAdmin(admin.ModelAdmin):
         'agree_newsletter',
         'fundraiser',
         'campaign',
         'agree_newsletter',
         'fundraiser',
         'campaign',
-        'is_cancelled',
+        ('cancelled_at', EmptyFieldListFilter),
         'needs_redo',
         'optout',
         'amount',
         'needs_redo',
         'optout',
         'amount',
@@ -69,7 +105,7 @@ class DirectDebitAdmin(admin.ModelAdmin):
             ]
         }),
         (_('Processing'), {"fields": [
             ]
         }),
         (_('Processing'), {"fields": [
-            ('is_cancelled', 'needs_redo', 'optout'),
+            ('cancelled_at', 'needs_redo', 'optout'),
             'submission_date',
             'fundraiser_commission',
             'fundraiser_bill',
             'submission_date',
             'fundraiser_commission',
             'fundraiser_bill',
@@ -152,7 +188,11 @@ class BankOrderAdmin(admin.ModelAdmin):
         order = get_object_or_404(
             models.BankOrder, pk=pk)
         try:
         order = get_object_or_404(
             models.BankOrder, pk=pk)
         try:
-            return bank.bank_order(order.payment_date, order.debits.all())
+            return bank.bank_order(
+                order.payment_date,
+                order.sent,
+                order.debits.all()
+            )
         except Exception as e:
             messages.error(request, str(e))
             return redirect('admin:pz_bankorder_change', pk)
         except Exception as e:
             messages.error(request, str(e))
             return redirect('admin:pz_bankorder_change', pk)
index 6837672..40b985a 100644 (file)
@@ -48,13 +48,14 @@ def parse_export_feedback(f):
         yield payment_id, status, comment
 
 
         yield payment_id, status, comment
 
 
-def bank_order(date, queryset):
+def bank_order(date, sent_at, queryset):
     response = HttpResponse(content_type='application/octet-stream')
     response['Content-Disposition'] = 'attachment; filename=order.PLD'
     rows = []
 
     no_dates = []
     no_amounts = []
     response = HttpResponse(content_type='application/octet-stream')
     response['Content-Disposition'] = 'attachment; filename=order.PLD'
     rows = []
 
     no_dates = []
     no_amounts = []
+    cancelled = []
 
     if date is None:
         raise ValueError('Payment date not set yet.')
 
     if date is None:
         raise ValueError('Payment date not set yet.')
@@ -64,8 +65,10 @@ def bank_order(date, queryset):
             no_dates.append(debit)
         if debit.amount is None:
             no_amounts.append(debit)
             no_dates.append(debit)
         if debit.amount is None:
             no_amounts.append(debit)
+        if debit.cancelled_at and debit.cancelled_at.date() <= date and (sent_at is None or debit.cancelled_at < sent_at):
+            cancelled.append(debit)
 
 
-    if no_dates or no_amounts:
+    if no_dates or no_amounts or cancelled:
         t = ''
         if no_dates:
             t += 'Bank acceptance not received for: '
         t = ''
         if no_dates:
             t += 'Bank acceptance not received for: '
@@ -85,6 +88,15 @@ def bank_order(date, queryset):
                 for debit in no_amounts
             )
             t += '. '
                 for debit in no_amounts
             )
             t += '. '
+        if cancelled:
+            t += 'Debits have been cancelled: '
+            t += ', '.join(
+                '<a href="/admin/pz/directdebit/{}/change">{}</a>'.format(
+                    debit.pk, debit
+                )
+                for debit in cancelled
+            )
+            t += '. '
         raise ValueError(mark_safe(t))
 
     for debit in queryset:
         raise ValueError(mark_safe(t))
 
     for debit in queryset:
diff --git a/src/pz/migrations/0007_auto_20211014_1100.py b/src/pz/migrations/0007_auto_20211014_1100.py
new file mode 100644 (file)
index 0000000..dc1a016
--- /dev/null
@@ -0,0 +1,23 @@
+# Generated by Django 2.2.19 on 2021-10-14 09:00
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('pz', '0006_bankorder'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='directdebit',
+            name='cancelled_at',
+            field=models.DateTimeField(blank=True, null=True, verbose_name='cancelled at'),
+        ),
+        migrations.AlterField(
+            model_name='bankorder',
+            name='debits',
+            field=models.ManyToManyField(blank=True, to='pz.DirectDebit'),
+        ),
+    ]
diff --git a/src/pz/migrations/0008_fill_cancelled_date.py b/src/pz/migrations/0008_fill_cancelled_date.py
new file mode 100644 (file)
index 0000000..0dadee2
--- /dev/null
@@ -0,0 +1,32 @@
+# Generated by Django 2.2.19 on 2021-10-14 09:01
+
+from django.db import migrations
+from django.utils.timezone import now
+
+
+def fill_cancelled_date(apps, schema_editor):
+    DirectDebit = apps.get_model('pz', 'DirectDebit')
+    DirectDebit.objects.filter(is_cancelled=True).update(
+        cancelled_at=now()
+    )
+
+
+def fill_is_cancelled(apps, schema_editor):
+    DirectDebit = apps.get_model('pz', 'DirectDebit')
+    DirectDebit.objects.exclude(cancelled_at=None).update(
+        is_cancelled=True
+    )
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('pz', '0007_auto_20211014_1100'),
+    ]
+
+    operations = [
+        migrations.RunPython(
+            fill_cancelled_date,
+            fill_is_cancelled
+        )
+    ]
diff --git a/src/pz/migrations/0009_remove_directdebit_is_cancelled.py b/src/pz/migrations/0009_remove_directdebit_is_cancelled.py
new file mode 100644 (file)
index 0000000..66847ce
--- /dev/null
@@ -0,0 +1,17 @@
+# Generated by Django 2.2.19 on 2021-10-14 09:04
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('pz', '0008_fill_cancelled_date'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='directdebit',
+            name='is_cancelled',
+        ),
+    ]
index 1a77217..2e44e38 100644 (file)
@@ -63,7 +63,7 @@ class DirectDebit(models.Model):
     notes = models.TextField(_('notes'), blank=True)
 
     needs_redo = models.BooleanField(_('needs redo'), default=False)
     notes = models.TextField(_('notes'), blank=True)
 
     needs_redo = models.BooleanField(_('needs redo'), default=False)
-    is_cancelled = models.BooleanField(_('is cancelled'), default=False)
+    cancelled_at = models.DateTimeField(_('cancelled at'), null=True, blank=True)
     optout = models.BooleanField(_('optout'), default=False)
 
     campaign = models.ForeignKey(Campaign, models.PROTECT, null=True, blank=True, verbose_name=_('campaign'))
     optout = models.BooleanField(_('optout'), default=False)
 
     campaign = models.ForeignKey(Campaign, models.PROTECT, null=True, blank=True, verbose_name=_('campaign'))