from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.admin.views.decorators import staff_member_required from django.contrib import messages from django.views.decorators.http import require_POST from django.urls import reverse from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger import django_filters as filters from admin_frontend.templatetags.public_urls import public_route from admin_frontend.nav import _nav_items from cms.views import breadcrumbs from orgs.models import Organization from recycle_core.models import Customer from recycle_core.forms import PaymentForm from .models import Invoice, Payment, Payout from django.db.models import Sum, F, ExpressionWrapper, DecimalField, Value from django.db.models.functions import Coalesce from decimal import Decimal from django.http import HttpResponse import csv # Invoices UI --------------------------------------------------------------- @staff_member_required @public_route(label="Invoices", order=50, icon="receipt-percent") @breadcrumbs(label="Invoices", name="re_invoices") def invoices_list(request): class InvoiceFilter(filters.FilterSet): organization = filters.ModelChoiceFilter(queryset=Organization.objects.all()) customer = filters.ModelChoiceFilter(queryset=Customer.objects.all()) status = filters.ChoiceFilter(field_name="status", choices=Invoice.STATUS_CHOICES) issued_at = filters.DateFromToRangeFilter(field_name="issued_at", label="Issued between") class Meta: model = Invoice fields = ["organization", "customer", "status", "issued_at"] base_qs = ( Invoice.objects.select_related("organization", "customer") .annotate( payments_total=Coalesce( Sum("payments__amount"), Value(Decimal("0.00")), output_field=DecimalField(max_digits=14, decimal_places=2), ), ) .annotate( balance=ExpressionWrapper( F("total_amount") - F("payments_total"), output_field=DecimalField(max_digits=14, decimal_places=2), ) ) .order_by("-issued_at", "-id") ) i_filter = InvoiceFilter(request.GET, queryset=base_qs) qs = i_filter.qs org = getattr(request, "org", None) if org is not None: qs = qs.filter(organization=org) paginator = Paginator(qs, 10) page = request.GET.get("page") try: page_obj = paginator.page(page) except PageNotAnInteger: page_obj = paginator.page(1) except EmptyPage: page_obj = paginator.page(paginator.num_pages) pay_form = PaymentForm() context = { "nav": _nav_items(), "invoices": page_obj.object_list, "page_obj": page_obj, "filter": i_filter, "pay_form": pay_form, } return render(request, "recycle_core/invoices_list.html", context) @staff_member_required def invoice_add_payment(request, pk: int): invoice = get_object_or_404(Invoice, pk=pk) form = PaymentForm(request.POST) if form.is_valid(): Payment.objects.create( invoice=invoice, amount=form.cleaned_data["amount"], currency_code=invoice.currency_code, received_at=form.cleaned_data.get("received_at") or None, reference=form.cleaned_data.get("reference", ""), ) messages.success(request, f"Payment recorded for invoice #{invoice.id}.") else: messages.error(request, "Invalid payment data.") return redirect("billing:invoices_list") @staff_member_required @breadcrumbs(label="Invoice Detail", parent="re_invoices") def invoice_detail(request, pk: int): inv = get_object_or_404( Invoice.objects.select_related("organization", "customer").prefetch_related("lines", "payments"), pk=pk ) # Scope check to current org if applicable org = getattr(request, "org", None) if org is not None and inv.organization_id != org.id: messages.error(request, "Invoice not in this organization.") return redirect("billing:invoices_list") # Totals payments_total = inv.payments.aggregate(s=Coalesce(Sum("amount"), Value(Decimal("0.00"))))["s"] or Decimal("0.00") balance = (Decimal(inv.total_amount) - Decimal(payments_total)).quantize(Decimal("0.01")) pay_form = PaymentForm() context = { "nav": _nav_items(), "invoice": inv, "payments_total": payments_total, "balance": balance, "pay_form": pay_form, "status_choices": Invoice.STATUS_CHOICES, } return render(request, "recycle_core/invoice_detail.html", context) @staff_member_required @require_POST def invoice_set_status(request, pk: int): inv = get_object_or_404(Invoice, pk=pk) new_status = (request.POST.get("status") or "").strip() valid = {k for k, _ in Invoice.STATUS_CHOICES} if new_status not in valid: messages.error(request, "Invalid status selection.") return redirect("billing:invoice_detail", pk=inv.id) inv.status = new_status inv.save(update_fields=["status", "updated_at"]) messages.success(request, f"Invoice status updated to '{new_status}'.") return redirect("billing:invoice_detail", pk=inv.id) # Payouts UI + CSV ---------------------------------------------------------- @staff_member_required @public_route(label="Payouts", order=52, icon="banknotes") @breadcrumbs(label="Payouts", name="re_payouts") def payouts_list(request): class PayoutFilter(filters.FilterSet): organization = filters.ModelChoiceFilter(queryset=Organization.objects.all()) customer = filters.ModelChoiceFilter(queryset=Customer.objects.all()) paid_at = filters.DateFromToRangeFilter(field_name="paid_at", label="Paid between") class Meta: model = Payout fields = ["organization", "customer", "paid_at"] base_qs = Payout.objects.select_related("organization", "customer").order_by("-paid_at", "-id") p_filter = PayoutFilter(request.GET, queryset=base_qs) qs = p_filter.qs org = getattr(request, "org", None) if org is not None: qs = qs.filter(organization=org) paginator = Paginator(qs, 10) page = request.GET.get("page") try: page_obj = paginator.page(page) except PageNotAnInteger: page_obj = paginator.page(1) except EmptyPage: page_obj = paginator.page(paginator.num_pages) context = { "nav": _nav_items(), "payouts": page_obj.object_list, "page_obj": page_obj, "filter": p_filter, } return render(request, "recycle_core/payouts_list.html", context) @staff_member_required def payouts_export_csv(request): qs = Payout.objects.select_related("organization", "customer").all() org_obj = getattr(request, "org", None) if org_obj is not None: qs = qs.filter(organization=org_obj) org = request.GET.get("organization") cust = request.GET.get("customer") start = request.GET.get("paid_at_after") or request.GET.get("paid_at_min") end = request.GET.get("paid_at_before") or request.GET.get("paid_at_max") from django.utils.dateparse import parse_datetime if org: qs = qs.filter(organization_id=org) if cust: qs = qs.filter(customer_id=cust) if start: dt = parse_datetime(start) if dt: qs = qs.filter(paid_at__gte=dt) if end: dt = parse_datetime(end) if dt: qs = qs.filter(paid_at__lte=dt) response = HttpResponse(content_type="text/csv") response["Content-Disposition"] = "attachment; filename=payouts.csv" writer = csv.writer(response) writer.writerow(["organization", "customer", "amount", "currency", "paid_at", "reference", "pickup_id"]) for p in qs.iterator(): writer.writerow([ p.organization.code, p.customer.name, p.amount, p.currency_code, p.paid_at.isoformat(), p.reference, p.pickup_id or "", ]) return response