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,11 +6,17 @@ Details for Chain {{ chain.name }} | {{ block.super }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h2>Details for Chain {{ chain.name }}</h2> <div class="panel panel-default">
<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> <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>
<h3>{{ chain.credebtors.count }} Credebtors for Chain {{ chain.name }}</h3> <hr>
<table class="table table-hover">
<h3>{{ chain.credebtors.count }} Credebtors for Chain {{ chain.name }}</h3>
<table class="table table-hover">
<thead> <thead>
<tr> <tr>
<th>Credebtor Name</th> <th>Credebtor Name</th>
@ -24,26 +30,32 @@ Details for Chain {{ chain.name }} | {{ block.super }}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for credebtor in chain.credebtors.all %} {% for credebtor in credebtors %}
<tr> <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><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.camp_expenses_count }}</span></td>
<td class="text-center">{{ credebtor.expenses_total }} DKK</td> <td class="text-center">{{ credebtor.camp_expenses_amount }} DKK</td>
<td class="text-center"><span class="badge">{{ credebtor.revenues.count }}</span></td> <td class="text-center"><span class="badge">{{ credebtor.camp_revenues_count }}</span></td>
<td class="text-center">{{ credebtor.revenues_total }} DKK</td> <td class="text-center">{{ credebtor.camp_revenues_amount }} DKK</td>
<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> <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 %} {% endfor %}
</tbody> </tbody>
</table> </table>
<h3>{{ chain.expenses_count }} Expenses for Chain {{ chain.name }}</h3> <hr>
{% include 'includes/expense_list_panel.html' with expense_list=chain.expenses.all %}
<h3>{{ chain.revenues_count }} Revenues for Chain {{ chain.name }}</h3> <h3>{{ expenses.count }} Expenses for Chain {{ chain.name }}</h3>
{% include 'includes/revenue_list_panel.html' with revenue_list=chain.revenues.all %} {% 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 %} {% endblock content %}

View file

