diff --git a/src/backoffice/static/audio/error.mp3 b/src/backoffice/static/audio/error.mp3 new file mode 100644 index 000000000..b5f71f03c Binary files /dev/null and b/src/backoffice/static/audio/error.mp3 differ diff --git a/src/backoffice/static/audio/success.mp3 b/src/backoffice/static/audio/success.mp3 new file mode 100644 index 000000000..cfc69383a Binary files /dev/null and b/src/backoffice/static/audio/success.mp3 differ diff --git a/src/backoffice/static/audio/tada.mp3 b/src/backoffice/static/audio/tada.mp3 new file mode 100644 index 000000000..70a9d27a3 Binary files /dev/null and b/src/backoffice/static/audio/tada.mp3 differ diff --git a/src/backoffice/static/js/inventory_scan.js b/src/backoffice/static/js/inventory_scan.js new file mode 100644 index 000000000..b97436373 --- /dev/null +++ b/src/backoffice/static/js/inventory_scan.js @@ -0,0 +1,56 @@ +document.addEventListener("DOMContentLoaded", () => { + "use strict"; + + const search_form = document.getElementById("search_form"); + const token_input = document.getElementById("token_input"); + + search_form.onsubmit = submit; + + if (document.getElementById("opr_id") && document.getElementById("opr_id")) { + new Audio('/static/audio/success.mp3').play() + } else if (document.getElementById("opr_id")) { + new Audio('/static/audio/tada.mp3').play(); + } else if (!document.getElementById("scan_again").hidden) { + new Audio('/static/audio/error.mp3').play() + } + + function submit(e) { + var product_quantity = document.getElementById("product_quantity"); + e.preventDefault(); + if (token_input.value === "#clear") { + window.location.replace(window.location.pathname); + } else if (product_quantity && token_input.value.split("/").length == 4) { + var quantity = Number(product_quantity.innerHTML) + var opr = document.getElementById("opr_id").innerHTML + var scanned_opr = token_input.value.split("/")[3]; + token_input.value = ""; + if (opr === scanned_opr) { + if (quantity > 1) { + product_quantity.innerHTML = quantity - 1; + new Audio('/static/audio/success.mp3').play() + } else { + if (quantity === 1) { + product_quantity.innerHTML = "0"; + document.getElementById("checkin_qr").removeAttribute("hidden");; + new Audio('/static/audio/tada.mp3').play() + } else { + new Audio('/static/audio/error.mp3').play() + token_input.value = "TO MANY ITEMS SCANNED!!!!!"; + } + } + } else { + new Audio('/static/audio/error.mp3').play() + token_input.value = "WRONG ITEM!!!!!"; + } + } else { + search_form.submit(); + } + } + + document.addEventListener("keydown", event => { + if (event.key === "#") { + token_input.value = ""; + token_input.focus(); + } + }); +}); diff --git a/src/backoffice/static/js/ticket_scan.js b/src/backoffice/static/js/ticket_scan.js index 05163e1d7..7da207316 100644 --- a/src/backoffice/static/js/ticket_scan.js +++ b/src/backoffice/static/js/ticket_scan.js @@ -24,6 +24,26 @@ document.addEventListener("DOMContentLoaded", () => { check_in_form.submit(); } else if (ticket_token_input.value.length === 65) { search_form.submit(); + } else if (ticket_token_input.value.startsWith("#bornhack://opr/")) { + var oprelement = document.getElementById("opr"); + if (oprelement) { + var opr = oprelement.innerHTML + var quantity = document.getElementById("product_quantity"); + var opr_scanned = ticket_token_input.value.split("/")[3] + if (opr == opr_scanned) { + if (quantity && quantity.innerHTML=="1") { + quantity.innerHTML = Number(quantity.innerHTML) - 1; + } else { + document.getElementById("checkin_qr").removeAttribute("hidden"); + } + ticket_token_input.value = ""; + } else { + ticket_token_input.value = "WRONG ITEM"; + } + } else { + scan_again.removeAttribute("hidden"); + ticket_token_input.value = "" + } } else { scan_again.removeAttribute("hidden"); } diff --git a/src/backoffice/templates/includes/index_info.html b/src/backoffice/templates/includes/index_info.html index 304a2eea6..0f1f9df8e 100644 --- a/src/backoffice/templates/includes/index_info.html +++ b/src/backoffice/templates/includes/index_info.html @@ -51,4 +51,15 @@

Shop Ticket Overview

Use this to list shop tickets

+ {% if not camp.read_only %} + +

+ Scan inventory +

+

+ Use this view to scan inventory QR codes to check in inventory. +

+
+ {% endif %} {% endif %} diff --git a/src/backoffice/templates/includes/index_orga.html b/src/backoffice/templates/includes/index_orga.html index d75f699ed..89777030a 100644 --- a/src/backoffice/templates/includes/index_orga.html +++ b/src/backoffice/templates/includes/index_orga.html @@ -8,6 +8,10 @@

Approve Public Credit Names

Merchandise Orders

Use this view to look at Merchandise Orders

+ +

Merchandise Orders Labels

+

Use this view to print Merchandise Order labels

+

Merchandise To Order

Use this view to generate a list of merchandise that needs to be ordered

diff --git a/src/backoffice/templates/info_desk/inventory.html b/src/backoffice/templates/info_desk/inventory.html new file mode 100644 index 000000000..17da50934 --- /dev/null +++ b/src/backoffice/templates/info_desk/inventory.html @@ -0,0 +1,66 @@ +{% extends 'base.html' %} +{% load static %} +{% load qrcode %} + +{% block title %} + Inventory | {{ block.super }} +{% endblock %} + +{% block content %} + +
+ {% csrf_token %} +
+
+

Inventory: {{ pos.name }}

+

Scan the item!

+ +
+ Scan again! +
+
+
+
+
+ {% if opr %} + {% with opr.id|stringformat:"i" as opr_id %} +
+ + + + + + + + + + + {% if opr.quantity > 1 %} + + + + + {% endif %} + + + + + + + + +

Checkin {{ opr.product.category }}

OPR

{{ opr.id }}

Product{{ opr.product.name }}
Quantity to check

{{ opr.quantity|add:-1 }}/{{ opr.quantity }}

Label{{ opr.label_printed|yesno }}
Ready{{ opr.ready_for_pickup|yesno }}
+
+
+
+ {% qr_code "clear" %} +
+
1 %}hidden{% endif %}> + {% qr_code "bornhack://opr/"|add:opr_id|add:"/checkin" %} +
+
+ {% endwith %} + {% endif %} + + +{% endblock content %} diff --git a/src/backoffice/templates/info_desk/inventory_index.html b/src/backoffice/templates/info_desk/inventory_index.html new file mode 100644 index 000000000..14cb79596 --- /dev/null +++ b/src/backoffice/templates/info_desk/inventory_index.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block title %} + Select PoS | Scan Inventory | {{ block.super }} +{% endblock %} + +{% block content %} +
+ {% for pos in pos_list %} + +

