From a057bd6464e61827393b8c9acb63a3391493b0fe Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Tue, 20 Nov 2018 17:12:32 +0100 Subject: [PATCH] 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. --- src/backoffice/templates/badge_handout.html | 7 - ...il.html => expense_detail_backoffice.html} | 11 +- .../templates/expense_list_backoffice.html | 28 +++ .../templates/expense_manage_list.html | 91 ------- src/backoffice/templates/index.html | 12 +- .../templates/manage_proposals.html | 6 - .../templates/merchandise_to_order.html | 5 - .../templates/orders_merchandise.html | 6 - src/backoffice/templates/orders_village.html | 6 - src/backoffice/templates/product_handout.html | 7 - .../templates/reimbursement_delete.html | 17 ++ .../reimbursement_detail_backoffice.html | 6 +- .../templates/reimbursement_form.html | 16 ++ .../templates/reimbursement_list.html | 55 ---- .../reimbursement_list_backoffice.html | 15 ++ .../templates/revenue_detail_backoffice.html | 22 ++ .../templates/revenue_list_backoffice.html | 28 +++ src/backoffice/templates/ticket_checkin.html | 7 - .../templates/village_to_order.html | 6 - src/backoffice/urls.py | 56 ++++- src/backoffice/views.py | 96 ++++++- .../migrations/0032_auto_20180917_1754.py | 17 ++ src/camps/models.py | 1 + src/economy/admin.py | 29 ++- src/economy/email.py | 45 +++- src/economy/forms.py | 32 ++- src/economy/migrations/0002_revenue.py | 40 +++ .../migrations/0003_auto_20180917_1933.py | 19 ++ src/economy/mixins.py | 19 +- src/economy/models.py | 128 +++++++++- src/economy/templates/dashboard.html | 45 ++++ ...txt => accountingsystem_expense_email.txt} | 0 .../emails/accountingsystem_revenue_email.txt | 10 + .../emails/revenue_approved_email.txt | 10 + .../revenue_awaiting_approval_email.txt | 10 + .../emails/revenue_rejected_email.txt | 12 + src/economy/templates/expense_delete.html | 16 ++ src/economy/templates/expense_form.html | 9 +- src/economy/templates/expense_list.html | 86 +------ .../includes/expense_detail_panel.html | 89 +++---- .../includes/expense_list_panel.html | 59 +++++ .../includes/reimbursement_detail_panel.html | 10 +- .../includes/reimbursement_list_panel.html | 39 +++ .../includes/revenue_detail_panel.html | 35 +++ .../includes/revenue_list_panel.html | 35 +++ .../templates/reimbursement_detail.html | 4 + src/economy/templates/reimbursement_list.html | 18 ++ src/economy/templates/revenue_delete.html | 16 ++ src/economy/templates/revenue_detail.html | 12 + src/economy/templates/revenue_form.html | 16 ++ src/economy/templates/revenue_list.html | 24 ++ src/economy/urls.py | 117 +++++++-- src/economy/views.py | 236 ++++++++++++++++-- src/templates/base.html | 3 +- src/templates/includes/menuitems.html | 4 +- 55 files changed, 1332 insertions(+), 416 deletions(-) rename src/backoffice/templates/{expense_manage_detail.html => expense_detail_backoffice.html} (58%) create mode 100644 src/backoffice/templates/expense_list_backoffice.html delete mode 100644 src/backoffice/templates/expense_manage_list.html create mode 100644 src/backoffice/templates/reimbursement_delete.html create mode 100644 src/backoffice/templates/reimbursement_form.html delete mode 100644 src/backoffice/templates/reimbursement_list.html create mode 100644 src/backoffice/templates/reimbursement_list_backoffice.html create mode 100644 src/backoffice/templates/revenue_detail_backoffice.html create mode 100644 src/backoffice/templates/revenue_list_backoffice.html create mode 100644 src/camps/migrations/0032_auto_20180917_1754.py create mode 100644 src/economy/migrations/0002_revenue.py create mode 100644 src/economy/migrations/0003_auto_20180917_1933.py create mode 100644 src/economy/templates/dashboard.html rename src/economy/templates/emails/{accountingsystem_email.txt => accountingsystem_expense_email.txt} (100%) create mode 100644 src/economy/templates/emails/accountingsystem_revenue_email.txt create mode 100644 src/economy/templates/emails/revenue_approved_email.txt create mode 100644 src/economy/templates/emails/revenue_awaiting_approval_email.txt create mode 100644 src/economy/templates/emails/revenue_rejected_email.txt create mode 100644 src/economy/templates/expense_delete.html create mode 100644 src/economy/templates/includes/expense_list_panel.html create mode 100644 src/economy/templates/includes/reimbursement_list_panel.html create mode 100644 src/economy/templates/includes/revenue_detail_panel.html create mode 100644 src/economy/templates/includes/revenue_list_panel.html create mode 100644 src/economy/templates/reimbursement_list.html create mode 100644 src/economy/templates/revenue_delete.html create mode 100644 src/economy/templates/revenue_detail.html create mode 100644 src/economy/templates/revenue_form.html create mode 100644 src/economy/templates/revenue_list.html diff --git a/src/backoffice/templates/badge_handout.html b/src/backoffice/templates/badge_handout.html index 27d6be4f..722f9795 100644 --- a/src/backoffice/templates/badge_handout.html +++ b/src/backoffice/templates/badge_handout.html @@ -47,12 +47,5 @@ - - {% endblock content %} - diff --git a/src/backoffice/templates/expense_manage_detail.html b/src/backoffice/templates/expense_detail_backoffice.html similarity index 58% rename from src/backoffice/templates/expense_manage_detail.html rename to src/backoffice/templates/expense_detail_backoffice.html index 7cb8646a..b0206c9b 100644 --- a/src/backoffice/templates/expense_manage_detail.html +++ b/src/backoffice/templates/expense_detail_backoffice.html @@ -6,19 +6,18 @@ {% include 'includes/expense_detail_panel.html' %} -{% if expense.approved != None %} -
This expense has already been approved/rejected.
-
Economy Team notes for this expense:
-
{{ expense.notes }}
-{% else %} +{% if expense.approved == None %}
{% csrf_token %} {% bootstrap_form form %} {% bootstrap_button " Approve Expense" button_type="submit" button_class="btn-success" name="approve" %} {% bootstrap_button " Reject Expense" button_type="submit" button_class="btn-danger" name="reject" %} - Cancel + Cancel
{% endif %} +
+ Back to Expense List + {% endblock content %} diff --git a/src/backoffice/templates/expense_list_backoffice.html b/src/backoffice/templates/expense_list_backoffice.html new file mode 100644 index 00000000..cd90805d --- /dev/null +++ b/src/backoffice/templates/expense_list_backoffice.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} +{% load staticfiles %} + +{% block extra_head %} + + +{% endblock extra_head %} + +{% block content %} +

