Revenue and economy revamp (#285)
* rework economy stuff, add revenue model, unfinished code! * part 2 of economy overhaul. add views for dealing with revenue. rework expense views.
This commit is contained in:
parent
a8051783cb
commit
a057bd6464
|
@ -47,12 +47,5 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
$('.table').DataTable();
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
|
||||
|
|
|
@ -6,19 +6,18 @@
|
|||
|
||||
{% include 'includes/expense_detail_panel.html' %}
|
||||
|
||||
{% if expense.approved != None %}
|
||||
<div class="alert alert-info">This expense has already been approved/rejected.</div>
|
||||
<div class="lead">Economy Team notes for this expense:</div>
|
||||
<div class="lead">{{ expense.notes }}</div>
|
||||
{% else %}
|
||||
{% if expense.approved == None %}
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
{% bootstrap_button "<i class='fas fa-check'></i> Approve Expense" button_type="submit" button_class="btn-success" name="approve" %}
|
||||
{% bootstrap_button "<i class='fas fa-times'></i> Reject Expense" button_type="submit" button_class="btn-danger" name="reject" %}
|
||||
<a href="{% url 'backoffice:expense_manage_list' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
|
||||
<a href="{% url 'backoffice:expense_list' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<br>
|
||||
<a href="{% url 'backoffice:expense_list' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Back to Expense List</a>
|
||||
|
||||
{% endblock content %}
|
||||
|
28
src/backoffice/templates/expense_list_backoffice.html
Normal file
28
src/backoffice/templates/expense_list_backoffice.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Manage Expenses for {{ camp.title }}</h2>
|
||||
|
||||
{% if unapproved_expenses %}
|
||||
<div class="lead">
|
||||
This table shows unapproved expenses for {{ camp.title }}.
|
||||
</div>
|
||||
|
||||
{% include 'includes/expense_list_panel.html' with expense_list=unapproved_expenses %}
|
||||
|
||||
<hr>
|
||||
{% endif %}
|
||||
|
||||
<div class="lead">
|
||||
This table shows all approved expenses for {{ camp.title }}.
|
||||
</div>
|
||||
|
||||
{% include 'includes/expense_list_panel.html' %}
|
||||
|
||||
{% endblock content %}
|
|
@ -1,91 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Manage Expenses for {{ camp.title }}</h2>
|
||||
|
||||
{% if unapproved_expenses %}
|
||||
<div class="lead">
|
||||
This table shows unapproved expenses for {{ camp.title }}.
|
||||
</div>
|
||||
|
||||
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Description</th>
|
||||
<th>Amount</th>
|
||||
<th>Paid by</th>
|
||||
<th>Approved?</th>
|
||||
<th>Reimbursement?</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for expense in unapproved_expenses %}
|
||||
<tr>
|
||||
<td>{{ expense.user }}</td>
|
||||
<td>{{ expense.description }}</td>
|
||||
<td>{{ expense.amount }} DKK</td>
|
||||
<td>{% if expense.paid_by_bornhack %}BornHack{% else %}{{ expense.user }}{% endif %}</td>
|
||||
<td>{{ expense.approval_status }}</td>
|
||||
<td>{% if expense.reimbursement %}<a href="{% url 'backoffice:reimbursement_detail' camp_slug=camp.slug pk=expense.reimbursement.pk %}" class="btn btn-primary">Details</a>{% else %}N/A{% endif %}</td>
|
||||
<td>
|
||||
<a class="btn btn-primary" href="{% url "backoffice:expense_detail" camp_slug=camp.slug pk=expense.pk %}">Details</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="lead">
|
||||
This table shows all expenses for {{ camp.title }}.
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Description</th>
|
||||
<th>Amount</th>
|
||||
<th>Paid by</th>
|
||||
<th>Approved?</th>
|
||||
<th>Reimbursement?</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for expense in expense_list %}
|
||||
<tr>
|
||||
<td>{{ expense.user }}</td>
|
||||
<td>{{ expense.description }}</td>
|
||||
<td>{{ expense.amount }} DKK</td>
|
||||
<td>{% if expense.paid_by_bornhack %}BornHack{% else %}{{ expense.user }}{% endif %}</td>
|
||||
<td>{{ expense.approval_status }}</td>
|
||||
<td>{% if expense.reimbursement %}<a href="{% url 'backoffice:reimbursement_detail' camp_slug=camp.slug pk=expense.reimbursement.pk %}" class="btn btn-primary">Details</a>{% else %}N/A{% endif %}</td>
|
||||
<td>
|
||||
<a class="btn btn-primary" href="{% url "backoffice:expense_detail" camp_slug=camp.slug pk=expense.pk %}">Details</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
$('.table').DataTable();
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock content %}
|
|
@ -64,13 +64,17 @@
|
|||
|
||||
{% if perms.camps.economyteam_permission %}
|
||||
<h3>Economy Team</h3>
|
||||
<a href="{% url 'backoffice:expense_list' camp_slug=camp.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">Expenses</h4>
|
||||
<p class="list-group-item-text">Use this view to see and approve/reject expenses.</p>
|
||||
</a>
|
||||
<a href="{% url 'backoffice:reimbursement_list' camp_slug=camp.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">Reimbursements</h4>
|
||||
<p class="list-group-item-text">Use this view to view and create reimbursements</p>
|
||||
<p class="list-group-item-text">Use this view to view and create reimbursements for approved expenses.</p>
|
||||
</a>
|
||||
<a href="{% url 'backoffice:expense_manage_list' camp_slug=camp.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">Expenses</h4>
|
||||
<p class="list-group-item-text">Use this view to see and approve/reject expenses</p>
|
||||
<a href="{% url 'backoffice:revenue_list' camp_slug=camp.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">Revenues</h4>
|
||||
<p class="list-group-item-text">Use this view to see and approve/reject revenues.</p>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -79,11 +79,5 @@
|
|||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
$('.table').DataTable();
|
||||
});
|
||||
</script>
|
||||
{% endblock content %}
|
||||
|
||||
|
||||
|
|
|
@ -36,9 +36,4 @@
|
|||
</table>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
$('.table').DataTable();
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock content %}
|
||||
|
|
|
@ -42,10 +42,4 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
$('.table').DataTable();
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock content %}
|
||||
|
|
|
@ -42,10 +42,4 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
$('.table').DataTable();
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock content %}
|
||||
|
|
|
@ -43,12 +43,5 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
$('.table').DataTable();
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
|
||||
|
|
17
src/backoffice/templates/reimbursement_delete.html
Normal file
17
src/backoffice/templates/reimbursement_delete.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<h3>Delete Reimbursement for User {{ reimbursement_user }}</h3>
|
||||
|
||||
<p>The total amount for this reimbursement is <b>{{ reimbursement.amount }} DKK</b></p>
|
||||
|
||||
<p class="lead">Really delete this reimbursement?</p>
|
||||
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_button "<i class='fas fa-times'></i> Yes, Delete it" button_type="submit" button_class="btn-danger" name="Delete" %}
|
||||
<a href="{% url 'backoffice:reimbursement_list' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
|
@ -2,11 +2,13 @@
|
|||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<h3>Manage Reimbursement</h3>
|
||||
<h3>Reimbursement Details</h3>
|
||||
|
||||
{% include 'includes/reimbursement_detail_panel.html' %}
|
||||
|
||||
<a class="btn btn-success" href="{% url "backoffice:reimbursement_list" camp_slug=camp.slug %}">Back to reimbursement list</a>
|
||||
<a class="btn btn-primary" href="{% url "backoffice:reimbursement_update" camp_slug=camp.slug pk=reimbursement.pk %}"><i class="fas fa-edit"></i> Update</a>
|
||||
<a class="btn btn-danger" href="{% url "backoffice:reimbursement_delete" camp_slug=camp.slug pk=reimbursement.pk %}"><i class="fas fa-times"></i> Delete</a>
|
||||
<a class="btn btn-primary" href="{% url "backoffice:reimbursement_list" camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Back to reimbursement list</a>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
|
|
16
src/backoffice/templates/reimbursement_form.html
Normal file
16
src/backoffice/templates/reimbursement_form.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<h3>Update Reimbursement for User {{ reimbursement_user }}</h3>
|
||||
|
||||
<p class="lead">The total amount for this reimbursement is <b>{{ reimbursement.amount }} DKK</b></p>
|
||||
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
{% bootstrap_button "<i class='fas fa-check'></i> Update" button_type="submit" button_class="btn-success" name="Update" %}
|
||||
<a href="{% url 'backoffice:reimbursement_list' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h2>Reimbursements for {{ camp.title }}</h2>
|
||||
</div>
|
||||
<br>
|
||||
<div class="row">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Camp</th>
|
||||
<th>Created</th>
|
||||
<th>Reimbursement User</th>
|
||||
<th>Economy Team Notes</th>
|
||||
<th>Amount</th>
|
||||
<th>Paid</th>
|
||||
<th>Expenses</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for reim in reimbursement_list %}
|
||||
<tr>
|
||||
<td>{{ reim.camp }}</td>
|
||||
<td>{{ reim.created }} by {{ reim.user }}</td>
|
||||
<td>{{ reim.reimbursement_user }}</td>
|
||||
<td>{{ reim.notes|default:"N/A" }}</td>
|
||||
<td>{{ reim.amount }} DKK</td>
|
||||
<td>{{ reim.paid }}</td>
|
||||
<td>{% for expense in reim.expenses.all %}{% if not expense.paid_by_bornhack %}{{ expense.pk }} - {{ expense.amount }} DKK - {{ expense.description }}<br>{% endif %}{% endfor %}</td>
|
||||
<td>
|
||||
<a class="btn btn-primary" href="{% url "backoffice:reimbursement_detail" camp_slug=camp.slug pk=reim.pk %}">Details</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<a class="btn btn-success" href="{% url "backoffice:reimbursement_create_userselect" camp_slug=camp.slug %}">Create New Reimbursement</a>
|
||||
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
$('.table').DataTable();
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock content %}
|
15
src/backoffice/templates/reimbursement_list_backoffice.html
Normal file
15
src/backoffice/templates/reimbursement_list_backoffice.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Reimbursements for {{ camp.title }}</h2>
|
||||
|
||||
{% include 'includes/reimbursement_list_panel.html' %}
|
||||
|
||||
<a class="btn btn-success" href="{% url "backoffice:reimbursement_create_userselect" camp_slug=camp.slug %}"><i class="fas fa-plus"></i> Create New Reimbursement</a>
|
||||
{% endblock content %}
|
22
src/backoffice/templates/revenue_detail_backoffice.html
Normal file
22
src/backoffice/templates/revenue_detail_backoffice.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<h3>Manage Revenue</h3>
|
||||
|
||||
{% include 'includes/revenue_detail_panel.html' %}
|
||||
|
||||
{% if revenue.approved == None %}
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
{% bootstrap_button "<i class='fas fa-check'></i> Approve Revenue" button_type="submit" button_class="btn-success" name="approve" %}
|
||||
{% bootstrap_button "<i class='fas fa-times'></i> Reject Revenue" button_type="submit" button_class="btn-danger" name="reject" %}
|
||||
<a href="{% url 'backoffice:revenue_list' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
|
||||
</form>
|
||||
{% endif %}
|
||||
<br>
|
||||
<a href="{% url 'backoffice:revenue_list' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Back to Revenue List</a>
|
||||
|
||||
{% endblock content %}
|
||||
|
28
src/backoffice/templates/revenue_list_backoffice.html
Normal file
28
src/backoffice/templates/revenue_list_backoffice.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Manage Revenues for {{ camp.title }}</h2>
|
||||
|
||||
{% if unapproved_revenues %}
|
||||
<div class="lead">
|
||||
This table shows unapproved revenues for {{ camp.title }}.
|
||||
</div>
|
||||
|
||||
{% include 'includes/revenue_list_panel.html' with revenue_list=unapproved_revenues %}
|
||||
|
||||
<hr>
|
||||
{% endif %}
|
||||
|
||||
<div class="lead">
|
||||
This table shows all approved revenues for {{ camp.title }}.
|
||||
</div>
|
||||
|
||||
{% include 'includes/revenue_list_panel.html' %}
|
||||
|
||||
{% endblock content %}
|
|
@ -47,12 +47,5 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
$('.table').DataTable();
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
|
||||
|
|
|
@ -35,10 +35,4 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
$('.table').DataTable();
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock content %}
|
||||
|
|
|
@ -6,24 +6,64 @@ app_name = 'backoffice'
|
|||
|
||||
urlpatterns = [
|
||||
path('', BackofficeIndexView.as_view(), name='index'),
|
||||
# infodesk
|
||||
path('product_handout/', ProductHandoutView.as_view(), name='product_handout'),
|
||||
path('badge_handout/', BadgeHandoutView.as_view(), name='badge_handout'),
|
||||
path('ticket_checkin/', TicketCheckinView.as_view(), name='ticket_checkin'),
|
||||
|
||||
# public names
|
||||
path('public_credit_names/', ApproveNamesView.as_view(), name='public_credit_names'),
|
||||
|
||||
# merchandise orders
|
||||
path('merchandise_orders/', MerchandiseOrdersView.as_view(), name='merchandise_orders'),
|
||||
path('merchandise_to_order/', MerchandiseToOrderView.as_view(), name='merchandise_to_order'),
|
||||
|
||||
# village orders
|
||||
path('village_orders/', VillageOrdersView.as_view(), name='village_orders'),
|
||||
path('village_to_order/', VillageToOrderView.as_view(), name='village_to_order'),
|
||||
|
||||
# manage proposals
|
||||
path('manage_proposals/', include([
|
||||
path('', ManageProposalsView.as_view(), name='manage_proposals'),
|
||||
path('speakers/<uuid:pk>/', SpeakerProposalManageView.as_view(), name='speakerproposal_manage'),
|
||||
path('events/<uuid:pk>/', EventProposalManageView.as_view(), name='eventproposal_manage'),
|
||||
])),
|
||||
path('village_orders/', VillageOrdersView.as_view(), name='village_orders'),
|
||||
path('village_to_order/', VillageToOrderView.as_view(), name='village_to_order'),
|
||||
path('economy/expenses/', ExpenseManageListView.as_view(), name='expense_manage_list'),
|
||||
path('economy/expenses/<uuid:pk>/', ExpenseManageDetailView.as_view(), name='expense_detail'),
|
||||
path('economy/reimbursements/', ReimbursementListView.as_view(), name='reimbursement_list'),
|
||||
path('economy/reimbursements/<uuid:pk>/', ReimbursementDetailView.as_view(), name='reimbursement_detail'),
|
||||
path('economy/reimbursements/create/', ReimbursementCreateUserSelectView.as_view(), name='reimbursement_create_userselect'),
|
||||
path('economy/reimbursements/create/<int:user_id>/', ReimbursementCreateView.as_view(), name='reimbursement_create'),
|
||||
|
||||
# economy
|
||||
path('economy/',
|
||||
include([
|
||||
# expenses
|
||||
path('expenses/',
|
||||
include([
|
||||
path('', ExpenseListView.as_view(), name='expense_list'),
|
||||
path('<uuid:pk>/', ExpenseDetailView.as_view(), name='expense_detail'),
|
||||
]),
|
||||
),
|
||||
|
||||
# revenues
|
||||
path('revenues/',
|
||||
include([
|
||||
path('', RevenueListView.as_view(), name='revenue_list'),
|
||||
path('<uuid:pk>/', RevenueDetailView.as_view(), name='revenue_detail'),
|
||||
]),
|
||||
),
|
||||
|
||||
# reimbursements
|
||||
path('reimbursements/',
|
||||
include([
|
||||
path('', ReimbursementListView.as_view(), name='reimbursement_list'),
|
||||
path('<uuid:pk>/',
|
||||
include([
|
||||
path('', ReimbursementDetailView.as_view(), name='reimbursement_detail'),
|
||||
path('update/', ReimbursementUpdateView.as_view(), name='reimbursement_update'),
|
||||
path('delete/', ReimbursementDeleteView.as_view(), name='reimbursement_delete'),
|
||||
]),
|
||||
),
|
||||
path('create/', ReimbursementCreateUserSelectView.as_view(), name='reimbursement_create_userselect'),
|
||||
path('create/<int:user_id>/', ReimbursementCreateView.as_view(), name='reimbursement_create'),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
),
|
||||
]
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ from itertools import chain
|
|||
from django.contrib.auth.mixins import PermissionRequiredMixin, UserPassesTestMixin
|
||||
from django.contrib.auth.models import User
|
||||
from django.views.generic import TemplateView, ListView, DetailView
|
||||
from django.views.generic.edit import CreateView, UpdateView
|
||||
from django.views.generic.edit import CreateView, UpdateView, DeleteView
|
||||
from django.shortcuts import redirect, get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.contrib import messages
|
||||
|
@ -18,7 +18,7 @@ from shop.models import OrderProductRelation
|
|||
from tickets.models import ShopTicket, SponsorTicket, DiscountTicket
|
||||
from profiles.models import Profile
|
||||
from program.models import SpeakerProposal, EventProposal
|
||||
from economy.models import Expense, Reimbursement
|
||||
from economy.models import Expense, Reimbursement, Revenue
|
||||
from utils.mixins import RaisePermissionRequiredMixin
|
||||
from teams.models import Team
|
||||
from .mixins import *
|
||||
|
@ -98,7 +98,7 @@ class ManageProposalsView(CampViewMixin, ContentTeamPermissionMixin, ListView):
|
|||
return context
|
||||
|
||||
|
||||
class ProposalManageView(CampViewMixin, ContentTeamPermissionMixin, UpdateView):
|
||||
class ProposalManageBaseView(CampViewMixin, ContentTeamPermissionMixin, UpdateView):
|
||||
"""
|
||||
This class contains the shared logic between SpeakerProposalManageView and EventProposalManageView
|
||||
"""
|
||||
|
@ -120,7 +120,7 @@ class ProposalManageView(CampViewMixin, ContentTeamPermissionMixin, UpdateView):
|
|||
return redirect(reverse('backoffice:manage_proposals', kwargs={'camp_slug': self.camp.slug}))
|
||||
|
||||
|
||||
class SpeakerProposalManageView(ProposalManageView):
|
||||
class SpeakerProposalManageView(ProposalManageBaseView):
|
||||
"""
|
||||
This view allows an admin to approve/reject SpeakerProposals
|
||||
"""
|
||||
|
@ -128,7 +128,7 @@ class SpeakerProposalManageView(ProposalManageView):
|
|||
template_name = "manage_speakerproposal.html"
|
||||
|
||||
|
||||
class EventProposalManageView(ProposalManageView):
|
||||
class EventProposalManageView(ProposalManageBaseView):
|
||||
"""
|
||||
This view allows an admin to approve/reject EventProposals
|
||||
"""
|
||||
|
@ -228,13 +228,16 @@ class VillageToOrderView(CampViewMixin, OrgaTeamPermissionMixin, TemplateView):
|
|||
return context
|
||||
|
||||
|
||||
class ExpenseManageListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
|
||||
################################
|
||||
########### EXPENSES ###########
|
||||
|
||||
class ExpenseListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
|
||||
model = Expense
|
||||
template_name = 'expense_manage_list.html'
|
||||
template_name = 'expense_list_backoffice.html'
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
"""
|
||||
Exclude unapproved expenses
|
||||
Exclude unapproved expenses, they are shown seperately
|
||||
"""
|
||||
queryset = super().get_queryset(**kwargs)
|
||||
return queryset.exclude(approved__isnull=True)
|
||||
|
@ -247,9 +250,10 @@ class ExpenseManageListView(CampViewMixin, EconomyTeamPermissionMixin, ListView)
|
|||
context['unapproved_expenses'] = Expense.objects.filter(camp=self.camp, approved__isnull=True)
|
||||
return context
|
||||
|
||||
class ExpenseManageDetailView(CampViewMixin, EconomyTeamPermissionMixin, UpdateView):
|
||||
|
||||
class ExpenseDetailView(CampViewMixin, EconomyTeamPermissionMixin, UpdateView):
|
||||
model = Expense
|
||||
template_name = 'expense_manage_detail.html'
|
||||
template_name = 'expense_detail_backoffice.html'
|
||||
fields = ['notes']
|
||||
|
||||
def form_valid(self, form):
|
||||
|
@ -265,12 +269,15 @@ class ExpenseManageDetailView(CampViewMixin, EconomyTeamPermissionMixin, UpdateV
|
|||
expense.reject(self.request)
|
||||
else:
|
||||
messages.error(self.request, "Unknown submit action")
|
||||
return redirect(reverse('backoffice:expense_manage_list', kwargs={'camp_slug': self.camp.slug}))
|
||||
return redirect(reverse('backoffice:expense_list', kwargs={'camp_slug': self.camp.slug}))
|
||||
|
||||
|
||||
######################################
|
||||
########### REIMBURSEMENTS ###########
|
||||
|
||||
class ReimbursementListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
|
||||
model = Reimbursement
|
||||
template_name = 'reimbursement_list.html'
|
||||
template_name = 'reimbursement_list_backoffice.html'
|
||||
|
||||
|
||||
class ReimbursementDetailView(CampViewMixin, EconomyTeamPermissionMixin, DetailView):
|
||||
|
@ -373,3 +380,68 @@ class ReimbursementCreateView(CampViewMixin, EconomyTeamPermissionMixin, CreateV
|
|||
messages.success(self.request, "Reimbursement %s has been created" % reimbursement.pk)
|
||||
return redirect(reverse('backoffice:reimbursement_detail', kwargs={'camp_slug': self.camp.slug, 'pk': reimbursement.pk}))
|
||||
|
||||
|
||||
class ReimbursementUpdateView(CampViewMixin, EconomyTeamPermissionMixin, UpdateView):
|
||||
model = Reimbursement
|
||||
template_name = 'reimbursement_form.html'
|
||||
fields = ['notes', 'paid']
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('backoffice:reimbursement_detail', kwargs={'camp_slug': self.camp.slug, 'pk': self.get_object().pk})
|
||||
|
||||
class ReimbursementDeleteView(CampViewMixin, EconomyTeamPermissionMixin, DeleteView):
|
||||
model = Reimbursement
|
||||
template_name = 'reimbursement_delete.html'
|
||||
fields = ['notes', 'paid']
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
if self.get_object().paid:
|
||||
messages.error(request, "This reimbursement has already been paid so it cannot be deleted")
|
||||
return redirect(reverse('backoffice:reimbursement_list', kwargs={'camp_slug': self.camp.slug}))
|
||||
return response
|
||||
|
||||
|
||||
################################
|
||||
########### REVENUES ###########
|
||||
|
||||
class RevenueListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
|
||||
model = Revenue
|
||||
template_name = 'revenue_list_backoffice.html'
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
"""
|
||||
Exclude unapproved revenues, they are shown seperately
|
||||
"""
|
||||
queryset = super().get_queryset(**kwargs)
|
||||
return queryset.exclude(approved__isnull=True)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""
|
||||
Include unapproved revenues seperately
|
||||
"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['unapproved_revenues'] = Revenue.objects.filter(camp=self.camp, approved__isnull=True)
|
||||
return context
|
||||
|
||||
|
||||
class RevenueDetailView(CampViewMixin, EconomyTeamPermissionMixin, UpdateView):
|
||||
model = Revenue
|
||||
template_name = 'revenue_detail_backoffice.html'
|
||||
fields = ['notes']
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
We have two submit buttons in this form, Approve and Reject
|
||||
"""
|
||||
revenue = form.save()
|
||||
if 'approve' in form.data:
|
||||
# approve button was pressed
|
||||
revenue.approve(self.request)
|
||||
elif 'reject' in form.data:
|
||||
# reject button was pressed
|
||||
revenue.reject(self.request)
|
||||
else:
|
||||
messages.error(self.request, "Unknown submit action")
|
||||
return redirect(reverse('backoffice:revenue_list', kwargs={'camp_slug': self.camp.slug}))
|
||||
|
||||
|
|
17
src/camps/migrations/0032_auto_20180917_1754.py
Normal file
17
src/camps/migrations/0032_auto_20180917_1754.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 2.0.4 on 2018-09-17 15:54
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('camps', '0031_auto_20180830_0014'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='permission',
|
||||
options={'default_permissions': (), 'managed': False, 'permissions': (('backoffice_permission', 'BackOffice access'), ('orgateam_permission', 'Orga Team permissions set'), ('infoteam_permission', 'Info Team permissions set'), ('economyteam_permission', 'Economy Team permissions set'), ('contentteam_permission', 'Content Team permissions set'), ('expense_create_permission', 'Expense Create permission'), ('revenue_create_permission', 'Revenue Create permission'))},
|
||||
),
|
||||
]
|
|
@ -25,6 +25,7 @@ class Permission(models.Model):
|
|||
("economyteam_permission", "Economy Team permissions set"),
|
||||
("contentteam_permission", "Content Team permissions set"),
|
||||
("expense_create_permission", "Expense Create permission"),
|
||||
("revenue_create_permission", "Revenue Create permission"),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
from django.contrib import admin
|
||||
from .models import Expense, Reimbursement
|
||||
from .models import Expense, Reimbursement, Revenue
|
||||
|
||||
|
||||
### expenses
|
||||
|
||||
def approve_expenses(modeladmin, request, queryset):
|
||||
for expense in queryset.all():
|
||||
expense.approve(request)
|
||||
|
@ -22,11 +24,34 @@ class ExpenseAdmin(admin.ModelAdmin):
|
|||
actions = [approve_expenses, reject_expenses]
|
||||
|
||||
|
||||
### revenues
|
||||
|
||||
def approve_revenues(modeladmin, request, queryset):
|
||||
for revenue in queryset.all():
|
||||
revenue.approve(request)
|
||||
approve_revenues.short_description = "Approve Revenues"
|
||||
|
||||
|
||||
def reject_revenues(modeladmin, request, queryset):
|
||||
for revenue in queryset.all():
|
||||
revenue.reject(request)
|
||||
reject_revenues.short_description = "Reject Revenues"
|
||||
|
||||
|
||||
@admin.register(Revenue)
|
||||
class RevenueAdmin(admin.ModelAdmin):
|
||||
list_filter = ['camp', 'responsible_team', 'approved', 'user']
|
||||
list_display = ['user', 'description', 'amount', 'camp', 'responsible_team', 'approved']
|
||||
search_fields = ['description', 'amount', 'user']
|
||||
actions = [approve_revenues, reject_revenues]
|
||||
|
||||
|
||||
### reimbursements
|
||||
|
||||
@admin.register(Reimbursement)
|
||||
class ReimbursementAdmin(admin.ModelAdmin):
|
||||
def get_amount(self, obj):
|
||||
return obj.amount
|
||||
|
||||
list_filter = ['camp', 'user', 'reimbursement_user', 'paid']
|
||||
list_display = ['camp', 'user', 'reimbursement_user', 'paid', 'notes', 'get_amount']
|
||||
search_fields = ['user__username', 'reimbursement_user__username', 'notes']
|
||||
|
|
|
@ -4,14 +4,15 @@ from django.conf import settings
|
|||
|
||||
from utils.email import add_outgoing_email
|
||||
|
||||
# expense emails
|
||||
|
||||
def send_accountingsystem_email(expense):
|
||||
def send_accountingsystem_expense_email(expense):
|
||||
"""
|
||||
Sends an email to the accountingsystem with the invoice as an attachment,
|
||||
and with the expense uuid and description in email subject
|
||||
"""
|
||||
add_outgoing_email(
|
||||
"emails/accountingsystem_email.txt",
|
||||
"emails/accountingsystem_expense_email.txt",
|
||||
formatdict=dict(expense=expense),
|
||||
subject="Expense %s for %s" % (expense.pk, expense.camp.title),
|
||||
to_recipients=[settings.ACCOUNTINGSYSTEM_EMAIL],
|
||||
|
@ -43,3 +44,43 @@ def send_expense_rejected_email(expense):
|
|||
to_recipients=[expense.user.emailaddress_set.get(primary=True).email],
|
||||
)
|
||||
|
||||
# revenue emails
|
||||
|
||||
def send_accountingsystem_revenue_email(revenue):
|
||||
"""
|
||||
Sends an email to the accountingsystem with the invoice as an attachment,
|
||||
and with the revenue uuid and description in email subject
|
||||
"""
|
||||
add_outgoing_email(
|
||||
"emails/accountingsystem_revenue_email.txt",
|
||||
formatdict=dict(revenue=revenue),
|
||||
subject="Revenue %s for %s" % (revenue.pk, revenue.camp.title),
|
||||
to_recipients=[settings.ACCOUNTINGSYSTEM_EMAIL],
|
||||
attachment=revenue.invoice.read(),
|
||||
attachment_filename=os.path.basename(revenue.invoice.file.name),
|
||||
)
|
||||
|
||||
|
||||
def send_revenue_approved_email(revenue):
|
||||
"""
|
||||
Sends a revenue-approved email to the user who created the revenue
|
||||
"""
|
||||
add_outgoing_email(
|
||||
"emails/revenue_approved_email.txt",
|
||||
formatdict=dict(revenue=revenue),
|
||||
subject="Your revenue for %s has been approved." % revenue.camp.title,
|
||||
to_recipients=[revenue.user.emailaddress_set.get(primary=True).email],
|
||||
)
|
||||
|
||||
|
||||
def send_revenue_rejected_email(revenue):
|
||||
"""
|
||||
Sends an revenue-rejected email to the user who created the revenue
|
||||
"""
|
||||
add_outgoing_email(
|
||||
"emails/revenue_rejected_email.txt",
|
||||
formatdict=dict(revenue=revenue),
|
||||
subject="Your revenue for %s has been rejected." % revenue.camp.title,
|
||||
to_recipients=[revenue.user.emailaddress_set.get(primary=True).email],
|
||||
)
|
||||
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
import os, magic, copy
|
||||
|
||||
from django import forms
|
||||
from .models import Expense
|
||||
from .models import Expense, Revenue
|
||||
|
||||
|
||||
class ExpenseCreateForm(forms.ModelForm):
|
||||
class CleanInvoiceForm(forms.ModelForm):
|
||||
"""
|
||||
We have to define this form explicitly because we want our ImageField to accept PDF files as well as images,
|
||||
and we cannot change the clean_* methods with an autogenerated form from inside views.py
|
||||
"""
|
||||
class Meta:
|
||||
model = Expense
|
||||
fields = ['description', 'amount', 'invoice', 'paid_by_bornhack', 'responsible_team']
|
||||
|
||||
invoice = forms.FileField()
|
||||
|
||||
def clean_invoice(self):
|
||||
|
@ -36,3 +32,27 @@ class ExpenseCreateForm(forms.ModelForm):
|
|||
# this is either a valid image, or has mimetype application/pdf, all good
|
||||
return uploaded_file
|
||||
|
||||
|
||||
class ExpenseCreateForm(CleanInvoiceForm):
|
||||
class Meta:
|
||||
model = Expense
|
||||
fields = ['description', 'amount', 'invoice', 'paid_by_bornhack', 'responsible_team']
|
||||
|
||||
|
||||
class ExpenseUpdateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Expense
|
||||
fields = ['description', 'amount', 'paid_by_bornhack', 'responsible_team']
|
||||
|
||||
|
||||
class RevenueCreateForm(CleanInvoiceForm):
|
||||
class Meta:
|
||||
model = Revenue
|
||||
fields = ['description', 'amount', 'invoice', 'responsible_team']
|
||||
|
||||
|
||||
class RevenueUpdateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Revenue
|
||||
fields = ['description', 'amount', 'responsible_team']
|
||||
|
||||
|
|
40
src/economy/migrations/0002_revenue.py
Normal file
40
src/economy/migrations/0002_revenue.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
# Generated by Django 2.0.4 on 2018-09-16 13:27
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shop', '0057_order_notes'),
|
||||
('teams', '0049_auto_20180815_1119'),
|
||||
('camps', '0031_auto_20180830_0014'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('economy', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Revenue',
|
||||
fields=[
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('updated', models.DateTimeField(auto_now=True)),
|
||||
('amount', models.DecimalField(decimal_places=2, help_text='The amount of this revenue in DKK. Must match the amount on the documentation uploaded below.', max_digits=12)),
|
||||
('description', models.CharField(help_text='A short description of this revenue. Please keep it meningful as it helps the Economy team a lot when categorising revenue. 200 characters or fewer.', max_length=200)),
|
||||
('invoice', models.ImageField(help_text='The invoice file for this revenue. Please make sure the amount on the invoice matches the amount you entered above. All common image formats are accepted, as well as PDF.', upload_to='revenues/')),
|
||||
('approved', models.NullBooleanField(default=None, help_text='True if this Revenue has been approved by the responsible team. False if it has been rejected. Blank if noone has decided yet.')),
|
||||
('notes', models.TextField(blank=True, help_text='Economy Team notes for this revenue. Only visible to the Economy team and the submitting user.')),
|
||||
('camp', models.ForeignKey(help_text='The camp to which this revenue belongs', on_delete=django.db.models.deletion.PROTECT, related_name='revenues', to='camps.Camp')),
|
||||
('invoice_fk', models.ForeignKey(help_text='The Invoice object to which this Revenue object relates. Can be None if this revenue does not have a related BornHack Invoice.', on_delete=django.db.models.deletion.PROTECT, related_name='revenues', to='shop.Invoice')),
|
||||
('responsible_team', models.ForeignKey(help_text='The team to which this revenue belongs. When in doubt pick the Economy team.', on_delete=django.db.models.deletion.PROTECT, related_name='revenues', to='teams.Team')),
|
||||
('user', models.ForeignKey(help_text='The user who submitted this revenue', on_delete=django.db.models.deletion.PROTECT, related_name='revenues', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
19
src/economy/migrations/0003_auto_20180917_1933.py
Normal file
19
src/economy/migrations/0003_auto_20180917_1933.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 2.0.4 on 2018-09-17 17:33
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('economy', '0002_revenue'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='revenue',
|
||||
name='invoice_fk',
|
||||
field=models.ForeignKey(blank=True, help_text='The Invoice object to which this Revenue object relates. Can be None if this revenue does not have a related BornHack Invoice.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='revenues', to='shop.Invoice'),
|
||||
),
|
||||
]
|
|
@ -10,7 +10,20 @@ class ExpensePermissionMixin(object):
|
|||
if obj.user == self.request.user or self.request.user.has_perm('camps.economyteam_permission'):
|
||||
return obj
|
||||
else:
|
||||
# the current user is different from the user who submitted the expense, and user is not in the economy team; fuckery is afoot, no thanks
|
||||
# the current user is different from the user who submitted the expense, and current user is not in the economy team; fuckery is afoot, no thanks
|
||||
raise Http404()
|
||||
|
||||
|
||||
class RevenuePermissionMixin(object):
|
||||
"""
|
||||
This mixin checks if request.user submitted the Revenue, or if request.user has camps.economyteam_permission
|
||||
"""
|
||||
def get_object(self, queryset=None):
|
||||
obj = super().get_object(queryset=queryset)
|
||||
if obj.user == self.request.user or self.request.user.has_perm('camps.economyteam_permission'):
|
||||
return obj
|
||||
else:
|
||||
# the current user is different from the user who submitted the revenue, and current user is not in the economy team; fuckery is afoot, no thanks
|
||||
raise Http404()
|
||||
|
||||
|
||||
|
@ -20,9 +33,9 @@ class ReimbursementPermissionMixin(object):
|
|||
"""
|
||||
def get_object(self, queryset=None):
|
||||
obj = super().get_object(queryset=queryset)
|
||||
if obj.user == self.request.user or self.request.user.has_perm('camps.economyteam_permission'):
|
||||
if obj.reimbursement_user == self.request.user or self.request.user.has_perm('camps.economyteam_permission'):
|
||||
return obj
|
||||
else:
|
||||
# the current user is different from the user who owns the reimbursement, and user is not in the economy team; fuckery is afoot, no thanks
|
||||
# the current user is different from the user who "owns" the reimbursement, and current user is not in the economy team; fuckery is afoot, no thanks
|
||||
raise Http404()
|
||||
|
||||
|
|
|
@ -9,6 +9,122 @@ from django.core.exceptions import ValidationError
|
|||
from utils.models import CampRelatedModel, UUIDModel
|
||||
from .email import *
|
||||
|
||||
class Revenue(CampRelatedModel, UUIDModel):
|
||||
"""
|
||||
The Revenue model represents any type of income for BornHack.
|
||||
Most Revenue objects will have a FK to the Invoice model, but only if the revenue relates directly to an Invoice in our system.
|
||||
Other Revenue objects (such as money returned from bottle deposits) will not have a related BornHack Invoice object.
|
||||
"""
|
||||
camp = models.ForeignKey(
|
||||
'camps.Camp',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='revenues',
|
||||
help_text='The camp to which this revenue belongs',
|
||||
)
|
||||
|
||||
user = models.ForeignKey(
|
||||
'auth.User',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='revenues',
|
||||
help_text='The user who submitted this revenue',
|
||||
)
|
||||
|
||||
amount = models.DecimalField(
|
||||
decimal_places=2,
|
||||
max_digits=12,
|
||||
help_text='The amount of this revenue in DKK. Must match the amount on the documentation uploaded below.',
|
||||
)
|
||||
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
help_text='A short description of this revenue. Please keep it meningful as it helps the Economy team a lot when categorising revenue. 200 characters or fewer.',
|
||||
)
|
||||
|
||||
invoice = models.ImageField(
|
||||
help_text='The invoice file for this revenue. Please make sure the amount on the invoice matches the amount you entered above. All common image formats are accepted, as well as PDF.',
|
||||
upload_to='revenues/',
|
||||
)
|
||||
|
||||
invoice_fk = models.ForeignKey(
|
||||
'shop.Invoice',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='revenues',
|
||||
help_text='The Invoice object to which this Revenue object relates. Can be None if this revenue does not have a related BornHack Invoice.',
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
responsible_team = models.ForeignKey(
|
||||
'teams.Team',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='revenues',
|
||||
help_text='The team to which this revenue belongs. When in doubt pick the Economy team.'
|
||||
)
|
||||
|
||||
approved = models.NullBooleanField(
|
||||
default=None,
|
||||
help_text='True if this Revenue has been approved by the responsible team. False if it has been rejected. Blank if noone has decided yet.'
|
||||
)
|
||||
|
||||
notes = models.TextField(
|
||||
blank=True,
|
||||
help_text='Economy Team notes for this revenue. Only visible to the Economy team and the submitting user.'
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
if self.amount < 0:
|
||||
raise ValidationError('Amount of a Revenue object can not be negative')
|
||||
|
||||
@property
|
||||
def invoice_filename(self):
|
||||
return os.path.basename(self.invoice.file.name)
|
||||
|
||||
@property
|
||||
def approval_status(self):
|
||||
if self.approved == None:
|
||||
return "Pending approval"
|
||||
elif self.approved == True:
|
||||
return "Approved"
|
||||
else:
|
||||
return "Rejected"
|
||||
|
||||
def approve(self, request):
|
||||
"""
|
||||
This method marks a revenue as approved.
|
||||
Approving a revenue triggers an email to the economy system, and another email to the user who submitted the revenue
|
||||
"""
|
||||
if request.user == self.user:
|
||||
messages.error(request, "You cannot approve your own revenues, aka. the anti-stein-bagger defense")
|
||||
return
|
||||
|
||||
# mark as approved and save
|
||||
self.approved = True
|
||||
self.save()
|
||||
|
||||
# send email to economic for this revenue
|
||||
send_accountingsystem_revenue_email(revenue=self)
|
||||
|
||||
# send email to the user
|
||||
send_revenue_approved_email(revenue=self)
|
||||
|
||||
# message to the browser
|
||||
messages.success(request, "Revenue %s approved" % self.pk)
|
||||
|
||||
def reject(self, request):
|
||||
"""
|
||||
This method marks a revenue as not approved.
|
||||
Not approving a revenue triggers an email to the user who submitted the revenue in the first place.
|
||||
"""
|
||||
# mark as not approved and save
|
||||
self.approved = False
|
||||
self.save()
|
||||
|
||||
# send email to the user
|
||||
send_revenue_rejected_email(revenue=self)
|
||||
|
||||
# message to the browser
|
||||
messages.success(request, "Revenue %s rejected" % self.pk)
|
||||
|
||||
|
||||
class Expense(CampRelatedModel, UUIDModel):
|
||||
camp = models.ForeignKey(
|
||||
|
@ -95,7 +211,7 @@ class Expense(CampRelatedModel, UUIDModel):
|
|||
Approving an expense triggers an email to the economy system, and another email to the user who submitted the expense in the first place.
|
||||
"""
|
||||
if request.user == self.user:
|
||||
messages.error(request, "You cannot approve your own expenses, aka. the anti-stein-bagger defence")
|
||||
messages.error(request, "You cannot approve your own expenses, aka. the anti-stein-bagger defense")
|
||||
return
|
||||
|
||||
# mark as approved and save
|
||||
|
@ -103,7 +219,7 @@ class Expense(CampRelatedModel, UUIDModel):
|
|||
self.save()
|
||||
|
||||
# send email to economic for this expense
|
||||
send_accountingsystem_email(expense=self)
|
||||
send_accountingsystem_expense_email(expense=self)
|
||||
|
||||
# send email to the user
|
||||
send_expense_approved_email(expense=self)
|
||||
|
@ -162,6 +278,13 @@ class Reimbursement(CampRelatedModel, UUIDModel):
|
|||
help_text="Check when this reimbursement has been paid to the user",
|
||||
)
|
||||
|
||||
@property
|
||||
def covered_expenses(self):
|
||||
"""
|
||||
Returns a queryset of all expenses covered by this reimbursement. Does not include the expense that paid for the reimbursement.
|
||||
"""
|
||||
return self.expenses.filter(paid_by_bornhack=False)
|
||||
|
||||
@property
|
||||
def amount(self):
|
||||
"""
|
||||
|
@ -172,3 +295,4 @@ class Reimbursement(CampRelatedModel, UUIDModel):
|
|||
amount += expense.amount
|
||||
return amount
|
||||
|
||||
|
||||
|
|
45
src/economy/templates/dashboard.html
Normal file
45
src/economy/templates/dashboard.html
Normal file
|
@ -0,0 +1,45 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block title %}
|
||||
Economy | {{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
<{% block content %}
|
||||
<h3>Your {{ camp.title }} Economy Overview</h3>
|
||||
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>What</th>
|
||||
<th>Description</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><h4>Expenses</h4></td>
|
||||
<td>You have <b>{{ expense_count }} expense{{ expense_count|pluralize }}</b> ({{ approved_expense_count }} approved, {{ rejected_expense_count }} rejected, and {{ unapproved_expense_count }} pending approval) for {{ camp.title }}, for a total of <b>{{ expense_total|default:"0" }} DKK</b>.</td>
|
||||
<td>
|
||||
<a href="{% url "economy:expense_list" camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-list"></i> List Expenses</a>
|
||||
<a href="{% url "economy:expense_create" camp_slug=camp.slug %}" class="btn btn-success"><i class="fas fa-plus"></i> Create Expense</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><h4>Reimbursements</h4></td>
|
||||
<td>You have <b>{{ reimbursement_count }} reimbursement{{ reimbursement_count|pluralize }}</b> ({{ paid_reimbursement_count }} paid, {{ unpaid_reimbursement_count }} pending payment) for {{ camp.title }}, for a total of <b>{{ reimbursement_total }} DKK</b>.</td>
|
||||
<td>
|
||||
<a href="{% url "economy:reimbursement_list" camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-list"></i> List Reimbursements</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><h4>Revenue</h4></td>
|
||||
<td>You have <b>{{ revenue_count }} revenue{{ revenue_count|pluralize }}</b> ({{ approved_revenue_count }} approved, {{ rejected_revenue_count }} rejected, and {{ unapproved_revenue_count }} still pending approval) for {{ camp.title }}, for a total of <b>{{ revenue_total|default:"0" }} DKK</b>.</td>
|
||||
<td>
|
||||
<a href="{% url "economy:revenue_list" camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-list"></i> List Revenues</a>
|
||||
<a href="{% url "economy:revenue_create" camp_slug=camp.slug %}" class="btn btn-success"><i class="fas fa-plus"></i> Create Revenue</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
|
@ -0,0 +1,10 @@
|
|||
New revenue for {{ revenue.camp }}
|
||||
|
||||
The attached receipt for revenue {{ revenue.pk }} has the following description:
|
||||
|
||||
{{ revenue.description }}
|
||||
|
||||
Greetings
|
||||
|
||||
The {{ revenue.camp.title }} Economy Team
|
||||
|
10
src/economy/templates/emails/revenue_approved_email.txt
Normal file
10
src/economy/templates/emails/revenue_approved_email.txt
Normal file
|
@ -0,0 +1,10 @@
|
|||
Hi,
|
||||
|
||||
Your revenue {{ revenue.pk }} for {{ revenue.camp.title }} has been approved. The amount is DKK {{ revenue.amount }} and description of the revenue is:
|
||||
|
||||
{{ revenue.description }}
|
||||
|
||||
Have a nice day!
|
||||
|
||||
The {{ revenue.camp.title }} Economy Team
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
Hi,
|
||||
|
||||
A new revenue {{ revenue.pk }} for {{ revenue.responsible_team.name }} Team was just submitted by user {{ revenue.user }}. The amount is DKK {{ revenue.amount }} and description of the revenue is:
|
||||
|
||||
{{ revenue.description }}
|
||||
|
||||
Have a nice day!
|
||||
|
||||
The {{ revenue.camp.title }} Team
|
||||
|
12
src/economy/templates/emails/revenue_rejected_email.txt
Normal file
12
src/economy/templates/emails/revenue_rejected_email.txt
Normal file
|
@ -0,0 +1,12 @@
|
|||
Hi,
|
||||
|
||||
Your revenue {{ revenue.pk }} for {{ revenue.camp.title }} has been rejected. The amount is DKK {{ revenue.amount }} and description of the revenue is:
|
||||
|
||||
{{ revenue.description }}
|
||||
|
||||
Please contact us for more info.
|
||||
|
||||
Have a nice day!
|
||||
|
||||
The {{ revenue.camp.title }} Economy Team
|
||||
|
16
src/economy/templates/expense_delete.html
Normal file
16
src/economy/templates/expense_delete.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block title %}
|
||||
Delete Expense | {{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h3>Really delete expense {{ expense.uuid }}?</h3>
|
||||
{% include 'includes/expense_detail_panel.html' %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-danger" type="submit"><i class="fas fa-times"></i> Delete Expense</button>
|
||||
<a href="{% url 'economy:expense_list' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -1,11 +1,16 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block title %}
|
||||
{% if object %}Update{% else %}Create{% endif %} Expense | {{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h3>Create {{ camp.title }} Expense</h3>
|
||||
<h3>{% if object %}Update{% else %}Create{% endif %} {{ camp.title }} Expense</h3>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<button class="btn btn-primary pull-right" type="submit">Save</button>
|
||||
<button class="btn btn-success" type="submit"><i class="fas fa-plus"></i> Save</button>
|
||||
<a href="{% url 'economy:expense_list' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
|
@ -13,38 +13,7 @@ Expenses | {{ block.super }}
|
|||
<{% block content %}
|
||||
<h3>Your {{ camp.title }} Expenses</h3>
|
||||
|
||||
{% if expense_list %}
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Paid by</th>
|
||||
<th>Amount</th>
|
||||
<th>Description</th>
|
||||
<th>Responsible Team</th>
|
||||
<th>Approved</th>
|
||||
<th>Reimbursement?</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for expense in expense_list %}
|
||||
<tr>
|
||||
<td>{% if expense.paid_by_bornhack %}BornHack{% else %}You ({{ expense.user }}){% endif %}</td>
|
||||
<td>{{ expense.amount }} DKK</td>
|
||||
<td>{{ expense.description }}</td>
|
||||
<td>{{ expense.responsible_team.name }} Team</td>
|
||||
<td>{{ expense.approval_status }}</td>
|
||||
<td>{% if expense.reimbursement %}<a href="{% url 'economy:reimbursement_detail' camp_slug=camp.slug pk=expense.reimbursement.pk %}" class="btn btn-primary">Details</a>{% else %}N/A{% endif %}</td>
|
||||
<td>
|
||||
<a class="btn btn-primary" href="{% url 'economy:expense_detail' camp_slug=camp.slug pk=expense.uuid %}"><i class="fas fa-search"></i> Details</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<h4>No expenses found for BornHack 2018, you haven't submitted any yet!</h4>
|
||||
{% endif %}
|
||||
{% include 'includes/expense_list_panel.html' %}
|
||||
|
||||
{% if perms.camps.expense_create_permission %}
|
||||
<a class="btn btn-primary" href="{% url 'economy:expense_create' camp_slug=camp.slug %}"><i class="fas fa-plus"></i> Create Expense</a>
|
||||
|
@ -52,57 +21,4 @@ Expenses | {{ block.super }}
|
|||
<div class="alert alert-danger"><p class="lead"><span class="text-error">You don't have permission to add expenses. Please ask someone from the Economy team to add the permission if you need it.</p></div>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>Your {{ camp.title }} Reimbursements</h3>
|
||||
|
||||
{% if reimbursement_list %}
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Camp</th>
|
||||
<th>User</th>
|
||||
<th>Economy Team Notes</th>
|
||||
<th>Amount</th>
|
||||
<th>Paid</th>
|
||||
<th>Expenses</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for reim in reimbursement_list %}
|
||||
<tr>
|
||||
<td>{{ reim.camp }}</td>
|
||||
<td>{{ reim.user }}</td>
|
||||
<td>{{ reim.notes|default:"N/A" }}</td>
|
||||
<td>{{ reim.amount }} DKK</td>
|
||||
<td>{{ reim.paid }}</td>
|
||||
<td>
|
||||
{% for expense in reim.expenses.all %}
|
||||
{% if not expense.paid_by_bornhack %}
|
||||
{% if request.resolver_match.app_name == "backoffice" %}
|
||||
<a href="{% url 'backoffice:expense_detail' camp_slug=camp.slug pk=expense.pk %}" class="btn btn-primary">Details</a> {{ expense.amount }} DKK - {{ expense.description }}<br>
|
||||
{% else %}
|
||||
<a href="{% url 'economy:expense_detail' camp_slug=camp.slug pk=expense.pk %}" class="btn btn-primary">Details</a> {{ expense.amount }} DKK - {{ expense.description }}<br>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
<a class="btn btn-primary" href="{% url "economy:reimbursement_detail" camp_slug=camp.slug pk=reim.pk %}">Details</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<h4>No reimbursements found for BornHack 2018</h4>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
$('.table').DataTable();
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,49 +1,54 @@
|
|||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Expense Details for {{ expense.pk }}</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Amount</th>
|
||||
<td>{{ expense.amount }} DKK</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<td>{{ expense.description }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Paid by BornHack?</th>
|
||||
<td>This expense was paid by {% if expense.paid_by_bornhack %}<b>BornHack</b>{% else %}<b>{{ expense.user }}</b>, and will be reimbursed when approved.{% endif %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Filename</th>
|
||||
<td>{{ expense.invoice }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Approved?</th>
|
||||
<td>{{ expense.approval_status }}</td>
|
||||
</tr>
|
||||
{% if not expense.paid_by_bornhack %}
|
||||
<tr>
|
||||
<th>Reimbursement?</th>
|
||||
{% if request.resolver_match.app_name == "backoffice" %}
|
||||
<td>{% if expense.reimbursement %}<a class="btn btn-primary" target="_blank" href="{% url 'backoffice:reimbursement_detail' camp_slug=camp.slug pk=expense.reimbursement.pk %}">{{ expense.reimbursement.pk }}</a>{% else %}N/A{% endif %}</td>
|
||||
{% else %}
|
||||
<td>{% if expense.reimbursement %}<a class="btn btn-primary" target="_blank" href="{% url 'economy:reimbursement_detail' camp_slug=camp.slug pk=expense.reimbursement.pk %}">{{ expense.reimbursement.pk }}</a>{% else %}N/A{% endif %}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Amount</th>
|
||||
<td>{{ expense.amount }} DKK</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<td>{{ expense.description }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Paid by BornHack?</th>
|
||||
<td>This expense was paid by {% if expense.paid_by_bornhack %}<b>BornHack</b>{% else %}<b>{{ expense.user }}</b>, and will be reimbursed when approved.{% endif %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Filename</th>
|
||||
<td>{{ expense.invoice }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Approved?</th>
|
||||
<td>{{ expense.approval_status }}</td>
|
||||
</tr>
|
||||
{% if not expense.paid_by_bornhack %}
|
||||
<tr>
|
||||
<th>Reimbursement?</th>
|
||||
<td>
|
||||
{% if expense.reimbursement %}
|
||||
{% if request.resolver_match.app_name == "backoffice" %}
|
||||
<a class="btn btn-primary" href="{% url 'backoffice:reimbursement_detail' camp_slug=camp.slug pk=expense.reimbursement.pk %}">{{ expense.reimbursement.pk }}</a>
|
||||
{% else %}
|
||||
<a class="btn btn-primary" href="{% url 'economy:reimbursement_detail' camp_slug=camp.slug pk=expense.reimbursement.pk %}">{{ expense.reimbursement.pk }}</a>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th>Invoice</th>
|
||||
<td>
|
||||
<a href="{% url 'economy:expense_invoice' camp_slug=camp.slug pk=expense.pk %}"><img src="{% url 'economy:expense_invoice' camp_slug=camp.slug pk=expense.pk %}?preview" height=200></a><br>
|
||||
Filename: {{ expense.invoice_filename }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Economy Team Notes</th>
|
||||
<td>{{ expense.notes|default:"N/A" }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th>Invoice</th>
|
||||
<td>
|
||||
<a href="{% url 'economy:expense_invoice' camp_slug=camp.slug pk=expense.pk %}" target="_blank"><img src="{% url 'economy:expense_invoice' camp_slug=camp.slug pk=expense.pk %}?preview" height=200></a><br>
|
||||
Filename: {{ expense.invoice_filename }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Economy Team Notes</th>
|
||||
<td>{{ expense.notes|default:"N/A" }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
59
src/economy/templates/includes/expense_list_panel.html
Normal file
59
src/economy/templates/includes/expense_list_panel.html
Normal file
|
@ -0,0 +1,59 @@
|
|||
{% if expense_list %}
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if not reimbursement %}
|
||||
<th>Paid by</th>
|
||||
{% endif %}
|
||||
<th>Amount</th>
|
||||
<th>Description</th>
|
||||
<th>Responsible Team</th>
|
||||
{% if not reimbursement %}
|
||||
<th>Approved</th>
|
||||
<th>Reimbursement</th>
|
||||
{% endif %}
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for expense in expense_list %}
|
||||
<tr>
|
||||
{% if not reimbursement %}
|
||||
<td>{% if expense.paid_by_bornhack %}BornHack{% else %}{{ expense.user }}{% endif %}</td>
|
||||
{% endif %}
|
||||
<td>{{ expense.amount }} DKK</td>
|
||||
<td>{{ expense.description }}</td>
|
||||
<td>{{ expense.responsible_team.name }} Team</td>
|
||||
|
||||
{% if not reimbursement %}
|
||||
<td>{{ expense.approval_status }}</td>
|
||||
<td>
|
||||
{% if expense.reimbursement and not expense.paid_by_bornhack %}
|
||||
{% if request.resolver_match.app_name == "backoffice" %}
|
||||
<a href="{% url 'backoffice:reimbursement_detail' camp_slug=camp.slug pk=expense.reimbursement.pk %}" class="btn btn-primary"><i class="fas fa-search"></i> Details</a>
|
||||
{% else %}
|
||||
<a href="{% url 'economy:reimbursement_detail' camp_slug=camp.slug pk=expense.reimbursement.pk %}" class="btn btn-primary"><i class="fas fa-search"></i> Details</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
<td>
|
||||
{% if request.resolver_match.app_name == "backoffice" %}
|
||||
<a class="btn btn-primary" href="{% url 'backoffice:expense_detail' camp_slug=camp.slug pk=expense.uuid %}"><i class="fas fa-search"></i> Details</a>
|
||||
{% else %}
|
||||
<a class="btn btn-primary" href="{% url 'economy:expense_detail' camp_slug=camp.slug pk=expense.uuid %}"><i class="fas fa-search"></i> Details</a>
|
||||
<a class="btn btn-primary" href="{% url 'economy:expense_update' camp_slug=camp.slug pk=expense.uuid %}"><i class="fas fa-edit"></i> Update</a>
|
||||
<a class="btn btn-danger" href="{% url 'economy:expense_delete' camp_slug=camp.slug pk=expense.uuid %}"><i class="fas fa-times"></i> Delete</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<h4>No expenses found.</h4>
|
||||
{% endif %}
|
||||
|
|
@ -25,15 +25,7 @@
|
|||
<tr>
|
||||
<th>Expenses covered by this Reimbursement</th>
|
||||
<td>
|
||||
{% for expense in reimbursement.expenses.all %}
|
||||
{% if not expense.paid_by_bornhack %}
|
||||
{% if request.resolver_match.app_name == "backoffice" %}
|
||||
<a href="{% url 'backoffice:expense_detail' camp_slug=camp.slug pk=expense.pk %}" class="btn btn-primary">{{ expense.pk }}</a> {{ expense.amount }} DKK - {{ expense.description }}<br>
|
||||
{% else %}
|
||||
<a href="{% url 'economy:expense_detail' camp_slug=camp.slug pk=expense.pk %}" class="btn btn-primary">{{ expense.pk }}</a> {{ expense.amount }} DKK - {{ expense.description }}<br>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% include 'includes/expense_list_panel.html' with expense_list=reimbursement.covered_expenses.all %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
39
src/economy/templates/includes/reimbursement_list_panel.html
Normal file
39
src/economy/templates/includes/reimbursement_list_panel.html
Normal file
|
@ -0,0 +1,39 @@
|
|||
{% if reimbursement_list %}
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Camp</th>
|
||||
<th>User</th>
|
||||
<th>Economy Team Notes</th>
|
||||
<th>Amount</th>
|
||||
<th>Paid</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for reim in reimbursement_list %}
|
||||
<tr>
|
||||
<td>{{ reim.camp }}</td>
|
||||
<td>{{ reim.user }}</td>
|
||||
<td>{{ reim.notes|default:"N/A" }}</td>
|
||||
<td>{{ reim.amount }} DKK</td>
|
||||
<td>{{ reim.paid }}</td>
|
||||
<td>
|
||||
{% if request.resolver_match.app_name == "backoffice" %}
|
||||
<a class="btn btn-primary" href="{% url "backoffice:reimbursement_detail" camp_slug=camp.slug pk=reim.pk %}"><i class="fas fa-search"></i> Details</a>
|
||||
<a class="btn btn-primary" href="{% url "backoffice:reimbursement_update" camp_slug=camp.slug pk=reim.pk %}"><i class="fas fa-edit"></i> Update</a>
|
||||
{% if not reim.paid %}
|
||||
<a class="btn btn-danger" href="{% url "backoffice:reimbursement_delete" camp_slug=camp.slug pk=reim.pk %}"><i class="fas fa-times"></i> Delete</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<a class="btn btn-primary" href="{% url "economy:reimbursement_detail" camp_slug=camp.slug pk=reim.pk %}"><i class="fas fa-search"></i> Details</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<h4>No reimbursements found for {{ camp.title }}</h4>
|
||||
{% endif %}
|
||||
|
35
src/economy/templates/includes/revenue_detail_panel.html
Normal file
35
src/economy/templates/includes/revenue_detail_panel.html
Normal file
|
@ -0,0 +1,35 @@
|
|||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Revenue Details for {{ revenue.pk }}</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Amount</th>
|
||||
<td>{{ revenue.amount }} DKK</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<td>{{ revenue.description }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Filename</th>
|
||||
<td>{{ revenue.invoice }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Approved?</th>
|
||||
<td>{{ revenue.approval_status }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Invoice</th>
|
||||
<td>
|
||||
<a href="{% url 'economy:revenue_invoice' camp_slug=camp.slug pk=revenue.pk %}"><img src="{% url 'economy:revenue_invoice' camp_slug=camp.slug pk=revenue.pk %}?preview" height=200></a><br>
|
||||
Filename: {{ revenue.invoice_filename }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Economy Team Notes</th>
|
||||
<td>{{ revenue.notes|default:"N/A" }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
35
src/economy/templates/includes/revenue_list_panel.html
Normal file
35
src/economy/templates/includes/revenue_list_panel.html
Normal file
|
@ -0,0 +1,35 @@
|
|||
{% if revenue_list %}
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Amaount</th>
|
||||
<th>Description</th>
|
||||
<th>Responsible Team</th>
|
||||
<th>Approved</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for revenue in revenue_list %}
|
||||
<tr>
|
||||
<td>{{ revenue.amount }} DKK</td>
|
||||
<td>{{ revenue.description }}</td>
|
||||
<td>{{ revenue.responsible_team.name }} Team</td>
|
||||
<td>{{ revenue.approval_status }}</td>
|
||||
<td>
|
||||
{% if request.resolver_match.app_name == "backoffice" %}
|
||||
<a class="btn btn-primary" href="{% url 'backoffice:revenue_detail' camp_slug=camp.slug pk=revenue.uuid %}"><i class="fas fa-search"></i> Details</a>
|
||||
{% else %}
|
||||
<a class="btn btn-primary" href="{% url 'economy:revenue_detail' camp_slug=camp.slug pk=revenue.uuid %}"><i class="fas fa-search"></i> Details</a>
|
||||
<a class="btn btn-primary" href="{% url 'economy:revenue_update' camp_slug=camp.slug pk=revenue.uuid %}"><i class="fas fa-edit"></i> Update</a>
|
||||
<a class="btn btn-danger" href="{% url 'economy:revenue_delete' camp_slug=camp.slug pk=revenue.uuid %}"><i class="fas fa-times"></i> Delete</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<h4>No revenues found.</h4>
|
||||
{% endif %}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block title %}
|
||||
Reimbursement Details | {{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h3>Reimbursement Details</h3>
|
||||
|
||||
|
|
18
src/economy/templates/reimbursement_list.html
Normal file
18
src/economy/templates/reimbursement_list.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block title %}
|
||||
Expenses | {{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
|
||||
<{% block content %}
|
||||
<h3>Your {{ camp.title }} Reimbursements</h3>
|
||||
|
||||
{% include 'includes/reimbursement_list_panel.html' %}
|
||||
|
||||
{% endblock %}
|
16
src/economy/templates/revenue_delete.html
Normal file
16
src/economy/templates/revenue_delete.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block title %}
|
||||
Delete Revenue | {{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h3>Really delete revenue {{ revenue.uuid }}?</h3>
|
||||
{% include 'includes/revenue_detail_panel.html' %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-danger" type="submit"><i class="fas fa-times"></i> Delete Revenue</button>
|
||||
<a href="{% url 'economy:revenue_list' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
|
||||
</form>
|
||||
{% endblock %}
|
12
src/economy/templates/revenue_detail.html
Normal file
12
src/economy/templates/revenue_detail.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}
|
||||
Revenue Details | {{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
{% include 'includes/revenue_detail_panel.html' %}
|
||||
</div>
|
||||
<a class="btn btn-primary" href="{% url 'economy:revenue_list' camp_slug=camp.slug %}">Back to Revenue List</a>
|
||||
{% endblock %}
|
16
src/economy/templates/revenue_form.html
Normal file
16
src/economy/templates/revenue_form.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block title %}
|
||||
{% if object %}Update{% else %}Create{% endif %} Revenue | {{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h3>{% if object %}Update{% else %}Create{% endif %} {{ camp.title }} Revenue</h3>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<button class="btn btn-primary" type="submit"><i class="fas fa-check"></i> Save</button>
|
||||
<a href="{% url 'economy:revenue_list' camp_slug=camp.slug %}" class="btn btn-danger"><i class="fas fa-undo"></i> Cancel</a>
|
||||
</form>
|
||||
{% endblock %}
|
24
src/economy/templates/revenue_list.html
Normal file
24
src/economy/templates/revenue_list.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block title %}
|
||||
Revenues | {{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
|
||||
{% endblock extra_head %}
|
||||
|
||||
<{% block content %}
|
||||
<h3>Your {{ camp.title }} Revenues</h3>
|
||||
|
||||
{% include 'includes/revenue_list_panel.html' %}
|
||||
|
||||
{% if perms.camps.revenue_create_permission %}
|
||||
<a class="btn btn-success" href="{% url 'economy:revenue_create' camp_slug=camp.slug %}"><i class="fas fa-plus"></i> Create Revenue</a>
|
||||
{% else %}
|
||||
<div class="alert alert-danger"><p class="lead"><span class="text-error">You don't have permission to add revenue. Please ask someone from the Economy team to add the permission if you need it.</p></div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
|
@ -4,30 +4,111 @@ from .views import *
|
|||
app_name = 'economy'
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
'',
|
||||
EconomyDashboardView.as_view(),
|
||||
name='dashboard'
|
||||
),
|
||||
|
||||
# expenses
|
||||
path(
|
||||
'expenses/',
|
||||
ExpenseListView.as_view(),
|
||||
name='expense_list'
|
||||
include([
|
||||
path(
|
||||
'',
|
||||
ExpenseListView.as_view(),
|
||||
name='expense_list'
|
||||
),
|
||||
path(
|
||||
'add/',
|
||||
ExpenseCreateView.as_view(),
|
||||
name='expense_create'
|
||||
),
|
||||
path(
|
||||
'<uuid:pk>/',
|
||||
include([
|
||||
path(
|
||||
'',
|
||||
ExpenseDetailView.as_view(),
|
||||
name='expense_detail'
|
||||
),
|
||||
path(
|
||||
'update/',
|
||||
ExpenseUpdateView.as_view(),
|
||||
name='expense_update'
|
||||
),
|
||||
path(
|
||||
'delete/',
|
||||
ExpenseDeleteView.as_view(),
|
||||
name='expense_delete'
|
||||
),
|
||||
path(
|
||||
'invoice/',
|
||||
ExpenseInvoiceView.as_view(),
|
||||
name='expense_invoice'
|
||||
),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
),
|
||||
|
||||
# reimbursements
|
||||
path(
|
||||
'expenses/add/',
|
||||
ExpenseCreateView.as_view(),
|
||||
name='expense_create'
|
||||
'reimbursements/',
|
||||
include([
|
||||
path(
|
||||
'',
|
||||
ReimbursementListView.as_view(),
|
||||
name='reimbursement_list'
|
||||
),
|
||||
path(
|
||||
'<uuid:pk>/',
|
||||
ReimbursementDetailView.as_view(),
|
||||
name='reimbursement_detail'
|
||||
),
|
||||
]),
|
||||
),
|
||||
|
||||
# revenue
|
||||
path(
|
||||
'expenses/<uuid:pk>/',
|
||||
ExpenseDetailView.as_view(),
|
||||
name='expense_detail'
|
||||
),
|
||||
path(
|
||||
'expenses/<uuid:pk>/invoice/',
|
||||
ExpenseInvoiceView.as_view(),
|
||||
name='expense_invoice'
|
||||
),
|
||||
path(
|
||||
'reimbursements/<uuid:pk>/',
|
||||
ReimbursementDetailView.as_view(),
|
||||
name='reimbursement_detail'
|
||||
'revenues/',
|
||||
include([
|
||||
path(
|
||||
'',
|
||||
RevenueListView.as_view(),
|
||||
name='revenue_list'
|
||||
),
|
||||
path(
|
||||
'add/',
|
||||
RevenueCreateView.as_view(),
|
||||
name='revenue_create'
|
||||
),
|
||||
path(
|
||||
'<uuid:pk>/',
|
||||
include([
|
||||
path(
|
||||
'',
|
||||
RevenueDetailView.as_view(),
|
||||
name='revenue_detail'
|
||||
),
|
||||
path(
|
||||
'update/',
|
||||
RevenueUpdateView.as_view(),
|
||||
name='revenue_update'
|
||||
),
|
||||
path(
|
||||
'delete/',
|
||||
RevenueDeleteView.as_view(),
|
||||
name='revenue_delete'
|
||||
),
|
||||
path(
|
||||
'invoice/',
|
||||
RevenueInvoiceView.as_view(),
|
||||
name='revenue_invoice'
|
||||
),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
),
|
||||
]
|
||||
|
||||
|
|
|
@ -1,23 +1,61 @@
|
|||
import os, magic
|
||||
|
||||
from django.shortcuts import render
|
||||
from django.shortcuts import render, redirect
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpResponseRedirect, HttpResponse, Http404
|
||||
from django.urls import reverse
|
||||
from django.views.generic import CreateView, ListView, DetailView
|
||||
from django.views.generic import CreateView, ListView, DetailView, TemplateView, UpdateView, DeleteView
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.db.models import Sum
|
||||
|
||||
from camps.mixins import CampViewMixin
|
||||
from utils.email import add_outgoing_email
|
||||
from utils.mixins import RaisePermissionRequiredMixin
|
||||
from teams.models import Team
|
||||
from .models import Expense, Reimbursement
|
||||
from .mixins import ExpensePermissionMixin, ReimbursementPermissionMixin
|
||||
from .forms import ExpenseCreateForm
|
||||
from .models import *
|
||||
from .mixins import *
|
||||
from .forms import *
|
||||
|
||||
|
||||
class EconomyDashboardView(LoginRequiredMixin, CampViewMixin, TemplateView):
|
||||
template_name = 'dashboard.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""
|
||||
Add expenses, reimbursements and revenues to the context
|
||||
"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
# get reimbursement stats
|
||||
context['reimbursement_count'] = Reimbursement.objects.filter(reimbursement_user=self.request.user).count()
|
||||
context['unpaid_reimbursement_count'] = Reimbursement.objects.filter(reimbursement_user=self.request.user, paid=False).count()
|
||||
context['paid_reimbursement_count'] = Reimbursement.objects.filter(reimbursement_user=self.request.user, paid=True).count()
|
||||
reimbursement_total = 0
|
||||
for reimbursement in Reimbursement.objects.filter(user=self.request.user):
|
||||
reimbursement_total += reimbursement.amount
|
||||
context['reimbursement_total'] = reimbursement_total
|
||||
|
||||
# get expense stats
|
||||
context['expense_count'] = Expense.objects.filter(user=self.request.user).count()
|
||||
context['unapproved_expense_count'] = Expense.objects.filter(user=self.request.user, approved__isnull=True).count()
|
||||
context['approved_expense_count'] = Expense.objects.filter(user=self.request.user, approved=True).count()
|
||||
context['rejected_expense_count'] = Expense.objects.filter(user=self.request.user, approved=False).count()
|
||||
context['expense_total'] = Expense.objects.filter(user=self.request.user).aggregate(Sum('amount'))['amount__sum']
|
||||
|
||||
# get revenue stats
|
||||
context['revenue_count'] = Revenue.objects.filter(user=self.request.user).count()
|
||||
context['unapproved_revenue_count'] = Revenue.objects.filter(user=self.request.user, approved__isnull=True).count()
|
||||
context['approved_revenue_count'] = Revenue.objects.filter(user=self.request.user, approved=True).count()
|
||||
context['rejected_revenue_count'] = Revenue.objects.filter(user=self.request.user, approved=False).count()
|
||||
context['revenue_total'] = Revenue.objects.filter(user=self.request.user).aggregate(Sum('amount'))['amount__sum']
|
||||
|
||||
return context
|
||||
|
||||
|
||||
########### Expense related views ###############
|
||||
|
||||
class ExpenseListView(LoginRequiredMixin, CampViewMixin, ListView):
|
||||
model = Expense
|
||||
template_name = 'expense_list.html'
|
||||
|
@ -26,14 +64,6 @@ class ExpenseListView(LoginRequiredMixin, CampViewMixin, ListView):
|
|||
# only return Expenses belonging to the current user
|
||||
return super().get_queryset().filter(user=self.request.user)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""
|
||||
Add reimbursements to the context
|
||||
"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['reimbursement_list'] = Reimbursement.objects.filter(reimbursement_user=self.request.user)
|
||||
return context
|
||||
|
||||
|
||||
class ExpenseDetailView(CampViewMixin, ExpensePermissionMixin, DetailView):
|
||||
model = Expense
|
||||
|
@ -55,7 +85,6 @@ class ExpenseCreateView(CampViewMixin, RaisePermissionRequiredMixin, CreateView)
|
|||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
# TODO: make sure this user has permission to create expenses
|
||||
expense = form.save(commit=False)
|
||||
expense.user = self.request.user
|
||||
expense.camp = self.camp
|
||||
|
@ -79,6 +108,50 @@ class ExpenseCreateView(CampViewMixin, RaisePermissionRequiredMixin, CreateView)
|
|||
return HttpResponseRedirect(reverse('economy:expense_list', kwargs={'camp_slug': self.camp.slug}))
|
||||
|
||||
|
||||
class ExpenseUpdateView(CampViewMixin, ExpensePermissionMixin, UpdateView):
|
||||
model = Expense
|
||||
template_name = 'expense_form.html'
|
||||
form_class = ExpenseUpdateForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
if self.get_object().approved:
|
||||
messages.error(self.request, "This expense has already been approved, it cannot be updated")
|
||||
return redirect(reverse('economy:expense_list', kwargs={'camp_slug': self.camp.slug}))
|
||||
return response
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""
|
||||
Do not show teams that are not part of the current camp in the dropdown
|
||||
"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['form'].fields['responsible_team'].queryset = Team.objects.filter(camp=self.camp)
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
messages.success(self.request, "Expense %s has been updated" % self.get_object().pk)
|
||||
return(reverse('economy:expense_list', kwargs={'camp_slug': self.camp.slug}))
|
||||
|
||||
|
||||
class ExpenseDeleteView(CampViewMixin, ExpensePermissionMixin, UpdateView):
|
||||
model = Expense
|
||||
template_name = 'expense_delete.html'
|
||||
fields = []
|
||||
|
||||
def form_valid(self, form):
|
||||
expense = self.get_object()
|
||||
if expense.approved:
|
||||
messages.error(self.request, "This expense has already been approved, it cannot be deleted")
|
||||
else:
|
||||
message = "Expense %s has been deleted" % expense.pk
|
||||
expense.delete()
|
||||
messages.success(self.request, message)
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self):
|
||||
return(reverse('economy:expense_list', kwargs={'camp_slug': self.camp.slug}))
|
||||
|
||||
|
||||
class ExpenseInvoiceView(CampViewMixin, ExpensePermissionMixin, DetailView):
|
||||
"""
|
||||
This view returns the invoice for an Expense with the proper mimetype
|
||||
|
@ -103,7 +176,142 @@ class ExpenseInvoiceView(CampViewMixin, ExpensePermissionMixin, DetailView):
|
|||
return response
|
||||
|
||||
|
||||
########### Reimbursement related views ###############
|
||||
|
||||
|
||||
class ReimbursementListView(LoginRequiredMixin, CampViewMixin, ListView):
|
||||
model = Reimbursement
|
||||
template_name = 'reimbursement_list.html'
|
||||
|
||||
def get_queryset(self):
|
||||
# only return Expenses belonging to the current user
|
||||
return super().get_queryset().filter(reimbursement_user=self.request.user)
|
||||
|
||||
|
||||
class ReimbursementDetailView(CampViewMixin, ReimbursementPermissionMixin, DetailView):
|
||||
model = Reimbursement
|
||||
template_name = 'reimbursement_detail.html'
|
||||
|
||||
|
||||
########### Revenue related views ###############
|
||||
|
||||
|
||||
class RevenueListView(LoginRequiredMixin, CampViewMixin, ListView):
|
||||
model = Revenue
|
||||
template_name = 'revenue_list.html'
|
||||
|
||||
def get_queryset(self):
|
||||
# only return Revenues belonging to the current user
|
||||
return super().get_queryset().filter(user=self.request.user)
|
||||
|
||||
|
||||
class RevenueDetailView(CampViewMixin, RevenuePermissionMixin, DetailView):
|
||||
model = Revenue
|
||||
template_name = 'revenue_detail.html'
|
||||
|
||||
|
||||
class RevenueCreateView(CampViewMixin, RaisePermissionRequiredMixin, CreateView):
|
||||
model = Revenue
|
||||
template_name = 'revenue_form.html'
|
||||
permission_required = ("camps.revenue_create_permission")
|
||||
form_class = RevenueCreateForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""
|
||||
Do not show teams that are not part of the current camp in the dropdown
|
||||
"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['form'].fields['responsible_team'].queryset = Team.objects.filter(camp=self.camp)
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
revenue = form.save(commit=False)
|
||||
revenue.user = self.request.user
|
||||
revenue.camp = self.camp
|
||||
revenue.save()
|
||||
|
||||
# a message for the user
|
||||
messages.success(
|
||||
self.request,
|
||||
"The revenue has been saved. It is now awaiting approval by the economy team.",
|
||||
)
|
||||
|
||||
# send an email to the economy team
|
||||
add_outgoing_email(
|
||||
"emails/revenue_awaiting_approval_email.txt",
|
||||
formatdict=dict(revenue=revenue),
|
||||
subject="New %s revenue for %s Team is awaiting approval" % (revenue.camp.title, revenue.responsible_team.name),
|
||||
to_recipients=[settings.ECONOMYTEAM_EMAIL],
|
||||
)
|
||||
|
||||
# return to the revenue list page
|
||||
return HttpResponseRedirect(reverse('economy:revenue_list', kwargs={'camp_slug': self.camp.slug}))
|
||||
|
||||
|
||||
class RevenueUpdateView(CampViewMixin, RevenuePermissionMixin, UpdateView):
|
||||
model = Revenue
|
||||
template_name = 'revenue_form.html'
|
||||
form_class = RevenueUpdateForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
if self.get_object().approved:
|
||||
messages.error(self.request, "This revenue has already been approved, it cannot be updated")
|
||||
return redirect(reverse('economy:revenue_list', kwargs={'camp_slug': self.camp.slug}))
|
||||
return response
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""
|
||||
Do not show teams that are not part of the current camp in the dropdown
|
||||
"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['form'].fields['responsible_team'].queryset = Team.objects.filter(camp=self.camp)
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
messages.success(self.request, "Revenue %s has been updated" % self.get_object().pk)
|
||||
return(reverse('economy:revenue_list', kwargs={'camp_slug': self.camp.slug}))
|
||||
|
||||
|
||||
class RevenueDeleteView(CampViewMixin, RevenuePermissionMixin, UpdateView):
|
||||
model = Revenue
|
||||
template_name = 'revenue_delete.html'
|
||||
fields = []
|
||||
|
||||
def form_valid(self, form):
|
||||
revenue = self.get_object()
|
||||
if revenue.approved:
|
||||
messages.error(self.request, "This revenue has already been approved, it cannot be deleted")
|
||||
else:
|
||||
message = "Revenue %s has been deleted" % revenue.pk
|
||||
revenue.delete()
|
||||
messages.success(self.request, message)
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self):
|
||||
return(reverse('economy:revenue_list', kwargs={'camp_slug': self.camp.slug}))
|
||||
|
||||
|
||||
class RevenueInvoiceView(CampViewMixin, RevenuePermissionMixin, DetailView):
|
||||
"""
|
||||
This view returns a http response with the invoice for a Revenue object, with the proper mimetype
|
||||
Uses RevenuePermissionMixin to make sure the user is allowed to see the file
|
||||
"""
|
||||
model = Revenue
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
# get revenue
|
||||
revenue = self.get_object()
|
||||
# read invoice file
|
||||
invoicedata = revenue.invoice.read()
|
||||
# find mimetype
|
||||
mimetype = magic.from_buffer(invoicedata, mime=True)
|
||||
# check if we have a PDF, no preview if so, load a pdf icon instead
|
||||
if mimetype=="application/pdf" and 'preview' in request.GET:
|
||||
invoicedata = open(os.path.join(settings.DJANGO_BASE_PATH, "static_src/img/pdf.png"), "rb").read()
|
||||
mimetype = magic.from_buffer(invoicedata, mime=True)
|
||||
# put the response together and return it
|
||||
response = HttpResponse(content_type=mimetype)
|
||||
response.write(invoicedata)
|
||||
return response
|
||||
|
||||
|
|
|
@ -113,10 +113,11 @@
|
|||
{% endblock %}
|
||||
</div>
|
||||
</footer>
|
||||
<!-- Enable all bootstrap tooltips on the page, only works if the useragent has JS enabled -->
|
||||
<!-- Enable all bootstrap tooltips and jquery datatables on the page, only works if the useragent has JS enabled -->
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
$('.table').DataTable();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
<a class="btn {% menubuttonclass 'feedback' %}" href="{% url 'feedback' camp_slug=camp.slug %}">Feedback</a>
|
||||
{% endif %}
|
||||
|
||||
{% if perms.camps.expense_create_permission %}
|
||||
<a class="btn {% menubuttonclass 'economy' %}" href="{% url 'economy:expense_list' camp_slug=camp.slug %}">Expenses</a>
|
||||
{% if perms.camps.expense_create_permission or perms.camps.revenue_create_permission %}
|
||||
<a class="btn {% menubuttonclass 'economy' %}" href="{% url 'economy:dashboard' camp_slug=camp.slug %}">Economy</a>
|
||||
{% endif %}
|
||||
|
||||
{% if perms.camps.backoffice_permission %}
|
||||
|
|
Loading…
Reference in a new issue