Repository: svthalia/concrexit

Issue:
Fix moneybird sync skipping many invoices
### Describe the bug
For some reason some queryset.exists() is False incorrectly. We need to avoid that, but also find out why and get it fixed in django or django-queryable-properties.

See also https://github.com/W1ldPo1nter/django-queryable-properties/issues/13.

In the specific case that's a problem right now, this queryset does not `.exists()` despite having many results:
https://github.com/svthalia/concrexit/blob/9d4d5cdb72bdc1a57679e33c7f6071a9bdfb4b4f/website/moneybirdsynchronization/services.py#L397-L415

That in turn causes the invoice creation to be skipped here:
https://github.com/svthalia/concrexit/blob/9d4d5cdb72bdc1a57679e33c7f6071a9bdfb4b4f/website/moneybirdsynchronization/services.py#L304-L306

### How to reproduce
Yeah I'm not quite sure about this.

### Expected behaviour
`qs.exists() == (qs.count() > 0)` for all `qs`. But for now, let's just not use `.exists()`.


Discussion:

Resolving patch:
diff --git a/website/events/models/event_registration.py b/website/events/models/event_registration.py
--- a/website/events/models/event_registration.py
+++ b/website/events/models/event_registration.py
@@ -2,7 +2,7 @@
 from django.core.exceptions import ValidationError
 from django.db import models
 from django.db.models import Case, Count, F, Q, When
-from django.db.models.functions import Greatest, NullIf
+from django.db.models.functions import Coalesce, Greatest, NullIf
 from django.utils import timezone
 from django.utils.translation import gettext_lazy as _
 
@@ -183,10 +183,7 @@ def payment_amount(self):
     @payment_amount.annotater
     @classmethod
     def payment_amount(cls):
-        return Case(
-            When(Q(special_price__isnull=False), then=F("special_price")),
-            default=F("event__price"),
-        )
+        return Coalesce("special_price", "event__price")
 
     def would_cancel_after_deadline(self):
         now = timezone.now()
diff --git a/website/moneybirdsynchronization/services.py b/website/moneybirdsynchronization/services.py
--- a/website/moneybirdsynchronization/services.py
+++ b/website/moneybirdsynchronization/services.py
@@ -199,13 +199,9 @@ def synchronize_moneybird():
 def _delete_invoices():
     """Delete the invoices that have been marked for deletion from moneybird."""
     invoices = MoneybirdExternalInvoice.objects.filter(needs_deletion=True)
-
-    if not invoices.exists():
-        return
-
     logger.info("Deleting %d invoices.", invoices.count())
-    moneybird = get_moneybird_api_service()
 
+    moneybird = get_moneybird_api_service()
     for invoice in invoices:
         try:
             if invoice.moneybird_invoice_id is not None:
@@ -222,20 +218,20 @@ def _sync_outdated_invoices():
         needs_synchronization=True, needs_deletion=False
     ).order_by("payable_model", "object_id")
 
-    if invoices.exists():
-        logger.info("Resynchronizing %d invoices.", invoices.count())
-        for invoice in invoices:
-            try:
-                instance = invoice.payable_object
-                create_or_update_external_invoice(instance)
-            except Administration.Error as e:
-                logger.exception("Moneybird synchronization error: %s", e)
-                send_sync_error(e, instance)
-            except ObjectDoesNotExist:
-                logger.exception("Payable object for outdated invoice does not exist.")
+    logger.info("Resynchronizing %d invoices.", invoices.count())
+    for invoice in invoices:
+        try:
+            instance = invoice.payable_object
+            create_or_update_external_invoice(instance)
+        except Administration.Error as e:
+            logger.exception("Moneybird synchronization error: %s", e)
+            send_sync_error(e, instance)
+        except ObjectDoesNotExist:
+            logger.exception("Payable object for outdated invoice does not exist.")
 
 
 def _sync_contacts():