Manage Expenses for {{ camp.title }}

+ +{% if unapproved_expenses %} +
+ This table shows unapproved expenses for {{ camp.title }}. +
+ +{% include 'includes/expense_list_panel.html' with expense_list=unapproved_expenses %} + +
+{% endif %} + +
+ This table shows all approved expenses for {{ camp.title }}. +
+ +{% include 'includes/expense_list_panel.html' %} + +{% endblock content %} diff --git a/src/backoffice/templates/expense_manage_list.html b/src/backoffice/templates/expense_manage_list.html deleted file mode 100644 index f62adcf3..00000000 --- a/src/backoffice/templates/expense_manage_list.html +++ /dev/null @@ -1,91 +0,0 @@ -{% extends 'base.html' %} -{% load staticfiles %} - -{% block extra_head %} - - -{% endblock extra_head %} - -{% block content %} -

Manage Expenses for {{ camp.title }}

- -{% if unapproved_expenses %} -
- This table shows unapproved expenses for {{ camp.title }}. -
- - - - - - - - - - - - - - - - {% for expense in unapproved_expenses %} - - - - - - - - - - {% endfor %} - -
UserDescriptionAmountPaid byApproved?Reimbursement?Actions
{{ expense.user }}{{ expense.description }}{{ expense.amount }} DKK{% if expense.paid_by_bornhack %}BornHack{% else %}{{ expense.user }}{% endif %}{{ expense.approval_status }}{% if expense.reimbursement %}Details{% else %}N/A{% endif %} - Details -
-{% endif %} - -
- -
- This table shows all expenses for {{ camp.title }}. -
- -
- - - - - - - - - - - - - - - {% for expense in expense_list %} - - - - - - - - - - {% endfor %} - -
UserDescriptionAmountPaid byApproved?Reimbursement?Actions
{{ expense.user }}{{ expense.description }}{{ expense.amount }} DKK{% if expense.paid_by_bornhack %}BornHack{% else %}{{ expense.user }}{% endif %}{{ expense.approval_status }}{% if expense.reimbursement %}Details{% else %}N/A{% endif %} - Details -
- - - -{% endblock content %} diff --git a/src/backoffice/templates/index.html b/src/backoffice/templates/index.html index 8db37aa3..792c4bc1 100644 --- a/src/backoffice/templates/index.html +++ b/src/backoffice/templates/index.html @@ -64,13 +64,17 @@ {% if perms.camps.economyteam_permission %}

Economy Team

+ +

Expenses

+

Use this view to see and approve/reject expenses.

+

Reimbursements

-

Use this view to view and create reimbursements

+

Use this view to view and create reimbursements for approved expenses.

- -

Expenses

-

Use this view to see and approve/reject expenses

+
+

Revenues

+

Use this view to see and approve/reject revenues.

