diff --git a/website/orders/api/v1/serializers.py b/website/orders/api/v1/serializers.py index e00b5354..c9e3f7fc 100644 --- a/website/orders/api/v1/serializers.py +++ b/website/orders/api/v1/serializers.py @@ -108,10 +108,22 @@ class Meta: "ready_at", "paid", "paid_at", + "picked_up", + "picked_up_at", "type", "priority", ] - read_only_fields = ["id", "created", "user", "product", "order_price", "ready_at", "paid_at"] + read_only_fields = [ + "id", + "created", + "user", + "product", + "order_price", + "ready_at", + "paid_at", + "picked_up_at", + "prioritize", + ] class ShiftSerializer(WritableModelSerializer): diff --git a/website/orders/api/v1/views.py b/website/orders/api/v1/views.py index 863220f2..2cd16e3f 100644 --- a/website/orders/api/v1/views.py +++ b/website/orders/api/v1/views.py @@ -72,7 +72,7 @@ def perform_create(self, serializer): # Save the order while ignoring the order_type, user, paid and ready argument as the user does not have # permissions to save orders for all users in the shift. order = serializer.save( - shift=shift, type=Order.TYPE_ORDERED, user=self.request.user, paid=False, ready=False + shift=shift, type=Order.TYPE_ORDERED, user=self.request.user, paid=False, ready=False, picked_up=False ) log_action(self.request.user, order, CHANGE, "Created order via API.") @@ -108,6 +108,8 @@ class OrderRetrieveUpdateDestroyAPIView(LoggedRetrieveUpdateDestroyAPIView): "ready": {"type": "boolean"}, "paid": {"type": "boolean"}, "priority": {"type": "number"}, + "picked_up": {"type": "boolean"}, + "deprioritize": {"type": "boolean"}, }, } ) diff --git a/website/orders/migrations/0008_order_picked_up_order_picked_up_at.py b/website/orders/migrations/0008_order_picked_up_order_picked_up_at.py new file mode 100644 index 00000000..554576a0 --- /dev/null +++ b/website/orders/migrations/0008_order_picked_up_order_picked_up_at.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.4 on 2023-11-12 09:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("orders", "0007_order_deprioritize"), + ] + + operations = [ + migrations.AddField( + model_name="order", + name="picked_up", + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name="order", + name="picked_up_at", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name="order", + name="picked_up", + field=models.BooleanField(default=False), + ), + ] diff --git a/website/orders/models.py b/website/orders/models.py index 8bb84dd5..e3705ff7 100644 --- a/website/orders/models.py +++ b/website/orders/models.py @@ -438,7 +438,9 @@ def _clean(self): elif old_instance is not None and not old_instance.finalized and self.finalized: # Shift was not finalized yet but will be made finalized now if not self.shift_done: - raise ValidationError({"finalized": "Shift can't be finalized if not all Orders are paid and ready"}) + raise ValidationError( + {"finalized": "Shift can't be finalized if not all Orders are paid, ready and picked up."} + ) if self.end <= self.start: raise ValidationError({"end": "End date cannot be before start date."}) @@ -545,7 +547,10 @@ class Order(models.Model): paid = models.BooleanField(default=False) paid_at = models.DateTimeField(null=True, blank=True) - type = models.PositiveIntegerField(choices=TYPES, default=TYPE_ORDERED) + picked_up = models.BooleanField(default=False) + picked_up_at = models.DateTimeField(null=True, blank=True) + + type = models.PositiveIntegerField(choices=TYPES, default=0) priority = models.PositiveIntegerField(choices=PRIORITIES, default=PRIORITY_NORMAL) @@ -587,12 +592,22 @@ def venue(self): """ return self.shift.venue + @property + def completed(self) -> bool: + """ + Check if an Order is completed. + + :return: True if this Order is paid, ready and picked up, False otherwise. + :rtype: boolean + """ + return self.paid and self.ready and self.picked_up + @property def done(self): """ Check if an Order is done. - :return: True if this Order is paid and ready, False otherwise + :return: True if this Order is paid, ready and picked up, False otherwise :rtype: boolean """ return self.paid and self.ready diff --git a/website/orders/services.py b/website/orders/services.py index 37fdf0b5..9ec03709 100644 --- a/website/orders/services.py +++ b/website/orders/services.py @@ -56,7 +56,7 @@ def execute_data_minimisation(dry_run=False): return users -def add_scanned_order(product: Product, shift: Shift, ready=True, paid=True) -> Order: +def add_scanned_order(product: Product, shift: Shift, ready=True, paid=True, picked_up=True) -> Order: """ Add a single Scanned Order (of type TYPE_SCANNED). @@ -64,6 +64,7 @@ def add_scanned_order(product: Product, shift: Shift, ready=True, paid=True) -> :param shift: The shift for which the Orders have to be created :param ready: Whether the Order should be directly made ready :param paid: Whether the Order should be directly made paid + :param picked_up: Whether the Order should be directly made picked up :return: The created Order """ # Check if Shift is not finalized @@ -79,7 +80,14 @@ def add_scanned_order(product: Product, shift: Shift, ready=True, paid=True) -> raise OrderException("This Product is not available in this Shift") return Order.objects.create( - product=product, shift=shift, type=Order.TYPE_SCANNED, user=None, user_association=None, ready=ready, paid=paid + product=product, + shift=shift, + type=Order.TYPE_SCANNED, + user=None, + user_association=None, + ready=ready, + paid=paid, + picked_up=picked_up, ) @@ -90,6 +98,7 @@ def add_user_order( priority: int = Order.PRIORITY_NORMAL, paid: bool = False, ready: bool = False, + picked_up: bool = False, **kwargs, ) -> Order: """ @@ -101,6 +110,7 @@ def add_user_order( :param priority: Which priority the Order should have :param paid: Whether the order should be set as paid :param ready: Whether the order should be set as ready + :param picked_up: Whether the order should be set as picked up :return: The created Order """ # Check order permissions @@ -147,6 +157,8 @@ def add_user_order( paid=paid, ready=ready, priority=priority, + picked_up=picked_up, + priority=priority, ) diff --git a/website/orders/templates/orders/order_admin_list.html b/website/orders/templates/orders/order_admin_list.html index d70fb6d7..e007fda6 100644 --- a/website/orders/templates/orders/order_admin_list.html +++ b/website/orders/templates/orders/order_admin_list.html @@ -220,6 +220,20 @@

