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:
parent
833fc85e46
commit
fbda2b4b53
|
@ -6,44 +6,56 @@ 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">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Credebtor Name</th>
|
|
||||||
<th>Address</th>
|
|
||||||
<th>Notes</th>
|
|
||||||
<th class="text-center">Expenses</th>
|
|
||||||
<th class="text-center">Expenses Total</th>
|
|
||||||
<th class="text-center">Revenues</th>
|
|
||||||
<th class="text-center">Revenues Total</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for credebtor in chain.credebtors.all %}
|
|
||||||
<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>
|
|
||||||
<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>
|
<h3>{{ chain.credebtors.count }} Credebtors for Chain {{ chain.name }}</h3>
|
||||||
{% include 'includes/expense_list_panel.html' with expense_list=chain.expenses.all %}
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Credebtor Name</th>
|
||||||
|
<th>Address</th>
|
||||||
|
<th>Notes</th>
|
||||||
|
<th class="text-center">Expenses</th>
|
||||||
|
<th class="text-center">Expenses Total</th>
|
||||||
|
<th class="text-center">Revenues</th>
|
||||||
|
<th class="text-center">Revenues Total</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% 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.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.revenues_count }} Revenues for Chain {{ chain.name }}</h3>
|
<hr>
|
||||||
{% 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 %}
|
{% endblock content %}
|
||||||
|
|
||||||
|
|
|
@ -7,42 +7,46 @@ 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">
|
||||||
{% if chain_list %}
|
<p class="lead">Showing {{ chain_list.count }} chains. Not all chains have expenses or revenues for {{ camp.title }}.</p>
|
||||||
<table class="table table-hover datatable">
|
<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>
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Chain Name</th>
|
|
||||||
<th>Notes</th>
|
|
||||||
<th class="text-center">Credebtors</th>
|
|
||||||
<th class="text-center">Expenses</th>
|
|
||||||
<th class="text-center">Expenses Total</th>
|
|
||||||
<th class="text-center">Revenues</th>
|
|
||||||
<th class="text-center">Revenues Total</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for chain in chain_list %}
|
|
||||||
<tr>
|
|
||||||
<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>
|
|
||||||
<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>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% else %}
|
|
||||||
<p class="lead"><i>No Chains found.</i></p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
{% if chain_list %}
|
||||||
|
<table class="table table-hover datatable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Chain Name</th>
|
||||||
|
<th>Notes</th>
|
||||||
|
<th class="text-center">Credebtors</th>
|
||||||
|
<th class="text-center">Expenses</th>
|
||||||
|
<th class="text-center">Expenses Total</th>
|
||||||
|
<th class="text-center">Revenues</th>
|
||||||
|
<th class="text-center">Revenues Total</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for chain in chain_list %}
|
||||||
|
<tr>
|
||||||
|
<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.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>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="lead"><i>No Chains found.</i></p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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(
|
||||||
|
|
28
src/shop/migrations/0064_add_product_comment_and_cost.py
Normal file
28
src/shop/migrations/0064_add_product_comment_and_cost.py
Normal 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.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue