diff --git a/sstreasury/jinja2/sstreasury/claim_print.html b/sstreasury/jinja2/sstreasury/claim_print.html
new file mode 100644
index 0000000..2d6866d
--- /dev/null
+++ b/sstreasury/jinja2/sstreasury/claim_print.html
@@ -0,0 +1,121 @@
+{% extends 'ssmain/base.html' %}
+
+{#
+ Society Self-Service
+ Copyright © 2018-2019 Yingtong Li (RunasSudo)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see
.
+#}
+
+{% block title %}{{ claim.purpose }}{% endblock %}
+
+{% block content %}
+
{{ claim.purpose }}
+
+
+
+
+
+
+ ID |
+ RE-{{ claim.id }} |
+
+
+ Purpose |
+ {{ claim.purpose }} |
+
+
+ Expenditure date |
+ {{ claim.date }} |
+
+
+ Claimee |
+
+
+ |
+
+
+ Budget ID |
+ {{ claim.budget_id }} |
+
+
+ Comments |
+ {{ claim.comments }} |
+
+
+ Items |
+
+
+ |
+
+
+
+{% endblock %}
+
+{% block head %}
+ {{ super() }}
+
+
+
+
+
+{% endblock %}
+
+{% block script %}
+ {{ super() }}
+
+
+
+
+
+
+{% endblock %}
diff --git a/sstreasury/jinja2/sstreasury/claim_view.html b/sstreasury/jinja2/sstreasury/claim_view.html
index 999474d..d9c0fc0 100644
--- a/sstreasury/jinja2/sstreasury/claim_view.html
+++ b/sstreasury/jinja2/sstreasury/claim_view.html
@@ -23,17 +23,42 @@
{% block maincontent %}
{{ claim.purpose }}
-
ID |
- {{ claim.id }} |
+ RE-{{ claim.id }} |
Purpose |
@@ -62,6 +87,10 @@
+
+ Budget ID |
+ {{ claim.budget_id }} |
+
Comments |
{{ claim.comments }} |
@@ -75,7 +104,19 @@
- {# TODO #}
+
{% for item in history %}
diff --git a/sstreasury/jinja2/sstreasury/email/claim_approved.md b/sstreasury/jinja2/sstreasury/email/claim_approved.md
new file mode 100644
index 0000000..fd1da57
--- /dev/null
+++ b/sstreasury/jinja2/sstreasury/email/claim_approved.md
@@ -0,0 +1,3 @@
+Your reimbursement claim titled *{{ claim.purpose }}* has been reviewed and approved by Treasury, and will be paid in the next pay cycle.
+
+{{ baseurl }}{{ url('claim_view', kwargs={'id': claim.id}) }}
diff --git a/sstreasury/jinja2/sstreasury/email/claim_commented.md b/sstreasury/jinja2/sstreasury/email/claim_commented.md
new file mode 100644
index 0000000..3b5f67f
--- /dev/null
+++ b/sstreasury/jinja2/sstreasury/email/claim_commented.md
@@ -0,0 +1,7 @@
+{{ comment.author.first_name }} {{ comment.author.last_name }} made a new comment on the reimbursement claim *{{ claim.purpose }}*:
+
+```
+{{ comment.content }}
+```
+
+{{ baseurl }}{{ url('claim_view', kwargs={'id': claim.id}) }}
diff --git a/sstreasury/jinja2/sstreasury/email/claim_returned.md b/sstreasury/jinja2/sstreasury/email/claim_returned.md
new file mode 100644
index 0000000..17ee9c4
--- /dev/null
+++ b/sstreasury/jinja2/sstreasury/email/claim_returned.md
@@ -0,0 +1,3 @@
+Your reimbursement claim titled *{{ claim.purpose }}* has been reviewed by Treasury and returned to you for re-drafting. You should make any requested changes and resubmit the claim.
+
+{{ baseurl }}{{ url('claim_view', kwargs={'id': claim.id}) }}
diff --git a/sstreasury/jinja2/sstreasury/email/claim_submitted_drafter.md b/sstreasury/jinja2/sstreasury/email/claim_submitted_drafter.md
new file mode 100644
index 0000000..c3bd92c
--- /dev/null
+++ b/sstreasury/jinja2/sstreasury/email/claim_submitted_drafter.md
@@ -0,0 +1,3 @@
+Your reimbursement claim titled *{{ claim.purpose }}* has been submitted for Treasury review.
+
+{{ baseurl }}{{ url('claim_view', kwargs={'id': claim.id}) }}
diff --git a/sstreasury/jinja2/sstreasury/email/claim_submitted_treasurer.md b/sstreasury/jinja2/sstreasury/email/claim_submitted_treasurer.md
new file mode 100644
index 0000000..c404a97
--- /dev/null
+++ b/sstreasury/jinja2/sstreasury/email/claim_submitted_treasurer.md
@@ -0,0 +1,3 @@
+A reimbursement claim titled *{{ claim.purpose }}* has been submitted for your review.
+
+{{ baseurl }}{{ url('claim_view', kwargs={'id': claim.id}) }}
diff --git a/sstreasury/models.py b/sstreasury/models.py
index 87711da..48910ce 100644
--- a/sstreasury/models.py
+++ b/sstreasury/models.py
@@ -109,6 +109,7 @@ class ClaimAction(DescriptionEnum):
class ReimbursementClaim(models.Model):
purpose = models.CharField(max_length=100)
date = models.DateField()
+ budget_id = models.CharField(max_length=20)
comments = models.TextField()
author = models.ForeignKey(User, on_delete=models.PROTECT, related_name='+')
diff --git a/sstreasury/urls.py b/sstreasury/urls.py
index 1a0114d..9583951 100644
--- a/sstreasury/urls.py
+++ b/sstreasury/urls.py
@@ -28,5 +28,8 @@ urlpatterns = [
path('claims/', views.claim_list, name='claim_list'),
path('claims/new/', views.claim_new, name='claim_new'),
path('claims/view/', views.claim_view, name='claim_view'),
+ path('claims/view//print', views.claim_print, name='claim_print'),
+ path('claims/edit/', views.claim_edit, name='claim_edit'),
+ path('claims/action/', views.claim_action, name='claim_action'),
path('', views.index, name='treasury'),
]
diff --git a/sstreasury/views.py b/sstreasury/views.py
index aa8e457..064ad60 100644
--- a/sstreasury/views.py
+++ b/sstreasury/views.py
@@ -121,7 +121,6 @@ def revision_from_form(budget, revision, form):
revision.date = form['date'] if form['date'] else None
revision.comments = form['comments']
- revision.state = models.BudgetState.DRAFT.value
revision.revenue = json.loads(form['revenue'])
revision.revenue_comments = form['revenue_comments']
revision.expense = json.loads(form['expense'])
@@ -154,6 +153,7 @@ def budget_new(request):
revision.author = request.user
revision.time = timezone.now()
revision.action = models.BudgetAction.CREATE.value
+ revision.state = models.BudgetState.DRAFT.value
revision = revision_from_form(budget, revision, request.POST)
if request.POST['submit'] == 'Save':
@@ -197,9 +197,8 @@ def budget_editable(viewfunc):
if not request.user.groups.filter(name='Treasury').exists():
raise PermissionDenied
- if request.user != revision.author and request.user not in revision.contributors.all():
- if not request.user.groups.filter(name='Treasury').exists():
- raise PermissionDenied
+ if not revision.can_view(request.user):
+ raise PermissionDenied
return viewfunc(request, budget, revision)
return func
@@ -393,24 +392,10 @@ def claim_list(request):
'claims_closed': claims_closed
})
-@login_required
-def claim_view(request, id):
- claim = models.ReimbursementClaim.objects.get(id=id)
-
- if not claim.can_view(request.user):
- raise PermissionDenied
-
- history = list(itertools.chain(claim.claimhistory_set.all(), claim.claimcomment_set.all()))
- history.sort(key=lambda x: x.time, reverse=True)
-
- return render(request, 'sstreasury/claim_view.html', {
- 'claim': claim,
- 'history': history
- })
-
def claim_from_form(claim, form):
claim.purpose = form['purpose']
claim.date = form['date'] if form['date'] else None
+ claim.budget_id = form['budget_id']
claim.comments = form['comments']
claim.state = models.ClaimState.DRAFT.value
@@ -428,6 +413,7 @@ def claim_new(request):
claim.author = request.user
claim.time = timezone.now()
#revision.action = models.BudgetAction.CREATE.value
+ claim.state = models.BudgetState.DRAFT.value
claim = claim_from_form(claim, request.POST)
claim_history = models.ClaimHistory()
@@ -450,3 +436,186 @@ def claim_new(request):
return render(request, 'sstreasury/claim_edit.html', {
'claim': claim
})
+
+def uses_claim(viewfunc):
+ @functools.wraps(viewfunc)
+ def func(request, id):
+ claim = models.ReimbursementClaim.objects.get(id=id)
+ return viewfunc(request, claim)
+ return func
+
+def claim_viewable(viewfunc):
+ @functools.wraps(viewfunc)
+ def func(request, claim):
+ if not claim.can_view(request.user):
+ raise PermissionDenied
+
+ return viewfunc(request, claim)
+ return func
+
+def claim_editable(viewfunc):
+ @functools.wraps(viewfunc)
+ def func(request, claim):
+ if claim.state == models.ClaimState.PAID.value:
+ raise PermissionDenied
+
+ if claim.state != models.ClaimState.DRAFT.value and claim.state != models.ClaimState.RESUBMIT.value:
+ if not request.user.groups.filter(name='Treasury').exists():
+ raise PermissionDenied
+
+ if not claim.can_view(request.user):
+ raise PermissionDenied
+
+ return viewfunc(request, claim)
+ return func
+
+@login_required
+@uses_claim
+@claim_viewable
+def claim_view(request, claim):
+ history = list(itertools.chain(claim.claimhistory_set.all(), claim.claimcomment_set.all()))
+ history.sort(key=lambda x: x.time, reverse=True)
+
+ return render(request, 'sstreasury/claim_view.html', {
+ 'claim': claim,
+ 'history': history
+ })
+
+@login_required
+@uses_claim
+@claim_viewable
+def claim_print(request, claim):
+ return render(request, 'sstreasury/claim_print.html', {
+ 'claim': claim
+ })
+
+@login_required
+@uses_claim
+@claim_editable
+def claim_edit(request, claim):
+ if request.method == 'POST':
+ if request.POST['submit'] == 'Delete':
+ claim.delete()
+ return redirect(reverse('claim_list'))
+
+ with transaction.atomic():
+ claim = claim_from_form(claim, request.POST)
+
+ claim_history = models.ClaimHistory()
+ claim_history.claim = claim
+ claim_history.author = request.user
+ claim_history.state = claim.state
+ claim_history.time = timezone.now()
+ claim_history.action = models.ClaimAction.EDIT.value
+ claim_history.save()
+
+ if request.POST['submit'] == 'Save':
+ return redirect(reverse('claim_view', kwargs={'id': claim.id}))
+ else:
+ return redirect(reverse('claim_edit', kwargs={'id': claim.id}))
+ else:
+ return render(request, 'sstreasury/claim_edit.html', {
+ 'claim': claim
+ })
+
+@login_required
+@uses_claim
+@claim_editable
+def claim_action(request, claim):
+ actions = request.POST['action'].split(',')
+
+ if 'Comment' in actions and request.POST.get('comment', None):
+ with transaction.atomic():
+ comment = models.ClaimComment()
+ comment.claim = claim
+ comment.author = request.user
+ comment.time = timezone.now()
+ comment.content = request.POST['comment']
+ comment.save()
+
+ emailer = Emailer()
+ for user in User.objects.filter(groups__name='Treasury'):
+ if user != request.user:
+ emailer.send_mail([user.email], 'New comment on reimbursement claim: {}'.format(claim.purpose), 'sstreasury/email/claim_commented.md', {'claim': claim, 'comment': comment})
+ if comment.author != request.user:
+ emailer.send_mail([comment.author], 'New comment on reimbursement claim: {}'.format(revision.name), 'sstreasury/email/claim_commented.md', {'claim': claim, 'comment': comment})
+
+ if 'Submit' in actions:
+ if claim.state != models.ClaimState.DRAFT.value and claim.state != models.ClaimState.RESUBMIT.value:
+ raise PermissionDenied
+
+ with transaction.atomic():
+ claim.state = models.ClaimState.AWAIT_REVIEW.value
+ claim.save()
+
+ claim_history = models.ClaimHistory()
+ claim_history.claim = claim
+ claim_history.author = request.user
+ claim_history.state = claim.state
+ claim_history.time = timezone.now()
+ claim_history.action = models.ClaimAction.UPDATE_STATE.value
+ claim_history.save()
+
+ emailer = Emailer()
+ for user in User.objects.filter(groups__name='Treasury'):
+ emailer.send_mail([user.email], 'Action required: Reimbursement claim submitted: {}'.format(claim.purpose), 'sstreasury/email/claim_submitted_treasurer.md', {'claim': claim})
+ emailer.send_mail([claim.author.email], 'Reimbursement claim submitted: {}'.format(claim.purpose), 'sstreasury/email/claim_submitted_drafter.md', {'claim': claim})
+
+ if 'Withdraw' in actions:
+ if claim.state == models.ClaimState.DRAFT.value or claim.state == models.ClaimState.RESUBMIT.value or claim.state == models.ClaimState.PAID.value:
+ raise PermissionDenied
+
+ claim.state = models.ClaimState.DRAFT.value
+ claim.save()
+
+ claim_history = models.ClaimHistory()
+ claim_history.claim = claim
+ claim_history.author = request.user
+ claim_history.state = claim.state
+ claim_history.time = timezone.now()
+ claim_history.action = models.ClaimAction.UPDATE_STATE.value
+ claim_history.save()
+
+ if 'Approve' in actions:
+ if claim.state != models.ClaimState.AWAIT_REVIEW.value:
+ raise PermissionDenied
+ if not request.user.groups.filter(name='Treasury').exists():
+ raise PermissionDenied
+
+ with transaction.atomic():
+ claim.state = models.ClaimState.APPROVED.value
+ claim.save()
+
+ claim_history = models.ClaimHistory()
+ claim_history.claim = claim
+ claim_history.author = request.user
+ claim_history.state = claim.state
+ claim_history.time = timezone.now()
+ claim_history.action = models.ClaimAction.UPDATE_STATE.value
+ claim_history.save()
+
+ emailer = Emailer()
+ emailer.send_mail([claim.author.email], 'Claim approved, awaiting payment: {}'.format(claim.purpose), 'sstreasury/email/claim_approved.md', {'claim': claim})
+
+ if 'Return' in actions:
+ if claim.state != models.ClaimState.AWAIT_REVIEW.value:
+ raise PermissionDenied
+ if not request.user.groups.filter(name='Treasury').exists():
+ raise PermissionDenied
+
+ with transaction.atomic():
+ claim.state = models.ClaimState.RESUBMIT.value
+ claim.save()
+
+ claim_history = models.ClaimHistory()
+ claim_history.claim = claim
+ claim_history.author = request.user
+ claim_history.state = claim.state
+ claim_history.time = timezone.now()
+ claim_history.action = models.ClaimAction.UPDATE_STATE.value
+ claim_history.save()
+
+ emailer = Emailer()
+ emailer.send_mail([claim.author.email], 'Action required: Reimbursement claim returned for re-drafting: {}'.format(claim.purpose), 'sstreasury/email/claim_returned.md', {'claim': claim})
+
+ return redirect(reverse('claim_view', kwargs={'id': claim.id}))