Automatically approve budget when reached threshold for approval
This commit is contained in:
parent
7f6bf1b07f
commit
490faa8147
@ -34,8 +34,8 @@ PROMO_LOGO_LINK = 'https://example.com'
|
|||||||
PROMO_GROUPS_MANDATORY = ['All Years']
|
PROMO_GROUPS_MANDATORY = ['All Years']
|
||||||
|
|
||||||
AVAILABLE_APPROVERS = [
|
AVAILABLE_APPROVERS = [
|
||||||
# Tuples (committee name, description)
|
# Tuples (committee name, (description, votes required to approve))
|
||||||
('Committee', 'Management Committee'),
|
('Committee', ('Management Committee', 10)),
|
||||||
]
|
]
|
||||||
BUDGET_ENABLE_VOTING = True
|
BUDGET_ENABLE_VOTING = True
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@
|
|||||||
<div class="eleven wide column" style="padding-left: 0; padding-right: 0;">
|
<div class="eleven wide column" style="padding-left: 0; padding-right: 0;">
|
||||||
<select class="ui dropdown" name="approver">
|
<select class="ui dropdown" name="approver">
|
||||||
{% for approver in settings.AVAILABLE_APPROVERS %}
|
{% for approver in settings.AVAILABLE_APPROVERS %}
|
||||||
<option value="{{ approver[0] }}"{% if approver[0] == revision.approver %} selected{% endif %}>{{ approver[1] }}</option>
|
<option value="{{ approver[0] }}"{% if approver[0] == revision.approver %} selected{% endif %}>{{ approver[1][0] }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
@ -115,7 +115,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Responsible committee</td>
|
<td>Responsible committee</td>
|
||||||
<td>{{ dict(settings.AVAILABLE_APPROVERS)[revision.approver] }}</td>
|
<td>{{ dict(settings.AVAILABLE_APPROVERS)[revision.approver][0] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Comments</td>
|
<td>Comments</td>
|
||||||
@ -212,15 +212,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="summary">
|
<div class="summary">
|
||||||
<i class="user circle icon"></i>
|
|
||||||
{% if item.action == import('sstreasury.models').BudgetAction.CREATE.value %}
|
{% if item.action == import('sstreasury.models').BudgetAction.CREATE.value %}
|
||||||
<a href="mailto:{{ item.author.email }}">{{ item.author.first_name }} {{ item.author.last_name }}</a> created the budget <a href="{{ url('budget_view', kwargs={'id': revision.budget.id}) }}?revision={{ item.id }}">(view)</a>
|
<i class="user circle icon"></i> <a href="mailto:{{ item.author.email }}">{{ item.author.first_name }} {{ item.author.last_name }}</a> created the budget <a href="{{ url('budget_view', kwargs={'id': revision.budget.id}) }}?revision={{ item.id }}">(view)</a>
|
||||||
{% elif item.action == import('sstreasury.models').BudgetAction.EDIT.value %}
|
{% elif item.action == import('sstreasury.models').BudgetAction.EDIT.value %}
|
||||||
<a href="mailto:{{ item.author.email }}">{{ item.author.first_name }} {{ item.author.last_name }}</a> edited the budget <a href="{{ url('budget_view', kwargs={'id': revision.budget.id}) }}?revision={{ item.id }}">(view)</a>
|
<i class="user circle icon"></i> <a href="mailto:{{ item.author.email }}">{{ item.author.first_name }} {{ item.author.last_name }}</a> edited the budget <a href="{{ url('budget_view', kwargs={'id': revision.budget.id}) }}?revision={{ item.id }}">(view)</a>
|
||||||
{% elif item.action == import('sstreasury.models').BudgetAction.UPDATE_STATE.value %}
|
{% elif item.action == import('sstreasury.models').BudgetAction.UPDATE_STATE.value %}
|
||||||
<a href="mailto:{{ item.author.email }}">{{ item.author.first_name }} {{ item.author.last_name }}</a> changed the state to: {{ item.get_state_display() }} <a href="{{ url('budget_view', kwargs={'id': revision.budget.id}) }}?revision={{ item.id }}">(view)</a>
|
<i class="user circle icon"></i> <a href="mailto:{{ item.author.email }}">{{ item.author.first_name }} {{ item.author.last_name }}</a> changed the state to: {{ item.get_state_display() }} <a href="{{ url('budget_view', kwargs={'id': revision.budget.id}) }}?revision={{ item.id }}">(view)</a>
|
||||||
|
{% elif item.action == import('sstreasury.models').BudgetAction.AUTO_APPROVE.value %}
|
||||||
|
System changed the state to: {{ item.get_state_display() }} <a href="{{ url('budget_view', kwargs={'id': revision.budget.id}) }}?revision={{ item.id }}">(view)</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="mailto:{{ item.author.email }}">{{ item.author.first_name }} {{ item.author.last_name }}</a> modified the budget <a href="{{ url('budget_view', kwargs={'id': revision.budget.id}) }}?revision={{ item.id }}">(view)</a>
|
<i class="user circle icon"></i> <a href="mailto:{{ item.author.email }}">{{ item.author.first_name }} {{ item.author.last_name }}</a> modified the budget <a href="{{ url('budget_view', kwargs={'id': revision.budget.id}) }}?revision={{ item.id }}">(view)</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="date">
|
<div class="date">
|
||||||
{{ localtime(item.time) }}
|
{{ localtime(item.time) }}
|
||||||
@ -253,78 +254,82 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if revision.state == import('sstreasury.models').BudgetState.ENDORSED.value %}
|
{% if revision.state == import('sstreasury.models').BudgetState.ENDORSED.value %}
|
||||||
<h2>Committee voting</h2>
|
<h2>Committee voting</h2>
|
||||||
|
|
||||||
<form class="ui three column grid" action="{{ url('budget_action', kwargs={'id': revision.budget.id}) }}" method="POST">
|
<p>{{ dict(settings.AVAILABLE_APPROVERS)[revision.approver][1] }} votes in favour are required for approval.</p>
|
||||||
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
|
|
||||||
<div class="column">
|
<form class="ui three column grid" action="{{ url('budget_action', kwargs={'id': revision.budget.id}) }}" method="POST">
|
||||||
<div class="ui fluid card">
|
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
|
||||||
<div class="content">
|
<div class="column">
|
||||||
<div class="header">In favour ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value).count() }})</div>
|
<div class="ui fluid card">
|
||||||
{% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value).exists() %}
|
<div class="content">
|
||||||
<div class="description">
|
<div class="header">In favour ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value).count() }})</div>
|
||||||
<ul style="margin-bottom:0">
|
{% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value).exists() %}
|
||||||
{% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value) %}
|
<div class="description">
|
||||||
<li>{{ vote.voter.first_name }} {{ vote.voter.last_name }}</li>
|
<ul style="margin-bottom:0">
|
||||||
{% endfor %}
|
{% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value) %}
|
||||||
</ul>
|
<li>{{ vote.voter.first_name }} {{ vote.voter.last_name }}</li>
|
||||||
</div>
|
{% endfor %}
|
||||||
{% endif %}
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% if revision.can_vote(request.user) %}
|
|
||||||
<button class="ui bottom attached positive button" type="submit" name="action" value="VoteInFavour">
|
|
||||||
Vote in favour
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if is_latest and revision.can_vote(request.user) %}
|
||||||
|
<button class="ui bottom attached positive button" type="submit" name="action" value="VoteInFavour">
|
||||||
|
Vote in favour
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
</div>
|
||||||
<div class="ui fluid card">
|
<div class="column">
|
||||||
<div class="content">
|
<div class="ui fluid card">
|
||||||
<div class="header">Against ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value).count() }})</div>
|
<div class="content">
|
||||||
{% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value).exists() %}
|
<div class="header">Against ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value).count() }})</div>
|
||||||
<div class="description">
|
{% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value).exists() %}
|
||||||
<ul style="margin-bottom:0">
|
<div class="description">
|
||||||
{% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value) %}
|
<ul style="margin-bottom:0">
|
||||||
<li>{{ vote.voter.first_name }} {{ vote.voter.last_name }}</li>
|
{% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value) %}
|
||||||
{% endfor %}
|
<li>{{ vote.voter.first_name }} {{ vote.voter.last_name }}</li>
|
||||||
</ul>
|
{% endfor %}
|
||||||
</div>
|
</ul>
|
||||||
{% endif %}
|
</div>
|
||||||
</div>
|
|
||||||
{% if revision.can_vote(request.user) %}
|
|
||||||
<button class="ui bottom attached negative button" type="submit" name="action" value="VoteAgainst">
|
|
||||||
Vote against
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if is_latest and revision.can_vote(request.user) %}
|
||||||
|
<button class="ui bottom attached negative button" type="submit" name="action" value="VoteAgainst">
|
||||||
|
Vote against
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
</div>
|
||||||
<div class="ui fluid card">
|
<div class="column">
|
||||||
<div class="content">
|
<div class="ui fluid card">
|
||||||
<div class="header">Abstentions ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value).count() }})</div>
|
<div class="content">
|
||||||
{% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value).exists() %}
|
<div class="header">Abstentions ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value).count() }})</div>
|
||||||
<div class="description">
|
{% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value).exists() %}
|
||||||
<ul style="margin-bottom:0">
|
<div class="description">
|
||||||
{% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value) %}
|
<ul style="margin-bottom:0">
|
||||||
<li>{{ vote.voter.first_name }} {{ vote.voter.last_name }}</li>
|
{% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value) %}
|
||||||
{% endfor %}
|
<li>{{ vote.voter.first_name }} {{ vote.voter.last_name }}</li>
|
||||||
</ul>
|
{% endfor %}
|
||||||
</div>
|
</ul>
|
||||||
{% endif %}
|
</div>
|
||||||
</div>
|
|
||||||
{% if revision.can_vote(request.user) %}
|
|
||||||
<button class="ui bottom attached secondary button" type="submit" name="action" value="VoteAbstain">
|
|
||||||
Abstain
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if is_latest and revision.can_vote(request.user) %}
|
||||||
|
<button class="ui bottom attached secondary button" type="submit" name="action" value="VoteAbstain">
|
||||||
|
Abstain
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
{% endif %}
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if is_latest %}
|
||||||
{% if claims is not none %}
|
{% if claims is not none %}
|
||||||
<h2>Reimbursement claims</h2>
|
<h2>Reimbursement claims</h2>
|
||||||
|
|
||||||
|
@ -57,6 +57,7 @@ class BudgetAction(DescriptionEnum):
|
|||||||
CREATE = 5, 'Created'
|
CREATE = 5, 'Created'
|
||||||
EDIT = 10, 'Edited'
|
EDIT = 10, 'Edited'
|
||||||
UPDATE_STATE = 20, 'Updated state'
|
UPDATE_STATE = 20, 'Updated state'
|
||||||
|
AUTO_APPROVE = 30, 'Automatically approved'
|
||||||
|
|
||||||
class BudgetRevision(models.Model):
|
class BudgetRevision(models.Model):
|
||||||
budget = models.ForeignKey(Budget, on_delete=models.CASCADE)
|
budget = models.ForeignKey(Budget, on_delete=models.CASCADE)
|
||||||
|
@ -508,7 +508,27 @@ def budget_action(request, budget, revision):
|
|||||||
vote.vote_type = vote_type.value
|
vote.vote_type = vote_type.value
|
||||||
vote.save()
|
vote.save()
|
||||||
|
|
||||||
# TODO: Check for vote threshold
|
# Check if threshold for automatic approval is reached
|
||||||
|
votes_in_favour = revision.budgetvote_set.filter(is_current=True, vote_type=models.BudgetVoteType.IN_FAVOUR.value).count()
|
||||||
|
if votes_in_favour >= dict(settings.AVAILABLE_APPROVERS)[revision.approver][1]:
|
||||||
|
# Automatically approve
|
||||||
|
revision.copy()
|
||||||
|
revision.time = timezone.now()
|
||||||
|
revision.state = models.BudgetState.APPROVED.value
|
||||||
|
revision.action = models.BudgetAction.AUTO_APPROVE.value
|
||||||
|
revision.save()
|
||||||
|
|
||||||
|
# Send emails
|
||||||
|
users_to_email = set()
|
||||||
|
|
||||||
|
for user in revision.contributors.all():
|
||||||
|
users_to_email.add(user.email)
|
||||||
|
for user in User.objects.filter(groups__name=revision.approver):
|
||||||
|
users_to_email.add(user.email)
|
||||||
|
|
||||||
|
emailer = Emailer()
|
||||||
|
for email in users_to_email:
|
||||||
|
emailer.send_mail([email], 'Budget approved: {} (BU-{})'.format(revision.name, budget.id), 'sstreasury/email/budget_approved.md', {'revision': revision})
|
||||||
|
|
||||||
return redirect(reverse('budget_view', kwargs={'id': budget.id}))
|
return redirect(reverse('budget_view', kwargs={'id': budget.id}))
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user