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']
|
||||
|
||||
AVAILABLE_APPROVERS = [
|
||||
# Tuples (committee name, description)
|
||||
('Committee', 'Management Committee'),
|
||||
# Tuples (committee name, (description, votes required to approve))
|
||||
('Committee', ('Management Committee', 10)),
|
||||
]
|
||||
BUDGET_ENABLE_VOTING = True
|
||||
|
||||
|
@ -84,7 +84,7 @@
|
||||
<div class="eleven wide column" style="padding-left: 0; padding-right: 0;">
|
||||
<select class="ui dropdown" name="approver">
|
||||
{% 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 %}
|
||||
</select>
|
||||
</div>
|
||||
|
@ -115,7 +115,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Responsible committee</td>
|
||||
<td>{{ dict(settings.AVAILABLE_APPROVERS)[revision.approver] }}</td>
|
||||
<td>{{ dict(settings.AVAILABLE_APPROVERS)[revision.approver][0] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Comments</td>
|
||||
@ -212,15 +212,16 @@
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="summary">
|
||||
<i class="user circle icon"></i>
|
||||
{% 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 %}
|
||||
<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 %}
|
||||
<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 %}
|
||||
<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 %}
|
||||
<div class="date">
|
||||
{{ localtime(item.time) }}
|
||||
@ -253,78 +254,82 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if revision.state == import('sstreasury.models').BudgetState.ENDORSED.value %}
|
||||
<h2>Committee voting</h2>
|
||||
|
||||
{% if revision.state == import('sstreasury.models').BudgetState.ENDORSED.value %}
|
||||
<h2>Committee voting</h2>
|
||||
|
||||
<form class="ui three column grid" action="{{ url('budget_action', kwargs={'id': revision.budget.id}) }}" method="POST">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
|
||||
<div class="column">
|
||||
<div class="ui fluid card">
|
||||
<div class="content">
|
||||
<div class="header">In favour ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value).count() }})</div>
|
||||
{% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value).exists() %}
|
||||
<div class="description">
|
||||
<ul style="margin-bottom:0">
|
||||
{% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value) %}
|
||||
<li>{{ vote.voter.first_name }} {{ vote.voter.last_name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</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 %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="ui fluid card">
|
||||
<div class="content">
|
||||
<div class="header">Against ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value).count() }})</div>
|
||||
{% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value).exists() %}
|
||||
<div class="description">
|
||||
<ul style="margin-bottom:0">
|
||||
{% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value) %}
|
||||
<li>{{ vote.voter.first_name }} {{ vote.voter.last_name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if 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 class="ui fluid card">
|
||||
<div class="content">
|
||||
<div class="header">Abstentions ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value).count() }})</div>
|
||||
{% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value).exists() %}
|
||||
<div class="description">
|
||||
<ul style="margin-bottom:0">
|
||||
{% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value) %}
|
||||
<li>{{ vote.voter.first_name }} {{ vote.voter.last_name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if revision.can_vote(request.user) %}
|
||||
<button class="ui bottom attached secondary button" type="submit" name="action" value="VoteAbstain">
|
||||
Abstain
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
<p>{{ dict(settings.AVAILABLE_APPROVERS)[revision.approver][1] }} votes in favour are required for approval.</p>
|
||||
|
||||
<form class="ui three column grid" action="{{ url('budget_action', kwargs={'id': revision.budget.id}) }}" method="POST">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
|
||||
<div class="column">
|
||||
<div class="ui fluid card">
|
||||
<div class="content">
|
||||
<div class="header">In favour ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value).count() }})</div>
|
||||
{% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value).exists() %}
|
||||
<div class="description">
|
||||
<ul style="margin-bottom:0">
|
||||
{% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.IN_FAVOUR.value) %}
|
||||
<li>{{ vote.voter.first_name }} {{ vote.voter.last_name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</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 class="ui fluid card">
|
||||
<div class="content">
|
||||
<div class="header">Against ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value).count() }})</div>
|
||||
{% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value).exists() %}
|
||||
<div class="description">
|
||||
<ul style="margin-bottom:0">
|
||||
{% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.AGAINST.value) %}
|
||||
<li>{{ vote.voter.first_name }} {{ vote.voter.last_name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</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 class="ui fluid card">
|
||||
<div class="content">
|
||||
<div class="header">Abstentions ({{ revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value).count() }})</div>
|
||||
{% if revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value).exists() %}
|
||||
<div class="description">
|
||||
<ul style="margin-bottom:0">
|
||||
{% for vote in revision.budgetvote_set.filter(is_current=True, vote_type=import('sstreasury.models').BudgetVoteType.ABSTAIN.value) %}
|
||||
<li>{{ vote.voter.first_name }} {{ vote.voter.last_name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</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>
|
||||
{% endif %}
|
||||
|
||||
{% if is_latest %}
|
||||
{% if claims is not none %}
|
||||
<h2>Reimbursement claims</h2>
|
||||
|
||||
|
@ -57,6 +57,7 @@ class BudgetAction(DescriptionEnum):
|
||||
CREATE = 5, 'Created'
|
||||
EDIT = 10, 'Edited'
|
||||
UPDATE_STATE = 20, 'Updated state'
|
||||
AUTO_APPROVE = 30, 'Automatically approved'
|
||||
|
||||
class BudgetRevision(models.Model):
|
||||
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.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}))
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user