diff --git a/invoices/logos/sageteam-logo.png b/invoices/logos/sageteam-logo.png new file mode 100644 index 0000000..889e8a6 Binary files /dev/null and b/invoices/logos/sageteam-logo.png differ diff --git a/invoices/signatures/Digital_Sign_Sepehr_Akbarzadeh.png b/invoices/signatures/Digital_Sign_Sepehr_Akbarzadeh.png new file mode 100644 index 0000000..d8125e4 Binary files /dev/null and b/invoices/signatures/Digital_Sign_Sepehr_Akbarzadeh.png differ diff --git a/sage_invoice/admin/invoice.py b/sage_invoice/admin/invoice.py index faa9fc4..841d4ec 100644 --- a/sage_invoice/admin/invoice.py +++ b/sage_invoice/admin/invoice.py @@ -14,7 +14,7 @@ class ItemInline(admin.TabularInline): readonly_fields = ("total_price",) -class ColumnInline(admin.TabularInline): +class ColumnInline(admin.StackedInline): model = Column extra = 1 diff --git a/sage_invoice/migrations/0003_alter_item_quantity.py b/sage_invoice/migrations/0003_alter_item_quantity.py new file mode 100644 index 0000000..4057c5c --- /dev/null +++ b/sage_invoice/migrations/0003_alter_item_quantity.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.2 on 2024-10-17 11:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("sage_invoice", "0002_alter_invoice_template_choice"), + ] + + operations = [ + migrations.AlterField( + model_name="item", + name="quantity", + field=models.PositiveIntegerField( + blank=True, + db_comment="The quantity of the invoice item", + help_text="The quantity of the item.", + null=True, + verbose_name="Quantity", + ), + ), + ] diff --git a/sage_invoice/migrations/0004_alter_expense_invoice.py b/sage_invoice/migrations/0004_alter_expense_invoice.py new file mode 100644 index 0000000..6c949b0 --- /dev/null +++ b/sage_invoice/migrations/0004_alter_expense_invoice.py @@ -0,0 +1,26 @@ +# Generated by Django 5.1.2 on 2024-10-17 13:00 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("sage_invoice", "0003_alter_item_quantity"), + ] + + operations = [ + migrations.AlterField( + model_name="expense", + name="invoice", + field=models.OneToOneField( + db_comment="Reference to the associated invoice", + help_text="The invoice associated with this total.", + on_delete=django.db.models.deletion.CASCADE, + related_name="expense", + to="sage_invoice.invoice", + verbose_name="Invoice", + ), + ), + ] diff --git a/sage_invoice/models/expense.py b/sage_invoice/models/expense.py index d23c63b..c1fde75 100644 --- a/sage_invoice/models/expense.py +++ b/sage_invoice/models/expense.py @@ -73,7 +73,7 @@ class Expense(models.Model): "Invoice", verbose_name=_("Invoice"), on_delete=models.CASCADE, - related_name="total", + related_name="expense", help_text=_("The invoice associated with this total."), db_comment="Reference to the associated invoice", ) diff --git a/sage_invoice/models/item.py b/sage_invoice/models/item.py index bf53624..4535e23 100644 --- a/sage_invoice/models/item.py +++ b/sage_invoice/models/item.py @@ -11,7 +11,8 @@ class Item(models.Model): ) quantity = models.PositiveIntegerField( verbose_name=_("Quantity"), - default=1, + null=True, + blank=True, help_text=_("The quantity of the item."), db_comment="The quantity of the invoice item", ) @@ -49,7 +50,10 @@ class Item(models.Model): ) def save(self, *args, **kwargs): - self.total_price = self.quantity * self.unit_price + if self.quantity: + self.total_price = self.quantity * self.unit_price + else: + self.total_price = self.unit_price super().save(*args, **kwargs) def __str__(self): diff --git a/sage_invoice/templates/quotation_4.html b/sage_invoice/templates/quotation_4.html index ee44fab..45159c0 100644 --- a/sage_invoice/templates/quotation_4.html +++ b/sage_invoice/templates/quotation_4.html @@ -1,16 +1,16 @@ -{% load static custom_filters %} +{% load static custom_filters humanize i18n %} - {{ title }} +
@@ -18,36 +18,47 @@
- {% if logo_url %} - + {% if invoice.logo %} + {% endif %}
-
{{ title }}
+
{{ invoice.title }}
-

Invoice No: {{ tracking_code }}

-

Date: {{ invoice_date }}

+

+ {% trans "Invoice No: " %} + {{ invoice.tracking_code }} +

+

+ {% trans "Date: " %} + {{ invoice.invoice_date }} +

-

Invoice To:

+

+ + {% trans "Invoice To:" %} + +

- {{ customer_name }}
- {{ customer_email }}
- {{ customer_phone }} + {{ invoice.customer_name }}
+ {% if customer_email %} + {{ customer_email|default:'' }} + {% else %} + {{ customer_phone|default:'' }} + {% endif %}

-

Pay To:

-

- Your Company Name
- Company Address
-