{{ pos.name }}

+
+ {% endfor %} +
+{% endblock content %} diff --git a/src/backoffice/templates/info_desk/scan.html b/src/backoffice/templates/info_desk/scan.html index 7f0fd8b55..545384229 100644 --- a/src/backoffice/templates/info_desk/scan.html +++ b/src/backoffice/templates/info_desk/scan.html @@ -33,100 +33,133 @@

Scan the ticket!


- - - + + + {% endif %} + {% if ticket.product.category.name == "Merchandise" %} + + + + + {% endif %} + + + + + + + + + {% endif %} - - + {% if ticket.sponsor %} + + + + + {% endif %} + +
- Type: + + + + + + + + + + + {% if ticket.ticket_type.includes_badge %} + + + + + {% endif %} + {% if ticket.product %} + + + + + {% if ticket.ticket_type.single_ticket_per_product and ticket.shortname == "shop" %} - - - - - -
+ Type: + {{ ticket.ticket_type }} +
+ Used?: + + {% if ticket.used_at %} + True + {% else %} + False + {% endif %} +
+ Badge handed out?: + + {{ ticket.badge_handed_out }} +
+ Product: + + {{ ticket.product }} +
- Used?: - - {% if ticket.used_at %} - True - {% else %} - False - {% endif %} - - {% if ticket.ticket_type.includes_badge %} -
- Badge handed out?: - - {{ ticket.badge_handed_out }} - {% endif %} - - {% if ticket.product %} -
- Product: - - {{ ticket.product }} - {% if ticket.ticket_type.single_ticket_per_product and ticket.shortname == "shop" %} -
- Quantity: - - {{ ticket.quantity }} - {% endif %} -
- Order: - - {{ ticket.order }} -
- User Email: - - {{ ticket.order.user.email }} - {% endif %} - - {% if ticket.sponsor %} -
- Sponsor - - {{ ticket.sponsor }} - {% endif %} - - -
- -
- -
{% csrf_token %} - + Quantity: +
+

