From e70a00748206ffd4f18efe5f9113d74d2cab5bc1 Mon Sep 17 00:00:00 2001 From: Trey <73353716+TreyWW@users.noreply.github.com> Date: Sat, 30 Mar 2024 17:56:46 +0000 Subject: [PATCH] feature: Added invoice discounts --- backend/api/base/modal.py | 7 ++ backend/api/invoices/edit.py | 46 ++++++++- backend/api/invoices/urls.py | 1 + backend/models.py | 32 +++--- .../modals/invoices_edit_discount.html | 64 +++++++++--- .../pages/invoices/dashboard/manage.html | 5 +- .../pages/invoices/view/invoice.html | 6 +- .../pages/invoices/view/invoice_page.html | 3 +- tests/api/test_invoices.py | 99 +++++++++++++++++++ 9 files changed, 231 insertions(+), 32 deletions(-) diff --git a/backend/api/base/modal.py b/backend/api/base/modal.py index ee6ec881a..24ad36062 100644 --- a/backend/api/base/modal.py +++ b/backend/api/base/modal.py @@ -44,6 +44,13 @@ def open_modal(request: HttpRequest, modal_name, context_type=None, context_valu # context["to_city"] = invoice.client_city # context["to_county"] = invoice.client_county # context["to_country"] = invoice.client_country + elif context_type == "invoice": + try: + invoice = Invoice.objects.get(id=context_value) + if invoice.has_access(request.user): + context["invoice"] = invoice + except Invoice.DoesNotExist: + ... else: context[context_type] = context_value diff --git a/backend/api/invoices/edit.py b/backend/api/invoices/edit.py index 27cd8ea92..6f80fdec3 100644 --- a/backend/api/invoices/edit.py +++ b/backend/api/invoices/edit.py @@ -1,4 +1,5 @@ from datetime import datetime +from typing import NoReturn from django.contrib import messages from django.http import HttpRequest, JsonResponse, HttpResponse @@ -99,12 +100,55 @@ def change_status(request: HttpRequest, invoice_id: int, status: str) -> HttpRes return render(request, "pages/invoices/dashboard/_modify_payment_status.html", {"status": status, "invoice_id": invoice_id}) +@require_POST +def edit_discount(request: HttpRequest, invoice_id: str): + discount_type = "percentage" if request.POST.get("discount_type") == "on" else "amount" + discount_amount_str: str = request.POST.get("discount_amount") + percentage_amount_str: str = request.POST.get("percentage_amount") + + if not request.htmx: + return redirect("invoices:dashboard") + + try: + invoice: Invoice = Invoice.objects.get(id=invoice_id) + except Invoice.DoesNotExist: + return return_message(request, "Invoice not found", False) + + if not invoice.has_access(request.user): + return return_message(request, "You don't have permission to make changes to this invoice.", False) + + if discount_type == "percentage": + try: + percentage_amount = int(percentage_amount_str) + if percentage_amount < 0 or percentage_amount > 100: + raise ValueError + except ValueError: + return return_message(request, "Please enter a valid percentage amount (between 0 and 100)", False) + invoice.discount_percentage = percentage_amount + else: + try: + discount_amount = int(discount_amount_str) + if discount_amount < 0: + raise ValueError + except ValueError: + return return_message(request, "Please enter a valid discount amount", False) + invoice.discount_amount = discount_amount + + invoice.save() + + messages.success(request, "Discount was applied successfully") + + response = render(request, "base/toasts.html") + response["HX-Trigger"] = "update_invoice" + return response + + def return_message(request: HttpRequest, message: str, success: bool = True) -> HttpResponse: send_message(request, message, success) return render(request, "base/toasts.html") -def send_message(request: HttpRequest, message: str, success: bool = False) -> HttpResponse: +def send_message(request: HttpRequest, message: str, success: bool = False) -> NoReturn: if success: messages.success(request, message) else: diff --git a/backend/api/invoices/urls.py b/backend/api/invoices/urls.py index 8bc66e9df..a22ed57b3 100644 --- a/backend/api/invoices/urls.py +++ b/backend/api/invoices/urls.py @@ -31,6 +31,7 @@ name="edit", ), path("edit//set_status//", edit.change_status, name="edit status"), + path("edit//discount/", edit.edit_discount, name="edit discount"), path("fetch/", fetch.fetch_all_invoices, name="fetch"), path("schedules/receive/", schedule.receive_scheduled_invoice, name="receive_scheduled_invoice"), path("create_schedule/", schedule.create_schedule, name="create_schedule"), diff --git a/backend/models.py b/backend/models.py index 293a0503b..95438a9d4 100644 --- a/backend/models.py +++ b/backend/models.py @@ -1,4 +1,3 @@ -import decimal from decimal import Decimal from uuid import uuid4 @@ -312,6 +311,17 @@ class Invoice(models.Model): class Meta: constraints = [USER_OR_ORGANIZATION_CONSTRAINT()] + def __str__(self): + invoice_id = self.invoice_id or self.id + if self.client_name: + client = self.client_name + elif self.client_to: + client = self.client_to.name + else: + client = "Unknown Client" + + return f"Invoice #{invoice_id} for {client}" + @property def dynamic_payment_status(self): if self.date_due and timezone.now().date() > self.date_due and self.payment_status == "pending": @@ -334,17 +344,6 @@ def get_to_details(self) -> tuple[str, dict[str, str]]: "company": self.client_company, } - def __str__(self): - invoice_id = self.invoice_id or self.id - if self.client_name: - client = self.client_name - elif self.client_to: - client = self.client_to.name - else: - client = "Unknown Client" - - return f"Invoice #{invoice_id} for {client}" - def get_subtotal(self): subtotal = 0 for item in self.items.all(): @@ -380,6 +379,15 @@ def get_total_price(self): return round(total, 2) + def has_access(self, user: User) -> bool: + if not user.is_authenticated: + return False + + if user.logged_in_as_team: + return self.organization == user.logged_in_as_team + else: + return self.user == user + class InvoiceURL(models.Model): uuid = ShortUUIDField(length=8, primary_key=True) diff --git a/frontend/templates/modals/invoices_edit_discount.html b/frontend/templates/modals/invoices_edit_discount.html index 8a5637345..06610f503 100644 --- a/frontend/templates/modals/invoices_edit_discount.html +++ b/frontend/templates/modals/invoices_edit_discount.html @@ -2,26 +2,64 @@ {% fill "content" %}