+
@@ -58,29 +69,54 @@ - - - {% for column in custom_columns %} - + + + + {% for column in invoice.columns.all %} + {% endfor %} - - - + + + - {% for item in items %} + {% for item in invoice.items.all %} - {% for column in custom_columns %} - + + {% for column in invoice.columns.all %} + {% endfor %} - - - - + + {% endfor %} @@ -92,36 +128,60 @@
ItemDescription{{ column }}{% trans "Item" %}{% trans "Description" %} + {% trans "Qty" %} + + {{ column }} + PriceQtyTotal + {% trans "Price" %} + + {% trans "Total" %} +
{{ forloop.counter }}. {{ item.description }}{{ item.custom_data|get_item:column }} + {% if item.quantity %} + {{ item.quantity }} + {% else %} + - + {% endif %} + {{ item.measurement|default:'' }} + + {% if column.item == item %} + {{ column.value }} + {% else %} + - + {% endif %} + {{ item.unit_price }} {{ currency }}{{ item.quantity }} {{ item.measurement }}{{ item.total_price }} {{ currency }} + {{ item.unit_price|intcomma }} {{ invoice.currency }} + + {{ item.total_price|intcomma }} {{ invoice.currency }} +
- - + + - - + + - - + + - - + + - - + +
Subtotal{{ subtotal }} {{currency}} + {% trans "Subtotal" %} + + {{ invoice.expense.subtotal|intcomma }} + {{currency}} +
Tax ({{ tax_percentage }}%){{ tax_amount }} + {% trans "Tax" %} ({{ invoice.expense.tax_percentage }}%) + + {{ invoice.expense.tax_amount }} +
Discount ({{ discount_percentage }}%)-{{ discount_amount }} + {% trans "Discount" %} ({{ invoice.expense.discount_percentage }}%) + + - {{ invoice.expense.discount_amount }} +
Concession ({{ concession_percentage }}%)-{{ concession_amount }} + {% trans "Concession" %} + ({{ invoice.expense.concession_percentage }}%) + -{{ invoice.expense.concession_amount }} +
Grand Total{{ grand_total }} {{currency}} + {% trans "Grand Total" %} + + {{ invoice.expense.total_amount|intcomma }} {{ invoice.currency}}
@@ -139,14 +199,20 @@
{% endfor %} - -
- + + + + + Print @@ -158,4 +224,5 @@ - + + \ No newline at end of file diff --git a/sage_invoice/views/invoice.py b/sage_invoice/views/invoice.py index 6914501..a215f64 100644 --- a/sage_invoice/views/invoice.py +++ b/sage_invoice/views/invoice.py @@ -1,46 +1,43 @@ -from django.contrib.auth.mixins import LoginRequiredMixin -from django.core.exceptions import PermissionDenied -from django.http import JsonResponse +from django.views import View from django.shortcuts import render +from django.http import JsonResponse +from django.views.generic import TemplateView, DetailView from django.template.loader import render_to_string -from django.views import View -from django.views.generic import TemplateView +from django.core.exceptions import PermissionDenied +from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from sage_invoice.helpers.funcs import get_template_choices from sage_invoice.models import Invoice from sage_invoice.service.invoice_create import QuotationService -class InvoiceDetailView(LoginRequiredMixin, TemplateView): - template_name = "" - permission_denied_message = ( - "No access - You do not have permission to view this page." - ) +class InvoiceDetailView(LoginRequiredMixin, UserPassesTestMixin, DetailView): + model = Invoice + context_object_name = "invoice" + queryset = Invoice.objects.select_related("category", "expense").prefetch_related("items", "columns") - # Define where to redirect the user if not authenticated - login_url = "admin/login/" # Replace with your actual login URL + def test_func(self): + # Ensure that the user is a staff member + return self.request.user.is_staff - def dispatch(self, request, *args, **kwargs): - # Check if the user is staff before rendering the page - if not request.user.is_staff: - raise PermissionDenied() + def handle_no_permission(self): + # If the user is not authenticated, it will redirect to login + # If the user is authenticated but lacks permission, it raises PermissionDenied + if self.request.user.is_authenticated: + raise PermissionDenied(self.permission_denied_message) + return super().handle_no_permission() - return super().dispatch(request, *args, **kwargs) + def get_template_names(self): + # Dynamically select the template based on the invoice type + invoice = self.get_object() + return [f"{invoice.template_choice}.html"] def get_context_data(self, **kwargs): + # Add additional context using the service class context = super().get_context_data(**kwargs) - invoice_slug = self.kwargs.get("slug") - invoice = Invoice.objects.filter(slug=invoice_slug).first() - service = QuotationService() - rendered_content = service.render_context(invoice) - context.update(rendered_content) - - # Dynamically choose the template based on invoice type and choice - if invoice.receipt: - self.template_name = f"{invoice.template_choice}.html" - else: - self.template_name = f"{invoice.template_choice}.html" - + invoice = self.get_object() + context["customer_email"] = invoice.contacts.get("Contact Info").get("email", None) + context["customer_phone"] = invoice.contacts.get("Contact Info").get("phone", None) return context