Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of the cancellation process in Tapir #495

Merged
merged 13 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from 9 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
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)")
tvedeane marked this conversation as resolved.
Show resolved Hide resolved
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)
tvedeane marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -386,7 +385,7 @@ class ShareOwnership(DurationModelMixin, models.Model):
max_digits=10,
decimal_places=2,
)

kerstenkenan marked this conversation as resolved.
Show resolved Hide resolved
def is_fully_paid(self):
return self.amount_paid >= COOP_SHARE_PRICE

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
Loading