+    logger.info("Synchronizing contacts...")
     # make moneybird contacts for people that dont have it
     for member in Member.objects.filter(
         moneybird_contact__isnull=True, profile__is_minimized=False
@@ -287,11 +283,9 @@ def _sync_contacts_with_outdated_mandates():
         .exclude(sepa_mandate_id=None, moneybird_sepa_mandate_id=None)
     )
 
-    if contacts.exists():
-        logger.info(
-            "Pushing %d contacts with outdated mandates to Moneybird.",
-            contacts.count(),
-        )
+    logger.info(
+        "Pushing %d contacts with outdated mandates to Moneybird.", contacts.count()
+    )
 
     for contact in contacts:
         try:
@@ -302,13 +296,8 @@ def _sync_contacts_with_outdated_mandates():
 
 
 def _try_create_or_update_external_invoices(queryset):
-    if not queryset.exists():
-        return
-
     logger.info(
-        "Pushing %d %s to Moneybird.",
-        queryset.count(),
-        model_ngettext(queryset),
+        "Pushing %d %s to Moneybird.", queryset.count(), model_ngettext(queryset)
     )
 
     for instance in queryset:
@@ -321,6 +310,7 @@ def _try_create_or_update_external_invoices(queryset):
 
 def _sync_food_orders():
     """Create invoices for new food orders."""
+    logger.info("Synchronizing food orders...")
     food_orders = FoodOrder.objects.filter(
         food_event__event__start__date__gte=settings.MONEYBIRD_START_DATE,
     ).exclude(
@@ -337,6 +327,7 @@ def _sync_food_orders():
 
 def _sync_sales_orders():
     """Create invoices for new sales orders."""
+    logger.info("Synchronizing sales orders...")
     sales_orders = Order.objects.filter(
         shift__start__date__gte=settings.MONEYBIRD_START_DATE,
         payment__isnull=False,
@@ -354,6 +345,7 @@ def _sync_sales_orders():
 
 def _sync_registrations():
     """Create invoices for new, paid registrations."""
+    logger.info("Synchronizing registrations...")
     registrations = Registration.objects.filter(
         created_at__date__gte=settings.MONEYBIRD_START_DATE,
         payment__isnull=False,
@@ -371,6 +363,7 @@ def _sync_registrations():
 
 def _sync_renewals():
     """Create invoices for new, paid renewals."""
+    logger.info("Synchronizing renewals...")
     renewals = Renewal.objects.filter(
         created_at__date__gte=settings.MONEYBIRD_START_DATE,
         payment__isnull=False,
@@ -394,6 +387,7 @@ def _sync_event_registrations():
     However, some changes to the event or registrations for  the same event might not trigger saving
     the event registration, but still change its queue position or payment amount.
     """
+    logger.info("Synchronizing event registrations...")
     event_registrations = (
         EventRegistration.objects.select_properties("queue_position", "payment_amount")
         .filter(
@@ -432,18 +426,17 @@ def _sync_event_registrations():
         )
     )
 
-    if to_remove.exists():
-        logger.info(
-            "Removing invoices for %d event registrations from Moneybird.",
-            to_remove.count(),
-        )
+    logger.info(
+        "Removing invoices for %d event registrations from Moneybird.",
+        to_remove.count(),
+    )
 
-        for instance in to_remove:
-            try:
-                delete_external_invoice(instance)
-            except Administration.Error as e:
-                logger.exception("Moneybird synchronization error: %s", e)
-                send_sync_error(e, instance)
+    for instance in to_remove:
+        try:
+            delete_external_invoice(instance)
+        except Administration.Error as e:
+            logger.exception("Moneybird synchronization error: %s", e)
+            send_sync_error(e, instance)
 
 
 def _sync_moneybird_payments():
@@ -454,6 +447,8 @@ def _sync_moneybird_payments():
     if not settings.MONEYBIRD_SYNC_ENABLED:
         return
 
+    logger.info("Synchronizing payments...")
+
     for payment_type in [Payment.CASH, Payment.CARD, Payment.TPAY]:
         payments = Payment.objects.filter(
             type=payment_type,


Test patch: