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/migrations/0026_invoice_discount_amount_invoice_discount_percentage.py b/backend/migrations/0026_invoice_discount_amount_invoice_discount_percentage.py new file mode 100644 index 000000000..04c63b4f1 --- /dev/null +++ b/backend/migrations/0026_invoice_discount_amount_invoice_discount_percentage.py @@ -0,0 +1,26 @@ +# Generated by Django 5.0.3 on 2024-03-29 20:00 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("backend", "0025_alter_invoiceonetimeschedule_stored_schedule_arn"), + ] + + operations = [ + migrations.AddField( + model_name="invoice", + name="discount_amount", + field=models.DecimalField(decimal_places=2, default=0, max_digits=15), + ), + migrations.AddField( + model_name="invoice", + name="discount_percentage", + field=models.DecimalField( + decimal_places=2, default=0, max_digits=5, validators=[django.core.validators.MaxValueValidator(100)] + ), + ), + ] diff --git a/backend/models.py b/backend/models.py index b20f408b6..95438a9d4 100644 --- a/backend/models.py +++ b/backend/models.py @@ -1,7 +1,9 @@ +from decimal import Decimal from uuid import uuid4 from django.contrib.auth.hashers import make_password, check_password from django.contrib.auth.models import UserManager, AbstractUser, AnonymousUser +from django.core.validators import MaxValueValidator from django.db import models from django.db.models import Count from django.utils import timezone @@ -303,9 +305,23 @@ class Invoice(models.Model): date_due = models.DateField() date_issued = models.DateField(blank=True, null=True) + discount_amount = models.DecimalField(max_digits=15, default=0, decimal_places=2) + discount_percentage = models.DecimalField(default=0, max_digits=5, decimal_places=2, validators=[MaxValueValidator(100)]) + 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": @@ -328,29 +344,50 @@ 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(): subtotal += item.get_total_price() return round(subtotal, 2) + def get_tax(self, amount: float = 0.00) -> float: + amount = amount or self.get_subtotal() + if self.vat_number: + return round(amount * 0.2, 2) + return 0 + + def get_percentage_amount(self, subtotal: float = 0.00) -> Decimal: + total = subtotal or self.get_subtotal() + + if self.discount_percentage > 0: + return round(total * (self.discount_percentage / 100), 2) + return Decimal(0) + def get_total_price(self): - total = 0 - subtotal = self.get_subtotal() - total = subtotal * 1.2 if self.vat_number else subtotal + total = self.get_subtotal() or 0.00 + + total -= self.get_percentage_amount() + + discount_amount = self.discount_amount + + total -= discount_amount + + if 0 > total: + total = 0 + else: + total -= self.get_tax(total) + 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/base/_head.html b/frontend/templates/base/_head.html index 0cf667885..303e4398c 100644 --- a/frontend/templates/base/_head.html +++ b/frontend/templates/base/_head.html @@ -56,7 +56,6 @@ htmx.config.globalViewTransitions = true htmx.config.useTemplateFragments = true // for swapping of table items Alpine.start() - {{ analytics|safe }} {% load tz_detect %} diff --git a/frontend/templates/modals/invoices_edit_discount.html b/frontend/templates/modals/invoices_edit_discount.html new file mode 100644 index 000000000..6b6dc146f --- /dev/null +++ b/frontend/templates/modals/invoices_edit_discount.html @@ -0,0 +1,78 @@ +{% component_block "modal" id="modal_invoices_edit_discount" start_open="true" title="Edit invoice discount" %} +{% fill "content" %} + +{% endfill %} +{% endcomponent_block %} diff --git a/frontend/templates/pages/invoices/dashboard/manage.html b/frontend/templates/pages/invoices/dashboard/manage.html index f44bd6ad7..9a7b9f834 100644 --- a/frontend/templates/pages/invoices/dashboard/manage.html +++ b/frontend/templates/pages/invoices/dashboard/manage.html @@ -24,10 +24,11 @@

Invoice #{{ invoice.id }}

+
diff --git a/frontend/templates/pages/invoices/view/invoice.html b/frontend/templates/pages/invoices/view/invoice.html index a44cea8cc..f192547c4 100644 --- a/frontend/templates/pages/invoices/view/invoice.html +++ b/frontend/templates/pages/invoices/view/invoice.html @@ -102,14 +102,25 @@

INVOICE

{% endif %} - {% if invoice.discount %} + {% if invoice.discount_percentage %} Discount - - -{{ invoice.discount }}% + + {{ invoice.discount_percentage }}% off (-{{ currency_symbol }}{{ invoice.get_percentage_amount }}) + + + {% endif %} + {% if invoice.discount_amount %} + + + {% if not invoice.discount_percentage %}Discount{% endif %} + + + -{{ currency_symbol }}{{ invoice.discount_amount }} {% endif %} diff --git a/frontend/templates/pages/invoices/view/invoice_page.html b/frontend/templates/pages/invoices/view/invoice_page.html index 147ee4a82..c1ad5a15a 100644 --- a/frontend/templates/pages/invoices/view/invoice_page.html +++ b/frontend/templates/pages/invoices/view/invoice_page.html @@ -52,7 +52,7 @@ Print Share Invoice + href="{% url "invoices:manage_access" invoice_id=invoice.id %}">Share Invoice