fix a few things in backoffice chain/credebtor views and templates, and update bootstrap_devsite script to create chains, credebtors, expenses and revenues

This commit is contained in:
Thomas Steen Rasmussen 2020-10-16 17:22:41 +02:00
parent 02f0d77c21
commit 833fc85e46
7 changed files with 123 additions and 19 deletions

View file

@ -7,7 +7,7 @@ Details for Chain {{ chain.name }} | {{ block.super }}
{% block content %} {% block content %}
<h2>Details for Chain {{ chain.name }}</h2> <h2>Details for Chain {{ chain.name }}</h2>
<a href="{% url "backoffice:chain_list" camp_slug=camp.slug %}">Back to Chain list</a> <a class="btn btn-default" href="{% url "backoffice:chain_list" camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Back to Chain list</a>
<h3>{{ chain.credebtors.count }} Credebtors for Chain {{ chain.name }}</h3> <h3>{{ chain.credebtors.count }} Credebtors for Chain {{ chain.name }}</h3>
<table class="table table-hover"> <table class="table table-hover">
@ -26,7 +26,7 @@ Details for Chain {{ chain.name }} | {{ block.super }}
<tbody> <tbody>
{% for credebtor in chain.credebtors.all %} {% for credebtor in chain.credebtors.all %}
<tr> <tr>
<td>{{ credebtor.name }}</td> <td><a class="btn btn-primary" href="{% url 'backoffice:credebtor_detail' camp_slug=camp.slug chain_slug=chain.slug credebtor_slug=credebtor.slug %}">{{ credebtor.name }}</a></td>
<td><address>{{ credebtor.address }}</address></td> <td><address>{{ credebtor.address }}</address></td>
<td>{{ credebtor.notes|default:"N/A" }}</td> <td>{{ credebtor.notes|default:"N/A" }}</td>
<td class="text-center"><span class="badge">{{ credebtor.expenses.count }}</span></td> <td class="text-center"><span class="badge">{{ credebtor.expenses.count }}</span></td>
@ -39,10 +39,10 @@ Details for Chain {{ chain.name }} | {{ block.super }}
</tbody> </tbody>
</table> </table>
<h3>{{ chain.expenses.count }} Expenses for Chain {{ chain.name }}</h3> <h3>{{ chain.expenses_count }} Expenses for Chain {{ chain.name }}</h3>
{% include 'includes/expense_list_panel.html' with expense_list=chain.expenses.all %} {% include 'includes/expense_list_panel.html' with expense_list=chain.expenses.all %}
<h3>{{ chain.revenues.count }} Revenues for Chain {{ chain.name }}</h3> <h3>{{ chain.revenues_count }} Revenues for Chain {{ chain.name }}</h3>
{% include 'includes/revenue_list_panel.html' with revenue_list=chain.revenues.all %} {% include 'includes/revenue_list_panel.html' with revenue_list=chain.revenues.all %}
{% endblock content %} {% endblock content %}

View file

@ -8,7 +8,7 @@ Select Chain | {{ block.super }}
{% block content %} {% block content %}
<h2>Chains</h2> <h2>Chains</h2>
<p><a href="{% url "backoffice:index" camp_slug=camp.slug %}">Back to Backoffice Index</a></p> <p><a class="btn btn-default" href="{% url "backoffice:index" camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Back to Backoffice Index</a></p>
{% if chain_list %} {% if chain_list %}
<table class="table table-hover datatable"> <table class="table table-hover datatable">
@ -30,9 +30,9 @@ Select Chain | {{ block.super }}
<td>{{ chain.name }}</td> <td>{{ chain.name }}</td>
<td>{{ chain.notes|default:"N/A" }}</td> <td>{{ chain.notes|default:"N/A" }}</td>
<td class="text-center"><span class="badge">{{ chain.credebtors.count }}</span></td> <td class="text-center"><span class="badge">{{ chain.credebtors.count }}</span></td>
<td class="text-center"><span class="badge">{{ chain.expenses.count }}</span></td> <td class="text-center"><span class="badge">{{ chain.expenses_count }}</span></td>
<td class="text-center">{{ chain.expenses_total|default:"0" }} DKK</td> <td class="text-center">{{ chain.expenses_total|default:"0" }} DKK</td>
<td class="text-center"><span class="badge">{{ chain.revenues.count }}</span></td> <td class="text-center"><span class="badge">{{ chain.revenues_count }}</span></td>
<td class="text-center">{{ chain.revenues_total|default:"0" }} DKK</td> <td class="text-center">{{ chain.revenues_total|default:"0" }} DKK</td>
<td> <td>
<a class="btn btn-primary" href="{% url 'backoffice:chain_detail' camp_slug=camp.slug chain_slug=chain.slug %}"><i class="fas fa-search"></i> Details</a> <a class="btn btn-primary" href="{% url 'backoffice:chain_detail' camp_slug=camp.slug chain_slug=chain.slug %}"><i class="fas fa-search"></i> Details</a>

View file

@ -7,7 +7,7 @@ Details for Credebtor {{ credebtor.name }} (Chain {{ credebtor.chain.name }}) |
{% block content %} {% block content %}
<h2>Details for Credebtor {{ credebtor.name }} (Chain {{ credebtor.chain.name }})</h2> <h2>Details for Credebtor {{ credebtor.name }} (Chain {{ credebtor.chain.name }})</h2>
<a href="{% url "backoffice:chain_detail" camp_slug=camp.slug chain_slug=credebtor.chain.slug %}">Back to Credebtor list</a> <a class="btn btn-default" href="{% url "backoffice:chain_detail" camp_slug=camp.slug chain_slug=credebtor.chain.slug %}"><i class="fas fa-undo"></i> Back to Credebtor list</a>
<h3>{{ credebtor.expenses.count }} Expenses for Credebtor {{ credebtor.name }}</h3> <h3>{{ credebtor.expenses.count }} Expenses for Credebtor {{ credebtor.name }}</h3>
{% include 'includes/expense_list_panel.html' with expense_list=credebtor.expenses.all %} {% include 'includes/expense_list_panel.html' with expense_list=credebtor.expenses.all %}

View file

@ -16,8 +16,8 @@ class ChainAdmin(admin.ModelAdmin):
@admin.register(Credebtor) @admin.register(Credebtor)
class CredebtorAdmin(admin.ModelAdmin): class CredebtorAdmin(admin.ModelAdmin):
list_filter = ["chain", "name"] list_filter = ["chain", "name"]
list_display = ["chain", "name", "notes"] list_display = ["name", "chain", "address", "notes"]
search_fields = ["chain", "name", "notes"] search_fields = ["chain", "name", "address", "notes"]
############################### ###############################

View file