Orders total

}); return orders_ready; }, + orders_picked_up() { + return this.orders.filter(order => order.picked_up); + }, + orders_picked_up_grouped() { + let orders_picked_up = {}; + this.orders_picked_up.forEach(order => { + if (order.product.name in orders_picked_up) { + orders_picked_up[order.product.name].amount += 1; + } else { + orders_picked_up[order.product.name] = {"product": order.product, "amount": 1}; + } + }); + return orders_picked_up; + }, orders_grouped() { let orders_total = {}; this.orders.forEach(order => { @@ -232,10 +246,10 @@

Orders total

return orders_total; }, orders_finished() { - return this.orders.filter(order => order.ready && order.paid && order.type !== 1); + return this.orders.filter(order => order.ready && order.paid && order.picked_up && order.type !== 1); }, orders_to_process() { - return this.orders.filter(order => !order.ready || !order.paid && order.type !== 1); + return this.orders.filter(order => !order.ready || !order.picked_up || !order.paid && order.type !== 1); }, orders_scanned() { return this.orders.filter(order => order.type === 1); @@ -378,6 +392,32 @@

Orders total

} }).catch(error => show_error_from_api(error)); }, + toggle_picked_up(order) { + fetch( + `/api/v1/shifts/{{ shift.id }}/orders/${order.id}/`, + { + method: 'PATCH', + headers: { + "X-CSRFToken": get_csrf_token(), + "Accept": 'application/json', + "Content-Type": 'application/json', + }, + body: JSON.stringify({ + picked_up: !order.picked_up + }) + } + ).then(response => { + if (response.status === 200) { + return response; + } else { + throw response; + } + }).then(() => { + if (typeof (update_refresh_list) !== 'undefined') { + update_refresh_list(); + } + }).catch(error => show_error_from_api(error)); + }, delete_order(order) { if (window.confirm('Do you want to delete this order?')) { fetch( diff --git a/website/orders/templates/orders/order_admin_order.html b/website/orders/templates/orders/order_admin_order.html index 20d94fff..a7b4cf18 100644 --- a/website/orders/templates/orders/order_admin_order.html +++ b/website/orders/templates/orders/order_admin_order.html @@ -1,30 +1,37 @@ \ No newline at end of file diff --git a/website/orders/templates/orders/status_screen.html b/website/orders/templates/orders/status_screen.html new file mode 100644 index 00000000..e425ea52 --- /dev/null +++ b/website/orders/templates/orders/status_screen.html @@ -0,0 +1,95 @@ +{% extends 'tosti/base.html' %} +{% load static %} + +{% block styles %} + +{% endblock %} + +{% block header %} + +{% endblock %} + +{% block page %} +
+
+
+

In progress

+
+
+

Ready

+
+
+
+{% endblock %} + +{% block footer %} + +{% endblock %} + +{% block js %} + +{% endblock %} \ No newline at end of file diff --git a/website/orders/urls.py b/website/orders/urls.py index fbb6ca6d..66f261cb 100644 --- a/website/orders/urls.py +++ b/website/orders/urls.py @@ -12,4 +12,5 @@ path("/admin/", views.ShiftManagementView.as_view(), name="shift_admin"), path("/overview/", views.ShiftView.as_view(), name="shift_overview"), path("/join/", views.JoinShiftView.as_view(), name="shift_join"), + path("/status/", views.StatusScreen.as_view(), name="status"), ] diff --git a/website/orders/views.py b/website/orders/views.py index 41680424..53f8e7c7 100644 --- a/website/orders/views.py +++ b/website/orders/views.py @@ -145,6 +145,17 @@ def post(self, request, **kwargs): return render(request, self.template_name, {"shift": shift}) +class StatusScreen(TemplateView): + """Status screen for a Shift.""" + + template_name = "orders/status_screen.html" + + def get(self, request, **kwargs): + """GET request for status screen view.""" + shift = kwargs.get("shift") + return render(request, self.template_name, {"shift": shift}) + + class AccountHistoryTabView(LoginRequiredMixin, TemplateView): """Account order history view.""" diff --git a/website/tosti/static/tosti/css/status-screen.css b/website/tosti/static/tosti/css/status-screen.css new file mode 100644 index 00000000..217befab --- /dev/null +++ b/website/tosti/static/tosti/css/status-screen.css @@ -0,0 +1,9 @@ +.site-header { + height: 84px; +} + +@media screen and (max-width: 992px) { + .site-header { + height: 59px; + } +} \ No newline at end of file diff --git a/website/tosti/templates/tosti/base.html b/website/tosti/templates/tosti/base.html index 3567b67f..b96bbc7b 100644 --- a/website/tosti/templates/tosti/base.html +++ b/website/tosti/templates/tosti/base.html @@ -184,6 +184,77 @@

Identify yourself


+ {% endblock %}
{% block page %}{% endblock %} @@ -202,76 +273,5 @@

Identify yourself


{% endblock %} {% bootstrap_javascript %} {% block js %}{% endblock %} -