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.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") # 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