Skip to content

Commit

Permalink
Recurring Invoices + New Layout (#459)
Browse files Browse the repository at this point in the history
- added aws SQS handler
- added recurring invoice profiles
- added new webhook handler
- added new invoice page layout
- removed invoice reminders
- removed invoice one time schedules
  • Loading branch information
TreyWW authored Aug 22, 2024
1 parent e9d776e commit 34e0a7b
Show file tree
Hide file tree
Showing 223 changed files with 5,385 additions and 2,660 deletions.
3 changes: 2 additions & 1 deletion .typos.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
[files]
extend-exclude = ["**/bundle.js", "**/bundle.js.map"]
extend-exclude = ["**/bundle.js", "**/bundle.js.map", "**/cal_filters.py"]

[default]
extend-ignore-identifiers-re = [
"zUfuhFKKZCbHTY6aRR46gxiqszMk5tcHjsVFxnUo8VMus4kHGVdIYVbOYYNlKmHV",
"sha384-zUfuhFKKZCbHTY6aRR46gxiqszMk5tcHjsVFxnUo8VMus4kHGVdIYVbOYYNlKmHV"
]
24 changes: 13 additions & 11 deletions assets/scripts/htmx.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,16 @@ htmx.config.useTemplateFragments = true; // for swapping of table items
htmx.config.scrollIntoViewOnBoost = false; // to stop hx-boost scrolling down automatically


window.addEventListener("DOMContentLoaded", (event) => {
document.body.addEventListener("htmx:sendError", htmx_resend);
document.body.addEventListener("htmx:responseError", htmx_resend);
document.body.addEventListener("htmx:loadError", htmx_resend);
document.body.addEventListener("htmx:afterRequest", (event) => {
const drawer = document.getElementById("service_list_drawer");
if (drawer) {
toggleDrawer(false, true);
}
});
});
// window.addEventListener("DOMContentLoaded", (event) => {
// document.body.addEventListener("htmx:sendError", htmx_resend);
// document.body.addEventListener("htmx:responseError", htmx_resend);
// document.body.addEventListener("htmx:loadError", htmx_resend);
// document.body.addEventListener("htmx:afterRequest", (event) => {
// const drawer = document.getElementById("service_list_drawer");
// if (drawer) {
// toggleDrawer(false, true);
// }
// });
// });

// todo: fix retry | only retry connection ones
2 changes: 2 additions & 0 deletions backend/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from __future__ import absolute_import, unicode_literals

__version__ = "0.4.6"
4 changes: 2 additions & 2 deletions backend/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
FeatureFlags,
VerificationCodes,
APIKey,
InvoiceOnetimeSchedule,
QuotaLimit,
QuotaOverrides,
QuotaUsage,
Expand All @@ -32,6 +31,7 @@
ReceiptDownloadToken,
EmailSendStatus,
InvoiceReminder,
InvoiceRecurringProfile,
)

from backend.api.public.models import APIAuthToken
Expand All @@ -58,11 +58,11 @@
FeatureFlags,
VerificationCodes,
APIKey,
InvoiceOnetimeSchedule,
Receipt,
ReceiptDownloadToken,
InvoiceReminder,
APIAuthToken,
InvoiceRecurringProfile,
]
)

Expand Down
33 changes: 0 additions & 33 deletions backend/api/admin/api_keys.py

This file was deleted.

13 changes: 0 additions & 13 deletions backend/api/admin/urls.py

This file was deleted.

20 changes: 20 additions & 0 deletions backend/api/base/breadcrumbs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from django.http import HttpResponse
from django.shortcuts import render

from backend.types.requests import WebRequest
from backend.service.base.breadcrumbs import get_breadcrumbs


def update_breadcrumbs_endpoint(request: WebRequest):
url = request.GET.get("url")

breadcrumb_dict: dict = get_breadcrumbs(url=url)
return render(
request,
"base/breadcrumbs.html",
{
"breadcrumb": breadcrumb_dict.get("breadcrumb"),
"swapping": True,
# "swap": True
},
)
2 changes: 2 additions & 0 deletions backend/api/base/modal.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.shortcuts import render

from backend.api.public.permissions import SCOPE_DESCRIPTIONS
from backend.api.public.models import APIAuthToken
from backend.models import Client, Receipt, User
from backend.models import Invoice
from backend.models import QuotaLimit
Expand Down Expand Up @@ -150,6 +151,7 @@ def open_modal(request: WebRequest, modal_name, context_type=None, context_value
{"name": group, "description": perms["description"], "options": perms["options"]}
for group, perms in SCOPE_DESCRIPTIONS.items()
]
context["APIAuthToken_types"] = APIAuthToken.AdministratorServiceTypes

if modal_name == "edit_team_member_permissions":
team = request.user.logged_in_as_team
Expand Down
3 changes: 2 additions & 1 deletion backend/api/base/urls.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.urls import path
from . import modal, notifications
from . import modal, notifications, breadcrumbs

urlpatterns = [
path(
Expand All @@ -23,6 +23,7 @@
notifications.delete_notification,
name="notifications delete",
),
path("breadcrumbs/refetch/", breadcrumbs.update_breadcrumbs_endpoint, name="breadcrumbs refetch"),
]

app_name = "base"
2 changes: 1 addition & 1 deletion backend/api/clients/fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ def fetch_clients_dropdown(request: HtmxHttpRequest):

return render(
request,
"pages/invoices/create/_view_clients_dropdown.html",
"pages/invoices/create/destinations/_view_clients_dropdown.html",
{"clients": clients.response, "selected_client": selected_client},
)
4 changes: 2 additions & 2 deletions backend/api/invoices/create/services/add_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
from django.views.decorators.http import require_http_methods

