diff --git a/src/backoffice/mixins.py b/src/backoffice/mixins.py
index ee43b700..b3aca3b4 100644
--- a/src/backoffice/mixins.py
+++ b/src/backoffice/mixins.py
@@ -1,3 +1,7 @@
+from camps.mixins import CampViewMixin
+from django.core.exceptions import PermissionDenied
+from django.shortcuts import get_object_or_404
+from economy.models import Pos
from utils.mixins import RaisePermissionRequiredMixin
@@ -37,3 +41,27 @@ class ContentTeamPermissionMixin(RaisePermissionRequiredMixin):
"camps.backoffice_permission",
"camps.contentteam_permission",
)
+
+
+class PosViewMixin(CampViewMixin):
+ """A mixin to set self.pos based on pos_slug in url kwargs."""
+
+ def setup(self, *args, **kwargs):
+ super().setup(*args, **kwargs)
+ self.pos = get_object_or_404(
+ Pos, team__camp=self.camp, slug=self.kwargs["pos_slug"]
+ )
+
+ def get_permission_required(self):
+ """
+ This view requires two permissions, camps.backoffice_permission and the permission_set for the team in question.
+ """
+ if not self.pos.team.permission_set:
+ raise PermissionDenied("No permissions set defined for this team")
+ perms = ["camps.backoffice_permission"]
+ return perms
+
+ def get_context_data(self, *args, **kwargs):
+ context = super().get_context_data(*args, **kwargs)
+ context["pos"] = self.pos
+ return context
diff --git a/src/backoffice/templates/includes/pos_list_table.html b/src/backoffice/templates/includes/pos_list_table.html
new file mode 100644
index 00000000..3e1b1afc
--- /dev/null
+++ b/src/backoffice/templates/includes/pos_list_table.html
@@ -0,0 +1,28 @@
+
+
+
+ Name |
+ Team |
+ Slug |
+ Actions |
+
+
+
+ {% for pos in pos_list %}
+
+ {{ pos.name }} |
+ {{ pos.team }} |
+ {{ pos.slug }} |
+
+
+ |
+
+ {% endfor %}
+
+
diff --git a/src/backoffice/templates/includes/posreport_list_table.html b/src/backoffice/templates/includes/posreport_list_table.html
new file mode 100644
index 00000000..a3b7e008
--- /dev/null
+++ b/src/backoffice/templates/includes/posreport_list_table.html
@@ -0,0 +1,34 @@
+
+
+
+ Date |
+ Pos |
+ Bank Responsible |
+ Pos Responsible |
+ Actions |
+
+
+
+ {% for pr in posreport_list %}
+
+ {{ pr.date }} |
+ {{ pr.pos.name }} |
+ {{ pr.bank_responsible }} |
+ {{ pr.pos_responsible }} |
+
+
+ |
+
+ {% endfor %}
+
+
diff --git a/src/backoffice/templates/index.html b/src/backoffice/templates/index.html
index a6cc4231..e3ed808d 100644
--- a/src/backoffice/templates/index.html
+++ b/src/backoffice/templates/index.html
@@ -166,6 +166,12 @@
Proxied Content
Use this view to see proxied content
+
+ Point of Sale
+
+ Point of Sale
+ Use this view to see a list of Pos objects, and to see and submit PosReports
+
diff --git a/src/backoffice/templates/pos_delete.html b/src/backoffice/templates/pos_delete.html
new file mode 100644
index 00000000..a04d4c1f
--- /dev/null
+++ b/src/backoffice/templates/pos_delete.html
@@ -0,0 +1,18 @@
+{% extends 'base.html' %}
+{% load bootstrap3 %}
+
+{% block content %}
+
+
+
Delete Pos {{ pos.name }}?
+
+
+
This Pos has {{ pos.pos_reports.count }} PosReports which will also be deleted.
+
+
+
+{% endblock content %}
diff --git a/src/backoffice/templates/pos_detail.html b/src/backoffice/templates/pos_detail.html
new file mode 100644
index 00000000..887086ba
--- /dev/null
+++ b/src/backoffice/templates/pos_detail.html
@@ -0,0 +1,42 @@
+{% extends 'base.html' %}
+
+{% block title %}
+{{ pos.name }} | Pos | BackOffice | {{ block.super }}
+{% endblock %}
+
+{% block content %}
+
+
+
{{ pos.name }} | Pos | BackOffice
+
+
+
+ Update Pos
+ Delete Pos
+ Pos List
+
+
+
+
+ Pos Name |
+ {{ pos.name }} |
+
+
+ Team |
+ {{ pos.team }}
+ |
+
+
+
Pos Reports
+ {% if pos.pos_reports.exists %}
+ {% include "includes/posreport_list_table.html" with posreport_list=pos.pos_reports.all %}
+ {% else %}
+ None found
+ {% endif %}
+
+{% if perms.camps.orgateam_permission %}
+
Create PosReport
+{% endif %}
+
+
+{% endblock %}
diff --git a/src/backoffice/templates/pos_form.html b/src/backoffice/templates/pos_form.html
new file mode 100644
index 00000000..a36fe4a7
--- /dev/null
+++ b/src/backoffice/templates/pos_form.html
@@ -0,0 +1,21 @@
+{% extends 'base.html' %}
+{% load bootstrap3 %}
+{% load static %}
+
+{% block content %}
+
+
+
{% if request.resolver_match.url_name == "pos_update" %}Update{% else %}Create new{% endif %} Pos
+
+
+
+{% endblock content %}
diff --git a/src/backoffice/templates/pos_list.html b/src/backoffice/templates/pos_list.html
new file mode 100644
index 00000000..26967beb
--- /dev/null
+++ b/src/backoffice/templates/pos_list.html
@@ -0,0 +1,29 @@
+{% extends 'base.html' %}
+{% load bornhack %}
+
+{% block title %}
+Pos List | Backoffice | {{ block.super }}
+{% endblock %}
+
+{% block content %}
+
+
Pos List - BackOffice
+
+
A Pos is a place where we sell stuff for DKK and/or HAX.
+ {% if not pos_list %}
+
No Pos found.
+ {% else %}
+
+ Backoffice
+ {% include "includes/pos_list_table.html" %}
+
+ {% endif %}
+
+{% if perms.camps.orgateam_permission %}
+ Create Pos
+{% endif %}
+ Backoffice
+
+
+
+{% endblock content %}
diff --git a/src/backoffice/templates/posreport_detail.html b/src/backoffice/templates/posreport_detail.html
new file mode 100644
index 00000000..a4d21647
--- /dev/null
+++ b/src/backoffice/templates/posreport_detail.html
@@ -0,0 +1,115 @@
+{% extends 'base.html' %}
+
+{% block title %}
+PosReport {{ posreport.date }} {{ posreport.pos.name }} | Pos | BackOffice | {{ block.super }}
+{% endblock %}
+
+{% block content %}
+
+
+
PosReport {{ posreport.date }} {{ posreport.pos.name }} | Pos | BackOffice
+
+
+
+ {% if "camps.orgateam_permission" in perms %}
+ Update
+ {% endif %}
+ {% if request.user == posreport.bank_responsible %}
+ Bank Count Start
+ Bank Count End
+ {% endif %}
+ {% if request.user == posreport.pos_responsible %}
+ Pos Count Start
+ Pos Count End
+ {% endif %}
+
+
+
+
+ PosReport UUID |
+ {{ posreport.uuid }} |
+
+
+ PosReport Date |
+ {{ posreport.date }} |
+
+
+ Pos Name |
+ {{ posreport.pos.name }} |
+
+
+ Team |
+ {{ posreport.pos.team }}
+ |
+
+ Bank Responsible |
+ {{ posreport.bank_responsible }}
+ |
+
+ Pos Responsible |
+ {{ posreport.pos_responsible }}
+ |
+
+ Counts |
+
+
+
+
+ What |
+ Bank Start |
+ Bank End |
+ Pos Start |
+ Pos End |
+
+
+
+
+ DKK |
+ {{ posreport.bank_count_dkk_start }} |
+ {{ posreport.bank_count_dkk_end }} |
+ {{ posreport.pos_count_dkk_start }} |
+ {{ posreport.pos_count_dkk_end }} |
+
+
+ 5 HAX |
+ {{ posreport.bank_count_hax5_start }} |
+ {{ posreport.bank_count_hax5_end }} |
+ {{ posreport.pos_count_hax5_start }} |
+ {{ posreport.pos_count_hax5_end }} |
+
+
+ 10 HAX |
+ {{ posreport.bank_count_hax10_start }} |
+ {{ posreport.bank_count_hax10_end }} |
+ {{ posreport.pos_count_hax10_start }} |
+ {{ posreport.pos_count_hax10_end }} |
+
+ 20 HAX |
+ {{ posreport.bank_count_hax20_start }} |
+ {{ posreport.bank_count_hax20_end }} |
+ {{ posreport.pos_count_hax20_start }} |
+ {{ posreport.pos_count_hax20_end }} |
+
+
+ 50 HAX |
+ {{ posreport.bank_count_hax50_start }} |
+ {{ posreport.bank_count_hax50_end }} |
+ {{ posreport.pos_count_hax50_start }} |
+ {{ posreport.pos_count_hax50_end }} |
+
+
+ 100 HAX |
+ {{ posreport.bank_count_hax100_start }} |
+ {{ posreport.bank_count_hax100_end }} |
+ {{ posreport.pos_count_hax100_start }} |
+ {{ posreport.pos_count_hax100_end }} |
+
+
+
+ |
+
+
+
+
+
+{% endblock %}
diff --git a/src/backoffice/templates/posreport_form.html b/src/backoffice/templates/posreport_form.html
new file mode 100644
index 00000000..ed3d5b28
--- /dev/null
+++ b/src/backoffice/templates/posreport_form.html
@@ -0,0 +1,21 @@
+{% extends 'base.html' %}
+{% load bootstrap3 %}
+{% load static %}
+
+{% block content %}
+
+
+
{% if request.resolver_match.url_name == "posreport_create" %}Create{% else %}Update{% endif %} PosReport
+
+
+
+{% endblock content %}
diff --git a/src/backoffice/templates/posreport_list.html b/src/backoffice/templates/posreport_list.html
new file mode 100644
index 00000000..75e9e7ba
--- /dev/null
+++ b/src/backoffice/templates/posreport_list.html
@@ -0,0 +1,25 @@
+{% extends 'base.html' %}
+{% load bornhack %}
+
+{% block title %}
+PosReport List for {{ pos.name }} | Backoffice | {{ block.super }}
+{% endblock %}
+
+{% block content %}
+
+
PosReport List for {{ pos.name }} - BackOffice
+
+
A PosReport contains the start and end counts of HAX+DKK from a point-of-sale and the exported JSON file from the Pos.
+ {% if not pos_list %}
+
No Pos found.
+ {% else %}
+
+ {% include "includes/posreport_list_table.html" %}
+
+ {% endif %}
+
+ Backoffice
+
+
+
+{% endblock content %}
diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py
index e96a49eb..bed78562 100644
--- a/src/backoffice/urls.py
+++ b/src/backoffice/urls.py
@@ -60,6 +60,18 @@ from .views import (
MerchandiseToOrderView,
OutgoingEmailMassUpdateView,
PendingProposalsView,
+ PosCreateView,
+ PosDeleteView,
+ PosDetailView,
+ PosListView,
+ PosReportBankCountEndView,
+ PosReportBankCountStartView,
+ PosReportCreateView,
+ PosReportDetailView,
+ PosReportPosCountEndView,
+ PosReportPosCountStartView,
+ PosReportUpdateView,
+ PosUpdateView,
ProductHandoutView,
ReimbursementCreateUserSelectView,
ReimbursementCreateView,
@@ -632,4 +644,77 @@ urlpatterns = [
OutgoingEmailMassUpdateView.as_view(),
name="outgoing_email_release",
),
+ # point-of-sale
+ path(
+ "pos/",
+ include(
+ [
+ path("", PosListView.as_view(), name="pos_list",),
+ path("create/", PosCreateView.as_view(), name="pos_create",),
+ path(
+ "/",
+ include(
+ [
+ path("", PosDetailView.as_view(), name="pos_detail",),
+ path(
+ "update/", PosUpdateView.as_view(), name="pos_update",
+ ),
+ path(
+ "delete/", PosDeleteView.as_view(), name="pos_delete",
+ ),
+ path(
+ "reports/",
+ include(
+ [
+ path(
+ "create/",
+ PosReportCreateView.as_view(),
+ name="posreport_create",
+ ),
+ path(
+ "/",
+ include(
+ [
+ path(
+ "",
+ PosReportDetailView.as_view(),
+ name="posreport_detail",
+ ),
+ path(
+ "update/",
+ PosReportUpdateView.as_view(),
+ name="posreport_update",
+ ),
+ path(
+ "bankcount/start/",
+ PosReportBankCountStartView.as_view(),
+ name="posreport_bank_count_start",
+ ),
+ path(
+ "bankcount/end/",
+ PosReportBankCountEndView.as_view(),
+ name="posreport_bank_count_end",
+ ),
+ path(
+ "poscount/start/",
+ PosReportPosCountStartView.as_view(),
+ name="posreport_pos_count_start",
+ ),
+ path(
+ "poscount/end/",
+ PosReportPosCountEndView.as_view(),
+ name="posreport_pos_count_end",
+ ),
+ ]
+ ),
+ ),
+ ]
+ ),
+ ),
+ ]
+ ),
+ ),
+ ],
+ ),
+ ),
]
diff --git a/src/backoffice/views.py b/src/backoffice/views.py
index daa54a80..702e2a66 100644
--- a/src/backoffice/views.py
+++ b/src/backoffice/views.py
@@ -20,7 +20,15 @@ from django.utils import timezone
from django.utils.safestring import mark_safe
from django.views.generic import DetailView, ListView, TemplateView
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
-from economy.models import Chain, Credebtor, Expense, Reimbursement, Revenue
+from economy.models import (
+ Chain,
+ Credebtor,
+ Expense,
+ Pos,
+ PosReport,
+ Reimbursement,
+ Revenue,
+)
from facilities.models import (
Facility,
FacilityFeedback,
@@ -59,6 +67,7 @@ from .mixins import (
EconomyTeamPermissionMixin,
InfoTeamPermissionMixin,
OrgaTeamPermissionMixin,
+ PosViewMixin,
RaisePermissionRequiredMixin,
)
@@ -1955,3 +1964,219 @@ class OutgoingEmailMassUpdateView(CampViewMixin, OrgaTeamPermissionMixin, FormVi
def get_success_url(self, *args, **kwargs):
"""Return to the backoffice index."""
return reverse("backoffice:index", kwargs={"camp_slug": self.camp.slug})
+
+
+################################
+# Pos and PosReport views
+
+
+class PosListView(CampViewMixin, RaisePermissionRequiredMixin, ListView):
+ """Show a list of Pos this user has access to (through team memberships)."""
+
+ permission_required = "camps.backoffice_permission"
+ model = Pos
+ template_name = "pos_list.html"
+
+
+class PosDetailView(PosViewMixin, RaisePermissionRequiredMixin, DetailView):
+ """Show details for a Pos."""
+
+ model = Pos
+ template_name = "pos_detail.html"
+ slug_url_kwarg = "pos_slug"
+
+
+class PosCreateView(CampViewMixin, OrgaTeamPermissionMixin, CreateView):
+ """Create a new Pos (orga only)."""
+
+ model = Pos
+ template_name = "pos_form.html"
+ fields = ["name", "team"]
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["form"].fields["team"].queryset = Team.objects.filter(camp=self.camp)
+ return context
+
+
+class PosUpdateView(CampViewMixin, OrgaTeamPermissionMixin, UpdateView):
+ """Update a Pos."""
+
+ model = Pos
+ template_name = "pos_form.html"
+ slug_url_kwarg = "pos_slug"
+ fields = ["name", "team"]
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["form"].fields["team"].queryset = Team.objects.filter(camp=self.camp)
+ return context
+
+
+class PosDeleteView(CampViewMixin, OrgaTeamPermissionMixin, DeleteView):
+ model = Pos
+ template_name = "pos_delete.html"
+ slug_url_kwarg = "pos_slug"
+
+ def delete(self, *args, **kwargs):
+ self.get_object().pos_reports.all().delete()
+ return super().delete(*args, **kwargs)
+
+ def get_success_url(self):
+ messages.success(
+ self.request, "The Pos and all related PosReports has been deleted"
+ )
+ return reverse("backoffice:pos_list", kwargs={"camp_slug": self.camp.slug})
+
+
+class PosReportCreateView(PosViewMixin, RaisePermissionRequiredMixin, CreateView):
+ """Use this view to create new PosReports."""
+
+ model = PosReport
+ fields = ["date", "bank_responsible", "pos_responsible"]
+ template_name = "posreport_form.html"
+
+ 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",
+ ).approved_members.all()
+ context["form"].fields[
+ "pos_responsible"
+ ].queryset = self.pos.team.responsible_members.all()
+ return context
+
+ def form_valid(self, form):
+ """
+ Set Pos before saving
+ """
+ pr = form.save(commit=False)
+ pr.pos = self.pos
+ pr.save()
+ messages.success(self.request, f"New PosReport created successfully!")
+ return redirect(
+ reverse(
+ "backoffice:posreport_detail",
+ kwargs={
+ "camp_slug": self.camp.slug,
+ "pos_slug": self.pos.slug,
+ "posreport_uuid": pr.uuid,
+ },
+ )
+ )
+
+
+class PosReportUpdateView(PosViewMixin, RaisePermissionRequiredMixin, UpdateView):
+ """Use this view to update PosReports."""
+
+ model = PosReport
+ fields = ["date", "bank_responsible", "pos_responsible"]
+ template_name = "posreport_form.html"
+ pk_url_kwarg = "posreport_uuid"
+
+ 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",
+ ).approved_members.all()
+ context["form"].fields[
+ "pos_responsible"
+ ].queryset = self.pos.team.responsible_members.all()
+ return context
+
+
+class PosReportDetailView(PosViewMixin, RaisePermissionRequiredMixin, DetailView):
+ """Show details for a PosReport."""
+
+ model = PosReport
+ template_name = "posreport_detail.html"
+ pk_url_kwarg = "posreport_uuid"
+
+
+class PosReportBankCountStartView(
+ PosViewMixin, RaisePermissionRequiredMixin, UpdateView
+):
+ """The bank responsible for a PosReport uses this view to add day-start HAX and DKK counts to a PosReport."""
+
+ model = PosReport
+ template_name = "posreport_form.html"
+ fields = [
+ "bank_count_dkk_start",
+ "bank_count_hax5_start",
+ "bank_count_hax10_start",
+ "bank_count_hax20_start",
+ "bank_count_hax50_start",
+ "bank_count_hax100_start",
+ ]
+ pk_url_kwarg = "posreport_uuid"
+
+ def setup(self, *args, **kwargs):
+ super().setup(*args, **kwargs)
+ if self.request.user != self.get_object().bank_responsible:
+ raise PermissionDenied("Only the bank responsible can do this")
+
+
+class PosReportBankCountEndView(PosViewMixin, RaisePermissionRequiredMixin, UpdateView):
+ """The bank responsible for a PosReport uses this view to add day-end HAX and DKK counts to a PosReport."""
+
+ model = PosReport
+ template_name = "posreport_form.html"
+ fields = [
+ "bank_count_dkk_end",
+ "bank_count_hax5_end",
+ "bank_count_hax10_end",
+ "bank_count_hax20_end",
+ "bank_count_hax50_end",
+ "bank_count_hax100_end",
+ ]
+ pk_url_kwarg = "posreport_uuid"
+
+ def setup(self, *args, **kwargs):
+ super().setup(*args, **kwargs)
+ if self.request.user != self.get_object().bank_responsible:
+ raise PermissionDenied("Only the bank responsible can do this")
+
+
+class PosReportPosCountStartView(
+ PosViewMixin, RaisePermissionRequiredMixin, UpdateView
+):
+ """The Pos responsible for a PosReport uses this view to add day-start HAX and DKK counts to a PosReport."""
+
+ model = PosReport
+ template_name = "posreport_form.html"
+ fields = [
+ "pos_count_dkk_start",
+ "pos_count_hax5_start",
+ "pos_count_hax10_start",
+ "pos_count_hax20_start",
+ "pos_count_hax50_start",
+ "pos_count_hax100_start",
+ ]
+ pk_url_kwarg = "posreport_uuid"
+
+ def setup(self, *args, **kwargs):
+ super().setup(*args, **kwargs)
+ if self.request.user != self.get_object().pos_responsible:
+ raise PermissionDenied("Only the Pos responsible can do this")
+
+
+class PosReportPosCountEndView(PosViewMixin, RaisePermissionRequiredMixin, UpdateView):
+ """The Pos responsible for a PosReport uses this view to add day-end HAX and DKK counts to a PosReport."""
+
+ model = PosReport
+ template_name = "posreport_form.html"
+ fields = [
+ "pos_count_dkk_end",
+ "pos_count_hax5_end",
+ "pos_count_hax10_end",
+ "pos_count_hax20_end",
+ "pos_count_hax50_end",
+ "pos_count_hax100_end",
+ "pos_json",
+ ]
+ pk_url_kwarg = "posreport_uuid"
+
+ def setup(self, *args, **kwargs):
+ super().setup(*args, **kwargs)
+ if self.request.user != self.get_object().pos_responsible:
+ raise PermissionDenied("Only the pos responsible can do this")
diff --git a/src/economy/admin.py b/src/economy/admin.py
index d5360d52..bf08576c 100644
--- a/src/economy/admin.py
+++ b/src/economy/admin.py
@@ -1,6 +1,6 @@
from django.contrib import admin
-from .models import Chain, Credebtor, Expense, Reimbursement, Revenue
+from .models import Chain, Credebtor, Expense, Pos, PosReport, Reimbursement, Revenue
###############################
# chains and credebtors
@@ -113,3 +113,18 @@ class ReimbursementAdmin(admin.ModelAdmin):
list_filter = ["camp", "user", "reimbursement_user", "paid"]
list_display = ["camp", "user", "reimbursement_user", "paid", "notes", "get_amount"]
search_fields = ["user__username", "reimbursement_user__username", "notes"]
+
+
+################################
+# pos
+
+
+@admin.register(Pos)
+class PosAdmin(admin.ModelAdmin):
+ list_display = ["name", "team"]
+ list_filter = ["team"]
+
+
+@admin.register(PosReport)
+class PosReportAdmin(admin.ModelAdmin):
+ list_display = ["uuid", "pos"]
diff --git a/src/economy/migrations/0011_pos_posreport.py b/src/economy/migrations/0011_pos_posreport.py
new file mode 100644
index 00000000..f1e91180
--- /dev/null
+++ b/src/economy/migrations/0011_pos_posreport.py
@@ -0,0 +1,284 @@
+# Generated by Django 3.1 on 2020-08-10 23:41
+
+import uuid
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ("teams", "0052_team_permission_set"),
+ ("economy", "0010_auto_20190330_1045"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="Pos",
+ fields=[
+ (
+ "uuid",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ("created", models.DateTimeField(auto_now_add=True)),
+ ("updated", models.DateTimeField(auto_now=True)),
+ (
+ "name",
+ models.CharField(
+ help_text="The point-of-sale name", max_length=255
+ ),
+ ),
+ (
+ "slug",
+ models.SlugField(
+ blank=True,
+ help_text="Url slug for this POS. Leave blank to generate based on POS name.",
+ max_length=255,
+ ),
+ ),
+ (
+ "team",
+ models.ForeignKey(
+ help_text="The Team managning this POS",
+ on_delete=django.db.models.deletion.PROTECT,
+ to="teams.team",
+ ),
+ ),
+ ],
+ options={"ordering": ["name"],},
+ ),
+ migrations.CreateModel(
+ name="PosReport",
+ fields=[
+ (
+ "uuid",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ("created", models.DateTimeField(auto_now_add=True)),
+ ("updated", models.DateTimeField(auto_now=True)),
+ (
+ "date",
+ models.DateField(
+ help_text="The date this report covers (pick the starting date if opening hours cross midnight)."
+ ),
+ ),
+ (
+ "pos_json",
+ models.JSONField(
+ blank=True,
+ help_text="The JSON exported from the external POS system",
+ null=True,
+ ),
+ ),
+ (
+ "bank_count_dkk_start",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of DKK handed out from the bank to the POS at the start of the business day (counted by the bank responsible)",
+ ),
+ ),
+ (
+ "bank_count_hax5_start",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 5 HAX coins handed out from the bank to the POS at the start of the business day (counted by the bank responsible)",
+ ),
+ ),
+ (
+ "bank_count_hax10_start",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 10 HAX coins handed out from the bank to the POS at the start of the business day (counted by the bank responsible)",
+ ),
+ ),
+ (
+ "bank_count_hax20_start",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 20 HAX coins handed out from the bank to the POS at the start of the business day (counted by the bank responsible)",
+ ),
+ ),
+ (
+ "bank_count_hax50_start",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 50 HAX coins handed out from the bank to the POS at the start of the business day (counted by the bank responsible)",
+ ),
+ ),
+ (
+ "bank_count_hax100_start",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 100 HAX coins handed out from the bank to the POS at the start of the business day (counted by the bank responsible)",
+ ),
+ ),
+ (
+ "pos_count_dkk_start",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of DKK handed out from the bank to the POS at the start of the business day (counted by the POS responsible)",
+ ),
+ ),
+ (
+ "pos_count_hax5_start",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 5 HAX coins received by the POS from the bank at the start of the business day (counted by the POS responsible)",
+ ),
+ ),
+ (
+ "pos_count_hax10_start",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 10 HAX coins received by the POS from the bank at the start of the business day (counted by the POS responsible)",
+ ),
+ ),
+ (
+ "pos_count_hax20_start",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 20 HAX coins received by the POS from the bank at the start of the business day (counted by the POS responsible)",
+ ),
+ ),
+ (
+ "pos_count_hax50_start",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 50 HAX coins received by the POS from the bank at the start of the business day (counted by the POS responsible)",
+ ),
+ ),
+ (
+ "pos_count_hax100_start",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 100 HAX coins received by the POS from the bank at the start of the business day (counted by the POS responsible)",
+ ),
+ ),
+ (
+ "bank_count_dkk_end",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of DKK handed back from the POS to the bank at the end of the business day (counted by the bank responsible)",
+ ),
+ ),
+ (
+ "bank_count_hax5_end",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 5 HAX coins handed back from the POS to the bank at the end of the business day (counted by the bank responsible)",
+ ),
+ ),
+ (
+ "bank_count_hax10_end",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 10 HAX coins handed back from the POS to the bank at the end of the business day (counted by the bank responsible)",
+ ),
+ ),
+ (
+ "bank_count_hax20_end",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 20 HAX coins handed back from the POS to the bank at the end of the business day (counted by the bank responsible)",
+ ),
+ ),
+ (
+ "bank_count_hax50_end",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 50 HAX coins handed back from the POS to the bank at the end of the business day (counted by the bank responsible)",
+ ),
+ ),
+ (
+ "bank_count_hax100_end",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 100 HAX coins handed back from the POS to the bank at the end of the business day (counted by the bank responsible)",
+ ),
+ ),
+ (
+ "pos_count_dkk_end",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of DKK handed back from the POS to the bank at the end of the business day (counted by the POS responsible)",
+ ),
+ ),
+ (
+ "pos_count_hax5_end",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 5 HAX coins received by the bank from the POS at the end of the business day (counted by the POS responsible)",
+ ),
+ ),
+ (
+ "pos_count_hax10_end",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 10 HAX coins received by the bank from the POS at the end of the business day (counted by the POS responsible)",
+ ),
+ ),
+ (
+ "pos_count_hax20_end",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 20 HAX coins received by the bank from the POS at the end of the business day (counted by the POS responsible)",
+ ),
+ ),
+ (
+ "pos_count_hax50_end",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 50 HAX coins received by the bank from the POS at the end of the business day (counted by the POS responsible)",
+ ),
+ ),
+ (
+ "pos_count_hax100_end",
+ models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 100 HAX coins received by the bank from the POS at the end of the business day (counted by the POS responsible)",
+ ),
+ ),
+ (
+ "bank_responsible",
+ models.ForeignKey(
+ help_text="The banker responsible for this PosReport",
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="pos_reports_banker",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ (
+ "pos",
+ models.ForeignKey(
+ help_text="The Pos this PosReport belongs to.",
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="pos_reports",
+ to="economy.pos",
+ ),
+ ),
+ (
+ "pos_responsible",
+ models.ForeignKey(
+ help_text="The POS person responsible for this PosReport",
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="pos_reports_poser",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ],
+ options={"abstract": False,},
+ ),
+ ]
diff --git a/src/economy/models.py b/src/economy/models.py
index d0122a53..e92f11b3 100644
--- a/src/economy/models.py
+++ b/src/economy/models.py
@@ -3,6 +3,7 @@ import os
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.db import models
+from django.urls import reverse
from utils.models import CampRelatedModel, CreatedUpdatedModel, UUIDModel
from utils.slugs import unique_slugify
@@ -465,3 +466,224 @@ class Reimbursement(CampRelatedModel, UUIDModel):
for expense in self.expenses.filter(paid_by_bornhack=False):
amount += expense.amount
return amount
+
+
+class Pos(CampRelatedModel, UUIDModel):
+ """A Pos is a point-of-sale like the bar or infodesk."""
+
+ class Meta:
+ ordering = ["name"]
+
+ name = models.CharField(max_length=255, help_text="The point-of-sale name")
+
+ slug = models.SlugField(
+ max_length=255,
+ blank=True,
+ help_text="Url slug for this POS. Leave blank to generate based on POS name.",
+ )
+
+ team = models.ForeignKey(
+ "teams.Team", on_delete=models.PROTECT, help_text="The Team managning this POS",
+ )
+
+ def save(self, **kwargs):
+ """Generate slug if needed."""
+ if not self.slug:
+ self.slug = unique_slugify(
+ self.name,
+ slugs_in_use=self.__class__.objects.filter(
+ team__camp=self.team.camp
+ ).values_list("slug", flat=True),
+ )
+ super().save(**kwargs)
+
+ @property
+ def camp(self):
+ return self.team.camp
+
+ camp_filter = "team__camp"
+
+ def get_absolute_url(self):
+ return reverse(
+ "backoffice:pos_detail",
+ kwargs={"camp_slug": self.team.camp.slug, "pos_slug": self.slug},
+ )
+
+
+class PosReport(CampRelatedModel, UUIDModel):
+ """A PosReport contains the HAX/DKK counts and the csv report from the POS system."""
+
+ pos = models.ForeignKey(
+ "economy.Pos",
+ on_delete=models.PROTECT,
+ related_name="pos_reports",
+ help_text="The Pos this PosReport belongs to.",
+ )
+
+ bank_responsible = models.ForeignKey(
+ "auth.User",
+ on_delete=models.PROTECT,
+ related_name="pos_reports_banker",
+ help_text="The banker responsible for this PosReport",
+ )
+
+ pos_responsible = models.ForeignKey(
+ "auth.User",
+ on_delete=models.PROTECT,
+ related_name="pos_reports_poser",
+ help_text="The POS person responsible for this PosReport",
+ )
+
+ date = models.DateField(
+ help_text="The date this report covers (pick the starting date if opening hours cross midnight).",
+ )
+
+ pos_json = models.JSONField(
+ null=True,
+ blank=True,
+ help_text="The JSON exported from the external POS system",
+ )
+
+ # bank count start of day
+
+ bank_count_dkk_start = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of DKK handed out from the bank to the POS at the start of the business day (counted by the bank responsible)",
+ )
+
+ bank_count_hax5_start = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 5 HAX coins handed out from the bank to the POS at the start of the business day (counted by the bank responsible)",
+ )
+
+ bank_count_hax10_start = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 10 HAX coins handed out from the bank to the POS at the start of the business day (counted by the bank responsible)",
+ )
+
+ bank_count_hax20_start = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 20 HAX coins handed out from the bank to the POS at the start of the business day (counted by the bank responsible)",
+ )
+
+ bank_count_hax50_start = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 50 HAX coins handed out from the bank to the POS at the start of the business day (counted by the bank responsible)",
+ )
+
+ bank_count_hax100_start = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 100 HAX coins handed out from the bank to the POS at the start of the business day (counted by the bank responsible)",
+ )
+
+ # POS count start of day
+
+ pos_count_dkk_start = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of DKK handed out from the bank to the POS at the start of the business day (counted by the POS responsible)",
+ )
+
+ pos_count_hax5_start = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 5 HAX coins received by the POS from the bank at the start of the business day (counted by the POS responsible)",
+ )
+
+ pos_count_hax10_start = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 10 HAX coins received by the POS from the bank at the start of the business day (counted by the POS responsible)",
+ )
+
+ pos_count_hax20_start = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 20 HAX coins received by the POS from the bank at the start of the business day (counted by the POS responsible)",
+ )
+
+ pos_count_hax50_start = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 50 HAX coins received by the POS from the bank at the start of the business day (counted by the POS responsible)",
+ )
+
+ pos_count_hax100_start = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 100 HAX coins received by the POS from the bank at the start of the business day (counted by the POS responsible)",
+ )
+
+ # bank count end of day
+
+ bank_count_dkk_end = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of DKK handed back from the POS to the bank at the end of the business day (counted by the bank responsible)",
+ )
+
+ bank_count_hax5_end = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 5 HAX coins handed back from the POS to the bank at the end of the business day (counted by the bank responsible)",
+ )
+
+ bank_count_hax10_end = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 10 HAX coins handed back from the POS to the bank at the end of the business day (counted by the bank responsible)",
+ )
+
+ bank_count_hax20_end = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 20 HAX coins handed back from the POS to the bank at the end of the business day (counted by the bank responsible)",
+ )
+
+ bank_count_hax50_end = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 50 HAX coins handed back from the POS to the bank at the end of the business day (counted by the bank responsible)",
+ )
+
+ bank_count_hax100_end = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 100 HAX coins handed back from the POS to the bank at the end of the business day (counted by the bank responsible)",
+ )
+
+ # pos count end of day
+
+ pos_count_dkk_end = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of DKK handed back from the POS to the bank at the end of the business day (counted by the POS responsible)",
+ )
+
+ pos_count_hax5_end = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 5 HAX coins received by the bank from the POS at the end of the business day (counted by the POS responsible)",
+ )
+
+ pos_count_hax10_end = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 10 HAX coins received by the bank from the POS at the end of the business day (counted by the POS responsible)",
+ )
+
+ pos_count_hax20_end = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 20 HAX coins received by the bank from the POS at the end of the business day (counted by the POS responsible)",
+ )
+
+ pos_count_hax50_end = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 50 HAX coins received by the bank from the POS at the end of the business day (counted by the POS responsible)",
+ )
+
+ pos_count_hax100_end = models.PositiveIntegerField(
+ default=0,
+ help_text="The number of 100 HAX coins received by the bank from the POS at the end of the business day (counted by the POS responsible)",
+ )
+
+ @property
+ def camp(self):
+ return self.pos.team.camp
+
+ camp_filter = "pos__team__camp"
+
+ def get_absolute_url(self):
+ return reverse(
+ "backoffice:posreport_detail",
+ kwargs={
+ "camp_slug": self.camp.slug,
+ "pos_slug": self.pos.slug,
+ "posreport_uuid": self.uuid,
+ },
+ )
diff --git a/src/requirements/production.txt b/src/requirements/production.txt
index 4cc4ae6f..cdb9c752 100644
--- a/src/requirements/production.txt
+++ b/src/requirements/production.txt
@@ -1,4 +1,4 @@
-Django==3.0.8
+Django==3.1
channels==2.4.0
channels-redis==3.0.1