@ -7,11 +7,14 @@ Select Chain | {{ block.super }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h2>Chains</h2> <div class="panel panel-default">
<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> <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 %} {% if chain_list %}
<table class="table table-hover datatable"> <table class="table table-hover datatable">
<thead> <thead>
<tr> <tr>
<th>Chain Name</th> <th>Chain Name</th>
@ -30,19 +33,20 @@ 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.camp_expenses_count }}</span></td>
<td class="text-center">{{ chain.expenses_total|default:"0" }} DKK</td> <td class="text-center">{{ chain.camp_expenses_amount|default:"0" }} DKK</td>
<td class="text-center"><span class="badge">{{ chain.revenues_count }}</span></td> <td class="text-center"><span class="badge">{{ chain.camp_revenues_count }}</span></td>
<td class="text-center">{{ chain.revenues_total|default:"0" }} DKK</td> <td class="text-center">{{ chain.camp_revenues_amount|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>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% else %} {% else %}
<p class="lead"><i>No Chains found.</i></p> <p class="lead"><i>No Chains found.</i></p>
{% endif %} {% endif %}
</div>
</div>
{% endblock %} {% endblock %}

View file

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

View file

@ -298,7 +298,11 @@ class FacilityCreateView(CampViewMixin, OrgaTeamPermissionMixin, CreateView):
def get_form(self, *args, **kwargs): def get_form(self, *args, **kwargs):
form = super().get_form(*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 return form
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -324,7 +328,11 @@ class FacilityUpdateView(CampViewMixin, OrgaTeamPermissionMixin, UpdateView):
def get_form(self, *args, **kwargs): def get_form(self, *args, **kwargs):
form = super().get_form(*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 return form
def get_success_url(self): def get_success_url(self):
@ -445,7 +453,7 @@ class FacilityOpeningHoursCreateView(
hours = form.save(commit=False) hours = form.save(commit=False)
hours.facility = self.facility hours.facility = self.facility
hours.save() hours.save()
messages.success(self.request, f"New opening hours created successfully!") messages.success(self.request, "New opening hours created successfully!")
return redirect( return redirect(
reverse( reverse(
"backoffice:facility_detail", "backoffice:facility_detail",
@ -547,7 +555,9 @@ class SpeakerProposalListView(CampViewMixin, ContentTeamPermissionMixin, ListVie
class SpeakerProposalDetailView( class SpeakerProposalDetailView(
AvailabilityMatrixViewMixin, ContentTeamPermissionMixin, DetailView, AvailabilityMatrixViewMixin,
ContentTeamPermissionMixin,
DetailView,
): ):
""" This view permits Content Team members to see SpeakerProposal details """ """ This view permits Content Team members to see SpeakerProposal details """
@ -915,15 +925,16 @@ class EventDeleteView(CampViewMixin, ContentTeamPermissionMixin, DeleteView):
def get_success_url(self): def get_success_url(self):
messages.success( 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}) return reverse("backoffice:event_list", kwargs={"camp_slug": self.camp.slug})
class EventScheduleView(CampViewMixin, ContentTeamPermissionMixin, FormView): 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 It shows a table with radioselect buttons for the available slots for the
EventType of the Event """ EventType of the Event"""
form_class = EventScheduleForm form_class = EventScheduleForm
template_name = "event_schedule.html" template_name = "event_schedule.html"
@ -1259,9 +1270,9 @@ class AutoScheduleCrashCourseView(
class AutoScheduleValidateView(CampViewMixin, ContentTeamPermissionMixin, FormView): 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 either validate the currently applied schedule or a new similar schedule, or a
brand new schedule """ brand new schedule"""
template_name = "autoschedule_validate.html" template_name = "autoschedule_validate.html"
form_class = AutoScheduleValidateForm form_class = AutoScheduleValidateForm
@ -1314,7 +1325,7 @@ class AutoScheduleDiffView(CampViewMixin, ContentTeamPermissionMixin, TemplateVi
class AutoScheduleApplyView(CampViewMixin, ContentTeamPermissionMixin, FormView): 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. all autoscheduled Events, and scheduling all Event/Slot combinations in the schedule.
TODO: see comment in program.autoscheduler.AutoScheduler.apply() method. TODO: see comment in program.autoscheduler.AutoScheduler.apply() method.
@ -1481,18 +1492,118 @@ class ChainListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
model = Chain model = Chain
template_name = "chain_list_backoffice.html" 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): class ChainDetailView(CampViewMixin, EconomyTeamPermissionMixin, DetailView):
model = Chain model = Chain
template_name = "chain_detail_backoffice.html" template_name = "chain_detail_backoffice.html"
slug_url_kwarg = "chain_slug" 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): class CredebtorDetailView(CampViewMixin, EconomyTeamPermissionMixin, DetailView):
model = Credebtor model = Credebtor
template_name = "credebtor_detail_backoffice.html" template_name = "credebtor_detail_backoffice.html"
slug_url_kwarg = "credebtor_slug" 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 # EXPENSES
@ -1507,7 +1618,11 @@ class ExpenseListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
Exclude unapproved expenses, they are shown seperately Exclude unapproved expenses, they are shown seperately
""" """
queryset = super().get_queryset(**kwargs) 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): def get_context_data(self, **kwargs):
""" """
@ -1516,6 +1631,10 @@ class ExpenseListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["unapproved_expenses"] = Expense.objects.filter( context["unapproved_expenses"] = Expense.objects.filter(
camp=self.camp, approved__isnull=True camp=self.camp, approved__isnull=True
).prefetch_related(
"creditor",
"user",
"responsible_team",
) )
return context return context
@ -1747,7 +1866,11 @@ class RevenueListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
Exclude unapproved revenues, they are shown seperately Exclude unapproved revenues, they are shown seperately
""" """
queryset = super().get_queryset(**kwargs) 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): def get_context_data(self, **kwargs):
""" """
@ -2040,7 +2163,8 @@ class PosReportCreateView(PosViewMixin, CreateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["form"].fields["bank_responsible"].queryset = Team.objects.get( context["form"].fields["bank_responsible"].queryset = Team.objects.get(
camp=self.camp, name="Orga", camp=self.camp,
name="Orga",
).approved_members.all() ).approved_members.all()
context["form"].fields[ context["form"].fields[
"pos_responsible" "pos_responsible"
@ -2054,7 +2178,7 @@ class PosReportCreateView(PosViewMixin, CreateView):
pr = form.save(commit=False) pr = form.save(commit=False)
pr.pos = self.pos pr.pos = self.pos
pr.save() pr.save()
messages.success(self.request, f"New PosReport created successfully!") messages.success(self.request, "New PosReport created successfully!")
return redirect( return redirect(
reverse( reverse(
"backoffice:posreport_detail", "backoffice:posreport_detail",
@ -2086,7 +2210,8 @@ class PosReportUpdateView(PosViewMixin, UpdateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["form"].fields["bank_responsible"].queryset = Team.objects.get( context["form"].fields["bank_responsible"].queryset = Team.objects.get(
camp=self.camp, name="Orga", camp=self.camp,
name="Orga",
).approved_members.all() ).approved_members.all()
context["form"].fields[ context["form"].fields[
"pos_responsible" "pos_responsible"

View file

@ -26,11 +26,26 @@ class ChainManager(models.Manager):
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.prefetch_related(
qs = qs.annotate(expenses_total=models.Sum("credebtors__expenses__amount")) models.Prefetch("credebtors__expenses", to_attr="all_expenses"),
qs = qs.annotate(expenses_count=models.Count("credebtors__expenses", distinct=True)) models.Prefetch("credebtors__revenues", to_attr="all_revenues"),
qs = qs.annotate(revenues_total=models.Sum("credebtors__revenues__amount")) )
qs = qs.annotate(revenues_count=models.Count("credebtors__revenues", distinct=True)) 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 return qs
@ -92,9 +107,12 @@ class CredebtorManager(models.Manager):
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
qs = qs.prefetch_related("expenses", "revenues") qs = qs.prefetch_related(
qs = qs.annotate(expenses_total=models.Sum("expenses__amount")) models.Prefetch("expenses", to_attr="all_expenses"),
qs = qs.annotate(revenues_total=models.Sum("revenues__amount")) 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 return qs
@ -494,7 +512,9 @@ class Pos(CampRelatedModel, UUIDModel):
) )
team = models.ForeignKey( 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): def save(self, **kwargs):
@ -559,7 +579,8 @@ class PosReport(CampRelatedModel, UUIDModel):
) )
comments = models.TextField( 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( dkk_sales_izettle = models.PositiveIntegerField(
@ -567,7 +588,8 @@ class PosReport(CampRelatedModel, UUIDModel):
) )
hax_sold_izettle = models.PositiveIntegerField( 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( 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, 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() objects = ProductQuerySet.as_manager()
def __str__(self): def __str__(self):
@ -442,7 +451,7 @@ class Product(CreatedUpdatedModel, UUIDModel):
raise ValidationError("Products with category Tickets need a ticket_type") raise ValidationError("Products with category Tickets need a ticket_type")
def is_available(self): def is_available(self):
""" Is the product available or not? """Is the product available or not?
Checks for the following: Checks for the following:

View file

@ -11,11 +11,12 @@ from camps.models import Camp
from django.contrib.auth.models import User 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 import call_command from django.core.management import call_command
from django.core.management.base import BaseCommand
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
from economy.models import Chain, Credebtor, Expense, Revenue
from events.models import Routing, Type from events.models import Routing, Type
from facilities.models import ( from facilities.models import (
Facility, Facility,
@ -53,7 +54,6 @@ 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")
@ -65,7 +65,11 @@ class ChainFactory(factory.django.DjangoModelFactory):
model = Chain model = Chain
name = factory.Faker("company") 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): class CredebtorFactory(factory.django.DjangoModelFactory):
@ -74,7 +78,11 @@ class CredebtorFactory(factory.django.DjangoModelFactory):
chain = factory.SubFactory(ChainFactory) chain = factory.SubFactory(ChainFactory)
name = factory.Faker("company") 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") address = factory.Faker("address", locale="dk_DK")
notes = factory.Faker("text") notes = factory.Faker("text")
@ -89,7 +97,9 @@ class ExpenseFactory(factory.django.DjangoModelFactory):
amount = factory.Faker("random_int", min=20, max=20000) amount = factory.Faker("random_int", min=20, max=20000)
description = factory.Faker("text") description = factory.Faker("text")
paid_by_bornhack = factory.Faker("random_element", elements=[True, True, False]) 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") invoice_date = factory.Faker("date")
responsible_team = factory.Faker("random_element", elements=Team.objects.all()) responsible_team = factory.Faker("random_element", elements=Team.objects.all())
approved = factory.Faker("random_element", elements=[True, True, False]) 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()) user = factory.Faker("random_element", elements=User.objects.all())
amount = factory.Faker("random_int", min=20, max=20000) amount = factory.Faker("random_int", min=20, max=20000)
description = factory.Faker("text") 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") invoice_date = factory.Faker("date")
responsible_team = factory.Faker("random_element", elements=Team.objects.all()) responsible_team = factory.Faker("random_element", elements=Team.objects.all())
approved = factory.Faker("random_element", elements=[True, True, False]) approved = factory.Faker("random_element", elements=[True, True, False])
@ -567,7 +579,7 @@ class Command(BaseCommand):
try: try:
CredebtorFactory.create_batch(50) CredebtorFactory.create_batch(50)
except ValidationError: except ValidationError:
self.outout("Name conflict, retrying...") self.output("Name conflict, retrying...")
CredebtorFactory.create_batch(50) CredebtorFactory.create_batch(50)
for _ in range(20): for _ in range(20):
# add 20 more credebtors to random existing chains # add 20 more credebtors to random existing chains
@ -856,10 +868,18 @@ class Command(BaseCommand):
capacity=50, capacity=50,
) )
locations["food_area"] = EventLocation.objects.create( 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( 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 # add workshop room conflicts (the big root can not be used while either