{{ ticket.quantity }}

+
+ ID: + +

{{ ticket.opr.id }}

+
+ Order: + + {{ ticket.order }} +
+ User Email: + + {{ ticket.order.user.email }} +
+ Sponsor + + {{ ticket.sponsor }} +
+ +
+ +
{% csrf_token %} + + + +
-
-
- {% qr_code "clear" %} -
+
+
+ {% qr_code "clear" %} +
- {% if not ticket.used_at %} -
- {% qr_code "checkin" %} -
- {% endif %} + {% if not ticket.used_at %} +
+ {% qr_code "checkin" %} +
+ {% endif %} - {% if ticket.ticket_type.includes_badge and not ticket.badge_handed_out %} -
- {% qr_code "handoutbadge" %} -
- {% endif %} -
+ {% if ticket.ticket_type.includes_badge and not ticket.badge_handed_out %} +
+ {% qr_code "handoutbadge" %} +
+ {% endif %} +
{% endif %}
@@ -152,95 +185,109 @@

Order #{{ order.id }}

Total - - {{ order.total }} DKK + + + {{ order.total }} DKK + + + + + Paid? + + + {{ order.paid }} + + + + + + Payment type + + + {{ order.get_payment_method_display }} + + + + + + {% if order.paid %} +

Tickets

+ + + + + + + + + + + + + {% for ticket in tickets %} -
+ Product + + Quantity + + Ticket Type + + Name? + + OPR? + + Status? +
- Paid? - - {{ order.paid }} - -
- Payment type - - {{ order.get_payment_method_display }} - -
- - {% if order.paid %} -

Tickets

- - - - - - - - - - - {% for ticket in tickets %} - - - - - - - - {% endfor %} - - - -
- Product - - Quantity - - Ticket Type - - Name? - - Status? -
- {{ ticket.product.name }} - - {% if ticket.ticket_type.single_ticket_per_product and ticket.shortname == "shop" %} - {{ ticket.quantity }} - {% else %} - - - {% endif %} - - {{ ticket.ticket_type.name }} - - {{ ticket.name }} - - {% if ticket.used_at %} - Checked in - {% else %} -
- {% csrf_token %} - - -
- {% endif %} -
- {% endif %} - - {% if not order.paid and order.payment_method == "in_person" %} -
- {% csrf_token %} - -
- {% endif %} + {{ ticket.product.name }} + + + {% if ticket.ticket_type.single_ticket_per_product and ticket.shortname == "shop" %} + {{ ticket.quantity }} + {% else %} + - + {% endif %} + + + {{ ticket.ticket_type.name }} + + + {{ ticket.name }} + + + {{ ticket.opr.id }} + + + {% if ticket.used_at %} + Checked in + {% else %} +
+ {% csrf_token %} + + +
+ {% endif %} + + + {% endfor %} + + + {% endif %} + + {% if not order.paid and order.payment_method == "in_person" %} +
+ {% csrf_token %} + +
+ {% endif %} {% else %} diff --git a/src/backoffice/templates/merchandise_to_order.html b/src/backoffice/templates/merchandise_to_order.html index 4c0d90634..5793cdb2e 100644 --- a/src/backoffice/templates/merchandise_to_order.html +++ b/src/backoffice/templates/merchandise_to_order.html @@ -16,6 +16,9 @@

Merchandise To Order

This table shows all different merchandise that needs to be ordered
+
+

Total: {{ total_items }}

+

diff --git a/src/backoffice/templates/orders_merchandise.html b/src/backoffice/templates/orders_merchandise.html index 2f29fc7fc..99314e738 100644 --- a/src/backoffice/templates/orders_merchandise.html +++ b/src/backoffice/templates/orders_merchandise.html @@ -27,6 +27,8 @@

Merchandise Orders

Product Quantity Paid + Label + Ready Ticket Generated @@ -39,6 +41,8 @@

Merchandise Orders

{{ productrel.product.name }} {{ productrel.non_refunded_quantity }} {{ productrel.order.paid|yesno }} + {{ productrel.label_printed|yesno }} + {{ productrel.ready_for_pickup|yesno }} {{ productrel.order.ticket_generated|yesno }} {% endfor %} diff --git a/src/backoffice/templates/orders_merchandise_labels.html b/src/backoffice/templates/orders_merchandise_labels.html new file mode 100644 index 000000000..71a9cc195 --- /dev/null +++ b/src/backoffice/templates/orders_merchandise_labels.html @@ -0,0 +1,65 @@ +{% extends 'base.html' %} +{% load commonmark %} +{% load static %} +{% load imageutils %} + +{% block title %} + Merchandise Orders | {{ block.super }} +{% endblock %} + +{% block content %} +
+