from backend.types.htmx import HtmxHttpRequest
from backend.service.invoices.create.services.add import add
from backend.service.invoices.common.create.services.add import add


@require_http_methods(["POST"])
def add_service_endpoint(request: HtmxHttpRequest):
return render(request, "pages/invoices/create/_services_table_body.html", add(request))
return render(request, "pages/invoices/create/services/_services_table_body.html", add(request))
4 changes: 2 additions & 2 deletions backend/api/invoices/create/set_destination.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def set_destination_to(request: HtmxHttpRequest):
except Client.DoesNotExist:
messages.error(request, "Client not found")

return render(request, "pages/invoices/create/_to_destination.html", context)
return render(request, "pages/invoices/create/destinations/_to_destination.html", context)


@require_http_methods(["POST"])
Expand All @@ -33,4 +33,4 @@ def set_destination_from(request: HtmxHttpRequest):

context.update({key: request.POST.get(key, "") for key in to_get})

return render(request, "pages/invoices/create/_from_destination.html", context)
return render(request, "pages/invoices/create/destinations/_from_destination.html", context)
4 changes: 2 additions & 2 deletions backend/api/invoices/edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def change_status(request: HtmxHttpRequest, invoice_id: int, status: str) -> Htt
status = status.lower() if status else ""

if not request.htmx:
return redirect("invoices:dashboard")
return redirect("invoices:single:dashboard")

try:
invoice = Invoice.objects.get(id=invoice_id)
Expand Down Expand Up @@ -118,7 +118,7 @@ def edit_discount(request: HtmxHttpRequest, invoice_id: str):
percentage_amount_str: str = request.POST.get("percentage_amount", "")

if not request.htmx:
return redirect("invoices:dashboard")
return redirect("invoices:single:dashboard")

try:
invoice: Invoice = Invoice.objects.get(id=invoice_id)
Expand Down
27 changes: 24 additions & 3 deletions backend/api/invoices/fetch.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
from django.db.models import When, CharField, F
from django.db.models.expressions import Case, Value
from django.shortcuts import render, redirect
from django.utils import timezone
from django.views.decorators.http import require_http_methods

from backend.decorators import web_require_scopes
from backend.models import Invoice
from backend.types.htmx import HtmxHttpRequest
from backend.service.invoices.fetch import get_context
from backend.service.invoices.common.fetch import get_context


@require_http_methods(["GET"])
@web_require_scopes("invoices:read", True, True)
def fetch_all_invoices(request: HtmxHttpRequest):
# Redirect if not an HTMX request
if not request.htmx:
return redirect("invoices:dashboard")
return redirect("invoices:single:dashboard")

if request.user.logged_in_as_team:
invoices = Invoice.objects.filter(organization=request.user.logged_in_as_team)
Expand All @@ -39,7 +42,25 @@ def fetch_all_invoices(request: HtmxHttpRequest):
},
}

context, _ = get_context(invoices, sort_by, previous_filters, sort_direction, action_filter_type, action_filter_by)
context, invoices = get_context(invoices, sort_by, previous_filters, sort_direction, action_filter_type, action_filter_by)

# check/update payment status to make sure it is correct before invoices are filtered and displayed
invoices.update(
payment_status=Case(
When(
date_due__lt=timezone.now().date(),
payment_status="pending",
then=Value("overdue"),
),
When(
date_due__gt=timezone.now().date(),
payment_status="overdue",
then=Value("pending"),
),
default=F("payment_status"),
output_field=CharField(),
)
)

# Render the HTMX response
return render(request, "pages/invoices/dashboard/_fetch_body.html", context)
File renamed without changes.
52 changes: 52 additions & 0 deletions backend/api/invoices/recurring/delete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from django.contrib import messages
from django.http import JsonResponse, QueryDict, HttpResponse, HttpResponseRedirect
from django.shortcuts import render
from django.urls import resolve, reverse
from django.urls.exceptions import Resolver404
from django.views.decorators.http import require_http_methods

from backend.decorators import web_require_scopes
from backend.models import QuotaLimit, InvoiceRecurringProfile
from backend.service.asyn_tasks.tasks import Task
from backend.service.boto3.scheduler.delete_schedule import delete_boto_schedule
from backend.types.requests import WebRequest


@require_http_methods(["DELETE"])
@web_require_scopes("invoices:write", True, True)
def delete_invoice_recurring_profile_endpoint(request: WebRequest):
delete_items = QueryDict(request.body)

redirect = delete_items.get("redirect", None)

try:
invoice_profile = InvoiceRecurringProfile.objects.get(id=delete_items.get("invoice_profile", ""))
except InvoiceRecurringProfile.DoesNotExist:
messages.error(request, "Invoice recurring profile Not Found")
return render(request, "base/toasts.html")

if not invoice_profile.has_access(request.user):
messages.error(request, "You do not have permission to delete this Invoice recurring profile")
return render(request, "base/toasts.html")

# QuotaLimit.delete_quota_usage("invoices-count", request.user, invoice.id, invoice.date_created)

Task().queue_task(delete_boto_schedule, "InvoiceRecurringSet", invoice_profile.id)

invoice_profile.active = False
invoice_profile.save()

if request.htmx:
if not redirect:
messages.success(request, "Invoice profile deleted")
return render(request, "base/toasts.html")

try:
resolve(redirect)
response = HttpResponse(status=200)
response["HX-Location"] = redirect
return response
except Resolver404:
return HttpResponseRedirect(reverse("invoices:recurring:dashboard"))

return JsonResponse({"message": "Invoice successfully deleted"}, status=200)
Loading

0 comments on commit 34e0a7b

Please sign in to comment.