From fbda2b4b53dc2cb266d1d7c13ba0aad59d9079df Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sat, 17 Oct 2020 02:35:12 +0200 Subject: [PATCH] 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 --- .../templates/chain_detail_backoffice.html | 84 ++++++---- .../templates/chain_list_backoffice.html | 78 ++++----- .../credebtor_detail_backoffice.html | 17 +- src/backoffice/views.py | 155 ++++++++++++++++-- src/economy/models.py | 44 +++-- .../0064_add_product_comment_and_cost.py | 28 ++++ src/shop/models.py | 11 +- .../management/commands/bootstrap_devsite.py | 38 ++++- 8 files changed, 339 insertions(+), 116 deletions(-) create mode 100644 src/shop/migrations/0064_add_product_comment_and_cost.py diff --git a/src/backoffice/templates/chain_detail_backoffice.html b/src/backoffice/templates/chain_detail_backoffice.html index 814bb7c3..06c1c8e3 100644 --- a/src/backoffice/templates/chain_detail_backoffice.html +++ b/src/backoffice/templates/chain_detail_backoffice.html @@ -6,44 +6,56 @@ Details for Chain {{ chain.name }} | {{ block.super }} {% endblock %} {% block content %} -

Details for Chain {{ chain.name }}

- Back to Chain list +
+

Details for Chain {{ chain.name }} - BackOffice

+
+

This chain has {{ chain.camp_expenses_count }} expenses for a total of {{ chain.camp_expenses_amount }} DKK and {{ chain.camp_revenues_count }} revenues for a total of {{ chain.camp_revenues_amount }} DKK for {{ camp.title }}.

+

This chain has {{ chain.all_expenses_count }} expenses for a total of {{ chain.all_expenses_amount }} DKK and {{ chain.all_revenues_count }} revenues for a total of {{ chain.all_revenues_amount }} DKK across all camps.

+ Back to Chain list -

{{ chain.credebtors.count }} Credebtors for Chain {{ chain.name }}

- - - - - - - - - - - - - - - {% for credebtor in chain.credebtors.all %} - - - - - - - - - -
Credebtor NameAddressNotesExpensesExpenses TotalRevenuesRevenues TotalActions
{{ credebtor.name }}
{{ credebtor.address }}
{{ credebtor.notes|default:"N/A" }}{{ credebtor.expenses.count }}{{ credebtor.expenses_total }} DKK{{ credebtor.revenues.count }}{{ credebtor.revenues_total }} DKK - Details - {% endfor %} -
+
-

{{ chain.expenses_count }} Expenses for Chain {{ chain.name }}

-{% include 'includes/expense_list_panel.html' with expense_list=chain.expenses.all %} +

{{ chain.credebtors.count }} Credebtors for Chain {{ chain.name }}

+ + + + + + + + + + + + + + + {% for credebtor in credebtors %} + + + + + + + + + +
Credebtor NameAddressNotesExpensesExpenses TotalRevenuesRevenues TotalActions
{{ credebtor.name }}
{{ credebtor.address }}
{{ credebtor.notes|default:"N/A" }}{{ credebtor.camp_expenses_count }}{{ credebtor.camp_expenses_amount }} DKK{{ credebtor.camp_revenues_count }}{{ credebtor.camp_revenues_amount }} DKK + Details + {% endfor %} +
-

{{ chain.revenues_count }} Revenues for Chain {{ chain.name }}

-{% include 'includes/revenue_list_panel.html' with revenue_list=chain.revenues.all %} +
+ +

{{ expenses.count }} Expenses for Chain {{ chain.name }}

+ {% include 'includes/expense_list_panel.html' with expense_list=expenses %} + +
+ +

{{ revenues.count }} Revenues for Chain {{ chain.name }}

+ {% include 'includes/revenue_list_panel.html' with revenue_list=revenues %} + +
+
{% endblock content %} - diff --git a/src/backoffice/templates/chain_list_backoffice.html b/src/backoffice/templates/chain_list_backoffice.html index b075cc08..3c6fc6f0 100644 --- a/src/backoffice/templates/chain_list_backoffice.html +++ b/src/backoffice/templates/chain_list_backoffice.html @@ -7,42 +7,46 @@ Select Chain | {{ block.super }} {% endblock %} {% block content %} -

Chains

-

Back to Backoffice Index

- -{% if chain_list %} - - - - - - - - - - - - - - - {% for chain in chain_list %} - - - - - - - - - - - {% endfor %} - -
Chain NameNotesCredebtorsExpensesExpenses TotalRevenuesRevenues TotalActions
{{ chain.name }}{{ chain.notes|default:"N/A" }}{{ chain.credebtors.count }}{{ chain.expenses_count }}{{ chain.expenses_total|default:"0" }} DKK{{ chain.revenues_count }}{{ chain.revenues_total|default:"0" }} DKK - Details -
-{% else %} -

No Chains found.

-{% endif %} +
+

Chains - BackOffice

+
+

Showing {{ chain_list.count }} chains. Not all chains have expenses or revenues for {{ camp.title }}.

+

Back to Backoffice Index

