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 @@ + + + + + + + + + + + {% for pos in pos_list %} + + + + + + + {% endfor %} + +
NameTeamSlugActions
{{ pos.name }}{{ pos.team }}{{ pos.slug }} +
+ Details + {% if perms.camps.orgateam_permission %} + Update + Delete + {% endif %} +
+
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 @@ + + + + + + + + + + + + {% for pr in posreport_list %} + + + + + + + + {% endfor %} + +
DatePosBank ResponsiblePos ResponsibleActions
{{ pr.date }}{{ pr.pos.name }}{{ pr.bank_responsible }}{{ pr.pos_responsible }} +
+ Details + {% if request.user == pr.pos.bank_responsible %} + Bank Count Start + Bank Count End + {% endif %} + {% if request.user == pr.pos.pos_responsible %} + Pos Count Start + Pos Count End + {% endif %} +
+
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.

+
+ {% csrf_token %} + + Cancel +
+
+
+{% 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

+
+
+
+ {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + + Cancel + {% endbuttons %} +
+
+
+{% 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
WhatBank StartBank EndPos StartPos 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

+
+
+
+ {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + + Cancel + {% endbuttons %} +
+
+
+{% 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