Skip to content

Commit 24b804c

Browse files
authored
Economy makeover (#1946)
1 parent 12240e1 commit 24b804c

21 files changed

+712
-264
lines changed

src/backoffice/templates/reimbursement_list_backoffice.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ <h2>BackOffice: Reimbursements for {{ camp.title }}</h2>
1010
<p class="lead">This view shows all existing reimbursements for {{ camp.title }}. Users have to create their own reimbursements when they are done adding expenses. The user will be asked for a bank account when creating the reimbursement.</p>
1111

1212
<p>
13-
<a class="btn btn-secondary" href="{% url 'backoffice:index' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Backoffice</a>
13+
<a class="btn btn-secondary" href="{% url 'backoffice:index' camp_slug=camp.slug %}#economy"><i class="fas fa-undo"></i> Backoffice</a>
1414
</p>
1515

1616
{% include 'includes/reimbursement_list_panel.html' %}
1717

1818
<p>
19-
<a class="btn btn-secondary" href="{% url 'backoffice:index' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Backoffice</a>
19+
<a class="btn btn-secondary" href="{% url 'backoffice:index' camp_slug=camp.slug %}#economy"><i class="fas fa-undo"></i> Backoffice</a>
2020
</p>
2121
{% endblock content %}

src/backoffice/views/economy.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -160,21 +160,21 @@ def get_context_data(self, *args, **kwargs):
160160
context["expenses"] = Expense.objects.filter(
161161
camp=self.camp,
162162
creditor__chain=self.get_object(),
163-
).prefetch_related("responsible_team", "user", "creditor")
163+
).prefetch_related("user", "creditor")
164164
context["revenues"] = Revenue.objects.filter(
165165
camp=self.camp,
166166
debtor__chain=self.get_object(),
167-
).prefetch_related("responsible_team", "user", "debtor")
167+
).prefetch_related("user", "debtor")
168168

169169
# Include past years expenses and revenues for the Chain in context as separate querysets
170170
context["past_expenses"] = Expense.objects.filter(
171171
camp__camp__lt=self.camp.camp,
172172
creditor__chain=self.get_object(),
173-
).prefetch_related("responsible_team", "user", "creditor")
173+
).prefetch_related("user", "creditor")
174174
context["past_revenues"] = Revenue.objects.filter(
175175
camp__camp__lt=self.camp.camp,
176176
debtor__chain=self.get_object(),
177-
).prefetch_related("responsible_team", "user", "debtor")
177+
).prefetch_related("user", "debtor")
178178

179179
return context
180180

@@ -187,10 +187,10 @@ class CredebtorDetailView(CampViewMixin, EconomyTeamPermissionMixin, DetailView)
187187
def get_context_data(self, *args, **kwargs):
188188
context = super().get_context_data(*args, **kwargs)
189189
context["expenses"] = (
190-
self.get_object().expenses.filter(camp=self.camp).prefetch_related("responsible_team", "user", "creditor")
190+
self.get_object().expenses.filter(camp=self.camp).prefetch_related("user", "creditor")
191191
)
192192
context["revenues"] = (
193-
self.get_object().revenues.filter(camp=self.camp).prefetch_related("responsible_team", "user", "debtor")
193+
self.get_object().revenues.filter(camp=self.camp).prefetch_related("user", "debtor")
194194
)
195195
return context
196196

@@ -209,7 +209,6 @@ def get_queryset(self, **kwargs):
209209
return queryset.exclude(approved__isnull=True).prefetch_related(
210210
"creditor",
211211
"user",
212-
"responsible_team",
213212
)
214213

215214
def get_context_data(self, **kwargs):
@@ -221,7 +220,6 @@ def get_context_data(self, **kwargs):
221220
).prefetch_related(
222221
"creditor",
223222
"user",
224-
"responsible_team",
225223
)
226224
return context
227225

@@ -275,19 +273,36 @@ class ReimbursementUpdateView(
275273
):
276274
model = Reimbursement
277275
template_name = "reimbursement_form.html"
278-
fields = ["notes", "paid"]
276+
fields = ["notes"]
279277

280278
def get_context_data(self, **kwargs):
281279
context = super().get_context_data(**kwargs)
282-
context["expenses"] = self.object.expenses.filter(paid_by_bornhack=False)
283-
context["total_amount"] = context["expenses"].aggregate(Sum("amount"))
280+
context["expenses"] = self.object.covered_expenses.all()
281+
context["revenues"] = self.object.covered_revenues.all()
282+
context["total_amount"] = self.object.amount
284283
context["reimbursement_user"] = self.object.reimbursement_user
285284
context["cancelurl"] = reverse(
286285
"backoffice:reimbursement_list",
287286
kwargs={"camp_slug": self.camp.slug},
288287
)
289288
return context
290289

