diff --git a/backend/api/invoices/edit.py b/backend/api/invoices/edit.py
index c278aae2..05a6b27f 100644
--- a/backend/api/invoices/edit.py
+++ b/backend/api/invoices/edit.py
@@ -1,5 +1,4 @@
from datetime import datetime
-from typing import NoReturn
from django.contrib import messages
from django.http import HttpRequest, JsonResponse, HttpResponse
@@ -29,7 +28,7 @@ def edit_invoice(request: HtmxHttpRequest):
)
attributes_to_updates = {
- "date_due": datetime.strptime(request.POST.get("date_due", ""), "%Y-%m-%d").date(),
+ "date_due": request.POST.get("date_due"),
"date_issued": request.POST.get("date_issued"),
"client_name": request.POST.get("to_name"),
"client_company": request.POST.get("to_company"),
@@ -53,7 +52,14 @@ def edit_invoice(request: HtmxHttpRequest):
}
for column_name, new_value in attributes_to_updates.items():
- setattr(invoice, column_name, new_value)
+ if new_value is not None:
+ if column_name == "date_due":
+ try:
+ new_value = datetime.strptime(new_value, "%Y-%m-%d").date()
+ except ValueError:
+ messages.error(request, "Invalid date format for date_due")
+ return render(request, "base/toasts.html")
+ setattr(invoice, column_name, new_value)
invoice.save()
diff --git a/backend/api/invoices/fetch.py b/backend/api/invoices/fetch.py
index a0baff63..6e326555 100644
--- a/backend/api/invoices/fetch.py
+++ b/backend/api/invoices/fetch.py
@@ -1,21 +1,9 @@
-from django.db.models import (
- Q,
- Case,
- When,
- F,
- FloatField,
- ExpressionWrapper,
- CharField,
- Value,
- Sum,
- Prefetch,
-)
from django.shortcuts import render, redirect
-from django.utils import timezone
from django.views.decorators.http import require_http_methods
-from backend.models import Invoice, InvoiceItem
+from backend.models import Invoice
from backend.types.htmx import HtmxHttpRequest
+from backend.service.invoices.fetch import get_context
@require_http_methods(["GET"])
@@ -24,7 +12,10 @@ def fetch_all_invoices(request: HtmxHttpRequest):
if not request.htmx:
return redirect("invoices:dashboard")
- context: dict = {}
+ if request.user.logged_in_as_team:
+ invoices = Invoice.objects.filter(organization=request.user.logged_in_as_team)
+ else:
+ invoices = Invoice.objects.filter(user=request.user)
# Get filter and sort parameters from the request
sort_by = request.GET.get("sort")
@@ -46,110 +37,7 @@ def fetch_all_invoices(request: HtmxHttpRequest):
},
}
- # Fetch invoices for the user, prefetch related items, and select specific fields
- if request.user.logged_in_as_team:
- invoices = Invoice.objects.filter(organization=request.user.logged_in_as_team)
- else:
- invoices = Invoice.objects.filter(user=request.user)
-
- invoices = (
- invoices.prefetch_related(
- Prefetch(
- "items",
- queryset=InvoiceItem.objects.annotate(
- subtotal=ExpressionWrapper(
- F("hours") * F("price_per_hour"),
- output_field=FloatField(),
- ),
- ),
- ),
- )
- .select_related("client_to", "client_to__user")
- .only("invoice_id", "id", "payment_status", "date_due", "client_to", "client_name")
- .annotate(
- subtotal=Sum(F("items__hours") * F("items__price_per_hour")),
- amount=Case(
- When(vat_number=True, then=F("subtotal") * 1.2),
- default=F("subtotal"),
- output_field=FloatField(),
- ),
- )
- .distinct() # just an extra precaution
- )
-
- # Initialize context variables
- context["selected_filters"] = []
- context["all_filters"] = {item: [i for i, _ in dictio.items()] for item, dictio in previous_filters.items()}
-
- # Initialize OR conditions for filters using Q objects
- or_conditions = Q()
-
- # Iterate through previous filters to build OR conditions
- for filter_type, filter_by_list in previous_filters.items():
- or_conditions_filter = Q() # Initialize OR conditions for each filter type
- for filter_by, status in filter_by_list.items():
- # Determine if the filter was selected in the previous request
- was_previous_selection = True if status else False
- # Determine if the filter is selected in the current request
- has_just_been_selected = True if action_filter_by == filter_by and action_filter_type == filter_type else False
-
- # Check if the filter status has changed
- if (was_previous_selection and not has_just_been_selected) or (not was_previous_selection and has_just_been_selected):
- # Construct filter condition dynamically based on filter_type
- if "+" in filter_by:
- numeric_part = float(filter_by.split("+")[0])
- filter_condition: dict[str, str | float] = {f"{filter_type}__gte": numeric_part}
- else:
- filter_condition = {f"{filter_type}": filter_by}
- or_conditions_filter |= Q(**filter_condition)
- context["selected_filters"].append(filter_by)
-
- # Combine OR conditions for each filter type with AND
- or_conditions &= or_conditions_filter
-
- # 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(),
- )
- )
-
- # Apply OR conditions to the invoices queryset
- invoices = invoices.filter(or_conditions)
-
- # Validate and sanitize the sort_by parameter
- all_sort_options = ["date_due", "id", "payment_status"]
- context["all_sort_options"] = all_sort_options
-
- # Apply sorting to the invoices queryset
- if sort_by not in all_sort_options:
- sort_by = "id"
- elif sort_by in all_sort_options:
- # True is for reverse order
- # first time set direction is none
- if sort_direction is not None and sort_direction.lower() == "true" or sort_direction == "":
- context["sort"] = f"-{sort_by}"
- context["sort_direction"] = False
- invoices = invoices.order_by(f"-{sort_by}")
- else:
- # sort_direction is False
- context["sort"] = sort_by
- context["sort_direction"] = True
- invoices = invoices.order_by(sort_by)
-
- # Add invoices to the context
- context["invoices"] = invoices
+ context, _ = get_context(invoices, sort_by, sort_direction, action_filter_type, action_filter_by, previous_filters)
# Render the HTMX response
return render(request, "pages/invoices/dashboard/_fetch_body.html", context)
diff --git a/backend/api/public/endpoints/Invoices/add.py b/backend/api/public/endpoints/Invoices/add_service.py
similarity index 100%
rename from backend/api/public/endpoints/Invoices/add.py
rename to backend/api/public/endpoints/Invoices/add_service.py
diff --git a/backend/api/public/endpoints/Invoices/delete.py b/backend/api/public/endpoints/Invoices/delete.py
new file mode 100644
index 00000000..f170ce1d
--- /dev/null
+++ b/backend/api/public/endpoints/Invoices/delete.py
@@ -0,0 +1,26 @@
+from django.http import QueryDict
+from django.urls import resolve, Resolver404
+from rest_framework import status
+from rest_framework.decorators import api_view
+from rest_framework.response import Response
+
+from backend.models import Invoice, QuotaLimit
+
+
+@api_view(["DELETE"])
+def delete_invoice_endpoint(request):
+ delete_items = QueryDict(request.body)
+
+ try:
+ invoice = Invoice.objects.get(id=delete_items.get("invoice", ""))
+ except Invoice.DoesNotExist:
+ return Response({"error": "Invoice Not Found"}, status=status.HTTP_404_NOT_FOUND)
+
+ if not invoice.has_access(request.user):
+ return Response({"error": "You do not have permission to delete this invoice"}, status=status.HTTP_403_FORBIDDEN)
+
+ QuotaLimit.delete_quota_usage("invoices-count", request.user, invoice.id, invoice.date_created)
+
+ invoice.delete()
+
+ return Response({"message": "Invoice successfully deleted"}, status=status.HTTP_200_OK)
diff --git a/backend/api/public/endpoints/Invoices/edit.py b/backend/api/public/endpoints/Invoices/edit.py
new file mode 100644
index 00000000..53f23fa0
--- /dev/null
+++ b/backend/api/public/endpoints/Invoices/edit.py
@@ -0,0 +1,134 @@
+from datetime import datetime
+from rest_framework import status
+from rest_framework.decorators import api_view
+from rest_framework.response import Response
+
+from backend.models import Invoice
+
+
+@api_view(["POST"])
+def edit_invoice_endpoint(request):
+ invoice_id = request.data.get("invoice_id", "")
+ if not invoice_id:
+ return Response({"error": "Invoice ID is required"}, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ invoice = Invoice.objects.get(id=invoice_id)
+ except Invoice.DoesNotExist:
+ return Response({"error": "Invoice Not Found"}, status=status.HTTP_404_NOT_FOUND)
+
+ if request.user.logged_in_as_team and request.user.logged_in_as_team != invoice.organization:
+ return Response(
+ {"error": "You do not have permission to edit this invoice"},
+ status=status.HTTP_403_FORBIDDEN,
+ )
+ elif request.user != invoice.user:
+ return Response(
+ {"error": "You do not have permission to edit this invoice"},
+ status=status.HTTP_403_FORBIDDEN,
+ )
+
+ attributes_to_updates = {
+ "date_due": request.POST.get("date_due"),
+ "date_issued": request.POST.get("date_issued"),
+ "client_name": request.POST.get("to_name"),
+ "client_company": request.POST.get("to_company"),
+ "client_address": request.POST.get("to_address"),
+ "client_city": request.POST.get("to_city"),
+ "client_county": request.POST.get("to_county"),
+ "client_country": request.POST.get("to_country"),
+ "self_name": request.POST.get("from_name"),
+ "self_company": request.POST.get("from_company"),
+ "self_address": request.POST.get("from_address"),
+ "self_city": request.POST.get("from_city"),
+ "self_county": request.POST.get("from_county"),
+ "self_country": request.POST.get("from_country"),
+ "notes": request.POST.get("notes"),
+ "invoice_number": request.POST.get("invoice_number"),
+ "vat_number": request.POST.get("vat_number"),
+ "reference": request.POST.get("reference"),
+ "sort_code": request.POST.get("sort_code"),
+ "account_number": request.POST.get("account_number"),
+ "account_holder_name": request.POST.get("account_holder_name"),
+ }
+
+ for column_name, new_value in attributes_to_updates.items():
+ if new_value is not None:
+ if column_name == "date_due":
+ try:
+ new_value = datetime.strptime(new_value, "%Y-%m-%d").date()
+ except ValueError:
+ return Response({"error": "Invalid date format for date_due"}, status=status.HTTP_400_BAD_REQUEST)
+ setattr(invoice, column_name, new_value)
+
+ invoice.save()
+
+ return Response({"message": "Invoice successfully edited"}, status=status.HTTP_200_OK)
+
+
+@api_view(["POST"])
+def change_status_endpoint(request, invoice_id: int, invoice_status: str):
+ invoice_status = invoice_status.lower() if invoice_status else ""
+
+ try:
+ invoice = Invoice.objects.get(id=invoice_id)
+ except Invoice.DoesNotExist:
+ return Response({"error": "Invoice Not Found"}, status=status.HTTP_404_NOT_FOUND)
+
+ if request.user.logged_in_as_team and request.user.logged_in_as_team != invoice.organization or request.user != invoice.user:
+ return Response({"error": "You do not have permission to edit this invoice"}, status=status.HTTP_403_FORBIDDEN)
+
+ if invoice_status not in ["paid", "overdue", "pending"]:
+ return Response({"error": "Invalid status. Please choose from: pending, paid, overdue"}, status=status.HTTP_400_BAD_REQUEST)
+
+ if invoice.payment_invoice_status == invoice_status:
+ return Response({"error": f"Invoice status is already {invoice_status}"}, status=status.HTTP_400_BAD_REQUEST)
+
+ invoice.payment_status = invoice_status
+ invoice.save()
+
+ dps = invoice.dynamic_payment_status
+ if (invoice_status == "overdue" and dps == "pending") or (invoice_status == "pending" and dps == "overdue"):
+ message = f"""
+ The invoice status was automatically changed from {invoice_status} to {dps}
+ as the invoice dates override the manual status.
+ """
+ return Response({"error": message}, status=status.HTTP_400_BAD_REQUEST)
+
+ return Response({"message": f"Invoice status been changed to {invoice_status}"}, status=status.HTTP_200_OK)
+
+
+@api_view(["POST"])
+def edit_discount_endpoint(request, invoice_id: str):
+ discount_type = "percentage" if request.data.get("discount_type") == "on" else "amount"
+ discount_amount_str: str = request.data.get("discount_amount", "")
+ percentage_amount_str: str = request.data.get("percentage_amount", "")
+
+ try:
+ invoice: Invoice = Invoice.objects.get(id=invoice_id)
+ except Invoice.DoesNotExist:
+ return Response({"error": "Invoice not found"}, status=status.HTTP_404_NOT_FOUND)
+
+ if not invoice.has_access(request.user):
+ return Response({"error": "You don't have permission to make changes to this invoice."}, status=status.HTTP_403_FORBIDDEN)
+
+ if discount_type == "percentage":
+ try:
+ percentage_amount = int(percentage_amount_str)
+ if percentage_amount < 0 or percentage_amount > 100:
+ raise ValueError
+ except ValueError:
+ return Response({"error": "Please enter a valid percentage amount (between 0 and 100)"}, status=status.HTTP_400_BAD_REQUEST)
+ invoice.discount_percentage = percentage_amount
+ else:
+ try:
+ discount_amount = int(discount_amount_str)
+ if discount_amount < 0:
+ raise ValueError
+ except ValueError:
+ return Response({"error": "Please enter a valid discount amount"}, status=status.HTTP_400_BAD_REQUEST)
+ invoice.discount_amount = discount_amount
+
+ invoice.save()
+
+ return Response({"message": "Discount was applied successfully"}, status=status.HTTP_200_OK)
diff --git a/backend/api/public/endpoints/Invoices/fetch.py b/backend/api/public/endpoints/Invoices/fetch.py
new file mode 100644
index 00000000..dbabab47
--- /dev/null
+++ b/backend/api/public/endpoints/Invoices/fetch.py
@@ -0,0 +1,95 @@
+from drf_yasg import openapi
+from drf_yasg.utils import swagger_auto_schema
+from rest_framework import status
+from rest_framework.decorators import api_view, authentication_classes, permission_classes
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.response import Response
+
+from backend.api.public.authentication import BearerAuthentication
+from backend.api.public.serializers.invoices import InvoiceSerializer
+from backend.models import Invoice
+from backend.service.invoices.fetch import get_context
+
+
+@swagger_auto_schema(
+ method="get",
+ operation_description="Fetch all invoices",
+ operation_id="invoices_list",
+ manual_parameters=[
+ openapi.Parameter(
+ "sort",
+ openapi.IN_QUERY,
+ description="Field you want to order by to. Sort options: 'date_due', 'id', 'payment_status'. Default by " "'id'.",
+ type=openapi.TYPE_STRING,
+ ),
+ openapi.Parameter(
+ "sort_direction",
+ openapi.IN_QUERY,
+ description="Order by descending or ascending. False for descending and True for ascending. Default is " "ascending.",
+ type=openapi.TYPE_BOOLEAN,
+ ),
+ openapi.Parameter(
+ "filter_type",
+ openapi.IN_QUERY,
+ description="Select filter type by which results will be filtered. Filter types are 'payment_status' and "
+ "'amount'. By default there is no filter types applied.",
+ type=openapi.TYPE_STRING,
+ ),
+ openapi.Parameter(
+ "filter",
+ openapi.IN_QUERY,
+ description="Select filter by which results will be filtered. Filters for 'payment_status' are 'paid', "
+ "'pending', 'overdue' and for 'amount' are '20+', '50+', '100+'. By default there is no "
+ "filter applied.",
+ type=openapi.TYPE_STRING,
+ ),
+ ],
+ responses={
+ 200: openapi.Response(
+ description="List of invoices",
+ schema=openapi.Schema(
+ type=openapi.TYPE_OBJECT,
+ properties={
+ "success": openapi.Schema(type=openapi.TYPE_BOOLEAN),
+ "invoices": openapi.Schema(type=openapi.TYPE_ARRAY, items=openapi.Schema(type=openapi.TYPE_OBJECT)),
+ },
+ ),
+ )
+ },
+)
+@api_view(["GET"])
+# @authentication_classes([BearerAuthentication])
+# @permission_classes([IsAuthenticated])
+def fetch_all_invoices_endpoint(request):
+ if request.user.is_authenticated:
+ if hasattr(request.user, "logged_in_as_team"):
+ invoices = Invoice.objects.filter(organization=request.user.logged_in_as_team)
+ else:
+ invoices = Invoice.objects.filter(user=request.user)
+ else:
+ invoices = Invoice.objects.none()
+
+ sort_by = request.data.get("sort")
+ sort_direction = request.data.get("sort_direction", "")
+ action_filter_type = request.data.get("filter_type")
+ action_filter_by = request.data.get("filter")
+
+ # TODO: Decide on how to handle this part or to get rid of it in API
+ previous_filters = {
+ "payment_status": {
+ "paid": True if request.data.get("payment_status_paid") else False,
+ "pending": True if request.data.get("payment_status_pending") else False,
+ "overdue": True if request.data.get("payment_status_overdue") else False,
+ },
+ "amount": {
+ "20+": True if request.data.get("amount_20+") else False,
+ "50+": True if request.data.get("amount_50+") else False,
+ "100+": True if request.data.get("amount_100+") else False,
+ },
+ }
+
+ _, invoices = get_context(invoices, sort_by, sort_direction, action_filter_type, action_filter_by, previous_filters)
+
+ serializer = InvoiceSerializer(invoices, many=True)
+
+ return Response({"success": True, "invoices": serializer.data}, status=status.HTTP_200_OK)
diff --git a/backend/api/public/endpoints/Invoices/set_destination.py b/backend/api/public/endpoints/Invoices/set_destination.py
new file mode 100644
index 00000000..74f541bf
--- /dev/null
+++ b/backend/api/public/endpoints/Invoices/set_destination.py
@@ -0,0 +1,38 @@
+from rest_framework.decorators import api_view
+from rest_framework.response import Response
+from rest_framework import status
+
+from backend.models import Client
+from backend.api.public.serializers.clients import ClientSerializer
+
+
+to_get = ["name", "address", "city", "country", "company", "is_representative"]
+
+
+@api_view(["POST"])
+def set_destination_from_endpoint(request):
+ context: dict = {"swapping": True}
+
+ data = request.data
+ context.update({key: data.get(key, "") for key in to_get})
+
+ return Response(context, status=status.HTTP_200_OK)
+
+
+@api_view(["POST"])
+def set_destination_to_endpoint(request):
+ context: dict = {"swapping": True}
+
+ data = request.data
+ context.update({key: data.get(key, "") for key in to_get})
+
+ use_existing = True if request.data.get("use_existing") == "true" else False
+ selected_client = request.data.get("selected_client") if use_existing else None
+
+ if selected_client:
+ try:
+ client = Client.objects.get(user=request.user, id=selected_client)
+ context["existing_client"] = ClientSerializer(client).data
+ except Client.DoesNotExist:
+ return Response({"detail": "Client not found"}, status=status.HTTP_404_NOT_FOUND)
+ return Response(context, status=status.HTTP_200_OK)
diff --git a/backend/api/public/endpoints/Invoices/urls.py b/backend/api/public/endpoints/Invoices/urls.py
index d8b27c07..2ad9a11a 100644
--- a/backend/api/public/endpoints/Invoices/urls.py
+++ b/backend/api/public/endpoints/Invoices/urls.py
@@ -1,12 +1,12 @@
from django.urls import path
-from . import add, create
+from . import add_service, create, set_destination, delete, edit, fetch
urlpatterns = [
path(
"add_service",
- add.add_service_endpoint,
+ add_service.add_service_endpoint,
name="services add",
),
path(
@@ -14,6 +14,29 @@
create.create_invoice_endpoint,
name="create",
),
+ path(
+ "set_destination/to/",
+ set_destination.set_destination_to_endpoint,
+ name="set_destination to",
+ ),
+ path(
+ "set_destination/from/",
+ set_destination.set_destination_from_endpoint,
+ name="set_destination from",
+ ),
+ path(
+ "delete/",
+ delete.delete_invoice_endpoint,
+ name="delete",
+ ),
+ path(
+ "edit/",
+ edit.edit_invoice_endpoint,
+ name="edit",
+ ),
+ path("edit//set_status//", edit.change_status_endpoint, name="edit status"),
+ path("edit//discount/", edit.edit_discount_endpoint, name="edit discount"),
+ path("fetch/", fetch.fetch_all_invoices_endpoint, name="fetch"),
]
app_name = "invoices"
diff --git a/backend/api/public/endpoints/clients/list.py b/backend/api/public/endpoints/clients/list.py
index 412f5bf1..bfd7be45 100644
--- a/backend/api/public/endpoints/clients/list.py
+++ b/backend/api/public/endpoints/clients/list.py
@@ -41,7 +41,7 @@ def list_clients_endpoint(request):
# paginator = PageNumberPagination()
# paginator.page_size = 5
- search_text = request.GET.get("search")
+ search_text = request.data.get("search")
clients: QuerySet[Client] = fetch_clients(request, search_text=search_text)
diff --git a/backend/api/public/serializers/invoices.py b/backend/api/public/serializers/invoices.py
index 10432936..faeb2603 100644
--- a/backend/api/public/serializers/invoices.py
+++ b/backend/api/public/serializers/invoices.py
@@ -15,34 +15,7 @@ class InvoiceSerializer(serializers.ModelSerializer):
class Meta:
model = Invoice
- fields = [
- "date_due",
- "date_issued",
- "currency",
- "client_id",
- "client_name",
- "client_company",
- "client_address",
- "client_city",
- "client_county",
- "client_country",
- "client_is_representative",
- "self_name",
- "self_company",
- "self_address",
- "self_city",
- "self_county",
- "self_country",
- "notes",
- "invoice_number",
- "vat_number",
- "logo",
- "reference",
- "sort_code",
- "account_number",
- "account_holder_name",
- "items",
- ]
+ fields = "__all__"
# def create(self, validated_data):
# items_data = validated_data.pop('items')
diff --git a/backend/service/invoices/fetch.py b/backend/service/invoices/fetch.py
new file mode 100644
index 00000000..a119e17c
--- /dev/null
+++ b/backend/service/invoices/fetch.py
@@ -0,0 +1,109 @@
+from django.db.models import Prefetch, ExpressionWrapper, F, FloatField, Sum, Case, When, Q, Value, CharField
+from django.utils import timezone
+
+from backend.models import Invoice, InvoiceItem
+
+
+def get_context(invoices, sort_by, sort_direction=True, action_filter_type=None, action_filter_by=None, previous_filters=None):
+ context: dict = {}
+
+ invoices = (
+ invoices.prefetch_related(
+ Prefetch(
+ "items",
+ queryset=InvoiceItem.objects.annotate(
+ subtotal=ExpressionWrapper(
+ F("hours") * F("price_per_hour"),
+ output_field=FloatField(),
+ ),
+ ),
+ ),
+ )
+ .select_related("client_to", "client_to__user")
+ .only("invoice_id", "id", "payment_status", "date_due", "client_to", "client_name")
+ .annotate(
+ subtotal=Sum(F("items__hours") * F("items__price_per_hour")),
+ amount=Case(
+ When(vat_number=True, then=F("subtotal") * 1.2),
+ default=F("subtotal"),
+ output_field=FloatField(),
+ ),
+ )
+ .distinct() # just an extra precaution
+ )
+
+ # Initialize context variables
+ context["selected_filters"] = []
+ context["all_filters"] = {item: [i for i, _ in dictio.items()] for item, dictio in previous_filters.items()}
+
+ # Initialize OR conditions for filters using Q objects
+ or_conditions = Q()
+
+ # Iterate through previous filters to build OR conditions
+ for filter_type, filter_by_list in previous_filters.items():
+ or_conditions_filter = Q() # Initialize OR conditions for each filter type
+ for filter_by, status in filter_by_list.items():
+ # Determine if the filter was selected in the previous request
+ was_previous_selection = True if status else False
+ # Determine if the filter is selected in the current request
+ has_just_been_selected = True if action_filter_by == filter_by and action_filter_type == filter_type else False
+
+ # Check if the filter status has changed
+ if (was_previous_selection and not has_just_been_selected) or (not was_previous_selection and has_just_been_selected):
+ # Construct filter condition dynamically based on filter_type
+ if "+" in filter_by:
+ numeric_part = float(filter_by.split("+")[0])
+ filter_condition: dict[str, str | float] = {f"{filter_type}__gte": numeric_part}
+ else:
+ filter_condition = {f"{filter_type}": filter_by}
+ or_conditions_filter |= Q(**filter_condition)
+ context["selected_filters"].append(filter_by)
+
+ # Combine OR conditions for each filter type with AND
+ or_conditions &= or_conditions_filter
+
+ # 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(),
+ )
+ )
+
+ # Apply OR conditions to the invoices queryset
+ invoices = invoices.filter(or_conditions)
+
+ # Validate and sanitize the sort_by parameter
+ all_sort_options = ["date_due", "id", "payment_status"]
+ context["all_sort_options"] = all_sort_options
+
+ # Apply sorting to the invoices queryset
+ if sort_by not in all_sort_options:
+ context["sort"] = "id"
+ elif sort_by in all_sort_options:
+ # True is for reverse order
+ # first time set direction is none
+ if sort_direction is not None and sort_direction.lower() == "true" or sort_direction == "":
+ context["sort"] = f"-{sort_by}"
+ context["sort_direction"] = False
+ invoices = invoices.order_by(f"-{sort_by}")
+ else:
+ # sort_direction is False
+ context["sort"] = sort_by
+ context["sort_direction"] = True
+ invoices = invoices.order_by(sort_by)
+
+ # Add invoices to the context
+ context["invoices"] = invoices
+
+ return context, invoices