more work on backoffice economy stuff, chains and credebtors section now only shows expenses and revenues for the current camp, thanks to some ORM foo; while here add a cost and a comment field to the Product table, and fix a bug in bootstrap_devsite and a few nags to make new flake8 happy

This commit is contained in:
Thomas Steen Rasmussen 2020-10-17 02:35:12 +02:00
parent 833fc85e46
commit fbda2b4b53
8 changed files with 339 additions and 116 deletions

View file

@ -6,9 +6,15 @@ Details for Chain {{ chain.name }} | {{ block.super }}
{% endblock %}
{% block content %}
<h2>Details for Chain {{ chain.name }}</h2>
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title">Details for Chain {{ chain.name }} - BackOffice</h3></div>
<div class="panel-body">
<p class="lead">This chain has <b>{{ chain.camp_expenses_count }} expenses</b> for a total of <b>{{ chain.camp_expenses_amount }} DKK</b> and <b>{{ chain.camp_revenues_count }} revenues</b> for a total of <b>{{ chain.camp_revenues_amount }} DKK</b> for {{ camp.title }}.</p>
<p class="lead">This chain has <b>{{ chain.all_expenses_count }} expenses</b> for a total of <b>{{ chain.all_expenses_amount }} DKK</b> and <b>{{ chain.all_revenues_count }} revenues</b> for a total of <b>{{ chain.all_revenues_amount }} DKK</b> across all camps.</p>
<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>
<hr>
<h3>{{ chain.credebtors.count }} Credebtors for Chain {{ chain.name }}</h3>
<table class="table table-hover">
<thead>
@ -24,26 +30,32 @@ Details for Chain {{ chain.name }} | {{ block.super }}
</tr>
</thead>
<tbody>
{% for credebtor in chain.credebtors.all %}
{% for credebtor in credebtors %}
<tr>
<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>{{ credebtor.notes|default:"N/A" }}</td>
<td class="text-center"><span class="badge">{{ credebtor.expenses.count }}</span></td>
<td class="text-center">{{ credebtor.expenses_total }} DKK</td>
<td class="text-center"><span class="badge">{{ credebtor.revenues.count }}</span></td>
<td class="text-center">{{ credebtor.revenues_total }} DKK</td>
<td class="text-center"><span class="badge">{{ credebtor.camp_expenses_count }}</span></td>
<td class="text-center">{{ credebtor.camp_expenses_amount }} DKK</td>
<td class="text-center"><span class="badge">{{ credebtor.camp_revenues_count }}</span></td>
<td class="text-center">{{ credebtor.camp_revenues_amount }} DKK</td>
<td>
<a class="btn btn-primary" href="{% url 'backoffice:credebtor_detail' camp_slug=camp.slug chain_slug=chain.slug credebtor_slug=credebtor.slug %}"><i class="fas fa-search"></i> Details</a>
{% endfor %}
</tbody>
</table>
<h3>{{ chain.expenses_count }} Expenses for Chain {{ chain.name }}</h3>
{% include 'includes/expense_list_panel.html' with expense_list=chain.expenses.all %}
<hr>
<h3>{{ chain.revenues_count }} Revenues for Chain {{ chain.name }}</h3>
{% include 'includes/revenue_list_panel.html' with revenue_list=chain.revenues.all %}
<h3>{{ expenses.count }} Expenses for Chain {{ chain.name }}</h3>
{% include 'includes/expense_list_panel.html' with expense_list=expenses %}
<hr>
<h3>{{ revenues.count }} Revenues for Chain {{ chain.name }}</h3>
{% include 'includes/revenue_list_panel.html' with revenue_list=revenues %}
</div>
</div>
{% endblock content %}

View file

@ -7,7 +7,10 @@ Select Chain | {{ block.super }}
{% endblock %}
{% block content %}
<h2>Chains</h2>
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title">Chains - BackOffice</h3></div>
<div class="panel-body">
<p class="lead">Showing {{ chain_list.count }} chains. Not all chains have expenses or revenues for {{ camp.title }}.</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 %}
@ -30,10 +33,10 @@ Select Chain | {{ block.super }}
<td>{{ chain.name }}</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.expenses_count }}</span></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">{{ chain.revenues_total|default:"0" }} DKK</td>
<td class="text-center"><span class="badge">{{ chain.camp_expenses_count }}</span></td>
<td class="text-center">{{ chain.camp_expenses_amount|default:"0" }} DKK</td>
<td class="text-center"><span class="badge">{{ chain.camp_revenues_count }}</span></td>
<td class="text-center">{{ chain.camp_revenues_amount|default:"0" }} DKK</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>
</td>
@ -44,5 +47,6 @@ Select Chain | {{ block.super }}
{% else %}
<p class="lead"><i>No Chains found.</i></p>
{% endif %}
</div>
</div>
{% endblock %}

View file

@ -6,14 +6,17 @@ Details for Credebtor {{ credebtor.name }} (Chain {{ credebtor.chain.name }}) |
{% endblock %}
{% block content %}
<h2>Details for Credebtor {{ credebtor.name }} (Chain {{ credebtor.chain.name }})</h2>
<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>
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title">Details for Credebtor {{ credebtor.name }} (Chain {{ credebtor.chain.name }}) - BackOffice</h3></div>
<div class="panel-body">
<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 for {{ credebtor.chain.name }}</a>
<h3>{{ credebtor.expenses.count }} Expenses for Credebtor {{ credebtor.name }}</h3>
{% include 'includes/expense_list_panel.html' with expense_list=credebtor.expenses.all %}
<h3>{{ credebtor.revenues.count }} Revenues for Credebtor {{ credebtor.name }}</h3>
{% include 'includes/revenue_list_panel.html' with revenue_list=credebtor.revenues.all %}
<h3>{{ expenses.count }} Expenses for Credebtor {{ credebtor.name }}</h3>
{% include 'includes/expense_list_panel.html' with expense_list=expenses %}
<h3>{{ revenues.count }} Revenues for Credebtor {{ credebtor.name }}</h3>
{% include 'includes/revenue_list_panel.html' with revenue_list=revenues %}
</div>
</div>
{% endblock content %}

View file

