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:
parent
1aa8d6a17c
commit
f248a5e0ca
|
@ -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()
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
69
src/economy/migrations/0008_auto_20190327_1721.py
Normal file
69
src/economy/migrations/0008_auto_20190327_1721.py
Normal 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')},
|
||||
),
|
||||
]
|
|
@ -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):
|
||||
|
|
|
@ -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(
|
||||
|
|
16
src/economy/templates/chain_create.html
Normal file
16
src/economy/templates/chain_create.html
Normal 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 %}
|
41
src/economy/templates/chain_list.html
Normal file
41
src/economy/templates/chain_list.html
Normal 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 %}
|
16
src/economy/templates/credebtor_create.html
Normal file
16
src/economy/templates/credebtor_create.html
Normal 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 %}
|
53
src/economy/templates/credebtor_list.html
Normal file
53
src/economy/templates/credebtor_list.html
Normal 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 %}
|
|
@ -8,6 +8,7 @@ Economy | {{ block.super }}
|
|||
<{% block content %}
|
||||
<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">
|
||||
<thead>
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
@ -8,5 +8,6 @@ Expense Details | {{ block.super }}
|
|||
<div class="row">
|
||||
{% include 'includes/expense_detail_panel.html' %}
|
||||
</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 %}
|
||||
|
|
|
@ -6,11 +6,31 @@
|
|||
{% endblock %}
|
||||
|
||||
{% 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">
|
||||
{% 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 %}
|
||||
<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>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
|
@ -16,7 +16,7 @@ Expenses | {{ block.super }}
|
|||
{% include 'includes/expense_list_panel.html' %}
|
||||
|
||||
{% if perms.camps.expense_create_permission %}
|
||||
<a class="btn btn-primary" href="{% url 'economy:expense_create' camp_slug=camp.slug %}"><i class="fas fa-plus"></i> Create Expense</a>
|
||||
<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 %}
|
||||
<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 %}
|
||||
|
|
|
@ -10,6 +10,14 @@
|
|||
<th>Amount</th>
|
||||
<td>{{ expense.amount }} DKK</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Chain</th>
|
||||
<td>{{ expense.creditor.chain.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Creditor</th>
|
||||
<td>{{ expense.creditor.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Invoice Date</th>
|
||||
<td>{{ expense.invoice_date }}</td>
|
||||
|
|
|
@ -2,12 +2,13 @@
|
|||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Invoice Date</th>
|
||||
<th>Created By</th>
|
||||
{% if not reimbursement %}
|
||||
<th>Paid by</th>
|
||||
{% endif %}
|
||||
<th>Creditor</th>
|
||||
<th>Amount</th>
|
||||
<th>Invoice Date</th>
|
||||
<th>Description</th>
|
||||
<th>Responsible Team</th>
|
||||
{% if not reimbursement %}
|
||||
|
@ -20,12 +21,13 @@
|
|||
<tbody>
|
||||
{% for expense in expense_list %}
|
||||
<tr>
|
||||
<td>{{ expense.invoice_date }}</td>
|
||||
<td>{{ expense.user }}</td>
|
||||
{% if not reimbursement %}
|
||||
<td>{% if expense.paid_by_bornhack %}BornHack{% else %}{{ expense.user }}{% endif %}</td>
|
||||
{% endif %}
|
||||
<td>{{ expense.creditor.name }}</td>
|
||||
<td>{{ expense.amount }} DKK</td>
|
||||
<td>{{ expense.invoice_date }}</td>
|
||||
<td>{{ expense.description }}</td>
|
||||
<td>{{ expense.responsible_team.name }} Team</td>
|
||||
|
||||
|
|
|
@ -10,6 +10,14 @@
|
|||
<th>Amount</th>
|
||||
<td>{{ revenue.amount }} DKK</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Chain</th>
|
||||
<td>{{ revenue.debtor.chain.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Creditor</th>
|
||||
<td>{{ revenue.debtor.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Invoice Date</th>
|
||||
<td>{{ revenue.invoice_date }}</td>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th>Created By</th>
|
||||
<th>Debtor</th>
|
||||
<th>Amount</th>
|
||||
<th>Invoice Date</th>
|
||||
<th>Description</th>
|
||||
|
@ -15,6 +16,7 @@
|
|||
{% for revenue in revenue_list %}
|
||||
<tr>
|
||||
<td>{{ revenue.user }}</td>
|
||||
<td>{{ revenue.debtor }}</td>
|
||||
<td>{{ revenue.amount }} DKK</td>
|
||||
<td>{{ revenue.invoice_date }}</td>
|
||||
<td>{{ revenue.description }}</td>
|
||||
|
|
|
@ -6,9 +6,25 @@
|
|||
{% endblock %}
|
||||
|
||||
{% 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">
|
||||
{% 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 %}
|
||||
<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>
|
||||
|
|
|
@ -16,7 +16,7 @@ Revenues | {{ block.super }}
|
|||
{% include 'includes/revenue_list_panel.html' %}
|
||||
|
||||
{% if perms.camps.revenue_create_permission %}
|
||||
<a class="btn btn-success" href="{% url 'economy:revenue_create' camp_slug=camp.slug %}"><i class="fas fa-plus"></i> Create Revenue</a>
|
||||
<a class="btn btn-success" href="{% url 'economy:chain_list' camp_slug=camp.slug %}"><i class="fas fa-plus"></i> Create Revenue</a>
|
||||
{% else %}
|
||||
<div class="alert alert-danger"><p class="lead"><span class="text-error">You don't have permission to add revenue. Please ask someone from the Economy team to add the permission if you need it.</p></div>
|
||||
{% endif %}
|
||||
|
|
|
@ -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(
|
||||
'<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
|
||||
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(
|
||||
'<uuid:pk>/',
|
||||
|
@ -78,11 +119,6 @@ urlpatterns = [
|
|||
RevenueListView.as_view(),
|
||||
name='revenue_list'
|
||||
),
|
||||
path(
|
||||
'add/',
|
||||
RevenueCreateView.as_view(),
|
||||
name='revenue_create'
|
||||
),
|
||||
path(
|
||||
'<uuid:pk>/',
|
||||
include([
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue