diff --git a/inventory/migrations/0015_merge_0008_vatrate_0014_useractivitylog.py b/inventory/migrations/0015_merge_0008_vatrate_0014_useractivitylog.py index 00fd4c37..99663c5c 100644 --- a/inventory/migrations/0015_merge_0008_vatrate_0014_useractivitylog.py +++ b/inventory/migrations/0015_merge_0008_vatrate_0014_useractivitylog.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.4 on 2024-12-30 11:11 +# Generated by Django 4.2.17 on 2024-12-30 11:11 from django.db import migrations diff --git a/inventory/urls.py b/inventory/urls.py index 4de53a8b..b733c446 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -127,6 +127,14 @@ urlpatterns = [ path('sales/estimates//estimate_mark_as/', views.estimate_mark_as, name='estimate_mark_as'), path('sales/estimates//preview/', views.EstimatePreviewView.as_view(), name='estimate_preview'), path('send_email/', views.send_email, name='send_email'), + # Invoice + path('sales/invoices/', views.InvoiceListView.as_view(), name='invoice_list'), + path('sales/invoices//', views.InvoiceDetailView.as_view(), name='invoice_detail'), + path('sales/invoices//preview/', views.InvoicePreviewView.as_view(), name='invoice_preview'), +# path('sales/estimates/create/', views.create_estimate, name='estimate_create'), + path('sales/invoices//invoice_mark_as/', views.invoice_mark_as, name='invoice_mark_as'), +# path('sales/estimates//preview/', views.EstimatePreviewView.as_view(), name='estimate_preview'), +# path('send_email/', views.send_email, name='send_email'), ] diff --git a/inventory/views.py b/inventory/views.py index f9dd7b77..4a2e8aeb 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -1,10 +1,11 @@ +from django.core.mail import send_mail from django.core.paginator import Paginator from django.views.decorators.csrf import csrf_exempt from django_ledger.models import EntityModel, InvoiceModel,BankAccountModel,AccountModel,JournalEntryModel,TransactionModel,EstimateModel,CustomerModel -from django.core.mail import send_mail from django_ledger.forms.bank_account import BankAccountCreateForm,BankAccountUpdateForm from django_ledger.forms.account import AccountModelCreateForm,AccountModelUpdateForm from django_ledger.forms.estimate import EstimateModelCreateForm +from django_ledger.io import roles from django.contrib.admin.models import LogEntry import logging import json @@ -888,7 +889,7 @@ def generate_invoice(request, pk): ledger = entity.get_ledgers().filter(name=f"Payment Ledger for Invoice {invoice_model}").first() if not ledger: ledger = entity.create_ledger(name=f"Payment Ledger for Invoice {invoice_model}",posted=True) - journal_entry = JournalEntryModel.objects.create( + journal_entry = JournalEntryModel.objects.create( posted=False, description=f"Payment for Invoice {invoice_model}", ledger=ledger, @@ -1690,10 +1691,41 @@ class EstimateDetailView(LoginRequiredMixin, DetailView): kwargs["total"] = (total * vat.vat_rate) + total kwargs["vat"] = vat.rate return super().get_context_data(**kwargs) + + +class InvoiceListView(LoginRequiredMixin, ListView): + model = InvoiceModel + template_name = "sales/invoices/invoice_list.html" + context_object_name = "invoices" + def get_queryset(self): + entity = self.request.user.dealer.entity + return entity.get_invoices() + + +class InvoiceDetailView(LoginRequiredMixin, DetailView): + model = InvoiceModel + template_name = "sales/invoices/invoice_detail.html" + context_object_name = "invoice" + + def get_context_data(self, **kwargs): + invoice = kwargs.get("object") + + if invoice.get_itemtxs_data(): + total = sum(x.unit_cost for x in invoice.get_itemtxs_data()[0].all()) + total = int(total) + vat = models.VatRate.objects.filter(is_active=True).first() + kwargs["vate_amount"] = (total * int(vat.vat_rate)) + kwargs["total"] = (total * int(vat.vat_rate)) + total + kwargs["vat"] = vat.rate + return super().get_context_data(**kwargs) + + + @login_required def estimate_mark_as(request, pk): estimate = get_object_or_404(EstimateModel, pk=pk) + entity = estimate.entity mark = request.GET.get('mark') if mark: if mark == "review": @@ -1706,14 +1738,58 @@ def estimate_mark_as(request, pk): messages.error(request, "Estimate is not ready for approval") return redirect("estimate_detail", pk=estimate.pk) estimate.mark_as_approved() + elif mark == "complete": if not estimate.can_complete(): messages.error(request, "Estimate is not ready for completion") return redirect("estimate_detail", pk=estimate.pk) - estimate.mark_as_completed() + # estimate.mark_as_completed() + invoice = entity.create_invoice(customer_model=estimate.customer, + terms=estimate.terms, + cash_account=entity.get_default_coa_accounts().get(name="Cash"), + prepaid_account=entity.get_default_coa_accounts().get(name="Accounts Receivable"), + coa_model=entity.get_default_coa() + ) + + unit_items = estimate.get_itemtxs_data()[0] + invoice_itemtxs = { + i.item_model.item_number: { + 'unit_cost': i.ce_unit_cost_estimate, + 'quantity': i.ce_quantity, + 'total_amount': i.ce_cost_estimate + } for i in unit_items + } + + invoice_itemtxs = invoice.migrate_itemtxs(itemtxs=invoice_itemtxs, + commit=True, + operation=InvoiceModel.ITEMIZE_APPEND) + invoice.bind_estimate(estimate) + invoice.mark_as_review() + invoice.save() + estimate.save() + return redirect("invoice_detail", pk=invoice.pk) + estimate.save() messages.success(request, "Estimate marked as " + mark.upper()) return redirect("estimate_detail", pk=estimate.pk) +@login_required +def invoice_mark_as(request, pk): + invoice = get_object_or_404(InvoiceModel, pk=pk) + entity = EntityModel.objects.first() # will change later + user = entity.admin + mark = request.GET.get('mark') + if mark: + if mark == "accept": + if not invoice.can_approve(): + messages.error(request, "invoice is not ready for approval") + return redirect("invoice_detail", pk=invoice.pk) + invoice.mark_as_approved(entity_slug=entity.slug,user_model=user) + invoice.save() + # elif mark == "complete": + # if not invoice.can_complete(): + # messages.error(request, "invoice is not ready for completion") + return redirect("invoice_detail", pk=invoice.pk) + def send_email(request,pk): estimate = get_object_or_404(EstimateModel, pk=pk) if not estimate.can_review(): @@ -1721,25 +1797,28 @@ def send_email(request,pk): return redirect("estimate_detail", pk=estimate.pk) msg = f""" السلام عليكم - {estimate.customer.customer_name}, - - أود أن أشارككم تقدير المشروع الذي ناقشناه. يرجى العثور على الوثيقة التفصيلية للمقترح المرفقة. - - يرجى مراجعة المقترح وإعلامي إذا كانت لديك أي أسئلة أو مخاوف. إذا كانت كل شيء يبدو جيدًا، يمكننا المضي قدمًا في المشروع. - - شكراً لاهتمامكم بهذا الأمر. - Estimate - تحياتي, Dear {estimate.customer.customer_name}, - I hope this email finds you well. I wanted to share with you the estimate for the project we discussed. Please find attached the detailed estimate document. + أود أن أشارككم تقدير المشروع الذي ناقشناه. يرجى العثور على الوثيقة التفصيلية للمقترح المرفقة. - Please review the estimate and let me know if you have any questions or concerns. If everything looks good, we can proceed with the project. + I hope this email finds you well. I wanted to share with you the estimate for the project we discussed. Please find the detailed estimate document attached. - Thank you for your attention to this matter. + يرجى مراجعة المقترح وإعلامي إذا كانت لديك أي أسئلة أو مخاوف. إذا كانت كل شيء يبدو جيدًا، يمكننا المضي قدمًا في المشروع. - Best regards + Please review the estimate and let me know if you have any questions or concerns. If everything looks good, we can proceed with the project. + شكراً لاهتمامكم بهذا الأمر. + Thank you for your attention to this matter. + + Estimate Link: + View Estimate + + تحياتي, + Best regards, + [Your Name] + [Your Position] + [Your Company Name] + [Your Contact Information] """ subject = f'Estimate-{estimate.estimate_number} Review' message = msg @@ -1766,6 +1845,22 @@ class EstimatePreviewView(LoginRequiredMixin, DetailView): kwargs["vat"] = vat.rate return super().get_context_data(**kwargs) +class InvoicePreviewView(LoginRequiredMixin, DetailView): + model = InvoiceModel + context_object_name = "invoice" + template_name = "sales/invoices/invoice_preview.html" + + def get_context_data(self, **kwargs): + invoice = kwargs.get("object") + if invoice.get_itemtxs_data(): + total = sum(x.unit_cost for x in invoice.get_itemtxs_data()[0].all()) + total = int(total) + vat = models.VatRate.objects.filter(is_active=True).first() + kwargs["vate_amount"] = (total * vat.vat_rate) + kwargs["total"] = (total * vat.vat_rate) + total + kwargs["vat"] = vat.rate + return super().get_context_data(**kwargs) + class UserActivityLogListView(ListView): model = models.UserActivityLog @@ -1777,4 +1872,9 @@ class UserActivityLogListView(ListView): queryset = super().get_queryset() if 'user' in self.request.GET: queryset = queryset.filter(user__email=self.request.GET['user']) - return queryset \ No newline at end of file + return queryset + + + +def record_payment(request): + invoice = get_object_or_404(InvoiceModel, pk=request.POST.get('invoice')) \ No newline at end of file diff --git a/templates/sales/estimates/estimate_detail.html b/templates/sales/estimates/estimate_detail.html index 90482f76..8801434a 100644 --- a/templates/sales/estimates/estimate_detail.html +++ b/templates/sales/estimates/estimate_detail.html @@ -42,8 +42,8 @@ {% if estimate.status == 'in_review' %} {% endif %} - {% if estimate.status == 'accepted' %} - + {% if estimate.status == 'approved' %} + {% endif %} Preview diff --git a/templates/sales/invoice/invoice_detail.html b/templates/sales/invoice/invoice_detail.html deleted file mode 100644 index 91e4d832..00000000 --- a/templates/sales/invoice/invoice_detail.html +++ /dev/null @@ -1,77 +0,0 @@ -{% extends "base.html" %} -{% load i18n %} - -{% block title %}{{ _("View Customer") }}{% endblock title %} - -{% block content %} - - - -
-
-
-