@ -298,7 +298,11 @@ class FacilityCreateView(CampViewMixin, OrgaTeamPermissionMixin, CreateView):
def get_form(self, *args, **kwargs):
form = super().get_form(*args, **kwargs)
form.fields["location"].widget = LeafletWidget(attrs={"display_raw": "true",})
form.fields["location"].widget = LeafletWidget(
attrs={
"display_raw": "true",
}
)
return form
def get_context_data(self, **kwargs):
@ -324,7 +328,11 @@ class FacilityUpdateView(CampViewMixin, OrgaTeamPermissionMixin, UpdateView):
def get_form(self, *args, **kwargs):
form = super().get_form(*args, **kwargs)
form.fields["location"].widget = LeafletWidget(attrs={"display_raw": "true",})
form.fields["location"].widget = LeafletWidget(
attrs={
"display_raw": "true",
}
)
return form
def get_success_url(self):
@ -445,7 +453,7 @@ class FacilityOpeningHoursCreateView(
hours = form.save(commit=False)
hours.facility = self.facility
hours.save()
messages.success(self.request, f"New opening hours created successfully!")
messages.success(self.request, "New opening hours created successfully!")
return redirect(
reverse(
"backoffice:facility_detail",
@ -547,7 +555,9 @@ class SpeakerProposalListView(CampViewMixin, ContentTeamPermissionMixin, ListVie
class SpeakerProposalDetailView(
AvailabilityMatrixViewMixin, ContentTeamPermissionMixin, DetailView,
AvailabilityMatrixViewMixin,
ContentTeamPermissionMixin,
DetailView,
):
""" This view permits Content Team members to see SpeakerProposal details """
@ -915,7 +925,8 @@ class EventDeleteView(CampViewMixin, ContentTeamPermissionMixin, DeleteView):
def get_success_url(self):
messages.success(
self.request, f"Event '{self.get_object().title}' has been deleted!",
self.request,
f"Event '{self.get_object().title}' has been deleted!",
)
return reverse("backoffice:event_list", kwargs={"camp_slug": self.camp.slug})
@ -1481,18 +1492,118 @@ class ChainListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
model = Chain
template_name = "chain_list_backoffice.html"
def get_queryset(self, *args, **kwargs):
"""Annotate the total count and amount for expenses and revenues for all credebtors in each chain."""
qs = Chain.objects.annotate(
camp_expenses_amount=Sum(
"credebtors__expenses__amount",
filter=Q(credebtors__expenses__camp=self.camp),
distinct=True,
),
camp_expenses_count=Count(
"credebtors__expenses",
filter=Q(credebtors__expenses__camp=self.camp),
distinct=True,
),
camp_revenues_amount=Sum(
"credebtors__revenues__amount",
filter=Q(credebtors__revenues__camp=self.camp),
distinct=True,
),
camp_revenues_count=Count(
"credebtors__revenues",
filter=Q(credebtors__revenues__camp=self.camp),
distinct=True,
),
)
return qs
class ChainDetailView(CampViewMixin, EconomyTeamPermissionMixin, DetailView):
model = Chain
template_name = "chain_detail_backoffice.html"
slug_url_kwarg = "chain_slug"
def get_queryset(self, *args, **kwargs):
"""Annotate the Chain object with the camp filtered expense and revenue info."""
qs = super().get_queryset(*args, **kwargs)
qs = qs.annotate(
camp_expenses_amount=Sum(
"credebtors__expenses__amount",
filter=Q(credebtors__expenses__camp=self.camp),
distinct=True,
),
camp_expenses_count=Count(
"credebtors__expenses",
filter=Q(credebtors__expenses__camp=self.camp),
distinct=True,
),
camp_revenues_amount=Sum(
"credebtors__revenues__amount",
filter=Q(credebtors__revenues__camp=self.camp),
distinct=True,
),
camp_revenues_count=Count(
"credebtors__revenues",
filter=Q(credebtors__revenues__camp=self.camp),
distinct=True,
),
)
return qs
def get_context_data(self, *args, **kwargs):
"""Add credebtors, expenses and revenues to the context in camp-filtered versions."""
context = super().get_context_data(*args, **kwargs)
# include credebtors as a seperate queryset with annotations for total number and
# amount of expenses and revenues
context["credebtors"] = Credebtor.objects.filter(
chain=self.get_object()
).annotate(
camp_expenses_amount=Sum(
"expenses__amount", filter=Q(expenses__camp=self.camp), distinct=True
),
camp_expenses_count=Count(
"expenses", filter=Q(expenses__camp=self.camp), distinct=True
),
camp_revenues_amount=Sum(
"revenues__amount", filter=Q(revenues__camp=self.camp), distinct=True
),
camp_revenues_count=Count(
"revenues", filter=Q(revenues__camp=self.camp), distinct=True
),
)
# Include expenses and revenues for the Chain in context as seperate querysets,
# since accessing them through the relatedmanager returns for all camps
context["expenses"] = Expense.objects.filter(
camp=self.camp, creditor__chain=self.get_object()
).prefetch_related("responsible_team", "user", "creditor")
context["revenues"] = Revenue.objects.filter(
camp=self.camp, debtor__chain=self.get_object()
).prefetch_related("responsible_team", "user", "debtor")
return context
class CredebtorDetailView(CampViewMixin, EconomyTeamPermissionMixin, DetailView):
model = Credebtor
template_name = "credebtor_detail_backoffice.html"
slug_url_kwarg = "credebtor_slug"
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context["expenses"] = (
self.get_object()
.expenses.filter(camp=self.camp)
.prefetch_related("responsible_team", "user", "creditor")
)
context["revenues"] = (
self.get_object()
.revenues.filter(camp=self.camp)
.prefetch_related("responsible_team", "user", "debtor")
)
return context
################################
# EXPENSES
@ -1507,7 +1618,11 @@ class ExpenseListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
Exclude unapproved expenses, they are shown seperately
"""
queryset = super().get_queryset(**kwargs)
return queryset.exclude(approved__isnull=True)
return queryset.exclude(approved__isnull=True).prefetch_related(
"creditor",
"user",
"responsible_team",
)
def get_context_data(self, **kwargs):
"""
@ -1516,6 +1631,10 @@ class ExpenseListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
context = super().get_context_data(**kwargs)
context["unapproved_expenses"] = Expense.objects.filter(
camp=self.camp, approved__isnull=True
).prefetch_related(
"creditor",
"user",
"responsible_team",
)
return context
@ -1747,7 +1866,11 @@ class RevenueListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
Exclude unapproved revenues, they are shown seperately
"""
queryset = super().get_queryset(**kwargs)
return queryset.exclude(approved__isnull=True)
return queryset.exclude(approved__isnull=True).prefetch_related(
"debtor",
"user",
"responsible_team",
)
def get_context_data(self, **kwargs):
"""
@ -2040,7 +2163,8 @@ class PosReportCreateView(PosViewMixin, CreateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["form"].fields["bank_responsible"].queryset = Team.objects.get(
camp=self.camp, name="Orga",
camp=self.camp,
name="Orga",
).approved_members.all()
context["form"].fields[
"pos_responsible"
@ -2054,7 +2178,7 @@ class PosReportCreateView(PosViewMixin, CreateView):
pr = form.save(commit=False)
pr.pos = self.pos
pr.save()
messages.success(self.request, f"New PosReport created successfully!")
messages.success(self.request, "New PosReport created successfully!")
return redirect(
reverse(
"backoffice:posreport_detail",
@ -2086,7 +2210,8 @@ class PosReportUpdateView(PosViewMixin, UpdateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["form"].fields["bank_responsible"].queryset = Team.objects.get(
camp=self.camp, name="Orga",
camp=self.camp,
name="Orga",
).approved_members.all()
context["form"].fields[
"pos_responsible"

View file

@ -26,11 +26,26 @@ class ChainManager(models.Manager):
def get_queryset(self):
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_count=models.Count("credebtors__expenses", distinct=True))
qs = qs.annotate(revenues_total=models.Sum("credebtors__revenues__amount"))
qs = qs.annotate(revenues_count=models.Count("credebtors__revenues", distinct=True))
qs = qs.prefetch_related(
models.Prefetch("credebtors__expenses", to_attr="all_expenses"),
models.Prefetch("credebtors__revenues", to_attr="all_revenues"),
)
qs = qs.annotate(
all_expenses_amount=models.Sum(
"credebtors__expenses__amount", distinct=True
)
)
qs = qs.annotate(
all_expenses_count=models.Count("credebtors__expenses", distinct=True)
)
qs = qs.annotate(
all_revenues_amount=models.Sum(
"credebtors__revenues__amount", distinct=True
)
)
qs = qs.annotate(
all_revenues_count=models.Count("credebtors__revenues", distinct=True)
)
return qs
@ -92,9 +107,12 @@ class CredebtorManager(models.Manager):
def get_queryset(self):
qs = super().get_queryset()
qs = qs.prefetch_related("expenses", "revenues")
qs = qs.annotate(expenses_total=models.Sum("expenses__amount"))
qs = qs.annotate(revenues_total=models.Sum("revenues__amount"))
qs = qs.prefetch_related(
models.Prefetch("expenses", to_attr="all_expenses"),
models.Prefetch("revenues", to_attr="all_revenues"),
)
qs = qs.annotate(all_expenses_amount=models.Sum("expenses__amount"))
qs = qs.annotate(all_revenues_amount=models.Sum("revenues__amount"))
return qs
@ -494,7 +512,9 @@ class Pos(CampRelatedModel, UUIDModel):
)
team = models.ForeignKey(
"teams.Team", on_delete=models.PROTECT, help_text="The Team managning this POS",
"teams.Team",
on_delete=models.PROTECT,
help_text="The Team managning this POS",
)
def save(self, **kwargs):
@ -559,7 +579,8 @@ class PosReport(CampRelatedModel, UUIDModel):
)
comments = models.TextField(
blank=True, help_text="Any comments about this PosReport",
blank=True,
help_text="Any comments about this PosReport",
)
dkk_sales_izettle = models.PositiveIntegerField(
@ -567,7 +588,8 @@ class PosReport(CampRelatedModel, UUIDModel):
)
hax_sold_izettle = models.PositiveIntegerField(
default=0, help_text="The number of HAX sold through the iZettle from the POS",
default=0,
help_text="The number of HAX sold through the iZettle from the POS",
)
hax_sold_website = models.PositiveIntegerField(

View file

@ -0,0 +1,28 @@
# Generated by Django 3.1 on 2020-10-17 00:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("shop", "0063_auto_20200812_1732"),
]
operations = [
migrations.AddField(
model_name="product",
name="comment",
field=models.TextField(
blank=True, help_text="Internal comments for this product."
),
),
migrations.AddField(
model_name="product",
name="cost",
field=models.IntegerField(
default=0,
help_text="The cost for this product, including VAT. Used for profit calculations in the economy system.",
),
),
]

View file

@ -432,6 +432,15 @@ class Product(CreatedUpdatedModel, UUIDModel):
blank=True,
)
cost = models.IntegerField(
default=0,
help_text="The cost for this product, including VAT. Used for profit calculations in the economy system.",
)
comment = models.TextField(
blank=True, help_text="Internal comments for this product."
)
objects = ProductQuerySet.as_manager()
def __str__(self):

View file

@ -11,11 +11,12 @@ from camps.models import Camp
from django.contrib.auth.models import User
from django.contrib.gis.geos import Point
from django.core.exceptions import ValidationError
from django.core.management.base import BaseCommand
from django.core.management import call_command
from django.core.management.base import BaseCommand
from django.db.models.signals import post_save
from django.utils import timezone
from django.utils.crypto import get_random_string
from economy.models import Chain, Credebtor, Expense, Revenue
from events.models import Routing, Type
from facilities.models import (
Facility,
@ -53,7 +54,6 @@ from tickets.models import TicketType
from tokens.models import Token, TokenFind
from utils.slugs import unique_slugify
from villages.models import Village
from economy.models import Chain, Credebtor, Expense, Revenue
fake = Faker()
tz = pytz.timezone("Europe/Copenhagen")
@ -65,7 +65,11 @@ class ChainFactory(factory.django.DjangoModelFactory):
model = Chain
name = factory.Faker("company")
slug = factory.LazyAttribute(lambda f: unique_slugify(f.name, Chain.objects.all().values_list("slug", flat=True)))
slug = factory.LazyAttribute(
lambda f: unique_slugify(
f.name, Chain.objects.all().values_list("slug", flat=True)
)
)
class CredebtorFactory(factory.django.DjangoModelFactory):
@ -74,7 +78,11 @@ class CredebtorFactory(factory.django.DjangoModelFactory):
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)))
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")
@ -89,7 +97,9 @@ class ExpenseFactory(factory.django.DjangoModelFactory):
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 = 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])
@ -105,7 +115,9 @@ class RevenueFactory(factory.django.DjangoModelFactory):
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 = 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])
@ -567,7 +579,7 @@ class Command(BaseCommand):
try:
CredebtorFactory.create_batch(50)
except ValidationError:
self.outout("Name conflict, retrying...")
self.output("Name conflict, retrying...")
CredebtorFactory.create_batch(50)
for _ in range(20):
# add 20 more credebtors to random existing chains
@ -856,10 +868,18 @@ class Command(BaseCommand):
capacity=50,
)
locations["food_area"] = EventLocation.objects.create(
name="Food Area", slug="food-area", icon="utensils", camp=camp, capacity=50,
name="Food Area",
slug="food-area",
icon="utensils",
camp=camp,
capacity=50,
)
locations["infodesk"] = EventLocation.objects.create(
name="Infodesk", slug="infodesk", icon="info", camp=camp, capacity=20,
name="Infodesk",
slug="infodesk",
icon="info",
camp=camp,
capacity=20,
)
# add workshop room conflicts (the big root can not be used while either