Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions fragdenstaat_de/fds_donation/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
DonationGift,
DonationGiftOrder,
Donor,
DonorEvent,
DonorTag,
Recurrence,
)
Expand Down Expand Up @@ -1552,3 +1553,29 @@ def sum_amount(self, obj):

def days(self, obj):
return obj.days()


@admin.register(DonorEvent)
class DonorEventAdmin(admin.ModelAdmin):
readonly_fields = (
"donor",
"timestamp",
"kind",
"reference",
"context",
)
raw_id_fields = ("donor",)
list_display = (
"donor",
"timestamp",
"kind",
"reference",
)
date_hierarchy = "timestamp"
list_filter = (
"kind",
("donor", ForeignKeyFilter),
)

def get_queryset(self, request):
return super().get_queryset(request).select_related("donor")
39 changes: 39 additions & 0 deletions fragdenstaat_de/fds_donation/cms_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@
from fragdenstaat_de.fds_cms.utils import get_plugin_children
from fragdenstaat_de.fds_mailing.cms_plugins import EmailRenderMixin, EmailTemplateMixin

from .forms import RecurrenceUpgradeForm
from .models import (
DefaultDonation,
DonationFormCMSPlugin,
DonationFormViewCount,
DonationGiftFormCMSPlugin,
DonationProgressBarCMSPlugin,
Donor,
DonorEvent,
EmailDonationButtonCMSPlugin,
RemoteDonationFormCMSPlugin,
UpgradeRecurrenceFormCMSPlugin,
)
from .utils import get_donor_from_request

Expand Down Expand Up @@ -52,6 +55,42 @@ def render(self, context, instance, placeholder):
donor=context["donor"],
category=instance.category,
)
return context


@plugin_pool.register_plugin
class UpgradeRecurrencePlugin(CMSPluginBase):
model = UpgradeRecurrenceFormCMSPlugin
module = _("Donations")
name = _("Upgrade recurrence")
text_enabled = True
cache = False
allow_children = True
render_template = "fds_donation/cms_plugins/upgrade_recurrence.html"

def render(self, context, instance, placeholder):
context = super().render(context, instance, placeholder)

context["donor"] = get_donor_from_request(context["request"])
context["next_url"] = instance.next_url or "/"

if context["donor"]:
DonorEvent.objects.create(
donor=context["donor"],
reference=context["request"].GET.get("pk_campaign", ""),
kind=DonorEvent.Kind.PROMPT_UPGRADE_RECURRENCE,
)
active_recurrence = context["donor"].get_current_recurrence()
if active_recurrence and active_recurrence.should_upgrade(
instance.days_since
):
context["form"] = RecurrenceUpgradeForm(
recurrence=active_recurrence,
choice_count=instance.choice_count,
initial={
"reference": context["request"].GET.get("pk_campaign", "")
},
)

return context

Expand Down
23 changes: 7 additions & 16 deletions fragdenstaat_de/fds_donation/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

from django import forms
from django.http import HttpResponse, StreamingHttpResponse
from django.template.defaultfilters import floatformat
from django.utils import formats, timezone
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
Expand All @@ -18,13 +17,13 @@
from froide.helper.csv_utils import dict_to_csv_stream, export_csv_response
from froide.helper.email_sending import mail_registry