{% endif %} diff --git a/src/backoffice/templates/manage_proposals.html b/src/backoffice/templates/manage_proposals.html index 5a6d47de..40a9eaf0 100644 --- a/src/backoffice/templates/manage_proposals.html +++ b/src/backoffice/templates/manage_proposals.html @@ -79,11 +79,5 @@ {% endif %} - {% endblock content %} - diff --git a/src/backoffice/templates/merchandise_to_order.html b/src/backoffice/templates/merchandise_to_order.html index bbf6a6b2..edcb9aed 100644 --- a/src/backoffice/templates/merchandise_to_order.html +++ b/src/backoffice/templates/merchandise_to_order.html @@ -36,9 +36,4 @@ - {% endblock content %} diff --git a/src/backoffice/templates/orders_merchandise.html b/src/backoffice/templates/orders_merchandise.html index dcaf94be..7c4566fb 100644 --- a/src/backoffice/templates/orders_merchandise.html +++ b/src/backoffice/templates/orders_merchandise.html @@ -42,10 +42,4 @@ - - {% endblock content %} diff --git a/src/backoffice/templates/orders_village.html b/src/backoffice/templates/orders_village.html index 93ccbd3c..358c7659 100644 --- a/src/backoffice/templates/orders_village.html +++ b/src/backoffice/templates/orders_village.html @@ -42,10 +42,4 @@ - - {% endblock content %} diff --git a/src/backoffice/templates/product_handout.html b/src/backoffice/templates/product_handout.html index 8496a8a5..677ff974 100644 --- a/src/backoffice/templates/product_handout.html +++ b/src/backoffice/templates/product_handout.html @@ -43,12 +43,5 @@ - - {% endblock content %} - diff --git a/src/backoffice/templates/reimbursement_delete.html b/src/backoffice/templates/reimbursement_delete.html new file mode 100644 index 00000000..19dcbbcb --- /dev/null +++ b/src/backoffice/templates/reimbursement_delete.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} +{% load bootstrap3 %} + +{% block content %} +

Delete Reimbursement for User {{ reimbursement_user }}

+ +

The total amount for this reimbursement is {{ reimbursement.amount }} DKK

+ +

Really delete this reimbursement?

+ +
+ {% csrf_token %} + {% bootstrap_button " Yes, Delete it" button_type="submit" button_class="btn-danger" name="Delete" %} + Cancel +
+{% endblock content %} + diff --git a/src/backoffice/templates/reimbursement_detail_backoffice.html b/src/backoffice/templates/reimbursement_detail_backoffice.html index d89a4a35..6a649f6c 100644 --- a/src/backoffice/templates/reimbursement_detail_backoffice.html +++ b/src/backoffice/templates/reimbursement_detail_backoffice.html @@ -2,11 +2,13 @@ {% load bootstrap3 %} {% block content %} -

Manage Reimbursement

+

Reimbursement Details

{% include 'includes/reimbursement_detail_panel.html' %} -Back to reimbursement list + Update + Delete + Back to reimbursement list {% endblock content %} diff --git a/src/backoffice/templates/reimbursement_form.html b/src/backoffice/templates/reimbursement_form.html new file mode 100644 index 00000000..43764be4 --- /dev/null +++ b/src/backoffice/templates/reimbursement_form.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% load bootstrap3 %} + +{% block content %} +

Update Reimbursement for User {{ reimbursement_user }}

+ +

The total amount for this reimbursement is {{ reimbursement.amount }} DKK

+ +
+ {% csrf_token %} + {% bootstrap_form form %} + {% bootstrap_button " Update" button_type="submit" button_class="btn-success" name="Update" %} + Cancel +
+{% endblock content %} + diff --git a/src/backoffice/templates/reimbursement_list.html b/src/backoffice/templates/reimbursement_list.html deleted file mode 100644 index 9c4a52a7..00000000 --- a/src/backoffice/templates/reimbursement_list.html +++ /dev/null @@ -1,55 +0,0 @@ -{% extends 'base.html' %} -{% load staticfiles %} - -{% block extra_head %} - - -{% endblock extra_head %} - -{% block content %} -
-

Reimbursements for {{ camp.title }}

-
-
-
- - - - - - - - - - - - - - - {% for reim in reimbursement_list %} - - - - - - - - - - - {% endfor %} - -
CampCreatedReimbursement UserEconomy Team NotesAmountPaidExpensesActions
{{ reim.camp }}{{ reim.created }} by {{ reim.user }}{{ reim.reimbursement_user }}{{ reim.notes|default:"N/A" }}{{ reim.amount }} DKK{{ reim.paid }}{% for expense in reim.expenses.all %}{% if not expense.paid_by_bornhack %}{{ expense.pk }} - {{ expense.amount }} DKK - {{ expense.description }}
{% endif %}{% endfor %}
- Details -
-
- -Create New Reimbursement - - - -{% endblock content %} diff --git a/src/backoffice/templates/reimbursement_list_backoffice.html b/src/backoffice/templates/reimbursement_list_backoffice.html new file mode 100644 index 00000000..f0519888 --- /dev/null +++ b/src/backoffice/templates/reimbursement_list_backoffice.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% load staticfiles %} + +{% block extra_head %} + + +{% endblock extra_head %} + +{% block content %} +

