Skip to content

Commit

Permalink
Implementation of the cancellation process in Tapir (#495)
Browse files Browse the repository at this point in the history
* Correct some mistakes and included some new translations (Kersten)

* Implementation of the cancellation process in Tapir.

* Little margin and padding improvements for better prints of shifts (regarding safe area)

* Fixes of the cancellation-process-implementation

* Revert "Little margin and padding improvements for better prints of shifts (regarding safe area)"

This reverts commit 18dd282.

* Removed comments and added new line at the end of files

* More fixes and new migration

* migrations files

* old migrations files removed

* some final improvements part 1

* removed space between 388-389 in coop/models.py

* removed space between 208-209 in models.py

* now the shareowner.py
  • Loading branch information
kerstenkenan authored Jun 3, 2024
1 parent 7e7db40 commit 8d6ee38
Show file tree
Hide file tree
Showing 17 changed files with 692 additions and 12 deletions.
3 changes: 2 additions & 1 deletion tapir/coop/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.contrib import admin

from tapir.coop.models import ShareOwnership, DraftUser
from tapir.coop.models import ShareOwnership, DraftUser, ResignedMembership

admin.site.register(ShareOwnership)
admin.site.register(DraftUser)
admin.site.register(ResignedMembership)
72 changes: 69 additions & 3 deletions tapir/coop/forms.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from django.utils import timezone
from dateutil.relativedelta import relativedelta
from django import forms
from django.core.exceptions import ValidationError
from django.forms import DateField, IntegerField
Expand All @@ -15,8 +17,10 @@
ShareOwner,
IncomingPayment,
MembershipPause,
ResignedMembership,
TapirUser,
)
from tapir.shifts.forms import ShareOwnerChoiceField
from tapir.shifts.forms import ShareOwnerChoiceField, TapirUserChoiceField
from tapir.utils.forms import DateInputTapir, TapirPhoneNumberField


Expand Down Expand Up @@ -235,8 +239,6 @@ class Meta:
"then the fields can be different."
)
)


class MembershipPauseForm(forms.ModelForm):
class Meta:
model = MembershipPause
Expand All @@ -247,3 +249,67 @@ class Meta:
}

share_owner = ShareOwnerChoiceField()

class MembershipCancelForm(forms.ModelForm):
already_resigned = ResignedMembership.objects.values("share_owner")
in_three_years = _(f"Coop buys back share(s)")
cancellation_reason = forms.CharField(max_length=1000, widget=forms.Textarea(
attrs={"rows": 2, "placeholder": _("Please not more than 1000 characters.")}
))
coop_buys_shares_back = forms.BooleanField(label=in_three_years, required=False)
share_owner = ShareOwnerChoiceField()
willing_to_gift_shares_to_coop = forms.BooleanField(label="Willing to gift shares to coop", required=False)
transfering_shares_to = TapirUserChoiceField(required=False, label=_("Transfering share(s) to"))
class Meta:
model = ResignedMembership
fields = ["share_owner",
"cancellation_reason",
"cancellation_date",
"coop_buys_shares_back",
"willing_to_gift_shares_to_coop",
"transfering_shares_to",
"paid_out",
]
widgets = {"cancellation_date": DateInputTapir()}

def clean(self):
cleaned_data = super().clean()
share_owner = cleaned_data.get("share_owner")
coop_buys_shares_back = cleaned_data.get("coop_buys_shares_back")
willing_to_gift_shares_to_coop = cleaned_data.get("willing_to_gift_shares_to_coop")
transfering_shares_to = cleaned_data.get("transfering_shares_to")
paid_out = cleaned_data.get("paid_out")
cancellation_date = cleaned_data.get("cancellation_date")
errmsg = _("Please take only one choice.")

if self.instance.pk is None:
for alreadyResignedMember in self.already_resigned.values("share_owner"):
if alreadyResignedMember['share_owner'] == share_owner.id:
self.add_error("share_owner", ValidationError(
_("This member is already resigned.")
))
break

if coop_buys_shares_back and willing_to_gift_shares_to_coop:
self.add_error("coop_buys_shares_back", errmsg)
self.add_error("willing_to_gift_shares_to_coop", errmsg)
elif transfering_shares_to != None and (coop_buys_shares_back or willing_to_gift_shares_to_coop):
self.add_error("transfering_shares_to", errmsg)
if coop_buys_shares_back:
self.add_error("coop_buys_shares_back", errmsg)
elif willing_to_gift_shares_to_coop:
self.add_error("willing_to_gift_shares_to_coop", errmsg)
if transfering_shares_to is not None:
if transfering_shares_to.share_owner == share_owner:
self.add_error("transfering_shares_to", ValidationError(
_("Sender and receiver of tranfering the share(s) cannot be the same.")
))
if (transfering_shares_to != None and paid_out) or (willing_to_gift_shares_to_coop and paid_out) :
self.add_error("paid_out", ValidationError(_("Cannot pay out, because shares have been gifted.")
))
if transfering_shares_to == None and not willing_to_gift_shares_to_coop and not coop_buys_shares_back:
self.add_error("transfering_shares_to", ValidationError(_("Please make a least one choice.")))
self.add_error("willing_to_gift_shares_to_coop", ValidationError(_("Please make a least one choice.")))
self.add_error("coop_buys_shares_back", ValidationError(_("Please make a least one choice.")))
if coop_buys_shares_back:
self.instance.pay_out_day = cancellation_date + relativedelta(day=31, month=12, years=3)
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Generated by Django 3.2.23 on 2024-06-01 16:34