290+
def form_valid(self, form):
291+
"""Backoffice has two submit buttons in this form, 'Just Save', and 'Mark as Paid'."""
292+
if "paid" in form.data:
293+
# mark as paid button was pressed
294+
reimbursement = form.save()
295+
reimbursement.mark_as_paid()
296+
messages.success(self.request, "Reimbursement marked as paid, related expenses and revenues payment_status set accordingly")
297+
elif "save" in form.data:
298+
reimbursement = form.save()
299+
messages.success(self.request, "Reimbursement notes updated")
300+
else:
301+
messages.error(self.request, "Unknown submit action")
302+
return redirect(
303+
reverse("backoffice:expense_list", kwargs={"camp_slug": self.camp.slug}),
304+
)
305+
291306
def get_success_url(self):
292307
return reverse(
293308
"backoffice:reimbursement_detail",
@@ -299,7 +314,7 @@ class ReimbursementDeleteView(CampViewMixin, EconomyTeamPermissionMixin, DeleteV
299314
model = Reimbursement
300315
template_name = "reimbursement_delete.html"
301316

302-
def get(self, request, *args, **kwargs):
317+
def dispatch(self, request, *args, **kwargs):
303318
if self.get_object().paid:
304319
messages.error(
305320
request,
@@ -312,7 +327,7 @@ def get(self, request, *args, **kwargs):
312327
),
313328
)
314329
# continue with the request
315-
return super().get(request, *args, **kwargs)
330+
return super().dispatch(request, *args, **kwargs)
316331

317332
def get_success_url(self):
318333
messages.success(
@@ -339,7 +354,6 @@ def get_queryset(self, **kwargs):
339354
return queryset.exclude(approved__isnull=True).prefetch_related(
340355
"debtor",
341356
"user",
342-
"responsible_team",
343357
)
344358

345359
def get_context_data(self, **kwargs):

src/bornhack/environment_settings.py.dist

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,14 @@ DATABASES = {
1313
'HOST': '{{ django_postgres_host }}',
1414
# comment this out for non-tls connection to postgres
1515
'OPTIONS': {'sslmode': 'verify-full', 'sslrootcert': 'system'},
16+
# always use transactions
17+
'ATOMIC_REQUESTS': True,
1618
},
1719
}
1820

1921
DEBUG={{ django_debug }}
2022
DEBUG_TOOLBAR_ENABLED={{ django_debug_toolbar_enabled }}
2123

22-
23-
# start redirecting to the next camp instead of the previous camp after
24-
# this much of the time between the camps has passed
25-
CAMP_REDIRECT_PERCENT=15
26-
2724
### changes below here are only needed for production
2825

2926
# email settings

src/economy/admin.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ class ExpenseAdmin(admin.ModelAdmin):
6868
"camp",
6969
"creditor__chain",
7070
"creditor",
71-
"responsible_team",
7271
"approved",
7372
"user",
7473
]
@@ -79,7 +78,6 @@ class ExpenseAdmin(admin.ModelAdmin):
7978
"amount",
8079
"camp",
8180
"creditor",
82-
"responsible_team",
8381
"approved",
8482
"reimbursement",
8583
]
@@ -109,14 +107,13 @@ def reject_revenues(modeladmin, request, queryset) -> None:
109107

110108
@admin.register(Revenue)
111109
class RevenueAdmin(admin.ModelAdmin):
112-
list_filter = ["camp", "responsible_team", "approved", "user"]
110+
list_filter = ["camp", "approved", "user"]
113111
list_display = [
114112
"user",
115113
"description",
116114
"invoice_date",
117115
"amount",
118116
"camp",
119-
"responsible_team",
120117
"approved",
121118
]
122119
search_fields = ["description", "amount", "user"]

src/economy/factories.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -549,9 +549,9 @@ class Meta:
549549
color=random.choice(["#ff0000", "#00ff00", "#0000ff"]),
550550
)
551551
invoice_date = factory.Faker("date")
552-
responsible_team = factory.Faker("random_element", elements=Team.objects.all())
553552
approved = factory.Faker("random_element", elements=[True, True, False, None])
554553
notes = factory.Faker("text")
554+
payment_status = factory.Faker("random_element", elements=["PAID_IN_NETBANK", "PAID_WITH_TYKLINGS_MASTERCARD", "PAID_WITH_AHFS_MASTERCARD", "PAID_NEEDS_REIMBURSEMENT"])
555555

556556

557557
class RevenueFactory(factory.django.DjangoModelFactory):
@@ -571,6 +571,6 @@ class Meta:
571571
color=random.choice(["#ff0000", "#00ff00", "#0000ff"]),
572572
)
573573
invoice_date = factory.Faker("date")
574-
responsible_team = factory.Faker("random_element", elements=Team.objects.all())
575574
approved = factory.Faker("random_element", elements=[True, True, False, None])
576575
notes = factory.Faker("text")
576+
payment_status = factory.Faker("random_element", elements=["PAID_IN_NETBANK", "PAID_TO_TYKLINGS_MASTERCARD", "PAID_TO_AHFS_MASTERCARD", "PAID_NEEDS_REDISBURSEMENT"])

src/economy/forms.py

Lines changed: 69 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,8 @@
99
from .models import Revenue
1010