+ {% if chain_list %} + + + + + + + + + + + + + + + {% for chain in chain_list %} + + + + + + + + + + + {% endfor %} + +
Chain NameNotesCredebtorsExpensesExpenses TotalRevenuesRevenues TotalActions
{{ chain.name }}{{ chain.notes|default:"N/A" }}{{ chain.credebtors.count }}{{ chain.camp_expenses_count }}{{ chain.camp_expenses_amount|default:"0" }} DKK{{ chain.camp_revenues_count }}{{ chain.camp_revenues_amount|default:"0" }} DKK + Details +
+ {% else %} +

No Chains found.

+ {% endif %} +
+
{% endblock %} diff --git a/src/backoffice/templates/credebtor_detail_backoffice.html b/src/backoffice/templates/credebtor_detail_backoffice.html index 1d2151f2..9dd61f67 100644 --- a/src/backoffice/templates/credebtor_detail_backoffice.html +++ b/src/backoffice/templates/credebtor_detail_backoffice.html @@ -6,14 +6,17 @@ Details for Credebtor {{ credebtor.name }} (Chain {{ credebtor.chain.name }}) | {% endblock %} {% block content %} -

Details for Credebtor {{ credebtor.name }} (Chain {{ credebtor.chain.name }})

- Back to Credebtor list +
+

Details for Credebtor {{ credebtor.name }} (Chain {{ credebtor.chain.name }}) - BackOffice

+
+ Back to Credebtor list for {{ credebtor.chain.name }} -

{{ credebtor.expenses.count }} Expenses for Credebtor {{ credebtor.name }}

-{% include 'includes/expense_list_panel.html' with expense_list=credebtor.expenses.all %} - -

{{ credebtor.revenues.count }} Revenues for Credebtor {{ credebtor.name }}

-{% include 'includes/revenue_list_panel.html' with revenue_list=credebtor.revenues.all %} +

{{ expenses.count }} Expenses for Credebtor {{ credebtor.name }}

+ {% include 'includes/expense_list_panel.html' with expense_list=expenses %} +

{{ revenues.count }} Revenues for Credebtor {{ credebtor.name }}

+ {% include 'includes/revenue_list_panel.html' with revenue_list=revenues %} +
+
{% endblock content %} diff --git a/src/backoffice/views.py b/src/backoffice/views.py index 0c77c4b5..248a3427 100644 --- a/src/backoffice/views.py +++ b/src/backoffice/views.py @@ -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,15 +925,16 @@ 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}) class EventScheduleView(CampViewMixin, ContentTeamPermissionMixin, FormView): - """ This view is used by the Content Team to manually schedule Events. + """This view is used by the Content Team to manually schedule Events. It shows a table with radioselect buttons for the available slots for the - EventType of the Event """ + EventType of the Event""" form_class = EventScheduleForm template_name = "event_schedule.html" @@ -1259,9 +1270,9 @@ class AutoScheduleCrashCourseView( class AutoScheduleValidateView(CampViewMixin, ContentTeamPermissionMixin, FormView): - """ This view is used to validate schedules. It uses the AutoScheduler and can + """This view is used to validate schedules. It uses the AutoScheduler and can either validate the currently applied schedule or a new similar schedule, or a - brand new schedule """ + brand new schedule""" template_name = "autoschedule_validate.html" form_class = AutoScheduleValidateForm @@ -1314,7 +1325,7 @@ class AutoScheduleDiffView(CampViewMixin, ContentTeamPermissionMixin, TemplateVi class AutoScheduleApplyView(CampViewMixin, ContentTeamPermissionMixin, FormView): - """ This view is used by the Content Team to apply a new schedules by unscheduling + """This view is used by the Content Team to apply a new schedules by unscheduling all autoscheduled Events, and scheduling all Event/Slot combinations in the schedule. TODO: see comment in program.autoscheduler.AutoScheduler.apply() method. @@ -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" diff --git a/src/economy/models.py b/src/economy/models.py index fb604359..a729cf0e 100644 --- a/src/economy/models.py +++ b/src/economy/models.py @@ -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( diff --git a/src/shop/migrations/0064_add_product_comment_and_cost.py b/src/shop/migrations/0064_add_product_comment_and_cost.py new file mode 100644 index 00000000..f6fd2e1e --- /dev/null +++ b/src/shop/migrations/0064_add_product_comment_and_cost.py @@ -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.", + ), + ), + ] diff --git a/src/shop/models.py b/src/shop/models.py index 6381d39d..2b689d19 100644 --- a/src/shop/models.py +++ b/src/shop/models.py @@ -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): @@ -442,7 +451,7 @@ class Product(CreatedUpdatedModel, UUIDModel): raise ValidationError("Products with category Tickets need a ticket_type") def is_available(self): - """ Is the product available or not? + """Is the product available or not? Checks for the following: diff --git a/src/utils/management/commands/bootstrap_devsite.py b/src/utils/management/commands/bootstrap_devsite.py index a14dcba3..e64f087b 100644 --- a/src/utils/management/commands/bootstrap_devsite.py +++ b/src/utils/management/commands/bootstrap_devsite.py @@ -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