- {% if share_owner %}
+ {% if share_owner and not share_owner.is_soft_deleted %}
{% translate "Status" %}:
diff --git a/tapir/coop/tests/test_shareowner_delete_view.py b/tapir/coop/tests/test_shareowner_delete_view.py
new file mode 100644
index 000000000..a31c47ba8
--- /dev/null
+++ b/tapir/coop/tests/test_shareowner_delete_view.py
@@ -0,0 +1,68 @@
+from http import HTTPStatus
+
+from django.urls import reverse
+
+from tapir import settings
+from tapir.accounts.tests.factories.factories import TapirUserFactory
+from tapir.coop.models import DeleteShareOwnerLogEntry, ShareOwner
+from tapir.utils.tests_utils import (
+ PermissionTestMixin,
+ FeatureFlagTestMixin,
+ TapirFactoryTestBase,
+)
+
+
+class TestShareOwnerDeleteView(
+ PermissionTestMixin, FeatureFlagTestMixin, TapirFactoryTestBase
+):
+ def setUp(self) -> None:
+ super().setUp()
+
+ def get_allowed_groups(self):
+ return [
+ settings.GROUP_VORSTAND,
+ settings.GROUP_EMPLOYEES,
+ ]
+
+ def do_request(self):
+ tapir_user = TapirUserFactory()
+ return self.client.post(
+ reverse("coop:shareowner_delete", args=[tapir_user.share_owner.id]),
+ follow=True,
+ )
+
+ def test_ShareOwnerDeleteView_default_setsDeletedAtField(self):
+ self.login_as_vorstand()
+ tapir_user = TapirUserFactory()
+ self.assertIsNotNone(tapir_user.share_owner)
+ self.assertIsNone(tapir_user.share_owner.deleted_at)
+ response = self.client.post(
+ reverse("coop:shareowner_delete", args=[tapir_user.share_owner.id]),
+ follow=True,
+ )
+ self.assertStatusCode(response, HTTPStatus.OK)
+ tapir_user.share_owner.refresh_from_db()
+ self.assertIsNotNone(tapir_user.share_owner.deleted_at)
+ self.assertEqual(DeleteShareOwnerLogEntry.objects.count(), 1)
+
+ def test_ShareOwnerDeleteView_normalMemberTriesToDeleteItself_doesntDelete(self):
+ tapir_user = self.login_as_normal_user()
+ response = self.client.post(
+ reverse("coop:shareowner_delete", args=[tapir_user.share_owner.id]),
+ follow=True,
+ )
+ self.assertStatusCode(response, HTTPStatus.FORBIDDEN)
+ tapir_user.share_owner.refresh_from_db()
+ self.assertIsNone(tapir_user.share_owner.deleted_at)
+ self.assertEqual(DeleteShareOwnerLogEntry.objects.count(), 0)
+
+ def test_ShareOwnerDeleteView_adminTriesToDeleteItself_doesntDelete(self):
+ tapir_user = self.login_as_vorstand()
+ response = self.client.post(
+ reverse("coop:shareowner_delete", args=[tapir_user.share_owner.id]),
+ follow=True,
+ )
+ self.assertStatusCode(response, HTTPStatus.FORBIDDEN)
+ tapir_user.share_owner.refresh_from_db()
+ self.assertIsNone(tapir_user.share_owner.deleted_at)
+ self.assertEqual(DeleteShareOwnerLogEntry.objects.count(), 0)
diff --git a/tapir/coop/tests/test_shareowner_softdelete.py b/tapir/coop/tests/test_shareowner_softdelete.py
new file mode 100644
index 000000000..ba4f36794
--- /dev/null
+++ b/tapir/coop/tests/test_shareowner_softdelete.py
@@ -0,0 +1,71 @@
+import pytest
+from django.urls import reverse
+from django.utils import timezone
+
+from tapir.accounts.tests.factories.factories import TapirUserFactory
+from tapir.coop.models import ShareOwner
+from tapir.coop.tests.factories import ShareOwnerFactory
+from tapir.utils.tests_utils import TapirFactoryTestBase
+
+
+class TestShareOwnershipSoftDelete(TapirFactoryTestBase):
+ def test_softDelete_default_setsDeletedAt(self):
+ share_owner = ShareOwnerFactory.create()
+
+ self.assertIsNone(share_owner.deleted_at)
+ share_owner.soft_delete()
+
+ share_owner.refresh_from_db()
+
+ self.assertIsNotNone(share_owner.deleted_at)
+ self.assertLessEqual(share_owner.deleted_at, timezone.now())
+
+ def test_restore_default_deletedAtSetToNone(self):
+ share_owner = ShareOwnerFactory.create()
+
+ share_owner.soft_delete()
+ share_owner.refresh_from_db()
+ self.assertIsNotNone(share_owner.deleted_at)
+
+ share_owner.restore()
+ share_owner.refresh_from_db()
+
+ self.assertIsNone(share_owner.deleted_at)
+
+ def test_shareOwnerDefaultManager_default_containsOnlyNotDeletedShareOwners(self):
+ active_owner = ShareOwnerFactory.create()
+
+ soft_deleted_owner = ShareOwnerFactory.create()
+ soft_deleted_owner.soft_delete()
+
+ non_deleted_owners = ShareOwner.objects.all()
+ self.assertIn(active_owner, non_deleted_owners)
+ self.assertNotIn(soft_deleted_owner, non_deleted_owners)
+
+ def test_shareOwnerEverythingManager_default_containsAlsoDeletedShareowners(self):
+ owner1 = ShareOwnerFactory.create()
+
+ owner2 = ShareOwnerFactory.create()
+ owner2.soft_delete()
+
+ all_owners = ShareOwner.everything.all()
+
+ self.assertIn(owner1, all_owners)
+ self.assertIn(owner2, all_owners)
+
+ def test_hardDelete_default_shareOwnerHardDeleted(self):
+ share_owner = ShareOwnerFactory.create()
+ all_users = ShareOwner.everything.all()
+ self.assertIn(share_owner, all_users)
+ share_owner.hard_delete()
+
+ all_users = ShareOwner.everything.all()
+ self.assertNotIn(share_owner, all_users)
+
+ def test_delete_cannotDeleteOwnAccount(self):
+ vorstand_user = self.login_as_vorstand()
+ response = self.client.post(
+ reverse("coop:shareowner_delete", args=[vorstand_user.share_owner.id])
+ )
+ self.assertEqual(response.status_code, 403)
+ self.assertIn(vorstand_user.share_owner, ShareOwner.everything.all())
diff --git a/tapir/coop/urls.py b/tapir/coop/urls.py
index b1c8c0dad..668231a13 100644
--- a/tapir/coop/urls.py
+++ b/tapir/coop/urls.py
@@ -135,6 +135,11 @@
views.ShareOwnerUpdateView.as_view(),
name="shareowner_update",
),
+ path(
+ "member//delete",
+ views.ShareOwnerDeleteView.as_view(),
+ name="shareowner_delete",
+ ),
path(
"statistics",
views.StatisticsView.as_view(),
diff --git a/tapir/coop/views/shareowner.py b/tapir/coop/views/shareowner.py
index 2ea6abdbc..078e1c622 100644
--- a/tapir/coop/views/shareowner.py
+++ b/tapir/coop/views/shareowner.py
@@ -8,6 +8,7 @@
from django.contrib import messages
from django.contrib.auth.decorators import permission_required, login_required
from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
+from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import Q, F, OuterRef, Subquery
from django.http import (
@@ -18,6 +19,7 @@
)
from django.shortcuts import get_object_or_404, redirect
from django.template import Template, Context
+from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django.views import generic, View
@@ -62,6 +64,8 @@
CreateShareOwnershipsLogEntry,
UpdateShareOwnershipLogEntry,
ExtraSharesForAccountingRecap,
+ ShareOwnerQuerySet,
+ DeleteShareOwnerLogEntry,
)
from tapir.coop.serializers import MemberRegistrationRequestSerializer
from tapir.coop.services.investing_status_service import InvestingStatusService
@@ -79,6 +83,7 @@
PERMISSION_COOP_ADMIN,
PERMISSION_ACCOUNTS_MANAGE,
PERMISSION_COOP_VIEW,
+ PERMISSION_GROUP_MANAGE,
)
from tapir.shifts.models import (
SHIFT_USER_CAPABILITY_CHOICES,
@@ -284,6 +289,35 @@ def get_context_data(self, **kwargs):
return context
+class ShareOwnerDeleteView(
+ LoginRequiredMixin,
+ PermissionRequiredMixin,
+ generic.DeleteView,
+):
+ permission_required = PERMISSION_GROUP_MANAGE
+ model = ShareOwner
+
+ def get_success_url(self):
+ return reverse("coop:shareowner_list")
+
+ def dispatch(self, request, *args, **kwargs):
+ obj = self.get_object()
+ if request.user == obj.user:
+ return HttpResponseForbidden("You cannot delete your own account.")
+ return super().dispatch(request, *args, **kwargs)
+
+ @transaction.atomic
+ def form_valid(self, form):
+ share_owner = self.get_object()
+ share_owner.soft_delete()
+ DeleteShareOwnerLogEntry().populate(
+ share_owner=share_owner,
+ actor=self.request.user,
+ model=self.object,
+ ).save()
+ return HttpResponseRedirect(self.get_success_url())
+
+
@require_POST
@csrf_protect
@login_required
@@ -675,7 +709,7 @@ def __init__(self, *args, **kwargs):
)
@staticmethod
- def shift_slot_filter(queryset: ShareOwner.ShareOwnerQuerySet, name, value: str):
+ def shift_slot_filter(queryset: ShareOwnerQuerySet, name, value: str):
return queryset.filter(
# Find all Tapir-Users currently enrolled in that shift-name "value"
user__in=Shift.objects.filter(
@@ -684,7 +718,7 @@ def shift_slot_filter(queryset: ShareOwner.ShareOwnerQuerySet, name, value: str)
).distinct()
@staticmethod
- def display_name_filter(queryset: ShareOwner.ShareOwnerQuerySet, name, value: str):
+ def display_name_filter(queryset: ShareOwnerQuerySet, name, value: str):
# This is an ugly hack to enable searching by Mitgliedsnummer from the
# one-stop search box in the top right
if value.isdigit():
@@ -692,13 +726,11 @@ def display_name_filter(queryset: ShareOwner.ShareOwnerQuerySet, name, value: st
return queryset.with_name(value).distinct()
- def status_filter(self, queryset: ShareOwner.ShareOwnerQuerySet, name, value: str):
+ def status_filter(self, queryset: ShareOwnerQuerySet, name, value: str):
return queryset.with_status(value, self.reference_time).distinct()
@staticmethod
- def shift_attendance_mode_filter(
- queryset: ShareOwner.ShareOwnerQuerySet, name, value: str
- ):
+ def shift_attendance_mode_filter(queryset: ShareOwnerQuerySet, name, value: str):
queryset = ShiftAttendanceModeService.annotate_share_owner_queryset_with_attendance_mode_at_datetime(
queryset
)
@@ -710,7 +742,7 @@ def shift_attendance_mode_filter(
@staticmethod
def registered_to_abcd_slot_with_capability_filter(
- queryset: ShareOwner.ShareOwnerQuerySet, name, value: str
+ queryset: ShareOwnerQuerySet, name, value: str
):
return queryset.filter(
user__in=TapirUser.objects.registered_to_abcd_shift_slot_with_capability(
@@ -720,44 +752,36 @@ def registered_to_abcd_slot_with_capability_filter(
@staticmethod
def registered_to_slot_with_capability_filter(
- queryset: ShareOwner.ShareOwnerQuerySet, name, value: str
+ queryset: ShareOwnerQuerySet, name, value: str
):
return queryset.filter(
user__in=TapirUser.objects.registered_to_shift_slot_with_capability(value)
).distinct()
@staticmethod
- def has_capability_filter(
- queryset: ShareOwner.ShareOwnerQuerySet, name, value: str
- ):
+ def has_capability_filter(queryset: ShareOwnerQuerySet, name, value: str):
return queryset.filter(
user__in=TapirUser.objects.has_capability(value)
).distinct()
@staticmethod
- def not_has_capability_filter(
- queryset: ShareOwner.ShareOwnerQuerySet, name, value: str
- ):
+ def not_has_capability_filter(queryset: ShareOwnerQuerySet, name, value: str):
return queryset.exclude(
user__in=TapirUser.objects.has_capability(value)
).distinct()
@staticmethod
- def has_tapir_account_filter(
- queryset: ShareOwner.ShareOwnerQuerySet, name, value: bool
- ):
+ def has_tapir_account_filter(queryset: ShareOwnerQuerySet, name, value: bool):
return queryset.exclude(user__isnull=value).distinct()
@staticmethod
- def abcd_week_filter(queryset: ShareOwner.ShareOwnerQuerySet, name, value: str):
+ def abcd_week_filter(queryset: ShareOwnerQuerySet, name, value: str):
return queryset.filter(
user__shift_attendance_templates__slot_template__shift_template__group__name=value
).distinct()
@staticmethod
- def is_fully_paid_filter(
- queryset: ShareOwner.ShareOwnerQuerySet, name, value: bool
- ):
+ def is_fully_paid_filter(queryset: ShareOwnerQuerySet, name, value: bool):
payment_filter = {
f"{PaymentStatusService.ANNOTATION_CREDITED_PAYMENTS_SUM_AT_DATE}__gte": F(
PaymentStatusService.ANNOTATION_EXPECTED_PAYMENTS_SUM_AT_DATE
@@ -771,7 +795,7 @@ def is_fully_paid_filter(
@staticmethod
def is_currently_exempted_from_shifts_filter(
- queryset: ShareOwner.ShareOwnerQuerySet, name, value: bool
+ queryset: ShareOwnerQuerySet, name, value: bool
):
exemption_filter = Q(
user__shift_user_data__shift_exemptions__in=ShiftExemption.objects.active_temporal()
@@ -781,21 +805,17 @@ def is_currently_exempted_from_shifts_filter(
return queryset.filter(exemption_filter).distinct()
@staticmethod
- def has_shift_partner_filter(
- queryset: ShareOwner.ShareOwnerQuerySet, name, value: bool
- ):
+ def has_shift_partner_filter(queryset: ShareOwnerQuerySet, name, value: bool):
return queryset.filter(user__shift_user_data__shift_partner__isnull=not value)
@staticmethod
- def is_shift_partner_of_filter(
- queryset: ShareOwner.ShareOwnerQuerySet, name, value: bool
- ):
+ def is_shift_partner_of_filter(queryset: ShareOwnerQuerySet, name, value: bool):
return queryset.filter(
user__shift_user_data__shift_partner_of__isnull=not value
)
@staticmethod
- def filter_by_join_date(queryset: ShareOwner.ShareOwnerQuerySet, name, value):
+ def filter_by_join_date(queryset: ShareOwnerQuerySet, name, value):
"""Filter ShareOwners based on their first share ownership start date."""
if value.start or value.stop:
# Get the earliest start date for each share owner
@@ -817,9 +837,7 @@ def filter_by_join_date(queryset: ShareOwner.ShareOwnerQuerySet, name, value):
return queryset.distinct()
@staticmethod
- def filter_by_user_date_joined(
- queryset: ShareOwner.ShareOwnerQuerySet, name, value
- ):
+ def filter_by_user_date_joined(queryset: ShareOwnerQuerySet, name, value):
"""Filter ShareOwners based on when their associated TapirUser account was created."""
if value.start or value.stop:
queryset = queryset.filter(user__isnull=False)
diff --git a/tapir/core/models.py b/tapir/core/models.py
index e6214643c..a08e30ab3 100644
--- a/tapir/core/models.py
+++ b/tapir/core/models.py
@@ -1,4 +1,5 @@
from django.db import models
+from django.utils import timezone
from django.utils.translation import gettext_lazy as _
@@ -27,3 +28,32 @@ def ensure_flag_exists(cls, flag_name):
if cls.objects.filter(flag_name=flag_name).exists():
return
cls.objects.create(flag_name=flag_name)
+
+
+class NonDeleted(models.Manager):
+ def get_queryset(self):
+ return super().get_queryset().filter(deleted_at__isnull=True)
+
+
+class SoftDeleteMixin(models.Model):
+ deleted_at = models.DateTimeField(null=True, blank=True, default=None)
+
+ class Meta:
+ abstract = True
+
+ objects = NonDeleted()
+ everything = models.Manager()
+
+ def soft_delete(self, using=None, keep_parents=False):
+ self.deleted_at = timezone.now()
+ self.save(update_fields=["deleted_at"], using=using)
+
+ def hard_delete(self, using=None, keep_parents=False):
+ super().delete(using=using, keep_parents=keep_parents)
+
+ def restore(self):
+ self.deleted_at = None
+ self.save(update_fields=["deleted_at"])
+
+ def is_soft_deleted(self):
+ return self.deleted_at is not None
diff --git a/tapir/translations/locale/de/LC_MESSAGES/django.po b/tapir/translations/locale/de/LC_MESSAGES/django.po
index b4497e421..2daa16ac8 100644
--- a/tapir/translations/locale/de/LC_MESSAGES/django.po
+++ b/tapir/translations/locale/de/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-02-07 14:00+0100\n"
+"POT-Creation-Date: 2026-02-07 17:28+0100\n"
"PO-Revision-Date: 2025-07-07 13:06+0000\n"
"Last-Translator: Weblate Admin \n"
"Language-Team: German \n"
@@ -64,12 +64,12 @@ msgstr "Wichtige E-Mails"
msgid "Displayed name"
msgstr "Angezeigter Name"
-#: accounts/models.py:70 coop/models.py:70 coop/models.py:445
+#: accounts/models.py:70 coop/models.py:153 coop/models.py:450
msgid "Pronouns"
msgstr "Pronomen"
#: accounts/models.py:71 accounts/templates/accounts/user_detail.html:71
-#: coop/models.py:72 coop/models.py:447
+#: coop/models.py:155 coop/models.py:452
#: coop/templates/coop/draftuser_detail.html:71
#: coop/templates/coop/draftuser_detail.html:142
#: coop/templates/coop/shareowner_detail.html:51
@@ -77,29 +77,29 @@ msgid "Phone number"
msgstr "Telefonnummer"
#: accounts/models.py:72 accounts/templates/accounts/user_detail.html:81
-#: coop/models.py:73 coop/models.py:448
+#: coop/models.py:156 coop/models.py:453
#: coop/templates/coop/draftuser_detail.html:146
#: coop/templates/coop/shareowner_detail.html:55
msgid "Birthdate"
msgstr "Geburtsdatum"
-#: accounts/models.py:73 coop/models.py:74 coop/models.py:449
+#: accounts/models.py:73 coop/models.py:157 coop/models.py:454
msgid "Street and house number"
msgstr "Straße und Hausnummer"
-#: accounts/models.py:74 coop/models.py:75 coop/models.py:450
+#: accounts/models.py:74 coop/models.py:158 coop/models.py:455
msgid "Extra address line"
msgstr "Adresszusatz"
-#: accounts/models.py:75 coop/models.py:76 coop/models.py:451
+#: accounts/models.py:75 coop/models.py:159 coop/models.py:456
msgid "Postcode"
msgstr "Postleitzahl"
-#: accounts/models.py:76 coop/models.py:77 coop/models.py:452
+#: accounts/models.py:76 coop/models.py:160 coop/models.py:457
msgid "City"
msgstr "Ort"
-#: accounts/models.py:77 coop/models.py:78 coop/models.py:453
+#: accounts/models.py:77 coop/models.py:161 coop/models.py:458
msgid "Country"
msgstr "Land"
@@ -126,7 +126,7 @@ msgid "Allow purchase tracking"
msgstr "Erlaube Aufzeichnen deiner Einkäufe"
#: accounts/models.py:90 accounts/templates/accounts/user_detail.html:101
-#: coop/models.py:81 coop/models.py:456
+#: coop/models.py:164 coop/models.py:461
#: coop/templates/coop/shareowner_detail.html:75
msgid "Preferred Language"
msgstr "Bevorzugte Sprache"
@@ -289,8 +289,8 @@ msgid "I agree that my membership card will be scanned at the checkout when I ma
msgstr "Ich bin damit einverstanden, dass meine Mitgliedskarte beim Einkauf an der Kasse gescannt und somit mein Einkauf erfasst und gespeichert wird:"
#: accounts/templates/accounts/purchase_tracking_card.html:30
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:168
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:172
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:177
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:181
msgid "Yes,No"
msgstr "Ja, Nein"
@@ -307,7 +307,7 @@ msgstr "Aktivieren"
msgid "You can only look at your own barcode unless you have admin rights"
msgstr "Du kannst nur deinen eigenen Strichcode sehen, es sei denn du hast Admin-Rechte"
-#: accounts/templates/accounts/user_detail.html:20 coop/models.py:724
+#: accounts/templates/accounts/user_detail.html:20 coop/models.py:729
#: coop/templates/coop/draftuser_detail.html:86
#: coop/templates/coop/shareowner_detail.html:10 log/views.py:94
#: log/views.py:152 shifts/templates/shifts/shift_detail_printable.html:50
@@ -337,9 +337,9 @@ msgstr "Benutzername bearbeiten"
#: coop/templates/coop/draftuser_detail.html:238
#: coop/templates/coop/membershipresignation_detail.html:107
#: coop/templates/coop/shareowner_detail.html:30
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:22
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:90
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:113
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:23
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:99
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:122
#: core/templates/core/featureflag_list.html:30
#: shifts/templates/shifts/shift_detail.html:61
#: shifts/templates/shifts/shift_template_detail.html:29
@@ -530,8 +530,8 @@ msgstr "Schickt mir eine Anleitung!"
msgid "Enter a valid username. This value may contain only letters, numbers, and ./-/_ characters."
msgstr "Der angegebene Name ist ungültig. Er darf nur Buchstaben, Zahlen und die Symbole ./-/_ enthalten."
-#: accounts/views.py:129 accounts/views.py:134 coop/views/shareowner.py:276
-#: coop/views/shareowner.py:281
+#: accounts/views.py:129 accounts/views.py:134 coop/views/shareowner.py:281
+#: coop/views/shareowner.py:286
#, python-format
msgid "Edit member: %(name)s"
msgstr "Mitglied bearbeiten: %(name)s"
@@ -648,7 +648,7 @@ msgstr "Anzahl zu erstellender Anteile"
msgid "The end date must be later than the start date."
msgstr "Das Enddatum muss später als das Start-Datum sein"
-#: coop/forms.py:105 coop/models.py:462
+#: coop/forms.py:105 coop/models.py:467
msgid "Number of Shares"
msgstr "Anzahl Anteile"
@@ -729,168 +729,168 @@ msgstr "Absender und Empfänger der Übertragung der Anteile können nicht ident
msgid "Cannot pay out, because shares have been gifted."
msgstr "Die Anteile können nicht ausgezahlt werden, da sie verschenkt wurden"
-#: coop/models.py:54
+#: coop/models.py:137
msgid "Is company"
msgstr "Ist eine Firma"
-#: coop/models.py:61 coop/models.py:436
+#: coop/models.py:144 coop/models.py:441
msgid "Administrative first name"
msgstr "Amtlicher Vorname"
-#: coop/models.py:63 coop/models.py:438
+#: coop/models.py:146 coop/models.py:443
msgid "Last name"
msgstr "Nachname"
-#: coop/models.py:65 coop/models.py:440
+#: coop/models.py:148 coop/models.py:445
msgid "Usage name"
msgstr "Angezeigter Name"
-#: coop/models.py:71 coop/models.py:446
+#: coop/models.py:154 coop/models.py:451
msgid "Email address"
msgstr "E-Mail-Adresse"
-#: coop/models.py:88
+#: coop/models.py:171
msgid "Is investing member"
msgstr "Ist investierendes Mitglied"
-#: coop/models.py:90 coop/models.py:476
+#: coop/models.py:173 coop/models.py:481
#: coop/templates/coop/draftuser_detail.html:176
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:48
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:167
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:57
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:176
msgid "Ratenzahlung"
msgstr "Ratenzahlung"
-#: coop/models.py:92 coop/models.py:469
+#: coop/models.py:175 coop/models.py:474
msgid "Attended Welcome Session"
msgstr "An Willkommenstreffen teilgenommen"
-#: coop/models.py:95
+#: coop/models.py:178
msgid "Is willing to gift a share"
msgstr "Ist bereit Anteile zu verschenken"
-#: coop/models.py:199
+#: coop/models.py:204
msgid "Cannot be a company and have a Tapir account"
msgstr "Kann keine Firma sein und ein Tapir-Konto haben"
-#: coop/models.py:215
+#: coop/models.py:220
msgid "User info should be stored in associated Tapir account"
msgstr "Benutzer Infos sollen in dem Tapir Konto gespeichert warden"
-#: coop/models.py:367
+#: coop/models.py:372
msgid "Not a member"
msgstr "Kein Mitlied"
-#: coop/models.py:368 coop/templates/coop/draftuser_detail.html:169
+#: coop/models.py:373 coop/templates/coop/draftuser_detail.html:169
msgid "Investing"
msgstr "Investierend"
-#: coop/models.py:369 coop/templates/coop/draftuser_detail.html:171
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:103
+#: coop/models.py:374 coop/templates/coop/draftuser_detail.html:171
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:112
#: coop/views/statistics.py:135
msgid "Active"
msgstr "Aktiv"
-#: coop/models.py:370
+#: coop/models.py:375
msgid "Paused"
msgstr "Pausiert"
-#: coop/models.py:465
+#: coop/models.py:470
msgid "Investing member"
msgstr "Investierendes Mitglied"
-#: coop/models.py:472
+#: coop/models.py:477
msgid "Signed Beteiligungserklärung"
msgstr "Beteiligungserklärung unterschrieben"
-#: coop/models.py:474
+#: coop/models.py:479
msgid "Paid Entrance Fee"
msgstr "Eintrittsgeld bezahlt"
-#: coop/models.py:531
+#: coop/models.py:536
msgid "Email address must be set."
msgstr "Email-Adresse muss gesetzt sein."
-#: coop/models.py:533
+#: coop/models.py:538
msgid "First name must be set."
msgstr "Vorname muss gesetzt sein."
-#: coop/models.py:535
+#: coop/models.py:540
msgid "Last name must be set."
msgstr "Nachname muss gesetzt sein."
-#: coop/models.py:539
+#: coop/models.py:544
msgid "Membership agreement must be signed."
msgstr "Mitgliedsantrag muss unterschrieben sein."
-#: coop/models.py:541
+#: coop/models.py:546
msgid "Amount of requested shares must be positive."
msgstr "Die Anzahl der erwünschten Anteile muss positiv sein."
-#: coop/models.py:543
+#: coop/models.py:548
msgid "Member already created."
msgstr "Mitglied schon vorhanden."
-#: coop/models.py:570
+#: coop/models.py:575
msgid "Paying member"
msgstr "Zahlendes Mitglied"
-#: coop/models.py:578
+#: coop/models.py:583
msgid "Credited member"
msgstr "Empfangendes Mitglied"
-#: coop/models.py:585
+#: coop/models.py:590
msgid "Amount"
msgstr "Betrag"
-#: coop/models.py:591
+#: coop/models.py:596
msgid "Payment date"
msgstr "Zahldatum"
-#: coop/models.py:594
+#: coop/models.py:599
msgid "Creation date"
msgstr "Erstellungsdatum"
-#: coop/models.py:599
+#: coop/models.py:604
msgid "Created by"
msgstr "Erstellt durch"
-#: coop/models.py:774
+#: coop/models.py:779
msgid "The cooperative buys the shares back from the member"
msgstr "Die Kooperative kauft die Anteile des Mitglieds zurück."
-#: coop/models.py:777
+#: coop/models.py:782
msgid "The member gifts the shares to the cooperative"
msgstr "Das Mitglied schenkt die Anteile der Genossenschaft."
-#: coop/models.py:779
+#: coop/models.py:784
msgid "The shares get transferred to another member"
msgstr "Die Anteile werden an ein anderes Mitglied übertragen"
-#: coop/models.py:782
+#: coop/models.py:787
msgid "Financial reasons"
msgstr "Finanzielle Gründe"
-#: coop/models.py:783
+#: coop/models.py:788
msgid "Health reasons"
msgstr "Gesundheit"
-#: coop/models.py:784
+#: coop/models.py:789
msgid "Distance"
msgstr "Entfernung"
-#: coop/models.py:785
+#: coop/models.py:790
msgid "Strategic orientation of SuperCoop"
msgstr "Strategische Ausrichtung von Supercoop"
-#: coop/models.py:786
+#: coop/models.py:791
msgid "Other"
msgstr "Andere"
-#: coop/models.py:791 coop/templates/coop/membershipresignation_detail.html:55
+#: coop/models.py:796 coop/templates/coop/membershipresignation_detail.html:55
msgid "Shareowner"
msgstr "Genossenschaftsmitglied"
-#: coop/models.py:810
+#: coop/models.py:815
msgid "Leave this empty if the resignation type is not a transfer to another member"
msgstr "Lass das Feld leer, wenn es sich nicht um eine Übertragung auf ein anderes Mitglied handelt"
@@ -929,7 +929,9 @@ msgstr "Löschen bestätigen"
#: coop/templates/coop/confirm_delete_incoming_payment.html:31
#: coop/templates/coop/confirm_delete_share_ownership.html:24
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:127
+#: coop/templates/coop/shareowner_confirm_delete.html:24
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:30
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:136
#: financingcampaign/templates/financingcampaign/confirm_delete.html:24
#: shifts/templates/shifts/shift_confirm_delete.html:30
msgid "Delete"
@@ -991,9 +993,9 @@ msgid "List of similar members"
msgstr "Liste ähnlicher Mitglieder"
#: coop/templates/coop/draftuser_detail.html:69
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:31
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:88
-#: coop/views/shareowner.py:602
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:40
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:97
+#: coop/views/shareowner.py:636
#: shifts/templates/shifts/user_shifts_overview_tag.html:26
msgid "Status"
msgstr "Status"
@@ -1016,7 +1018,7 @@ msgid "Shares requested"
msgstr "Angeforderte Anteile"
#: coop/templates/coop/draftuser_detail.html:190
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:53
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:62
#: shifts/models.py:46
msgid "Welcome Session"
msgstr "Willkommenstreffen"
@@ -1974,7 +1976,7 @@ msgid "General Tapir Accounts"
msgstr "Allgemeine Tapir-Konten"
#: coop/templates/coop/incoming_payment_list.html:9
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:160
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:169
msgid "Payments"
msgstr "Zahlungen"
@@ -2011,6 +2013,14 @@ msgstr ""
"\n"
" Reaktiviertes ausgeschiedenes Mitglied\n"
+#: coop/templates/coop/log/delete_share_owner_log_entry.html:2
+#, fuzzy
+#| msgid "Member can be created"
+msgid ""
+"\n"
+"Member has been deleted.\n"
+msgstr "Mitglied kann erstellt werden: "
+
#: coop/templates/coop/log/update_incoming_payment_log_entry.html:2
msgid "Updated payment:"
msgstr "Aktualisierte Zahlung:"
@@ -2264,6 +2274,26 @@ msgstr ""
msgid "Organisation logo"
msgstr ""
+#: coop/templates/coop/shareowner_confirm_delete.html:7
+#: coop/templates/coop/shareowner_confirm_delete.html:13
+#, fuzzy
+#| msgid "Confirm delete"
+msgid "Confirm Delete"
+msgstr "Löschen bestätigen"
+
+#: coop/templates/coop/shareowner_confirm_delete.html:16
+msgid "Are you sure you want to delete this member?"
+msgstr ""
+
+#: coop/templates/coop/shareowner_confirm_delete.html:18
+msgid "Please note that deleting this member will permanently remove their account and all associated data such as owned shares and payments. The deleted user will no longer be part of the cooperative. However, all shift records and logs will remain intact."
+msgstr ""
+
+#: coop/templates/coop/shareowner_confirm_delete.html:27
+#: shifts/templates/shifts/user_shifts_overview_tag.html:177
+msgid "Cancel"
+msgstr "Abmelden"
+
#: coop/templates/coop/shareowner_detail.html:22
msgid "Go to user page"
msgstr "Zur Nutzer*innen-Seite"
@@ -2360,11 +2390,11 @@ msgstr ""
" Mitglied #%(coop_share_owner_id)s\n"
" "
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:17
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:18
msgid "Membership confirmation"
msgstr "Mitgliedsbestätigung"
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:35
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:44
#, python-format
msgid ""
"\n"
@@ -2372,7 +2402,7 @@ msgid ""
" "
msgstr ""
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:40
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:49
#, python-format
msgid ""
"\n"
@@ -2380,7 +2410,7 @@ msgid ""
" "
msgstr ""
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:56
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:65
#: shifts/models.py:943 shifts/templates/shifts/shift_day_printable.html:207
#: shifts/templates/shifts/shift_day_printable.html:269
#: shifts/templates/shifts/shift_detail.html:298
@@ -2388,36 +2418,36 @@ msgstr ""
msgid "Attended"
msgstr "Teilgenommen"
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:58
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:67
#: shifts/models.py:942
msgid "Pending"
msgstr "Ausstehend"
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:65
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:74
msgid "Mark Attended"
msgstr "Als teilgenommen markieren"
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:74
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:83
msgid "Owned shares"
msgstr "Anteile"
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:82
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:91
msgid "List of shares owned by this member"
msgstr "Liste der Anteile die dieses Mitglied gehören"
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:86
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:95
msgid "Starts at"
msgstr "Anfangsdatum"
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:87
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:96
msgid "Ends at"
msgstr "Endet am"
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:105
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:114
msgid "Sold or future"
msgstr ""
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:130
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:139
msgid ""
"\n"
" Only use this to correct mistakes, i.e. if the share was\n"
@@ -2431,19 +2461,19 @@ msgstr ""
"Bitte nur verwenden, wenn ein tatsächlicher Fehler vorliegt, z.B. ein Anteil eingetragen wurde, der nie gekauft wurde. Wenn die Person ihren Anteil einfach an den Coop zurückverkauft hat, markiere den Anteil bitte als 'verkauft'.\n"
" "
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:153
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:162
msgid "Add Shares"
msgstr "Anteile hinzufügen"
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:171
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:180
msgid "Willing to gift a share"
msgstr "Bereit Anteile zu schenken"
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:181
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:190
msgid "Send membership confirmation email"
msgstr "Mitgliedsbestätigung per Mail senden"
-#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:187
+#: coop/templates/coop/tags/user_coop_share_ownership_list_tag.html:196
msgid "User is not a cooperative member."
msgstr "Benutzer*in ist kein Genossenschaftsmitglied."
@@ -2523,66 +2553,66 @@ msgstr "Auszahlungsende"
msgid "Cancel membership of %(name)s"
msgstr "Beende Mitgliedschaft von %(name)s"
-#: coop/views/shareowner.py:137 coop/views/shareowner.py:142
+#: coop/views/shareowner.py:142 coop/views/shareowner.py:147
#, python-format
msgid "Edit share: %(name)s"
msgstr "Anteil bearbeiten: %(name)s"
-#: coop/views/shareowner.py:162 coop/views/shareowner.py:167
+#: coop/views/shareowner.py:167 coop/views/shareowner.py:172
#, python-format
msgid "Add shares to %(name)s"
msgstr "Anteile zu %(name)s hinzufügen"
-#: coop/views/shareowner.py:381
+#: coop/views/shareowner.py:415
msgid "Membership confirmation email sent."
msgstr "Mitgliedsbestätigung per Mail gesendet."
-#: coop/views/shareowner.py:603 shifts/templates/shifts/shift_filters.html:45
+#: coop/views/shareowner.py:637 shifts/templates/shifts/shift_filters.html:45
msgid "Any"
msgstr "Alle"
-#: coop/views/shareowner.py:608
+#: coop/views/shareowner.py:642
#: shifts/templates/shifts/user_shifts_overview_tag.html:79
msgid "Shift Status"
msgstr "Schichtstatus"
-#: coop/views/shareowner.py:616
+#: coop/views/shareowner.py:650
msgid "Is registered to an ABCD-slot that requires a qualification"
msgstr "Ist für eine ABCD-Schicht eingetragen, die Qualifikation erfordert"
-#: coop/views/shareowner.py:624
+#: coop/views/shareowner.py:658
msgid "Is registered to a slot that requires a qualification"
msgstr "Ist für eine Schicht eingetragen, die Qualifikation erfordert"
-#: coop/views/shareowner.py:632
+#: coop/views/shareowner.py:666
msgid "Has qualification"
msgstr "Hat Qualifikation"
-#: coop/views/shareowner.py:640
+#: coop/views/shareowner.py:674
msgid "Does not have qualification"
msgstr "Hat die Qualifikation nicht"
-#: coop/views/shareowner.py:647 shifts/forms.py:777
+#: coop/views/shareowner.py:681 shifts/forms.py:777
msgid "ABCD Week"
msgstr "ABCD-Woche"
-#: coop/views/shareowner.py:650
+#: coop/views/shareowner.py:684
msgid "Is fully paid"
msgstr "Hat vollständig bezahlt"
-#: coop/views/shareowner.py:653
+#: coop/views/shareowner.py:687
msgid "Name or member ID"
msgstr "Name oder Mitgliedsnummer"
-#: coop/views/shareowner.py:657
+#: coop/views/shareowner.py:691
msgid "Is currently exempted from shifts"
msgstr "Ist derzeit von der Schichtarbeit befreit"
-#: coop/views/shareowner.py:662
+#: coop/views/shareowner.py:696
msgid "Shift Name"
msgstr "Schichtname"
-#: coop/views/shareowner.py:978
+#: coop/views/shareowner.py:996
msgctxt "Willing to give a share"
msgid "No"
msgstr "Nein"
@@ -2659,11 +2689,11 @@ msgstr "Mitgliederbüro kontaktieren"
msgid "About tapir"
msgstr "Über Tapir"
-#: core/models.py:7
+#: core/models.py:8
msgid "Flag name"
msgstr ""
-#: core/models.py:10
+#: core/models.py:11
msgid "Flag value"
msgstr ""
@@ -4717,10 +4747,6 @@ msgstr "Solidaritäts-Schicht spenden"
msgid "One of your banked shifts will be donated as a solidarity shift. Do you want to continue?"
msgstr "Eine deiner gearbeiteten Schichten wird als Solidaritäts-Schicht gespendet. Möchtest du fortfahren?"
-#: shifts/templates/shifts/user_shifts_overview_tag.html:177
-msgid "Cancel"
-msgstr "Abmelden"
-
#: shifts/templates/shifts/user_shifts_overview_tag.html:180
msgid "Confirm"
msgstr ""