79 lines
3.9 KiB
Python
79 lines
3.9 KiB
Python
"""Bounded-query projections use only the current evidence revision for financial totals."""
|
|
from django.urls import reverse
|
|
from django.db.models import BigIntegerField, F, OuterRef, Subquery, Sum, Value
|
|
from django.db.models.functions import Coalesce
|
|
from .models import PaymentReceiptRevision
|
|
|
|
|
|
def notices_with_totals(queryset):
|
|
approved = PaymentReceiptRevision.objects.filter(
|
|
receipt__notice_id=OuterRef('pk'), receipt__status='APPROVED',
|
|
version=F('receipt__current_version'), destination_mismatch=False
|
|
).values('receipt__notice_id').annotate(total=Sum('amount')).values('total')
|
|
return queryset.annotate(approved_total=Coalesce(Subquery(approved), Value(0), output_field=BigIntegerField()))
|
|
|
|
|
|
def receipts_with_current_values(queryset):
|
|
current = PaymentReceiptRevision.objects.filter(receipt_id=OuterRef('pk'), version=OuterRef('current_version'))
|
|
return queryset.annotate(current_amount=Subquery(current.values('amount')[:1]),
|
|
current_paid_date=Subquery(current.values('paid_date')[:1]))
|
|
|
|
|
|
def notice_form_data(notice, staff=False):
|
|
"""Customer-owned notice summary; account numbers stay on the notice page."""
|
|
return {'id': str(notice.pk), 'subject': notice.subject, 'amount': str(notice.amount),
|
|
'account': notice.account_id, 'deposit': notice.deposit_id,
|
|
'invoice': notice.invoice_number,
|
|
'url': reverse('staff_payments:notice_detail' if staff else 'payments:notice_detail', args=[notice.public_id])}
|
|
|
|
|
|
def receipt_review_summaries(receipts):
|
|
"""Attach current-version decisions in one query, preserving snapshot actor names."""
|
|
from .models import ReceiptDecision
|
|
receipts = list(receipts)
|
|
indexed = {receipt.pk: receipt for receipt in receipts}
|
|
for receipt in receipts:
|
|
receipt.review_decisions = []
|
|
if indexed:
|
|
decisions = ReceiptDecision.objects.filter(
|
|
cycle__revision__receipt_id__in=indexed,
|
|
cycle__revision__version=F('cycle__revision__receipt__current_version')
|
|
).select_related('cycle__revision').order_by('created_at', 'pk')
|
|
for decision in decisions:
|
|
indexed[decision.cycle.revision.receipt_id].review_decisions.append(decision)
|
|
return receipts
|
|
|
|
|
|
def attach_notice_receipts(notices, visible_receipts):
|
|
"""Only fetch linked receipts visible to the current viewer, for this page."""
|
|
notices = list(notices)
|
|
indexed = {notice.pk: notice for notice in notices}
|
|
for notice in notices:
|
|
notice.related_receipts = []
|
|
if indexed:
|
|
receipts = receipt_review_summaries(receipts_with_current_values(
|
|
visible_receipts.filter(notice_id__in=indexed)))
|
|
for receipt in receipts:
|
|
indexed[receipt.notice_id].related_receipts.append(receipt)
|
|
return notices
|
|
|
|
|
|
def recent_pending_notice_choices(form, retain_original=None):
|
|
"""Limit the visible menu, keeping server-side ownership validation unsliced.
|
|
|
|
An explicitly selected pending notice replaces the last option when needed.
|
|
A revision retains its original notice even if it has since been settled.
|
|
This does not change which notices the financial service may validate.
|
|
"""
|
|
field = form.fields['notice_id']
|
|
pending = notices_with_totals(field.queryset).filter(amount__gt=F('approved_total'))
|
|
available = list(pending.order_by('-created_at', '-pk')[:10])
|
|
selected = form['notice_id'].value()
|
|
if selected and not any(str(n.pk) == str(selected) for n in available):
|
|
source = field.queryset if str(selected) == str(retain_original) else pending
|
|
selected_notice = source.filter(pk=selected).first() if str(selected).isdecimal() and len(str(selected)) <= 19 and int(selected) <= 9223372036854775807 else None
|
|
if selected_notice:
|
|
available = [selected_notice] + available[:9]
|
|
field.widget.choices = [('', field.empty_label)] + [(n.pk, field.label_from_instance(n)) for n in available]
|
|
return available
|