From 5f08a0acbab641716241ae206a168ac97c971333 Mon Sep 17 00:00:00 2001 From: Daniel Holbach Date: Wed, 1 Jul 2026 14:09:07 +0200 Subject: [PATCH] fix: suppress already-billed pending calendar events from billing and queue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pending calendar events for sessions that are already billed were surfacing as false alarms in /billing/open/ and cluttering the calendar approval queue. - _gather_billing_data: exclude pending events where a matching InvoiceItem already exists (same client, same date, duration ±5 min) - calendar_approval_queue: filter out already-billed events from the listing; pass stale_count separately so the banner still offers a one-click cleanup (mark as skipped) - Remove per-row duplicate badge (events no longer appear in list) Co-Authored-By: Claude Sonnet 4.6 --- app/my_practice/views/calendar_views.py | 17 +++++++++--- app/my_practice/views/invoice_views.py | 27 +++++++++++++------ .../my_practice/calendar_approval_queue.html | 15 +++++------ 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/app/my_practice/views/calendar_views.py b/app/my_practice/views/calendar_views.py index 3c59366..61fc995 100644 --- a/app/my_practice/views/calendar_views.py +++ b/app/my_practice/views/calendar_views.py @@ -228,13 +228,24 @@ def calendar_approval_queue(request: HttpRequest) -> HttpResponse: session__duration__lte=OuterRef("duration_minutes") + 5, ) + # Stale count: already-billed events still sitting as pending (shown separately, not in list) + stale_count = ( + PendingCalendarEvent.objects.filter( + practice=practice, + status=PendingCalendarEvent.Status.PENDING, + ) + .annotate(is_duplicate=Exists(duplicate_subquery)) + .filter(is_duplicate=True) + .count() + ) + pending_events = ( PendingCalendarEvent.objects.filter( practice=practice, status=PendingCalendarEvent.Status.PENDING, ) .select_related("matched_client", "suggested_service_type") - .annotate(is_duplicate=Exists(duplicate_subquery)) + .exclude(Exists(duplicate_subquery)) .order_by("matched_client__client_code", "event_date") ) @@ -251,7 +262,6 @@ def calendar_approval_queue(request: HttpRequest) -> HttpResponse: "month": month, "events": events, "count": len(events), - "duplicate_count": sum(1 for e in events if e.is_duplicate), } ) @@ -271,7 +281,6 @@ def calendar_approval_queue(request: HttpRequest) -> HttpResponse: group["invoices"] = draft_invoice_map.get(group["client"].pk, []) if group["client"] else [] total_pending = pending_events.count() - total_duplicates = sum(1 for e in pending_events if e.is_duplicate) return render( request, @@ -279,7 +288,7 @@ def calendar_approval_queue(request: HttpRequest) -> HttpResponse: { "grouped": grouped, "total_pending": total_pending, - "total_duplicates": total_duplicates, + "stale_count": stale_count, }, ) diff --git a/app/my_practice/views/invoice_views.py b/app/my_practice/views/invoice_views.py index 2a11b6f..926596c 100644 --- a/app/my_practice/views/invoice_views.py +++ b/app/my_practice/views/invoice_views.py @@ -11,7 +11,7 @@ from django.contrib import messages from django.contrib.auth.decorators import login_required from django.db import transaction -from django.db.models import Case, Count, DecimalField, F, Q, QuerySet, Sum, When +from django.db.models import Case, Count, DecimalField, Exists, F, OuterRef, Q, QuerySet, Sum, When from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse, reverse_lazy from django.utils.html import format_html @@ -720,14 +720,25 @@ def _gather_billing_data(practice, year, month_num): ): unbilled_sessions_by_client.setdefault(session.client_id, []).append(session) + already_billed_subquery = InvoiceItem.objects.filter( + invoice__client=OuterRef("matched_client"), + session__session_date=OuterRef("event_date"), + session__duration__gte=OuterRef("duration_minutes") - 5, + session__duration__lte=OuterRef("duration_minutes") + 5, + ).exclude(invoice__status=Invoice.Status.CANCELLED) + pending_by_client: dict[int, int] = {} - for row in PendingCalendarEvent.objects.filter( - practice=practice, - event_date__year=year, - event_date__month=month_num, - status=PendingCalendarEvent.Status.PENDING, - matched_client__isnull=False, - ).values("matched_client_id"): + for row in ( + PendingCalendarEvent.objects.filter( + practice=practice, + event_date__year=year, + event_date__month=month_num, + status=PendingCalendarEvent.Status.PENDING, + matched_client__isnull=False, + ) + .exclude(Exists(already_billed_subquery)) + .values("matched_client_id") + ): cid = row["matched_client_id"] pending_by_client[cid] = pending_by_client.get(cid, 0) + 1 diff --git a/app/templates/my_practice/calendar_approval_queue.html b/app/templates/my_practice/calendar_approval_queue.html index db104ad..ca43b75 100644 --- a/app/templates/my_practice/calendar_approval_queue.html +++ b/app/templates/my_practice/calendar_approval_queue.html @@ -27,16 +27,16 @@ -{% if total_duplicates > 0 %} -
+{% if stale_count > 0 %} +
- ⚠️ {{ total_duplicates }} bereits importiert
+ ℹ️ {{ stale_count }} Termin{{ stale_count|pluralize:"e" }} ausgeblendet
- Diese Termine haben bereits passende Rechnungspositionen und können übersprungen werden. + Diese Termine haben bereits passende Rechnungspositionen und werden nicht angezeigt.
-
{% endif %} @@ -117,9 +117,6 @@

{% endif %} - {% if event.is_duplicate %} - ✓ vorhanden - {% endif %}