Reimbursements for {{ camp.title }}

+ +{% include 'includes/reimbursement_list_panel.html' %} + + Create New Reimbursement +{% endblock content %} diff --git a/src/backoffice/templates/revenue_detail_backoffice.html b/src/backoffice/templates/revenue_detail_backoffice.html new file mode 100644 index 00000000..32a0413d --- /dev/null +++ b/src/backoffice/templates/revenue_detail_backoffice.html @@ -0,0 +1,22 @@ +{% extends 'base.html' %} +{% load bootstrap3 %} + +{% block content %} +

Manage Revenue

+ +{% include 'includes/revenue_detail_panel.html' %} + +{% if revenue.approved == None %} +
+ {% csrf_token %} + {% bootstrap_form form %} + {% bootstrap_button " Approve Revenue" button_type="submit" button_class="btn-success" name="approve" %} + {% bootstrap_button " Reject Revenue" button_type="submit" button_class="btn-danger" name="reject" %} + Cancel +
+{% endif %} +
+ Back to Revenue List + +{% endblock content %} + diff --git a/src/backoffice/templates/revenue_list_backoffice.html b/src/backoffice/templates/revenue_list_backoffice.html new file mode 100644 index 00000000..cd18fbbe --- /dev/null +++ b/src/backoffice/templates/revenue_list_backoffice.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} +{% load staticfiles %} + +{% block extra_head %} + + +{% endblock extra_head %} + +{% block content %} +

Manage Revenues for {{ camp.title }}

+ +{% if unapproved_revenues %} +
+ This table shows unapproved revenues for {{ camp.title }}. +
+ +{% include 'includes/revenue_list_panel.html' with revenue_list=unapproved_revenues %} + +
+{% endif %} + +
+ This table shows all approved revenues for {{ camp.title }}. +
+ +{% include 'includes/revenue_list_panel.html' %} + +{% endblock content %} diff --git a/src/backoffice/templates/ticket_checkin.html b/src/backoffice/templates/ticket_checkin.html index 177f7c1b..b35181ef 100644 --- a/src/backoffice/templates/ticket_checkin.html +++ b/src/backoffice/templates/ticket_checkin.html @@ -47,12 +47,5 @@ - - {% endblock content %} - diff --git a/src/backoffice/templates/village_to_order.html b/src/backoffice/templates/village_to_order.html index ae3501c0..9f8099e5 100644 --- a/src/backoffice/templates/village_to_order.html +++ b/src/backoffice/templates/village_to_order.html @@ -35,10 +35,4 @@ - - {% endblock content %} diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py index ef24d43f..ae7ea6e3 100644 --- a/src/backoffice/urls.py +++ b/src/backoffice/urls.py @@ -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//', SpeakerProposalManageView.as_view(), name='speakerproposal_manage'), path('events//', 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//', ExpenseManageDetailView.as_view(), name='expense_detail'), - path('economy/reimbursements/', ReimbursementListView.as_view(), name='reimbursement_list'), - path('economy/reimbursements//', ReimbursementDetailView.as_view(), name='reimbursement_detail'), - path('economy/reimbursements/create/', ReimbursementCreateUserSelectView.as_view(), name='reimbursement_create_userselect'), - path('economy/reimbursements/create//', ReimbursementCreateView.as_view(), name='reimbursement_create'), + + # economy + path('economy/', + include([ + # expenses + path('expenses/', + include([ + path('', ExpenseListView.as_view(), name='expense_list'), + path('/', ExpenseDetailView.as_view(), name='expense_detail'), + ]), + ), + + # revenues + path('revenues/', + include([ + path('', RevenueListView.as_view(), name='revenue_list'), + path('/', RevenueDetailView.as_view(), name='revenue_detail'), + ]), + ), + + # reimbursements + path('reimbursements/', + include([ + path('', ReimbursementListView.as_view(), name='reimbursement_list'), + path('/', + 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//', ReimbursementCreateView.as_view(), name='reimbursement_create'), + ]), + ), + ]), + ), ] diff --git a/src/backoffice/views.py b/src/backoffice/views.py index e69fa2e9..47e466d2 100644 --- a/src/backoffice/views.py +++ b/src/backoffice/views.py @@ -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})) + diff --git a/src/camps/migrations/0032_auto_20180917_1754.py b/src/camps/migrations/0032_auto_20180917_1754.py new file mode 100644 index 00000000..b4fe20a7 --- /dev/null +++ b/src/camps/migrations/0032_auto_20180917_1754.py @@ -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'))}, + ), + ] diff --git a/src/camps/models.py b/src/camps/models.py index e6ede33d..cb4b1c18 100644 --- a/src/camps/models.py +++ b/src/camps/models.py @@ -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"), ) diff --git a/src/economy/admin.py b/src/economy/admin.py index d1615be5..43388faa 100644 --- a/src/economy/admin.py +++ b/src/economy/admin.py @@ -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'] diff --git a/src/economy/email.py b/src/economy/email.py index 7fabd8bf..c3620bac 100644 --- a/src/economy/email.py +++ b/src/economy/email.py @@ -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], + ) + diff --git a/src/economy/forms.py b/src/economy/forms.py index 7ef2dcd9..3dad210b 100644 --- a/src/economy/forms.py +++ b/src/economy/forms.py @@ -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'] + diff --git a/src/economy/migrations/0002_revenue.py b/src/economy/migrations/0002_revenue.py new file mode 100644 index 00000000..216f28eb --- /dev/null +++ b/src/economy/migrations/0002_revenue.py @@ -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, + }, + ), + ] diff --git a/src/economy/migrations/0003_auto_20180917_1933.py b/src/economy/migrations/0003_auto_20180917_1933.py new file mode 100644 index 00000000..f5b9036c --- /dev/null +++ b/src/economy/migrations/0003_auto_20180917_1933.py @@ -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'), + ), + ] diff --git a/src/economy/mixins.py b/src/economy/mixins.py index c4db4766..40a683f9 100644 --- a/src/economy/mixins.py +++ b/src/economy/mixins.py @@ -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() diff --git a/src/economy/models.py b/src/economy/models.py index d94d816e..531d0033 100644 --- a/src/economy/models.py +++ b/src/economy/models.py @@ -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 + diff --git a/src/economy/templates/dashboard.html b/src/economy/templates/dashboard.html new file mode 100644 index 00000000..98f1f16e --- /dev/null +++ b/src/economy/templates/dashboard.html @@ -0,0 +1,45 @@ +{% extends 'base.html' %} +{% load staticfiles %} + +{% block title %} +Economy | {{ block.super }} +{% endblock %} + +<{% block content %} +

