Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Discounts #244

Merged
merged 4 commits into from
Mar 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions backend/api/base/modal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
46 changes: 45 additions & 1 deletion backend/api/invoices/edit.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import datetime
from typing import NoReturn

from django.contrib import messages
from django.http import HttpRequest, JsonResponse, HttpResponse
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions backend/api/invoices/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
name="edit",
),
path("edit/<int:invoice_id>/set_status/<str:status>/", edit.change_status, name="edit status"),
path("edit/<str:invoice_id>/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"),
Expand Down
Original file line number Diff line number Diff line change
@@ -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)]
),
),
]
65 changes: 51 additions & 14 deletions backend/models.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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":
Expand All @@ -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)
Expand Down
1 change: 0 additions & 1 deletion frontend/templates/base/_head.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@
htmx.config.globalViewTransitions = true
htmx.config.useTemplateFragments = true // for swapping of table items
Alpine.start()

</script>
{{ analytics|safe }}
{% load tz_detect %}
Expand Down
78 changes: 78 additions & 0 deletions frontend/templates/modals/invoices_edit_discount.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{% component_block "modal" id="modal_invoices_edit_discount" start_open="true" title="Edit invoice discount" %}
{% fill "content" %}
<form class="py-4"
id="modal_invoices_edit_discount-form"
hx-post="{% url 'api:invoices:edit discount' invoice_id=invoice.id %}"
hx-swap="none">
{% csrf_token %}
<div class="form-control my-4">
<label class="label cursor-pointer">
<span class="label-text">Fixed Amount</span>
<input data-discount="checkbox"
name="discount_type"
type="checkbox"
class="toggle"
checked />
<span class="label-text">Percentage</span>
</label>
</div>
<script>
const checkbox = document.querySelector('input[data-discount="checkbox"]');
const amount_label = document.querySelector('label[data-discount="amount"]')
const percentage_label = document.querySelector('label[data-discount="percentage"]')

checkbox.addEventListener("change", function () {
if (checkbox.checked) {
amount_label.classList.add("hidden")
percentage_label.classList.remove("hidden")
amount_label.querySelector("input").required = false;
percentage_label.querySelector("input").required = true;
amount_label.querySelector("input").pattern = "[0-9]+";
percentage_label.querySelector("input").removeAttribute("pattern");
} else {
percentage_label.classList.add("hidden")
amount_label.classList.remove("hidden")
amount_label.querySelector("input").required = true;
percentage_label.querySelector("input").required = false;
amount_label.querySelector("input").pattern = "[0-9]+";
percentage_label.querySelector("input").removeAttribute("pattern");
}
})
</script>
<label data-discount="percentage"
class="input input-bordered flex items-center gap-2">
<i class="fa fa-solid fa-percentage mr-2"></i>
<input required
type="text"
class="grow"
placeholder="Percentage"
value="{{ invoice.discount_percentage|floatformat:0 }}"
name="percentage_amount"
pattern="[0-9]+" />
</label>
<label data-discount="amount"
class="input input-bordered flex items-center gap-2 hidden">
<i class="fa fa-solid fa-pound-sign mr-2"></i>
<input required
type="text"
class="grow"
value="{{ invoice.discount_percentage|floatformat:0 }}"
placeholder="Amount"
name="discount_amount"
pattern="[0-9]+" />
</label>
<div class="modal-action">
<button type="submit"
id="modal_invoices_edit_discount-submit"
class="btn btn-primary"
_="on click if #modal_invoices_edit_discount-form.checkValidity() call #modal_invoices_edit_discount.close() end">
Save
</button>
<button type="reset" class="btn btn-error">Reset</button>
<button type="button"
_="on click call #modal_invoices_edit_discount.close()"
class="btn">Close</button>
</div>
</form>
{% endfill %}
{% endcomponent_block %}
12 changes: 11 additions & 1 deletion frontend/templates/pages/invoices/dashboard/manage.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ <h1 class="text-center col-span-2">Invoice #{{ invoice.id }}</h1>
<div class="card bg-base-100 shadow-xl w-full p-6 flex-col gap-y-4">
<div class="flex flex-wrap gap-y-4">
<button class="btn btn-md btn-outline btn-default grow loading-htmx mr-4"
data-htmx="preview-button"
hx-target="#container"
hx-get="{% url "api:invoices:tab preview" invoice_id=invoice.id %}"
hx-swap="innerHTML"
hx-trigger="load,click,queue:last"
hx-trigger="load,click,update_invoice from:body,queue:last"
hx-indicator="this">
<span class="loading-htmx-text">
<i class="fa-solid fa-file-pdf"></i>
Expand Down Expand Up @@ -90,6 +91,15 @@ <h1 class="text-center col-span-2">Invoice #{{ invoice.id }}</h1>
</li>
</ul>
</div>
<button class="btn btn-secondary"
hx-trigger="click once"
hx-swap="beforeend"
hx-target="#modal_container"
_="on click call modal_invoices_edit_discount.showModal()"
hx-get="{% url "api:base:modal retrieve with context" context_type="invoice" context_value=invoice.id modal_name="invoices_edit_discount" %}">
<i class="fa fa-solid fa-pound-sign mr-2"></i>
Edit Discount
</button>
</div>
</div>
</div>
Expand Down
17 changes: 14 additions & 3 deletions frontend/templates/pages/invoices/view/invoice.html
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,25 @@ <h2 class="text-2xl font-bold text-gray-800">INVOICE</h2>
</td>
</tr>
{% endif %}
{% if invoice.discount %}
{% if invoice.discount_percentage %}
<tr class="bg-gray-50">
<th colspan="3"
class="px-6 py-3 text-left text-sm font-medium text-gray-500 uppercase tracking-wider">
Discount
</th>
<td class="px-6 py-3 text-right text-sm font-medium text-gray-500 uppercase tracking-wider">
-{{ invoice.discount }}%
<td class="px-6 py-3 text-right text-sm font-medium text-error uppercase tracking-wider text-balance">
{{ invoice.discount_percentage }}% off (-{{ currency_symbol }}{{ invoice.get_percentage_amount }})
</td>
</tr>
{% endif %}
{% if invoice.discount_amount %}
<tr class="bg-gray-50">
<th colspan="3"
class="px-6 py-3 text-left text-sm font-medium text-gray-500 uppercase tracking-wider">
{% if not invoice.discount_percentage %}Discount{% endif %}
</th>
<td class="px-6 py-3 text-right text-sm font-medium text-error uppercase tracking-wider">
-{{ currency_symbol }}{{ invoice.discount_amount }}
</td>
</tr>
{% endif %}
Expand Down
2 changes: 1 addition & 1 deletion frontend/templates/pages/invoices/view/invoice_page.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
Print
</button>
<a class="btn btn-sm gradient-btn me-8"
href="{% url "invoices: manage_access" invoice_id=invoice.id %}">Share Invoice</a>
href="{% url "invoices:manage_access" invoice_id=invoice.id %}">Share Invoice</a>
<div class="flex items-center">
<button onclick="document.getElementById('status_banner').remove();"
type="button"
Expand Down
Loading
Loading