{{ _("Customer Details") }}

-
-
-
-
-

{{ _("First Name") }}: {{ customer.first_name }}

-

{{ _("Middle Name") }}: {{ customer.middle_name }}

-

{{ _("Last Name") }}: {{ customer.last_name }}

-
-
-

{{ _("Email") }}: {{ customer.email }}

-

{{ _("National ID") }}: {{ customer.national_id }}

-

{{ _("Phone Number") }}: {{ customer.phone_number }}

-

{{ _("Address") }}: {{ customer.address }}

-
-
-
- -
-
-{% endblock %} diff --git a/templates/sales/invoices/invoice_detail.html b/templates/sales/invoices/invoice_detail.html new file mode 100644 index 00000000..9d28d86a --- /dev/null +++ b/templates/sales/invoices/invoice_detail.html @@ -0,0 +1,180 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block title %}{{ _("View Estimate") }}{% endblock title %} + +{% block content %} + + + +
+
+
+

Invoice

+
+ {% if invoice.invoice_status == 'in_review' %} + + {% endif %} + Preview +
+
+
+
+
+
+
+
+
+
invoice No :
+
+
+

#{{invoice.invoice_number}}

+
+
+
+
+
+
+
invoice Date :
+
+
+

{{invoice.created}}

+
+
+
+
+
+
+
+
+
Customer :
+

