From 0c3cce0b69dbc75cdf931ab9413c46edfe23f06d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lian?= Date: Thu, 2 Oct 2025 19:22:05 +0200 Subject: [PATCH 01/13] Accept card payments using SumUp Cloud API - Accept card payments (badge/card) input is no longer required - If card/badge is provided create a new user and make a card payment - Orders have a new payment state - Foodtracker only works on paid orders - Add 'card' payments to littleadmin stats --- p0sx/p0sx/settings/base.py | 6 +- p0sx/p0sx/urls.py | 3 +- p0sx/pos/admin.py | 11 +- .../pos/migrations/0021_auto_20251005_0945.py | 33 ++++++ p0sx/pos/migrations/0022_sumupreader.py | 29 +++++ p0sx/pos/models/stock.py | 101 ++++++++++++------ p0sx/pos/models/sumup_cloud.py | 15 +++ p0sx/pos/serializers/stock.py | 79 ++++++++++---- p0sx/pos/service/sumup.py | 41 ++++++- p0sx/pos/views/foodtracker.py | 44 ++++---- p0sx/pos/views/littleadmin.py | 23 +++- p0sx/pos/views/stock.py | 12 +-- p0sx/pos/views/sumup.py | 41 ++++++- p0sx/templates/pos/sale_overview.djhtml | 8 +- 14 files changed, 349 insertions(+), 97 deletions(-) create mode 100644 p0sx/pos/migrations/0021_auto_20251005_0945.py create mode 100644 p0sx/pos/migrations/0022_sumupreader.py create mode 100644 p0sx/pos/models/sumup_cloud.py diff --git a/p0sx/p0sx/settings/base.py b/p0sx/p0sx/settings/base.py index 28746b5..ace25fe 100755 --- a/p0sx/p0sx/settings/base.py +++ b/p0sx/p0sx/settings/base.py @@ -55,7 +55,7 @@ 'rest_framework', 'rest_framework.authtoken', 'django_filters', - 'django_q', + 'django_q' ) MIDDLEWARE = [ @@ -150,11 +150,13 @@ # SumUp affiliate key. Create one on your SumUp account with application-id com.polarparty.p0sx SUMUP_AFFILIATE_KEY = '' -# SumUp callback hostname, must include http:// or https:// and port if required. +# SumUp callback hostname, must include https:// and port if required. SUMUP_CALLBACK_HOSTNAME = '' # SumUp Merchant code SUMUP_MERCHANT_CODE = '' +SUMUP_BEARER_TOKEN = '' + # GeekEvents event id for the current party GE_EVENT_ID = None GE_SSO_SUCCESS_REDIRECT = None diff --git a/p0sx/p0sx/urls.py b/p0sx/p0sx/urls.py index d7c269d..a9681f2 100644 --- a/p0sx/p0sx/urls.py +++ b/p0sx/p0sx/urls.py @@ -40,7 +40,7 @@ OrderLineViewSet, OrderViewSet, PurchaseViewSet) -from pos.views.sumup import SumUpAuthView, get_pending_transactions, set_processing, sumup_callback, sumup_callbackonline +from pos.views.sumup import SumUpAuthView, get_pending_transactions, set_processing, sumup_callback, sumup_callbackonline, sumup_ordercallback from pos.views.user import UserViewSet @@ -119,6 +119,7 @@ # url(r'callback/(?P\d+)', sumup_callback, name='sumup_callback') path('callback/', sumup_callback, name='sumup_callback'), path('callbackonline/', sumup_callbackonline, name='sumup_callbackonline'), + path('order-callback/', sumup_ordercallback, name='sumup_ordercallback'), path('foodtracker/active/', active_orders, name='active_orders'), path('foodtracker/production_station/', production_station, name='production_station'), path('foodtracker/production_station//', production_station_single, name='production_station_single'), diff --git a/p0sx/pos/admin.py b/p0sx/pos/admin.py index 416a56b..8d9f56c 100644 --- a/p0sx/pos/admin.py +++ b/p0sx/pos/admin.py @@ -5,6 +5,7 @@ from pos.models.sumup import SumUpAPIKey, SumUpCard, SumUpOnline, SumUpTerminal, SumUpTransaction from pos.models.user import CreditUpdate, User, GeekeventsToken from pos.models.printer import Printer +from pos.models.sumup_cloud import SumupReader class CreditUpdateAdmin(admin.ModelAdmin): @@ -135,14 +136,16 @@ def has_add_permission(self, request, obj=None): class OrderAdmin(admin.ModelAdmin): - readonly_fields = ('user', 'payment_method', 'cashier', 'authenticated_user') - list_display = ('id', 'user', 'date', 'sum', 'state') + readonly_fields = ('user', 'payment_method', 'cashier', 'authenticated_user', 'payment_state', 'payment_reference') + list_display = ('id', 'user', 'date', 'sum', 'state', 'payment_state', 'payment_method') inlines = [OrderLineInline] class PrinterAdmin(admin.ModelAdmin): pass +class SumupReaderAdmin(admin.ModelAdmin): + pass admin.site.register(User, UserAdmin) admin.site.register(Ingredient, IngredientAdmin) @@ -163,4 +166,6 @@ class PrinterAdmin(admin.ModelAdmin): admin.site.register(FoodLog, FoodLogAdmin) -admin.site.register(Printer, PrinterAdmin) \ No newline at end of file +admin.site.register(Printer, PrinterAdmin) + +admin.site.register(SumupReader, SumupReaderAdmin) \ No newline at end of file diff --git a/p0sx/pos/migrations/0021_auto_20251005_0945.py b/p0sx/pos/migrations/0021_auto_20251005_0945.py new file mode 100644 index 0000000..17226cf --- /dev/null +++ b/p0sx/pos/migrations/0021_auto_20251005_0945.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.25 on 2025-10-05 09:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pos', '0020_printer'), + ] + + operations = [ + migrations.AddField( + model_name='order', + name='payment_reference', + field=models.CharField(blank=True, max_length=256), + ), + migrations.AddField( + model_name='order', + name='payment_state', + field=models.SmallIntegerField(choices=[(0, 'PAID'), (1, 'PENDING'), (2, 'FAILED'), (3, 'CANCELLED')], db_index=True, default=0), + ), + migrations.AlterField( + model_name='discount', + name='payment_method', + field=models.SmallIntegerField(choices=[(0, 'CASH'), (1, 'CREDIT'), (2, 'CARD'), (3, 'VIPPS'), (4, 'PREPAID'), (7, 'UNDO')], default=0), + ), + migrations.AlterField( + model_name='order', + name='payment_method', + field=models.SmallIntegerField(choices=[(0, 'CASH'), (1, 'CREDIT'), (2, 'CARD'), (3, 'VIPPS'), (4, 'PREPAID'), (7, 'UNDO')], db_index=True, default=0), + ), + ] diff --git a/p0sx/pos/migrations/0022_sumupreader.py b/p0sx/pos/migrations/0022_sumupreader.py new file mode 100644 index 0000000..a3d4f1a --- /dev/null +++ b/p0sx/pos/migrations/0022_sumupreader.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.25 on 2025-10-05 10:16 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('pos', '0021_auto_20251005_0945'), + ] + + operations = [ + migrations.CreateModel( + name='SumupReader', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('reader_id', models.CharField(max_length=255, unique=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'SumUp Card Reader', + 'verbose_name_plural': 'SumUp Card Readers', + }, + ), + ] diff --git a/p0sx/pos/models/stock.py b/p0sx/pos/models/stock.py index 35daef6..b3d580b 100644 --- a/p0sx/pos/models/stock.py +++ b/p0sx/pos/models/stock.py @@ -1,26 +1,52 @@ from datetime import timedelta +from enum import IntEnum from django.contrib.auth.models import User as DjangoUser from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver +class OrderState(IntEnum): + Open = 0, + Processing = 1, + Done = 2, + Archived = 3 + +class PaymentMethod(IntEnum): + Cash = 0, + Credit = 1, + Card = 2, + Vipps = 3, + Prepaid = 4, + Undo = 7 + +class PaymentState(IntEnum): + Paid = 0, + Pending = 1, + Failed = 2, + Cancelled = 3 + ORDER_STATE = ( - (0, 'OPEN'), - (1, 'PROCESSING'), - (2, 'DONE'), - (3, 'ARCHIVED') + (int(OrderState.Open), 'OPEN'), + (int(OrderState.Processing), 'PROCESSING'), + (int(OrderState.Done), 'DONE'), + (int(OrderState.Archived), 'ARCHIVED') ) PAYMENT_METHOD = ( - (0, 'CASH'), - (1, 'CREDIT'), - (2, 'CARD'), - (3, 'VIPPS'), - (4, 'PREPAID'), - (5, 'MOBILEPAY'), - (6, 'IZETTLE'), - (7, 'UNDO') + (int(PaymentMethod.Cash), 'CASH'), + (int(PaymentMethod.Credit), 'CREDIT'), + (int(PaymentMethod.Card), 'CARD'), + (int(PaymentMethod.Vipps), 'VIPPS'), + (int(PaymentMethod.Prepaid), 'PREPAID'), + (int(PaymentMethod.Undo), 'UNDO') +) + +PAYMENT_STATE = ( + (int(PaymentState.Paid), 'PAID'), + (int(PaymentState.Pending), 'PENDING'), + (int(PaymentState.Failed), 'FAILED'), + (int(PaymentState.Cancelled), 'CANCELLED') ) @@ -78,8 +104,9 @@ class Order(models.Model): authenticated_user = models.ForeignKey(DjangoUser, on_delete=models.CASCADE) date = models.DateTimeField(auto_now_add=True) state = models.SmallIntegerField(default=0, choices=ORDER_STATE) - payment_method = models.SmallIntegerField( - default=0, choices=PAYMENT_METHOD, db_index=True) + payment_method = models.SmallIntegerField(default=0, choices=PAYMENT_METHOD, db_index=True) + payment_state = models.SmallIntegerField(default=0, choices=PAYMENT_STATE, db_index=True) + payment_reference = models.CharField(max_length=256, blank=True) message = models.CharField(max_length=64, blank=True) def __str__(self): @@ -116,10 +143,11 @@ class OrderLine(models.Model): def __str__(self): s = self.item.name - if len(self.ingredients.all()) > 0: + actual_ingredients = self.ingredients.all() + print(actual_ingredients) + if len(actual_ingredients) > 0: s += ' med ' - s += ', '.join([str(item).lower() - for item in self.ingredients.all()]) + s += ', '.join([str(item).lower() for item in actual_ingredients]) return s @@ -131,16 +159,18 @@ def create(cls, item, order, price, message): class Purchase: - def __init__(self, order, card, undo, cashier_card): + def __init__(self, order): self.id = order.pk self.order = order self.user = order.user_id self.lines = OrderLine.objects.filter(order=order) self.payment_method = order.payment_method self.message = order.message - self.card = card - self.undo = undo - self.cashier_card = cashier_card + self.card = '' if order.user is None else order.user.card + self.undo = order.payment_method == PaymentMethod.Undo + self.cashier_card = order.cashier.card + self.state = order.state + self.payment_state = order.payment_state def __str__(self): s = str(self.order) @@ -152,10 +182,11 @@ def __str__(self): class CreditCheck: - def __init__(self, used, credit_limit): + def __init__(self, used, user): self.used = used - self.credit_limit = credit_limit - self.left = credit_limit - used + self.credit_limit = user.credit + self.left = user.credit - used + self.is_crew = user.is_crew class Discount(models.Model): @@ -181,21 +212,21 @@ def create_logline(sender, instance, **kwargs): if instance.item.created_in_the_kitchen: FoodLog.objects.get_or_create(orderline=instance, state=instance.state) - ol = OrderLine.objects.filter(order=instance.order).exclude(item__created_in_the_kitchen=False) - o = Order.objects.filter(pk=instance.order.id) + order_line = OrderLine.objects.filter(order=instance.order).exclude(item__created_in_the_kitchen=False) + order = Order.objects.filter(pk=instance.order.id) processing = 0 done = 0 archived = 0 - for line in ol: - if line.state == 1: + for line in order_line: + if line.state == OrderState.Processing: processing += 1 - elif line.state == 2: + elif line.state == OrderState.Done: done += 1 - elif line.state == 3: + elif line.state == OrderState.Archived: archived += 1 if processing >= 1: - o.update(state=1) - if done == ol.count(): - o.update(state=2) - if archived == ol.count(): - o.update(state=3) + order.update(state=OrderState.Processing) + if done == order_line.count(): + order.update(state=OrderState.Done) + if archived == order_line.count(): + order.update(state=OrderState.Archived) diff --git a/p0sx/pos/models/sumup_cloud.py b/p0sx/pos/models/sumup_cloud.py new file mode 100644 index 0000000..9e96df7 --- /dev/null +++ b/p0sx/pos/models/sumup_cloud.py @@ -0,0 +1,15 @@ +from django.contrib.auth.models import User as DjangoUser +from django.db import models + + +class SumupReader(models.Model): + name = models.CharField(max_length=255) + reader_id = models.CharField(max_length=255, unique=True) + user = models.ForeignKey(DjangoUser, on_delete=models.RESTRICT) + + def __str__(self): + return self.name + + class Meta: + verbose_name = 'SumUp Card Reader' + verbose_name_plural = 'SumUp Card Readers' diff --git a/p0sx/pos/serializers/stock.py b/p0sx/pos/serializers/stock.py index 09768dc..a766767 100644 --- a/p0sx/pos/serializers/stock.py +++ b/p0sx/pos/serializers/stock.py @@ -1,12 +1,15 @@ -from django.core.exceptions import ValidationError -from django.shortcuts import get_object_or_404 +from django.core.exceptions import ValidationError, ObjectDoesNotExist -from pos.models.stock import Category, Discount, Ingredient, Item, ItemIngredient, Order, OrderLine, Purchase, PAYMENT_METHOD +from pos.models.stock import Category, Discount, Ingredient, Item, ItemIngredient, Order, OrderLine, Purchase, OrderState, PaymentMethod, PaymentState from pos.models.user import User +from pos.models.sumup_cloud import SumupReader from django_q.tasks import async_task from rest_framework import serializers +from rest_framework.exceptions import APIException + +from pos.service import sumup class DiscountSerializer(serializers.ModelSerializer): @@ -93,7 +96,7 @@ class OrderLineSerializer(serializers.ModelSerializer): class Meta: model = OrderLine - fields = ('id', 'ingredients', 'item', 'message') + fields = ('id', 'ingredients', 'item', 'message', 'state') class OrderSerializer(serializers.ModelSerializer): @@ -102,13 +105,14 @@ class OrderSerializer(serializers.ModelSerializer): class Meta: model = Order fields = ('id', 'user', 'date', 'state', - 'payment_method', 'orderlines') + 'payment_method', 'orderlines', 'payment_state') class CreditCheckSerializer(serializers.Serializer): used = serializers.IntegerField() credit_limit = serializers.IntegerField() left = serializers.IntegerField() + is_crew = serializers.BooleanField() def update(self, instance, validated_data): pass @@ -126,6 +130,8 @@ class PurchaseSerializer(serializers.Serializer): message = serializers.CharField(required=False, allow_blank=True) id = serializers.IntegerField(required=False) undo = serializers.BooleanField() + state = serializers.IntegerField(required=False) + payment_state = serializers.IntegerField(required=False) def create(self, validated_data, request): card = validated_data.get('card') @@ -140,24 +146,59 @@ def create(self, validated_data, request): undo = validated_data.get('undo') order_lines = validated_data.get('lines') + order_total = 0 for order_line in order_lines: + ingredients = order_line.get('ingredients') item = order_line.get('item') + + order_total += item.price + sum(i.price for i in ingredients) count = len([line for line in order_lines if line.get('item').id == item.id]) if item.stock < count: raise ValidationError('{} is not in stock'.format(item.name)) + user = None if card: - user = get_object_or_404(User.objects.all(), card__iexact=card) - payment_method = 1 if user.is_crew else 4 + try: + user = User.objects.get(card__iexact=card) + except ObjectDoesNotExist: + user = User( + card=card, + first_name="Navn", + last_name="Navnesen", + phone="", + crew="", + role="", + email="", + credit=0, + is_crew=False + ) + user.save() + payment_method = PaymentMethod.Credit if user.is_crew else PaymentMethod.Prepaid #if payment_method != 1 and crew.is_crew: # raise ValidationError('The user is marked as crew but payment method was not CREDIT') #if payment_method == 1 and not crew.is_crew: # raise ValidationError('Only users marked as crew can use payment method CREDIT') - order = Order.create( - user, cashier, authenticated_user, payment_method, message) - else: - order = Order.create( - None, cashier, authenticated_user, payment_method, message) + + user_orders = Order.objects.filter(user=user) + user_order_lines = OrderLine.objects.filter(order__in=user_orders) + + total = sum(ol.price for ol in user_order_lines) + credit_left = user.credit - total + if credit_left < order_total: + if user.is_crew: + raise ValidationError('Not enough credit left on card') + else: + payment_method = PaymentMethod.Card + + sumup_reader = None + if payment_method == PaymentMethod.Card: + try: + sumup_reader = SumupReader.objects.get(user=authenticated_user) + except SumupReader.DoesNotExist: + raise APIException() + + order = Order.create(user, cashier, authenticated_user, payment_method, message) + order.payment_state = PaymentState.Pending if payment_method == PaymentMethod.Card else PaymentState.Paid order.save() prepared_order = False @@ -169,11 +210,8 @@ def create(self, validated_data, request): price = item.price + sum(i.price for i in ingredients) price = price * -1 if undo else price - count = count * -1 if undo else count - item.stock -= count line = OrderLine.create(item, order, price, message) - item.save() line.save() if len(ingredients): line.ingredients.set((i.pk for i in ingredients)) @@ -182,18 +220,21 @@ def create(self, validated_data, request): if line.item.created_in_the_kitchen and not undo: prepared_order = True else: - line.state = 3 + line.state = OrderState.Archived line.save() # Set the order to ARCHIVED if its not going to the kitchen if not prepared_order: - order.state = 3 + order.state = OrderState.Archived order.save() - else: + + if order.payment_method == PaymentMethod.Card: + sumup.init_order_card_payment(order, reader_id=sumup_reader.reader_id) + elif prepared_order: async_task("pos.services.print_pickup_receipts", order.id, task_name='Pickup receipts for order {id}'.format(id=order.id)) - return Purchase(order, card, undo, cashier_card) + return Purchase(order) def update(self, instance, validated_data): pass diff --git a/p0sx/pos/service/sumup.py b/p0sx/pos/service/sumup.py index 2e713f5..8d49236 100644 --- a/p0sx/pos/service/sumup.py +++ b/p0sx/pos/service/sumup.py @@ -6,7 +6,7 @@ import json import requests -from p0sx.settings.base import SITE_URL, SUMUP_CALLBACK_HOSTNAME, SUMUP_MERCHANT_CODE +from p0sx.settings.base import SITE_URL, SUMUP_CALLBACK_HOSTNAME, SUMUP_MERCHANT_CODE, SUMUP_BEARER_TOKEN, EVENT_NAME API_URL = 'https://api.sumup.com/' @@ -124,3 +124,42 @@ def create_checkout(api_key, tid, amount, phone): else: return False + +def init_order_card_payment(order, reader_id): + """ + Initializes payment with a SumUp terminal + """ + # curl example: + """ + curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer BEARER-TOKEN" + -d '{ + "total_amount": { + "value": 100, + "currency": "NOK", + "minor_unit": 2 + }, + "description": "testbetaling gjort frå kommandolinje", + } + ' https://api.sumup.com/v0.1/merchants/MERCHANT_ID/readers/READER_ID/checkout + """ + payload = { + "total_amount": {"value": int(order.sum * 100), "currency": "NOK", "minor_unit": 2}, + "description": f"{EVENT_NAME} ordre {order.pk}", + "return_url": SUMUP_CALLBACK_HOSTNAME + '/order-callback/' + str(order.pk), + } + url = f"https://api.sumup.com/v0.1/merchants/{SUMUP_MERCHANT_CODE}/readers/{reader_id}/checkout" + headers = {"Authorization": f"Bearer {SUMUP_BEARER_TOKEN}"} + response = requests.post(url, headers=headers, json=payload) + if response.status_code == 201: + order.payment_reference = response.json()["data"]["client_transaction_id"] + order.save() + return response.json() + + +def get_sumup_transaction(transaction_id): + headers = {"Authorization": f"Bearer {SUMUP_BEARER_TOKEN}"} + url = f"https://api.sumup.com/v2.1/merchants/{SUMUP_MERCHANT_CODE}/transactions?client_transaction_id={transaction_id}" + response = requests.get(url, headers=headers) + if response.status_code == 200: + return response.json() + return None \ No newline at end of file diff --git a/p0sx/pos/views/foodtracker.py b/p0sx/pos/views/foodtracker.py index 87d625b..5eb8f97 100644 --- a/p0sx/pos/views/foodtracker.py +++ b/p0sx/pos/views/foodtracker.py @@ -4,16 +4,20 @@ from django.shortcuts import render from django.utils import timezone -from pos.models.stock import Category, FoodLog, Order, OrderLine +from pos.models.stock import Category, FoodLog, Order, OrderLine, PaymentState, OrderState +def get_order_lines(state): + orders = Order.objects.filter(payment_state=PaymentState.Paid) + return OrderLine.objects.filter(order__in=orders, state=state) + def active_orders(request): # Fetch orders without status ARCHIVED/DONE and order them. # TODO: Make it possible to change status from this screen. # Important: Update the OrderLines, not the Orders. - open_orders = Order.objects.filter(state=0).order_by('date') - processing_orders = Order.objects.filter(state=1).filter(orderlines__log__state=1).order_by('orderlines__log__timestamp') - done_orders = Order.objects.filter(state=2).filter(orderlines__log__state=2).order_by('orderlines__log__timestamp') + open_orders = Order.objects.filter(state=OrderState.Open, payment_state=PaymentState.Paid).order_by('date') + processing_orders = Order.objects.filter(state=OrderState.Processing, payment_state=PaymentState.Paid).filter(orderlines__log__state=1).order_by('orderlines__log__timestamp') + done_orders = Order.objects.filter(state=OrderState.Done, payment_state=PaymentState.Paid).filter(orderlines__log__state=2).order_by('orderlines__log__timestamp') return render(request, 'pos/active_orders.djhtml', { 'open_orders': open_orders, @@ -26,7 +30,7 @@ def production_station(request): if request.method == 'GET' and 'done' in request.GET: orderline_id = request.GET['done'] orderline = OrderLine.objects.get(pk=orderline_id) - orderline.state = 2 + orderline.state = OrderState.Done orderline.save() url = request.build_absolute_uri(request.path) return HttpResponseRedirect(url) @@ -34,14 +38,14 @@ def production_station(request): elif request.method == 'GET' and 'start' in request.GET: orderline_id = request.GET['start'] orderline = OrderLine.objects.get(pk=orderline_id) - orderline.state = 1 + orderline.state = OrderState.Processing orderline.save() url = request.build_absolute_uri(request.path) return HttpResponseRedirect(url) else: - open_orderlines = OrderLine.objects.filter(state=0).order_by('order__date') - processing_orderlines = OrderLine.objects.filter(state=1).filter(log__state=1).order_by('log__timestamp') + open_orderlines = get_order_lines(OrderState.Open).order_by('order__date') + processing_orderlines = get_order_lines(OrderState.Processing).filter(log__state=1).order_by('log__timestamp') return render(request, 'pos/production_station.djhtml', { 'open_orderlines': open_orderlines, @@ -53,7 +57,7 @@ def production_station_single(request, category): if request.method == 'GET' and 'done' in request.GET: orderline_id = request.GET['done'] orderline = OrderLine.objects.get(pk=orderline_id) - orderline.state = 2 + orderline.state = OrderState.Done orderline.save() url = request.build_absolute_uri(request.path) return HttpResponseRedirect(url) @@ -61,14 +65,14 @@ def production_station_single(request, category): elif request.method == 'GET' and 'start' in request.GET: orderline_id = request.GET['start'] orderline = OrderLine.objects.get(pk=orderline_id) - orderline.state = 1 + orderline.state = OrderState.Processing orderline.save() url = request.build_absolute_uri(request.path) return HttpResponseRedirect(url) else: - open_orderlines = OrderLine.objects.filter(state=0).filter(item__category=category).order_by('order__date') - processing_orderlines = OrderLine.objects.filter(state=1).filter(item__category=category).filter(log__state=1).order_by('log__timestamp') + open_orderlines = get_order_lines(OrderState.Open).filter(item__category=category).order_by('order__date') + processing_orderlines = get_order_lines(OrderState.Processing).filter(item__category=category).filter(log__state=1).order_by('log__timestamp') category_name = Category.objects.get(pk=category) return render(request, 'pos/production_station.djhtml', { @@ -82,7 +86,7 @@ def production_station_exclude(request, category): if request.method == 'GET' and 'done' in request.GET: orderline_id = request.GET['done'] orderline = OrderLine.objects.get(pk=orderline_id) - orderline.state = 2 + orderline.state = OrderState.Done orderline.save() url = request.build_absolute_uri(request.path) return HttpResponseRedirect(url) @@ -90,14 +94,14 @@ def production_station_exclude(request, category): elif request.method == 'GET' and 'start' in request.GET: orderline_id = request.GET['start'] orderline = OrderLine.objects.get(pk=orderline_id) - orderline.state = 1 + orderline.state = OrderState.Processing orderline.save() url = request.build_absolute_uri(request.path) return HttpResponseRedirect(url) else: - open_orderlines = OrderLine.objects.filter(state=0).exclude(item__category=category).order_by('order__date') - processing_orderlines = OrderLine.objects.filter(state=1).exclude(item__category=category).filter(log__state=1).order_by('log__timestamp') + open_orderlines = get_order_lines(OrderState.Open).exclude(item__category=category).order_by('order__date') + processing_orderlines = get_order_lines(OrderState.Processing).exclude(item__category=category).filter(log__state=1).order_by('log__timestamp') category_name = Category.objects.get(pk=category) return render(request, 'pos/production_station.djhtml', { @@ -111,14 +115,14 @@ def delivery_station(request): if request.method == 'GET' and 'delivered' in request.GET: orderline_id = request.GET['delivered'] orderline = OrderLine.objects.get(pk=orderline_id) - orderline.state = 3 + orderline.state = OrderState.Archived orderline.save() url = request.build_absolute_uri(request.path) return HttpResponseRedirect(url) else: - processing_orderlines = OrderLine.objects.filter(state=1).filter(log__state=1).order_by('log__timestamp') - done_orderlines = OrderLine.objects.filter(state=2).filter(log__state=2).order_by('log__timestamp') + processing_orderlines = get_order_lines(OrderState.Processing).filter(log__state=1).order_by('log__timestamp') + done_orderlines = get_order_lines(OrderState.Done).filter(log__state=2).order_by('log__timestamp') return render(request, 'pos/delivery_station.djhtml', { 'processing_orderlines': processing_orderlines, @@ -128,7 +132,7 @@ def delivery_station(request): def delivery_screen(request): time_threshold = timezone.now() - timedelta(minutes=5) - orderlines = OrderLine.objects.filter(state=2) + orderlines = get_order_lines(OrderState.Done) return render(request, 'pos/delivery_screen.djhtml', { 'orderlines': orderlines diff --git a/p0sx/pos/views/littleadmin.py b/p0sx/pos/views/littleadmin.py index 48b6c78..251116e 100644 --- a/p0sx/pos/views/littleadmin.py +++ b/p0sx/pos/views/littleadmin.py @@ -149,7 +149,7 @@ def sale_overview_csv(request): ) writer = csv.writer(response) - writer.writerow(["Category", "Name", "First sold", "Last sold", "Prepaid", "Credit", "Sold"]) + writer.writerow(["Category", "Name", "First sold", "Last sold", "Prepaid", "Credit", "Card", "Sold"]) for item in items: per_payment_method = order_lines.filter(item_id=item['id']) first_sold = per_payment_method.aggregate(Min('first_sold'))['first_sold__min'] @@ -163,7 +163,12 @@ def sale_overview_csv(request): except IndexError: prepaid = {'sold': 0, 'total': 0, 'first_sold': None, 'last_sold': None} - writer.writerow([item['category__name'], item['name'], first_sold, last_sold, prepaid['total'], credit['total'], prepaid['sold'] + credit['sold']]) + try: + card = per_payment_method.filter(order__payment_method=2)[0] + except IndexError: + card = {'sold': 0, 'total': 0, 'first_sold': None, 'last_sold': None} + + writer.writerow([item['category__name'], item['name'], first_sold, last_sold, prepaid['total'], credit['total'], card['total'], prepaid['sold'] + credit['sold'] + card['sold']]) return response @@ -189,7 +194,7 @@ def sale_overview(request): sold=Sum(Case(When(price__gte=0, then=1), default=-1, output_field=IntegerField()))) items = Item.objects.all().values('name', 'category__name', 'id', 'price') - total = {'prepaid': 0, 'credit': 0, 'total': 0} + total = {'prepaid': 0, 'credit': 0, 'card': 0, 'total': 0} overview = {} @@ -205,13 +210,18 @@ def sale_overview(request): prepaid = per_payment_method.filter(order__payment_method=4)[0] except IndexError: prepaid = {'sold': 0, 'total': 0, 'first_sold': None, 'last_sold': None} + try: + card = per_payment_method.filter(order__payment_method=2)[0] + except IndexError: + card = {'sold': 0, 'total': 0, 'first_sold': None, 'last_sold': None} item['first_sold'] = first_sold item['last_sold'] = last_sold item['prepaid'] = prepaid['total'] item['credit'] = credit['total'] - item['sold'] = prepaid['sold'] + credit['sold'] - item['total'] = item['prepaid'] + item['credit'] + item['card'] = card['total'] + item['sold'] = prepaid['sold'] + credit['sold'] + card['sold'] + item['total'] = item['prepaid'] + item['credit'] + item['card'] if item['price'] < 0: item['sold'] *= -1 @@ -223,6 +233,7 @@ def sale_overview(request): total['prepaid'] += item['prepaid'] total['credit'] += item['credit'] + total['card'] += item['card'] total['total'] += item['total'] category_totals = [] @@ -232,12 +243,14 @@ def sale_overview(request): category_total['name'] = "Total" category_total['prepaid'] = 0 category_total['credit'] = 0 + category_total['card'] = 0 category_total['sold'] = 0 category_total['total'] = 0 for category_item in category_items: print(category_item) category_total['prepaid'] = category_total['prepaid'] + category_item['prepaid'] category_total['credit'] = category_total['credit'] + category_item['credit'] + category_total['card'] = category_total['card'] + category_item['card'] category_total['sold'] = category_total['sold'] + category_item['sold'] category_total['total'] = category_total['total'] + category_item['total'] category_items.append(category_total) diff --git a/p0sx/pos/views/stock.py b/p0sx/pos/views/stock.py index 5815cc9..d677dbb 100644 --- a/p0sx/pos/views/stock.py +++ b/p0sx/pos/views/stock.py @@ -2,7 +2,7 @@ from django.shortcuts import get_object_or_404 -from pos.models.stock import Category, CreditCheck, Discount, Item, Order, OrderLine, Purchase +from pos.models.stock import Category, CreditCheck, Discount, Item, Order, OrderLine, Purchase, PaymentMethod from pos.models.user import User from pos.serializers.stock import (CategorySerializer, CreditCheckSerializer, @@ -48,15 +48,12 @@ class PurchaseViewSet(viewsets.ViewSet): def list(self, request): orders = Order.objects.all() - queryset = [] - for order in orders: - queryset.append(Purchase(order)) + queryset = [Purchase(o) for o in orders] serializer = PurchaseSerializer(queryset, many=True) return Response(serializer.data) def retrieve(self, request, pk=None): - orders = Order.objects.all() - order = get_object_or_404(orders, pk=pk) + order = get_object_or_404(Order, pk=pk) queryset = Purchase(order) serializer = PurchaseSerializer(queryset) return Response(serializer.data) @@ -87,9 +84,8 @@ def retrieve(request, pk=None): orderlines = OrderLine.objects.filter(order__in=orders) total = sum(ol.price for ol in orderlines) - credit_limit = user.credit - queryset = CreditCheck(total, credit_limit) + queryset = CreditCheck(total, user) serializer = CreditCheckSerializer(queryset) return Response(serializer.data) diff --git a/p0sx/pos/views/sumup.py b/p0sx/pos/views/sumup.py index 4fd32a5..0f87bee 100644 --- a/p0sx/pos/views/sumup.py +++ b/p0sx/pos/views/sumup.py @@ -1,4 +1,3 @@ -from tabnanny import check import uuid from datetime import timedelta from urllib.parse import urlencode, urljoin @@ -12,12 +11,17 @@ from django.views.decorators.csrf import csrf_exempt from django.views.generic import View +from django_q.tasks import async_task + +from pos.models.stock import Order, PaymentState, OrderState from pos.models.sumup import SumUpAPIKey, SumUpCard, SumUpOnline -from pos.service.sumup import API_URL, fetch_onlinetransaction_status, fetch_transaction_status +from pos.service.sumup import API_URL, fetch_onlinetransaction_status, fetch_transaction_status, get_sumup_transaction import json import requests +import logging +logger = logging.getLogger(__name__) @permission_required('pos.update_credit') def get_pending_transactions(request): @@ -91,6 +95,39 @@ def sumup_callbackonline(request, tid): return HttpResponse('OK') +@csrf_exempt +def sumup_ordercallback(request, order_id): + callback = json.loads(request.body) + transaction_id = callback['payload']['client_transaction_id'] + logger.debug(callback) + status = callback['payload']['status'] + + try: + order = Order.objects.get(pk=order_id, payment_reference=transaction_id) + transaction = get_sumup_transaction(transaction_id) + logger.debug(transaction) + transaction_status = None if transaction is None else transaction['status'] + if transaction_status == 'SUCCESSFUL': + order.payment_state = PaymentState.Paid + if order.state == OrderState.Open: + async_task("pos.services.print_pickup_receipts", order.id, + task_name='Pickup receipts for order {id}'.format(id=order.id)) + order.save() + elif transaction_status == 'FAILED': + order.payment_state = PaymentState.Failed + order.save() + elif transaction_status == 'CANCELLED': + order.payment_state = PaymentState.Cancelled + order.save() + elif transaction_status == 'PENDING': + logger.info(f"Payment for order {order_id} is still pending...") + + return HttpResponse('OK') + except: + logger.error(f"pk: {order_id} client_transaction_id: '{transaction_id}' failed to get order") + return HttpResponse(status=500) + + def set_processing(request, transaction): tr = SumUpCard.objects.get(id=transaction) tr.status = 1 diff --git a/p0sx/templates/pos/sale_overview.djhtml b/p0sx/templates/pos/sale_overview.djhtml index ef573cb..9950961 100644 --- a/p0sx/templates/pos/sale_overview.djhtml +++ b/p0sx/templates/pos/sale_overview.djhtml @@ -34,6 +34,7 @@ Sale overview Sold Prepaid Credit + Card Total Crew factor @@ -51,6 +52,7 @@ Sale overview {{ item.sold }} {{ item.prepaid }} {{ item.credit }} + {{ item.card }} {{ item.total }} {% widthratio item.credit item.total 100 %}% @@ -69,6 +71,7 @@ Sale overview Sold Prepaid Credit + Card Total Crew factor @@ -79,6 +82,7 @@ Sale overview {{ category.sold }} {{ category.prepaid }} {{ category.credit }} + {{ category.card }} {{ category.total }} {% widthratio category.credit category.total 100 %}% @@ -94,6 +98,7 @@ Sale overview Prepaid Credit + Card Total Crew factor @@ -101,8 +106,9 @@ Sale overview {{ total.prepaid }} {{ total.credit }} + {{ total.card }} {{ total.total }} - {% widthratio total.credit total.total 100 %}% + {% widthratio total.credit total.total 100 %}% {% endblock content %} From 59c55d81a33fe2768c94a7574bfb1a9fe37e3ed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lian?= Date: Sun, 5 Oct 2025 19:35:48 +0200 Subject: [PATCH 02/13] Adopt the SumUp reader via Django admin --- p0sx/pos/admin.py | 29 +++++++++++++++++++++++++++-- p0sx/pos/models/sumup_cloud.py | 5 +++++ p0sx/pos/service/sumup.py | 20 +++++++++++++++++++- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/p0sx/pos/admin.py b/p0sx/pos/admin.py index 8d9f56c..ad329e1 100644 --- a/p0sx/pos/admin.py +++ b/p0sx/pos/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin +from django import forms from pos.models.shift import Shift from pos.models.stock import Category, Discount, FoodLog, Ingredient, Item, ItemIngredient, Order, OrderLine @@ -6,7 +7,7 @@ from pos.models.user import CreditUpdate, User, GeekeventsToken from pos.models.printer import Printer from pos.models.sumup_cloud import SumupReader - +from pos.service.sumup import pair_sumup_reader class CreditUpdateAdmin(admin.ModelAdmin): readonly_fields = ('timestamp', 'amount', 'user', 'updated_by_user', 'geekevents_id') @@ -144,8 +145,32 @@ class OrderAdmin(admin.ModelAdmin): class PrinterAdmin(admin.ModelAdmin): pass + +class SumupReaderAdminForm(forms.ModelForm): + pairing_code = forms.CharField(max_length=255, required=True) + + class Meta: + model = SumupReader + fields = ('name', 'user', 'reader_id') # Include all model fields, or specify a subset + class SumupReaderAdmin(admin.ModelAdmin): - pass + form = SumupReaderAdminForm + readonly_fields = ('reader_id',) + + def get_form(self, request, obj=None, change=False, **kwargs): + form = super().get_form(request, obj, **kwargs) + + if obj: + del form.base_fields['pairing_code'] + + return form + + def save_model(self, request, obj, form, change): + if not change: + obj.reader_id = pair_sumup_reader(form.cleaned_data.get('name'), form.cleaned_data.get('pairing_code')) + + super().save_model(request, obj, form, change) # Call the original save_model + admin.site.register(User, UserAdmin) admin.site.register(Ingredient, IngredientAdmin) diff --git a/p0sx/pos/models/sumup_cloud.py b/p0sx/pos/models/sumup_cloud.py index 9e96df7..fac09c2 100644 --- a/p0sx/pos/models/sumup_cloud.py +++ b/p0sx/pos/models/sumup_cloud.py @@ -1,6 +1,7 @@ from django.contrib.auth.models import User as DjangoUser from django.db import models +from pos.service.sumup import delete_sumup_reader class SumupReader(models.Model): name = models.CharField(max_length=255) @@ -10,6 +11,10 @@ class SumupReader(models.Model): def __str__(self): return self.name + def delete(self, *args, **kwargs): + delete_sumup_reader(self.reader_id) + super().delete(*args, **kwargs) + class Meta: verbose_name = 'SumUp Card Reader' verbose_name_plural = 'SumUp Card Readers' diff --git a/p0sx/pos/service/sumup.py b/p0sx/pos/service/sumup.py index 8d49236..403150b 100644 --- a/p0sx/pos/service/sumup.py +++ b/p0sx/pos/service/sumup.py @@ -162,4 +162,22 @@ def get_sumup_transaction(transaction_id): response = requests.get(url, headers=headers) if response.status_code == 200: return response.json() - return None \ No newline at end of file + return None + +def pair_sumup_reader(name, pairing_code): + payload = { + "name": name, + "pairing_code": pairing_code + } + url = f"https://api.sumup.com/v0.1/merchants/{SUMUP_MERCHANT_CODE}/readers" + headers = {"Authorization": f"Bearer {SUMUP_BEARER_TOKEN}"} + response = requests.post(url, headers=headers, json=payload) + if response.status_code == 201: + return response.json()["id"] + return None + +def delete_sumup_reader(reader_id): + url = f"https://api.sumup.com/v0.1/merchants/{SUMUP_MERCHANT_CODE}/readers/{reader_id}" + headers = {"Authorization": f"Bearer {SUMUP_BEARER_TOKEN}"} + response = requests.delete(url, headers=headers) + return response.status_code From a9207c48aa0dea9e5b05510a2c13c3e5032b7e6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lian?= Date: Sun, 5 Oct 2025 20:28:49 +0200 Subject: [PATCH 03/13] Changes from server --- p0sx/p0sx/settings/base.py | 3 ++- p0sx/pos/service/sumup.py | 2 +- p0sx/pos/views/sumup.py | 2 +- requirements/base.txt | 4 ++-- requirements/production.txt | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/p0sx/p0sx/settings/base.py b/p0sx/p0sx/settings/base.py index ace25fe..955d3e0 100755 --- a/p0sx/p0sx/settings/base.py +++ b/p0sx/p0sx/settings/base.py @@ -165,7 +165,8 @@ 'name': 'p0sX', 'workers': 1, 'recycle': 500, - 'timeout': 60, + 'timeout': 10, + 'retry': 10, 'compress': True, 'save_limit': 250, 'queue_limit': 500, diff --git a/p0sx/pos/service/sumup.py b/p0sx/pos/service/sumup.py index 403150b..eebdb3c 100644 --- a/p0sx/pos/service/sumup.py +++ b/p0sx/pos/service/sumup.py @@ -144,7 +144,7 @@ def init_order_card_payment(order, reader_id): """ payload = { "total_amount": {"value": int(order.sum * 100), "currency": "NOK", "minor_unit": 2}, - "description": f"{EVENT_NAME} ordre {order.pk}", + "description": f"{EVENT_NAME} p0sX ordre {order.pk}", "return_url": SUMUP_CALLBACK_HOSTNAME + '/order-callback/' + str(order.pk), } url = f"https://api.sumup.com/v0.1/merchants/{SUMUP_MERCHANT_CODE}/readers/{reader_id}/checkout" diff --git a/p0sx/pos/views/sumup.py b/p0sx/pos/views/sumup.py index 0f87bee..9210dfb 100644 --- a/p0sx/pos/views/sumup.py +++ b/p0sx/pos/views/sumup.py @@ -157,7 +157,7 @@ def init(self, *args, **kwargs): data = { 'response_type': 'code', 'state': instance.access_code_state, - 'scope': 'transactions.history payments', + # 'scope': 'transactions.history payments', 'redirect_uri': urljoin(settings.SUMUP_CALLBACK_HOSTNAME, reverse('littleadmin:sumup_return')), 'client_id': instance.client_id, } diff --git a/requirements/base.txt b/requirements/base.txt index ab43959..eeea661 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,7 +4,7 @@ djangorestframework django-extensions Werkzeug==0.15.3 ipython -https://github.com/kradalby/pyGE/archive/master/0.0.4.zip +https://github.com/kradalby/pyGE/archive/master/0.0.5.zip django-environ django-q -escpos \ No newline at end of file +escpos diff --git a/requirements/production.txt b/requirements/production.txt index c6a2119..3b51caf 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -1,4 +1,4 @@ -r base.txt uwsgi -psycopg2==2.7.5 +psycopg2==2.9.4 pillow==7.1.0 From 426567504afbf74fbe25e4657d8722a421da1609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lian?= Date: Sun, 5 Oct 2025 20:31:00 +0200 Subject: [PATCH 04/13] Remove print --- p0sx/pos/views/littleadmin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/p0sx/pos/views/littleadmin.py b/p0sx/pos/views/littleadmin.py index 251116e..a527fcd 100644 --- a/p0sx/pos/views/littleadmin.py +++ b/p0sx/pos/views/littleadmin.py @@ -247,7 +247,6 @@ def sale_overview(request): category_total['sold'] = 0 category_total['total'] = 0 for category_item in category_items: - print(category_item) category_total['prepaid'] = category_total['prepaid'] + category_item['prepaid'] category_total['credit'] = category_total['credit'] + category_item['credit'] category_total['card'] = category_total['card'] + category_item['card'] From 1a94b240fe0a754d772137dc4d98e2d15fa79914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lian?= Date: Sun, 5 Oct 2025 21:45:54 +0200 Subject: [PATCH 05/13] Fix wrong settings import in sumup --- p0sx/pos/service/sumup.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/p0sx/pos/service/sumup.py b/p0sx/pos/service/sumup.py index eebdb3c..0b94096 100644 --- a/p0sx/pos/service/sumup.py +++ b/p0sx/pos/service/sumup.py @@ -6,7 +6,7 @@ import json import requests -from p0sx.settings.base import SITE_URL, SUMUP_CALLBACK_HOSTNAME, SUMUP_MERCHANT_CODE, SUMUP_BEARER_TOKEN, EVENT_NAME +from django.conf import settings API_URL = 'https://api.sumup.com/' @@ -110,8 +110,8 @@ def create_checkout(api_key, tid, amount, phone): 'checkout_reference': str(tid), 'amount': amount, 'currency': 'NOK', - 'merchant_code': SUMUP_MERCHANT_CODE, - 'return_url': SUMUP_CALLBACK_HOSTNAME + 'callbackonline/' + str(tid), + 'merchant_code': settings.SUMUP_MERCHANT_CODE, + 'return_url': settings.SUMUP_CALLBACK_HOSTNAME + 'callbackonline/' + str(tid), 'description': 'PolarPæng Online ' + phone #'redirect_url': SITE_URL + 'littleadmin/sumup-return/' # + str(tid) } @@ -144,11 +144,11 @@ def init_order_card_payment(order, reader_id): """ payload = { "total_amount": {"value": int(order.sum * 100), "currency": "NOK", "minor_unit": 2}, - "description": f"{EVENT_NAME} p0sX ordre {order.pk}", - "return_url": SUMUP_CALLBACK_HOSTNAME + '/order-callback/' + str(order.pk), + "description": f"{settings.EVENT_NAME} p0sX ordre {order.pk}", + "return_url": settings.SUMUP_CALLBACK_HOSTNAME + '/order-callback/' + str(order.pk), } - url = f"https://api.sumup.com/v0.1/merchants/{SUMUP_MERCHANT_CODE}/readers/{reader_id}/checkout" - headers = {"Authorization": f"Bearer {SUMUP_BEARER_TOKEN}"} + url = f"https://api.sumup.com/v0.1/merchants/{settings.SUMUP_MERCHANT_CODE}/readers/{reader_id}/checkout" + headers = {"Authorization": f"Bearer {settings.SUMUP_BEARER_TOKEN}"} response = requests.post(url, headers=headers, json=payload) if response.status_code == 201: order.payment_reference = response.json()["data"]["client_transaction_id"] @@ -157,8 +157,8 @@ def init_order_card_payment(order, reader_id): def get_sumup_transaction(transaction_id): - headers = {"Authorization": f"Bearer {SUMUP_BEARER_TOKEN}"} - url = f"https://api.sumup.com/v2.1/merchants/{SUMUP_MERCHANT_CODE}/transactions?client_transaction_id={transaction_id}" + headers = {"Authorization": f"Bearer {settings.SUMUP_BEARER_TOKEN}"} + url = f"https://api.sumup.com/v2.1/merchants/{settings.SUMUP_MERCHANT_CODE}/transactions?client_transaction_id={transaction_id}" response = requests.get(url, headers=headers) if response.status_code == 200: return response.json() @@ -169,15 +169,15 @@ def pair_sumup_reader(name, pairing_code): "name": name, "pairing_code": pairing_code } - url = f"https://api.sumup.com/v0.1/merchants/{SUMUP_MERCHANT_CODE}/readers" - headers = {"Authorization": f"Bearer {SUMUP_BEARER_TOKEN}"} + url = f"https://api.sumup.com/v0.1/merchants/{settings.SUMUP_MERCHANT_CODE}/readers" + headers = {"Authorization": f"Bearer {settings.SUMUP_BEARER_TOKEN}"} response = requests.post(url, headers=headers, json=payload) if response.status_code == 201: return response.json()["id"] return None def delete_sumup_reader(reader_id): - url = f"https://api.sumup.com/v0.1/merchants/{SUMUP_MERCHANT_CODE}/readers/{reader_id}" - headers = {"Authorization": f"Bearer {SUMUP_BEARER_TOKEN}"} + url = f"https://api.sumup.com/v0.1/merchants/{settings.SUMUP_MERCHANT_CODE}/readers/{reader_id}" + headers = {"Authorization": f"Bearer {settings.SUMUP_BEARER_TOKEN}"} response = requests.delete(url, headers=headers) return response.status_code From 6722620abf03027f11683e233460ed3e48c27c35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lian?= Date: Mon, 6 Oct 2025 00:35:39 +0200 Subject: [PATCH 06/13] New SumUp stuff when adding credit --- p0sx/p0sx/urls.py | 32 +--- p0sx/pos/admin.py | 15 +- .../pos/migrations/0023_auto_20251005_2217.py | 48 +++++ p0sx/pos/migrations/0024_sumuptransaction.py | 26 +++ p0sx/pos/models/sumup.py | 173 ----------------- p0sx/pos/models/sumup_cloud.py | 11 ++ p0sx/pos/service/sumup.py | 149 ++------------- p0sx/pos/views/littleadmin.py | 130 ++++--------- p0sx/pos/views/remotepay.py | 66 ------- p0sx/pos/views/sumup.py | 179 ++++-------------- p0sx/templates/pos/verify_add_credit.djhtml | 6 +- 11 files changed, 185 insertions(+), 650 deletions(-) create mode 100644 p0sx/pos/migrations/0023_auto_20251005_2217.py create mode 100644 p0sx/pos/migrations/0024_sumuptransaction.py delete mode 100644 p0sx/pos/models/sumup.py delete mode 100644 p0sx/pos/views/remotepay.py diff --git a/p0sx/p0sx/urls.py b/p0sx/p0sx/urls.py index a9681f2..9be7230 100644 --- a/p0sx/p0sx/urls.py +++ b/p0sx/p0sx/urls.py @@ -9,12 +9,10 @@ from pos.views.foodtracker import active_orders, delivery_screen, delivery_station, production_station, production_station_exclude, production_station_single from pos.views.littleadmin import ( - AddUserSumupCredit, add_credit_stats, add_user, add_user_credit, check_credit, - check_sumup_status, credit_edit, credit_overview, crew_report, @@ -26,7 +24,6 @@ verify_add_credit, verify_add_credit_cash ) -from pos.views.remotepay import pay, pay_callback, pay_success, pay_error, pay_hold from pos.views.shift import AllShiftsViewSet, CurrentShiftViewSet, NewShiftViewSet, ShiftViewSet from pos.views.sso import ( add_user, @@ -40,21 +37,14 @@ OrderLineViewSet, OrderViewSet, PurchaseViewSet) -from pos.views.sumup import SumUpAuthView, get_pending_transactions, set_processing, sumup_callback, sumup_callbackonline, sumup_ordercallback +from pos.views.sumup import sumup_ordercallback, sumup_creditcallback from pos.views.user import UserViewSet from rest_framework import routers -sale_url = [ - url(r'^$', RedirectView.as_view(url=reverse_lazy('littleadmin:sale'))), - url(r'overview', sale_overview, name='overview') -] - littleadmin_url = [ url(r'^$', RedirectView.as_view(url=reverse_lazy('littleadmin:overview'))), - url(r'sumup-init/(?P\d+)/$', SumUpAuthView.as_view(), name='sumup_auth', kwargs={'action': 'init'}), - url(r'sumup-return/$', SumUpAuthView.as_view(), name='sumup_return', kwargs={'action': 'sumup_return'}), url(r'check/', check_credit, name='check'), url(r'overview/', credit_overview, name='overview'), url(r'edit_crew_credit/(?P\w+)', credit_edit, name='edit_crew_credit'), @@ -65,18 +55,9 @@ url(r'fetch_from_ge/', fetch_credit_from_ge, name='fetch_credit_from_ge'), url(r'scan_user_card', scan_user_card, name='scan_user_card'), url(r'add_user_credit/(?P\w+)$', add_user_credit, name='add_user_credit'), - url(r'add_user_credit/(?P\w+)/sumup$', AddUserSumupCredit.as_view(), name='add_user_credit_sumup'), - url(r'add_user_credit/(?P\w+)/sumup/(?P\d+)/verify$', AddUserSumupCredit.as_view(), - name='add_user_credit_sumup_verify', kwargs={'verify': True}), url(r'add_user/(?P\w+)', add_user, name='add_user'), url(r'verify_add_credit_cash/(?P\d+)/(?P\d+)', verify_add_credit_cash, name='verify_add_credit_cash'), path('verify_add_credit/', verify_add_credit, name='verify_add_credit'), - # url(r'check_sumup_status/(?P\[a-zA-Z0-9-]+)', check_sumup_status, name='check_sumup_status'), - path('check_sumup_status/', check_sumup_status, name='check_sumup_status'), - # url(r'get_pending_transactions/', get_pending_transactions, name='get_pending_transactions'), - path('get_pending_transactions/', get_pending_transactions, name='get_pending_transactions'), - # url(r'set_processing/(?P\[a-zA-Z0-9-]+)', set_processing, name='set_processing'), - path('set_processing/', set_processing, name='set_processing'), url(r'add_credit_stats', add_credit_stats, name='add_credit_stats'), path('update_ge_user/', update_ge_user, name='update_ge_user'), ] @@ -111,15 +92,10 @@ url(r'^', include(router.urls)), url(r'littleadmin/', include((littleadmin_url, 'pos'), namespace='littleadmin')), url(r'sso/', include((sso_url, 'pos'), namespace='sso')), - path('pay/', pay, name='pay'), - path('pay/callback/', pay_callback, name='pay_callback'), - path('pay/success/', pay_success, name='pay_success'), - path('pay/hold/', pay_hold, name='pay_hold'), - path('pay/error/', pay_error, name='pay_error'), - # url(r'callback/(?P\d+)', sumup_callback, name='sumup_callback') - path('callback/', sumup_callback, name='sumup_callback'), - path('callbackonline/', sumup_callbackonline, name='sumup_callbackonline'), + path('order-callback/', sumup_ordercallback, name='sumup_ordercallback'), + path('credit-callback/', sumup_creditcallback, name='sumup_creditcallback'), + path('foodtracker/active/', active_orders, name='active_orders'), path('foodtracker/production_station/', production_station, name='production_station'), path('foodtracker/production_station//', production_station_single, name='production_station_single'), diff --git a/p0sx/pos/admin.py b/p0sx/pos/admin.py index ad329e1..6329c8a 100644 --- a/p0sx/pos/admin.py +++ b/p0sx/pos/admin.py @@ -3,10 +3,9 @@ from pos.models.shift import Shift from pos.models.stock import Category, Discount, FoodLog, Ingredient, Item, ItemIngredient, Order, OrderLine -from pos.models.sumup import SumUpAPIKey, SumUpCard, SumUpOnline, SumUpTerminal, SumUpTransaction from pos.models.user import CreditUpdate, User, GeekeventsToken from pos.models.printer import Printer -from pos.models.sumup_cloud import SumupReader +from pos.models.sumup_cloud import SumupReader, SumupTransaction from pos.service.sumup import pair_sumup_reader class CreditUpdateAdmin(admin.ModelAdmin): @@ -171,6 +170,9 @@ def save_model(self, request, obj, form, change): super().save_model(request, obj, form, change) # Call the original save_model +class SumupTransactionAdmin(admin.ModelAdmin): + readonly_fields = ('user', 'authenticated_user', 'payment_state', 'payment_reference', 'amount') + admin.site.register(User, UserAdmin) admin.site.register(Ingredient, IngredientAdmin) @@ -183,14 +185,9 @@ def save_model(self, request, obj, form, change): admin.site.register(Discount, DiscountAdmin) admin.site.register(CreditUpdate, CreditUpdateAdmin) -admin.site.register(SumUpAPIKey, SumUpAPIKeyAdmin) -admin.site.register(SumUpTerminal, SumUpTerminalAdmin) -admin.site.register(SumUpTransaction, SumUpTransactionAdmin) -admin.site.register(SumUpCard, SumUpCardAdmin) -admin.site.register(SumUpOnline, SumUpOnlineAdmin) - admin.site.register(FoodLog, FoodLogAdmin) admin.site.register(Printer, PrinterAdmin) -admin.site.register(SumupReader, SumupReaderAdmin) \ No newline at end of file +admin.site.register(SumupReader, SumupReaderAdmin) +admin.site.register(SumupTransaction, SumupTransactionAdmin) \ No newline at end of file diff --git a/p0sx/pos/migrations/0023_auto_20251005_2217.py b/p0sx/pos/migrations/0023_auto_20251005_2217.py new file mode 100644 index 0000000..70d6469 --- /dev/null +++ b/p0sx/pos/migrations/0023_auto_20251005_2217.py @@ -0,0 +1,48 @@ +# Generated by Django 3.2.25 on 2025-10-05 22:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('pos', '0022_sumupreader'), + ] + + operations = [ + migrations.RemoveField( + model_name='sumupcard', + name='authorized_user', + ), + migrations.RemoveField( + model_name='sumupcard', + name='user', + ), + migrations.RemoveField( + model_name='sumuponline', + name='user', + ), + migrations.RemoveField( + model_name='sumupterminal', + name='key', + ), + migrations.RemoveField( + model_name='sumuptransaction', + name='key', + ), + migrations.DeleteModel( + name='SumUpAPIKey', + ), + migrations.DeleteModel( + name='SumUpCard', + ), + migrations.DeleteModel( + name='SumUpOnline', + ), + migrations.DeleteModel( + name='SumUpTerminal', + ), + migrations.DeleteModel( + name='SumUpTransaction', + ), + ] diff --git a/p0sx/pos/migrations/0024_sumuptransaction.py b/p0sx/pos/migrations/0024_sumuptransaction.py new file mode 100644 index 0000000..68b84da --- /dev/null +++ b/p0sx/pos/migrations/0024_sumuptransaction.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.25 on 2025-10-05 22:18 + +from django.db import migrations, models +import django.db.models.deletion +import pos.models.stock + + +class Migration(migrations.Migration): + + dependencies = [ + ('pos', '0023_auto_20251005_2217'), + ] + + operations = [ + migrations.CreateModel( + name='SumupTransaction', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.IntegerField()), + ('payment_state', models.SmallIntegerField(choices=[(0, 'PAID'), (1, 'PENDING'), (2, 'FAILED'), (3, 'CANCELLED')], default=pos.models.stock.PaymentState['Pending'])), + ('payment_reference', models.CharField(blank=True, max_length=256, null=True)), + ('authenticated_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transaction_authorized_user', to='pos.user')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transaction_user', to='pos.user')), + ], + ), + ] diff --git a/p0sx/pos/models/sumup.py b/p0sx/pos/models/sumup.py deleted file mode 100644 index 038cdbb..0000000 --- a/p0sx/pos/models/sumup.py +++ /dev/null @@ -1,173 +0,0 @@ -import uuid -from datetime import timedelta - -from django.contrib.auth.models import User as DjangoUser -from django.db import models, transaction -from django.utils import timezone - -from pos.models.user import User -from pos.service import sumup as service - - -ACCESS_CODE_TIMEOUT = 60 - - -class SumUpAPIKey(models.Model): - """ - One single API key can be used for multiple terminals - """ - client_id = models.CharField(max_length=512) - client_secret = models.CharField(max_length=512) - access_code_state = models.UUIDField(null=True, blank=True) - - token = models.CharField(max_length=256, null=True, blank=True) - token_expiry = models.DateTimeField(null=True, blank=True) - - refresh_token = models.CharField(max_length=256, null=True, blank=True) - refresh_token_expiry = models.DateTimeField(null=True, blank=True) - - def __str__(self): - return self.client_id - - @property - def token_expired(self): - return self.token_expiry <= timezone.now() - - def refresh_current_token(self): - service.refresh_token(self) - - def get_unhandled_transactions(self, seconds=300): - """ - Get the latest unhandled transactions - - :param seconds: Number of seconds to cover - :return: QuerySet(SumUpTransaction) - """ - service.update_transactions(self, seconds=seconds) - transactions = self.transactions.all().filter(timestamp__gte=timezone.now() - timedelta(seconds=seconds)) - return transactions.filter(handled=False) - - -class SumUpTerminal(models.Model): - """ - Aa physical terminal - """ - key = models.ForeignKey(SumUpAPIKey, related_name='terminals', on_delete=models.CASCADE) - serial = models.CharField(max_length=32) - - -class SumUpTransaction(models.Model): - def __str__(self): - return '{} - {}kr ({}) [{}]'.format( - self.transaction_code, - self.amount, - self.summary, - 'Handled' if self.handled else 'Unhandled' - ) - key = models.ForeignKey(SumUpAPIKey, related_name='transactions', on_delete=models.CASCADE) - handled = models.BooleanField(default=False) - - transaction_id = models.CharField(max_length=256) - transaction_code = models.CharField(max_length=64) - user = models.CharField(max_length=256) - status = models.CharField(max_length=64) - summary = models.CharField(max_length=256) - amount = models.DecimalField(max_digits=9, decimal_places=2) - timestamp = models.DateTimeField() - - @transaction.atomic - def use_on_user(self, user, crew_user): - """ - - :param user: The user to add the credit to - :param crew_user: The crew user adding the credit - :return: True - """ - if not self.status == 'SUCCESSFUL': - return False - user.credit += self.amount - user.save() - - from pos.models.user import CreditUpdate - cu = CreditUpdate.create(user, crew_user, self.amount) - cu.save() - self.handled = True - self.save() - return True - - -class SumUpCard(models.Model): - - def __str__(self): - return '{} to {} with status {}'.format(self.amount, self.user, self.get_status_display()) - SUMUP_STATUS = [ - (0, 'CREATED'), - (1, 'PROCESSING'), - (2, 'SUCCESS'), - (3, 'FAILED'), - (4, 'COMPLETE'), - ] - - id = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True) - user = models.ForeignKey(User, on_delete=models.PROTECT) - authorized_user = models.ForeignKey(DjangoUser, on_delete=models.PROTECT) - amount = models.DecimalField(max_digits=9, decimal_places=2) - status = models.SmallIntegerField(choices=SUMUP_STATUS, default=0) - transaction_id = models.CharField(max_length=64, null=True, blank=True) - transaction_comment = models.CharField(max_length=256, null=True, blank=True) - created = models.DateTimeField(auto_now_add=True) - timestamp = models.DateTimeField(auto_now=True) - - @transaction.atomic - def update_user(self): - if not self.status == 2: - return False - - user = self.user - user.credit += self.amount - user.save() - - from pos.models.user import CreditUpdate - cu = CreditUpdate.sumup_create(user=self.user, amount=self.amount) - cu.save() - self.status = 4 - self.save() - return True - - -class SumUpOnline(models.Model): - - def __str__(self): - return '{} to {} with status {}'.format(self.amount, self.user, self.get_status_display()) - SUMUP_STATUS = [ - (0, 'CREATED'), - (1, 'PROCESSING'), - (2, 'SUCCESS'), - (3, 'FAILED'), - (4, 'COMPLETE'), - ] - - id = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True) - user = models.ForeignKey(User, on_delete=models.PROTECT) - amount = models.DecimalField(max_digits=9, decimal_places=2) - status = models.SmallIntegerField(choices=SUMUP_STATUS, default=0) - transaction_id = models.CharField(max_length=64, null=True, blank=True) - transaction_comment = models.CharField(max_length=256, null=True, blank=True) - created = models.DateTimeField(auto_now_add=True) - timestamp = models.DateTimeField(auto_now=True) - - @transaction.atomic - def update_user(self): - if not self.status == 2: - return False - - user = self.user - user.credit += self.amount - user.save() - - from pos.models.user import CreditUpdate - cu = CreditUpdate.sumup_create(user=self.user, amount=self.amount) - cu.save() - self.status = 4 - self.save() - return True diff --git a/p0sx/pos/models/sumup_cloud.py b/p0sx/pos/models/sumup_cloud.py index fac09c2..5aa9d91 100644 --- a/p0sx/pos/models/sumup_cloud.py +++ b/p0sx/pos/models/sumup_cloud.py @@ -2,6 +2,8 @@ from django.db import models from pos.service.sumup import delete_sumup_reader +from pos.models.stock import PAYMENT_STATE, PaymentState +from pos.models.user import User class SumupReader(models.Model): name = models.CharField(max_length=255) @@ -18,3 +20,12 @@ def delete(self, *args, **kwargs): class Meta: verbose_name = 'SumUp Card Reader' verbose_name_plural = 'SumUp Card Readers' + + +class SumupTransaction(models.Model): + user = models.ForeignKey(User, related_name='transaction_user', on_delete=models.CASCADE) + authenticated_user = models.ForeignKey(User, related_name='transaction_authorized_user', on_delete=models.CASCADE) + amount = models.IntegerField() + payment_state = models.SmallIntegerField(default=PaymentState.Pending, choices=PAYMENT_STATE) + payment_reference = models.CharField(max_length=256, blank=True, null=True) + used = models.BooleanField(default=False) diff --git a/p0sx/pos/service/sumup.py b/p0sx/pos/service/sumup.py index 0b94096..37602f2 100644 --- a/p0sx/pos/service/sumup.py +++ b/p0sx/pos/service/sumup.py @@ -1,9 +1,3 @@ -from datetime import datetime, timedelta -from urllib.parse import urljoin - -from django.utils import timezone - -import json import requests from django.conf import settings @@ -11,137 +5,10 @@ API_URL = 'https://api.sumup.com/' - -def update_transactions(api_key, seconds=300): - if api_key.token_expired: - api_key.refresh_current_token() - url = urljoin(API_URL, '/v0.1/me/transactions/history') - data = { - 'changes_since': (datetime.now() - timedelta(seconds=seconds)).isoformat() - } - req = requests.get( - url=url, - data=data, - headers={'Authorization': 'Bearer {}'.format(api_key.token)} - ) - transactions = [] - for item in req.json()['items']: - trans, created = api_key.transactions.get_or_create( - transaction_id=item['transaction_id'], - defaults={ - 'user': item['user'], - 'status': item['status'], - 'summary': item.get('product_summary', ''), - 'transaction_code': item['transaction_code'], - 'amount': item['amount'], - 'timestamp': item['timestamp'] - } - ) - if not created and trans.status != item['status']: - trans.status = item['status'] - trans.save() - transactions.append(trans.pk) - return api_key.transactions.filter(pk__in=transactions) - - -def fetch_transaction_status(api_key, txid): - if api_key.token_expired: - api_key.refresh_current_token() - url = urljoin(API_URL, '/v0.1/me/transactions') - data = { - 'transaction_code': txid - } - req = requests.get( - url=url, - data=data, - headers={'Authorization': 'Bearer {}'.format(api_key.token)} - ) - td = req.json() - if 'status' in td: - if td['status'] == 'SUCCESSFUL': - return True - else: - return False - - -def fetch_onlinetransaction_status(api_key, tid): - if api_key.token_expired: - api_key.refresh_current_token() - url = urljoin(API_URL, '/v0.1/checkouts/' + tid) - req = requests.get( - url=url, - headers={'Authorization': 'Bearer {}'.format(api_key.token)} - ) - td = req.json() - print(td) - if 'status' in td: - if td['status'] == 'PAID': - return True - else: - return False - - -def refresh_token(api_key): - req = requests.post( - url=urljoin(API_URL, 'token'), - data={ - 'grant_type': 'refresh_token', - 'client_id': api_key.client_id, - 'client_secret': api_key.client_secret, - 'refresh_token': api_key.refresh_token - } - ) - ret = req.json() - api_key.token = ret['access_token'] - api_key.token_expiry = timezone.now() + timedelta(seconds=ret['expires_in']) - api_key.refresh_token = ret['refresh_token'] - api_key.refresh_token_expiry = timezone.now() + timedelta(days=180) - print(ret['scope']) - api_key.save() - - -def create_checkout(api_key, tid, amount, phone): - if api_key.token_expired: - api_key.refresh_current_token() - req = requests.post( - url=urljoin(API_URL, '/v0.1/checkouts'), - headers={'Authorization': 'Bearer {}'.format(api_key.token)}, - data={ - 'checkout_reference': str(tid), - 'amount': amount, - 'currency': 'NOK', - 'merchant_code': settings.SUMUP_MERCHANT_CODE, - 'return_url': settings.SUMUP_CALLBACK_HOSTNAME + 'callbackonline/' + str(tid), - 'description': 'PolarPæng Online ' + phone - #'redirect_url': SITE_URL + 'littleadmin/sumup-return/' # + str(tid) - } - ) - ret = req.json() - - if ret['id']: - txid = ret['id'] - return txid - - else: - return False - def init_order_card_payment(order, reader_id): """ Initializes payment with a SumUp terminal """ - # curl example: - """ - curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer BEARER-TOKEN" - -d '{ - "total_amount": { - "value": 100, - "currency": "NOK", - "minor_unit": 2 - }, - "description": "testbetaling gjort frå kommandolinje", - } - ' https://api.sumup.com/v0.1/merchants/MERCHANT_ID/readers/READER_ID/checkout - """ payload = { "total_amount": {"value": int(order.sum * 100), "currency": "NOK", "minor_unit": 2}, "description": f"{settings.EVENT_NAME} p0sX ordre {order.pk}", @@ -155,6 +22,22 @@ def init_order_card_payment(order, reader_id): order.save() return response.json() +def init_credit_fill_payment(transaction, reader_id): + """ + Initializes payment with a SumUp terminal + """ + payload = { + "total_amount": {"value": int(transaction.amount * 100), "currency": "NOK", "minor_unit": 2}, + "description": f"{settings.EVENT_NAME} p0sX ordre {transaction.pk}", + "return_url": settings.SUMUP_CALLBACK_HOSTNAME + '/credit-callback/' + str(transaction.pk), + } + url = f"https://api.sumup.com/v0.1/merchants/{settings.SUMUP_MERCHANT_CODE}/readers/{reader_id}/checkout" + headers = {"Authorization": f"Bearer {settings.SUMUP_BEARER_TOKEN}"} + response = requests.post(url, headers=headers, json=payload) + if response.status_code == 201: + transaction.payment_reference = response.json()["data"]["client_transaction_id"] + transaction.save() + return response.json() def get_sumup_transaction(transaction_id): headers = {"Authorization": f"Bearer {settings.SUMUP_BEARER_TOKEN}"} diff --git a/p0sx/pos/views/littleadmin.py b/p0sx/pos/views/littleadmin.py index a527fcd..09af6d1 100644 --- a/p0sx/pos/views/littleadmin.py +++ b/p0sx/pos/views/littleadmin.py @@ -1,5 +1,5 @@ import csv -from datetime import datetime +from datetime import datetime, timedelta from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required @@ -7,21 +7,18 @@ from django.db.models import Case, IntegerField, Sum, When, Max, Min from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect, render -from django.urls import reverse, reverse_lazy -from django.utils import timezone -from django.utils.decorators import method_decorator -from django.views.generic import TemplateView +from django.urls import reverse_lazy from ..forms import AddCreditForm, AddUserForm, ChangeCreditForm, CheckCreditForm, TimeFilterForm from ..ge_importer import GeekEventsImporter from ..models.shift import Shift -from ..models.stock import Item, Order, OrderLine -from ..models.sumup import SumUpAPIKey, SumUpCard, SumUpTransaction +from ..models.stock import Item, Order, OrderLine, PaymentState from ..models.user import CreditUpdate, GeekeventsToken, User +from ..models.sumup_cloud import SumupTransaction, SumupReader from ..serializers.shift import ShiftSerializer -from pos.service.ge_sso import get_user - +from ..service.ge_sso import get_user +from ..service.sumup import init_credit_fill_payment def check_credit(request): if request.POST: @@ -34,7 +31,7 @@ def check_credit(request): if not user: return HttpResponseRedirect(reverse_lazy('littleadmin:check')) - orders = Order.objects.filter(user_id=user[0].id).order_by('-date')[0:3] + orders = Order.objects.filter(user_id=user[0].pk).order_by('-date')[0:3] return render(request, 'pos/credit_check.djhtml', { 'form': CheckCreditForm(), @@ -239,13 +236,7 @@ def sale_overview(request): category_totals = [] for category_name, category_items in overview.items(): - category_total = {} - category_total['name'] = "Total" - category_total['prepaid'] = 0 - category_total['credit'] = 0 - category_total['card'] = 0 - category_total['sold'] = 0 - category_total['total'] = 0 + category_total = {'name': "Total", 'prepaid': 0, 'credit': 0, 'card': 0, 'sold': 0, 'total': 0} for category_item in category_items: category_total['prepaid'] = category_total['prepaid'] + category_item['prepaid'] category_total['credit'] = category_total['credit'] + category_item['credit'] @@ -254,7 +245,7 @@ def sale_overview(request): category_total['total'] = category_total['total'] + category_item['total'] category_items.append(category_total) - category_total_copy = category_total.copy(); + category_total_copy = category_total.copy() category_total_copy['name'] = category_name category_totals.append(category_total_copy) @@ -356,6 +347,12 @@ def add_user_credit(request, card=None): cash = form.cleaned_data['cash'] user = get_object_or_404(User, card__iexact=card) + try: + sumup_reader = SumupReader.objects.get(user=request.user) + except SumupReader.DoesNotExist: + messages.error(request,"The logged in user has no payment terminal associated with it.") + return redirect('littleadmin:add_user_credit', card) + if user.is_crew: messages.error(request, "You cannot change the credit of Crew") return redirect('littleadmin:scan_user_card') @@ -369,12 +366,12 @@ def add_user_credit(request, card=None): messages.error(request, "The maximum credit that can be added at once is 1000. Add multiple times if more is needed") return redirect('littleadmin:add_user_credit', card) - if(cash): - return redirect('littleadmin:verify_add_credit_cash', user.id, amount) - + if cash: + return redirect('littleadmin:verify_add_credit_cash', user.pk, amount) - transaction = SumUpCard.objects.create(user=user, amount=amount, authorized_user=request.user) - tid = transaction.id + sumup_transaction = SumupTransaction.objects.create(user=user, amount=amount, authenticated_user=request.user) + tid = sumup_transaction.pk + init_credit_fill_payment(sumup_transaction, sumup_reader.reader_id) return redirect('littleadmin:verify_add_credit', tid) else: @@ -387,67 +384,7 @@ def add_user_credit(request, card=None): return redirect('littleadmin:scan_user_card') form = AddCreditForm() - sumup_url = reverse('littleadmin:add_user_credit_sumup', kwargs={'card': card}) - return render(request, 'pos/add_credit.djhtml', {'form': form, 'target': user, 'sumup_url': sumup_url}) - - -def check_sumup_status(request, tid): - obj = SumUpCard.objects.get(pk=tid) - return HttpResponse(obj.status) - - -class AddUserSumupCredit(TemplateView): - UPDATE_SECONDS = 300 - - template_name = 'pos/add_credit_sumup.djhtml' - transaction = None - user = None - - @method_decorator(login_required) - @method_decorator(permission_required('pos.update_credit')) - def dispatch(self, *args, **kwargs): - self.user = get_object_or_404(User, card__iexact=kwargs.get('card', None)) - self.transaction = SumUpTransaction.objects.all().filter(handled=False, pk=kwargs.get('transaction_id', - 0)).first() - return super().dispatch(*args, **kwargs) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['user'] = self.user - for key in SumUpAPIKey.objects.all(): - key.get_unhandled_transactions(self.UPDATE_SECONDS) - context['available'] = SumUpTransaction.objects.all().filter( - timestamp__gte=timezone.now() - datetime.timedelta(seconds=self.UPDATE_SECONDS), - status='SUCCESSFUL', handled=False - ) - context['seconds'] = self.UPDATE_SECONDS - context['url'] = self.request.path - context['form'] = CheckCreditForm() - context['trans'] = self.transaction - return context - - def get(self, *args, **kwargs): - if kwargs.get('verify', None): - self.template_name = 'pos/add_credit_sumup_confirm.djhtml' - return super().get(*args, **kwargs) - - def post(self, *args, **kwargs): - if not self.transaction: - # Crewmember must input badge ID - self.transaction = get_object_or_404( - SumUpTransaction, - pk=self.request.POST.get('transaction_id', None), - handled=False - ) - return HttpResponse(reverse('littleadmin:add_user_credit_sumup_verify', kwargs={ - 'card': self.user.card, - 'transaction_id': self.transaction.pk, - })) - self.crew_badge_id = self.request.POST.get('card', None) - self.crew_member = get_object_or_404(User, card__iexact=self.crew_badge_id) - self.transaction.use_on_user(self.user, self.crew_member) - messages.success(self.request, f"Success! Credit set to {self.user.left}") - return redirect('littleadmin:scan_user_card') + return render(request, 'pos/add_credit.djhtml', {'form': form, 'target': user, 'sumup_url': None}) @permission_required('pos.update_credit') @@ -477,19 +414,19 @@ def add_user(request, card=None): @permission_required('pos.update_credit') def verify_add_credit(request, tid=''): - transaction = SumUpCard.objects.get(pk=tid) - if transaction.status == 4: - messages.success(request, str(transaction.amount) + ' added to ' + str(transaction.user)) + sumup_transaction = SumupTransaction.objects.get(pk=tid) + if sumup_transaction.payment_state == PaymentState.Paid: + messages.success(request, str(sumup_transaction.amount) + ' added to ' + str(sumup_transaction.user)) return HttpResponseRedirect(reverse_lazy('littleadmin:check')) - if transaction.status == 3: + if sumup_transaction.payment_state == PaymentState.Failed or sumup_transaction.payment_state == PaymentState.Cancelled: messages.error(request, 'Transaction failed') return HttpResponseRedirect(reverse_lazy('littleadmin:scan_user_card')) else: return render(request, 'pos/verify_add_credit.djhtml', { 'tid': tid, - 'status': transaction.get_status_display() + 'status': sumup_transaction.get_payment_state_display(), }) @@ -527,17 +464,17 @@ def verify_add_credit_cash(request, user='', amount=''): def group_credit_updates(date, group_by): times = [ - datetime.timedelta(hours=0), - datetime.timedelta(hours=6), - datetime.timedelta(hours=12), - datetime.timedelta(hours=18) + timedelta(hours=0), + timedelta(hours=6), + timedelta(hours=12), + timedelta(hours=18) ] seconds = (date.hour * 60 * 60) + (date.minute * 60) + date.second seconds /= group_by.total_seconds() index = int(seconds) - timestamp = datetime.datetime.combine(date.date(), datetime.datetime.min.time()) + times[index] + timestamp = datetime.combine(date.date(), datetime.min.time()) + times[index] return timestamp @@ -581,7 +518,8 @@ def update_ge_user(request): if ge_id is not None: user = GeekeventsToken.objects.get(ge_user_id=ge_id) if not user: - return; + messages.error(request, f"Failed to find user") + return redirect('/littleadmin/update_ge_user') ge_user = get_user(user.ge_user_id, user.timestamp, user.token) user.user.card = ge_user['usercard'].split('||')[0] user.user.first_name = ge_user['first_name'] @@ -591,7 +529,7 @@ def update_ge_user(request): user.user.save() return redirect('/littleadmin/update_ge_user') - users = GeekeventsToken.objects.all(); + users = GeekeventsToken.objects.all() return render(request, 'pos/update_ge_user.djhtml', { 'users': users diff --git a/p0sx/pos/views/remotepay.py b/p0sx/pos/views/remotepay.py deleted file mode 100644 index f89d250..0000000 --- a/p0sx/pos/views/remotepay.py +++ /dev/null @@ -1,66 +0,0 @@ -from django.http import HttpResponseRedirect -from django.shortcuts import render -from django.views.generic import TemplateView -from pos.service.sumup import API_URL, create_checkout -from pos.models.sumup import SumUpAPIKey, SumUpOnline - -from pos.forms import RemotePayForm -from pos.models.user import User - - -class RemotePayView(TemplateView): - template_name = 'remotepay/pay.djhtml' - - -def pay(request): - if request.method == 'POST': - form = RemotePayForm(request.POST) - - if form.is_valid(): - phone = form.cleaned_data['phone'] - amount = form.cleaned_data['amount'] - # Check if user exists - try: - user = User.objects.get(phone=phone, is_crew=False) - except User.DoesNotExist: - return render(request, 'remotepay/pay.djhtml', {'form': form, 'error': True}) - - # Assuming the user exists, we proceed - t = SumUpOnline.objects.create(user=user, amount=amount) - - try: - txid = create_checkout(SumUpAPIKey.objects.all().last(), t.id, t.amount, user.phone) - t.transaction_id = txid - t.status = 1 - t.save() - return render(request, 'remotepay/process.djhtml', {'txid': txid, 'phone': phone, 'amount': amount}) - except: - return render(request, 'remotepay/pay.djhtml', {'form': form, 'systemerror': True}) - - else: - form = RemotePayForm - - return render(request, 'remotepay/pay.djhtml', {'form': form}) - -def pay_callback(request, checkoutid): - # Get the status of the transaction for the user - t = SumUpOnline.objects.get(transaction_id=checkoutid) - - if (t.status == 0 or t.status == 3): - return HttpResponseRedirect('/pay/error/') - elif (t.status == 4): - return HttpResponseRedirect('/pay/success/') - elif (t.status == 1) or (t.status == 2): - return render(request, 'remotepay/hold.djhtml', {'checkoutid': checkoutid}) - - -def pay_success(request): - return render(request, 'remotepay/success.djhtml') - - -def pay_error(request): - return render(request, 'remotepay/error.djhtml') - - -def pay_hold(request): - return render(request, 'remotepay/hold.djhtml') diff --git a/p0sx/pos/views/sumup.py b/p0sx/pos/views/sumup.py index 9210dfb..f82da69 100644 --- a/p0sx/pos/views/sumup.py +++ b/p0sx/pos/views/sumup.py @@ -1,106 +1,23 @@ -import uuid -from datetime import timedelta -from urllib.parse import urlencode, urljoin - -from django.conf import settings -from django.contrib.auth.decorators import permission_required -from django.http import HttpResponse, HttpResponseBadRequest -from django.shortcuts import get_object_or_404, redirect, render -from django.urls import reverse -from django.utils import timezone +from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt -from django.views.generic import View +from django.db import transaction as django_transaction, IntegrityError from django_q.tasks import async_task -from pos.models.stock import Order, PaymentState, OrderState -from pos.models.sumup import SumUpAPIKey, SumUpCard, SumUpOnline -from pos.service.sumup import API_URL, fetch_onlinetransaction_status, fetch_transaction_status, get_sumup_transaction +from ..models.stock import Order, PaymentState, OrderState +from ..models.sumup_cloud import SumupTransaction +from ..service.sumup import get_sumup_transaction import json -import requests import logging logger = logging.getLogger(__name__) -@permission_required('pos.update_credit') -def get_pending_transactions(request): - transactions = SumUpCard.objects.filter(status=0) - key = settings.SUMUP_AFFILIATE_KEY - callback = settings.SUMUP_CALLBACK_HOSTNAME - return render(request, 'pos/sumupcard.djhtml', { - 'transactions': transactions, - 'key': key, - 'url': callback - }) - - -def sumup_callback(request, tid): - callback = request.GET - tr = SumUpCard.objects.get(id=tid, status=1) - if tr: - if callback.get('smp-status') == 'failed': - tr.status = 3 - tr.transaction_id = callback.get('smp-tx-code') - tr.transaction_comment = callback.get('smp-message') - tr.save() - - elif callback.get('smp-status') == 'success': - api_key = SumUpAPIKey.objects.last() - txstatus = fetch_transaction_status(api_key, callback.get('smp-tx-code')) - if txstatus is True: - tr.status = 2 - tr.transaction_id = callback.get('smp-tx-code') - tr.transaction_comment = callback.get('smp-message') - tr.save() - tr.update_user() - return HttpResponse('') - - -# {"id":"88b907dd-a6ca-441f-b264-b3d02a8d1f79","status":"SUCCESSFUL","event_type":"CHECKOUT_STATUS_CHANGED"} -@csrf_exempt -def sumup_callbackonline(request, tid): - callback = json.loads(request.body) - checkoutid = callback['id'] - status = callback['status'] - - try: - tr = SumUpOnline.objects.get(id=tid, transaction_id=checkoutid, status=1) - - except: - print('DEBUG: Id:' + str(tid) + ' CheckoutId:' + checkoutid + ' combo not found!') - - if status == 'SUCCESSFUL': - api_key = SumUpAPIKey.objects.last() - transactionstatus = fetch_onlinetransaction_status(api_key, checkoutid) - if transactionstatus is True: - print('transaction checked and OK') - tr.status = 2 - tr.transaction_comment = status - tr.save() - tr.update_user() - - else: - print('transaction could not be confirmed ' + checkoutid) - tr.status = 3 - tr.transaction_comment = 'NOT CONFIRMED' - tr.save() - - else: - print('transaction failed') - tr.status = 3 - tr.transaction_comment = status - tr.save() - - return HttpResponse('OK') - - @csrf_exempt def sumup_ordercallback(request, order_id): callback = json.loads(request.body) transaction_id = callback['payload']['client_transaction_id'] logger.debug(callback) - status = callback['payload']['status'] try: order = Order.objects.get(pk=order_id, payment_reference=transaction_id) @@ -127,60 +44,38 @@ def sumup_ordercallback(request, order_id): logger.error(f"pk: {order_id} client_transaction_id: '{transaction_id}' failed to get order") return HttpResponse(status=500) +@csrf_exempt +def sumup_creditcallback(request, transaction_id): + callback = json.loads(request.body) + client_transaction_id = callback['payload']['client_transaction_id'] + logger.debug(callback) -def set_processing(request, transaction): - tr = SumUpCard.objects.get(id=transaction) - tr.status = 1 - tr.save() - return HttpResponse('OK') - - -class SumUpAuthView(View): - - def dispatch(self, *args, **kwargs): - self.action = kwargs.pop('action', None) - self.instance_id = kwargs.get('instance_id', None) - if not self.action: - return HttpResponseBadRequest() - return super().dispatch(*args, **kwargs) - - def get(self, *args, **kwargs): - func = getattr(self, self.action, None) - if not func: - return HttpResponseBadRequest() - return func(*args, **kwargs) - - def init(self, *args, **kwargs): - instance = get_object_or_404(SumUpAPIKey, pk=self.instance_id) - instance.access_code_state = uuid.uuid4() - instance.save() - data = { - 'response_type': 'code', - 'state': instance.access_code_state, - # 'scope': 'transactions.history payments', - 'redirect_uri': urljoin(settings.SUMUP_CALLBACK_HOSTNAME, reverse('littleadmin:sumup_return')), - 'client_id': instance.client_id, - } - url = urljoin(API_URL, 'authorize') + '?' + urlencode(data) - return redirect(url) + try: + transaction = SumupTransaction.objects.get(pk=transaction_id, payment_reference=client_transaction_id) + sumup_transaction = get_sumup_transaction(client_transaction_id) + logger.debug(sumup_transaction) + transaction_status = None if sumup_transaction is None else sumup_transaction['status'] + if transaction_status == 'SUCCESSFUL': + try: + with django_transaction.atomic(): + transaction.payment_state = PaymentState.Paid + if not transaction.used: + transaction.used = True + transaction.user.credit += transaction.amount + transaction.user.save() + transaction.save() + except IntegrityError: + return HttpResponse(status=500) + elif transaction_status == 'FAILED': + transaction.payment_state = PaymentState.Failed + transaction.save() + elif transaction_status == 'CANCELLED': + transaction.payment_state = PaymentState.Cancelled + transaction.save() + elif transaction_status == 'PENDING': + logger.info(f"Payment for transaction {transaction_id} is still pending...") - def sumup_return(self, *args, **kwargs): - code = self.request.GET['code'] - state = self.request.GET['state'] - instance = get_object_or_404(SumUpAPIKey, access_code_state=state) - req = requests.post( - url=urljoin(API_URL, 'token'), - data={ - 'grant_type': 'authorization_code', - 'client_id': instance.client_id, - 'client_secret': instance.client_secret, - 'code': code - } - ) - ret = req.json() - instance.token = ret['access_token'] - instance.token_expiry = timezone.now() + timedelta(seconds=ret['expires_in']) - instance.refresh_token = ret['refresh_token'] - instance.refresh_token_expiry = timezone.now() + timedelta(days=180) - instance.save() return HttpResponse('OK') + except: + logger.error(f"pk: {transaction_id} client_transaction_id: '{client_transaction_id}' failed to get transaction") + return HttpResponse(status=500) diff --git a/p0sx/templates/pos/verify_add_credit.djhtml b/p0sx/templates/pos/verify_add_credit.djhtml index 15bb4c2..ac3eb34 100644 --- a/p0sx/templates/pos/verify_add_credit.djhtml +++ b/p0sx/templates/pos/verify_add_credit.djhtml @@ -5,9 +5,9 @@ Kortbetaling {% block content %}

Fullfør kortbetaling med terminalen

-
-
-
+
+
+

Transaksjons-ID: {{ tid }} Status: {{ status }}

From 7a98de92055721ef504e542c282bfa60310beb79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lian?= Date: Mon, 6 Oct 2025 15:58:34 +0200 Subject: [PATCH 07/13] Bugfixes for new sumup credit stuff --- p0sx/p0sx/urls.py | 2 +- p0sx/pos/admin.py | 2 +- .../pos/migrations/0025_auto_20251006_1308.py | 26 +++++++++++++++++++ p0sx/pos/models/sumup_cloud.py | 2 +- p0sx/pos/service/sumup.py | 2 +- p0sx/pos/views/littleadmin.py | 2 +- p0sx/templates/base.djhtml | 6 ++--- p0sx/templates/pos/add_credit.djhtml | 4 +-- p0sx/templates/pos/verify_add_credit.djhtml | 17 ++---------- 9 files changed, 38 insertions(+), 25 deletions(-) create mode 100644 p0sx/pos/migrations/0025_auto_20251006_1308.py diff --git a/p0sx/p0sx/urls.py b/p0sx/p0sx/urls.py index 9be7230..0380298 100644 --- a/p0sx/p0sx/urls.py +++ b/p0sx/p0sx/urls.py @@ -57,7 +57,7 @@ url(r'add_user_credit/(?P\w+)$', add_user_credit, name='add_user_credit'), url(r'add_user/(?P\w+)', add_user, name='add_user'), url(r'verify_add_credit_cash/(?P\d+)/(?P\d+)', verify_add_credit_cash, name='verify_add_credit_cash'), - path('verify_add_credit/', verify_add_credit, name='verify_add_credit'), + path('verify_add_credit/', verify_add_credit, name='verify_add_credit'), url(r'add_credit_stats', add_credit_stats, name='add_credit_stats'), path('update_ge_user/', update_ge_user, name='update_ge_user'), ] diff --git a/p0sx/pos/admin.py b/p0sx/pos/admin.py index 6329c8a..3fc7a01 100644 --- a/p0sx/pos/admin.py +++ b/p0sx/pos/admin.py @@ -171,7 +171,7 @@ def save_model(self, request, obj, form, change): super().save_model(request, obj, form, change) # Call the original save_model class SumupTransactionAdmin(admin.ModelAdmin): - readonly_fields = ('user', 'authenticated_user', 'payment_state', 'payment_reference', 'amount') + readonly_fields = ('user', 'authenticated_user', 'payment_state', 'payment_reference', 'amount', 'used') admin.site.register(User, UserAdmin) diff --git a/p0sx/pos/migrations/0025_auto_20251006_1308.py b/p0sx/pos/migrations/0025_auto_20251006_1308.py new file mode 100644 index 0000000..46aadf5 --- /dev/null +++ b/p0sx/pos/migrations/0025_auto_20251006_1308.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.25 on 2025-10-06 13:08 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('pos', '0024_sumuptransaction'), + ] + + operations = [ + migrations.AddField( + model_name='sumuptransaction', + name='used', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='sumuptransaction', + name='authenticated_user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transaction_authorized_user', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/p0sx/pos/models/sumup_cloud.py b/p0sx/pos/models/sumup_cloud.py index 5aa9d91..a4914fa 100644 --- a/p0sx/pos/models/sumup_cloud.py +++ b/p0sx/pos/models/sumup_cloud.py @@ -24,7 +24,7 @@ class Meta: class SumupTransaction(models.Model): user = models.ForeignKey(User, related_name='transaction_user', on_delete=models.CASCADE) - authenticated_user = models.ForeignKey(User, related_name='transaction_authorized_user', on_delete=models.CASCADE) + authenticated_user = models.ForeignKey(DjangoUser, related_name='transaction_authorized_user', on_delete=models.CASCADE) amount = models.IntegerField() payment_state = models.SmallIntegerField(default=PaymentState.Pending, choices=PAYMENT_STATE) payment_reference = models.CharField(max_length=256, blank=True, null=True) diff --git a/p0sx/pos/service/sumup.py b/p0sx/pos/service/sumup.py index 37602f2..125a490 100644 --- a/p0sx/pos/service/sumup.py +++ b/p0sx/pos/service/sumup.py @@ -28,7 +28,7 @@ def init_credit_fill_payment(transaction, reader_id): """ payload = { "total_amount": {"value": int(transaction.amount * 100), "currency": "NOK", "minor_unit": 2}, - "description": f"{settings.EVENT_NAME} p0sX ordre {transaction.pk}", + "description": f"{settings.EVENT_NAME} P0sX påfyll for {transaction.user.card} - {transaction.pk}", "return_url": settings.SUMUP_CALLBACK_HOSTNAME + '/credit-callback/' + str(transaction.pk), } url = f"https://api.sumup.com/v0.1/merchants/{settings.SUMUP_MERCHANT_CODE}/readers/{reader_id}/checkout" diff --git a/p0sx/pos/views/littleadmin.py b/p0sx/pos/views/littleadmin.py index 09af6d1..a8246ee 100644 --- a/p0sx/pos/views/littleadmin.py +++ b/p0sx/pos/views/littleadmin.py @@ -417,7 +417,7 @@ def verify_add_credit(request, tid=''): sumup_transaction = SumupTransaction.objects.get(pk=tid) if sumup_transaction.payment_state == PaymentState.Paid: messages.success(request, str(sumup_transaction.amount) + ' added to ' + str(sumup_transaction.user)) - return HttpResponseRedirect(reverse_lazy('littleadmin:check')) + return HttpResponseRedirect(reverse_lazy('littleadmin:scan_user_card')) if sumup_transaction.payment_state == PaymentState.Failed or sumup_transaction.payment_state == PaymentState.Cancelled: messages.error(request, 'Transaction failed') diff --git a/p0sx/templates/base.djhtml b/p0sx/templates/base.djhtml index fd12a1c..bdf78c1 100644 --- a/p0sx/templates/base.djhtml +++ b/p0sx/templates/base.djhtml @@ -68,11 +68,11 @@ {% if messages %}
-
    {% for message in messages %} -
  • {{message}}
  • +
    +
    {{message}}
    +
    {% endfor %} -
{% endif %} diff --git a/p0sx/templates/pos/add_credit.djhtml b/p0sx/templates/pos/add_credit.djhtml index fd8673d..aac4a0d 100644 --- a/p0sx/templates/pos/add_credit.djhtml +++ b/p0sx/templates/pos/add_credit.djhtml @@ -26,8 +26,8 @@ label[for=id_cash]
{% csrf_token %} {{ form }} - - + +
{% endblock content %} \ No newline at end of file From 9d60e75b30970fc6e5b498936e31f0eb95b3a2ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lian?= Date: Mon, 6 Oct 2025 21:23:32 +0200 Subject: [PATCH 08/13] Oooops --- p0sx/pos/models/user.py | 4 ++-- p0sx/pos/serializers/stock.py | 2 +- p0sx/pos/views/stock.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/p0sx/pos/models/user.py b/p0sx/pos/models/user.py index aced714..3656701 100644 --- a/p0sx/pos/models/user.py +++ b/p0sx/pos/models/user.py @@ -1,7 +1,7 @@ from django.contrib.auth.models import User as DjangoUser from django.db import models -from .stock import Order +from .stock import Order, PaymentMethod class CreditUpdate(models.Model): @@ -42,7 +42,7 @@ class User(models.Model): @property def used(self): - orders = Order.objects.filter(user_id=self.id) + orders = Order.objects.filter(user_id=self.id).exclude(payment_method=PaymentMethod.Card) return sum([order.sum for order in orders]) @property diff --git a/p0sx/pos/serializers/stock.py b/p0sx/pos/serializers/stock.py index a766767..f135a8f 100644 --- a/p0sx/pos/serializers/stock.py +++ b/p0sx/pos/serializers/stock.py @@ -179,7 +179,7 @@ def create(self, validated_data, request): #if payment_method == 1 and not crew.is_crew: # raise ValidationError('Only users marked as crew can use payment method CREDIT') - user_orders = Order.objects.filter(user=user) + user_orders = Order.objects.filter(user=user).exclude(payment_method=PaymentMethod.Card) user_order_lines = OrderLine.objects.filter(order__in=user_orders) total = sum(ol.price for ol in user_order_lines) diff --git a/p0sx/pos/views/stock.py b/p0sx/pos/views/stock.py index d677dbb..52cd5bc 100644 --- a/p0sx/pos/views/stock.py +++ b/p0sx/pos/views/stock.py @@ -80,7 +80,7 @@ class CreditCheckViewSet(viewsets.ViewSet): def retrieve(request, pk=None): users = User.objects.all() user = get_object_or_404(users, card__iexact=pk) - orders = Order.objects.filter(user=user) + orders = Order.objects.filter(user=user).exclude(payment_method=PaymentMethod.Card) orderlines = OrderLine.objects.filter(order__in=orders) total = sum(ol.price for ol in orderlines) From 8e5d873afc0ffd85c8e62ed7c794ba5cb41b920f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lian?= Date: Tue, 7 Oct 2025 00:13:01 +0200 Subject: [PATCH 09/13] Remove Card purchases from credit overview --- p0sx/pos/views/littleadmin.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/p0sx/pos/views/littleadmin.py b/p0sx/pos/views/littleadmin.py index a8246ee..292ec2a 100644 --- a/p0sx/pos/views/littleadmin.py +++ b/p0sx/pos/views/littleadmin.py @@ -12,7 +12,7 @@ from ..forms import AddCreditForm, AddUserForm, ChangeCreditForm, CheckCreditForm, TimeFilterForm from ..ge_importer import GeekEventsImporter from ..models.shift import Shift -from ..models.stock import Item, Order, OrderLine, PaymentState +from ..models.stock import Item, Order, OrderLine, PaymentState, PaymentMethod from ..models.user import CreditUpdate, GeekeventsToken, User from ..models.sumup_cloud import SumupTransaction, SumupReader from ..serializers.shift import ShiftSerializer @@ -65,7 +65,7 @@ def get_order_details(orders): @login_required def credit_overview(request): - bought = OrderLine.objects.all().exclude(order__user__isnull=True).values('order__user').annotate(used=Sum('price')) + bought = OrderLine.objects.all().exclude(order__user__isnull=True, order__payment_method=PaymentMethod.Card).values('order__user').annotate(used=Sum('price')) users = User.objects.all().values() for user in users: for b in bought: @@ -369,9 +369,14 @@ def add_user_credit(request, card=None): if cash: return redirect('littleadmin:verify_add_credit_cash', user.pk, amount) - sumup_transaction = SumupTransaction.objects.create(user=user, amount=amount, authenticated_user=request.user) - tid = sumup_transaction.pk - init_credit_fill_payment(sumup_transaction, sumup_reader.reader_id) + try: + with transaction.atomic(): + sumup_transaction = SumupTransaction.objects.create(user=user, amount=amount, authenticated_user=request.user) + tid = sumup_transaction.pk + init_credit_fill_payment(sumup_transaction, sumup_reader.reader_id) + except: + messages.error(request, "Failed to add credit transaction") + return redirect('littleadmin:add_user_credit', card) return redirect('littleadmin:verify_add_credit', tid) else: From b860ac3ab23e501a68e6c7df19f0a7efb9209c29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lian?= Date: Tue, 7 Oct 2025 00:14:59 +0200 Subject: [PATCH 10/13] Wrap SumUp operations on database transactions --- p0sx/pos/serializers/stock.py | 81 +++++++++++++++++++---------------- p0sx/pos/service/sumup.py | 22 ++++++---- 2 files changed, 56 insertions(+), 47 deletions(-) diff --git a/p0sx/pos/serializers/stock.py b/p0sx/pos/serializers/stock.py index f135a8f..f5e7e9b 100644 --- a/p0sx/pos/serializers/stock.py +++ b/p0sx/pos/serializers/stock.py @@ -1,4 +1,5 @@ from django.core.exceptions import ValidationError, ObjectDoesNotExist +from django.db import transaction from pos.models.stock import Category, Discount, Ingredient, Item, ItemIngredient, Order, OrderLine, Purchase, OrderState, PaymentMethod, PaymentState from pos.models.user import User @@ -197,44 +198,48 @@ def create(self, validated_data, request): except SumupReader.DoesNotExist: raise APIException() - order = Order.create(user, cashier, authenticated_user, payment_method, message) - order.payment_state = PaymentState.Pending if payment_method == PaymentMethod.Card else PaymentState.Paid - order.save() - - prepared_order = False - - for line_dict in order_lines: - ingredients = line_dict.get('ingredients') - item = line_dict.get('item') - message = line_dict.get('message') - - price = item.price + sum(i.price for i in ingredients) - price = price * -1 if undo else price - - line = OrderLine.create(item, order, price, message) - line.save() - if len(ingredients): - line.ingredients.set((i.pk for i in ingredients)) - line.save() - - if line.item.created_in_the_kitchen and not undo: - prepared_order = True - else: - line.state = OrderState.Archived - line.save() - - # Set the order to ARCHIVED if its not going to the kitchen - if not prepared_order: - order.state = OrderState.Archived - order.save() - - if order.payment_method == PaymentMethod.Card: - sumup.init_order_card_payment(order, reader_id=sumup_reader.reader_id) - elif prepared_order: - async_task("pos.services.print_pickup_receipts", order.id, - task_name='Pickup receipts for order {id}'.format(id=order.id)) - - return Purchase(order) + try: + with transaction.atomic(): + order = Order.create(user, cashier, authenticated_user, payment_method, message) + order.payment_state = PaymentState.Pending if payment_method == PaymentMethod.Card else PaymentState.Paid + order.save() + + prepared_order = False + + for line_dict in order_lines: + ingredients = line_dict.get('ingredients') + item = line_dict.get('item') + message = line_dict.get('message') + + price = item.price + sum(i.price for i in ingredients) + price = price * -1 if undo else price + + line = OrderLine.create(item, order, price, message) + line.save() + if len(ingredients): + line.ingredients.set((i.pk for i in ingredients)) + line.save() + + if line.item.created_in_the_kitchen and not undo: + prepared_order = True + else: + line.state = OrderState.Archived + line.save() + + # Set the order to ARCHIVED if its not going to the kitchen + if not prepared_order: + order.state = OrderState.Archived + order.save() + + if order.payment_method == PaymentMethod.Card: + sumup.init_order_card_payment(order, reader_id=sumup_reader.reader_id) + elif prepared_order: + async_task("pos.services.print_pickup_receipts", order.id, + task_name='Pickup receipts for order {id}'.format(id=order.id)) + + return Purchase(order) + except: + raise ValidationError("Purchase failed") def update(self, instance, validated_data): pass diff --git a/p0sx/pos/service/sumup.py b/p0sx/pos/service/sumup.py index 125a490..7c94c15 100644 --- a/p0sx/pos/service/sumup.py +++ b/p0sx/pos/service/sumup.py @@ -1,5 +1,5 @@ import requests - +from django.core.exceptions import ValidationError from django.conf import settings @@ -17,10 +17,12 @@ def init_order_card_payment(order, reader_id): url = f"https://api.sumup.com/v0.1/merchants/{settings.SUMUP_MERCHANT_CODE}/readers/{reader_id}/checkout" headers = {"Authorization": f"Bearer {settings.SUMUP_BEARER_TOKEN}"} response = requests.post(url, headers=headers, json=payload) - if response.status_code == 201: - order.payment_reference = response.json()["data"]["client_transaction_id"] - order.save() - return response.json() + + if response.status_code != 201: + raise ValidationError(f"Unexpected status code from SumUp {response.status_code}") + order.payment_reference = response.json()["data"]["client_transaction_id"] + order.save() + def init_credit_fill_payment(transaction, reader_id): """ @@ -34,10 +36,12 @@ def init_credit_fill_payment(transaction, reader_id): url = f"https://api.sumup.com/v0.1/merchants/{settings.SUMUP_MERCHANT_CODE}/readers/{reader_id}/checkout" headers = {"Authorization": f"Bearer {settings.SUMUP_BEARER_TOKEN}"} response = requests.post(url, headers=headers, json=payload) - if response.status_code == 201: - transaction.payment_reference = response.json()["data"]["client_transaction_id"] - transaction.save() - return response.json() + + if response.status_code != 201: + raise ValidationError(f"Unexpected status code from SumUp {response.status_code}") + transaction.payment_reference = response.json()["data"]["client_transaction_id"] + transaction.save() + def get_sumup_transaction(transaction_id): headers = {"Authorization": f"Bearer {settings.SUMUP_BEARER_TOKEN}"} From 5ce7fdf24b493d674892ee8cff399a4d7ae2b858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lian?= Date: Tue, 7 Oct 2025 10:34:37 +0200 Subject: [PATCH 11/13] Use card payments for crew as well if there is not enough credit --- p0sx/pos/models/stock.py | 1 - p0sx/pos/serializers/stock.py | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/p0sx/pos/models/stock.py b/p0sx/pos/models/stock.py index b3d580b..5bd594a 100644 --- a/p0sx/pos/models/stock.py +++ b/p0sx/pos/models/stock.py @@ -144,7 +144,6 @@ def __str__(self): s = self.item.name actual_ingredients = self.ingredients.all() - print(actual_ingredients) if len(actual_ingredients) > 0: s += ' med ' s += ', '.join([str(item).lower() for item in actual_ingredients]) diff --git a/p0sx/pos/serializers/stock.py b/p0sx/pos/serializers/stock.py index f5e7e9b..9382e2d 100644 --- a/p0sx/pos/serializers/stock.py +++ b/p0sx/pos/serializers/stock.py @@ -186,10 +186,7 @@ def create(self, validated_data, request): total = sum(ol.price for ol in user_order_lines) credit_left = user.credit - total if credit_left < order_total: - if user.is_crew: - raise ValidationError('Not enough credit left on card') - else: - payment_method = PaymentMethod.Card + payment_method = PaymentMethod.Card sumup_reader = None if payment_method == PaymentMethod.Card: From ed2ae9b57b370534965cad2fdf0aef3a3b2a227e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lian?= Date: Tue, 7 Oct 2025 17:05:06 +0200 Subject: [PATCH 12/13] Small bugfixes and ui additions to the admin pages --- p0sx/pos/admin.py | 56 ++++++++++++++++++++++------------- p0sx/pos/forms.py | 7 ++++- p0sx/pos/views/littleadmin.py | 52 +++++++++++++++++++++----------- 3 files changed, 76 insertions(+), 39 deletions(-) diff --git a/p0sx/pos/admin.py b/p0sx/pos/admin.py index 3fc7a01..cf35b9c 100644 --- a/p0sx/pos/admin.py +++ b/p0sx/pos/admin.py @@ -1,5 +1,7 @@ from django.contrib import admin from django import forms +from django.urls import reverse +from django.utils.safestring import mark_safe from pos.models.shift import Shift from pos.models.stock import Category, Discount, FoodLog, Ingredient, Item, ItemIngredient, Order, OrderLine @@ -59,12 +61,22 @@ class IngredientAdmin(admin.ModelAdmin): class ItemAdmin(admin.ModelAdmin): - pass + list_display = ('name', 'price', 'active') class OrderLineAdmin(admin.ModelAdmin): readonly_fields = ('ingredients', 'item', 'price') - list_display = ('item', 'order', 'state') + list_display = ('id', 'item', 'price', 'order_link', 'state') + + def order_link(self, obj): + if obj.order is None: + return '-' + return mark_safe('{}'.format( + reverse("admin:pos_order_change", args=(obj.order.pk,)), + obj.order + )) + + order_link.short_description = 'order' class OrderLineInline(admin.TabularInline): @@ -90,23 +102,6 @@ class ShiftAdmin(admin.ModelAdmin): pass -class SumUpAPIKeyAdmin(admin.ModelAdmin): - pass - -class SumUpOnlineAdmin(admin.ModelAdmin): - readonly_fields = ('id', 'created', 'timestamp', 'transaction_id', 'transaction_comment') - ordering = ('-created',) - list_display = ('user', 'amount', 'status', 'created', 'transaction_id', 'transaction_comment',) - pass - -class SumUpTerminalAdmin(admin.ModelAdmin): - pass - - -class SumUpTransactionAdmin(admin.ModelAdmin): - pass - - class SumUpCardAdmin(admin.ModelAdmin): readonly_fields = ('id', 'created', 'timestamp') ordering = ('-created',) @@ -136,10 +131,21 @@ def has_add_permission(self, request, obj=None): class OrderAdmin(admin.ModelAdmin): + search_fields = ('id', 'user__card') readonly_fields = ('user', 'payment_method', 'cashier', 'authenticated_user', 'payment_state', 'payment_reference') - list_display = ('id', 'user', 'date', 'sum', 'state', 'payment_state', 'payment_method') + list_display = ('id', 'user_link', 'date', 'sum', 'state', 'payment_state', 'payment_method') inlines = [OrderLineInline] + def user_link(self, obj): + if obj.user is None: + return '-' + return mark_safe('{}'.format( + reverse("admin:pos_user_change", args=(obj.user.pk,)), + obj.user + )) + + user_link.short_description = 'user' + class PrinterAdmin(admin.ModelAdmin): pass @@ -172,7 +178,17 @@ def save_model(self, request, obj, form, change): class SumupTransactionAdmin(admin.ModelAdmin): readonly_fields = ('user', 'authenticated_user', 'payment_state', 'payment_reference', 'amount', 'used') + list_display = ('id', 'user_link', 'amount', 'payment_state', 'used') + + def user_link(self, obj): + if obj.user is None: + return '-' + return mark_safe('{}'.format( + reverse("admin:pos_user_change", args=(obj.user.pk,)), + obj.user + )) + user_link.short_description = 'user' admin.site.register(User, UserAdmin) admin.site.register(Ingredient, IngredientAdmin) diff --git a/p0sx/pos/forms.py b/p0sx/pos/forms.py index 6bc3b0e..53244ec 100644 --- a/p0sx/pos/forms.py +++ b/p0sx/pos/forms.py @@ -4,7 +4,7 @@ class CheckCreditForm(forms.Form): - card = forms.CharField(max_length=100, widget=forms.PasswordInput(attrs={'autofocus': 'autofocus'})) + card = forms.CharField(max_length=100, widget=forms.PasswordInput(attrs={'autofocus': 'autofocus', 'autocomplete': 'off'})) class AddCreditForm(forms.Form): @@ -38,6 +38,11 @@ class TimeFilterForm(forms.Form): to_time = forms.DateTimeField(input_formats=['%Y-%m-%dT%H:%M'], widget=forms.DateTimeInput(attrs={'type': 'datetime-local'}), required=False) +class CsvTimeFilterForm(forms.Form): + from_time = forms.DateTimeField(input_formats=['%Y-%m-%dT%H:%M'], widget=forms.DateTimeInput(attrs={'type': 'datetime-local'}), required=True) + to_time = forms.DateTimeField(input_formats=['%Y-%m-%dT%H:%M'], widget=forms.DateTimeInput(attrs={'type': 'datetime-local'}), required=True) + + class RemotePayForm(forms.Form): phone = forms.CharField(widget=forms.TextInput(attrs={'pattern': '[49]\d{7}', 'minlength': '8', 'maxlength': '8', 'oninvalid': 'this.setCustomValidity(\'Skriv inn et gyldig mobilnummer\')', 'oninput': 'this.setCustomValidity(\'\')'})) amount = forms.CharField(widget=forms.TextInput(attrs={'pattern': '(5[0-9]|[6-9][0-9]|[1-9][0-9]{2}|1000)', 'oninvalid': 'this.setCustomValidity(\'Skriv inn et beløp mellom kr. 50,- og kr. 1000,-\')', 'oninput': 'this.setCustomValidity(\'\')'})) diff --git a/p0sx/pos/views/littleadmin.py b/p0sx/pos/views/littleadmin.py index 292ec2a..0443369 100644 --- a/p0sx/pos/views/littleadmin.py +++ b/p0sx/pos/views/littleadmin.py @@ -31,7 +31,7 @@ def check_credit(request): if not user: return HttpResponseRedirect(reverse_lazy('littleadmin:check')) - orders = Order.objects.filter(user_id=user[0].pk).order_by('-date')[0:3] + orders = Order.objects.filter(user_id=user[0].pk).exclude(payment_method=PaymentMethod.Card).order_by('-date')[0:3] return render(request, 'pos/credit_check.djhtml', { 'form': CheckCreditForm(), @@ -264,7 +264,8 @@ def scan_user_card(request): user = User.objects.filter(card__iexact=card) if not user: - return redirect('littleadmin:add_user', card=card) + messages.error(request, "User not found") + return redirect('littleadmin:scan_user_card') return redirect('littleadmin:add_user_credit', card=card) else: @@ -294,7 +295,7 @@ def fetch_credit_from_ge(request): return render(request, 'pos/import_geekevents.djhtml', {'items': [], 'form': CheckCreditForm()}) importer = GeekEventsImporter(settings.GEEKEVENTS_TOKEN, settings.GEEKEVENTS_ITEM_ID) - items = [] + try: items = importer.get_unfetched_items() except: @@ -347,12 +348,6 @@ def add_user_credit(request, card=None): cash = form.cleaned_data['cash'] user = get_object_or_404(User, card__iexact=card) - try: - sumup_reader = SumupReader.objects.get(user=request.user) - except SumupReader.DoesNotExist: - messages.error(request,"The logged in user has no payment terminal associated with it.") - return redirect('littleadmin:add_user_credit', card) - if user.is_crew: messages.error(request, "You cannot change the credit of Crew") return redirect('littleadmin:scan_user_card') @@ -365,10 +360,19 @@ def add_user_credit(request, card=None): if amount > 1000: messages.error(request, "The maximum credit that can be added at once is 1000. Add multiple times if more is needed") return redirect('littleadmin:add_user_credit', card) + if amount <= 0: + messages.error(request,"The minimum credit that can be added at is 1") + return redirect('littleadmin:add_user_credit', card) if cash: return redirect('littleadmin:verify_add_credit_cash', user.pk, amount) + try: + sumup_reader = SumupReader.objects.get(user=request.user) + except SumupReader.DoesNotExist: + messages.error(request, "The logged in user has no payment terminal associated with it.") + return redirect('littleadmin:add_user_credit', card) + try: with transaction.atomic(): sumup_transaction = SumupTransaction.objects.create(user=user, amount=amount, authenticated_user=request.user) @@ -487,21 +491,33 @@ def group_credit_updates(date, group_by): @login_required() def add_credit_stats(request): if request.POST: - form = TimeFilterForm(request.POST) - if not form.is_valid(): - return redirect('littleadmin:add_credit_stats') - - from_time = form.cleaned_data['from_time'] - to_time = form.cleaned_data['to_time'] + from_time = None + to_time = None + + if request.POST: + form = TimeFilterForm(request.POST) + if form.is_valid(): + from_time = form.cleaned_data['from_time'] + to_time = form.cleaned_data['to_time'] + + updates = CreditUpdate.objects.all() + orders = Order.objects.all() + initial_value = {} + if from_time is not None: + updates = updates.filter(timestamp__gte=from_time) + orders = orders.filter(date__gte=from_time) + initial_value['from_time'] = f"{from_time:%Y-%m-%dT%H:%M}" + if to_time is not None: + updates = updates.filter(timestamp__lte=to_time) + orders = orders.filter(date__lte=to_time) + initial_value['to_time'] = f"{to_time:%Y-%m-%dT%H:%M}" - updates = CreditUpdate.objects.filter(timestamp__lte=to_time, timestamp__gte=from_time) - orders = Order.objects.filter(date__lte=to_time, date__gte=from_time) total_out = sum([x.sum for x in orders]) total = sum([x.amount for x in updates]) - form = TimeFilterForm(initial={'from_time': f"{from_time:%Y-%m-%dT%H:%M}", 'to_time': f"{to_time:%Y-%m-%dT%H:%M}"}) + form = TimeFilterForm(initial=initial_value) else: total = 0 total_out = 0 From 48d42a2e50f38830ba6021988e2e9f9b64760281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lian?= Date: Tue, 7 Oct 2025 18:52:28 +0200 Subject: [PATCH 13/13] More ignoring of Card payments... --- p0sx/pos/models/sumup_cloud.py | 3 +++ p0sx/pos/views/littleadmin.py | 35 ++++++++++++++++++++++------------ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/p0sx/pos/models/sumup_cloud.py b/p0sx/pos/models/sumup_cloud.py index a4914fa..1603bec 100644 --- a/p0sx/pos/models/sumup_cloud.py +++ b/p0sx/pos/models/sumup_cloud.py @@ -29,3 +29,6 @@ class SumupTransaction(models.Model): payment_state = models.SmallIntegerField(default=PaymentState.Pending, choices=PAYMENT_STATE) payment_reference = models.CharField(max_length=256, blank=True, null=True) used = models.BooleanField(default=False) + + def __str__(self): + return f"SumUp transaction to add {self.amount} to {self.user} by {self.authenticated_user}" diff --git a/p0sx/pos/views/littleadmin.py b/p0sx/pos/views/littleadmin.py index 0443369..192ce25 100644 --- a/p0sx/pos/views/littleadmin.py +++ b/p0sx/pos/views/littleadmin.py @@ -65,7 +65,7 @@ def get_order_details(orders): @login_required def credit_overview(request): - bought = OrderLine.objects.all().exclude(order__user__isnull=True, order__payment_method=PaymentMethod.Card).values('order__user').annotate(used=Sum('price')) + bought = OrderLine.objects.all().exclude(order__user__isnull=True).exclude(order__payment_method=PaymentMethod.Card).values('order__user').annotate(used=Sum('price')) users = User.objects.all().values() for user in users: for b in bought: @@ -130,7 +130,7 @@ def sale_overview_csv(request): else: form = TimeFilterForm() - order_lines = OrderLine.objects.all().values('item__id', 'order__payment_method') + order_lines = OrderLine.objects.filter(order__payment_state=PaymentState.Paid).values('item__id', 'order__payment_method') if from_time is not None and to_time is not None: order_lines = order_lines.filter(order__date__gte=from_time).filter(order__date__lt=to_time) order_lines = order_lines.annotate(total=Sum('price'), @@ -152,16 +152,16 @@ def sale_overview_csv(request): first_sold = per_payment_method.aggregate(Min('first_sold'))['first_sold__min'] last_sold = per_payment_method.aggregate(Max('last_sold'))['last_sold__max'] try: - credit = per_payment_method.filter(order__payment_method=1)[0] + credit = per_payment_method.filter(order__payment_method=PaymentMethod.Credit)[0] except IndexError: credit = {'sold': 0, 'total': 0, 'first_sold': None, 'last_sold': None} try: - prepaid = per_payment_method.filter(order__payment_method=4)[0] + prepaid = per_payment_method.filter(order__payment_method=PaymentMethod.Prepaid)[0] except IndexError: prepaid = {'sold': 0, 'total': 0, 'first_sold': None, 'last_sold': None} try: - card = per_payment_method.filter(order__payment_method=2)[0] + card = per_payment_method.filter(order__payment_method=PaymentMethod.Card)[0] except IndexError: card = {'sold': 0, 'total': 0, 'first_sold': None, 'last_sold': None} @@ -182,7 +182,7 @@ def sale_overview(request): else: form = TimeFilterForm() - order_lines = OrderLine.objects.all().values('item__id', 'order__payment_method') + order_lines = OrderLine.objects.filter(order__payment_state=PaymentState.Paid).values('item__id', 'order__payment_method') if from_time is not None and to_time is not None: order_lines = order_lines.filter(order__date__gte=from_time).filter(order__date__lt=to_time) order_lines = order_lines.annotate(total=Sum('price'), @@ -200,15 +200,15 @@ def sale_overview(request): first_sold = per_payment_method.aggregate(Min('first_sold'))['first_sold__min'] last_sold = per_payment_method.aggregate(Max('last_sold'))['last_sold__max'] try: - credit = per_payment_method.filter(order__payment_method=1)[0] + credit = per_payment_method.filter(order__payment_method=PaymentMethod.Credit)[0] except IndexError: credit = {'sold': 0, 'total': 0, 'first_sold': None, 'last_sold': None} try: - prepaid = per_payment_method.filter(order__payment_method=4)[0] + prepaid = per_payment_method.filter(order__payment_method=PaymentMethod.Prepaid)[0] except IndexError: prepaid = {'sold': 0, 'total': 0, 'first_sold': None, 'last_sold': None} try: - card = per_payment_method.filter(order__payment_method=2)[0] + card = per_payment_method.filter(order__payment_method=PaymentMethod.Card)[0] except IndexError: card = {'sold': 0, 'total': 0, 'first_sold': None, 'last_sold': None} @@ -501,7 +501,7 @@ def add_credit_stats(request): to_time = form.cleaned_data['to_time'] updates = CreditUpdate.objects.all() - orders = Order.objects.all() + orders = Order.objects.exclude(payment_method=PaymentMethod.Card) initial_value = {} if from_time is not None: updates = updates.filter(timestamp__gte=from_time) @@ -513,9 +513,20 @@ def add_credit_stats(request): initial_value['to_time'] = f"{to_time:%Y-%m-%dT%H:%M}" - total_out = sum([x.sum for x in orders]) - + updates = list(updates) total = sum([x.amount for x in updates]) + if from_time is None and to_time is None: + sumup_transactions = SumupTransaction.objects.filter(payment_state=PaymentState.Paid, used=True) + for t in sumup_transactions: + updates.append({ + 'amount': t.amount, + 'timestamp': None, + 'updated_by_user': t.authenticated_user, + 'user': t.user + }) + total += sum([x.amount for x in sumup_transactions]) + + total_out = sum([x.sum for x in orders]) form = TimeFilterForm(initial=initial_value) else: