3 UTC">%!s(int64=2) %!d(string=hace) años SC.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años SD.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años SE.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años SG.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años SH.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años SI.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años SK.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años SL.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años SM.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años SN.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años SO.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años SR.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años ST.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años SV.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años SY.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años SZ.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años TC.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años TD.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años TG.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años TH.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años TJ.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años TK.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años TL.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años TM.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años TN.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años TO.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años TR.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años TT.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años TV.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años TW.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años TZ.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años UA.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años UG.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años US.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años UY.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años UZ.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años VA.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años VC.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años VE.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años VG.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años VI.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años VN.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años VU.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años WF.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años WS.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años YE.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años YT.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años ZA.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años ZM.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años ZW.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años alt-af.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años alt-an.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años alt-as.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años alt-eu.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años alt-na.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años alt-oc.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años alt-sa.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años alt-ww.js 77628cf8bf first comm %!s(int64=2) %!d(string=hace) años tum/ecoloop - Gogs: Simplico Git Service

Sin descripción

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