1111

12-
class CleanInvoiceForm(forms.ModelForm):
13-
"""We have to define this form explicitly because we want our ImageField to accept PDF files as well as images,
14-
and we cannot change the clean_* methods with an autogenerated form from inside views.py.
15-
"""
16-
17-
invoice = forms.FileField()
12+
class CleanInvoiceMixin:
13+
"""We want our ImageFields to accept PDF files as well as images."""
1814

1915
def clean_invoice(self):
2016
# get the uploaded file from cleaned_data
@@ -38,44 +34,95 @@ def clean_invoice(self):
3834
return uploaded_file
3935

4036

41-
class ExpenseCreateForm(CleanInvoiceForm):
37+
class ExpenseUpdateForm(forms.ModelForm):
4238
class Meta:
4339
model = Expense
4440
fields = [
4541
"description",
4642
"amount",
43+
"payment_status",
4744
"invoice_date",
48-
"invoice",
49-
"paid_by_bornhack",
50-
"responsible_team",
5145
]
5246

47+
def __init__(self, *args, **kwargs):
48+
"""Remove some choices."""
49+
super().__init__(*args, **kwargs)
50+
# TODO: this is a subset of the choices in the model,
51+
# find a way to keep this more DRY
52+
self.fields["payment_status"].choices = [
53+
(
54+
"Paid by BornHack",
55+
(
56+
("PAID_WITH_TYKLINGS_MASTERCARD", "Expense was paid with Tyklings BornHack Mastercard"),
57+
("PAID_WITH_AHFS_MASTERCARD", "Expense was paid with ahfs BornHack Mastercard"),
58+
("PAID_WITH_VIDIRS_MASTERCARD", "Expense was paid with Vidirs BornHack Mastercard"),
59+
("PAID_IN_NETBANK", "Expense was paid with bank transfer from BornHacks netbank"),
60+
("PAID_WITH_BORNHACKS_CASH", "Expense was paid with BornHacks cash"),
61+
),
62+
),
63+
(
64+
"Paid by Participant",
65+
(("PAID_NEEDS_REIMBURSEMENT", "Expense was paid by me, I need a reimbursement"),),
66+
),
67+
(
68+
"Unpaid",
69+
(("UNPAID_NEEDS_PAYMENT", "Expense is unpaid"),),
70+
),
71+
]
72+
73+
74+
class ExpenseCreateForm(ExpenseUpdateForm, CleanInvoiceMixin):
75+
invoice = forms.FileField()
5376

54-
class ExpenseUpdateForm(forms.ModelForm):
5577
class Meta:
5678
model = Expense
5779
fields = [
5880
"description",
5981
"amount",
82+
"payment_status",
6083
"invoice_date",
61-
"paid_by_bornhack",
62-
"responsible_team",
84+
"invoice",
6385
]
6486

6587

66-
class RevenueCreateForm(CleanInvoiceForm):
88+
######### REVENUE ###############################
89+
90+
91+
class RevenueUpdateForm(forms.ModelForm):
6792
class Meta:
6893
model = Revenue
69-
fields = [
70-
"description",
71-
"amount",
72-
"invoice_date",
73-
"invoice",
74-
"responsible_team",
94+
fields = ["description", "amount", "payment_status", "invoice_date"]
95+
96+
def __init__(self, *args, **kwargs):
97+
"""Remove some choices."""
98+
super().__init__(*args, **kwargs)
99+
# TODO: this is a subset of the choices in the model,
100+
# find a way to keep this more DRY
101+
self.fields["payment_status"].choices = [
102+
(
103+
"Paid to BornHack",
104+
(
105+
("PAID_TO_TYKLINGS_MASTERCARD", "Revenue was credited to Tyklings BornHack Mastercard"),
106+
("PAID_TO_AHFS_MASTERCARD", "Revenue was credited to ahfs BornHack Mastercard"),
107+
("PAID_TO_VIDIRS_MASTERCARD", "Revenue was credited to Vidirs BornHack Mastercard"),
108+
("PAID_IN_NETBANK", "Revenue was transferred to a BornHack bank account"),
109+
("PAID_IN_CASH", "Revenue was paid to BornHack with cash"),
110+
),
111+
),
112+
(
113+
"Paid to Participant",
114+
(("PAID_NEEDS_REDISBURSEMENT", "Revenue has been paid out to me, a redisbursement is needed"),),
115+
),
116+
(
117+
"Unpaid",
118+
(("UNPAID_NEEDS_PAYMENT", "Revenue is unpaid"),),
119+
),
75120
]
76121

77122

78-
class RevenueUpdateForm(forms.ModelForm):
123+
class RevenueCreateForm(RevenueUpdateForm, CleanInvoiceMixin):
124+
invoice = forms.FileField()
125+
79126
class Meta:
80127
model = Revenue
81-
fields = ["description", "amount", "invoice_date", "responsible_team"]
128+
fields = ["description", "amount", "payment_status", "invoice_date", "invoice"]

0 commit comments

Comments
 (0)