Reimbursement claim workflow
This commit is contained in:
parent
f3287e127a
commit
b70ef9b8f6
@ -26,7 +26,7 @@
|
|||||||
<form class="ui form" method="POST">
|
<form class="ui form" method="POST">
|
||||||
<div class="ui disabled inline grid field">
|
<div class="ui disabled inline grid field">
|
||||||
<label class="three wide column">ID</label>
|
<label class="three wide column">ID</label>
|
||||||
<input class="eleven wide column" type="text" name="id" value="{{ revision.budget.id if revision.budget.id != None else '' }}">
|
<input class="eleven wide column" type="text" name="id" value="{{ 'BU-{}'.format(revision.budget.id) if revision.budget.id != None else '' }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="ui required inline grid field">
|
<div class="ui required inline grid field">
|
||||||
<label class="three wide column">Name</label>
|
<label class="three wide column">Name</label>
|
||||||
|
@ -35,7 +35,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="two wide">ID</td>
|
<td class="two wide">ID</td>
|
||||||
<td class="fourteen wide">{{ revision.budget.id }}</td>
|
<td class="fourteen wide">BU-{{ revision.budget.id }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Name</td>
|
<td>Name</td>
|
||||||
|
@ -30,11 +30,11 @@
|
|||||||
{% if revision.state == import('sstreasury.models').BudgetState.DRAFT.value or revision.state == import('sstreasury.models').BudgetState.RESUBMIT.value %}
|
{% if revision.state == import('sstreasury.models').BudgetState.DRAFT.value or revision.state == import('sstreasury.models').BudgetState.RESUBMIT.value %}
|
||||||
<button class="ui mini labeled primary icon button" type="submit" name="action" value="Submit" style="margin-left: 1em;" onclick="return confirm('Are you sure you want to submit this budget for Treasury approval? You will not be able to make any additional changes without withdrawing the budget.');"><i class="paper plane icon"></i> Submit</button>
|
<button class="ui mini labeled primary icon button" type="submit" name="action" value="Submit" style="margin-left: 1em;" onclick="return confirm('Are you sure you want to submit this budget for Treasury approval? You will not be able to make any additional changes without withdrawing the budget.');"><i class="paper plane icon"></i> Submit</button>
|
||||||
|
|
||||||
{% if request.user.groups.filter(name='Secretary').exists() %}
|
{#{% if request.user.groups.filter(name='Secretary').exists() %}
|
||||||
<button class="ui mini labeled positive icon button" type="submit" name="action" value="Approve" onclick="return confirm('Are you sure you want to mark this budget as committee-approved?');"><i class="check icon"></i> Approve</button>
|
<button class="ui mini labeled positive icon button" type="submit" name="action" value="Approve" onclick="return confirm('Are you sure you want to mark this budget as committee-approved?');"><i class="check icon"></i> Approve</button>
|
||||||
|
|
||||||
<button class="ui mini labeled basic negative icon button" type="submit" name="action" value="CmteReturn" onclick="return confirm('Are you sure you want to refuse this budget committee approval and return it for re-drafting?');"><i class="undo icon"></i> Return for re-drafting</button>
|
<button class="ui mini labeled basic negative icon button" type="submit" name="action" value="CmteReturn" onclick="return confirm('Are you sure you want to refuse this budget committee approval and return it for re-drafting?');"><i class="undo icon"></i> Return for re-drafting</button>
|
||||||
{% endif %}
|
{% endif %}#}
|
||||||
{% elif revision.state == import('sstreasury.models').BudgetState.AWAIT_REVIEW.value and request.user.groups.filter(name='Treasury').exists() %}
|
{% elif revision.state == import('sstreasury.models').BudgetState.AWAIT_REVIEW.value and request.user.groups.filter(name='Treasury').exists() %}
|
||||||
<button class="ui mini labeled positive icon button" type="submit" name="action" value="Endorse" style="margin-left: 1em;" onclick="return confirm('Are you sure you want to give this budget Treasury endorsement?');"><i class="check icon"></i> Endorse</button>
|
<button class="ui mini labeled positive icon button" type="submit" name="action" value="Endorse" style="margin-left: 1em;" onclick="return confirm('Are you sure you want to give this budget Treasury endorsement?');"><i class="check icon"></i> Endorse</button>
|
||||||
|
|
||||||
@ -76,7 +76,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="two wide">ID</td>
|
<td class="two wide">ID</td>
|
||||||
<td class="fourteen wide">{{ revision.budget.id }}</td>
|
<td class="fourteen wide">BU-{{ revision.budget.id }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Name</td>
|
<td>Name</td>
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
<form class="ui form" method="POST">
|
<form class="ui form" method="POST">
|
||||||
<div class="ui disabled inline grid field">
|
<div class="ui disabled inline grid field">
|
||||||
<label class="three wide column">ID</label>
|
<label class="three wide column">ID</label>
|
||||||
<input class="eleven wide column" type="text" name="id" value="{{ claim.id if claim.id != None else '' }}">
|
<input class="eleven wide column" type="text" name="id" value="{{ 'RE-{}'.format(claim.id) if claim.id != None else '' }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="ui required inline grid field">
|
<div class="ui required inline grid field">
|
||||||
<label class="three wide column">Purpose</label>
|
<label class="three wide column">Purpose</label>
|
||||||
@ -43,6 +43,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ui required inline grid field">
|
||||||
|
<label class="three wide column">Budget ID (if known)</label>
|
||||||
|
<input class="eleven wide column" type="text" name="budget_id" value="{{ claim.budget_id }}">
|
||||||
|
</div>
|
||||||
<div class="ui required inline grid field">
|
<div class="ui required inline grid field">
|
||||||
<label class="three wide column">Claimee</label>
|
<label class="three wide column">Claimee</label>
|
||||||
<div class="eleven wide column">
|
<div class="eleven wide column">
|
||||||
|
121
sstreasury/jinja2/sstreasury/claim_print.html
Normal file
121
sstreasury/jinja2/sstreasury/claim_print.html
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
#}
|
||||||
|
|
||||||
|
{% block title %}{{ claim.purpose }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ claim.purpose }}</h1>
|
||||||
|
|
||||||
|
<span class="ui header">Status: {{ claim.get_state_display() }}</span>
|
||||||
|
|
||||||
|
<table class="ui mydefinition table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="two wide">ID</td>
|
||||||
|
<td class="fourteen wide">RE-{{ claim.id }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Purpose</td>
|
||||||
|
<td>{{ claim.purpose }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Expenditure date</td>
|
||||||
|
<td>{{ claim.date }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Claimee</td>
|
||||||
|
<td>
|
||||||
|
<div class="ui list">
|
||||||
|
<div class="item">
|
||||||
|
<i class="user circle icon"></i>
|
||||||
|
<div class="content">
|
||||||
|
<a href="mailto:{{ claim.author.email }}">
|
||||||
|
{% if claim.author.first_name %}
|
||||||
|
{{ claim.author.first_name }} {{ claim.author.last_name }}
|
||||||
|
{% else %}
|
||||||
|
{{ claim.author.email }}
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Budget ID</td>
|
||||||
|
<td>{{ claim.budget_id }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Comments</td>
|
||||||
|
<td>{{ claim.comments }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Items</td>
|
||||||
|
<td>
|
||||||
|
<div id="items_grid"></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
{{ super() }}
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jsgrid@1.5.3/dist/jsgrid.min.css" integrity="sha256-a/jNbtm7jpeKiXCShJ8YC+eNL9Abh7CBiYXHgaofUVs=" crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jsgrid@1.5.3/dist/jsgrid-theme.min.css" integrity="sha256-0rD7ZUV4NLK6VtGhEim14ZUZGC45Kcikjdcr4N03ddA=" crossorigin="anonymous">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Fix the CSS */
|
||||||
|
|
||||||
|
.ui.mydefinition.table > tbody > tr > td:first-child:not(.ignored) {
|
||||||
|
background: rgba(0,0,0,.03);
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgba(0,0,0,.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jsgrid-align-right, .jsgrid-align-right input, .jsgrid-align-right select, .jsgrid-align-right textarea {
|
||||||
|
text-align: right !important;
|
||||||
|
}
|
||||||
|
.jsgrid-cell {
|
||||||
|
padding: .5em !important;
|
||||||
|
}
|
||||||
|
.jsgrid-header-row .jsgrid-header-cell {
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
{{ super() }}
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/jsgrid@1.5.3/dist/jsgrid.min.js" integrity="sha256-lzjMTpg04xOdI+MJdjBst98bVI6qHToLyVodu3EywFU=" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<script src="{{ static('sstreasury/claim.js') }}"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var items_data = JSON.parse({{ import('json').dumps(import('json').dumps(claim.items))|safe }});
|
||||||
|
for (var row of items_data) {
|
||||||
|
row['Unit price\n(incl GST)'] = row['Unit price'];
|
||||||
|
}
|
||||||
|
var editing = false;
|
||||||
|
makeGrid();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
@ -23,17 +23,42 @@
|
|||||||
{% block maincontent %}
|
{% block maincontent %}
|
||||||
<h1>{{ claim.purpose }}</h1>
|
<h1>{{ claim.purpose }}</h1>
|
||||||
|
|
||||||
<form class="ui form" action="{# TODO #}" method="POST">
|
<form class="ui form" action="{{ url('claim_action', kwargs={'id': claim.id}) }}" method="POST">
|
||||||
<span class="ui header">Status: {{ claim.get_state_display() }}</span>
|
<span class="ui header">Status: {{ claim.get_state_display() }}</span>
|
||||||
|
|
||||||
{# TODO #}
|
{% if claim.state == import('sstreasury.models').ClaimState.DRAFT.value or claim.state == import('sstreasury.models').ClaimState.RESUBMIT.value %}
|
||||||
|
<button class="ui mini labeled primary icon button" type="submit" name="action" value="Submit" style="margin-left: 1em;" onclick="return confirm('Are you sure you want to submit this claim for Treasury approval? You will not be able to make any additional changes without withdrawing the claim.');"><i class="paper plane icon"></i> Submit</button>
|
||||||
|
{% elif claim.state == import('sstreasury.models').ClaimState.AWAIT_REVIEW.value and request.user.groups.filter(name='Treasury').exists() %}
|
||||||
|
<button class="ui mini labeled positive icon button" type="submit" name="action" value="Approve" style="margin-left: 1em;" onclick="return confirm('Are you sure you want to approve this claim?');"><i class="check icon"></i> Approve</button>
|
||||||
|
|
||||||
|
<button class="ui mini labeled basic negative icon button" type="submit" name="action" value="Return" onclick="return confirm('Are you sure you want to refuse this claim and return it for re-drafting?');"><i class="undo icon"></i> Return for re-drafting</button>
|
||||||
|
{% elif claim.state == import('sstreasury.models').ClaimState.APPROVED.value or claim.state == import('sstreasury.models').ClaimState.PAID.value %}
|
||||||
|
{# Blank #}
|
||||||
|
{% else %}
|
||||||
|
<button class="ui mini labeled basic negative icon button" type="submit" name="action" value="Withdraw" style="margin-left: 1em;" onclick="return confirm('Are you sure you want to withdraw this budget from being considered for approval? The budget will be reverted to a draft.');"><i class="undo icon"></i> Withdraw</button>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if claim.state == import('sstreasury.models').ClaimState.DRAFT.value or claim.state == import('sstreasury.models').ClaimState.RESUBMIT.value or (claim.state != import('sstreasury.models').ClaimState.APPROVED.value and claim.state != import('sstreasury.models').ClaimState.PAID.value and request.user.groups.filter(name='Treasury').exists()) %}
|
||||||
|
<a class="ui mini labeled right floated icon button" href="{{ url('claim_edit', kwargs={'id': claim.id}) }}"><i class="edit icon"></i> Edit</a>
|
||||||
|
<a class="ui mini labeled right floated icon button" href="{{ url('claim_print', kwargs={'id': claim.id}) }}" target="_blank"><i class="print icon"></i> Print</a>
|
||||||
|
{% elif claim.state == import('sstreasury.models').ClaimState.APPROVED.value or claim.state == import('sstreasury.models').ClaimState.PAID.value %}
|
||||||
|
<a class="ui mini labeled right floated icon button" href="{{ url('claim_print', kwargs={'id': claim.id}) }}" target="_blank"><i class="print icon"></i> Print</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="ui mini labeled right floated icon button" href="{{ url('claim_print', kwargs={'id': claim.id}) }}" target="_blank"><i class="print icon"></i> Print</a>
|
||||||
|
|
||||||
|
<div class="ui message">
|
||||||
|
<p>This claim has been submitted and is now awaiting processing. If you wish to edit this claim, you must first withdraw it. This will revert the claim to a draft.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<table class="ui mydefinition table">
|
<table class="ui mydefinition table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="two wide">ID</td>
|
<td class="two wide">ID</td>
|
||||||
<td class="fourteen wide">{{ claim.id }}</td>
|
<td class="fourteen wide">RE-{{ claim.id }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Purpose</td>
|
<td>Purpose</td>
|
||||||
@ -62,6 +87,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Budget ID</td>
|
||||||
|
<td>{{ claim.budget_id }}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Comments</td>
|
<td>Comments</td>
|
||||||
<td>{{ claim.comments }}</td>
|
<td>{{ claim.comments }}</td>
|
||||||
@ -75,7 +104,19 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{# TODO #}
|
<form class="ui form" action="{{ url('claim_action', kwargs={'id': claim.id}) }}" method="POST">
|
||||||
|
<div class="required field">
|
||||||
|
<textarea rows="4" name="comment"></textarea>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
|
||||||
|
<input class="ui primary button" type="submit" name="action" value="Comment">
|
||||||
|
|
||||||
|
{% if claim.state == import('sstreasury.models').ClaimState.AWAIT_REVIEW.value and request.user.groups.filter(name='Treasury').exists() %}
|
||||||
|
<button class="ui right floated labeled basic negative icon button" type="submit" name="action" value="Comment,Return" onclick="return confirm('Are you sure you want to refuse this claim and return it for re-drafting?');"><i class="undo icon"></i> Comment and return for re-drafting</button>
|
||||||
|
|
||||||
|
<button class="ui right floated labeled positive icon button" type="submit" name="action" value="Comment,Approve" onclick="return confirm('Are you sure you want to approve this claim?');"><i class="check icon"></i> Comment and approve</button>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
|
||||||
<div class="ui feed">
|
<div class="ui feed">
|
||||||
{% for item in history %}
|
{% for item in history %}
|
||||||
|
3
sstreasury/jinja2/sstreasury/email/claim_approved.md
Normal file
3
sstreasury/jinja2/sstreasury/email/claim_approved.md
Normal file
@ -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}) }}
|
7
sstreasury/jinja2/sstreasury/email/claim_commented.md
Normal file
7
sstreasury/jinja2/sstreasury/email/claim_commented.md
Normal file
@ -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}) }}
|
3
sstreasury/jinja2/sstreasury/email/claim_returned.md
Normal file
3
sstreasury/jinja2/sstreasury/email/claim_returned.md
Normal file
@ -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}) }}
|
@ -0,0 +1,3 @@
|
|||||||
|
Your reimbursement claim titled *{{ claim.purpose }}* has been submitted for Treasury review.
|
||||||
|
|
||||||
|
{{ baseurl }}{{ url('claim_view', kwargs={'id': claim.id}) }}
|
@ -0,0 +1,3 @@
|
|||||||
|
A reimbursement claim titled *{{ claim.purpose }}* has been submitted for your review.
|
||||||
|
|
||||||
|
{{ baseurl }}{{ url('claim_view', kwargs={'id': claim.id}) }}
|
@ -109,6 +109,7 @@ class ClaimAction(DescriptionEnum):
|
|||||||
class ReimbursementClaim(models.Model):
|
class ReimbursementClaim(models.Model):
|
||||||
purpose = models.CharField(max_length=100)
|
purpose = models.CharField(max_length=100)
|
||||||
date = models.DateField()
|
date = models.DateField()
|
||||||
|
budget_id = models.CharField(max_length=20)
|
||||||
comments = models.TextField()
|
comments = models.TextField()
|
||||||
|
|
||||||
author = models.ForeignKey(User, on_delete=models.PROTECT, related_name='+')
|
author = models.ForeignKey(User, on_delete=models.PROTECT, related_name='+')
|
||||||
|
@ -28,5 +28,8 @@ urlpatterns = [
|
|||||||
path('claims/', views.claim_list, name='claim_list'),
|
path('claims/', views.claim_list, name='claim_list'),
|
||||||
path('claims/new/', views.claim_new, name='claim_new'),
|
path('claims/new/', views.claim_new, name='claim_new'),
|
||||||
path('claims/view/<int:id>', views.claim_view, name='claim_view'),
|
path('claims/view/<int:id>', views.claim_view, name='claim_view'),
|
||||||
|
path('claims/view/<int:id>/print', views.claim_print, name='claim_print'),
|
||||||
|
path('claims/edit/<int:id>', views.claim_edit, name='claim_edit'),
|
||||||
|
path('claims/action/<int:id>', views.claim_action, name='claim_action'),
|
||||||
path('', views.index, name='treasury'),
|
path('', views.index, name='treasury'),
|
||||||
]
|
]
|
||||||
|
@ -121,7 +121,6 @@ def revision_from_form(budget, revision, form):
|
|||||||
revision.date = form['date'] if form['date'] else None
|
revision.date = form['date'] if form['date'] else None
|
||||||
|
|
||||||
revision.comments = form['comments']
|
revision.comments = form['comments']
|
||||||
revision.state = models.BudgetState.DRAFT.value
|
|
||||||
revision.revenue = json.loads(form['revenue'])
|
revision.revenue = json.loads(form['revenue'])
|
||||||
revision.revenue_comments = form['revenue_comments']
|
revision.revenue_comments = form['revenue_comments']
|
||||||
revision.expense = json.loads(form['expense'])
|
revision.expense = json.loads(form['expense'])
|
||||||
@ -154,6 +153,7 @@ def budget_new(request):
|
|||||||
revision.author = request.user
|
revision.author = request.user
|
||||||
revision.time = timezone.now()
|
revision.time = timezone.now()
|
||||||
revision.action = models.BudgetAction.CREATE.value
|
revision.action = models.BudgetAction.CREATE.value
|
||||||
|
revision.state = models.BudgetState.DRAFT.value
|
||||||
revision = revision_from_form(budget, revision, request.POST)
|
revision = revision_from_form(budget, revision, request.POST)
|
||||||
|
|
||||||
if request.POST['submit'] == 'Save':
|
if request.POST['submit'] == 'Save':
|
||||||
@ -197,9 +197,8 @@ def budget_editable(viewfunc):
|
|||||||
if not request.user.groups.filter(name='Treasury').exists():
|
if not request.user.groups.filter(name='Treasury').exists():
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
|
||||||
if request.user != revision.author and request.user not in revision.contributors.all():
|
if not revision.can_view(request.user):
|
||||||
if not request.user.groups.filter(name='Treasury').exists():
|
raise PermissionDenied
|
||||||
raise PermissionDenied
|
|
||||||
|
|
||||||
return viewfunc(request, budget, revision)
|
return viewfunc(request, budget, revision)
|
||||||
return func
|
return func
|
||||||
@ -393,24 +392,10 @@ def claim_list(request):
|
|||||||
'claims_closed': claims_closed
|
'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):
|
def claim_from_form(claim, form):
|
||||||
claim.purpose = form['purpose']
|
claim.purpose = form['purpose']
|
||||||
claim.date = form['date'] if form['date'] else None
|
claim.date = form['date'] if form['date'] else None
|
||||||
|
claim.budget_id = form['budget_id']
|
||||||
|
|
||||||
claim.comments = form['comments']
|
claim.comments = form['comments']
|
||||||
claim.state = models.ClaimState.DRAFT.value
|
claim.state = models.ClaimState.DRAFT.value
|
||||||
@ -428,6 +413,7 @@ def claim_new(request):
|
|||||||
claim.author = request.user
|
claim.author = request.user
|
||||||
claim.time = timezone.now()
|
claim.time = timezone.now()
|
||||||
#revision.action = models.BudgetAction.CREATE.value
|
#revision.action = models.BudgetAction.CREATE.value
|
||||||
|
claim.state = models.BudgetState.DRAFT.value
|
||||||
claim = claim_from_form(claim, request.POST)
|
claim = claim_from_form(claim, request.POST)
|
||||||
|
|
||||||
claim_history = models.ClaimHistory()
|
claim_history = models.ClaimHistory()
|
||||||
@ -450,3 +436,186 @@ def claim_new(request):
|
|||||||
return render(request, 'sstreasury/claim_edit.html', {
|
return render(request, 'sstreasury/claim_edit.html', {
|
||||||
'claim': claim
|
'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}))
|
||||||
|
Loading…
Reference in New Issue
Block a user