説明なし

views.py 8.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. from django.shortcuts import render, redirect, get_object_or_404
  2. from django.contrib.admin.views.decorators import staff_member_required
  3. from django.contrib import messages
  4. from django.views.decorators.http import require_POST
  5. from django.urls import reverse
  6. from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
  7. import django_filters as filters
  8. from admin_frontend.templatetags.public_urls import public_route
  9. from admin_frontend.nav import _nav_items
  10. from cms.views import breadcrumbs
  11. from orgs.models import Organization
  12. from recycle_core.models import Customer
  13. from recycle_core.forms import PaymentForm
  14. from .models import Invoice, Payment, Payout
  15. from django.db.models import Sum, F, ExpressionWrapper, DecimalField, Value
  16. from django.db.models.functions import Coalesce
  17. from decimal import Decimal
  18. from django.http import HttpResponse
  19. import csv
  20. # Invoices UI ---------------------------------------------------------------
  21. @staff_member_required
  22. @public_route(label="Invoices", order=50, icon="receipt-percent")
  23. @breadcrumbs(label="Invoices", name="re_invoices")
  24. def invoices_list(request):
  25. class InvoiceFilter(filters.FilterSet):
  26. organization = filters.ModelChoiceFilter(queryset=Organization.objects.all())
  27. customer = filters.ModelChoiceFilter(queryset=Customer.objects.all())
  28. status = filters.ChoiceFilter(field_name="status", choices=Invoice.STATUS_CHOICES)
  29. issued_at = filters.DateFromToRangeFilter(field_name="issued_at", label="Issued between")
  30. class Meta:
  31. model = Invoice
  32. fields = ["organization", "customer", "status", "issued_at"]
  33. base_qs = (
  34. Invoice.objects.select_related("organization", "customer")
  35. .annotate(
  36. payments_total=Coalesce(
  37. Sum("payments__amount"),
  38. Value(Decimal("0.00")),
  39. output_field=DecimalField(max_digits=14, decimal_places=2),
  40. ),
  41. )
  42. .annotate(
  43. balance=ExpressionWrapper(
  44. F("total_amount") - F("payments_total"),
  45. output_field=DecimalField(max_digits=14, decimal_places=2),
  46. )
  47. )
  48. .order_by("-issued_at", "-id")
  49. )
  50. i_filter = InvoiceFilter(request.GET, queryset=base_qs)
  51. qs = i_filter.qs
  52. org = getattr(request, "org", None)
  53. if org is not None:
  54. qs = qs.filter(organization=org)
  55. paginator = Paginator(qs, 10)
  56. page = request.GET.get("page")
  57. try:
  58. page_obj = paginator.page(page)
  59. except PageNotAnInteger:
  60. page_obj = paginator.page(1)
  61. except EmptyPage:
  62. page_obj = paginator.page(paginator.num_pages)
  63. pay_form = PaymentForm()
  64. context = {
  65. "nav": _nav_items(),
  66. "invoices": page_obj.object_list,
  67. "page_obj": page_obj,
  68. "filter": i_filter,
  69. "pay_form": pay_form,
  70. }
  71. return render(request, "recycle_core/invoices_list.html", context)
  72. @staff_member_required
  73. def invoice_add_payment(request, pk: int):
  74. invoice = get_object_or_404(Invoice, pk=pk)
  75. form = PaymentForm(request.POST)
  76. if form.is_valid():
  77. Payment.objects.create(
  78. invoice=invoice,
  79. amount=form.cleaned_data["amount"],
  80. currency_code=invoice.currency_code,
  81. received_at=form.cleaned_data.get("received_at") or None,
  82. reference=form.cleaned_data.get("reference", ""),
  83. )
  84. messages.success(request, f"Payment recorded for invoice #{invoice.id}.")
  85. else:
  86. messages.error(request, "Invalid payment data.")
  87. return redirect("billing:invoices_list")
  88. @staff_member_required
  89. @breadcrumbs(label="Invoice Detail", parent="re_invoices")
  90. def invoice_detail(request, pk: int):
  91. inv = get_object_or_404(
  92. Invoice.objects.select_related("organization", "customer").prefetch_related("lines", "payments"), pk=pk
  93. )
  94. # Scope check to current org if applicable
  95. org = getattr(request, "org", None)
  96. if org is not None and inv.organization_id != org.id:
  97. messages.error(request, "Invoice not in this organization.")
  98. return redirect("billing:invoices_list")
  99. # Totals
  100. payments_total = inv.payments.aggregate(s=Coalesce(Sum("amount"), Value(Decimal("0.00"))))["s"] or Decimal("0.00")
  101. balance = (Decimal(inv.total_amount) - Decimal(payments_total)).quantize(Decimal("0.01"))
  102. pay_form = PaymentForm()
  103. context = {
  104. "nav": _nav_items(),
  105. "invoice": inv,
  106. "payments_total": payments_total,
  107. "balance": balance,
  108. "pay_form": pay_form,
  109. "status_choices": Invoice.STATUS_CHOICES,
  110. }
  111. return render(request, "recycle_core/invoice_detail.html", context)
  112. @staff_member_required
  113. @require_POST
  114. def invoice_set_status(request, pk: int):
  115. inv = get_object_or_404(Invoice, pk=pk)
  116. new_status = (request.POST.get("status") or "").strip()
  117. valid = {k for k, _ in Invoice.STATUS_CHOICES}
  118. if new_status not in valid:
  119. messages.error(request, "Invalid status selection.")
  120. return redirect("billing:invoice_detail", pk=inv.id)
  121. inv.status = new_status
  122. inv.save(update_fields=["status", "updated_at"])
  123. messages.success(request, f"Invoice status updated to '{new_status}'.")
  124. return redirect("billing:invoice_detail", pk=inv.id)
  125. # Payouts UI + CSV ----------------------------------------------------------
  126. @staff_member_required
  127. @public_route(label="Payouts", order=52, icon="banknotes")
  128. @breadcrumbs(label="Payouts", name="re_payouts")
  129. def payouts_list(request):
  130. class PayoutFilter(filters.FilterSet):
  131. organization = filters.ModelChoiceFilter(queryset=Organization.objects.all())
  132. customer = filters.ModelChoiceFilter(queryset=Customer.objects.all())
  133. paid_at = filters.DateFromToRangeFilter(field_name="paid_at", label="Paid between")
  134. class Meta:
  135. model = Payout
  136. fields = ["organization", "customer", "paid_at"]
  137. base_qs = Payout.objects.select_related("organization", "customer").order_by("-paid_at", "-id")
  138. p_filter = PayoutFilter(request.GET, queryset=base_qs)
  139. qs = p_filter.qs
  140. org = getattr(request, "org", None)
  141. if org is not None:
  142. qs = qs.filter(organization=org)
  143. paginator = Paginator(qs, 10)
  144. page = request.GET.get("page")
  145. try:
  146. page_obj = paginator.page(page)
  147. except PageNotAnInteger:
  148. page_obj = paginator.page(1)
  149. except EmptyPage:
  150. page_obj = paginator.page(paginator.num_pages)
  151. context = {
  152. "nav": _nav_items(),
  153. "payouts": page_obj.object_list,
  154. "page_obj": page_obj,
  155. "filter": p_filter,
  156. }
  157. return render(request, "recycle_core/payouts_list.html", context)
  158. @staff_member_required
  159. def payouts_export_csv(request):
  160. qs = Payout.objects.select_related("organization", "customer").all()
  161. org_obj = getattr(request, "org", None)
  162. if org_obj is not None:
  163. qs = qs.filter(organization=org_obj)
  164. org = request.GET.get("organization")
  165. cust = request.GET.get("customer")
  166. start = request.GET.get("paid_at_after") or request.GET.get("paid_at_min")
  167. end = request.GET.get("paid_at_before") or request.GET.get("paid_at_max")
  168. from django.utils.dateparse import parse_datetime
  169. if org:
  170. qs = qs.filter(organization_id=org)
  171. if cust:
  172. qs = qs.filter(customer_id=cust)
  173. if start:
  174. dt = parse_datetime(start)
  175. if dt:
  176. qs = qs.filter(paid_at__gte=dt)
  177. if end:
  178. dt = parse_datetime(end)
  179. if dt:
  180. qs = qs.filter(paid_at__lte=dt)
  181. response = HttpResponse(content_type="text/csv")
  182. response["Content-Disposition"] = "attachment; filename=payouts.csv"
  183. writer = csv.writer(response)
  184. writer.writerow(["organization", "customer", "amount", "currency", "paid_at", "reference", "pickup_id"])
  185. for p in qs.iterator():
  186. writer.writerow([
  187. p.organization.code,
  188. p.customer.name,
  189. p.amount,
  190. p.currency_code,
  191. p.paid_at.isoformat(),
  192. p.reference,
  193. p.pickup_id or "",
  194. ])
  195. return response