Add budget graphs
On the budget page, add a pie chart of expenses, and a bar chart comparing expenses to revenue.
This commit is contained in:
parent
8cce90f83b
commit
9c0d7122f6
@ -2,7 +2,8 @@
|
||||
|
||||
{#
|
||||
Society Self-Service
|
||||
Copyright © 2018–2020 Yingtong Li (RunasSudo)
|
||||
Copyright © 2018–2023 Yingtong Li (RunasSudo)
|
||||
Copyright © 2023 MUMUS Inc.
|
||||
|
||||
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
|
||||
@ -21,7 +22,7 @@
|
||||
{% block content %}
|
||||
<div class="ui stackable grid">
|
||||
{# side menu #}
|
||||
<div class="four wide column">
|
||||
<div class="four wide column" id="sidebar">
|
||||
<div class="ui vertical fluid menu">
|
||||
<div class="item">
|
||||
Budgets
|
||||
@ -43,8 +44,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="twelve wide column">
|
||||
{% block maincontent %}{% endblock %}
|
||||
</div>
|
||||
{% block aftersidebar %}
|
||||
<div class="twelve wide column">
|
||||
{% block maincontent %}{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -2,7 +2,8 @@
|
||||
|
||||
{#
|
||||
Society Self-Service
|
||||
Copyright © 2018–2020 Yingtong Li (RunasSudo)
|
||||
Copyright © 2018–2023 Yingtong Li (RunasSudo)
|
||||
Copyright © 2023 MUMUS Inc.
|
||||
|
||||
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
|
||||
@ -20,287 +21,297 @@
|
||||
|
||||
{% block title %}{{ revision.name }}{% endblock %}
|
||||
|
||||
{% block maincontent %}
|
||||
<h1>{{ revision.name }}</h1>
|
||||
{% block aftersidebar %}
|
||||
<div class="eight wide column">
|
||||
<h1>{{ revision.name }}</h1>
|
||||
|
||||
<form class="ui form" action="{{ url('budget_action', kwargs={'id': revision.budget.id}) }}" method="POST">
|
||||
<span class="ui header">Status: {{ revision.get_state_display() }}</span>
|
||||
|
||||
{% if is_latest %}
|
||||
{% if revision.can_submit(request.user) %}
|
||||
<button class="ui mini labeled primary icon button" data-action="Submit" style="margin-left: 1em;" onclick="return uiConfirm(this);"><i class="paper plane icon"></i> Submit</button>
|
||||
{% endif %}
|
||||
{% if revision.can_endorse(request.user) %}
|
||||
<button class="ui mini labeled positive icon button" data-action="Endorse" style="margin-left: 1em;" onclick="return uiConfirm(this);"><i class="check icon"></i> Endorse</button>
|
||||
|
||||
<button class="ui mini labeled basic negative icon button" data-action="Return" onclick="return uiConfirm(this);"><i class="undo icon"></i> Return for re-drafting</button>
|
||||
{% endif %}
|
||||
{% if revision.can_approve(request.user) %}
|
||||
<button class="ui mini labeled positive icon button" data-action="Approve" style="margin-left: 1em;" onclick="return uiConfirm(this);"><i class="check icon"></i> Approve</button>
|
||||
|
||||
<button class="ui mini labeled basic negative icon button" data-action="CmteReturn" onclick="return uiConfirm(this);"><i class="undo icon"></i> Return for re-drafting</button>
|
||||
{% endif %}
|
||||
{% if revision.can_withdraw(request.user) %}
|
||||
<button class="ui mini labeled basic negative icon button" data-action="Withdraw" style="margin-left: 1em;" onclick="return uiConfirm(this);"><i class="undo icon"></i> Withdraw</button>
|
||||
{% endif %}
|
||||
{% if revision.can_cancel(request.user) %}
|
||||
<button class="ui mini labeled basic negative icon button" data-action="Cancel" style="margin-left: 1em;" onclick="return uiConfirm(this);"><i class="times circle outline icon"></i> Cancel</button>
|
||||
{% endif %}
|
||||
|
||||
{% if revision.can_edit(request.user) %}
|
||||
<a class="ui mini labeled right floated icon button" href="{{ url('budget_edit', kwargs={'id': revision.budget.id}) }}"><i class="edit icon"></i> Edit</a>
|
||||
{% endif %}
|
||||
|
||||
<a class="ui mini labeled right floated icon button" href="{{ url('budget_print', kwargs={'id': revision.budget.id}) }}" target="_blank"><i class="print icon"></i> Print</a>
|
||||
|
||||
{% if not revision.can_edit(request.user) and revision.can_withdraw(request.user) %}
|
||||
<div class="ui message">
|
||||
<p>This budget has been submitted and is now awaiting approval. If you wish to edit this budget, you must first withdraw it. This will revert the budget to a draft.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<input type="hidden" name="action" value="">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
|
||||
{% else %}
|
||||
<a class="ui mini labeled right floated icon button" href="{{ url('budget_print', kwargs={'id': revision.budget.id}) }}?revision={{ revision.id }}" target="_blank"><i class="print icon"></i> Print</a>
|
||||
|
||||
<div class="ui visible warning message">
|
||||
<p>You are viewing an older version of this budget. To make any changes, <a href="{{ url('budget_view', kwargs={'id': revision.budget.id}) }}">click here</a> to return to the current version.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
<table class="ui mydefinition table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="two wide">ID</td>
|
||||
<td class="fourteen wide">BU-{{ revision.budget.id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Title</td>
|
||||
<td>{{ revision.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Due date</td>
|
||||
<td>{{ revision.date or '' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Event details</td>
|
||||
<td>
|
||||
{% if revision.event_dt %}{{ localtime(revision.event_dt) }}.{% endif %}
|
||||
{% if revision.event_attendees %}{{ revision.event_attendees }} attendees.{% endif %}
|
||||
{% if not revision.event_dt and not revision.event_attendees %}N/A{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Contributors</td>
|
||||
<td>
|
||||
<div class="ui list">
|
||||
{% for contributor in revision.contributors.all() %}
|
||||
<div class="item">
|
||||
<i class="user circle icon"></i>
|
||||
<div class="content"><a href="mailto:{{ contributor.email }}">
|
||||
{% if contributor.first_name %}
|
||||
{{ contributor.first_name }} {{ contributor.last_name }}
|
||||
{% else %}
|
||||
{{ contributor.email }}
|
||||
{% endif %}
|
||||
</a></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Comments</td>
|
||||
<td>{{ revision.comments|markdown }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Revenue</td>
|
||||
<td>
|
||||
<div id="revenue_grid"></div>
|
||||
{% if revision.revenue_comments %}
|
||||
<div class="ui accordion">
|
||||
<div class="active title">
|
||||
<i class="dropdown icon"></i>
|
||||
Revenue comments
|
||||
</div>
|
||||
<div class="active content">
|
||||
{{ revision.revenue_comments|markdown }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Expenses</td>
|
||||
<td>
|
||||
{% if revision.expense_no_emergency_fund %}
|
||||
<p><input type="checkbox" id="expense_no_emergency_fund" disabled checked> No emergency fund required (please add a comment explaining why)</p>
|
||||
{% endif %}
|
||||
<div id="expense_grid"></div>
|
||||
{% if revision.expense_comments %}
|
||||
<div class="ui accordion">
|
||||
<div class="active title">
|
||||
<i class="dropdown icon"></i>
|
||||
Expense comments
|
||||
</div>
|
||||
<div class="active content">
|
||||
{{ revision.expense_comments|markdown }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Profit (Loss)</td>
|
||||
<td style="text-align: right; font-weight: bold; padding-right: 2.3em;">{{ '${:.2f}'.format(revision.get_revenue_total() - revision.get_expense_total()) if revision.get_revenue_total() >= revision.get_expense_total() else '(${:.2f})'.format(revision.get_expense_total() - revision.get_revenue_total()) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if is_latest %}
|
||||
<form class="ui form" action="{{ url('budget_action', kwargs={'id': revision.budget.id}) }}" method="POST">
|
||||
<div class="required field">
|
||||
<textarea rows="4" name="comment"></textarea>
|
||||
</div>
|
||||
<input type="hidden" name="action">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
|
||||
<button class="ui primary button" data-action="Comment" onclick="return uiSubmit(this);">Comment</button>
|
||||
<span class="ui header">Status: {{ revision.get_state_display() }}</span>
|
||||
|
||||
{% if revision.state == import('sstreasury.models').BudgetState.AWAIT_REVIEW.value and request.user.groups.filter(name='Treasury').exists() %}
|
||||
<button class="ui right floated labeled basic negative icon button" data-action="Comment,Return" onclick="return uiConfirm(this);"><i class="undo icon"></i> Comment and return for re-drafting</button>
|
||||
{% if is_latest %}
|
||||
{% if revision.can_submit(request.user) %}
|
||||
<button class="ui mini labeled primary icon button" data-action="Submit" style="margin-left: 1em;" onclick="return uiConfirm(this);"><i class="paper plane icon"></i> Submit</button>
|
||||
{% endif %}
|
||||
{% if revision.can_endorse(request.user) %}
|
||||
<button class="ui mini labeled positive icon button" data-action="Endorse" style="margin-left: 1em;" onclick="return uiConfirm(this);"><i class="check icon"></i> Endorse</button>
|
||||
|
||||
<button class="ui right floated labeled positive icon button" data-action="Comment,Endorse" onclick="return uiConfirm(this);"><i class="check icon"></i> Comment and endorse</button>
|
||||
{% elif revision.state != import('sstreasury.models').BudgetState.APPROVED.value and request.user.groups.filter(name='Secretary').exists() %}
|
||||
<button class="ui right floated labeled basic negative icon button" data-action="Comment,CmteReturn" onclick="return uiConfirm(this);"><i class="undo icon"></i> Comment and return for re-drafting</button>
|
||||
<button class="ui mini labeled basic negative icon button" data-action="Return" onclick="return uiConfirm(this);"><i class="undo icon"></i> Return for re-drafting</button>
|
||||
{% endif %}
|
||||
{% if revision.can_approve(request.user) %}
|
||||
<button class="ui mini labeled positive icon button" data-action="Approve" style="margin-left: 1em;" onclick="return uiConfirm(this);"><i class="check icon"></i> Approve</button>
|
||||
|
||||
<button class="ui right floated labeled positive icon button" data-action="Comment,Approve" onclick="return uiConfirm(this);"><i class="check icon"></i> Comment and approve</button>
|
||||
<button class="ui mini labeled basic negative icon button" data-action="CmteReturn" onclick="return uiConfirm(this);"><i class="undo icon"></i> Return for re-drafting</button>
|
||||
{% endif %}
|
||||
{% if revision.can_withdraw(request.user) %}
|
||||
<button class="ui mini labeled basic negative icon button" data-action="Withdraw" style="margin-left: 1em;" onclick="return uiConfirm(this);"><i class="undo icon"></i> Withdraw</button>
|
||||
{% endif %}
|
||||
{% if revision.can_cancel(request.user) %}
|
||||
<button class="ui mini labeled basic negative icon button" data-action="Cancel" style="margin-left: 1em;" onclick="return uiConfirm(this);"><i class="times circle outline icon"></i> Cancel</button>
|
||||
{% endif %}
|
||||
|
||||
{% if revision.can_edit(request.user) %}
|
||||
<a class="ui mini labeled right floated icon button" href="{{ url('budget_edit', kwargs={'id': revision.budget.id}) }}"><i class="edit icon"></i> Edit</a>
|
||||
{% endif %}
|
||||
|
||||
<a class="ui mini labeled right floated icon button" href="{{ url('budget_print', kwargs={'id': revision.budget.id}) }}" target="_blank"><i class="print icon"></i> Print</a>
|
||||
|
||||
{% if not revision.can_edit(request.user) and revision.can_withdraw(request.user) %}
|
||||
<div class="ui message">
|
||||
<p>This budget has been submitted and is now awaiting approval. If you wish to edit this budget, you must first withdraw it. This will revert the budget to a draft.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<input type="hidden" name="action" value="">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
|
||||
{% else %}
|
||||
<a class="ui mini labeled right floated icon button" href="{{ url('budget_print', kwargs={'id': revision.budget.id}) }}?revision={{ revision.id }}" target="_blank"><i class="print icon"></i> Print</a>
|
||||
|
||||
<div class="ui visible warning message">
|
||||
<p>You are viewing an older version of this budget. To make any changes, <a href="{{ url('budget_view', kwargs={'id': revision.budget.id}) }}">click here</a> to return to the current version.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
<div class="ui feed">
|
||||
{% for item in history %}
|
||||
{% if item.__class__.__name__ == 'BudgetComment' %}
|
||||
<div class="event">
|
||||
<div class="label">
|
||||
<i class="comment alternate outline icon"></i>
|
||||
<table class="ui mydefinition table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="two wide">ID</td>
|
||||
<td class="fourteen wide">BU-{{ revision.budget.id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Title</td>
|
||||
<td>{{ revision.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Due date</td>
|
||||
<td>{{ revision.date or '' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Event details</td>
|
||||
<td>
|
||||
{% if revision.event_dt %}{{ localtime(revision.event_dt) }}.{% endif %}
|
||||
{% if revision.event_attendees %}{{ revision.event_attendees }} attendees.{% endif %}
|
||||
{% if not revision.event_dt and not revision.event_attendees %}N/A{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Contributors</td>
|
||||
<td>
|
||||
<div class="ui list">
|
||||
{% for contributor in revision.contributors.all() %}
|
||||
<div class="item">
|
||||
<i class="user circle icon"></i>
|
||||
<div class="content"><a href="mailto:{{ contributor.email }}">
|
||||
{% if contributor.first_name %}
|
||||
{{ contributor.first_name }} {{ contributor.last_name }}
|
||||
{% else %}
|
||||
{{ contributor.email }}
|
||||
{% endif %}
|
||||
</a></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="summary">
|
||||
<i class="user circle icon"></i>
|
||||
<a href="mailto:{{ item.author.email }}">{{ item.author.first_name }} {{ item.author.last_name }}</a> commented
|
||||
<div class="date">
|
||||
{{ localtime(item.time) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Comments</td>
|
||||
<td>{{ revision.comments|markdown }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Revenue</td>
|
||||
<td>
|
||||
<div id="revenue_grid"></div>
|
||||
{% if revision.revenue_comments %}
|
||||
<div class="ui accordion">
|
||||
<div class="active title">
|
||||
<i class="dropdown icon"></i>
|
||||
Revenue comments
|
||||
</div>
|
||||
<div class="active content">
|
||||
{{ revision.revenue_comments|markdown }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="extra text">
|
||||
{{ item.content|markdown }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% elif item.__class__.__name__ == 'BudgetRevision' %}
|
||||
<div class="event">
|
||||
<div class="label">
|
||||
<i class="edit icon"></i>
|
||||
</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>
|
||||
{% 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>
|
||||
{% 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() }}
|
||||
{% 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>
|
||||
{% endif %}
|
||||
<div class="date">
|
||||
{{ localtime(item.time) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Expenses</td>
|
||||
<td>
|
||||
{% if revision.expense_no_emergency_fund %}
|
||||
<p><input type="checkbox" id="expense_no_emergency_fund" disabled checked> No emergency fund required (please add a comment explaining why)</p>
|
||||
{% endif %}
|
||||
<div id="expense_grid"></div>
|
||||
{% if revision.expense_comments %}
|
||||
<div class="ui accordion">
|
||||
<div class="active title">
|
||||
<i class="dropdown icon"></i>
|
||||
Expense comments
|
||||
</div>
|
||||
<div class="active content">
|
||||
{{ revision.expense_comments|markdown }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Profit (Loss)</td>
|
||||
<td style="text-align: right; font-weight: bold; padding-right: 2.3em;">{{ '${:.2f}'.format(revision.get_revenue_total() - revision.get_expense_total()) if revision.get_revenue_total() >= revision.get_expense_total() else '(${:.2f})'.format(revision.get_expense_total() - revision.get_revenue_total()) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if claims is not none %}
|
||||
<h2>Reimbursement claims</h2>
|
||||
|
||||
{% if claims %}
|
||||
<table class="ui celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="nine wide">Purpose</th>
|
||||
<th class="four wide">Status</th>
|
||||
<th class="two wide">Total</th>
|
||||
<th class="one wide">View</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for claim in claims %}
|
||||
<tr>
|
||||
<td>{{ claim.purpose }}</td>
|
||||
<td>{{ claim.get_state_display() }}</td>
|
||||
<td>{{ '${:.2f}'.format(claim.get_total()) }}</td>
|
||||
<td>
|
||||
<a href="{{ url('claim_view', kwargs={'id': claim.id}) }}" class="ui tiny primary icon button"><i class="eye icon"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="ui warning message">
|
||||
<p>This list will not include invoices, or other transactions tracked outside of Self Service.</p>
|
||||
{% if is_latest %}
|
||||
<form class="ui form" action="{{ url('budget_action', kwargs={'id': revision.budget.id}) }}" method="POST">
|
||||
<div class="required field">
|
||||
<textarea rows="4" name="comment"></textarea>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>There are no claims to display.</p>
|
||||
<input type="hidden" name="action">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
|
||||
<button class="ui primary button" data-action="Comment" onclick="return uiSubmit(this);">Comment</button>
|
||||
|
||||
{% if revision.state == import('sstreasury.models').BudgetState.AWAIT_REVIEW.value and request.user.groups.filter(name='Treasury').exists() %}
|
||||
<button class="ui right floated labeled basic negative icon button" data-action="Comment,Return" onclick="return uiConfirm(this);"><i class="undo icon"></i> Comment and return for re-drafting</button>
|
||||
|
||||
<button class="ui right floated labeled positive icon button" data-action="Comment,Endorse" onclick="return uiConfirm(this);"><i class="check icon"></i> Comment and endorse</button>
|
||||
{% elif revision.state != import('sstreasury.models').BudgetState.APPROVED.value and request.user.groups.filter(name='Secretary').exists() %}
|
||||
<button class="ui right floated labeled basic negative icon button" data-action="Comment,CmteReturn" onclick="return uiConfirm(this);"><i class="undo icon"></i> Comment and return for re-drafting</button>
|
||||
|
||||
<button class="ui right floated labeled positive icon button" data-action="Comment,Approve" onclick="return uiConfirm(this);"><i class="check icon"></i> Comment and approve</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
<div class="ui feed">
|
||||
{% for item in history %}
|
||||
{% if item.__class__.__name__ == 'BudgetComment' %}
|
||||
<div class="event">
|
||||
<div class="label">
|
||||
<i class="comment alternate outline icon"></i>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="summary">
|
||||
<i class="user circle icon"></i>
|
||||
<a href="mailto:{{ item.author.email }}">{{ item.author.first_name }} {{ item.author.last_name }}</a> commented
|
||||
<div class="date">
|
||||
{{ localtime(item.time) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="extra text">
|
||||
{{ item.content|markdown }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% elif item.__class__.__name__ == 'BudgetRevision' %}
|
||||
<div class="event">
|
||||
<div class="label">
|
||||
<i class="edit icon"></i>
|
||||
</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>
|
||||
{% 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>
|
||||
{% 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() }}
|
||||
{% 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>
|
||||
{% endif %}
|
||||
<div class="date">
|
||||
{{ localtime(item.time) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if claims is not none %}
|
||||
<h2>Reimbursement claims</h2>
|
||||
|
||||
{% if claims %}
|
||||
<table class="ui celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="nine wide">Purpose</th>
|
||||
<th class="four wide">Status</th>
|
||||
<th class="two wide">Total</th>
|
||||
<th class="one wide">View</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for claim in claims %}
|
||||
<tr>
|
||||
<td>{{ claim.purpose }}</td>
|
||||
<td>{{ claim.get_state_display() }}</td>
|
||||
<td>{{ '${:.2f}'.format(claim.get_total()) }}</td>
|
||||
<td>
|
||||
<a href="{{ url('claim_view', kwargs={'id': claim.id}) }}" class="ui tiny primary icon button"><i class="eye icon"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="ui warning message">
|
||||
<p>This list will not include invoices, or other transactions tracked outside of Self Service.</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>There are no claims to display.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="ui modal">
|
||||
<div class="content" data-action="Submit">
|
||||
<p>Are you sure you want to submit this budget for Treasury review? You will not be able to make any additional changes without withdrawing the budget.</p>
|
||||
</div>
|
||||
<div class="content" data-action="Endorse">
|
||||
<p>Are you sure you want to give this budget Treasury endorsement?</p>
|
||||
</div>
|
||||
<div class="content" data-action="Comment,Endorse">
|
||||
<p>Are you sure you want to give this budget Treasury endorsement?</p>
|
||||
</div>
|
||||
<div class="content" data-action="Return">
|
||||
<p>Are you sure you want to refuse this budget Treasury endorsement and return it for re-drafting?</p>
|
||||
</div>
|
||||
<div class="content" data-action="Comment,Return">
|
||||
<p>Are you sure you want to refuse this budget Treasury endorsement and return it for re-drafting?</p>
|
||||
</div>
|
||||
<div class="content" data-action="Approve">
|
||||
<p>Are you sure you want to mark this budget as committee-approved?</p>
|
||||
</div>
|
||||
<div class="content" data-action="Comment,Approve">
|
||||
<p>Are you sure you want to mark this budget as committee-approved?</p>
|
||||
</div>
|
||||
<div class="content" data-action="CmteReturn">
|
||||
<p>Are you sure you want to refuse this budget committee approval and return it for re-drafting?</p>
|
||||
</div>
|
||||
<div class="content" data-action="Comment,CmteReturn">
|
||||
<p>Are you sure you want to refuse this budget committee approval and return it for re-drafting?</p>
|
||||
</div>
|
||||
<div class="content" data-action="Withdraw">
|
||||
<p>Are you sure you want to withdraw this budget from being considered for approval? The budget will be reverted to a draft.</p>
|
||||
</div>
|
||||
<div class="content" data-action="Cancel">
|
||||
<p>Are you sure you want to cancel this budget?</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="ui primary approve button">Continue</div>
|
||||
<div class="ui cancel button">Cancel</div>
|
||||
<div class="ui modal">
|
||||
<div class="content" data-action="Submit">
|
||||
<p>Are you sure you want to submit this budget for Treasury review? You will not be able to make any additional changes without withdrawing the budget.</p>
|
||||
</div>
|
||||
<div class="content" data-action="Endorse">
|
||||
<p>Are you sure you want to give this budget Treasury endorsement?</p>
|
||||
</div>
|
||||
<div class="content" data-action="Comment,Endorse">
|
||||
<p>Are you sure you want to give this budget Treasury endorsement?</p>
|
||||
</div>
|
||||
<div class="content" data-action="Return">
|
||||
<p>Are you sure you want to refuse this budget Treasury endorsement and return it for re-drafting?</p>
|
||||
</div>
|
||||
<div class="content" data-action="Comment,Return">
|
||||
<p>Are you sure you want to refuse this budget Treasury endorsement and return it for re-drafting?</p>
|
||||
</div>
|
||||
<div class="content" data-action="Approve">
|
||||
<p>Are you sure you want to mark this budget as committee-approved?</p>
|
||||
</div>
|
||||
<div class="content" data-action="Comment,Approve">
|
||||
<p>Are you sure you want to mark this budget as committee-approved?</p>
|
||||
</div>
|
||||
<div class="content" data-action="CmteReturn">
|
||||
<p>Are you sure you want to refuse this budget committee approval and return it for re-drafting?</p>
|
||||
</div>
|
||||
<div class="content" data-action="Comment,CmteReturn">
|
||||
<p>Are you sure you want to refuse this budget committee approval and return it for re-drafting?</p>
|
||||
</div>
|
||||
<div class="content" data-action="Withdraw">
|
||||
<p>Are you sure you want to withdraw this budget from being considered for approval? The budget will be reverted to a draft.</p>
|
||||
</div>
|
||||
<div class="content" data-action="Cancel">
|
||||
<p>Are you sure you want to cancel this budget?</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="ui primary approve button">Continue</div>
|
||||
<div class="ui cancel button">Cancel</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="four wide column">
|
||||
<p class="ui header">Expenses</p>
|
||||
<canvas id="chartExpenses"></canvas>
|
||||
|
||||
<p class="ui header">Revenue and expenses</p>
|
||||
<canvas id="chartRevExp"></canvas>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
@ -327,6 +338,15 @@
|
||||
.jsgrid-header-row .jsgrid-header-cell {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
/* Full screen width for graphs */
|
||||
@media only screen and (min-width: 768px) {
|
||||
.ui.container {
|
||||
width: auto;
|
||||
margin-left: 64px !important;
|
||||
margin-right: 64px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@ -355,6 +375,7 @@
|
||||
</script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jsgrid@1.5.3/dist/jsgrid.min.js" integrity="sha256-lzjMTpg04xOdI+MJdjBst98bVI6qHToLyVodu3EywFU=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.3.0/dist/chart.umd.js" integrity="sha256-5dsP1lVzcWPn5aOwu+zs+G+TqAu9oT8NUNM0C4n3H+4=" crossorigin="anonymous"></script>
|
||||
|
||||
<script src="{{ static('sstreasury/budget.js') }}"></script>
|
||||
|
||||
@ -363,5 +384,6 @@
|
||||
var expense_data = JSON.parse({{ import('json').dumps(import('json').dumps(revision.expense))|safe }});
|
||||
var editing = false;
|
||||
makeGrid();
|
||||
makeCharts();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -1,6 +1,7 @@
|
||||
/*
|
||||
Society Self-Service
|
||||
Copyright © 2018-2019 Yingtong Li (RunasSudo)
|
||||
Copyright © 2018-2023 Yingtong Li (RunasSudo)
|
||||
Copyright © 2023 MUMUS Inc.
|
||||
|
||||
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
|
||||
@ -18,22 +19,27 @@
|
||||
|
||||
$('.ui.accordion').accordion();
|
||||
|
||||
var revTotal = 0;
|
||||
var revTotalIWT = 0;
|
||||
var expTotal = 0;
|
||||
var emergency_fund_mult = 0.05;
|
||||
|
||||
function recalcRevTotal(args) {
|
||||
var total = 0;
|
||||
var totalIWT = 0;
|
||||
revTotal = 0;
|
||||
revTotalIWT = 0;
|
||||
for (var row of args.grid.data) {
|
||||
total += row['Unit price'] * row['Units'];
|
||||
revTotal += row['Unit price'] * row['Units'];
|
||||
if (row['Unit price'] > 0 && row['IWT']) {
|
||||
totalIWT += (row['Unit price'] - (row['Unit price'] - 0.8133) / 1.01884) * row['Units'];
|
||||
revTotalIWT += (row['Unit price'] - (row['Unit price'] - 0.8133) / 1.01884) * row['Units'];
|
||||
}
|
||||
}
|
||||
|
||||
$(args.grid._body).find('.totalrow').remove();
|
||||
|
||||
if (totalIWT > 0) {
|
||||
if (revTotalIWT > 0) {
|
||||
var totalrow = $('<tr class="jsgrid-row totalrow" style="font-style: italic;"></tr>');
|
||||
totalrow.append($('<td class="jsgrid-cell">Less IWT fees:</td>').prop('colspan', args.grid.fields.length - (editing ? 2 : 1)));
|
||||
totalrow.append($('<td class="jsgrid-cell jsgrid-align-right"></td>').text('($' + totalIWT.toFixed(2) + ')'));
|
||||
totalrow.append($('<td class="jsgrid-cell jsgrid-align-right"></td>').text('($' + revTotalIWT.toFixed(2) + ')'));
|
||||
if (editing) {
|
||||
totalrow.append($('<td class="jsgrid-cell"></td>'));
|
||||
}
|
||||
@ -42,7 +48,7 @@ function recalcRevTotal(args) {
|
||||
|
||||
var totalrow = $('<tr class="jsgrid-row totalrow" style="font-weight: bold;"></tr>');
|
||||
totalrow.append($('<td class="jsgrid-cell">Total:</td>').prop('colspan', args.grid.fields.length - (editing ? 2 : 1)));
|
||||
totalrow.append($('<td class="jsgrid-cell jsgrid-align-right"></td>').text('$' + (total - totalIWT).toFixed(2)));
|
||||
totalrow.append($('<td class="jsgrid-cell jsgrid-align-right"></td>').text('$' + (revTotal - revTotalIWT).toFixed(2)));
|
||||
if (editing) {
|
||||
totalrow.append($('<td class="jsgrid-cell"></td>'));
|
||||
}
|
||||
@ -50,21 +56,21 @@ function recalcRevTotal(args) {
|
||||
}
|
||||
|
||||
function recalcExpTotal(args) {
|
||||
var total = 0;
|
||||
expTotal = 0;
|
||||
for (var row of args.grid.data) {
|
||||
total += row['Unit price'] * row['Units'];
|
||||
expTotal += row['Unit price'] * row['Units'];
|
||||
}
|
||||
|
||||
$(args.grid._body).find('.totalrow').remove();
|
||||
|
||||
var emergency_fund_mult = 0.05;
|
||||
emergency_fund_mult = 0.05;
|
||||
if ($('#expense_no_emergency_fund').length > 0 && $('#expense_no_emergency_fund').prop('checked')) {
|
||||
emergency_fund_mult = 0;
|
||||
}
|
||||
|
||||
var totalrow = $('<tr class="jsgrid-row totalrow" style="font-style: italic;"></tr>');
|
||||
totalrow.append($('<td class="jsgrid-cell">Plus emergency fund:</td>').prop('colspan', args.grid.fields.length - (editing ? 2 : 1)));
|
||||
totalrow.append($('<td class="jsgrid-cell jsgrid-align-right"></td>').text('$' + (total * emergency_fund_mult).toFixed(2)));
|
||||
totalrow.append($('<td class="jsgrid-cell jsgrid-align-right"></td>').text('$' + (expTotal * emergency_fund_mult).toFixed(2)));
|
||||
if (editing) {
|
||||
totalrow.append($('<td class="jsgrid-cell"></td>'));
|
||||
}
|
||||
@ -72,7 +78,7 @@ function recalcExpTotal(args) {
|
||||
|
||||
var totalrow = $('<tr class="jsgrid-row totalrow" style="font-weight: bold;"></tr>');
|
||||
totalrow.append($('<td class="jsgrid-cell">Total:</td>').prop('colspan', args.grid.fields.length - (editing ? 2 : 1)));
|
||||
totalrow.append($('<td class="jsgrid-cell jsgrid-align-right"></td>').text('$' + (total * (1 + emergency_fund_mult)).toFixed(2)));
|
||||
totalrow.append($('<td class="jsgrid-cell jsgrid-align-right"></td>').text('$' + (expTotal * (1 + emergency_fund_mult)).toFixed(2)));
|
||||
if (editing) {
|
||||
totalrow.append($('<td class="jsgrid-cell"></td>'));
|
||||
}
|
||||
@ -146,3 +152,56 @@ function makeGrid() {
|
||||
$('.jsgrid-filter-row, .jsgrid-insert-row').attr('style', 'display: none !important;'); /* Override Semantic UI */
|
||||
}
|
||||
}
|
||||
|
||||
function makeCharts() {
|
||||
// Display expense, revenue charts on budget view page
|
||||
|
||||
new Chart(document.getElementById('chartExpenses'), {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: expense_data.map(e => e['Description']),
|
||||
datasets: [{
|
||||
label: 'Expenses',
|
||||
data: expense_data.map(e => e['Unit price'] * e['Units'])
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: i => '$' + i.parsed.toFixed(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
new Chart(document.getElementById('chartRevExp'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['Revenue', 'Expenses'],
|
||||
datasets: [{
|
||||
label: 'Budget',
|
||||
data: [revTotal - revTotalIWT, (expTotal * (1 + emergency_fund_mult))],
|
||||
backgroundColor: ['#36a2eb', '#ff6384']
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: i => '$' + i.parsed.y.toFixed(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user