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.

This commit is contained in:
Thomas Steen Rasmussen 2019-03-28 07:04:53 +01:00
parent 1aa8d6a17c
commit f248a5e0ca
22 changed files with 556 additions and 37 deletions

View file

@ -6,8 +6,7 @@ from django.utils.functional import cached_property
class CampViewMixin(object): class CampViewMixin(object):
""" """
This mixin makes sure self.camp is available (taken from url kwarg camp_slug) 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 It also filters out objects that belong to other camps when the queryset has a camp_filter
a direct relation to the Camp model.
""" """
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
@ -21,6 +20,10 @@ class CampViewMixin(object):
if not queryset: if not queryset:
return 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 # get the camp_filter from the model
camp_filter = self.model.get_camp_filter() camp_filter = self.model.get_camp_filter()

View file

@ -1,5 +1,22 @@
from django.contrib import admin 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 ### expenses
@ -18,8 +35,8 @@ reject_expenses.short_description = "Reject Expenses"
@admin.register(Expense) @admin.register(Expense)
class ExpenseAdmin(admin.ModelAdmin): class ExpenseAdmin(admin.ModelAdmin):
list_filter = ['camp', 'responsible_team', 'approved', 'user'] list_filter = ['camp', 'creditor__chain', 'creditor', 'responsible_team', 'approved', 'user']
list_display = ['user', 'description', 'invoice_date', 'amount', 'camp', 'responsible_team', 'approved', 'reimbursement'] list_display = ['user', 'description', 'invoice_date', 'amount', 'camp', 'creditor', 'responsible_team', 'approved', 'reimbursement']
search_fields = ['description', 'amount', 'uuid'] search_fields = ['description', 'amount', 'uuid']
actions = [approve_expenses, reject_expenses] actions = [approve_expenses, reject_expenses]

View file

@ -36,7 +36,7 @@ class CleanInvoiceForm(forms.ModelForm):
class ExpenseCreateForm(CleanInvoiceForm): class ExpenseCreateForm(CleanInvoiceForm):
class Meta: class Meta:
model = Expense 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): class ExpenseUpdateForm(forms.ModelForm):
@ -48,7 +48,7 @@ class ExpenseUpdateForm(forms.ModelForm):
class RevenueCreateForm(CleanInvoiceForm): class RevenueCreateForm(CleanInvoiceForm):
class Meta: class Meta:
model = Revenue model = Revenue
fields = ['description', 'amount', 'invoice', 'invoice_date', 'responsible_team'] fields = ['description', 'amount', 'invoice_date', 'invoice', 'responsible_team']
class RevenueUpdateForm(forms.ModelForm): class RevenueUpdateForm(forms.ModelForm):

View file

@ -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')},
),
]

View file

@ -1,4 +1,33 @@
from django.http import HttpResponseRedirect, Http404 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): class ExpensePermissionMixin(object):

View file