from fragdenstaat_de.fds_donation.remote_filing import backup_donation_file
from fragdenstaat_de.fds_donation.tasks import (
from .models import Donor
from .remote_filing import backup_donation_file
from .tasks import (
backup_jzwb_pdf_task,
send_jzwb_mailing_task,
)

from .models import Donor
from .utils import format_decimal_amount, format_decimal_amount_with_currency

MAX_DONATIONS_PER_PAGE = 26

Expand Down Expand Up @@ -195,14 +194,6 @@ def send_mailing(
}


def format_amount_with_currency(num: Decimal) -> str:
return "{} €".format(format_amount(num))


def format_amount(num: Decimal) -> str:
return floatformat(num, "2g")


def get_zwbs(donors, year: int):
for donor in donors:
data = get_zwb(donor, year)
Expand Down Expand Up @@ -252,7 +243,7 @@ def get_zwb_data(donor: Donor, donation_data):
"Land": donor.country.name,
"Anrede": donor.get_salutation_display(),
"Briefanrede": donor.get_german_salutation(),
"Jahressumme": format_amount(total_amount),
"Jahressumme": format_decimal_amount(total_amount),
"JahressummeInWorten": amount_to_words(total_amount),
"NutzerKonto": donor_account,
"receipt_already": any(d["receipt_date"] for d in donation_data),
Expand Down Expand Up @@ -301,7 +292,7 @@ def get_donation_data(donations, ignore_receipt_date: Optional[datetime] = None)
return [
{
"date": format_date(donation.received_timestamp),
"formatted_amount": format_amount_with_currency(donation.amount),
"formatted_amount": format_decimal_amount_with_currency(donation.amount),
"receipt_date": (
donation.receipt_date < ignore_receipt_date
if ignore_receipt_date and donation.receipt_date
Expand Down Expand Up @@ -406,7 +397,7 @@ def send_jzwb_mailing(
"donor": donor,
"name": donor.get_full_name(),
"salutation": donor.get_salutation(),
"total_amount": format_amount(total_amount),
"total_amount": format_decimal_amount(total_amount),
}

jzwb_mail = jzwb_mails[year]
Expand Down
109 changes: 103 additions & 6 deletions fragdenstaat_de/fds_donation/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.conf import settings
from django.contrib.admin.widgets import ForeignKeyRawIdWidget
from django.core.validators import MinValueValidator
from django.utils import timezone
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _

Expand Down Expand Up @@ -44,7 +45,13 @@
Recurrence,
)
from .services import get_or_create_donor, send_donor_login_link, send_email_change_link
from .utils import MERGE_DONOR_FIELDS
from .utils import (
MERGE_DONOR_FIELDS,
format_amount_with_currency,
get_next_min_amount,
get_upgrade_amounts,
make_presets,
)
from .validators import validate_not_too_many_uppercase
from .widgets import AmountInput

Expand All @@ -69,6 +76,7 @@ class BasicDonationForm(StartPaymentMixin, forms.Form):
"class": "text-end",
},
presets=[],
min_value=MIN_AMOUNT,
),
)
interval = forms.TypedChoiceField(
Expand Down Expand Up @@ -150,7 +158,9 @@ def __init__(self, *args, **kwargs):
# No once option -> not choosing purpose
self.fields["purpose"].widget = forms.HiddenInput()

self.fields["amount"].widget.presets = self.settings["amount_presets"]
self.fields["amount"].widget.presets = make_presets(
self.settings["amount_presets"]
)
self.fields["reference"].initial = self.settings["reference"]
self.fields["keyword"].initial = self.settings["keyword"]
if self.settings["purpose"]:
Expand Down Expand Up @@ -321,6 +331,7 @@ class RemoteDonationForm(forms.Form):
"class": "text-end",
},
presets=[],
min_value=MIN_AMOUNT,
),
)
initial_interval = forms.TypedChoiceField(
Expand All @@ -343,7 +354,9 @@ def __init__(self, *args, **kwargs):
self.fields["initial_interval"].initial = (
self.settings.get("initial_interval", None) or interval_choices[0][0]
)
self.fields["initial_amount"].widget.presets = self.settings["amount_presets"]
self.fields["initial_amount"].widget.presets = make_presets(
self.settings["amount_presets"]
)
self.fields["initial_amount"].initial = self.settings["initial_amount"]
self.fields["pk_campaign"].initial = self.settings["reference"]
self.fields["pk_keyword"].initial = self.settings["keyword"]
Expand Down Expand Up @@ -773,6 +786,8 @@ def __init__(self, *args, **kwargs):
if self.gift_options:
self.fields["chosen_gift"].queryset = self.gift_options
self.fields["chosen_gift"].initial = self.gift_options[0].id
if len(self.gift_options) == 1:
self.fields["chosen_gift"].widget = forms.HiddenInput()
self.fields["chosen_gift"].widget.attrs["data-needs-address"] = json.dumps(
{gift.id: gift.needs_address for gift in self.gift_options}
)
Expand Down Expand Up @@ -803,6 +818,13 @@ def clean(self):
_("No donation gifts are available for you at the moment.")
)
DonationGiftLogic.clean(self)
self.related_donation = (
self.donor.donations.all().filter(completed=True).latest("timestamp")
)
if DonationGiftOrder.objects.filter(donation=self.related_donation).exists():
raise forms.ValidationError(
_("You have already ordered this donation gift.")
)

def save(self):
if self.cleaned_data["update_address"]:
Expand All @@ -811,10 +833,9 @@ def save(self):
self.donor.city = self.cleaned_data["shipping_city"]
self.donor.country = self.cleaned_data["shipping_country"]
self.donor.save()

order = DonationGiftOrder.objects.create(
donation=self.donor.donations.all()
.filter(completed=True)
.latest("timestamp"),
donation=self.related_donation,
donation_gift=self.cleaned_data["chosen_gift"],
first_name=self.cleaned_data["shipping_first_name"],
last_name=self.cleaned_data["shipping_last_name"],
Expand Down Expand Up @@ -946,3 +967,79 @@ def send_login_link(self):
except Donor.DoesNotExist:
donor = None
send_donor_login_link(donor, email, next_path=next_path)


class RecurrenceUpgradeForm(forms.Form):
reference = forms.CharField(widget=forms.HiddenInput, required=False)

def __init__(self, recurrence=None, choice_count=3, *args, **kwargs):
self.recurrence: Recurrence | None = recurrence
super().__init__(*args, **kwargs)
self.fields["recurrence"] = forms.ModelChoiceField(
Recurrence.objects.all(),
initial=recurrence,
widget=forms.HiddenInput,
required=True,
)
amounts = get_upgrade_amounts(
recurrence.amount, recurrence.interval, choice_count=choice_count
)
new_min_amount = get_next_min_amount(recurrence.amount)
choices = [
(
amount,
format_amount_with_currency(amount),
)
for amount in amounts
]
if recurrence.interval == 1:
label = _("Your new monthly amount:")
elif recurrence.interval == 12:
label = _("Your new yearly amount:")
elif recurrence.interval == 3:
label = _("Your new quarterly amount:")
else:
label = _("Your new amount every {} months:").format(recurrence.interval)
self.fields["upgrade_amount"] = forms.DecimalField(
localize=True,
required=True,
initial=None,
min_value=new_min_amount,
max_digits=19,
decimal_places=2,
label=label,
widget=AmountInput(
attrs={
"title": _("Amount in Euro, comma as decimal separator"),
"class": "text-end",
},
presets=choices,
min_value=new_min_amount,
),
)

def can_upgrade(self):
subscription = self.recurrence.subscription
if not subscription:
return False
modify_info = subscription.get_modify_info()
return modify_info.can_modify

def save(self):
amount = self.cleaned_data["upgrade_amount"]
if not amount:
return
if self.can_upgrade():
subscription = self.recurrence.subscription
return self.modify_subscription(subscription)

def modify_subscription(self, subscription):
provider = subscription.get_provider()
result = provider.modify_subscription(
subscription,
amount=self.cleaned_data["upgrade_amount"],
interval=subscription.plan.interval,
)
if result:
self.recurrence.update_from_subscription(last_upgrade=timezone.now())
return result
4 changes: 3 additions & 1 deletion fragdenstaat_de/fds_donation/listeners.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,10 @@ def process_new_donation(donation, received_now=False, domain_obj=None):
def subscription_was_canceled(sender, **kwargs):
if sender is None:
return

Recurrence.objects.filter(subscription=sender).update(cancel_date=sender.canceled)
recurrence = Recurrence.objects.filter(subscription=sender).first()
if recurrence:
detect_recurring_on_donor(recurrence.donor)


def user_email_changed(sender, old_email=None, **kwargs):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.2.10 on 2026-04-16 14:54

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('cms', '0041_alter_pageurl_unique_together_pageurl_site_and_more'),
('fds_donation', '0070_donor_recurrence_streak_start'),
]

operations = [
migrations.CreateModel(
name='UpgradeRecurrenceFormCMSPlugin',
fields=[
('cmsplugin_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='%(app_label)s_%(class)s', serialize=False, to='cms.cmsplugin')),
('next_url', models.CharField(blank=True, max_length=255)),
],
bases=('cms.cmsplugin',),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.2.10 on 2026-04-23 09:31

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('fds_donation', '0071_upgraderecurrenceformcmsplugin'),
]

operations = [
migrations.AddField(
model_name='upgraderecurrenceformcmsplugin',
name='choice_count',
field=models.SmallIntegerField(default=3),
),
]
Loading
Loading