{{invoice.customer.customer_name}}

+
+
+
Email :
+

{{invoice.customer.email}}

+
+
+
+
+
+
+
invoice Status :
+
+ {% if invoice.invoice_status == 'draft' %} + Draft + {% elif invoice.invoice_status == 'in_review' %} + Sent + {% elif invoice.invoice_status == 'approved' %} + Approved + {% elif invoice.invoice_status == 'declined' %} + Declined + {% endif %} +
+
+
+
+
+
+
+
+ + + + + + + + + + + + {% for item in invoice.get_itemtxs_data.0 %} + + + + + + + + {% endfor %} + + + + + + + + + +
#ItemQuantityUnit PriceTotal
{{forloop.counter}}{{item.item_model.name}}{{item.quantity}}{{item.unit_cost}}{{item.total_amount}}
Vat ({{vat}}%) + {{vat_amount}} +
Grand Total + {{total}} +
+
+ +
+
+ + + +
+ + +{% endblock %} + +{% block extra_js %} + + +{% endblock %} \ No newline at end of file diff --git a/templates/sales/invoices/invoice_list.html b/templates/sales/invoices/invoice_list.html new file mode 100644 index 00000000..406592a1 --- /dev/null +++ b/templates/sales/invoices/invoice_list.html @@ -0,0 +1,50 @@ +{% extends "base.html" %} +{% load i18n static %} + +{% block title %}{{ _("Invoices") }}{% endblock title %} + +{% block content %} +
+

{% trans "Invoices" %}

+
+ +
+ + + + + + + + + + + + + {% for invoice in invoices %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Invoice Number" %}{% trans "Customer" %}{% trans "Status" %}{% trans "Status Date" %}{% trans "Created" %}{% trans "Actions" %}
{{ invoice.invoice_number }}{{ invoice.customer }}{{ invoice.invoice_status }}{{ invoice.date_approved }}{{ invoice.created }} + + {% trans "view" %} + +
{% trans "No Quotations Found" %}
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/sales/invoices/invoice_preview.html b/templates/sales/invoices/invoice_preview.html new file mode 100644 index 00000000..701fa95f --- /dev/null +++ b/templates/sales/invoices/invoice_preview.html @@ -0,0 +1,259 @@ + + + + + + invoice + + + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

invoice

+

Thank you for choosing us. We appreciate your business!

+
+ + +
+

invoice Number: #{{invoice.invoice_number}}

+

Date: {{invoice.date_in_review}}

+

Customer: {{invoice.customer.customer_name}}

+

Email: {{invoice.customer.email}}

+

Terms: {{invoice.terms|title}}

+
+ + +
+ + + + + + + + + + + {% for item in invoice.get_itemtxs_data.0 %} + + + + + + + {% endfor %} + +
ItemQuantityUnit PriceTotal
{{ item.item_model.name }}{{ item.quantity }}{{ item.unit_cost }}{{ item.total_amount }}
+
+ + +
+

VAT ({{vat}}%): ${{vate_amount}}

+

Additional Services: $50.00

+
+ + +
+

Total Amount: ${{total}}

+
+ + + + +
+ + + + + + + + + + \ No newline at end of file