@ -20,12 +20,17 @@ from .email import (
class ChainManager(models.Manager): class ChainManager(models.Manager):
""" """
ChainManager adds 'expenses_total' and 'revenues_total' to the Chain qs ChainManager adds 'expenses_total' and 'revenues_total' to the Chain qs
Also adds 'expenses_count' and 'revenues_count' and prefetches all expenses
and revenues for the credebtors.
""" """
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
qs = qs.prefetch_related("credebtors__expenses", "credebtors__revenues")
qs = qs.annotate(expenses_total=models.Sum("credebtors__expenses__amount")) qs = qs.annotate(expenses_total=models.Sum("credebtors__expenses__amount"))
qs = qs.annotate(expenses_count=models.Count("credebtors__expenses", distinct=True))
qs = qs.annotate(revenues_total=models.Sum("credebtors__revenues__amount")) qs = qs.annotate(revenues_total=models.Sum("credebtors__revenues__amount"))
qs = qs.annotate(revenues_count=models.Count("credebtors__revenues", distinct=True))
return qs return qs
@ -81,11 +86,13 @@ class Chain(CreatedUpdatedModel, UUIDModel):
class CredebtorManager(models.Manager): class CredebtorManager(models.Manager):
""" """
CredebtorManager adds 'expenses_total' and 'revenues_total' to the Credebtor qs CredebtorManager adds 'expenses_total' and 'revenues_total' to the Credebtor qs,
and prefetches expenses and revenues for the credebtor(s).
""" """
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
qs = qs.prefetch_related("expenses", "revenues")
qs = qs.annotate(expenses_total=models.Sum("expenses__amount")) qs = qs.annotate(expenses_total=models.Sum("expenses__amount"))
qs = qs.annotate(revenues_total=models.Sum("revenues__amount")) qs = qs.annotate(revenues_total=models.Sum("revenues__amount"))
return qs return qs

View file

@ -1,5 +1,5 @@
{% if expense_list %} {% if expense_list %}
<table class="table table-hover"> <table class="table table-hover datatable">
<thead> <thead>
<tr> <tr>
<th>Invoice Date</th> <th>Invoice Date</th>

View file

@ -12,6 +12,7 @@ from django.contrib.auth.models import User
from django.contrib.gis.geos import Point from django.contrib.gis.geos import Point
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.core.management import call_command
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.utils import timezone from django.utils import timezone
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
@ -52,13 +53,65 @@ from tickets.models import TicketType
from tokens.models import Token, TokenFind from tokens.models import Token, TokenFind
from utils.slugs import unique_slugify from utils.slugs import unique_slugify
from villages.models import Village from villages.models import Village
from economy.models import Chain, Credebtor, Expense, Revenue
fake = Faker() fake = Faker()
tz = pytz.timezone("Europe/Copenhagen") tz = pytz.timezone("Europe/Copenhagen")
logger = logging.getLogger("bornhack.%s" % __name__) logger = logging.getLogger("bornhack.%s" % __name__)
@factory.django.mute_signals(post_save) class ChainFactory(factory.django.DjangoModelFactory):
class Meta:
model = Chain
name = factory.Faker("company")
slug = factory.LazyAttribute(lambda f: unique_slugify(f.name, Chain.objects.all().values_list("slug", flat=True)))
class CredebtorFactory(factory.django.DjangoModelFactory):
class Meta:
model = Credebtor
chain = factory.SubFactory(ChainFactory)
name = factory.Faker("company")
slug = factory.LazyAttribute(lambda f: unique_slugify(f.name, Credebtor.objects.all().values_list("slug", flat=True)))
address = factory.Faker("address", locale="dk_DK")
notes = factory.Faker("text")
class ExpenseFactory(factory.django.DjangoModelFactory):
class Meta:
model = Expense
camp = factory.Faker("random_element", elements=Camp.objects.all())
creditor = factory.Faker("random_element", elements=Credebtor.objects.all())
user = factory.Faker("random_element", elements=User.objects.all())
amount = factory.Faker("random_int", min=20, max=20000)
description = factory.Faker("text")
paid_by_bornhack = factory.Faker("random_element", elements=[True, True, False])
invoice = factory.django.ImageField(color=random.choice(['#ff0000', '#00ff00', '#0000ff']))
invoice_date = factory.Faker("date")
responsible_team = factory.Faker("random_element", elements=Team.objects.all())
approved = factory.Faker("random_element", elements=[True, True, False])
notes = factory.Faker("text")
class RevenueFactory(factory.django.DjangoModelFactory):
class Meta:
model = Revenue
camp = factory.Faker("random_element", elements=Camp.objects.all())
debtor = factory.Faker("random_element", elements=Credebtor.objects.all())
user = factory.Faker("random_element", elements=User.objects.all())
amount = factory.Faker("random_int", min=20, max=20000)
description = factory.Faker("text")
invoice = factory.django.ImageField(color=random.choice(['#ff0000', '#00ff00', '#0000ff']))
invoice_date = factory.Faker("date")
responsible_team = factory.Faker("random_element", elements=Team.objects.all())
approved = factory.Faker("random_element", elements=[True, True, False])
notes = factory.Faker("text")
class ProfileFactory(factory.django.DjangoModelFactory): class ProfileFactory(factory.django.DjangoModelFactory):
class Meta: class Meta:
model = Profile model = Profile
@ -159,8 +212,9 @@ class Command(BaseCommand):
dict(year=2017, tagline="Make Tradition", colour="#750787", read_only=True), dict(year=2017, tagline="Make Tradition", colour="#750787", read_only=True),
dict(year=2018, tagline="scale it", colour="#008026", read_only=True), dict(year=2018, tagline="scale it", colour="#008026", read_only=True),
dict(year=2019, tagline="a new /home", colour="#ffed00", read_only=True), dict(year=2019, tagline="a new /home", colour="#ffed00", read_only=True),
dict(year=2020, tagline="Going Viral", colour="#ff8c00", read_only=False), dict(year=2020, tagline="Make Clean", colour="#ff8c00", read_only=False),
dict(year=2021, tagline="Undecided", colour="#e40303", read_only=False), dict(year=2021, tagline="Undecided", colour="#e40303", read_only=False),
dict(year=2022, tagline="Undecided", colour="#004dff", read_only=False),
] ]
camp_instances = [] camp_instances = []
@ -508,6 +562,21 @@ class Command(BaseCommand):
name="Recording", defaults={"icon": "fas fa-film"} name="Recording", defaults={"icon": "fas fa-film"}
) )
def create_credebtors(self):
self.output("Creating Chains and Credebtors...")
try:
CredebtorFactory.create_batch(50)
except ValidationError:
self.outout("Name conflict, retrying...")
CredebtorFactory.create_batch(50)
for _ in range(20):
# add 20 more credebtors to random existing chains
try:
CredebtorFactory.create(chain=Chain.objects.order_by("?").first())
except ValidationError:
self.outout("Name conflict, skipping...")
continue
def create_product_categories(self): def create_product_categories(self):
categories = {} categories = {}
self.output("Creating productcategories...") self.output("Creating productcategories...")
@ -686,7 +755,7 @@ class Command(BaseCommand):
orders = {} orders = {}
self.output("Creating orders...") self.output("Creating orders...")
orders[0] = Order.objects.create( orders[0] = Order.objects.create(
user=users[1], payment_method="cash", open=None, paid=True user=users[1], payment_method="in_person", open=None, paid=True
) )
orders[0].orderproductrelation_set.create( orders[0].orderproductrelation_set.create(
product=camp_products["ticket1"], quantity=1 product=camp_products["ticket1"], quantity=1
@ -697,7 +766,7 @@ class Command(BaseCommand):
orders[0].mark_as_paid(request=None) orders[0].mark_as_paid(request=None)
orders[1] = Order.objects.create( orders[1] = Order.objects.create(
user=users[2], payment_method="cash", open=None user=users[2], payment_method="in_person", open=None
) )
orders[1].orderproductrelation_set.create( orders[1].orderproductrelation_set.create(
product=camp_products["ticket1"], quantity=1 product=camp_products["ticket1"], quantity=1
@ -708,7 +777,7 @@ class Command(BaseCommand):
orders[1].mark_as_paid(request=None) orders[1].mark_as_paid(request=None)
orders[2] = Order.objects.create( orders[2] = Order.objects.create(
user=users[3], payment_method="cash", open=None user=users[3], payment_method="in_person", open=None
) )
orders[2].orderproductrelation_set.create( orders[2].orderproductrelation_set.create(
product=camp_products["ticket2"], quantity=1 product=camp_products["ticket2"], quantity=1
@ -722,7 +791,7 @@ class Command(BaseCommand):
orders[2].mark_as_paid(request=None) orders[2].mark_as_paid(request=None)
orders[3] = Order.objects.create( orders[3] = Order.objects.create(
user=users[4], payment_method="cash", open=None user=users[4], payment_method="in_person", open=None
) )
orders[3].orderproductrelation_set.create( orders[3].orderproductrelation_set.create(
product=global_products["product0"], quantity=1 product=global_products["product0"], quantity=1
@ -1189,6 +1258,13 @@ class Command(BaseCommand):
mailing_list="content@example.com", mailing_list="content@example.com",
permission_set="contentteam_permission", permission_set="contentteam_permission",
) )
teams["economy"] = Team.objects.create(
name="Economy",
description="The Economy Team handles the money and accounts.",
camp=camp,
mailing_list="economy@example.com",
permission_set="economyteam_permission",
)
return teams return teams
@ -1600,6 +1676,14 @@ class Command(BaseCommand):
for i in range(0, 6): for i in range(0, 6):
TokenFind.objects.create(token=tokens[i], user=users[1]) TokenFind.objects.create(token=tokens[i], user=users[1])
def create_camp_expenses(self, camp):
self.output(f"Creating expenses for {camp}...")
ExpenseFactory.create_batch(200, camp=camp)
def create_camp_revenues(self, camp):
self.output(f"Creating revenues for {camp}...")
RevenueFactory.create_batch(20, camp=camp)
def output(self, message): def output(self, message):
self.stdout.write( self.stdout.write(
"%s: %s" % (timezone.now().strftime("%Y-%m-%d %H:%M:%S"), message) "%s: %s" % (timezone.now().strftime("%Y-%m-%d %H:%M:%S"), message)
@ -1610,6 +1694,13 @@ class Command(BaseCommand):
self.output( self.output(
self.style.SUCCESS("----------[ Running bootstrap-devsite ]----------") self.style.SUCCESS("----------[ Running bootstrap-devsite ]----------")
) )
self.output(
self.style.SUCCESS(
"----------[ Deleting all data from database ]----------"
)
)
call_command("flush", "--noinput")
self.output(self.style.SUCCESS("----------[ Global stuff ]----------")) self.output(self.style.SUCCESS("----------[ Global stuff ]----------"))
camps = self.create_camps() camps = self.create_camps()
@ -1628,6 +1719,8 @@ class Command(BaseCommand):
quickfeedback_options = self.create_quickfeedback_options() quickfeedback_options = self.create_quickfeedback_options()
self.create_credebtors()
for (camp, read_only) in camps: for (camp, read_only) in camps:
year = camp.camp.lower.year year = camp.camp.lower.year
@ -1635,7 +1728,7 @@ class Command(BaseCommand):
self.style.SUCCESS("----------[ Bornhack {} ]----------".format(year)) self.style.SUCCESS("----------[ Bornhack {} ]----------".format(year))
) )
if year < 2021: if year < 2022:
ticket_types = self.create_camp_ticket_types(camp) ticket_types = self.create_camp_ticket_types(camp)
camp_products = self.create_camp_products( camp_products = self.create_camp_products(
@ -1716,6 +1809,10 @@ class Command(BaseCommand):
tokens = self.create_camp_tokens(camp) tokens = self.create_camp_tokens(camp)
self.create_camp_token_finds(camp, tokens, users) self.create_camp_token_finds(camp, tokens, users)
self.create_camp_expenses(camp)
self.create_camp_revenues(camp)
else: else:
self.output("Not creating anything for this year yet") self.output("Not creating anything for this year yet")