Your {{ camp.title }} Economy Overview

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
WhatDescriptionActions

Expenses

You have {{ expense_count }} expense{{ expense_count|pluralize }} ({{ approved_expense_count }} approved, {{ rejected_expense_count }} rejected, and {{ unapproved_expense_count }} pending approval) for {{ camp.title }}, for a total of {{ expense_total|default:"0" }} DKK. + List Expenses + Create Expense +

Reimbursements

You have {{ reimbursement_count }} reimbursement{{ reimbursement_count|pluralize }} ({{ paid_reimbursement_count }} paid, {{ unpaid_reimbursement_count }} pending payment) for {{ camp.title }}, for a total of {{ reimbursement_total }} DKK. + List Reimbursements +

Revenue

You have {{ revenue_count }} revenue{{ revenue_count|pluralize }} ({{ approved_revenue_count }} approved, {{ rejected_revenue_count }} rejected, and {{ unapproved_revenue_count }} still pending approval) for {{ camp.title }}, for a total of {{ revenue_total|default:"0" }} DKK. + List Revenues + Create Revenue +
+{% endblock %} diff --git a/src/economy/templates/emails/accountingsystem_email.txt b/src/economy/templates/emails/accountingsystem_expense_email.txt similarity index 100% rename from src/economy/templates/emails/accountingsystem_email.txt rename to src/economy/templates/emails/accountingsystem_expense_email.txt diff --git a/src/economy/templates/emails/accountingsystem_revenue_email.txt b/src/economy/templates/emails/accountingsystem_revenue_email.txt new file mode 100644 index 00000000..f666ec71 --- /dev/null +++ b/src/economy/templates/emails/accountingsystem_revenue_email.txt @@ -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 + diff --git a/src/economy/templates/emails/revenue_approved_email.txt b/src/economy/templates/emails/revenue_approved_email.txt new file mode 100644 index 00000000..6aee7c5c --- /dev/null +++ b/src/economy/templates/emails/revenue_approved_email.txt @@ -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 + diff --git a/src/economy/templates/emails/revenue_awaiting_approval_email.txt b/src/economy/templates/emails/revenue_awaiting_approval_email.txt new file mode 100644 index 00000000..9b78bb01 --- /dev/null +++ b/src/economy/templates/emails/revenue_awaiting_approval_email.txt @@ -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 + diff --git a/src/economy/templates/emails/revenue_rejected_email.txt b/src/economy/templates/emails/revenue_rejected_email.txt new file mode 100644 index 00000000..65c567b8 --- /dev/null +++ b/src/economy/templates/emails/revenue_rejected_email.txt @@ -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 + diff --git a/src/economy/templates/expense_delete.html b/src/economy/templates/expense_delete.html new file mode 100644 index 00000000..df1e30e3 --- /dev/null +++ b/src/economy/templates/expense_delete.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% load bootstrap3 %} + +{% block title %} +Delete Expense | {{ block.super }} +{% endblock %} + +{% block content %} +

Really delete expense {{ expense.uuid }}?