Print labels

+
+ Use this view to print labels for merchandise orders.
+
+ This table shows all OrderProductRelations which are Merchandise (not including cancelled orders or refunded OrderProductRelations). The table is initially sorted by order ID and product name but the sorting can be changed by clicking the column headlines (if javascript is enabled). +
+
+
+ +
+
+
+ {% csrf_token %} + + Backoffice + + + + + + + + + + + + + + + + + {% for productrel in orderproductrelation_list %} + + + + + + + + + + + + + {% endfor %} + +
SelectOrderEmailOPR IdProductQuantityPaidLabelReadyTicket Generated
Order #{{ productrel.order.id }}{{ productrel.order.user.email }}{{ productrel.id }}{{ productrel.product.name }}{{ productrel.non_refunded_quantity }}{{ productrel.order.paid|yesno }}{{ productrel.label_ready|yesno }}{{ productrel.ready_for_pickup|yesno }}{{ productrel.order.ticket_generated|yesno }}
+ + Backoffice +
+
+{% endblock content %} diff --git a/src/backoffice/templates/orders_merchandise_labels.zpl b/src/backoffice/templates/orders_merchandise_labels.zpl new file mode 100644 index 000000000..e376f5292 --- /dev/null +++ b/src/backoffice/templates/orders_merchandise_labels.zpl @@ -0,0 +1,32 @@ +{% for productrel in orderproductrelation_list %} +{% with ''|ljust:productrel.non_refunded_quantity as range %} +{% for i in range %} +^XA +^PW600 +^LL200 + +^FXQR Code +^FO20,20 +^BQN,2,6,N,7 +^FDLA,bornhack://opr/{{ productrel.id }}^FS + +^FXOPR +^FO180,30 +^A0N,90,90 +^FD{{ productrel.id }}^FS + +^FXProduct name +^FO190,110 +^A0N,25,25 +^FD{{ productrel.product.name }}^FS + +{% if productrel.non_refunded_quantity > 1 %} +^FXProduct x/z +^FO500,160 +^A0N,25,25 +^FD{{ forloop.counter }}/{{ productrel.non_refunded_quantity }}^FS +{% endif %} +^XZ +{% endfor %} +{% endwith %} +{% endfor %} diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py index 5bb80a822..edab35ff1 100644 --- a/src/backoffice/urls.py +++ b/src/backoffice/urls.py @@ -107,6 +107,7 @@ from .views import MapUserLocationTypeDeleteView from .views import MapUserLocationTypeListView from .views import MapUserLocationTypeUpdateView +from .views import MerchandiseOrdersLabelsView from .views import MerchandiseOrdersView from .views import MerchandiseToOrderView from .views import MobilePayCSVImportView @@ -151,6 +152,8 @@ from .views import RevenueDetailView from .views import RevenueListView from .views import RevenueUpdateView +from .views import ScanInventoryIndexView +from .views import ScanInventoryView from .views import ScanTicketsPosSelectView from .views import ScanTicketsView from .views import ShopTicketOverview @@ -437,6 +440,23 @@ ], ), ), + path( + "scan_inventory/", + include( + [ + path( + "", + ScanInventoryIndexView.as_view(), + name="scan_inventory_index", + ), + path( + "/", + ScanInventoryView.as_view(), + name="scan_inventory", + ), + ], + ), + ), path( "orders/", include( @@ -571,6 +591,11 @@ MerchandiseOrdersView.as_view(), name="merchandise_orders", ), + path( + "merchandise_orders_labels/", + MerchandiseOrdersLabelsView.as_view(), + name="merchandise_orders_labels", + ), path( "merchandise_to_order/", MerchandiseToOrderView.as_view(), diff --git a/src/backoffice/views/infodesk.py b/src/backoffice/views/infodesk.py index a6d80ea22..02babef18 100644 --- a/src/backoffice/views/infodesk.py +++ b/src/backoffice/views/infodesk.py @@ -67,10 +67,13 @@ class ScanTicketsPosSelectView( CampViewMixin, ListView, ): + """Class for the ticket scan index view.""" + model = Pos template_name = "scan_ticket_pos_select.html" def dispatch(self, *args, **kwargs): + """Method for preventing changes to read-only camps.""" if self.camp.read_only: return HttpResponseForbidden("Camp is read-only") return super().dispatch(*args, **kwargs) @@ -89,6 +92,7 @@ class ScanTicketsView( order_search = False def dispatch(self, *args, **kwargs): + """Method for preventing changes to read-only camps.""" if self.camp.read_only: return HttpResponseForbidden("Camp is read-only") return super().dispatch(*args, **kwargs) @@ -98,6 +102,7 @@ def setup(self, *args, **kwargs) -> None: self.pos = Pos.objects.get(team__camp=self.camp, slug=kwargs["pos_slug"]) def get_context_data(self, **kwargs): + """Method for loading the page data.""" context = super().get_context_data(**kwargs) context["pos"] = self.pos @@ -179,10 +184,103 @@ class ShopTicketOverview( context_object_name = "shop_tickets" def get_context_data(self, *, object_list=None, **kwargs): + """Method for loading the page data.""" kwargs["ticket_types"] = TicketType.objects.filter(camp=self.camp) return super().get_context_data(object_list=object_list, **kwargs) +class ScanInventoryView( + LoginRequiredMixin, + InfoTeamPermissionMixin, + CampViewMixin, + TemplateView, +): + """Class for checking in inventory.""" + + template_name = "info_desk/inventory.html" + + opr: None | OrderProductRelation = None + + def dispatch(self, *args, **kwargs): + """Method for preventing changes to read-only camps.""" + if self.camp.read_only: + return HttpResponseForbidden("Camp is read-only") + return super().dispatch(*args, **kwargs) + + def get_context_data(self, **kwargs): + """Method for loading the page data.""" + context = super().get_context_data(**kwargs) + + context["pos"] = Pos.objects.get(team__camp=self.camp, slug=kwargs["pos_slug"]) + + if "token" in self.request.POST: + context["failed"] = False + # Slice to get rid of the first character which is a '#' + try: + search_token = self.request.POST.get("token")[1:].split("/") + token_type = search_token[2] + token_id = int(search_token[3]) + except IndexError: + self.opr = None + context["failed"] = True + return context + token_action = None + if len(search_token) == 5: + token_action = search_token[4] + if token_type == "opr" and not token_action: + try: + self.opr = OrderProductRelation.objects.get(id=token_id) + except OrderProductRelation.DoesNotExist: + self.opr = None + context["failed"] = True + context["opr"] = self.opr + + return context + + def post(self, request, **kwargs): + """Method for saving the checkin.""" + try: + search_token = self.request.POST.get("token")[1:].split("/") + token_type = search_token[2] + token_id = int(search_token[3]) + except IndexError: + messages.error( + self.request, + "Not found.", + ) + return super().get(request, **kwargs) + token_action = None + if len(search_token) == 5: + token_action = search_token[4] + if token_type == "opr" and token_action == "checkin": + self.opr = OrderProductRelation.objects.get(id=token_id) + self.opr.ready_for_pickup = True + self.opr.save() + messages.success( + self.request, + "Item checked in.", + ) + return super().get(request, **kwargs) + + +class ScanInventoryIndexView( + LoginRequiredMixin, + InfoTeamPermissionMixin, + CampViewMixin, + ListView, +): + """View for selecting the POS location for check-in of items.""" + + model = Pos + template_name = "info_desk/inventory_index.html" + + def dispatch(self, *args, **kwargs): + """Method for preventing changes on read-only camp.""" + if self.camp.read_only: + return HttpResponseForbidden("Camp is read-only") + return super().dispatch(*args, **kwargs) + + ################################ # ORDERS & INVOICES diff --git a/src/backoffice/views/orga.py b/src/backoffice/views/orga.py index 8e4b7f6b5..ade297a98 100644 --- a/src/backoffice/views/orga.py +++ b/src/backoffice/views/orga.py @@ -1,14 +1,18 @@ from __future__ import annotations import logging +from datetime import datetime +from typing import TYPE_CHECKING from django.contrib import messages from django.forms import modelformset_factory from django.shortcuts import redirect +from django.shortcuts import render from django.urls import reverse from django.utils import timezone from django.views.generic import ListView from django.views.generic import TemplateView +from django.views.generic import View from django.views.generic.edit import FormView from backoffice.mixins import OrgaTeamPermissionMixin @@ -20,14 +24,22 @@ from tickets.models import TicketType from utils.models import OutgoingEmail +if TYPE_CHECKING: + from django.db.models import QuerySet + from django.http import HttpRequest + from django.http import HttpResponse + logger = logging.getLogger(f"bornhack.{__name__}") class ApproveNamesView(CampViewMixin, OrgaTeamPermissionMixin, ListView): + """View for showing the approved public credit names.""" + template_name = "approve_public_credit_names.html" context_object_name = "profiles" - def get_queryset(self, **kwargs): + def get_queryset(self, **kwargs) -> QuerySet: + """Method for showing the approved public credit names.""" return Profile.objects.filter(public_credit_name_approved=False).exclude( public_credit_name="", ) @@ -38,9 +50,12 @@ def get_queryset(self, **kwargs): class MerchandiseOrdersView(CampViewMixin, OrgaTeamPermissionMixin, ListView): + """View for listing all merchandise orders.""" + template_name = "orders_merchandise.html" - def get_queryset(self, **kwargs): + def get_queryset(self, **kwargs) -> QuerySet: + """Method for listing all merchandise orders.""" return ( OrderProductRelation.objects.not_fully_refunded() .not_cancelled() @@ -52,12 +67,57 @@ def get_queryset(self, **kwargs): ) +class MerchandiseOrdersLabelsView(CampViewMixin, OrgaTeamPermissionMixin, View): + """Class for printing merch labels.""" + + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + """Method for selecting labels to be printed.""" + template_name = "orders_merchandise_labels.html" + oprs = ( + OrderProductRelation.objects.not_fully_refunded() + .not_cancelled() + .filter( + product__category__name="Merchandise", + product__name__startswith=self.camp.title, + order__paid=True, + label_printed=False, + ) + .order_by("product__name", "order") + ) + return render(request, template_name, {"orderproductrelation_list": oprs}) + + def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + """Method for returning the labels in zpl format.""" + ids: list[int] = [] + for key, value in request.POST.dict().items(): + if "opr" in key and value == "on": + item_id = int(key.split("_")[1]) + ids.append(item_id) + template_name = "orders_merchandise_labels.zpl" + oprs = ( + OrderProductRelation.objects.not_fully_refunded() + .not_cancelled() + .filter( + product__category__name="Merchandise", + product__name__startswith=self.camp.title, + order__paid=True, + id__in=ids, + ) + .order_by("product__name", "order") + ) + for opr in oprs: + opr.label_printed = True + opr.save() + date_time = datetime.now().strftime("%Y-%m-%d-%H_%M") + response = render(request, template_name, {"orderproductrelation_list": oprs}) + response["Content-Disposition"] = f"""attachment; filename="{self.camp.slug}_merch_labels_{date_time}.zpl""" "" + return response + + class MerchandiseToOrderView(CampViewMixin, OrgaTeamPermissionMixin, TemplateView): template_name = "merchandise_to_order.html" def get_context_data(self, **kwargs): - camp_prefix = f"BornHack {timezone.now().year}" - order_relations = ( OrderProductRelation.objects.not_fully_refunded() .not_cancelled() @@ -77,6 +137,7 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["merchandise"] = merchandise_orders + context["total_items"] = sum(merchandise_orders.values()) return context diff --git a/src/shop/migrations/0089_orderproductrelation_label_printed_and_more.py b/src/shop/migrations/0089_orderproductrelation_label_printed_and_more.py new file mode 100644 index 000000000..15901a593 --- /dev/null +++ b/src/shop/migrations/0089_orderproductrelation_label_printed_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.21 on 2025-06-15 06:32 +from __future__ import annotations + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("shop", "0088_alter_coinifyapicallback_order"), + ] + + operations = [ + migrations.AddField( + model_name="orderproductrelation", + name="label_printed", + field=models.BooleanField(default=False, help_text="Is the label for this OPR printed"), + ), + migrations.AddField( + model_name="orderproductrelation", + name="ready_for_pickup", + field=models.BooleanField(default=False, help_text="Is this OPR ready for pickup"), + ), + ] diff --git a/src/shop/models.py b/src/shop/models.py index 7640f968e..b528320b5 100644 --- a/src/shop/models.py +++ b/src/shop/models.py @@ -689,6 +689,16 @@ def __str__(self) -> str: ), ) + label_printed = models.BooleanField( + default=False, + help_text="Is the label for this OPR printed", + ) + + ready_for_pickup = models.BooleanField( + default=False, + help_text="Is this OPR ready for pickup", + ) + @property def total(self): """Returns the total price for this OPR considering quantity."""