@ -5,15 +5,103 @@ from django.conf import settings
from django.db import models from django.db import models
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import ValidationError 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 * 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): class Revenue(CampRelatedModel, UUIDModel):
""" """
The Revenue model represents any type of income for BornHack. 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( camp = models.ForeignKey(
'camps.Camp', 'camps.Camp',
@ -22,6 +110,14 @@ class Revenue(CampRelatedModel, UUIDModel):
help_text='The camp to which this revenue belongs', 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( user = models.ForeignKey(
'auth.User', 'auth.User',
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -47,8 +143,6 @@ class Revenue(CampRelatedModel, UUIDModel):
invoice_date = models.DateField( 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.', 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( invoice_fk = models.ForeignKey(
@ -140,6 +234,14 @@ class Expense(CampRelatedModel, UUIDModel):
help_text='The camp to which this expense belongs', 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( user = models.ForeignKey(
'auth.User', 'auth.User',
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -170,8 +272,6 @@ class Expense(CampRelatedModel, UUIDModel):
invoice_date = models.DateField( 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.', 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( responsible_team = models.ForeignKey(

View file

@ -0,0 +1,16 @@
{% extends 'base.html' %}
{% load bootstrap3 %}
{% block title %}
Create Chain | {{ block.super }}
{% endblock %}
{% block content %}
<h3>Create {{ camp.title }} Creditor/Debtor Chain</h3>
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
<button class="btn btn-success" type="submit"><i class="fas fa-plus"></i> Create Chain</button>
<a href="{% url 'economy:expense_list' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
</form>
{% endblock %}

View file

@ -0,0 +1,41 @@
{% extends 'base.html' %}
{% load bootstrap3 %}
{% block title %}
Select Chain | {{ block.super }}
{% endblock %}
{% block content %}
<h2>Chains</h2>
<p class="lead">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 <i>Netto Gelsted</i> Belongs to the Chain <i>Netto</i>.</p>
<p class="lead">To continue, pick an existing Chain, or create a new.</p>
{% if chain_list %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Existing Chains</h3>
</div>
<div class="panel-body">
<div class="list-group">
{% for chain in chain_list %}
<a href="{% url 'economy:credebtor_list' camp_slug=camp.slug chain_slug=chain.slug %}" class="list-group-item">
<h4 class="list-group-item-heading">
<b>{{ chain.name }}</b> ({{ chain.credebtors.count }} credebtors)
</h4>
{% if chain.notes %}
<p>{{ chain.notes }}</p>
{% endif %}
</a>
{% endfor %}
</div>
</div>
</div>
{% else %}
<p class="lead"><i>No existing Chains found. You can create a new Chain below.</i></p>
{% endif %}
<p>
<a href="{% url 'economy:chain_create' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-plus"></i> Create New Chain</a>
<a href="{% url 'economy:expense_list' camp_slug=camp.slug %}" class="btn btn-danger"><i class="fas fa-undo"></i> Cancel</a>
</p>
{% endblock %}

View file

@ -0,0 +1,16 @@
{% extends 'base.html' %}
{% load bootstrap3 %}
{% block title %}
Create Credebtor in Chain {{ chain.name }} | {{ block.super }}
{% endblock %}
{% block content %}
<h3>Create New Credebtor in Chain {{ chain.name }}</h3>
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
<button class="btn btn-success" type="submit"><i class="fas fa-check"></i> Create Credebtor</button>
<a href="{% url 'economy:dashboard' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
</form>
{% endblock %}

View file

@ -0,0 +1,53 @@
{% extends 'base.html' %}
{% load bootstrap3 %}
{% block title %}
Credebtors in Chain {{ chain.name }} | {{ block.super }}
{% endblock %}
{% block content %}
<h3>Credebtors in Chain {{ chain.name }}</h3>
<p class="lead">A Credebtor is a specific store in a Chain - Like <i>Netto Gelsted</i> or <i>XL Byg Rønne</i>. Please pick an existing Credebtor below, or create a new.</p>
{% if credebtor_list %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Existing Credebtors</h3>
</div>
<div class="panel-body">
<table class="table table-hover">
<tbody>
{% for credebtor in chain.credebtors.all %}
<tr>
<td>
<h4>
<b>{{ credebtor.name }}</b>
</h4>
<address>
{{ credebtor.address }}
</address>
{% if credebtor.notes %}
<b>Notes:</b>
<p>{{ credebtor.notes }}</p>
{% endif %}
<a href="{% url 'economy:expense_create' camp_slug=camp.slug chain_slug=chain.slug credebtor_slug=credebtor.slug %}" class="btn btn-primary"><i class="fas fa-coins"></i> <i class="fas fa-divide"></i> Create Expense</a>
<a href="{% url 'economy:revenue_create' camp_slug=camp.slug chain_slug=chain.slug credebtor_slug=credebtor.slug %}" class="btn btn-primary"><i class="fas fa-coins"></i> <i class="fas fa-plus"></i> Create Revenue</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% else %}
<p class="lead"><i>No existing Credebtors found for {{ camp.title }}.</i></p>
{% endif %}
<p>
<a href="{% url 'economy:credebtor_create' camp_slug=camp.slug chain_slug=chain.slug %}" class="btn btn-primary"><i class="fas fa-plus"></i> Create New Credebtor</a>
<a href="{% url 'economy:chain_list' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-list"></i> Pick Another Chain</a>
<a href="{% url 'economy:chain_create' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-plus"></i> Create New Chain</a>
<a href="{% url 'economy:dashboard' camp_slug=camp.slug %}" class="btn btn-danger"><i class="fas fa-undo"></i> Cancel</a>
</p>
{% endblock %}

View file

@ -8,6 +8,7 @@ Economy | {{ block.super }}
<{% block content %} <{% block content %}
<h3>Your {{ camp.title }} Economy Overview</h3> <h3>Your {{ camp.title }} Economy Overview</h3>
<p>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 <a href="{% url 'teams:general' camp_slug=camp.slug team_slug='economy' %}">Economy Team</a> to make a reimbursement.</p>
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
@ -22,7 +23,7 @@ Economy | {{ block.super }}
<td>You have <b>{{ expense_count }} expense{{ expense_count|pluralize }}</b> ({{ approved_expense_count }} approved, {{ rejected_expense_count }} rejected, and {{ unapproved_expense_count }} pending approval) for {{ camp.title }}, for a total of <b>{{ expense_total|default:"0" }} DKK</b>.</td> <td>You have <b>{{ expense_count }} expense{{ expense_count|pluralize }}</b> ({{ approved_expense_count }} approved, {{ rejected_expense_count }} rejected, and {{ unapproved_expense_count }} pending approval) for {{ camp.title }}, for a total of <b>{{ expense_total|default:"0" }} DKK</b>.</td>
<td> <td>
<a href="{% url "economy:expense_list" camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-list"></i> List Expenses</a> <a href="{% url "economy:expense_list" camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-list"></i> List Expenses</a>
<a href="{% url "economy:expense_create" camp_slug=camp.slug %}" class="btn btn-success"><i class="fas fa-plus"></i> Create Expense</a> <a href="{% url "economy:chain_list" camp_slug=camp.slug %}" class="btn btn-success"><i class="fas fa-plus"></i> Create Expense</a>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -37,7 +38,7 @@ Economy | {{ block.super }}
<td>You have <b>{{ revenue_count }} revenue{{ revenue_count|pluralize }}</b> ({{ approved_revenue_count }} approved, {{ rejected_revenue_count }} rejected, and {{ unapproved_revenue_count }} still pending approval) for {{ camp.title }}, for a total of <b>{{ revenue_total|default:"0" }} DKK</b>.</td> <td>You have <b>{{ revenue_count }} revenue{{ revenue_count|pluralize }}</b> ({{ approved_revenue_count }} approved, {{ rejected_revenue_count }} rejected, and {{ unapproved_revenue_count }} still pending approval) for {{ camp.title }}, for a total of <b>{{ revenue_total|default:"0" }} DKK</b>.</td>
<td> <td>
<a href="{% url "economy:revenue_list" camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-list"></i> List Revenues</a> <a href="{% url "economy:revenue_list" camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-list"></i> List Revenues</a>
<a href="{% url "economy:revenue_create" camp_slug=camp.slug %}" class="btn btn-success"><i class="fas fa-plus"></i> Create Revenue</a> <a href="{% url "economy:chain_list" camp_slug=camp.slug %}" class="btn btn-success"><i class="fas fa-plus"></i> Create Revenue</a>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View file

@ -8,5 +8,6 @@ Expense Details | {{ block.super }}
<div class="row"> <div class="row">
{% include 'includes/expense_detail_panel.html' %} {% include 'includes/expense_detail_panel.html' %}
</div> </div>
<a class="btn btn-primary" href="{% url 'economy:expense_list' camp_slug=camp.slug %}">Back to Expense List</a> <a class="btn btn-primary" href="{% url 'economy:expense_update' camp_slug=camp.slug pk=expense.uuid %}"><i class="fas fa-edit"></i> Update Expense</a>
<a class="btn btn-primary" href="{% url 'economy:expense_list' camp_slug=camp.slug %}"><i class="fas fa-list"></i> Back to Expense List</a>
{% endblock %} {% endblock %}

View file

@ -6,11 +6,31 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h3>{% if object %}Update{% else %}Create{% endif %} {{ camp.title }} Expense</h3> <h3>
{% if object %}
Update {{ camp.title }} Expense {{ object.uuid }} for {{ creditor.name }}
{% else %}
Create {{ camp.title }} Expense for {{ creditor.name }}
{% endif %}
</h3>
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<b>Chain</b>
<p>{{ creditor.chain.name }}{% if not object %} <span class="small"><a href="{% url 'economy:chain_list' camp_slug=camp.slug %}">Change</a></span>{% endif %}</p>
{% if creditor.chain.notes %}
<b>Chain Notes</b>
<p>{{ creditor.chain.notes }}</p>
{% endif %}
<b>Creditor</b>
<p>{{ creditor.name }}{% if not object %} <span class="small"><a href="{% url 'economy:credebtor_list' camp_slug=camp.slug chain_slug=creditor.chain.slug %}">Change</a></span>{% endif %}</p>
<b>Creditor Address</b>
<address>{{ creditor.address }}</address>
{% if creditor.notes %}
<b>Creditor Notes</b>
<p>{{ creditor.notes }}</p>
{% endif %}
{% bootstrap_form form %} {% bootstrap_form form %}
<button class="btn btn-success" type="submit"><i class="fas fa-plus"></i> Save</button> <button class="btn btn-success" type="submit"><i class="fas fa-check"></i> Save</button>
<a href="{% url 'economy:expense_list' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a> <a href="{% url 'economy:expense_list' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
</form> </form>
{% endblock %} {% endblock %}

View file

@ -16,7 +16,7 @@ Expenses | {{ block.super }}
{% include 'includes/expense_list_panel.html' %} {% include 'includes/expense_list_panel.html' %}
{% if perms.camps.expense_create_permission %} {% if perms.camps.expense_create_permission %}
<a class="btn btn-primary" href="{% url 'economy:expense_create' camp_slug=camp.slug %}"><i class="fas fa-plus"></i> Create Expense</a> <a class="btn btn-primary" href="{% url 'economy:chain_list' camp_slug=camp.slug %}"><i class="fas fa-plus"></i> Create New Expense</a>
{% else %} {% else %}
<div class="alert alert-danger"><p class="lead"><span class="text-error">You don't have permission to add expenses. Please ask someone from the Economy team to add the permission if you need it.</p></div> <div class="alert alert-danger"><p class="lead"><span class="text-error">You don't have permission to add expenses. Please ask someone from the Economy team to add the permission if you need it.</p></div>
{% endif %} {% endif %}

View file

@ -10,6 +10,14 @@
<th>Amount</th> <th>Amount</th>
<td>{{ expense.amount }} DKK</td> <td>{{ expense.amount }} DKK</td>
</tr> </tr>
<tr>
<th>Chain</th>
<td>{{ expense.creditor.chain.name }}</td>
</tr>
<tr>
<th>Creditor</th>
<td>{{ expense.creditor.name }}</td>
</tr>
<tr> <tr>
<th>Invoice Date</th> <th>Invoice Date</th>
<td>{{ expense.invoice_date }}</td> <td>{{ expense.invoice_date }}</td>

View file

@ -2,12 +2,13 @@
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
<th>Invoice Date</th>
<th>Created By</th> <th>Created By</th>
{% if not reimbursement %} {% if not reimbursement %}
<th>Paid by</th> <th>Paid by</th>
{% endif %} {% endif %}
<th>Creditor</th>
<th>Amount</th> <th>Amount</th>
<th>Invoice Date</th>
<th>Description</th> <th>Description</th>
<th>Responsible Team</th> <th>Responsible Team</th>
{% if not reimbursement %} {% if not reimbursement %}
@ -20,12 +21,13 @@
<tbody> <tbody>
{% for expense in expense_list %} {% for expense in expense_list %}
<tr> <tr>
<td>{{ expense.invoice_date }}</td>
<td>{{ expense.user }}</td> <td>{{ expense.user }}</td>
{% if not reimbursement %} {% if not reimbursement %}
<td>{% if expense.paid_by_bornhack %}BornHack{% else %}{{ expense.user }}{% endif %}</td> <td>{% if expense.paid_by_bornhack %}BornHack{% else %}{{ expense.user }}{% endif %}</td>
{% endif %} {% endif %}
<td>{{ expense.creditor.name }}</td>
<td>{{ expense.amount }} DKK</td> <td>{{ expense.amount }} DKK</td>
<td>{{ expense.invoice_date }}</td>
<td>{{ expense.description }}</td> <td>{{ expense.description }}</td>
<td>{{ expense.responsible_team.name }} Team</td> <td>{{ expense.responsible_team.name }} Team</td>

View file

@ -10,6 +10,14 @@
<th>Amount</th> <th>Amount</th>
<td>{{ revenue.amount }} DKK</td> <td>{{ revenue.amount }} DKK</td>
</tr> </tr>
<tr>
<th>Chain</th>
<td>{{ revenue.debtor.chain.name }}</td>
</tr>
<tr>
<th>Creditor</th>
<td>{{ revenue.debtor.name }}</td>
</tr>
<tr> <tr>
<th>Invoice Date</th> <th>Invoice Date</th>
<td>{{ revenue.invoice_date }}</td> <td>{{ revenue.invoice_date }}</td>

View file

@ -3,6 +3,7 @@
<thead> <thead>
<tr> <tr>
<th>Created By</th> <th>Created By</th>
<th>Debtor</th>
<th>Amount</th> <th>Amount</th>
<th>Invoice Date</th> <th>Invoice Date</th>
<th>Description</th> <th>Description</th>
@ -15,6 +16,7 @@
{% for revenue in revenue_list %} {% for revenue in revenue_list %}
<tr> <tr>
<td>{{ revenue.user }}</td> <td>{{ revenue.user }}</td>
<td>{{ revenue.debtor }}</td>
<td>{{ revenue.amount }} DKK</td> <td>{{ revenue.amount }} DKK</td>
<td>{{ revenue.invoice_date }}</td> <td>{{ revenue.invoice_date }}</td>
<td>{{ revenue.description }}</td> <td>{{ revenue.description }}</td>

View file

@ -6,9 +6,25 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h3>{% if object %}Update{% else %}Create{% endif %} {{ camp.title }} Revenue</h3> <h3>
{% if object %}
Update {{ camp.title }} Revenue {{ object.uuid }} for {{ debtor.name }}
{% else %}
Create {{ camp.title }} Revenue for {{ debtor.name }}
{% endif %}
</h3>
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<b>Chain</b>
<p>{{ debtor.chain.name }}{% if not object %} <span class="small"><a href="{% url 'economy:chain_list' camp_slug=camp.slug %}">Change</a></span>{% endif %}</p>
{% if creditor.chain.notes %}
<b>Chain Notes</b>
<p>{{ creditor.chain.notes }}</p>
{% endif %}
<b>Debtor</b>
<p>{{ debtor.name }}{% if not object %} <span class="small"><a href="{% url 'economy:credebtor_list' camp_slug=camp.slug chain_slug=debtor.chain.slug %}">Change</a></span>{% endif %}</p>
<b>Debtor Address</b>
<address>{{ debtor.address }}</address>
{% bootstrap_form form %} {% bootstrap_form form %}
<button class="btn btn-primary" type="submit"><i class="fas fa-check"></i> Save</button> <button class="btn btn-primary" type="submit"><i class="fas fa-check"></i> Save</button>
<a href="{% url 'economy:revenue_list' camp_slug=camp.slug %}" class="btn btn-danger"><i class="fas fa-undo"></i> Cancel</a> <a href="{% url 'economy:revenue_list' camp_slug=camp.slug %}" class="btn btn-danger"><i class="fas fa-undo"></i> Cancel</a>

View file

@ -16,7 +16,7 @@ Revenues | {{ block.super }}
{% include 'includes/revenue_list_panel.html' %} {% include 'includes/revenue_list_panel.html' %}
{% if perms.camps.revenue_create_permission %} {% if perms.camps.revenue_create_permission %}
<a class="btn btn-success" href="{% url 'economy:revenue_create' camp_slug=camp.slug %}"><i class="fas fa-plus"></i> Create Revenue</a> <a class="btn btn-success" href="{% url 'economy:chain_list' camp_slug=camp.slug %}"><i class="fas fa-plus"></i> Create Revenue</a>
{% else %} {% else %}
<div class="alert alert-danger"><p class="lead"><span class="text-error">You don't have permission to add revenue. Please ask someone from the Economy team to add the permission if you need it.</p></div> <div class="alert alert-danger"><p class="lead"><span class="text-error">You don't have permission to add revenue. Please ask someone from the Economy team to add the permission if you need it.</p></div>
{% endif %} {% endif %}

View file

@ -10,6 +10,52 @@ urlpatterns = [
name='dashboard' name='dashboard'
), ),
# chains
path('chains/',
include([
path(
'',
ChainListView.as_view(),
name='chain_list',
),
path(
'add/',
ChainCreateView.as_view(),
name='chain_create',
),
path(
'<slug:chain_slug>/',
include([
path(
'',
CredebtorListView.as_view(),
name='credebtor_list',
),
path(
'add/',
CredebtorCreateView.as_view(),
name='credebtor_create',
),
path(
'<slug:credebtor_slug>/',
include([
path(
'add_expense/',
ExpenseCreateView.as_view(),
name='expense_create',
),
path(
'add_revenue/',
RevenueCreateView.as_view(),
name='revenue_create',
),
]),
),
]),
),
]),
),
# expenses # expenses
path( path(
'expenses/', 'expenses/',
@ -17,12 +63,7 @@ urlpatterns = [
path( path(
'', '',
ExpenseListView.as_view(), ExpenseListView.as_view(),
name='expense_list' name='expense_list',
),
path(
'add/',
ExpenseCreateView.as_view(),
name='expense_create'
), ),
path( path(
'<uuid:pk>/', '<uuid:pk>/',
@ -78,11 +119,6 @@ urlpatterns = [
RevenueListView.as_view(), RevenueListView.as_view(),
name='revenue_list' name='revenue_list'
), ),
path(
'add/',
RevenueCreateView.as_view(),
name='revenue_create'
),
path( path(
'<uuid:pk>/', '<uuid:pk>/',
include([ include([

View file

@ -54,8 +54,84 @@ class EconomyDashboardView(LoginRequiredMixin, CampViewMixin, TemplateView):
return context 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 ############### ########### Expense related views ###############
class ExpenseListView(LoginRequiredMixin, CampViewMixin, ListView): class ExpenseListView(LoginRequiredMixin, CampViewMixin, ListView):
model = Expense model = Expense
template_name = 'expense_list.html' template_name = 'expense_list.html'
@ -70,7 +146,7 @@ class ExpenseDetailView(CampViewMixin, ExpensePermissionMixin, DetailView):
template_name = 'expense_detail.html' template_name = 'expense_detail.html'
class ExpenseCreateView(CampViewMixin, RaisePermissionRequiredMixin, CreateView): class ExpenseCreateView(CampViewMixin, CredebtorViewMixin, RaisePermissionRequiredMixin, CreateView):
model = Expense model = Expense
template_name = 'expense_form.html' template_name = 'expense_form.html'
permission_required = ("camps.expense_create_permission") permission_required = ("camps.expense_create_permission")
@ -82,12 +158,14 @@ class ExpenseCreateView(CampViewMixin, RaisePermissionRequiredMixin, CreateView)
""" """
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['form'].fields['responsible_team'].queryset = Team.objects.filter(camp=self.camp) context['form'].fields['responsible_team'].queryset = Team.objects.filter(camp=self.camp)
context['creditor'] = self.credebtor
return context return context
def form_valid(self, form): def form_valid(self, form):
expense = form.save(commit=False) expense = form.save(commit=False)
expense.user = self.request.user expense.user = self.request.user
expense.camp = self.camp expense.camp = self.camp
expense.creditor = self.credebtor
expense.save() expense.save()
# a message for the user # a message for the user
@ -126,6 +204,7 @@ class ExpenseUpdateView(CampViewMixin, ExpensePermissionMixin, UpdateView):
""" """
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['form'].fields['responsible_team'].queryset = Team.objects.filter(camp=self.camp) context['form'].fields['responsible_team'].queryset = Team.objects.filter(camp=self.camp)
context['creditor'] = self.get_object().creditor
return context return context
def get_success_url(self): def get_success_url(self):
@ -210,7 +289,7 @@ class RevenueDetailView(CampViewMixin, RevenuePermissionMixin, DetailView):
template_name = 'revenue_detail.html' template_name = 'revenue_detail.html'
class RevenueCreateView(CampViewMixin, RaisePermissionRequiredMixin, CreateView): class RevenueCreateView(CampViewMixin, CredebtorViewMixin, RaisePermissionRequiredMixin, CreateView):
model = Revenue model = Revenue
template_name = 'revenue_form.html' template_name = 'revenue_form.html'
permission_required = ("camps.revenue_create_permission") permission_required = ("camps.revenue_create_permission")
@ -222,12 +301,14 @@ class RevenueCreateView(CampViewMixin, RaisePermissionRequiredMixin, CreateView)
""" """
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['form'].fields['responsible_team'].queryset = Team.objects.filter(camp=self.camp) context['form'].fields['responsible_team'].queryset = Team.objects.filter(camp=self.camp)
context['debtor'] = self.credebtor
return context return context
def form_valid(self, form): def form_valid(self, form):
revenue = form.save(commit=False) revenue = form.save(commit=False)
revenue.user = self.request.user revenue.user = self.request.user
revenue.camp = self.camp revenue.camp = self.camp
revenue.debtor = self.credebtor
revenue.save() revenue.save()
# a message for the user # a message for the user