+{% include 'includes/expense_detail_panel.html' %} +
+ {% csrf_token %} + + Cancel +
+{% endblock %} diff --git a/src/economy/templates/expense_form.html b/src/economy/templates/expense_form.html index ad29c3af..420bd999 100644 --- a/src/economy/templates/expense_form.html +++ b/src/economy/templates/expense_form.html @@ -1,11 +1,16 @@ {% extends 'base.html' %} {% load bootstrap3 %} +{% block title %} +{% if object %}Update{% else %}Create{% endif %} Expense | {{ block.super }} +{% endblock %} + {% block content %} -

Create {{ camp.title }} Expense

+

{% if object %}Update{% else %}Create{% endif %} {{ camp.title }} Expense

{% csrf_token %} {% bootstrap_form form %} - + + Cancel
{% endblock %} diff --git a/src/economy/templates/expense_list.html b/src/economy/templates/expense_list.html index d1849e9d..15d62c80 100644 --- a/src/economy/templates/expense_list.html +++ b/src/economy/templates/expense_list.html @@ -13,38 +13,7 @@ Expenses | {{ block.super }} <{% block content %}

Your {{ camp.title }} Expenses

-{% if expense_list %} - - - - - - - - - - - - - - {% for expense in expense_list %} - - - - - - - - - - {% endfor %} - -
Paid byAmountDescriptionResponsible TeamApprovedReimbursement?Actions
{% if expense.paid_by_bornhack %}BornHack{% else %}You ({{ expense.user }}){% endif %}{{ expense.amount }} DKK{{ expense.description }}{{ expense.responsible_team.name }} Team{{ expense.approval_status }}{% if expense.reimbursement %}Details{% else %}N/A{% endif %} - Details -
-{% else %} -

No expenses found for BornHack 2018, you haven't submitted any yet!

-{% endif %} +{% include 'includes/expense_list_panel.html' %} {% if perms.camps.expense_create_permission %} Create Expense @@ -52,57 +21,4 @@ Expenses | {{ block.super }}

You don't have permission to add expenses. Please ask someone from the Economy team to add the permission if you need it.

{% endif %} -
- -

Your {{ camp.title }} Reimbursements

- -{% if reimbursement_list %} - - - - - - - - - - - - - - {% for reim in reimbursement_list %} - - - - - - - - - - {% endfor %} - -
CampUserEconomy Team NotesAmountPaidExpensesActions
{{ reim.camp }}{{ reim.user }}{{ reim.notes|default:"N/A" }}{{ reim.amount }} DKK{{ reim.paid }} - {% for expense in reim.expenses.all %} - {% if not expense.paid_by_bornhack %} - {% if request.resolver_match.app_name == "backoffice" %} - Details {{ expense.amount }} DKK - {{ expense.description }}
- {% else %} - Details {{ expense.amount }} DKK - {{ expense.description }}
- {% endif %} - {% endif %} - {% endfor %} -
- Details -
-{% else %} -

No reimbursements found for BornHack 2018

-{% endif %} - - - {% endblock %} diff --git a/src/economy/templates/includes/expense_detail_panel.html b/src/economy/templates/includes/expense_detail_panel.html index eba3bd7a..89d17bee 100644 --- a/src/economy/templates/includes/expense_detail_panel.html +++ b/src/economy/templates/includes/expense_detail_panel.html @@ -1,49 +1,54 @@
Expense Details for {{ expense.pk }}
- - - - - - - - - - - - - - - - - - - - - - {% if not expense.paid_by_bornhack %} - - - {% if request.resolver_match.app_name == "backoffice" %} - - {% else %} - - {% endif %} - +
Amount{{ expense.amount }} DKK
Description{{ expense.description }}
Paid by BornHack?This expense was paid by {% if expense.paid_by_bornhack %}BornHack{% else %}{{ expense.user }}, and will be reimbursed when approved.{% endif %}
Filename{{ expense.invoice }}
Approved?{{ expense.approval_status }}
Reimbursement?{% if expense.reimbursement %}{{ expense.reimbursement.pk }}{% else %}N/A{% endif %}{% if expense.reimbursement %}{{ expense.reimbursement.pk }}{% else %}N/A{% endif %}
+ + + + + + + + + + + + + + + + + + + + + {% if not expense.paid_by_bornhack %} + + + - - - - - - - -
Amount{{ expense.amount }} DKK
Description{{ expense.description }}
Paid by BornHack?This expense was paid by {% if expense.paid_by_bornhack %}BornHack{% else %}{{ expense.user }}, and will be reimbursed when approved.{% endif %}
Filename{{ expense.invoice }}
Approved?{{ expense.approval_status }}
Reimbursement? + {% if expense.reimbursement %} + {% if request.resolver_match.app_name == "backoffice" %} + {{ expense.reimbursement.pk }} + {% else %} + {{ expense.reimbursement.pk }} {% endif %} -
Invoice -
- Filename: {{ expense.invoice_filename }} -
Economy Team Notes{{ expense.notes|default:"N/A" }}
+ {% else %} + N/A + {% endif %} + + {% endif %} + + Invoice + +
+ Filename: {{ expense.invoice_filename }} + + + + Economy Team Notes + {{ expense.notes|default:"N/A" }} + +
diff --git a/src/economy/templates/includes/expense_list_panel.html b/src/economy/templates/includes/expense_list_panel.html new file mode 100644 index 00000000..efbdeeac --- /dev/null +++ b/src/economy/templates/includes/expense_list_panel.html @@ -0,0 +1,59 @@ +{% if expense_list %} + + + + {% if not reimbursement %} + + {% endif %} + + + + {% if not reimbursement %} + + + {% endif %} + + + + + {% for expense in expense_list %} + + {% if not reimbursement %} + + {% endif %} + + + + + {% if not reimbursement %} + + + {% endif %} + + + + {% endfor %} + +
Paid byAmountDescriptionResponsible TeamApprovedReimbursementActions
{% if expense.paid_by_bornhack %}BornHack{% else %}{{ expense.user }}{% endif %}{{ expense.amount }} DKK{{ expense.description }}{{ expense.responsible_team.name }} Team{{ expense.approval_status }} + {% if expense.reimbursement and not expense.paid_by_bornhack %} + {% if request.resolver_match.app_name == "backoffice" %} + Details + {% else %} + Details + {% endif %} + {% else %} + N/A + {% endif %} + + {% if request.resolver_match.app_name == "backoffice" %} + Details + {% else %} + Details + Update + Delete + {% endif %} +
+{% else %} +

