From f248a5e0ca80d9a5cae7d326f271bc27200e0573 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Thu, 28 Mar 2019 07:04:53 +0100 Subject: [PATCH] Add Chain/Creditor/Debtor support in economy app. Make the Creditor/Debtor FK nullable for now, until we've backfilled Creditors/Debtors on all existing Expenses and Revenues. --- src/camps/mixins.py | 7 +- src/economy/admin.py | 23 +++- src/economy/forms.py | 4 +- .../migrations/0008_auto_20190327_1721.py | 69 +++++++++++ src/economy/mixins.py | 29 +++++ src/economy/models.py | 114 ++++++++++++++++-- src/economy/templates/chain_create.html | 16 +++ src/economy/templates/chain_list.html | 41 +++++++ src/economy/templates/credebtor_create.html | 16 +++ src/economy/templates/credebtor_list.html | 53 ++++++++ src/economy/templates/dashboard.html | 5 +- src/economy/templates/expense_detail.html | 3 +- src/economy/templates/expense_form.html | 24 +++- src/economy/templates/expense_list.html | 2 +- .../includes/expense_detail_panel.html | 8 ++ .../includes/expense_list_panel.html | 6 +- .../includes/revenue_detail_panel.html | 8 ++ .../includes/revenue_list_panel.html | 2 + src/economy/templates/revenue_form.html | 18 ++- src/economy/templates/revenue_list.html | 2 +- src/economy/urls.py | 58 +++++++-- src/economy/views.py | 85 ++++++++++++- 22 files changed, 556 insertions(+), 37 deletions(-) create mode 100644 src/economy/migrations/0008_auto_20190327_1721.py create mode 100644 src/economy/templates/chain_create.html create mode 100644 src/economy/templates/chain_list.html create mode 100644 src/economy/templates/credebtor_create.html create mode 100644 src/economy/templates/credebtor_list.html diff --git a/src/camps/mixins.py b/src/camps/mixins.py index 35a0fd53..e02c3300 100644 --- a/src/camps/mixins.py +++ b/src/camps/mixins.py @@ -6,8 +6,7 @@ from django.utils.functional import cached_property class CampViewMixin(object): """ This mixin makes sure self.camp is available (taken from url kwarg camp_slug) - It also filters out objects that belong to other camps when the queryset has - a direct relation to the Camp model. + It also filters out objects that belong to other camps when the queryset has a camp_filter """ def dispatch(self, request, *args, **kwargs): @@ -21,6 +20,10 @@ class CampViewMixin(object): if not queryset: return queryset + # do we have a camp_filter on this model + if not hasattr(self.model, 'camp_filter'): + return queryset + # get the camp_filter from the model camp_filter = self.model.get_camp_filter() diff --git a/src/economy/admin.py b/src/economy/admin.py index 26081059..d7ef9051 100644 --- a/src/economy/admin.py +++ b/src/economy/admin.py @@ -1,5 +1,22 @@ from django.contrib import admin -from .models import Expense, Reimbursement, Revenue + +from .models import Chain, Credebtor, Expense, Reimbursement, Revenue + + +### chains and credebtors + +@admin.register(Chain) +class ChainAdmin(admin.ModelAdmin): + list_filter = ['name'] + list_display = ['name', 'notes'] + search_fields = ['name', 'notes'] + + +@admin.register(Credebtor) +class ChainAdmin(admin.ModelAdmin): + list_filter = ['chain', 'name'] + list_display = ['chain', 'name', 'notes'] + search_fields = ['chain', 'name', 'notes'] ### expenses @@ -18,8 +35,8 @@ reject_expenses.short_description = "Reject Expenses" @admin.register(Expense) class ExpenseAdmin(admin.ModelAdmin): - list_filter = ['camp', 'responsible_team', 'approved', 'user'] - list_display = ['user', 'description', 'invoice_date', 'amount', 'camp', 'responsible_team', 'approved', 'reimbursement'] + list_filter = ['camp', 'creditor__chain', 'creditor', 'responsible_team', 'approved', 'user'] + list_display = ['user', 'description', 'invoice_date', 'amount', 'camp', 'creditor', 'responsible_team', 'approved', 'reimbursement'] search_fields = ['description', 'amount', 'uuid'] actions = [approve_expenses, reject_expenses] diff --git a/src/economy/forms.py b/src/economy/forms.py index 28bc3b2d..ef5c33e1 100644 --- a/src/economy/forms.py +++ b/src/economy/forms.py @@ -36,7 +36,7 @@ class CleanInvoiceForm(forms.ModelForm): class ExpenseCreateForm(CleanInvoiceForm): class Meta: model = Expense - fields = ['description', 'amount', 'invoice', 'invoice_date', 'paid_by_bornhack', 'responsible_team'] + fields = ['description', 'amount', 'invoice_date', 'invoice', 'paid_by_bornhack', 'responsible_team'] class ExpenseUpdateForm(forms.ModelForm): @@ -48,7 +48,7 @@ class ExpenseUpdateForm(forms.ModelForm): class RevenueCreateForm(CleanInvoiceForm): class Meta: model = Revenue - fields = ['description', 'amount', 'invoice', 'invoice_date', 'responsible_team'] + fields = ['description', 'amount', 'invoice_date', 'invoice', 'responsible_team'] class RevenueUpdateForm(forms.ModelForm): diff --git a/src/economy/migrations/0008_auto_20190327_1721.py b/src/economy/migrations/0008_auto_20190327_1721.py new file mode 100644 index 00000000..f820d02e --- /dev/null +++ b/src/economy/migrations/0008_auto_20190327_1721.py @@ -0,0 +1,69 @@ +# Generated by Django 2.1.7 on 2019-03-27 16:21 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('economy', '0007_auto_20190327_0936'), + ] + + operations = [ + migrations.CreateModel( + name='Chain', + 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)), + ('name', models.CharField(help_text='A short name for this Chain, like "Netto" or "XL Byg". 100 characters or fewer.', max_length=100, unique=True)), + ('slug', models.SlugField(help_text='The url slug for this Chain. Leave blank to auto generate a slug.', unique=True)), + ('notes', models.TextField(blank=True, help_text='Any notes for this Chain. Will be shown to anyone creating Expenses or Revenues for this Chain.')), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='Credebtor', + 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)), + ('name', models.CharField(help_text='The name of this Credebtor, like "XL Byg Rønne" or "Netto Gelsted". 100 characters or fewer.', max_length=100, unique=True)), + ('slug', models.SlugField(help_text='The url slug for this Credebtor. Leave blank to auto generate a slug.')), + ('address', models.TextField(help_text='The address of this Credebtor.')), + ('notes', models.TextField(blank=True, help_text='Any notes for this Credebtor. Shown when creating an Expense or Revenue for this Credebtor.')), + ('chain', models.ForeignKey(help_text='The Chain to which this Credebtor belongs.', on_delete=django.db.models.deletion.PROTECT, related_name='credebtors', to='economy.Chain')), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.AlterField( + model_name='expense', + name='invoice_date', + field=models.DateField(help_text='The invoice date for this Expense. This must match the invoice date on the documentation uploaded below. Format is YYYY-MM-DD.'), + ), + migrations.AlterField( + model_name='revenue', + name='invoice_date', + field=models.DateField(help_text='The invoice date for this Revenue. This must match the invoice date on the documentation uploaded below. Format is YYYY-MM-DD.'), + ), + migrations.AddField( + model_name='expense', + name='creditor', + field=models.ForeignKey(help_text='The Creditor to which this expense belongs', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='expenses', to='economy.Credebtor'), + ), + migrations.AddField( + model_name='revenue', + name='debtor', + field=models.ForeignKey(help_text='The Debtor to which this revenue belongs', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='revenues', to='economy.Credebtor'), + ), + migrations.AlterUniqueTogether( + name='credebtor', + unique_together={('chain', 'slug')}, + ), + ] diff --git a/src/economy/mixins.py b/src/economy/mixins.py index 40a683f9..7bd51fe3 100644 --- a/src/economy/mixins.py +++ b/src/economy/mixins.py @@ -1,4 +1,33 @@ from django.http import HttpResponseRedirect, Http404 +from django.shortcuts import redirect, get_object_or_404 + +from .models import Chain, Credebtor + + +class ChainViewMixin(object): + """ + The ChainViewMixin sets self.chain based on chain_slug from the URL + """ + def setup(self, *args, **kwargs): + if hasattr(super(), 'setup'): + super().setup(*args, **kwargs) + self.chain = get_object_or_404( + Chain, + slug=self.kwargs["chain_slug"], + ) + + +class CredebtorViewMixin(object): + """ + The CredebtorViewMixin sets self.credebtor based on credebtor_slug from the URL + """ + def setup(self, *args, **kwargs): + if hasattr(super(), 'setup'): + super().setup(*args, **kwargs) + self.credebtor = get_object_or_404( + Credebtor, + slug=self.kwargs["credebtor_slug"], + ) class ExpensePermissionMixin(object): diff --git a/src/economy/models.py b/src/economy/models.py index f1d28dff..4bde6ec9 100644 --- a/src/economy/models.py +++ b/src/economy/models.py @@ -5,15 +5,103 @@ from django.conf import settings from django.db import models from django.contrib import messages from django.core.exceptions import ValidationError +from django.utils.text import slugify -from utils.models import CampRelatedModel, UUIDModel +from utils.models import CampRelatedModel, CreatedUpdatedModel, UUIDModel from .email import * + +class Chain(CreatedUpdatedModel, UUIDModel): + """ + A chain of Credebtors. Used to group when several Creditors/Debtors + belong to the same Chain/company, like XL Byg stores or Netto stores. + """ + class Meta: + ordering = ['name'] + + name = models.CharField( + max_length=100, + unique=True, + help_text='A short name for this Chain, like "Netto" or "XL Byg". 100 characters or fewer.' + ) + + slug = models.SlugField( + unique=True, + help_text='The url slug for this Chain. Leave blank to auto generate a slug.' + ) + + notes = models.TextField( + help_text='Any notes for this Chain. Will be shown to anyone creating Expenses or Revenues for this Chain.', + blank=True, + ) + + def __str__(self): + return self.name + + def save(self, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super(Chain, self).save(**kwargs) + + +class Credebtor(CreatedUpdatedModel, UUIDModel): + """ + The Credebtor model represents the specific "instance" of a Chain, + like "XL Byg Rønne" or "Netto Gelsted". + The model is used for both creditors and debtors, since there is a + lot of overlap between them. + """ + class Meta: + ordering = ['name'] + unique_together=('chain', 'slug') + + chain = models.ForeignKey( + 'economy.Chain', + on_delete=models.PROTECT, + related_name='credebtors', + help_text='The Chain to which this Credebtor belongs.', + ) + + name = models.CharField( + max_length=100, + unique=True, + help_text='The name of this Credebtor, like "XL Byg Rønne" or "Netto Gelsted". 100 characters or fewer.' + ) + + slug = models.SlugField( + help_text='The url slug for this Credebtor. Leave blank to auto generate a slug.' + ) + + address = models.TextField( + help_text='The address of this Credebtor.', + ) + + notes = models.TextField( + blank=True, + help_text='Any notes for this Credebtor. Shown when creating an Expense or Revenue for this Credebtor.', + ) + + def __str__(self): + return self.name + + def save(self, **kwargs): + """ + Generate slug as needed + """ + if not self.slug: + self.slug = slugify(self.name) + super(Credebtor, self).save(**kwargs) + + 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. + + 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', @@ -22,6 +110,14 @@ class Revenue(CampRelatedModel, UUIDModel): help_text='The camp to which this revenue belongs', ) + debtor = models.ForeignKey( + 'economy.Credebtor', + on_delete=models.PROTECT, + related_name='revenues', + null=True, + help_text='The Debtor to which this revenue belongs', + ) + user = models.ForeignKey( 'auth.User', on_delete=models.PROTECT, @@ -47,8 +143,6 @@ class Revenue(CampRelatedModel, UUIDModel): invoice_date = models.DateField( help_text='The invoice date for this Revenue. This must match the invoice date on the documentation uploaded below. Format is YYYY-MM-DD.', - blank=True, - null=True, ) invoice_fk = models.ForeignKey( @@ -140,6 +234,14 @@ class Expense(CampRelatedModel, UUIDModel): help_text='The camp to which this expense belongs', ) + creditor = models.ForeignKey( + 'economy.Credebtor', + on_delete=models.PROTECT, + related_name='expenses', + null=True, + help_text='The Creditor to which this expense belongs', + ) + user = models.ForeignKey( 'auth.User', on_delete=models.PROTECT, @@ -170,8 +272,6 @@ class Expense(CampRelatedModel, UUIDModel): invoice_date = models.DateField( help_text='The invoice date for this Expense. This must match the invoice date on the documentation uploaded below. Format is YYYY-MM-DD.', - blank=True, - null=True, ) responsible_team = models.ForeignKey( diff --git a/src/economy/templates/chain_create.html b/src/economy/templates/chain_create.html new file mode 100644 index 00000000..6e2bd7da --- /dev/null +++ b/src/economy/templates/chain_create.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% load bootstrap3 %} + +{% block title %} +Create Chain | {{ block.super }} +{% endblock %} + +{% block content %} +

Create {{ camp.title }} Creditor/Debtor Chain

+
+ {% csrf_token %} + {% bootstrap_form form %} + + Cancel +
+{% endblock %} diff --git a/src/economy/templates/chain_list.html b/src/economy/templates/chain_list.html new file mode 100644 index 00000000..a5877abf --- /dev/null +++ b/src/economy/templates/chain_list.html @@ -0,0 +1,41 @@ +{% extends 'base.html' %} +{% load bootstrap3 %} + +{% block title %} +Select Chain | {{ block.super }} +{% endblock %} + +{% block content %} +

Chains

+

Expenses and Revenues for {{ camp.title }} must be associated with a Creditor or Debtor. Each Creditor and Debtor belongs to a Chain - e.g. the Creditor Netto Gelsted Belongs to the Chain Netto.

+

To continue, pick an existing Chain, or create a new.

+ +{% if chain_list %} +
+
+

Existing Chains

+
+ +
+{% else %} +

No existing Chains found. You can create a new Chain below.

+{% endif %} + +

+ Create New Chain + Cancel +

+{% endblock %} diff --git a/src/economy/templates/credebtor_create.html b/src/economy/templates/credebtor_create.html new file mode 100644 index 00000000..b3cb1e51 --- /dev/null +++ b/src/economy/templates/credebtor_create.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% load bootstrap3 %} + +{% block title %} +Create Credebtor in Chain {{ chain.name }} | {{ block.super }} +{% endblock %} + +{% block content %} +

Create New Credebtor in Chain {{ chain.name }}

+
+ {% csrf_token %} + {% bootstrap_form form %} + + Cancel +
+{% endblock %} diff --git a/src/economy/templates/credebtor_list.html b/src/economy/templates/credebtor_list.html new file mode 100644 index 00000000..fb2c3a6e --- /dev/null +++ b/src/economy/templates/credebtor_list.html @@ -0,0 +1,53 @@ +{% extends 'base.html' %} +{% load bootstrap3 %} + +{% block title %} +Credebtors in Chain {{ chain.name }} | {{ block.super }} +{% endblock %} + +{% block content %} +

Credebtors in Chain {{ chain.name }}

+

A Credebtor is a specific store in a Chain - Like Netto Gelsted or XL Byg Rønne. Please pick an existing Credebtor below, or create a new.

+ +{% if credebtor_list %} +
+
+

Existing Credebtors

+
+
+ + + {% for credebtor in chain.credebtors.all %} + + + + {% endfor %} + +
+

+ {{ credebtor.name }} +

+ +
+ {{ credebtor.address }} +
+ {% if credebtor.notes %} + Notes: +

{{ credebtor.notes }}

+ {% endif %} + Create Expense + Create Revenue +
+
+
+{% else %} +

No existing Credebtors found for {{ camp.title }}.

+{% endif %} + +

+ Create New Credebtor + Pick Another Chain + Create New Chain + Cancel +

+{% endblock %} diff --git a/src/economy/templates/dashboard.html b/src/economy/templates/dashboard.html index 98f1f16e..175ef064 100644 --- a/src/economy/templates/dashboard.html +++ b/src/economy/templates/dashboard.html @@ -8,6 +8,7 @@ Economy | {{ block.super }} <{% block content %}

Your {{ camp.title }} Economy Overview

+

This page shows your personal {{ camp.title }} economy overview. You can use the buttons to see the Expenses and Revenues you've registered in the system. You can also see any reimbursements you might have. If you registered an expense and you are waiting for a reimbursement please ask someone in the Economy Team to make a reimbursement.

@@ -22,7 +23,7 @@ Economy | {{ block.super }} @@ -37,7 +38,7 @@ Economy | {{ block.super }} diff --git a/src/economy/templates/expense_detail.html b/src/economy/templates/expense_detail.html index ac232641..39e58af0 100644 --- a/src/economy/templates/expense_detail.html +++ b/src/economy/templates/expense_detail.html @@ -8,5 +8,6 @@ Expense Details | {{ block.super }}
{% include 'includes/expense_detail_panel.html' %}
-Back to Expense List + Update Expense + Back to Expense List {% endblock %} diff --git a/src/economy/templates/expense_form.html b/src/economy/templates/expense_form.html index 420bd999..04fae023 100644 --- a/src/economy/templates/expense_form.html +++ b/src/economy/templates/expense_form.html @@ -6,11 +6,31 @@ {% endblock %} {% block content %} -

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

+

+{% if object %} +Update {{ camp.title }} Expense {{ object.uuid }} for {{ creditor.name }} +{% else %} +Create {{ camp.title }} Expense for {{ creditor.name }} +{% endif %} +

{% csrf_token %} + Chain +

{{ creditor.chain.name }}{% if not object %} Change{% endif %}

+{% if creditor.chain.notes %} + Chain Notes +

{{ creditor.chain.notes }}

+{% endif %} + Creditor +

{{ creditor.name }}{% if not object %} Change{% endif %}

+ Creditor Address +
{{ creditor.address }}
+ {% if creditor.notes %} + Creditor Notes +

{{ creditor.notes }}

+ {% endif %} {% bootstrap_form form %} - + Cancel {% endblock %} diff --git a/src/economy/templates/expense_list.html b/src/economy/templates/expense_list.html index 15d62c80..ab659626 100644 --- a/src/economy/templates/expense_list.html +++ b/src/economy/templates/expense_list.html @@ -16,7 +16,7 @@ Expenses | {{ block.super }} {% include 'includes/expense_list_panel.html' %} {% if perms.camps.expense_create_permission %} - Create Expense + Create New Expense {% else %}

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

{% endif %} diff --git a/src/economy/templates/includes/expense_detail_panel.html b/src/economy/templates/includes/expense_detail_panel.html index d83adb44..728b331f 100644 --- a/src/economy/templates/includes/expense_detail_panel.html +++ b/src/economy/templates/includes/expense_detail_panel.html @@ -10,6 +10,14 @@ + + + + + + + + diff --git a/src/economy/templates/includes/expense_list_panel.html b/src/economy/templates/includes/expense_list_panel.html index aca86383..dd8f9233 100644 --- a/src/economy/templates/includes/expense_list_panel.html +++ b/src/economy/templates/includes/expense_list_panel.html @@ -2,12 +2,13 @@
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 + Create Expense
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 + Create Revenue
Amount {{ expense.amount }} DKK
Chain{{ expense.creditor.chain.name }}
Creditor{{ expense.creditor.name }}
Invoice Date {{ expense.invoice_date }}
+ {% if not reimbursement %} {% endif %} + - {% if not reimbursement %} @@ -20,12 +21,13 @@ {% for expense in expense_list %} + {% if not reimbursement %} {% endif %} + - diff --git a/src/economy/templates/includes/revenue_detail_panel.html b/src/economy/templates/includes/revenue_detail_panel.html index 88ceb857..f1787d20 100644 --- a/src/economy/templates/includes/revenue_detail_panel.html +++ b/src/economy/templates/includes/revenue_detail_panel.html @@ -10,6 +10,14 @@ + + + + + + + + diff --git a/src/economy/templates/includes/revenue_list_panel.html b/src/economy/templates/includes/revenue_list_panel.html index a2165ac1..59e0715d 100644 --- a/src/economy/templates/includes/revenue_list_panel.html +++ b/src/economy/templates/includes/revenue_list_panel.html @@ -3,6 +3,7 @@ + @@ -15,6 +16,7 @@ {% for revenue in revenue_list %} + diff --git a/src/economy/templates/revenue_form.html b/src/economy/templates/revenue_form.html index cf8245d0..8e541824 100644 --- a/src/economy/templates/revenue_form.html +++ b/src/economy/templates/revenue_form.html @@ -6,9 +6,25 @@ {% endblock %} {% block content %} -

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

+

+{% if object %} +Update {{ camp.title }} Revenue {{ object.uuid }} for {{ debtor.name }} +{% else %} +Create {{ camp.title }} Revenue for {{ debtor.name }} +{% endif %} +

{% csrf_token %} + Chain +

{{ debtor.chain.name }}{% if not object %} Change{% endif %}

+{% if creditor.chain.notes %} + Chain Notes +

{{ creditor.chain.notes }}

+{% endif %} + Debtor +

{{ debtor.name }}{% if not object %} Change{% endif %}

+ Debtor Address +
{{ debtor.address }}
{% bootstrap_form form %} Cancel diff --git a/src/economy/templates/revenue_list.html b/src/economy/templates/revenue_list.html index bfc6737d..22848241 100644 --- a/src/economy/templates/revenue_list.html +++ b/src/economy/templates/revenue_list.html @@ -16,7 +16,7 @@ Revenues | {{ block.super }} {% include 'includes/revenue_list_panel.html' %} {% if perms.camps.revenue_create_permission %} - Create Revenue + 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 %} diff --git a/src/economy/urls.py b/src/economy/urls.py index 5ee3e1d5..c766e72c 100644 --- a/src/economy/urls.py +++ b/src/economy/urls.py @@ -10,6 +10,52 @@ urlpatterns = [ name='dashboard' ), + # chains + path('chains/', + include([ + path( + '', + ChainListView.as_view(), + name='chain_list', + ), + path( + 'add/', + ChainCreateView.as_view(), + name='chain_create', + ), + path( + '/', + include([ + path( + '', + CredebtorListView.as_view(), + name='credebtor_list', + ), + path( + 'add/', + CredebtorCreateView.as_view(), + name='credebtor_create', + ), + path( + '/', + include([ + path( + 'add_expense/', + ExpenseCreateView.as_view(), + name='expense_create', + ), + path( + 'add_revenue/', + RevenueCreateView.as_view(), + name='revenue_create', + ), + ]), + ), + ]), + ), + ]), + ), + # expenses path( 'expenses/', @@ -17,12 +63,7 @@ urlpatterns = [ path( '', ExpenseListView.as_view(), - name='expense_list' - ), - path( - 'add/', - ExpenseCreateView.as_view(), - name='expense_create' + name='expense_list', ), path( '/', @@ -78,11 +119,6 @@ urlpatterns = [ RevenueListView.as_view(), name='revenue_list' ), - path( - 'add/', - RevenueCreateView.as_view(), - name='revenue_create' - ), path( '/', include([ diff --git a/src/economy/views.py b/src/economy/views.py index cfac40e0..2a1e6caa 100644 --- a/src/economy/views.py +++ b/src/economy/views.py @@ -54,8 +54,84 @@ class EconomyDashboardView(LoginRequiredMixin, CampViewMixin, TemplateView): return context +########### Chain/Creditor related views ############### + + +class ChainCreateView(CampViewMixin, RaisePermissionRequiredMixin, CreateView): + model = Chain + template_name = 'chain_create.html' + permission_required = ("camps.expense_create_permission") + fields = ['name', 'notes'] + + def form_valid(self, form): + chain = form.save() + + # a message for the user + messages.success( + self.request, + "The new Chain %s has been saved. You can now add Creditor(s)/Debtor(s) for it." % chain.name, + ) + + return HttpResponseRedirect(reverse('economy:credebtor_create', kwargs={ + 'camp_slug': self.camp.slug, + 'chain_slug': chain.slug, + })) + + +class ChainListView(CampViewMixin, RaisePermissionRequiredMixin, ListView): + model = Chain + template_name = 'chain_list.html' + permission_required = ("camps.expense_create_permission") + + +class CredebtorCreateView(CampViewMixin, ChainViewMixin, RaisePermissionRequiredMixin, CreateView): + model = Credebtor + template_name = 'credebtor_create.html' + permission_required = ("camps.expense_create_permission") + fields = ['name', 'address', 'notes'] + + def get_context_data(self, **kwargs): + """ + Add chain to context + """ + context = super().get_context_data(**kwargs) + context['chain'] = self.chain + return context + + def form_valid(self, form): + credebtor = form.save(commit=False) + credebtor.chain = self.chain + credebtor.save() + + # a message for the user + messages.success( + self.request, + "The Creditor/Debtor %s has been saved. You can now add Expenses/Revenues for it." % credebtor.name, + ) + + return HttpResponseRedirect(reverse('economy:credebtor_list', kwargs={ + 'camp_slug': self.camp.slug, + 'chain_slug': self.chain.slug, + })) + + +class CredebtorListView(CampViewMixin, ChainViewMixin, RaisePermissionRequiredMixin, ListView): + model = Credebtor + template_name = 'credebtor_list.html' + permission_required = ("camps.expense_create_permission") + + def get_context_data(self, **kwargs): + """ + Add chain to context + """ + context = super().get_context_data(**kwargs) + context['chain'] = self.chain + return context + + ########### Expense related views ############### + class ExpenseListView(LoginRequiredMixin, CampViewMixin, ListView): model = Expense template_name = 'expense_list.html' @@ -70,7 +146,7 @@ class ExpenseDetailView(CampViewMixin, ExpensePermissionMixin, DetailView): template_name = 'expense_detail.html' -class ExpenseCreateView(CampViewMixin, RaisePermissionRequiredMixin, CreateView): +class ExpenseCreateView(CampViewMixin, CredebtorViewMixin, RaisePermissionRequiredMixin, CreateView): model = Expense template_name = 'expense_form.html' permission_required = ("camps.expense_create_permission") @@ -82,12 +158,14 @@ class ExpenseCreateView(CampViewMixin, RaisePermissionRequiredMixin, CreateView) """ context = super().get_context_data(**kwargs) context['form'].fields['responsible_team'].queryset = Team.objects.filter(camp=self.camp) + context['creditor'] = self.credebtor return context def form_valid(self, form): expense = form.save(commit=False) expense.user = self.request.user expense.camp = self.camp + expense.creditor = self.credebtor expense.save() # a message for the user @@ -126,6 +204,7 @@ class ExpenseUpdateView(CampViewMixin, ExpensePermissionMixin, UpdateView): """ context = super().get_context_data(**kwargs) context['form'].fields['responsible_team'].queryset = Team.objects.filter(camp=self.camp) + context['creditor'] = self.get_object().creditor return context def get_success_url(self): @@ -210,7 +289,7 @@ class RevenueDetailView(CampViewMixin, RevenuePermissionMixin, DetailView): template_name = 'revenue_detail.html' -class RevenueCreateView(CampViewMixin, RaisePermissionRequiredMixin, CreateView): +class RevenueCreateView(CampViewMixin, CredebtorViewMixin, RaisePermissionRequiredMixin, CreateView): model = Revenue template_name = 'revenue_form.html' permission_required = ("camps.revenue_create_permission") @@ -222,12 +301,14 @@ class RevenueCreateView(CampViewMixin, RaisePermissionRequiredMixin, CreateView) """ context = super().get_context_data(**kwargs) context['form'].fields['responsible_team'].queryset = Team.objects.filter(camp=self.camp) + context['debtor'] = self.credebtor return context def form_valid(self, form): revenue = form.save(commit=False) revenue.user = self.request.user revenue.camp = self.camp + revenue.debtor = self.credebtor revenue.save() # a message for the user
Invoice Date Created ByPaid byCreditor AmountInvoice Date Description Responsible Team
{{ expense.invoice_date }} {{ expense.user }}{% if expense.paid_by_bornhack %}BornHack{% else %}{{ expense.user }}{% endif %}{{ expense.creditor.name }} {{ expense.amount }} DKK{{ expense.invoice_date }} {{ expense.description }} {{ expense.responsible_team.name }} TeamAmount {{ revenue.amount }} DKK
Chain{{ revenue.debtor.chain.name }}
Creditor{{ revenue.debtor.name }}
Invoice Date {{ revenue.invoice_date }}
Created ByDebtor Amount Invoice Date Description
{{ revenue.user }}{{ revenue.debtor }} {{ revenue.amount }} DKK {{ revenue.invoice_date }} {{ revenue.description }}