From 6b5d39e06991287d115f65bab775ea249c84dbef Mon Sep 17 00:00:00 2001 From: gitea Date: Mon, 30 Dec 2024 11:06:48 +0000 Subject: [PATCH] update the estimate --- inventory/migrations/0008_vatrate.py | 23 ++ inventory/models.py | 11 + inventory/urls.py | 3 + inventory/views.py | 133 +++++++-- scripts/msg.py | 17 ++ static/js/main.js | 20 ++ templates/base.html | 2 +- .../sales/estimates/estimate_detail.html | 68 ++++- .../sales/estimates/estimate_preview.html | 259 ++++++++++++++++++ 9 files changed, 504 insertions(+), 32 deletions(-) create mode 100644 inventory/migrations/0008_vatrate.py create mode 100644 scripts/msg.py create mode 100644 templates/sales/estimates/estimate_preview.html diff --git a/inventory/migrations/0008_vatrate.py b/inventory/migrations/0008_vatrate.py new file mode 100644 index 00000000..754bc92d --- /dev/null +++ b/inventory/migrations/0008_vatrate.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.17 on 2024-12-30 09:45 + +from decimal import Decimal +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0007_vendor_created_at'), + ] + + operations = [ + migrations.CreateModel( + name='VatRate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('rate', models.DecimalField(decimal_places=2, default=Decimal('0.15'), max_digits=5)), + ('is_active', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + ] diff --git a/inventory/models.py b/inventory/models.py index 8025b71b..4768a62b 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -48,6 +48,17 @@ UNIT_CHOICES = ( ("m3", _("m3")), ) +class VatRate(models.Model): + rate = models.DecimalField(max_digits=5, decimal_places=2, default=Decimal('0.15')) + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + + @property + def vat_rate(self): + return self.rate / 100 + def __str__(self): + return f"Rate: {self.rate}%" + class CarMake(models.Model, LocalizedNameMixin): id_car_make = models.AutoField(primary_key=True) name = models.CharField(max_length=255) diff --git a/inventory/urls.py b/inventory/urls.py index 16e421e9..f949e65a 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -123,6 +123,9 @@ urlpatterns = [ path('sales/estimates/', views.EstimateListView.as_view(), name='estimate_list'), path('sales/estimates//', views.EstimateDetailView.as_view(), name='estimate_detail'), path('sales/estimates/create/', views.create_estimate, name='estimate_create'), + 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'), ] diff --git a/inventory/views.py b/inventory/views.py index c4cc26dc..f73c2c77 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -1,9 +1,8 @@ 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 - import logging import json import datetime @@ -1642,7 +1641,6 @@ class EstimateCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): @login_required def create_estimate(request): entity = request.user.dealer.entity - print(entity) if request.method == 'POST': try: data = json.loads(request.body) @@ -1657,27 +1655,39 @@ def create_estimate(request): contract_terms=terms) items = data.get('item[]', []) - items = [entity.get_items_all().filter(pk=item).first() for item in items] quantities = data.get('quantity[]', []) unit_costs = data.get('unitCost[]', []) unit_sales_prices = data.get('unitSalesPrice[]', []) total_costs = data.get('totalCost[]', []) - total_revenues = data.get('totalRevenue[]', []) - - estimate_itemtxs = { - item.item_number: { - 'unit_cost': float(unit_costs[i]), - 'unit_revenue': float(unit_sales_prices[i]), - 'quantity': float(quantities[i]), - 'total_amount': float(total_costs[i]) - } - for i, item in enumerate(items) - } - estimate.migrate_itemtxs( - itemtxs=estimate_itemtxs, - commit=True, - operation=EstimateModel.ITEMIZE_APPEND - ) + # total_revenues = data.get('totalRevenue[]', []) + if items: + if isinstance(items, list): + items = [entity.get_items_all().filter(pk=item).first() for item in items] + estimate_itemtxs = { + item.item_number: { + 'unit_cost': float(unit_costs[i]), + 'unit_revenue': float(unit_sales_prices[i]), + 'quantity': float(quantities[i]), + 'total_amount': float(total_costs[i]) + } + for i, item in enumerate(items) + } + else: + item = entity.get_items_all().filter(pk=items).first() + + estimate_itemtxs = { + item.item_number: { + 'unit_cost': float(unit_costs), + 'unit_revenue': float(unit_sales_prices), + 'quantity': float(quantities), + 'total_amount': float(total_costs) + } + } + estimate.migrate_itemtxs( + itemtxs=estimate_itemtxs, + commit=True, + operation=EstimateModel.ITEMIZE_APPEND + ) url = reverse_lazy('estimate_detail', kwargs={'pk': estimate.pk}) return JsonResponse({'status': 'success', 'message': 'Estimate created successfully!','url':url}) except Exception as e: @@ -1693,5 +1703,86 @@ class EstimateDetailView(LoginRequiredMixin, DetailView): context_object_name = "estimate" def get_context_data(self, **kwargs): - kwargs["total_unit_price"] = self.object.get_itemtxs_data()[0].first().ce_unit_cost_estimate * self.object.get_itemtxs_data()[0].first().ce_quantity + estimate = kwargs.get("object") + if estimate.get_itemtxs_data(): + total = sum(x.ce_cost_estimate for x in estimate.get_itemtxs_data()[0].all()) + 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) + +@login_required +def estimate_mark_as(request, pk): + estimate = get_object_or_404(EstimateModel, pk=pk) + mark = request.GET.get('mark') + if mark: + if mark == "review": + if not estimate.can_review(): + messages.error(request, "Estimate is not ready for review") + return redirect("estimate_detail", pk=estimate.pk) + estimate.mark_as_review() + elif mark == "accepted": + if not estimate.can_approve(): + 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() + messages.success(request, "Estimate marked as " + mark.upper()) + return redirect("estimate_detail", pk=estimate.pk) + +def send_email(request,pk): + estimate = get_object_or_404(EstimateModel, pk=pk) + if not estimate.can_review(): + messages.error(request, "Estimate is not ready for review") + 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. + + Thank you for your attention to this matter. + + Best regards + + """ + subject = f'Estimate-{estimate.estimate_number} Review' + message = msg + from_email = 'manager@tenhal.sa' + recipient_list = ['cutomer@tenhal.sa'] + + send_mail(subject, message, from_email, recipient_list) + messages.success(request, "Email sent successfully!") + estimate.mark_as_review() + return redirect("estimate_detail", pk=estimate.pk) + +class EstimatePreviewView(LoginRequiredMixin, DetailView): + model = EstimateModel + context_object_name = "estimate" + template_name = "sales/estimates/estimate_preview.html" + + def get_context_data(self, **kwargs): + estimate = kwargs.get("object") + if estimate.get_itemtxs_data(): + total = sum(x.ce_cost_estimate for x in estimate.get_itemtxs_data()[0].all()) + 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) \ No newline at end of file diff --git a/scripts/msg.py b/scripts/msg.py new file mode 100644 index 00000000..5aacab48 --- /dev/null +++ b/scripts/msg.py @@ -0,0 +1,17 @@ + +from django.core.mail import send_mail + +def send_test_email(): + subject = 'Test Email from Django' + message = 'Hello, this is a test email sent from Django!' + from_email = 'your-email@example.com' + recipient_list = ['recipient-email@example.com'] + + send_mail(subject, message, from_email, recipient_list) + print('Email sent successfully!') + +def run(): + send_test_email() + + + \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js index 8fcfb405..0bd343e5 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -13,3 +13,23 @@ function getCookie(name) { } return cookieValue; } + + + +const Toast = Swal.mixin({ + toast: true, + position: "top-end", + showConfirmButton: false, + timer: 3000, + timerProgressBar: true, + didOpen: (toast) => { + toast.onmouseenter = Swal.stopTimer; + toast.onmouseleave = Swal.resumeTimer; + } + }); + function notify(tag,msg){ + Toast.fire({ + icon: tag, + titleText: msg + }); + } \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 47aa2a31..5272c81c 100644 --- a/templates/base.html +++ b/templates/base.html @@ -653,7 +653,7 @@ } else { data[key] = value; } - }); + }); // Send data to the server using fetch fetch('http://10.10.1.120:8888/en/sales/estimates/create/', { method: 'POST', diff --git a/templates/sales/estimates/estimate_detail.html b/templates/sales/estimates/estimate_detail.html index 06bd2829..90482f76 100644 --- a/templates/sales/estimates/estimate_detail.html +++ b/templates/sales/estimates/estimate_detail.html @@ -4,15 +4,48 @@ {% block title %}{{ _("View Estimate") }}{% endblock title %} {% block content %} +

Estimate

-
- - +
+ {% if estimate.status == 'draft' %} + Send Estimate + + {% endif %} + {% if estimate.status == 'in_review' %} + + {% endif %} + {% if estimate.status == 'accepted' %} + + {% endif %} + Preview
@@ -60,7 +93,7 @@
{% if estimate.status == 'draft' %} Draft - {% elif estimate.status == 'sent' %} + {% elif estimate.status == 'in_review' %} Sent {% elif estimate.status == 'approved' %} Approved @@ -78,11 +111,12 @@ - - - + + + - + + @@ -95,10 +129,16 @@ {% endfor %} + + + + @@ -142,6 +182,14 @@ // Run the function on page load - window.onload = calculateTotals; + window.onload = calculateTotals; + + function setFormAction(action) { + // Get the form element + const form = document.getElementById('confirmForm'); + // Set the form action with the query parameter + form.action = "{% url 'estimate_mark_as' estimate.pk %}?mark=" + action; + } + {% endblock %} \ No newline at end of file diff --git a/templates/sales/estimates/estimate_preview.html b/templates/sales/estimates/estimate_preview.html new file mode 100644 index 00000000..ded66c2f --- /dev/null +++ b/templates/sales/estimates/estimate_preview.html @@ -0,0 +1,259 @@ + + + + + + Estimate + + + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Estimate

+

Thank you for choosing us. We appreciate your business!

+
+ + +
+

Estimate Number: #{{estimate.estimate_number}}

+

Date: {{estimate.date_in_review}}

+

Customer: {{estimate.customer.customer_name}}

+

Email: {{estimate.customer.email}}

+

Terms: {{estimate.terms|title}}

+
+ + +
+
#ItemQuantity#ItemQuantity Unit PriceTotalTotal
{{item.ce_total_amount}}
Vat ({{vat}}%) + {{vat_amount}} +
Grand Total - 0.00 + {{total}}
+ + + + + + + + + + {% for item in estimate.get_itemtxs_data.0 %} + + + + + + + {% endfor %} + +
ItemQuantityUnit PriceTotal
{{ item.item_model.name }}{{ item.ce_quantity }}{{ item.ce_unit_cost_estimate }}{{ item.ce_cost_estimate }}
+
+ + +
+

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

+

Additional Services: $50.00

+
+ + +
+

Total Amount: ${{total}}

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