No expenses found.

+{% endif %} + diff --git a/src/economy/templates/includes/reimbursement_detail_panel.html b/src/economy/templates/includes/reimbursement_detail_panel.html index 8d4d0cb1..e3cc158a 100644 --- a/src/economy/templates/includes/reimbursement_detail_panel.html +++ b/src/economy/templates/includes/reimbursement_detail_panel.html @@ -25,15 +25,7 @@ Expenses covered by this Reimbursement - {% for expense in reimbursement.expenses.all %} - {% if not expense.paid_by_bornhack %} - {% if request.resolver_match.app_name == "backoffice" %} - {{ expense.pk }} {{ expense.amount }} DKK - {{ expense.description }}
- {% else %} - {{ expense.pk }} {{ expense.amount }} DKK - {{ expense.description }}
- {% endif %} - {% endif %} - {% endfor %} + {% include 'includes/expense_list_panel.html' with expense_list=reimbursement.covered_expenses.all %} diff --git a/src/economy/templates/includes/reimbursement_list_panel.html b/src/economy/templates/includes/reimbursement_list_panel.html new file mode 100644 index 00000000..0d94c417 --- /dev/null +++ b/src/economy/templates/includes/reimbursement_list_panel.html @@ -0,0 +1,39 @@ +{% if reimbursement_list %} + + + + + + + + + + + + + {% for reim in reimbursement_list %} + + + + + + + + + {% endfor %} + +
CampUserEconomy Team NotesAmountPaidActions
{{ reim.camp }}{{ reim.user }}{{ reim.notes|default:"N/A" }}{{ reim.amount }} DKK{{ reim.paid }} + {% if request.resolver_match.app_name == "backoffice" %} + Details + Update + {% if not reim.paid %} + Delete + {% endif %} + {% else %} + Details + {% endif %} +
+{% else %} +

No reimbursements found for {{ camp.title }}

+{% endif %} + diff --git a/src/economy/templates/includes/revenue_detail_panel.html b/src/economy/templates/includes/revenue_detail_panel.html new file mode 100644 index 00000000..80e97413 --- /dev/null +++ b/src/economy/templates/includes/revenue_detail_panel.html @@ -0,0 +1,35 @@ +
+
Revenue Details for {{ revenue.pk }}
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Amount{{ revenue.amount }} DKK
Description{{ revenue.description }}
Filename{{ revenue.invoice }}
Approved?{{ revenue.approval_status }}
Invoice +
+ Filename: {{ revenue.invoice_filename }} +
Economy Team Notes{{ revenue.notes|default:"N/A" }}
+
+
+ diff --git a/src/economy/templates/includes/revenue_list_panel.html b/src/economy/templates/includes/revenue_list_panel.html new file mode 100644 index 00000000..4c2ac5ed --- /dev/null +++ b/src/economy/templates/includes/revenue_list_panel.html @@ -0,0 +1,35 @@ +{% if revenue_list %} + + + + + + + + + + + + {% for revenue in revenue_list %} + + + + + + + + {% endfor %} + +
AmaountDescriptionResponsible TeamApprovedActions
{{ revenue.amount }} DKK{{ revenue.description }}{{ revenue.responsible_team.name }} Team{{ revenue.approval_status }} + {% if request.resolver_match.app_name == "backoffice" %} + Details + {% else %} + Details + Update + Delete + {% endif %} +
+{% else %} +

No revenues found.

+{% endif %} + diff --git a/src/economy/templates/reimbursement_detail.html b/src/economy/templates/reimbursement_detail.html index ab8a43af..d1d7b03e 100644 --- a/src/economy/templates/reimbursement_detail.html +++ b/src/economy/templates/reimbursement_detail.html @@ -1,6 +1,10 @@ {% extends 'base.html' %} {% load bootstrap3 %} +{% block title %} +Reimbursement Details | {{ block.super }} +{% endblock %} + {% block content %}