from django.conf import settings
import django.contrib.postgres.fields.hstore
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone


class Migration(migrations.Migration):

dependencies = [
('log', '0006_alter_emaillogentry_email_content'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('coop', '0041_auto_20231221_1403'),
]

operations = [
migrations.CreateModel(
name='ResignMembershipCreateLogEntry',
fields=[
('logentry_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='log.logentry')),
('values', django.contrib.postgres.fields.hstore.HStoreField()),
],
options={
'abstract': False,
},
bases=('log.logentry',),
),
migrations.CreateModel(
name='ResignMembershipUpdateLogEntry',
fields=[
('logentry_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='log.logentry')),
('old_values', django.contrib.postgres.fields.hstore.HStoreField()),
('new_values', django.contrib.postgres.fields.hstore.HStoreField()),
],
options={
'abstract': False,
},
bases=('log.logentry',),
),
migrations.CreateModel(
name='ResignedMembership',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('cancellation_date', models.DateField(blank=True, default=django.utils.timezone.now)),
('pay_out_day', models.DateField(null=True)),
('cancellation_reason', models.CharField(max_length=1000)),
('coop_buys_shares_back', models.BooleanField()),
('willing_to_gift_shares_to_coop', models.BooleanField()),
('paid_out', models.BooleanField(default=False)),
('share_owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='coop.shareowner', verbose_name='Shareowner')),
('transfering_shares_to', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='TapirUser')),
],
),
]
63 changes: 61 additions & 2 deletions tapir/coop/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
from tapir.utils.shortcuts import get_html_link
from tapir.utils.user_utils import UserUtils


class ShareOwner(models.Model):
"""ShareOwner represents a share_owner of a ShareOwnership.
Expand Down Expand Up @@ -207,7 +206,7 @@ def clean(self):

def get_display_name(self, display_type):
return UserUtils.build_display_name(self, display_type)

def get_html_link(self, display_type):
return get_html_link(
url=self.get_absolute_url(), text=self.get_display_name(display_type)
Expand Down Expand Up @@ -719,3 +718,63 @@ def populate(
old_frozen=old_frozen,
new_frozen=new_frozen,
)

class ResignedMembership(models.Model):
share_owner = models.ForeignKey(
ShareOwner, on_delete=models.deletion.CASCADE, verbose_name=_("Shareowner")
)
cancellation_date = models.DateField(
default=timezone.now,
blank=True,
)
pay_out_day = models.DateField(null=True)
cancellation_reason = models.CharField(max_length = 1000)
coop_buys_shares_back = models.BooleanField()
willing_to_gift_shares_to_coop = models.BooleanField()
transfering_shares_to = models.ForeignKey(
TapirUser, on_delete=models.deletion.CASCADE, verbose_name=_("TapirUser"), null=True,
)
paid_out = models.BooleanField(default=False)

class ResignedMemberQuerySet(models.QuerySet):
def with_term(self, search_string: str):
searches = [s for s in search_string.split(" ") if s != ""]

for search in searches:
word_filter = (
Q(share_owner__user__usage_name__unaccent__icontains=search)
| Q(share_owner__user__first_name__unaccent__icontains=search)
| Q(share_owner__user__last_name__unaccent__icontains=search)
| Q(share_owner__id__icontains=search)
)
return self.filter(word_filter)

objects = ResignedMemberQuerySet.as_manager()

class ResignMembershipCreateLogEntry(ModelLogEntry):
template_name = "coop/log/create_resignmember_log_entry.html"

def populate(
self,
actor: User,
model: ResignedMembership,
):
return super().populate_base(
actor=actor, share_owner=model.share_owner, model=model
)
class ResignMembershipUpdateLogEntry(UpdateModelLogEntry):
template_name = "coop/log/update_resignmember_log_entry.html"

def populate(
self,
old_frozen: dict,
new_frozen: dict,
model: ResignedMembership,
actor: User,
):
return super().populate_base(
actor=actor,
share_owner=model.share_owner,
old_frozen=old_frozen,
new_frozen=new_frozen,
)
43 changes: 43 additions & 0 deletions tapir/coop/services/ResignMemberService.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import datetime

from tapir.accounts.models import TapirUser
from tapir.coop.models import ResignedMembership, ShareOwnership
from tapir.shifts.models import ShiftAttendanceTemplate, ShiftAttendance

from tapir.utils.shortcuts import get_timezone_aware_datetime

class ResignMemberService:
@staticmethod
def update_shifts_and_shares(member: ResignedMembership):
tapir_user: TapirUser = getattr(member.share_owner, "user", None)
if not tapir_user:
print("Couldn't find an existing Tapir user.")
return

new_end_date = member.cancellation_date
new_end_date = new_end_date.replace(month=12)
new_end_date = new_end_date.replace(day=31)
new_end_date = new_end_date.replace(year=new_end_date.year + 3)
ShareOwnership.objects.filter(share_owner=member.share_owner).update(end_date=new_end_date)

for attendance_template in ShiftAttendanceTemplate.objects.filter(
user=tapir_user
):
start_date = get_timezone_aware_datetime(
member.cancellation_date, datetime.time()
)
attendance_template.cancel_attendances(start_date)
attendance_template.delete()

attendances = ShiftAttendance.objects.filter(
user=tapir_user,
slot__shift__start_time__gte=start_date,
state=ShiftAttendance.State.PENDING,
)

for attendance in attendances:
attendance.state = ShiftAttendance.State.CANCELLED
attendance.save()

def delete_end_dates(member: ResignedMembership):
ShareOwnership.objects.filter(share_owner=member.share_owner).update(end_date=None)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{% load i18n %}
{% blocktranslate with member=entry.values.share_owner cancellation_reason=entry.values.cancellation_reason %}
Member resigned for reason: {{ cancellation_reason }}
{% endblocktranslate %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% load i18n %}
{% translate "Updated resigned membership:" %}
<br />
{% for change in changes %}
<strong>{{ change.0 }}</strong>: {{ change.1 }} → {{ change.2 }}
<br />
{% endfor %}
4 changes: 4 additions & 0 deletions tapir/coop/templates/coop/member_management.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ <h5 class="card-header">{% translate "Lists" %}</h5>
href="{% url 'coop:matching_program_list' %}">
<span class="material-icons button-icon">card_giftcard</span>{% translate 'Matching program' %}
</a>
<a class="{% tapir_button_link %}"
href="{% url 'coop:resigned_members_list' %}">
<span class="material-icons button-icon">close</span>{% translate 'Resigned members' %}
</a>
</div>
</div>
<div class="card mb-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ <h5>{% translate "Membership pauses" %}</h5>
</div>
</div>
<a class="{% tapir_button_link_to_action %}"
href="{% url "coop:membership_pause_create" %}">
href="{% url 'coop:membership_pause_create' %}">
<span class="material-icons">add_circle_outline</span>
{% translate "Create a new pause" %}
</a>
Expand Down
60 changes: 60 additions & 0 deletions tapir/coop/templates/coop/resigned_members_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{% extends "core/base.html" %}
{% load render_table from django_tables2 %}
{% load django_bootstrap5 %}
{% load i18n %}
{% load static %}
{% load querystring from django_tables2 %}
{% load export_url from django_tables2 %}
{% load core %}
{% load coop %}
{% load utils %}
{% block title %}
{% translate "List of resigned members" %}
{% endblock title %}
{% block head %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'coop/css/coop.css' %}">
{% endblock head %}
{% block content %}
<div class="card mb-2">
<div class="card-header d-flex justify-content-between align-items-center">
<h5>{% translate "List of resigned members" %} ({{ total_of_resigned_members }}) </h5>
<div>
<div class="btn-group">
<button type="button"
class="{% tapir_button_link %} btn dropdown-toggle"
data-bs-toggle="dropdown">{% translate "Export" %}</button>
<div class="dropdown-menu dropdown-menu-right">
{% for format in view.export_formats %}
<a class="dropdown-item" href="{% export_url format %}">
download <code>.{{ format }}</code>
</a>
{% endfor %}
</div>
</div>
<a class="{% tapir_button_link_to_action %}"
href="{% url 'coop:resign_new_membership' %}">
<span class="material-icons">add_circle_outline</span>
{% translate "Resign new member" %}
</a>
</div>
</div>
<br>
<form method="get">
<div class="px-3 mt-1">
<div class="member-filter-form-filters">{% bootstrap_form filter.form %}</div>
<div class="member-filter-form-buttons">
<button class="filter-button {% tapir_button_link %}">
<span class="material-icons">filter_alt</span>{% translate 'Filter' %}
</button>
<a class="{% tapir_button_link %}"
href="{% url 'coop:resigned_members_list' %}">
<span class="material-icons">clear</span>
{% translate "Clear all filters" %}
</a>
</div>
</div>
</form>
<li class="list-group-item">{% render_table table %}</li>
</div>
{% endblock content %}
Loading

0 comments on commit 8d6ee38

Please sign in to comment.