From d25e1c6ed14aa17a3fb5e8147355ee63ed5c5c3d Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Thu, 14 Oct 2021 11:21:52 +0200 Subject: [PATCH] PZ: prevent generationg orders for cancelled debits. --- src/pz/admin.py | 46 +++++++++++++++++-- src/pz/bank.py | 16 ++++++- src/pz/migrations/0007_auto_20211014_1100.py | 23 ++++++++++ src/pz/migrations/0008_fill_cancelled_date.py | 32 +++++++++++++ .../0009_remove_directdebit_is_cancelled.py | 17 +++++++ src/pz/models.py | 2 +- 6 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 src/pz/migrations/0007_auto_20211014_1100.py create mode 100644 src/pz/migrations/0008_fill_cancelled_date.py create mode 100644 src/pz/migrations/0009_remove_directdebit_is_cancelled.py diff --git a/src/pz/admin.py b/src/pz/admin.py index 25c700b95..9687948dc 100644 --- a/src/pz/admin.py +++ b/src/pz/admin.py @@ -1,5 +1,7 @@ from django.contrib import admin +from django.contrib.admin.filters import FieldListFilter 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 @@ -14,6 +16,40 @@ admin.site.register(models.Fundraiser) 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 @@ -41,7 +77,7 @@ class DirectDebitAdmin(admin.ModelAdmin): 'agree_newsletter', 'fundraiser', 'campaign', - 'is_cancelled', + ('cancelled_at', EmptyFieldListFilter), 'needs_redo', 'optout', 'amount', @@ -69,7 +105,7 @@ class DirectDebitAdmin(admin.ModelAdmin): ] }), (_('Processing'), {"fields": [ - ('is_cancelled', 'needs_redo', 'optout'), + ('cancelled_at', 'needs_redo', 'optout'), 'submission_date', 'fundraiser_commission', 'fundraiser_bill', @@ -152,7 +188,11 @@ class BankOrderAdmin(admin.ModelAdmin): 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) diff --git a/src/pz/bank.py b/src/pz/bank.py index 683767281..40b985a13 100644 --- a/src/pz/bank.py +++ b/src/pz/bank.py @@ -48,13 +48,14 @@ def parse_export_feedback(f): 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 = [] + cancelled = [] 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) + 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: ' @@ -85,6 +88,15 @@ def bank_order(date, queryset): for debit in no_amounts ) t += '. ' + if cancelled: + t += 'Debits have been cancelled: ' + t += ', '.join( + '{}'.format( + debit.pk, debit + ) + for debit in cancelled + ) + t += '. ' 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 index 000000000..dc1a01674 --- /dev/null +++ b/src/pz/migrations/0007_auto_20211014_1100.py @@ -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 index 000000000..0dadee22a --- /dev/null +++ b/src/pz/migrations/0008_fill_cancelled_date.py @@ -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 index 000000000..66847cec7 --- /dev/null +++ b/src/pz/migrations/0009_remove_directdebit_is_cancelled.py @@ -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', + ), + ] diff --git a/src/pz/models.py b/src/pz/models.py index 1a77217b9..2e44e388a 100644 --- a/src/pz/models.py +++ b/src/pz/models.py @@ -63,7 +63,7 @@ class DirectDebit(models.Model): 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')) -- 2.20.1