Reimbursement Details

diff --git a/src/economy/templates/reimbursement_list.html b/src/economy/templates/reimbursement_list.html new file mode 100644 index 00000000..823a824e --- /dev/null +++ b/src/economy/templates/reimbursement_list.html @@ -0,0 +1,18 @@ +{% extends 'base.html' %} +{% load staticfiles %} + +{% block title %} +Expenses | {{ block.super }} +{% endblock %} + +{% block extra_head %} + + +{% endblock extra_head %} + +<{% block content %} +

Your {{ camp.title }} Reimbursements

+ +{% include 'includes/reimbursement_list_panel.html' %} + +{% endblock %} diff --git a/src/economy/templates/revenue_delete.html b/src/economy/templates/revenue_delete.html new file mode 100644 index 00000000..234711aa --- /dev/null +++ b/src/economy/templates/revenue_delete.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% load bootstrap3 %} + +{% block title %} +Delete Revenue | {{ block.super }} +{% endblock %} + +{% block content %} +

Really delete revenue {{ revenue.uuid }}?

+{% include 'includes/revenue_detail_panel.html' %} +
+ {% csrf_token %} + + Cancel +
+{% endblock %} diff --git a/src/economy/templates/revenue_detail.html b/src/economy/templates/revenue_detail.html new file mode 100644 index 00000000..fc8dd0db --- /dev/null +++ b/src/economy/templates/revenue_detail.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block title %} +Revenue Details | {{ block.super }} +{% endblock %} + +{% block content %} +
+{% include 'includes/revenue_detail_panel.html' %} +
+Back to Revenue List +{% endblock %} diff --git a/src/economy/templates/revenue_form.html b/src/economy/templates/revenue_form.html new file mode 100644 index 00000000..cf8245d0 --- /dev/null +++ b/src/economy/templates/revenue_form.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% load bootstrap3 %} + +{% block title %} +{% if object %}Update{% else %}Create{% endif %} Revenue | {{ block.super }} +{% endblock %} + +{% block content %} +

{% if object %}Update{% else %}Create{% endif %} {{ camp.title }} Revenue

+
+ {% csrf_token %} + {% bootstrap_form form %} + + Cancel +
+{% endblock %} diff --git a/src/economy/templates/revenue_list.html b/src/economy/templates/revenue_list.html new file mode 100644 index 00000000..bfc6737d --- /dev/null +++ b/src/economy/templates/revenue_list.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} +{% load staticfiles %} + +{% block title %} +Revenues | {{ block.super }} +{% endblock %} + +{% block extra_head %} + + +{% endblock extra_head %} + +<{% block content %} +

Your {{ camp.title }} Revenues

+ +{% include 'includes/revenue_list_panel.html' %} + +{% if perms.camps.revenue_create_permission %} + Create Revenue +{% else %} +

You don't have permission to add revenue. Please ask someone from the Economy team to add the permission if you need it.

+{% endif %} + +{% endblock %} diff --git a/src/economy/urls.py b/src/economy/urls.py index 0f47bc75..5ee3e1d5 100644 --- a/src/economy/urls.py +++ b/src/economy/urls.py @@ -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( + '/', + 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( + '/', + ReimbursementDetailView.as_view(), + name='reimbursement_detail' + ), + ]), ), + + # revenue path( - 'expenses//', - ExpenseDetailView.as_view(), - name='expense_detail' - ), - path( - 'expenses//invoice/', - ExpenseInvoiceView.as_view(), - name='expense_invoice' - ), - path( - 'reimbursements//', - ReimbursementDetailView.as_view(), - name='reimbursement_detail' + 'revenues/', + include([ + path( + '', + RevenueListView.as_view(), + name='revenue_list' + ), + path( + 'add/', + RevenueCreateView.as_view(), + name='revenue_create' + ), + path( + '/', + 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' + ), + ]), + ), + ]), ), ] diff --git a/src/economy/views.py b/src/economy/views.py index 5dff9009..690ee731 100644 --- a/src/economy/views.py +++ b/src/economy/views.py @@ -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 + diff --git a/src/templates/base.html b/src/templates/base.html index aab66320..eb2004a6 100644 --- a/src/templates/base.html +++ b/src/templates/base.html @@ -113,10 +113,11 @@ {% endblock %} - + diff --git a/src/templates/includes/menuitems.html b/src/templates/includes/menuitems.html index b3567914..38143989 100644 --- a/src/templates/includes/menuitems.html +++ b/src/templates/includes/menuitems.html @@ -11,8 +11,8 @@ Feedback {% endif %} - {% if perms.camps.expense_create_permission %} - Expenses + {% if perms.camps.expense_create_permission or perms.camps.revenue_create_permission %} + Economy {% endif %} {% if perms.camps.backoffice_permission %}