From 10d48ca47dca5b1637e7843e5349593f0bc566cd Mon Sep 17 00:00:00 2001 From: ismail Date: Wed, 17 Sep 2025 15:44:47 +0300 Subject: [PATCH 01/22] fix notification issue --- car_inventory/asgi.py | 10 ++--- car_inventory/urls.py | 2 +- inventory/notifications/sse.py | 79 ---------------------------------- inventory/views.py | 43 +++++++++--------- templates/notifications.html | 45 ++++++++++--------- 5 files changed, 52 insertions(+), 127 deletions(-) diff --git a/car_inventory/asgi.py b/car_inventory/asgi.py index 6dd9d7fa..1a8723dc 100644 --- a/car_inventory/asgi.py +++ b/car_inventory/asgi.py @@ -35,9 +35,9 @@ from pathlib import Path # } # ) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings") -django.setup() +# django.setup() -BASE_DIR = Path(__file__).resolve().parent.parent +# BASE_DIR = Path(__file__).resolve().parent.parent app = get_asgi_application() # app = WhiteNoise(app, root=str(BASE_DIR / 'staticfiles')) @@ -50,7 +50,7 @@ application = ProtocolTypeRouter( path("sse/notifications/", NotificationSSEApp()), re_path( r"", app - ), # All other routes go to Django + ), ] ) ), @@ -58,5 +58,5 @@ application = ProtocolTypeRouter( ) -if django.conf.settings.DEBUG: - application = ASGIStaticFilesHandler(app) \ No newline at end of file +# if django.conf.settings.DEBUG: +# application = ASGIStaticFilesHandler(app) \ No newline at end of file diff --git a/car_inventory/urls.py b/car_inventory/urls.py index 0f1085b0..118d92a3 100644 --- a/car_inventory/urls.py +++ b/car_inventory/urls.py @@ -5,7 +5,7 @@ from django.urls import path, include from schema_graph.views import Schema from django.conf.urls.static import static from django.conf.urls.i18n import i18n_patterns -from inventory.notifications.sse import NotificationSSEApp +# from inventory.notifications.sse import NotificationSSEApp # import debug_toolbar # from two_factor.urls import urlpatterns as tf_urls diff --git a/inventory/notifications/sse.py b/inventory/notifications/sse.py index dd525d14..c00b0ae1 100644 --- a/inventory/notifications/sse.py +++ b/inventory/notifications/sse.py @@ -1,82 +1,3 @@ -# import json -# from django.contrib.auth.models import AnonymousUser -# from django.contrib.auth import get_user_model -# from django.db import close_old_connections -# from urllib.parse import parse_qs -# from channels.db import database_sync_to_async -# from inventory.models import Notification -# import asyncio - -# @database_sync_to_async -# def get_notifications(user, last_id): -# return Notification.objects.filter( -# user=user, id__gt=last_id, is_read=False -# ).order_by("created") - -# class NotificationSSEApp: -# async def __call__(self, scope, receive, send): -# if scope["type"] != "http": -# return - -# query_string = parse_qs(scope["query_string"].decode()) -# last_id = int(query_string.get("last_id", [0])[0]) - -# # Get user from scope if using AuthMiddlewareStack -# user = scope.get("user", AnonymousUser()) -# if not user.is_authenticated: -# await send({ -# "type": "http.response.start", -# "status": 403, -# "headers": [(b"content-type", b"text/plain")], -# }) -# await send({ -# "type": "http.response.body", -# "body": b"Unauthorized", -# }) -# return - -# await send({ -# "type": "http.response.start", -# "status": 200, -# "headers": [ -# (b"content-type", b"text/event-stream"), -# (b"cache-control", b"no-cache"), -# (b"x-accel-buffering", b"no"), -# ] -# }) - -# try: -# while True: -# close_old_connections() - -# notifications = await get_notifications(user, last_id) -# for notification in notifications: -# data = { -# "id": notification.id, -# "message": notification.message, -# "created": notification.created.isoformat(), -# "is_read": notification.is_read, -# } - -# event_str = ( -# f"id: {notification.id}\n" -# f"event: notification\n" -# f"data: {json.dumps(data)}\n\n" -# ) - -# await send({ -# "type": "http.response.body", -# "body": event_str.encode("utf-8"), -# "more_body": True -# }) - -# last_id = notification.id - -# await asyncio.sleep(2) - -# except asyncio.CancelledError: -# pass - import json import time from django.contrib.auth.models import AnonymousUser diff --git a/inventory/views.py b/inventory/views.py index fe6584f2..01b08977 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -5120,28 +5120,28 @@ class EstimatePrintView(EstimateDetailView): uses a dedicated, stripped-down print template. """ template_name = "sales/estimates/estimate_preview.html" - + def get(self, request, *args, **kwargs): - + self.object = self.get_object() context = self.get_context_data(object=self.object) - - + + # lang = request.GET.get('lang', 'ar') - - + + template_path = "sales/estimates/estimate_preview.html" - - + + html_string = render_to_string(template_path, context) - - + + pdf_file = HTML(string=html_string).write_pdf() - - + + response = HttpResponse(pdf_file, content_type='application/pdf') response['Content-Disposition'] = f'attachment; filename="estimate_{self.object.estimate_number}.pdf"' - + return response @@ -10238,7 +10238,8 @@ def payment_callback(request, dealer_slug): # return render(request, "payment_failed.html", {"message": message}) @login_required -async def sse_stream(request): # 👈 Mark as async! +async def sse_stream(request): + import asyncio def event_generator(): last_id = int(request.GET.get("last_id", 0)) @@ -11014,18 +11015,18 @@ class PurchaseOrderDetailView(LoginRequiredMixin, PermissionRequiredMixin, Detai "item_model", "bill_model" ) ) - + if self.object.po_status == 'fulfilled': context['po_items_list']=po_items_qs context['vendor']=po_items_qs.first().bill_model.vendor context['dealer']=request.dealer - + # Check if PDF format is requested if request.GET.get('format') == 'pdf': # Use a separate, print-friendly template for the PDF if request.GET.get('lang')=='en': html_string = render_to_string( - "purchase_orders/po_detail_en_pdf.html", + "purchase_orders/po_detail_en_pdf.html", context ) else: @@ -11033,12 +11034,12 @@ class PurchaseOrderDetailView(LoginRequiredMixin, PermissionRequiredMixin, Detai "purchase_orders/po_detail_ar_pdf.html", context ) - - - + + + # Use WeasyPrint to generate the PDF pdf = HTML(string=html_string).write_pdf() - + response = HttpResponse(pdf, content_type="application/pdf") response["Content-Disposition"] = f'attachment; filename="PO_{self.object.po_number}.pdf"' return response diff --git a/templates/notifications.html b/templates/notifications.html index 0c1d1811..b6a0b1f3 100644 --- a/templates/notifications.html +++ b/templates/notifications.html @@ -87,9 +87,13 @@ didOpen: (toast) => { toast.onmouseenter = Swal.stopTimer; toast.onmouseleave = Swal.resumeTimer; - } }); + } + }); + + {% with last_notif=notifications_|last %} + let lastNotificationId = {{ last_notif.id|default:0 }}; + {% endwith %} - let lastNotificationId = {{ notifications_.last.id|default:0 }}; let seenNotificationIds = new Set(); let counter = document.getElementById('notification-counter'); let notificationsContainer = document.getElementById('notifications-container'); @@ -100,7 +104,6 @@ let initialUnreadCount = {{ notifications_.count|default:0 }}; updateCounter(initialUnreadCount); - fetchInitialNotifications(); function fetchInitialNotifications() { @@ -108,29 +111,22 @@ .then(response => response.json()) .then(data => { if (data.notifications && data.notifications.length > 0) { - lastNotificationId = data.notifications[0].id; - seenNotificationIds = new Set(); let unreadCount = 0; - data.notifications.forEach(notification => { seenNotificationIds.add(notification.id); - if (!notification.is_read) { - unreadCount++; - } + if (!notification.is_read) unreadCount++; }); renderNotifications(data.notifications); - updateCounter(unreadCount); - - - setTimeout(() => { - connectSSE(); - }, 5000); } + // Always connect SSE after initial load + setTimeout(() => { + connectSSE(); + }, 1000); }) .catch(error => { console.error('Error fetching initial notifications:', error); @@ -143,12 +139,12 @@ eventSource.close(); } + // ✅ FIXED URL HERE eventSource = new EventSource("/sse/notifications/?last_id=" + lastNotificationId); eventSource.addEventListener('notification', function(e) { try { const data = JSON.parse(e.data); - if (seenNotificationIds.has(data.id)) return; seenNotificationIds.add(data.id); @@ -158,6 +154,11 @@ updateCounter('increment'); + if (!notificationsContainer) { + console.warn("Notification container missing, can't render SSE event"); + return; + } + const notificationElement = createNotificationElement(data); notificationsContainer.insertAdjacentHTML('afterbegin', notificationElement); @@ -168,7 +169,7 @@ Toast.fire({ icon: 'info', - html:`${data.message}` + html: `${data.message}` }); } catch (error) { @@ -220,7 +221,7 @@ - `; + `; } function updateCounter(action) { @@ -231,12 +232,14 @@ if (notificationCountDiv) { notificationCountDiv.innerHTML = ` 0 - `; + `; counter = document.getElementById('notification-counter'); } } } + if (!counter) return; + let currentCount = parseInt(counter.textContent) || 0; if (action === 'increment') { @@ -294,11 +297,11 @@ notificationCard.closest('.notification-card').classList.add('fade-out'); setTimeout(() => { notificationCard.closest('.notification-card').remove(); - }, 200); + }, 1000); } } }); } }); }); - + \ No newline at end of file From d87ed6d454fbe1109832961ff9bf0a103c9b78fd Mon Sep 17 00:00:00 2001 From: Faheed Date: Thu, 18 Sep 2025 14:29:49 +0300 Subject: [PATCH 02/22] new small changes for btns and cpaitalization and currency formats --- inventory/utils.py | 9 + inventory/views.py | 69 +- templates/groups/group_detail.html | 3 +- .../organizations/organization_list.html | 4 +- templates/purchase_orders/po_detail.html | 4 +- .../purchase_orders/po_detail_ar_pdf.html | 115 ++- .../purchase_orders/po_detail_en_pdf.html | 99 ++- .../tags/po_item_table_print_ar.html | 8 +- .../tags/po_item_table_print_en.html | 12 +- .../sales/estimates/estimate_detail.html | 10 +- .../sales/estimates/estimate_preview.html | 264 ------- .../sales/estimates/estimate_preview_ar.html | 357 ++++++++++ .../sales/estimates/estimate_preview_en.html | 357 ++++++++++ templates/sales/invoices/invoice_detail.html | 10 + templates/sales/invoices/invoice_list.html | 4 +- templates/sales/invoices/invoice_preview.html | 666 +++++++++--------- .../sales/invoices/invoice_preview_ar.html | 359 ++++++++++ .../sales/invoices/invoice_preview_en.html | 357 ++++++++++ templates/sales/sales_list.html | 4 +- templates/vendors/vendors_list.html | 2 +- 20 files changed, 2008 insertions(+), 705 deletions(-) delete mode 100644 templates/sales/estimates/estimate_preview.html create mode 100644 templates/sales/estimates/estimate_preview_ar.html create mode 100644 templates/sales/estimates/estimate_preview_en.html create mode 100644 templates/sales/invoices/invoice_preview_ar.html create mode 100644 templates/sales/invoices/invoice_preview_en.html diff --git a/inventory/utils.py b/inventory/utils.py index 4c8caf36..4cd9aa08 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -1329,7 +1329,9 @@ def get_finance_data(estimate, dealer): additional_services = car.get_additional_services() discounted_price = Decimal(car.marked_price) - discount vat_amount = discounted_price * vat.rate + total_services_amount=additional_services.get("total") total_services_vat = sum([x[1] for x in additional_services.get("services")]) + total_services_amount_=additional_services.get("total_") total_vat = vat_amount + total_services_vat return { "car": car, @@ -1340,9 +1342,16 @@ def get_finance_data(estimate, dealer): "discount_amount": discount, "additional_services": additional_services, "final_price": discounted_price + vat_amount, + + + "total_services_vat": total_services_vat, + "total_services_amount":total_services_amount, + "total_services_amount_":total_services_amount_, + "total_vat": total_vat, "grand_total": discounted_price + total_vat + additional_services.get("total"), + } # totals = self.calculate_totals() diff --git a/inventory/views.py b/inventory/views.py index fe6584f2..b12a475b 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -4656,7 +4656,7 @@ def sales_list_view(request, dealer_slug): search_query = request.GET.get('q', None) if search_query: qs = qs.filter( - Q(order_number__icontains=search_query)| + Q(customer__phone_number__icontains=search_query)| Q(customer__customer_name__icontains=search_query) ).distinct() @@ -5092,6 +5092,7 @@ class EstimateDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView def get_context_data(self, **kwargs): dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) estimate = kwargs.get("object") + if estimate.get_itemtxs_data(): # calculator = CarFinanceCalculator(estimate) # finance_data = calculator.get_finance_data() @@ -5099,6 +5100,8 @@ class EstimateDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView invoice_obj = InvoiceModel.objects.all().filter(ce_model=estimate).first() kwargs["data"] = finance_data + kwargs["customer_obj"]=estimate.customer.customer_set.first() + kwargs['dealer_info']=dealer kwargs["invoice"] = invoice_obj try: @@ -5119,7 +5122,8 @@ class EstimatePrintView(EstimateDetailView): It reuses the data-fetching logic from EstimateDetailView but uses a dedicated, stripped-down print template. """ - template_name = "sales/estimates/estimate_preview.html" + + def get(self, request, *args, **kwargs): @@ -5129,15 +5133,18 @@ class EstimatePrintView(EstimateDetailView): # lang = request.GET.get('lang', 'ar') - - template_path = "sales/estimates/estimate_preview.html" - + + if request.GET.get('lang')=='en': + template_path = "sales/estimates/estimate_preview_en.html" + else: + template_path = "sales/estimates/estimate_preview_ar.html" + html_string = render_to_string(template_path, context) - - pdf_file = HTML(string=html_string).write_pdf() - + base_url = request.build_absolute_uri('/') + pdf_file = HTML(string=html_string, base_url=base_url).write_pdf() + response = HttpResponse(pdf_file, content_type='application/pdf') response['Content-Disposition'] = f'attachment; filename="estimate_{self.object.estimate_number}.pdf"' @@ -5522,7 +5529,7 @@ class InvoiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) entity = dealer.entity staff = getattr(self.request.user, "staff", None) - qs = [] + qs=None try: if any( [ @@ -5871,7 +5878,6 @@ class InvoicePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailView model = InvoiceModel context_object_name = "invoice" - template_name = "sales/invoices/invoice_preview.html" permission_required = ["django_ledger.view_invoicemodel"] def get_context_data(self, **kwargs): @@ -5881,8 +5887,37 @@ class InvoicePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailView # calculator = CarFinanceCalculator(invoice) finance_data = get_finance_data(invoice,dealer) kwargs["data"] = finance_data - kwargs["dealer"] = dealer + kwargs["dealer_info"] = dealer + kwargs["customer_obj"]=invoice.customer.customer_set.first() return super().get_context_data(**kwargs) + def get(self, request, *args, **kwargs): + + self.object = self.get_object() + context = self.get_context_data(object=self.object) + + + # lang = request.GET.get('lang', 'ar') + + + if request.GET.get('lang')=='en': + template_path = "sales/invoices/invoice_preview_en.html" + elif request.GET.get('lang')=='ar': + template_path = "sales/invoices/invoice_preview_ar.html" + else: + # just for preview not for download + return render(request,'sales/invoices/invoice_preview.html',context) + + + html_string = render_to_string(template_path, context) + + base_url = request.build_absolute_uri('/') + pdf_file = HTML(string=html_string, base_url=base_url).write_pdf() + + + response = HttpResponse(pdf_file, content_type='application/pdf') + response['Content-Disposition'] = f'attachment; filename="invoice_{self.object.invoice_number}.pdf"' + + return response # payments @@ -6221,10 +6256,10 @@ class LeadListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): | Q(last_name__icontains=query) | Q(id_car_make__name__icontains=query) | Q(id_car_model__name__icontains=query) - | Q(email__icontains=query) | Q(phone_number__icontains=query) | Q(next_action__icontains=query) - | Q(staff__name__icontains=query) + | Q(staff__first_name__icontains=query) + | Q(staff__last_name__icontains=query) ) if self.request.is_dealer: # or self.request.is_manager: @@ -11005,6 +11040,9 @@ class PurchaseOrderDetailView(LoginRequiredMixin, PermissionRequiredMixin, Detai for i in po_items_qs.values("po_total_amount", "po_item_status") if i["po_item_status"] != "cancelled" ) + items = [{"total": x.total_amount, "q": x.quantity} for x in po_model.get_itemtxs_data()[0].all()] + po_quantity = sum(item["q"] for item in items) + context['po_quantity']=po_quantity return context def get(self, request, *args, **kwargs): self.object = self.get_object() @@ -11035,9 +11073,8 @@ class PurchaseOrderDetailView(LoginRequiredMixin, PermissionRequiredMixin, Detai ) - - # Use WeasyPrint to generate the PDF - pdf = HTML(string=html_string).write_pdf() + base_url = request.build_absolute_uri('/') + pdf = HTML(string=html_string, base_url=base_url).write_pdf() response = HttpResponse(pdf, content_type="application/pdf") response["Content-Disposition"] = f'attachment; filename="PO_{self.object.po_number}.pdf"' diff --git a/templates/groups/group_detail.html b/templates/groups/group_detail.html index b70abb6b..d4f269c8 100644 --- a/templates/groups/group_detail.html +++ b/templates/groups/group_detail.html @@ -35,7 +35,8 @@
-

{{ _("Group Details") }}

+

{{ _("Group Details") }}

+ {% trans "Group List" %}
diff --git a/templates/organizations/organization_list.html b/templates/organizations/organization_list.html index 8c307642..783740d8 100644 --- a/templates/organizations/organization_list.html +++ b/templates/organizations/organization_list.html @@ -125,7 +125,7 @@
- +
{{ org.created|date }} - + {% if perms.inventory.change_organization or perms.inventory.delete_organization %}
{% endif %}
diff --git a/templates/purchase_orders/po_detail_ar_pdf.html b/templates/purchase_orders/po_detail_ar_pdf.html index 1bc3809e..a1dbdc4d 100644 --- a/templates/purchase_orders/po_detail_ar_pdf.html +++ b/templates/purchase_orders/po_detail_ar_pdf.html @@ -1,6 +1,6 @@ {% load tenhal_tag %} {% load custom_filters %} - +{% load i18n static custom_filters num2words_tags %} @@ -109,50 +109,94 @@ /* Footer Styles */ .document-footer { + position: relative; + bottom: 0; + left: 0; + width: 100%; text-align: center; font-size: 10px; color: #888; - border-top: 1px solid #ddd; + padding-top: 15px; - margin-top: 30px; + margin: 0 20mm; + } .footer-flex { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - } + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + } + .footer-logo { + text-align: center; + } + .footer-logo img { + height: 20px; + width: 20px; + } + .footer-logo p { + font-size: 9px; + font-weight: bold; + } + .footer-powered p { + font-size: 11px; + } + .footer-powered span { + font-weight: lighter; + } + .footer-powered a { + color: #112e40; + text-decoration: none; + font-size: 9px; + } + +
+ +
+

{{ dealer.name }}

+ +
+ العنوان : {{ dealer.address }}
+ البريد الإلكتروني : {{ dealer.user.email }}
+ الهاتف : {{ dealer.phone_number }}
+ رقم السجل التجاري : {{ dealer.crn }}  |  رقم ضريبة القيمة المضافة : {{ dealer.vrn }} +
+
+ +
+ +
+

أمر شراء

{{ po_model.po_number }}

-
-

{{ dealer.name }}

-
- العنوان: {{ dealer.address }}
- البريد الإلكتروني: {{ dealer.user.email }}
- الهاتف: {{ dealer.phone_number }}
- رقم السجل التجاري: {{ dealer.crn }}  |  رقم ضريبة القيمة المضافة: {{ dealer.vrn }} -
-
- +

التفاصيل:

-

رقم أمر الشراء: {{ po_model.po_number }}

-

تاريخ الإصدار: {{ po_model.date_fulfilled|date:"Y/m/d" }}

+

رقم أمر الشراء :  {{ po_model.po_number }}

+

تاريخ الإصدار :  {{ po_model.date_fulfilled|date:"Y/m/d" }}

يُرسل إلى:

-

المورد: {{ vendor.vendor_name }}

-

البريد الإلكتروني: {{ vendor.email }}

-

الهاتف: {{ vendor.phone }}

-

العنوان: {{ vendor.address_1 }}

+

المورد :  {{ vendor.vendor_name }}

+

البريد الإلكتروني :  {{ vendor.email }}

+

الهاتف :  {{ vendor.phone }}

+

العنوان :  {{ vendor.address_1 }}

@@ -170,13 +214,24 @@
-

المبلغ الإجمالي: {{ po_total_amount|floatformat:'2g' }}

+

المبلغ الإجمالي :  {{ po_total_amount|currency_format }}

+
+
+
+ + - - \ No newline at end of file diff --git a/templates/purchase_orders/po_detail_en_pdf.html b/templates/purchase_orders/po_detail_en_pdf.html index f777cca5..6e6014a9 100644 --- a/templates/purchase_orders/po_detail_en_pdf.html +++ b/templates/purchase_orders/po_detail_en_pdf.html @@ -103,22 +103,49 @@ text-align: right; } - /* Footer Styles */ + /* Footer Styles */ .document-footer { + position: relative; + bottom: 0; + left: 0; + width: 100%; text-align: center; font-size: 10px; color: #888; - border-top: 1px solid #ddd; + padding-top: 15px; - margin-top: 30px; + margin: 0 20mm; + + } + .footer-flex { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + } + .footer-logo { + text-align: center; + } + .footer-logo img { + height: 20px; + width: 20px; + } + .footer-logo p { + font-size: 9px; + font-weight: bold; + } + .footer-powered p { + font-size: 11px; + } + .footer-powered span { + font-weight: lighter; + } + .footer-powered a { + color: #112e40; + text-decoration: none; + font-size: 9px; } - .footer-flex { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - } @@ -126,12 +153,25 @@

{{ dealer.name }}

- Address: {{ dealer.address}}
- Email: {{ dealer.user.email }}
- Phone: {{dealer.phone_number }}
- CRN: {{dealer.crn}}  | VRN: {{dealer.vrn}} + Address : {{ dealer.address}}
+ Email : {{ dealer.user.email }}
+ Phone : {{dealer.phone_number }}
+ CRN : {{dealer.crn}}  | VRN : {{dealer.vrn}}
+ +
+ +
+

PURCHASE ORDER

{{ po_model.po_number }}

@@ -141,15 +181,15 @@

BILL TO:

-

Vendor: {{vendor.vendor_name}}

-

Email: {{vendor.email}}

-

Phone: {{vendor.phone}}

-

Address: {{vendor.address_1}}

+

Vendor : {{vendor.vendor_name}}

+

Email : {{vendor.email}}

+

Phone : {{vendor.phone}}

+

Address : {{vendor.address_1}}

DETAILS:

-

PO Number: {{ po_model.po_number }}

-

Issue Date: {{ po_model.date_fulfilled|date:"F j, Y" }}

+

PO Number :  {{ po_model.po_number }}

+

Issue Date :  {{ po_model.date_fulfilled|date:"F j, Y" }}

@@ -161,14 +201,25 @@
-

Total Amount: {{ po_total_amount|floatformat:'2g' }}

+

Total Amount :  {{ po_total_amount|currency_format }}

+
-
{{ invoice.amount_owned }} diff --git a/templates/sales/invoices/invoice_list.html b/templates/sales/invoices/invoice_list.html index 87f2cf56..8f674b6f 100644 --- a/templates/sales/invoices/invoice_list.html +++ b/templates/sales/invoices/invoice_list.html @@ -14,9 +14,9 @@
-
+ {% comment %}
{% include 'partials/search_box.html' %}
-
+
{% endcomment %}
diff --git a/templates/sales/invoices/invoice_preview.html b/templates/sales/invoices/invoice_preview.html index a2663f78..fb6a71f0 100644 --- a/templates/sales/invoices/invoice_preview.html +++ b/templates/sales/invoices/invoice_preview.html @@ -1,367 +1,333 @@ {% load i18n static custom_filters num2words_tags %} - - - - - {% trans "Invoice" %} - - - - - - - -
-
- + /* Footer Styles */ + .document-footer { + text-align: center; + font-size: 10px; + color: #888; + border-top: 1px solid #ddd; + padding-top: 15px; + margin-top: 30px; + } + .footer-logo img { + height: 20px; + width: 20px; + } + .footer-logo p { + font-size: 9px; + font-weight: bold; + margin: 5px 0 0; + } + .footer-powered p { + font-size: 11px; + margin: 0; + } + .footer-powered span { + font-weight: lighter; + } + .footer-powered a { + color: #112e40; + text-decoration: none; + font-size: 9px; + } + + + +
+
+
+

{{ dealer_info.name }}

+
+ العنوان : {{ dealer_info.address }}
+ البريد الإلكتروني : {{ dealer_info.user.email }}
+ الهاتف : {{ dealer_info.phone_number }}
+ رقم السجل التجاري : {{dealer_info.crn }}  | الرقم الضريبي : {{ dealer_info.vrn }} +
-
- +
+

عرض سعر

+

{{ estimate.estimate_number }}

-
-
-
-
-
- Invoice / فاتورة -
-
-
-
-
-
- QR Code -
-
- -
-
- - - - - - - - - - - - - - - - - - - - -
- Dealership Name - {{ request.dealer.name }} - اسم الوكالة -
- Dealership Address - {{ request.dealer.address }} - عنوان الوكالة -
- Phone - {{ request.dealer.phone_number }} - جوال -
- VAT Number - {{ request.dealer.vrn }} - الرقم الضريبي -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
- Invoice Number - {{ invoice.invoice_number }} - رقم الفاتورة -
- Date - {{ invoice.date_approved| date:"Y/m/d" }} - التاريخ -
- Customer Name - {{ invoice.customer.customer_name }} - اسم العميل -
- Email - {{ invoice.customer.email |default:"N/A" }} - البريد الإلكتروني -
- Terms - {{ invoice.get_terms_display }} - شروط الدفع -
-
-
- Car Details - تفاصيل السيارة -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Make / الصانع - - Model / الموديل - - Series / السلسلة - - Trim / الفئة - - Year / السنة - - VIN / رقم الهيكل - - Quantity / الكمية - - Unit Price / سعر الوحدة - - Discount / الخصم - - VAT / الضريبة - - Total / الإجمالي -
{{ data.car.id_car_make.name }}{{ data.car.id_car_model.name }}{{ data.car.id_car_serie.name }}{{ data.car.id_car_trim.name }}{{ data.car.year }}{{ data.car.vin }}1{{ data.car.marked_price |floatformat:2 }}{{ data.discount_amount |floatformat:2 }}{{ data.vat_amount|floatformat:2 }}{{ data.final_price|floatformat:2 }}
-
-
- Additional Services - الخدمات الإضافية -
- {% if data.additional_services %} -
- - - - - - - - - - - {% for service in data.additional_services.services %} - - - - - - - {% endfor %} - -
Type / النوعPrice / السعرService VAT / ضريبة الخدمة - Total / الإجمالي -
{{ service.0.name }}{{ service.0.price|floatformat }}{{ service.1|floatformat }}{{ service.0.price_|floatformat }}
-
- {% endif %} -
-
- - - - - - - - - - - - - - -
- Total VAT - - {{ data.total_vat|floatformat }} - - إجمالي ضريبة القيمة المضافة -
- Grand Total - - {{ data.grand_total|floatformat }}  - - الإجمالي الكلي -
- كتابةً: {{ data.grand_total|num_to_words }}  -
-
-
+ +
+
+

{{ estimate.customer.customer_name }}: إلى

+

العميل : {{ customer_obj.full_name }}

+

البريد الإلكتروني : {{ customer_obj.email |default:"N/A"}}

+

الهاتف : {{ customer_obj.phone_number|default:"N/A" }}

+

العنوان : {{ customer_obj.address|default:"N/A" }}

- + + \ No newline at end of file diff --git a/templates/sales/invoices/invoice_preview_ar.html b/templates/sales/invoices/invoice_preview_ar.html new file mode 100644 index 00000000..de8d5299 --- /dev/null +++ b/templates/sales/invoices/invoice_preview_ar.html @@ -0,0 +1,359 @@ +{% load static custom_filters num2words_tags %} + + + + + + فاتورة + + + + + +
+
+

{{ dealer_info.name }}

+
+ العنوان : {{ dealer_info.address }}
+ البريد الإلكتروني : {{ dealer_info.user.email }}
+ الهاتف : {{ dealer_info.phone_number }}
+ رقم السجل التجاري : {{dealer_info.crn }}
+ الرقم الضريبي : {{ dealer_info.vrn }} +
+
+ +
+ +
+ +
+

فاتورة

+

{{ invoice.invoice_number }}

+
+
+ +
+
+

{{ customer_obj.full_name }}: إلى

+

العميل : {{ customer_obj.full_name }}

+

البريد الإلكتروني : {{ customer_obj.email |default:"N/A"}}

+

الهاتف : {{ customer_obj.phone_number|default:"N/A" }}

+

العنوان : {{ customer_obj.address|default:"N/A" }}

+
+
+

التفاصيل:

+

رقم عرض السعر :  {{ invoice.invoice_number }}

+

تاريخ الإصدار :  {{ invoice.date_approved|date:"Y/m/d" }}

+

طريقة الدفع :  {{ invoice.get_terms_display }}

+
+
+ +
+
+ تفاصيل السيارة +
+ + + + + + + + + + + + + + + + + + + + + +
+ الصانع + + الموديل + + السلسلة + + الفئة + + السنة + + رقم الهيكل +
{{ data.car.id_car_make.name }}{{ data.car.id_car_model.name }}{{ data.car.id_car_serie.name }}{{ data.car.id_car_trim.name }}{{ data.car.year }}{{ data.car.vin }}
+
+ +
+
+ التفاصيل المالية +
+ + + + + + + + + + + + + + + + + + + +
+ الكمية + + سعر الوحدة + + الخصم + + الضريبة + + الإجمالي +
1{{ data.car.marked_price|currency_format }}{{ data.discount_amount|currency_format}}{{ data.vat_amount|currency_format}}{{ data.final_price|currency_format }}
+
+ + {% if data.additional_services %} +
+
+ الخدمات الإضافية +
+ + + + + + + + + + + {% for service in data.additional_services.services %} + + + + + + + {% endfor %} + + + + + + + + + + +
النوعالقيمةضريبة الخدمةالإجمالي
{{ service.0.name }}{{ service.0.price|currency_format}}{{ service.1|currency_format }}{{ service.0.price_|currency_format}}
{{ data.total_services_amount|currency_format}}{{ data.total_services_vat|currency_format}}{{ data.total_services_amount_|currency_format }}
+
+ {% endif %} + +
+
+

إجمالي ضريبة القيمة المضافة :  {{ data.total_vat|currency_format }}

+

الإجمالي الكلي :  {{ data.grand_total|currency_format}}

+

كتابةً :  {{ data.grand_total|num_to_words }}

+
+
+
+ + + + \ No newline at end of file diff --git a/templates/sales/invoices/invoice_preview_en.html b/templates/sales/invoices/invoice_preview_en.html new file mode 100644 index 00000000..80f4cc5d --- /dev/null +++ b/templates/sales/invoices/invoice_preview_en.html @@ -0,0 +1,357 @@ +{% load static custom_filters num2words_tags %} + + + + + Invoice + + + + + +
+
+

{{ dealer_info.name }}

+
+ Address : {{ dealer_info.address }}
+ Email : {{ dealer_info.user.email }}
+ Phone : {{ dealer_info.phone_number }}
+ Commercial Registration No. : {{dealer_info.crn }}
+ VAT No. : {{ dealer_info.vrn }} +
+
+ +
+ +
+ +
+

Invoice

+

{{ invoice.invoice_number }}

+
+
+ +
+
+

To: {{ customer_obj.full_name }}

+

Customer : {{ customer_obj.full_name }}

+

Email : {{ customer_obj.email |default:"N/A"}}

+

Phone : {{ customer_obj.phone_number|default:"N/A" }}

+

Address : {{ customer_obj.address|default:"N/A" }}

+
+
+

Details:

+

Quotation Number :  {{ invoice.invoice_number }}

+

Issue Date :  {{ invoice.date_approved|date:"Y/m/d" }}

+

Payment Method :  {{ invoiceget_terms_display }}

+
+
+ +
+
+ Car Details +
+ + + + + + + + + + + + + + + + + + + + + +
+ Make + + Model + + Series + + Trim + + Year + + VIN +
{{ data.car.id_car_make.name }}{{ data.car.id_car_model.name }}{{ data.car.id_car_serie.name }}{{ data.car.id_car_trim.name }}{{ data.car.year }}{{ data.car.vin }}
+
+ +
+
+ Financial Details +
+ + + + + + + + + + + + + + + + + + + +
+ Quantity + + Unit Price + + Discount + + VAT + + Total +
1{{ data.car.marked_price|currency_format}}{{ data.discount_amount|currency_format }}{{ data.vat_amount|currency_format}}{{ data.final_price|currency_format }}
+
+ + {% if data.additional_services %} +
+
+ Additional Services +
+ + + + + + + + + + + {% for service in data.additional_services.services %} + + + + + + + {% endfor %} + + + + + + + + + + +
TypeValueService VATTotal
{{ service.0.name }}{{ service.0.price|currency_format }}{{ service.1|currency_format}}{{ service.0.price_|currency_format}}
{{ data.total_services_amount|currency_format}}{{ data.total_services_vat|currency_format}}{{ data.total_services_amount_|currency_format }}
+
+ {% endif %} + +
+
+

Total VAT :  {{ data.total_vat|currency_format}}

+

Grand Total :  {{ data.grand_total|currency_format}}

+

In words :  {{ data.grand_total|num_to_words }}

+
+
+
+ + + + \ No newline at end of file diff --git a/templates/sales/sales_list.html b/templates/sales/sales_list.html index 6fc7681a..7524945a 100644 --- a/templates/sales/sales_list.html +++ b/templates/sales/sales_list.html @@ -15,9 +15,9 @@
-
+ {% comment %}
{% include 'partials/search_box.html' %}
-
+
{% endcomment %}
diff --git a/templates/vendors/vendors_list.html b/templates/vendors/vendors_list.html index c0030da3..262d52d1 100644 --- a/templates/vendors/vendors_list.html +++ b/templates/vendors/vendors_list.html @@ -101,7 +101,7 @@ {{ vendor.arabic_name }}
-

{{ vendor.name }}

+

{{ vendor.name|title }}

From c39f2eb068091834a4b3aa034af22c39f226eb99 Mon Sep 17 00:00:00 2001 From: ismail Date: Thu, 18 Sep 2025 14:33:22 +0300 Subject: [PATCH 03/22] update 1 --- .gitignore | 5 +++- car_inventory/asgi.py | 16 +------------ car_inventory/urls.py | 6 +++-- inventory/signals.py | 8 +++---- inventory/views.py | 54 +++++++++++++++++++++++++------------------ 5 files changed, 45 insertions(+), 44 deletions(-) diff --git a/.gitignore b/.gitignore index a70c6b99..558db3ef 100644 --- a/.gitignore +++ b/.gitignore @@ -163,8 +163,11 @@ GitHub.sublime-settings .history +static-copy static +static/* staticfiles media tmp -logs \ No newline at end of file +logs +static/testdir \ No newline at end of file diff --git a/car_inventory/asgi.py b/car_inventory/asgi.py index 1a8723dc..f92da1b1 100644 --- a/car_inventory/asgi.py +++ b/car_inventory/asgi.py @@ -17,31 +17,17 @@ import django django.setup() - from django.urls import path from channels.routing import ProtocolTypeRouter, URLRouter -from whitenoise import WhiteNoise from channels.auth import AuthMiddlewareStack -from api import routing from inventory.notifications.sse import NotificationSSEApp from django.urls import re_path from django.core.asgi import get_asgi_application -from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler -from pathlib import Path -# application = ProtocolTypeRouter( -# { -# "http": get_asgi_application(), -# # "websocket": AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)), -# } -# ) -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings") -# django.setup() + # BASE_DIR = Path(__file__).resolve().parent.parent app = get_asgi_application() -# app = WhiteNoise(app, root=str(BASE_DIR / 'staticfiles')) - application = ProtocolTypeRouter( { "http": AuthMiddlewareStack( diff --git a/car_inventory/urls.py b/car_inventory/urls.py index 118d92a3..c2b4849b 100644 --- a/car_inventory/urls.py +++ b/car_inventory/urls.py @@ -33,5 +33,7 @@ urlpatterns += i18n_patterns( # path('', include(tf_urls)), ) -# if not settings.DEBUG: -urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += static(settings.STATIC_URL, document_root = settings.STATIC_ROOT) + diff --git a/inventory/signals.py b/inventory/signals.py index 7d38be84..e19327cf 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -1003,10 +1003,10 @@ def create_po_item_upload(sender, instance, created, **kwargs): if instance.po_status == "fulfilled" or instance.po_status == 'approved': for item in instance.get_itemtxs_data()[0]: dealer = models.Dealer.objects.get(entity=instance.entity) - if item.bill_model.is_paid(): - models.PoItemsUploaded.objects.get_or_create( - dealer=dealer, po=instance, item=item, status=instance.po_status - ) + if item.bill_model and item.bill_model.is_paid(): + models.PoItemsUploaded.objects.get_or_create( + dealer=dealer, po=instance, item=item, status=instance.po_status + ) # @receiver(post_save, sender=models.Staff) diff --git a/inventory/views.py b/inventory/views.py index 01b08977..aecbc742 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -10879,19 +10879,23 @@ def InventoryItemCreateView(request, dealer_slug): serie = request.POST.get("serie") trim = request.POST.get("trim") year = request.POST.get("year") - exterior = models.ExteriorColors.objects.get( - pk=request.POST.get("exterior") - ) - interior = models.InteriorColors.objects.get( - pk=request.POST.get("interior") - ) + exterior = request.POST.get("exterior") + interior = request.POST.get("interior") make_name = models.CarMake.objects.get(pk=make) model_name = models.CarModel.objects.get(pk=model) serie_name = models.CarSerie.objects.get(pk=serie) trim_name = models.CarTrim.objects.get(pk=trim) + exterior_name = models.ExteriorColors.objects.get( + pk=request.POST.get("exterior") + ) + interior_name = models.InteriorColors.objects.get( + pk=request.POST.get("interior") + ) + + inventory_name = f"{make_name.name} || {model_name.name} || {serie_name.name} || {trim_name.name} || {year} || {exterior_name.name} || {interior_name.name}" + display_name = f"{make_name.name} {model_name.name} {serie_name.name} {trim_name.name} {year} {exterior_name.name}" - inventory_name = f"{make_name.name} || {model_name.name} || {serie_name.name} || {trim_name.name} || {year} || {exterior.name} || {interior.name}" if ( inventory := entity.get_items_inventory() .filter(name=inventory_name) @@ -10899,17 +10903,27 @@ def InventoryItemCreateView(request, dealer_slug): ): messages.error(request, _("Inventory item already exists")) return response - uom = entity.get_uom_all().filter(name="Unit").first() if not uom: uom = entity.create_uom(name="Unit", unit_abbr="unit") - entity.create_item_inventory( - name=inventory_name, + item = entity.create_item_inventory( + name=display_name, uom_model=uom, item_type=ItemModel.ITEM_TYPE_MATERIAL, inventory_account=account, coa_model=coa, ) + item.additional_info.update( + { + "make": make, + "model": model, + "serie": serie, + "trim": trim, + "year": year, + "exterior": exterior, + "interior": interior, + }) + item.save() messages.success(request, _("Inventory item created successfully")) return response @@ -11224,18 +11238,14 @@ def upload_cars(request, dealer_slug, pk=None): ) try: if item: - data = [x.strip() for x in item.item_model.name.split("||")] - make = models.CarMake.objects.filter(is_sa_import=True).get( - name=data[0] - ) - model = make.carmodel_set.get(name=data[1]) - trim = models.CarTrim.objects.filter( - name=data[3], id_car_serie__id_car_model=model.id_car_model - ).first() - serie = trim.id_car_serie - year = data[4] - exterior = models.ExteriorColors.objects.get(name=data[5]) - interior = models.InteriorColors.objects.get(name=data[6]) + # data = [x.strip() for x in item.item_model.name.split("||")] + make = models.CarMake.objects.get(pk=item.addition_info.get("make")) + model = models.CarModel.objects.get(pk=item.addition_info.get("model")) + trim = models.CarTrim.objects.get(pk=item.addition_info.get("trim")) + serie = models.CarSerie.objects.get(pk=item.addition_info.get("serie")) + year = item.addition_info.get("year") + exterior = models.ExteriorColors.objects.get(pk=item.addition_info.get("exterior")) + interior = models.InteriorColors.objects.get(pk=item.addition_info.get("interior")) receiving_date = timezone.now() vendor_model = item.bill_model.vendor vendor = models.Vendor.objects.get(vendor_model=vendor_model) From 7c794a6390b15aa957c2d5dc00a86ac75e1cfece Mon Sep 17 00:00:00 2001 From: ismail Date: Thu, 18 Sep 2025 14:40:27 +0300 Subject: [PATCH 04/22] update the tickets --- templates/support/ticket_detail.html | 2 +- templates/support/ticket_list.html | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/templates/support/ticket_detail.html b/templates/support/ticket_detail.html index 984d6229..006ad2f1 100644 --- a/templates/support/ticket_detail.html +++ b/templates/support/ticket_detail.html @@ -48,7 +48,7 @@
{% trans 'Description' %}
{{ ticket.description|linebreaks }}
- {% if ticket.resolution_notes %} + {% if ticket.resolution_notes %}
{% trans 'Resolution Notes' %}
{{ ticket.resolution_notes|linebreaks }}
diff --git a/templates/support/ticket_list.html b/templates/support/ticket_list.html index dd786c27..c7b58975 100644 --- a/templates/support/ticket_list.html +++ b/templates/support/ticket_list.html @@ -87,7 +87,7 @@ {% trans "View" %} {% endcomment %} - +
- + {% empty %}
From 07bd2a34fbe3270f91e5cd25ba2ca079e9af7ab2 Mon Sep 17 00:00:00 2001 From: Faheed Date: Thu, 18 Sep 2025 16:11:21 +0300 Subject: [PATCH 05/22] alignment in the pdf print and alingment of buttons in bill_card --- inventory/signals.py | 16 +++++++++++++--- templates/bill/includes/card_bill.html | 13 +++++++++++-- .../purchase_orders/po_detail_ar_pdf.html | 11 +++-------- .../purchase_orders/po_detail_en_pdf.html | 19 ++++++++----------- .../sales/estimates/estimate_preview_ar.html | 2 +- .../sales/estimates/estimate_preview_en.html | 11 +++++++---- .../sales/invoices/invoice_preview_ar.html | 2 +- .../sales/invoices/invoice_preview_en.html | 12 ++++++++---- 8 files changed, 52 insertions(+), 34 deletions(-) diff --git a/inventory/signals.py b/inventory/signals.py index e19327cf..f3af7710 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -1004,9 +1004,19 @@ def create_po_item_upload(sender, instance, created, **kwargs): for item in instance.get_itemtxs_data()[0]: dealer = models.Dealer.objects.get(entity=instance.entity) if item.bill_model and item.bill_model.is_paid(): - models.PoItemsUploaded.objects.get_or_create( - dealer=dealer, po=instance, item=item, status=instance.po_status - ) + models.PoItemsUploaded.objects.update_or_create( + dealer=dealer, po=instance, item=item, + defaults={ + "status":instance.po_status + } + ) + + # po_item = models.PoItemsUploaded.objects.get_or_create( + # dealer=dealer, po=instance, item=item, + # defaults={ + # "status":instance.po_status + # } + # ) # @receiver(post_save, sender=models.Staff) diff --git a/templates/bill/includes/card_bill.html b/templates/bill/includes/card_bill.html index 4873cba1..a523990b 100644 --- a/templates/bill/includes/card_bill.html +++ b/templates/bill/includes/card_bill.html @@ -50,7 +50,7 @@ {% modal_action bill 'get' entity_slug %} -
+
{% trans 'View' %} {% if perms.django_ledger.change_billmodel %} @@ -199,7 +199,7 @@ {% endif %}
diff --git a/templates/header.html b/templates/header.html index 5a39dc54..2698966d 100644 --- a/templates/header.html +++ b/templates/header.html @@ -194,7 +194,7 @@ data-bs-parent="#navbarVerticalCollapse" id="nv-sales">
  • {% trans 'sales'|capfirst %}
  • - {% if perms.django_ledger.add_estimatemodel %} + {% comment %} {% if perms.django_ledger.add_estimatemodel %} - {% endif %} + {% endif %} {% endcomment %} {% if perms.django_ledger.view_estimatemodel %} + {% for bill_item in itemtxs_qs %} From 50589389bbec9d7e3a3ec5c6b3a378e4578d48fa Mon Sep 17 00:00:00 2001 From: Faheed Date: Sun, 21 Sep 2025 13:18:21 +0300 Subject: [PATCH 13/22] delete po fixed --- inventory/views.py | 3 +- .../plans/billing_info_create_or_update.html | 3 +- .../purchase_orders/includes/card_po.html | 24 +++++++++--- .../purchase_orders/po_confirm_delete.html | 31 ++++++---------- templates/purchase_orders/po_delete.html | 37 ------------------- templates/sales/invoices/invoice_detail.html | 4 +- 6 files changed, 36 insertions(+), 66 deletions(-) delete mode 100644 templates/purchase_orders/po_delete.html diff --git a/inventory/views.py b/inventory/views.py index c74e7fca..8c9cc63a 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -11152,7 +11152,6 @@ class BasePurchaseOrderActionActionView(BasePurchaseOrderActionActionViewBase): class PurchaseOrderModelDeleteView(PurchaseOrderModelDeleteViewBase): - template_name = "purchase_orders/po_delete.html" permission_required = "django_ledger.delete_purchaseordermodel" def get_success_url(self): @@ -11162,7 +11161,7 @@ class PurchaseOrderModelDeleteView(PurchaseOrderModelDeleteViewBase): level=messages.SUCCESS, ) return reverse( - "purchase_order_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]} + "purchase_order_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"],"entity_slug":self.kwargs['entity_slug']} ) diff --git a/templates/plans/billing_info_create_or_update.html b/templates/plans/billing_info_create_or_update.html index a63bd0c1..34f5da3f 100644 --- a/templates/plans/billing_info_create_or_update.html +++ b/templates/plans/billing_info_create_or_update.html @@ -19,6 +19,7 @@ method="post" class="needs-validation" novalidate> {% csrf_token %} {{ form|crispy }} +
    {{ _("Cancel") }} + type="submit">{{ _("Go Back") }}
    diff --git a/templates/purchase_orders/includes/card_po.html b/templates/purchase_orders/includes/card_po.html index d5f057c4..af856b04 100644 --- a/templates/purchase_orders/includes/card_po.html +++ b/templates/purchase_orders/includes/card_po.html @@ -45,7 +45,7 @@ href="{% url 'purchase_order_list' request.dealer.slug request.dealer.entity.slug %}" title="Click to view the complete list of Purchase Orders" role="button"> - {% trans 'PO List' %} + {% trans 'Purchase Order List' %}

    @@ -135,15 +135,29 @@ {% endif %} {% if po_model.can_delete %} {% if perms.django_ledger.delete_purchaseordermodel %} - + + {% endif %} {% endif %} {% if po_model.can_void %} {% endif %} diff --git a/templates/purchase_orders/po_confirm_delete.html b/templates/purchase_orders/po_confirm_delete.html index e0e90738..7a4f0789 100644 --- a/templates/purchase_orders/po_confirm_delete.html +++ b/templates/purchase_orders/po_confirm_delete.html @@ -1,19 +1,12 @@ - -{% extends "base.html" %} -{%load i18n %} -{% block title %}{% trans "Confirm Delete"%} - {{ block.super }}{% endblock %} -{% block content %} -
    -

    {% trans "Confirm Deletion" %}

    -

    - {% trans "Are you sure you want to delete the Purchase Order" %} "{{ object.po_number }}"? -

    -
    - {% csrf_token %} - - {% trans "Cancel" %} - -
    -{% endblock %} - +{% load i18n %} + + {% csrf_token %} +

    + {% blocktrans with po_number=po_model.po_number %} + Are you sure you want to delete #{{ po_number }}? + {% endblocktrans %} +

    + + {% trans "No" %} + \ No newline at end of file diff --git a/templates/purchase_orders/po_delete.html b/templates/purchase_orders/po_delete.html deleted file mode 100644 index 28f22968..00000000 --- a/templates/purchase_orders/po_delete.html +++ /dev/null @@ -1,37 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} -{% load static %} -{% load django_ledger %} -{% block title %} - {% trans "Delete Purchase Order" %} -{% endblock %} -{% block content %} -
    -
    -
    - {% csrf_token %} -
    -
    -

    - {% blocktrans %}Are you sure you want to delete - Purchase Order {{ po_model.po_number }}?{% endblocktrans %} -

    -

    - {% trans "All transactions associated with this Purchase Order will be deleted.If you want to void the PO instead," %} - {% trans "click here" %} -

    -
    - {% trans 'Go Back' %} - -
    -
    -
    - -
    -
    -{% endblock %} - - diff --git a/templates/sales/invoices/invoice_detail.html b/templates/sales/invoices/invoice_detail.html index 9c9072ed..16637121 100644 --- a/templates/sales/invoices/invoice_detail.html +++ b/templates/sales/invoices/invoice_detail.html @@ -142,8 +142,8 @@ {% endif %} {% endif %} {% endif %} - {% trans 'Preview' %} + {% trans 'Preview' %} From 7bf7e51c09f3c96679aada27a9b2ce55b471f58d Mon Sep 17 00:00:00 2001 From: ismail Date: Wed, 24 Sep 2025 09:49:07 +0300 Subject: [PATCH 14/22] bug fixes and updates --- inventory/override.py | 16 ++++++++++++++++ inventory/views.py | 6 +++--- templates/components/note_modal.html | 1 + templates/components/schedule_modal.html | 2 +- templates/crm/leads/lead_detail.html | 6 ++++-- templates/inventory/car_detail.html | 2 +- templates/ledger/bills/bill_list.html | 2 ++ templates/purchase_orders/includes/card_po.html | 6 ++++-- .../includes/po_item_formset.html | 4 ++-- 9 files changed, 34 insertions(+), 11 deletions(-) diff --git a/inventory/override.py b/inventory/override.py index f95ca4b3..7587fa2a 100644 --- a/inventory/override.py +++ b/inventory/override.py @@ -411,6 +411,22 @@ class BasePurchaseOrderActionActionView( f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. " f"Error: {e}" ) + except Exception as e: + print( + f"User {user_username} encountered an exception " + f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. " + f"Error: {e}" + ) + logger.warning( + f"User {user_username} encountered an exception " + f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. " + f"Error: {e}" + ) + messages.add_message( + request, + message=f"Failed to update PO {po_model.po_number}. {e}", + level=messages.ERROR, + ) return response diff --git a/inventory/views.py b/inventory/views.py index 8c9cc63a..220bb70a 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -5005,7 +5005,7 @@ def create_estimate(request, dealer_slug, slug=None): customer = opportunity.customer print(customer) form.fields["customer"].queryset = models.Customer.objects.filter( - pk=customer.pk + pk=customer.pk ) form.initial["customer"] = customer @@ -11161,9 +11161,9 @@ class PurchaseOrderModelDeleteView(PurchaseOrderModelDeleteViewBase): level=messages.SUCCESS, ) return reverse( - "purchase_order_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"],"entity_slug":self.kwargs['entity_slug']} + "purchase_order_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"], "entity_slug": self.kwargs["entity_slug"]} ) - + class PurchaseOrderMarkAsDraftView(BasePurchaseOrderActionActionView): diff --git a/templates/components/note_modal.html b/templates/components/note_modal.html index f27afd9c..7934052c 100644 --- a/templates/components/note_modal.html +++ b/templates/components/note_modal.html @@ -22,6 +22,7 @@ hx-target="#notesTable" hx-on::after-request="{ resetSubmitButton(document.querySelector('.add_note_form button[type=submit]')); $('#noteModal').modal('hide'); }" hx-swap="outerHTML show:window.top" + hx-select-oob="#timeline" method="post" class="add_note_form"> {% csrf_token %} diff --git a/templates/components/schedule_modal.html b/templates/components/schedule_modal.html index de29e3f0..f154aa5c 100644 --- a/templates/components/schedule_modal.html +++ b/templates/components/schedule_modal.html @@ -22,7 +22,7 @@ hx-target=".taskTable" hx-on::after-request="{ resetSubmitButton(document.querySelector('.add_schedule_form button[type=submit]')); $('#scheduleModal').modal('hide'); }" hx-swap="outerHTML" - hx-select-oob="#toast-container:outerHTML" + hx-select-oob="#toast-container:outerHTML,#timeline:outerHTML" method="post" class="add_schedule_form"> {% csrf_token %} diff --git a/templates/crm/leads/lead_detail.html b/templates/crm/leads/lead_detail.html index b29ab68c..a51c1c63 100644 --- a/templates/crm/leads/lead_detail.html +++ b/templates/crm/leads/lead_detail.html @@ -335,7 +335,7 @@
    -
    +
    {% for activity in activities %}
    @@ -353,7 +353,9 @@ {% elif activity.activity_type == "visit" %} {% elif activity.activity_type == "whatsapp" %} - + + {% elif activity.activity_type == "meeting" %} + {% endif %}
    {% if forloop.last %} diff --git a/templates/inventory/car_detail.html b/templates/inventory/car_detail.html index a0c300fd..81f72f57 100644 --- a/templates/inventory/car_detail.html +++ b/templates/inventory/car_detail.html @@ -542,7 +542,7 @@ data-bs-dismiss="modal" aria-label="Close">
    -
    +
    {% csrf_token %} {{estimate_form|crispy}} diff --git a/templates/ledger/bills/bill_list.html b/templates/ledger/bills/bill_list.html index 446ca5f1..9bc79a8c 100644 --- a/templates/ledger/bills/bill_list.html +++ b/templates/ledger/bills/bill_list.html @@ -49,6 +49,8 @@ {% elif bill.is_canceled %} + {% elif bill.is_void %} + {% endif %} {{ bill.bill_status }} diff --git a/templates/purchase_orders/includes/card_po.html b/templates/purchase_orders/includes/card_po.html index af856b04..83d53da4 100644 --- a/templates/purchase_orders/includes/card_po.html +++ b/templates/purchase_orders/includes/card_po.html @@ -155,12 +155,14 @@
    {% endif %} {% endif %} - {% if po_model.can_void %} + {% comment %} TODO: upgrade djnago ledger or replace core functionality {% endcomment %} + {% comment %} issue with django ledger base functionality when marking as void throughs error , will be fix in future {% endcomment %} + {% comment %} {% if po_model.can_void %} - {% endif %} + {% endif %} {% endcomment %} {% if po_model.can_cancel %} {% endif %} -
    + @@ -52,8 +52,8 @@ {{ f.item_model|add_class:"form-control" }} {% if f.errors %}
    {{ f.errors }}
    {% endif %} -
    + - + {% for bill_item in itemtxs_qs %} @@ -162,4 +162,4 @@
    {% include "bill/includes/mark_as.html" %} - {% endblock %} +{% endblock %} diff --git a/templates/bill/includes/card_bill.html b/templates/bill/includes/card_bill.html index a523990b..4c4bc747 100644 --- a/templates/bill/includes/card_bill.html +++ b/templates/bill/includes/card_bill.html @@ -55,8 +55,8 @@ class="btn btn-sm btn-phoenix-primary me-md-2">{% trans 'View' %} {% if perms.django_ledger.change_billmodel %} {% trans 'Update' %} + href="{% url 'django_ledger:bill-update' entity_slug=entity_slug bill_pk=bill.uuid %}" + class="btn btn-sm btn-phoenix-warning me-md-2">{% trans 'Update' %} {% if bill.can_pay %} @@ -203,8 +203,8 @@ {% if perms.django_ledger.change_billmodel %} {% if "update" not in request.path %} - + diff --git a/templates/bill/tags/bill_item_formset.html b/templates/bill/tags/bill_item_formset.html index b834cfb1..ee402054 100644 --- a/templates/bill/tags/bill_item_formset.html +++ b/templates/bill/tags/bill_item_formset.html @@ -2,119 +2,119 @@ {% load static %} {% load django_ledger %} {% load widget_tweaks %} - -
    + +
    -
    -
    -

    - - {% trans 'Bill Items' %} -

    -
    -
    +
    +
    +

    + + {% trans 'Bill Items' %} +

    +
    +
    -
    -
    - {% csrf_token %} - {{ item_formset.non_form_errors }} - {{ item_formset.management_form }} +
    +
    + {% csrf_token %} + {{ item_formset.non_form_errors }} + {{ item_formset.management_form }} -
    -
    +
    +
    -
    -
    {% trans 'PO' %}
    {% trans 'Unit Cost' %} {% trans 'Quantity' %}{% trans 'Unit Cost' %} {% trans 'Unit' %} {% trans 'Amount' %} {% trans 'Status' %}{{ f.po_unit_cost|add_class:"form-control" }} {{ f.po_quantity|add_class:"form-control" }}{{ f.po_unit_cost|add_class:"form-control" }} {{ f.entity_unit|add_class:"form-control" }} {{ CURRENCY }}{{ f.instance.po_total_amount | currency_format }} From 2727588f580bc094534271cfcf061eed6647a091 Mon Sep 17 00:00:00 2001 From: ismail Date: Wed, 24 Sep 2025 11:07:31 +0300 Subject: [PATCH 15/22] format and lint --- car_inventory/asgi.py | 6 +- car_inventory/urls.py | 3 +- inventory/admin.py | 66 +- inventory/forms.py | 30 +- inventory/hooks.py | 18 +- .../deactivate_unsubscribed_dealers.py | 10 +- .../management/commands/plans_maintenance.py | 6 +- .../commands/set_custom_permissions.py | 1 - inventory/management/commands/tenhal_plan.py | 69 +- inventory/management/commands/update_site.py | 5 +- inventory/models.py | 131 +- inventory/override.py | 1 + inventory/signals.py | 61 +- inventory/tasks.py | 27 +- inventory/templatetags/custom_filters.py | 14 +- inventory/urls.py | 186 +- inventory/utils.py | 134 +- inventory/validators.py | 7 +- inventory/views.py | 1975 ++++++++++------- requirements_prod.txt | 25 +- templates/account/account_inactive.html | 10 +- .../confirm_email_verification_code.html | 78 +- templates/account/confirm_login_code..html | 50 +- templates/account/email.html | 2 +- templates/account/email_change.html | 116 +- templates/account/lock-screen.html | 828 +++---- templates/account/login.html | 28 +- templates/account/password_change.html | 2 +- templates/account/password_set.html | 32 +- templates/account/reauthenticate.html | 32 +- templates/account/request_login_code.html | 46 +- templates/account/signup-wizard.html | 12 +- templates/account/snippets/warn_no_email.html | 2 +- templates/account/success.html | 4 +- templates/account/user_settings.html | 2 +- .../confirm_activate_account.html | 2 +- templates/admin_management/management.html | 66 +- templates/administration/staff_index.html | 2 +- templates/appointment/set_password.html | 8 +- templates/bill/bill_detail.html | 4 +- templates/bill/includes/card_bill.html | 8 +- templates/bill/tags/bill_item_formset.html | 198 +- .../bill/transactions/tags/txs_table.html | 2 +- templates/chart_of_accounts/coa_create.html | 4 +- templates/chart_of_accounts/coa_list.html | 2 +- templates/chart_of_accounts/coa_update.html | 2 +- .../chart_of_accounts/includes/coa_card.html | 2 +- templates/chat_support.html | 94 +- templates/components/note_modal.html | 2 +- templates/components/task_modal.html | 2 +- templates/crm/employee_calendar.html | 2 +- templates/crm/leads/lead_detail.html | 26 +- templates/crm/leads/lead_list.html | 36 +- templates/crm/leads/lead_send.html | 8 +- templates/crm/note_form.html | 26 +- templates/crm/notifications_history.html | 46 +- .../crm/opportunities/opportunity_detail.html | 1338 +++++------ .../crm/opportunities/opportunity_form.html | 52 +- .../crm/opportunities/opportunity_list.html | 4 +- .../partials/opportunity_grid.html | 232 +- templates/csv_upload.html | 2 +- templates/customers/customer_list.html | 24 +- templates/customers/note_form.html | 6 +- .../dashboards/aging_inventory_list.html | 14 +- templates/dashboards/general_dashboard.html | 572 ++--- templates/dashboards/partials/chart.html | 14 +- .../partials/financial_data_cards.html | 10 +- templates/dashboards/sales_dashboard.html | 2 +- templates/dealers/activity_log.html | 4 +- templates/dealers/dealer_detail.html | 96 +- templates/dealers/dealer_form.html | 10 +- templates/email_sender/thank_you_email.html | 168 +- templates/errors/404.html | 784 +++---- templates/errors/500.html | 784 +++---- templates/footer.html | 32 +- templates/groups/group_detail.html | 4 +- templates/groups/group_list.html | 32 +- templates/haikalbot/chat.html | 76 +- templates/header.html | 120 +- templates/inventory/add_colors.html | 116 +- templates/inventory/car_detail.html | 152 +- templates/inventory/car_form.html | 2 +- templates/inventory/car_inventory.html | 6 +- templates/inventory/car_list.html | 2 +- templates/inventory/car_list_view.html | 520 ++--- templates/inventory/colors.html | 2 +- templates/inventory/list.html | 2 +- templates/inventory/transfer_preview.html | 258 +-- templates/items/expenses/expense_detail.html | 140 +- templates/items/expenses/expenses_list.html | 6 +- templates/items/service/service_detail.html | 84 +- .../bank_accounts/bank_account_list.html | 42 +- templates/ledger/bills/bill_list.html | 108 +- .../ledger/coa_accounts/account_detail.html | 8 +- .../ledger/coa_accounts/account_form.html | 2 +- .../ledger/coa_accounts/account_list.html | 344 +-- .../coa_accounts/partials/account_table.html | 2 +- .../journal_entry_transactions.html | 2 +- .../journal_entry/journal_entry_txs.html | 2 +- templates/ledger/ledger/ledger_detail.html | 2 +- templates/ledger/reports/car_sale_report.html | 122 +- .../reports/components/period_navigator.html | 2 +- templates/ledger/reports/dashboard.html | 8 +- templates/notifications-copy.html | 24 +- templates/payment_failed.html | 6 +- templates/payment_success.html | 8 +- .../plans/billing_info_create_or_update.html | 56 +- templates/plans/billing_info_delete.html | 2 +- templates/plans/extend.html | 36 +- templates/plans/order_detail.html | 152 +- templates/plans/payment_success.html | 8 +- templates/plans/plan_table.html | 28 +- templates/pricing_page.html | 84 +- .../purchase_orders/includes/card_po.html | 4 +- .../purchase_orders/po_confirm_delete.html | 10 +- templates/purchase_orders/po_detail.html | 14 +- .../purchase_orders/po_detail_ar_pdf.html | 404 ++-- .../purchase_orders/po_detail_backup.html | 2 +- .../purchase_orders/po_detail_en_pdf.html | 382 ++-- templates/purchase_orders/po_list.html | 16 +- templates/purchase_orders/po_update.html | 2 +- templates/purchase_orders/po_upload_cars.html | 4 +- .../purchase_orders/tags/po_item_table.html | 2 +- .../tags/po_item_table_print_ar.html | 6 +- .../tags/po_item_table_print_en.html | 10 +- templates/recalls/recall_filter.html | 32 +- templates/registration/signup.html | 10 +- .../sales/estimates/estimate_detail.html | 352 +-- .../sales/estimates/estimate_form-copy.html | 4 +- .../sales/estimates/estimate_preview_ar.html | 632 +++--- .../sales/estimates/estimate_preview_en.html | 638 +++--- templates/sales/invoices/invoice_detail.html | 104 +- templates/sales/invoices/invoice_preview.html | 590 ++--- .../sales/invoices/invoice_preview_ar.html | 636 +++--- .../sales/invoices/invoice_preview_en.html | 638 +++--- templates/sales/invoices/invoice_update.html | 4 +- templates/sales/orders/order_details.html | 6 +- templates/sales/payments/payment_details.html | 2 +- templates/sales/payments/payment_form.html | 16 +- templates/sales/payments/payment_form1.html | 16 +- templates/sales/saleorder_detail.html | 4 +- templates/sales/sales_list.html | 228 +- templates/shared/submit_button.html | 14 +- templates/support/create_ticket.html | 2 +- templates/support/ticket_detail.html | 2 +- templates/support/ticket_list.html | 2 +- templates/support/ticket_update.html | 2 +- templates/toast-alert.html | 4 +- templates/two_factor/core/otp_required.html | 2 +- templates/two_factor/profile/profile.html | 2 +- templates/users/user_password_reset.html | 2 +- templates/vendors/vendors_list.html | 150 +- templates/welcome.html | 344 +-- templates/welcome_base.html | 10 +- 154 files changed, 8565 insertions(+), 8018 deletions(-) diff --git a/car_inventory/asgi.py b/car_inventory/asgi.py index f92da1b1..08aa0cd7 100644 --- a/car_inventory/asgi.py +++ b/car_inventory/asgi.py @@ -34,9 +34,7 @@ application = ProtocolTypeRouter( URLRouter( [ path("sse/notifications/", NotificationSSEApp()), - re_path( - r"", app - ), + re_path(r"", app), ] ) ), @@ -45,4 +43,4 @@ application = ProtocolTypeRouter( # if django.conf.settings.DEBUG: -# application = ASGIStaticFilesHandler(app) \ No newline at end of file +# application = ASGIStaticFilesHandler(app) diff --git a/car_inventory/urls.py b/car_inventory/urls.py index c2b4849b..c89e3c75 100644 --- a/car_inventory/urls.py +++ b/car_inventory/urls.py @@ -35,5 +35,4 @@ urlpatterns += i18n_patterns( if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) - urlpatterns += static(settings.STATIC_URL, document_root = settings.STATIC_ROOT) - + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/inventory/admin.py b/inventory/admin.py index d952714d..25940cfb 100644 --- a/inventory/admin.py +++ b/inventory/admin.py @@ -3,6 +3,7 @@ from django.contrib import admin from . import models from django_ledger import models as ledger_models from django.contrib import messages + # from django_pdf_actions.actions import export_to_pdf_landscape, export_to_pdf_portrait # from appointment import models as appointment_models from import_export.admin import ExportMixin @@ -177,55 +178,52 @@ class CarOptionAdmin(admin.ModelAdmin): # actions = [export_to_pdf_landscape, export_to_pdf_portrait] - @admin.register(models.UserRegistration) class UserRegistrationAdmin(admin.ModelAdmin): # Fields to display in the list view list_display = [ - 'name', - 'arabic_name', - 'email', - 'crn', - 'vrn', - 'phone_number', - 'is_created', - 'created_at', + "name", + "arabic_name", + "email", + "crn", + "vrn", + "phone_number", + "is_created", + "created_at", ] # Filters in the right sidebar list_filter = [ - 'is_created', - 'created_at', + "is_created", + "created_at", ] # Searchable fields - search_fields = [ - 'name', 'arabic_name', 'email', 'crn', 'vrn', 'phone_number' - ] + search_fields = ["name", "arabic_name", "email", "crn", "vrn", "phone_number"] # Read-only fields in detail view - readonly_fields = [ - 'created_at', 'updated_at', 'is_created', 'password' - ] + readonly_fields = ["created_at", "updated_at", "is_created", "password"] # Organize form layout fieldsets = [ - ('Account Information', { - 'fields': ('name', 'arabic_name', 'email', 'phone_number') - }), - ('Business Details', { - 'fields': ('crn', 'vrn', 'address') - }), - ('Status', { - 'fields': ('is_created', 'password', 'created_at', 'updated_at'), - 'classes': ('collapse',) - }), + ( + "Account Information", + {"fields": ("name", "arabic_name", "email", "phone_number")}, + ), + ("Business Details", {"fields": ("crn", "vrn", "address")}), + ( + "Status", + { + "fields": ("is_created", "password", "created_at", "updated_at"), + "classes": ("collapse",), + }, + ), ] # Custom action to create accounts - actions = ['create_dealer_accounts'] + actions = ["create_dealer_accounts"] - @admin.action(description='Create dealer account(s) for selected registrations') + @admin.action(description="Create dealer account(s) for selected registrations") def create_dealer_accounts(self, request, queryset): created_count = 0 already_created_count = 0 @@ -242,7 +240,7 @@ class UserRegistrationAdmin(admin.ModelAdmin): self.message_user( request, f"Error creating account for {registration.name}: {str(e)}", - level=messages.ERROR + level=messages.ERROR, ) failed_count += 1 @@ -251,17 +249,17 @@ class UserRegistrationAdmin(admin.ModelAdmin): self.message_user( request, f"Successfully created {created_count} account(s).", - level=messages.SUCCESS + level=messages.SUCCESS, ) if already_created_count > 0: self.message_user( request, f"{already_created_count} registration(s) were already created.", - level=messages.INFO + level=messages.INFO, ) if failed_count > 0: self.message_user( request, f"Failed to create {failed_count} account(s). Check logs.", - level=messages.ERROR - ) \ No newline at end of file + level=messages.ERROR, + ) diff --git a/inventory/forms.py b/inventory/forms.py index 52c69ed6..1d928a94 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -2,6 +2,7 @@ from django.core.cache import cache from datetime import datetime from luhnchecker.luhn import Luhn from django.contrib.auth.models import Permission + # from appointment.models import Service from django.core.validators import MinLengthValidator from django import forms @@ -57,7 +58,7 @@ from .models import ( Tasks, Recall, Ticket, - UserRegistration + UserRegistration, ) from django_ledger import models as ledger_models from django.forms import ( @@ -364,7 +365,14 @@ class CarForm( "receiving_date", "vendor", ] - required_fields = ["vin","id_car_make", "id_car_model", "id_car_serie", "id_car_trim", "vendor"] + required_fields = [ + "vin", + "id_car_make", + "id_car_model", + "id_car_serie", + "id_car_trim", + "vendor", + ] widgets = { "id_car_make": forms.Select(attrs={"class": "form-select form-select-sm"}), "receiving_date": forms.DateTimeInput(attrs={"type": "datetime-local"}), @@ -2123,8 +2131,7 @@ class AdditionalFinancesForm(forms.Form): for field in self.fields.values(): if isinstance(field, forms.ModelMultipleChoiceField): field.widget.choices = [ - (obj.pk, f"{obj.name} - {obj.price:.2f}") - for obj in field.queryset + (obj.pk, f"{obj.name} - {obj.price:.2f}") for obj in field.queryset ] @@ -2141,6 +2148,7 @@ class VatRateForm(forms.ModelForm): model = VatRate fields = ["rate"] + class CustomSetPasswordForm(SetPasswordForm): new_password1 = forms.CharField( label="New Password", @@ -2258,13 +2266,21 @@ class TicketResolutionForm(forms.ModelForm): self.fields["status"].choices = [("resolved", "Resolved"), ("closed", "Closed")] - class CarDealershipRegistrationForm(forms.ModelForm): # Add additional fields for the registration form class Meta: model = UserRegistration - fields = ("name","arabic_name", "email","phone_number", "crn", "vrn", "address") + fields = ( + "name", + "arabic_name", + "email", + "phone_number", + "crn", + "vrn", + "address", + ) + class CarDetailsEstimateCreate(forms.Form): customer = forms.ModelChoiceField( @@ -2272,4 +2288,4 @@ class CarDetailsEstimateCreate(forms.Form): required=True, label="Customer", widget=forms.Select(attrs={"class": "form-control"}), - ) \ No newline at end of file + ) diff --git a/inventory/hooks.py b/inventory/hooks.py index b972523c..c53894dc 100644 --- a/inventory/hooks.py +++ b/inventory/hooks.py @@ -18,10 +18,10 @@ def check_create_coa_accounts(task): logger.warning("Account creation task failed, checking status...") try: - dealer_id = task.kwargs.get('dealer_id',None) - coa_slug = task.kwargs.get('coa_slug', None) + dealer_id = task.kwargs.get("dealer_id", None) + coa_slug = task.kwargs.get("coa_slug", None) logger.info(f"Checking accounts for dealer {dealer_id}") - logger.info(f"COA slug: {coa_slug}") + logger.info(f"COA slug: {coa_slug}") if not dealer_id: logger.error("No dealer_id in task kwargs") return @@ -37,7 +37,9 @@ def check_create_coa_accounts(task): try: coa = entity.get_coa_model_qs().get(slug=coa_slug) except Exception as e: - logger.error(f"COA with slug {coa_slug} not found for entity {entity.pk}: {e}") + logger.error( + f"COA with slug {coa_slug} not found for entity {entity.pk}: {e}" + ) else: coa = entity.get_default_coa() if not coa: @@ -49,7 +51,11 @@ def check_create_coa_accounts(task): missing_accounts = [] for account_data in get_accounts_data(): - if not entity.get_all_accounts().filter(coa_model=coa,code=account_data["code"]).exists(): + if ( + not entity.get_all_accounts() + .filter(coa_model=coa, code=account_data["code"]) + .exists() + ): missing_accounts.append(account_data) logger.info(f"Missing account: {account_data['code']}") @@ -62,6 +68,8 @@ def check_create_coa_accounts(task): except Exception as e: logger.error(f"Error in check_create_coa_accounts hook: {e}") + + # def check_create_coa_accounts(task): # logger.info("Checking if all accounts are created") # instance = task.kwargs["dealer"] diff --git a/inventory/management/commands/deactivate_unsubscribed_dealers.py b/inventory/management/commands/deactivate_unsubscribed_dealers.py index 47f4d960..e3de9066 100644 --- a/inventory/management/commands/deactivate_unsubscribed_dealers.py +++ b/inventory/management/commands/deactivate_unsubscribed_dealers.py @@ -8,19 +8,23 @@ from django.core.management.base import BaseCommand User = get_user_model() + class Command(BaseCommand): help = "Deactivates expired user plans" def handle(self, *args, **options): users_without_plan = User.objects.filter( - is_active=True, userplan=None, dealer__isnull=False, date_joined__lte=timezone.now()-timedelta(days=7) + is_active=True, + userplan=None, + dealer__isnull=False, + date_joined__lte=timezone.now() - timedelta(days=7), ) count = users_without_plan.count() for user in users_without_plan: user.is_active = False user.save() - subject = 'Your account has been deactivated' + subject = "Your account has been deactivated" message = """ Hello {},\n Your account has been deactivated, please contact us at {} if you have any questions. @@ -30,7 +34,7 @@ class Command(BaseCommand): """.format(user.dealer.name, settings.DEFAULT_FROM_EMAIL) from_email = settings.DEFAULT_FROM_EMAIL recipient_list = user.email - send_email(from_email, recipient_list,subject, message) + send_email(from_email, recipient_list, subject, message) self.stdout.write( self.style.SUCCESS( diff --git a/inventory/management/commands/plans_maintenance.py b/inventory/management/commands/plans_maintenance.py index c7222f36..726b406f 100644 --- a/inventory/management/commands/plans_maintenance.py +++ b/inventory/management/commands/plans_maintenance.py @@ -63,14 +63,14 @@ class Command(BaseCommand): for plan in expired_plans: # try: - if dealer := getattr(plan.user,"dealer", None): + if dealer := getattr(plan.user, "dealer", None): dealer.user.is_active = False dealer.user.save() for staff in dealer.get_staff(): staff.deactivate_account() count = expired_plans.update(active=False) - # except: - # logger.warning(f"User {plan.user_id} does not exist") + # except: + # logger.warning(f"User {plan.user_id} does not exist") self.stdout.write(f"Deactivated {count} expired plans") def cleanup_old_orders(self): diff --git a/inventory/management/commands/set_custom_permissions.py b/inventory/management/commands/set_custom_permissions.py index 4fffa995..91d7a577 100644 --- a/inventory/management/commands/set_custom_permissions.py +++ b/inventory/management/commands/set_custom_permissions.py @@ -47,4 +47,3 @@ class Command(BaseCommand): codename="can_approve_estimatemodel", content_type=ContentType.objects.get_for_model(EstimateModel), ) - diff --git a/inventory/management/commands/tenhal_plan.py b/inventory/management/commands/tenhal_plan.py index 8d21a127..89589f53 100644 --- a/inventory/management/commands/tenhal_plan.py +++ b/inventory/management/commands/tenhal_plan.py @@ -23,19 +23,19 @@ class Command(BaseCommand): # Note: Deleting plans and quotas should cascade to related objects like PlanQuota and PlanPricing. self.stdout.write(self.style.SUCCESS("Data reset complete.")) else: - self.stdout.write(self.style.NOTICE("Creating or updating default plans and quotas...")) + self.stdout.write( + self.style.NOTICE("Creating or updating default plans and quotas...") + ) # Create or get quotas users_quota, created_u = Quota.objects.get_or_create( - codename="Users", - defaults={"name": "Users", "unit": "number"} + codename="Users", defaults={"name": "Users", "unit": "number"} ) if created_u: self.stdout.write(self.style.SUCCESS('Created quota: "Users"')) cars_quota, created_c = Quota.objects.get_or_create( - codename="Cars", - defaults={"name": "Cars", "unit": "number"} + codename="Cars", defaults={"name": "Cars", "unit": "number"} ) if created_c: self.stdout.write(self.style.SUCCESS('Created quota: "Cars"')) @@ -43,90 +43,81 @@ class Command(BaseCommand): # Create or get plans basic_plan, created_bp = Plan.objects.get_or_create( name="Basic", - defaults={"description": "basic plan", "available": True, "visible": True} + defaults={"description": "basic plan", "available": True, "visible": True}, ) if created_bp: self.stdout.write(self.style.SUCCESS('Created plan: "Basic"')) pro_plan, created_pp = Plan.objects.get_or_create( name="Pro", - defaults={"description": "Pro plan", "available": True, "visible": True} + defaults={"description": "Pro plan", "available": True, "visible": True}, ) if created_pp: self.stdout.write(self.style.SUCCESS('Created plan: "Pro"')) enterprise_plan, created_ep = Plan.objects.get_or_create( name="Enterprise", - defaults={"description": "Enterprise plan", "available": True, "visible": True} + defaults={ + "description": "Enterprise plan", + "available": True, + "visible": True, + }, ) if created_ep: self.stdout.write(self.style.SUCCESS('Created plan: "Enterprise"')) # Assign quotas to plans using get_or_create to prevent duplicates PlanQuota.objects.get_or_create( - plan=basic_plan, - quota=users_quota, - defaults={"value": 10000000} + plan=basic_plan, quota=users_quota, defaults={"value": 10000000} ) PlanQuota.objects.get_or_create( - plan=basic_plan, - quota=cars_quota, - defaults={"value": 10000000} + plan=basic_plan, quota=cars_quota, defaults={"value": 10000000} ) - + # Pro plan quotas PlanQuota.objects.get_or_create( - plan=pro_plan, - quota=users_quota, - defaults={"value": 10000000} + plan=pro_plan, quota=users_quota, defaults={"value": 10000000} ) PlanQuota.objects.get_or_create( - plan=pro_plan, - quota=cars_quota, - defaults={"value": 10000000} + plan=pro_plan, quota=cars_quota, defaults={"value": 10000000} ) # Enterprise plan quotas PlanQuota.objects.get_or_create( - plan=enterprise_plan, - quota=users_quota, - defaults={"value": 10000000} + plan=enterprise_plan, quota=users_quota, defaults={"value": 10000000} ) PlanQuota.objects.get_or_create( - plan=enterprise_plan, - quota=cars_quota, - defaults={"value": 10000000} + plan=enterprise_plan, quota=cars_quota, defaults={"value": 10000000} ) # Create or get pricing basic_pricing, created_bp_p = Pricing.objects.get_or_create( - name="3 Months", - defaults={"period": 90} + name="3 Months", defaults={"period": 90} ) pro_pricing, created_pp_p = Pricing.objects.get_or_create( - name="6 Months", - defaults={"period": 180} + name="6 Months", defaults={"period": 180} ) enterprise_pricing, created_ep_p = Pricing.objects.get_or_create( - name="1 Year", - defaults={"period": 365} + name="1 Year", defaults={"period": 365} ) # Assign pricing to plans PlanPricing.objects.get_or_create( plan=basic_plan, pricing=basic_pricing, - defaults={"price": Decimal("2997.00")} + defaults={"price": Decimal("2997.00")}, ) PlanPricing.objects.get_or_create( - plan=pro_plan, - pricing=pro_pricing, - defaults={"price": Decimal("5395.00")} + plan=pro_plan, pricing=pro_pricing, defaults={"price": Decimal("5395.00")} ) PlanPricing.objects.get_or_create( plan=enterprise_plan, pricing=enterprise_pricing, - defaults={"price": Decimal("9590.00")} + defaults={"price": Decimal("9590.00")}, ) - self.stdout.write(self.style.SUCCESS("Subscription plans structure successfully created or updated.")) \ No newline at end of file + self.stdout.write( + self.style.SUCCESS( + "Subscription plans structure successfully created or updated." + ) + ) diff --git a/inventory/management/commands/update_site.py b/inventory/management/commands/update_site.py index 9e000038..9fc9a7ed 100644 --- a/inventory/management/commands/update_site.py +++ b/inventory/management/commands/update_site.py @@ -3,12 +3,13 @@ from django.core.management.base import BaseCommand from django.contrib.sites.models import Site from django.conf import settings + class Command(BaseCommand): - help = 'Update the default site domain' + help = "Update the default site domain" def handle(self, *args, **options): site = Site.objects.get_current() site.domain = settings.SITE_DOMAIN site.name = settings.SITE_NAME site.save() - self.stdout.write(self.style.SUCCESS(f'Site updated to: {site.domain}')) \ No newline at end of file + self.stdout.write(self.style.SUCCESS(f"Site updated to: {site.domain}")) diff --git a/inventory/models.py b/inventory/models.py index 556529c3..71fc8dfa 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -10,7 +10,7 @@ from django.urls import reverse # from django.utils.text import slugify from slugify import slugify from django.utils import timezone -from django.core.validators import MinValueValidator,MaxValueValidator +from django.core.validators import MinValueValidator, MaxValueValidator import hashlib from django.db import models from datetime import timedelta @@ -57,15 +57,17 @@ from encrypted_model_fields.fields import ( EncryptedEmailField, EncryptedTextField, ) + # from plans.models import AbstractPlan # from simple_history.models import HistoricalRecords from plans.models import Invoice -from django_extensions.db.fields import RandomCharField,AutoSlugField +from django_extensions.db.fields import RandomCharField, AutoSlugField logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) + class Base(models.Model): id = models.UUIDField( unique=True, @@ -206,11 +208,8 @@ class VatRate(models.Model): max_digits=5, decimal_places=2, default=Decimal("0.15"), - validators=[ - MinValueValidator(0.0), - MaxValueValidator(1.0) - ], - help_text=_("VAT rate as decimal between 0 and 1 (e.g., 0.2 for 20%)") + validators=[MinValueValidator(0.0), MaxValueValidator(1.0)], + help_text=_("VAT rate as decimal between 0 and 1 (e.g., 0.2 for 20%)"), ) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) @@ -776,7 +775,7 @@ class Car(Base): make = self.id_car_make.name if self.id_car_make else "Unknown Make" model = self.id_car_model.name if self.id_car_model else "Unknown Model" trim = self.id_car_trim.name if self.id_car_trim else "Unknown Trim" - vin=self.vin if self.vin else None + vin = self.vin if self.vin else None return f"{self.year} - {make} - {model} - {trim}-{vin}" @property @@ -843,7 +842,7 @@ class Car(Base): def mark_as_sold(self): self.cancel_reservation() self.status = CarStatusChoices.SOLD - self.sold_date=timezone.now() + self.sold_date = timezone.now() self.save() def cancel_reservation(self): @@ -904,16 +903,23 @@ class Car(Base): def get_active_estimates(self): try: - qs = self.item_model.itemtransactionmodel_set.exclude(ce_model__status="canceled") + qs = self.item_model.itemtransactionmodel_set.exclude( + ce_model__status="canceled" + ) data = [] for item in qs: - x = ExtraInfo.objects.filter(object_id=item.ce_model.pk,content_type=ContentType.objects.get_for_model(EstimateModel)).first() + x = ExtraInfo.objects.filter( + object_id=item.ce_model.pk, + content_type=ContentType.objects.get_for_model(EstimateModel), + ).first() if x: data.append(x) return data except Exception as e: - logger.error(f"Error getting active estimates for car {self.vin} error: {e}") + logger.error( + f"Error getting active estimates for car {self.vin} error: {e}" + ) return [] @property @@ -1389,7 +1395,11 @@ class Dealer(models.Model, LocalizedNameMixin): options={"quality": 80}, ) entity = models.ForeignKey( - EntityModel, on_delete=models.SET_NULL, null=True, blank=True,related_name="dealers" + EntityModel, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="dealers", ) joined_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Joined At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) @@ -1421,6 +1431,7 @@ class Dealer(models.Model, LocalizedNameMixin): except Exception as e: print(e) return None + @property def is_plan_expired(self): try: @@ -1455,6 +1466,7 @@ class Dealer(models.Model, LocalizedNameMixin): def get_vendors(self): return VendorModel.objects.filter(entity_model=self.entity) + def get_staff(self): return Staff.objects.filter(dealer=self) @@ -1505,7 +1517,9 @@ class Staff(models.Model): first_name = models.CharField(max_length=255, verbose_name=_("First Name")) last_name = models.CharField(max_length=255, verbose_name=_("Last Name")) - arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"),null=True,blank=True) + arabic_name = models.CharField( + max_length=255, verbose_name=_("Arabic Name"), null=True, blank=True + ) phone_number = EncryptedCharField( max_length=255, verbose_name=_("Phone Number"), @@ -1794,7 +1808,7 @@ class Customer(models.Model): commit=False, customer_model_kwargs={ "customer_name": self.full_name, - "address_1": "",#self.address, + "address_1": "", # self.address, # "phone": self.phone_number, # "email": self.email, }, @@ -2305,10 +2319,14 @@ class Schedule(models.Model): help_text=_("What is the status of this schedule?"), ) created_at = models.DateTimeField( - auto_now_add=True, verbose_name=_("Created Date"), help_text=_("When was this schedule created?") + auto_now_add=True, + verbose_name=_("Created Date"), + help_text=_("When was this schedule created?"), ) updated_at = models.DateTimeField( - auto_now=True, verbose_name=_("Updated Date"), help_text=_("When was this schedule last updated?") + auto_now=True, + verbose_name=_("Updated Date"), + help_text=_("When was this schedule last updated?"), ) def __str__(self): @@ -2495,13 +2513,12 @@ class Opportunity(models.Model): def __str__(self): try: if self.customer: - return ( - f"Opportunity for {self.customer.first_name} {self.customer.last_name}" - ) + return f"Opportunity for {self.customer.first_name} {self.customer.last_name}" return f"Opportunity for {self.organization.name}" except Exception: return f"Opportunity for car :{self.car}" + class Notes(models.Model): dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="notes") content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) @@ -2765,7 +2782,6 @@ class Vendor(models.Model, LocalizedNameMixin): ), ] - def __str__(self): return self.name @@ -3155,7 +3171,7 @@ class CustomGroup(models.Model): "notes", "tasks", "activity", - "additionalservices" + "additionalservices", ], ) self.set_permissions( @@ -3272,7 +3288,7 @@ class CustomGroup(models.Model): "payment", "vendor", "additionalservices", - 'customer' + "customer", ], other_perms=[ "view_car", @@ -3340,7 +3356,9 @@ class DealerSettings(models.Model): on_delete=models.SET_NULL, null=True, blank=True, - help_text=_("Cash account to track cash transactions when an invoice is created."), + help_text=_( + "Cash account to track cash transactions when an invoice is created." + ), verbose_name=_("Invoice Cash Account"), ) invoice_prepaid_account = models.ForeignKey( @@ -3349,7 +3367,9 @@ class DealerSettings(models.Model): on_delete=models.SET_NULL, null=True, blank=True, - help_text=_("Prepaid Revenue account to track prepaid revenue when an invoice is created."), + help_text=_( + "Prepaid Revenue account to track prepaid revenue when an invoice is created." + ), verbose_name=_("Invoice Prepaid Account"), ) invoice_unearned_account = models.ForeignKey( @@ -3358,7 +3378,9 @@ class DealerSettings(models.Model): on_delete=models.SET_NULL, null=True, blank=True, - help_text=_("Unearned Revenue account to track unearned revenue when an invoice is created."), + help_text=_( + "Unearned Revenue account to track unearned revenue when an invoice is created." + ), verbose_name=_("Invoice Unearned Account"), ) invoice_tax_payable_account = models.ForeignKey( @@ -3367,7 +3389,9 @@ class DealerSettings(models.Model): on_delete=models.SET_NULL, null=True, blank=True, - help_text=_("Tax Payable account to track tax liabilities when an invoice is created."), + help_text=_( + "Tax Payable account to track tax liabilities when an invoice is created." + ), verbose_name=_("Invoice Tax Payable Account"), ) invoice_vehicle_sale_account = models.ForeignKey( @@ -3376,7 +3400,9 @@ class DealerSettings(models.Model): on_delete=models.SET_NULL, null=True, blank=True, - help_text=_("Vehicle Sales account to track vehicle sales revenue when an invoice is created."), + help_text=_( + "Vehicle Sales account to track vehicle sales revenue when an invoice is created." + ), verbose_name=_("Invoice Vehicle Sale Account"), ) invoice_additional_services_account = models.ForeignKey( @@ -3385,7 +3411,9 @@ class DealerSettings(models.Model): on_delete=models.SET_NULL, null=True, blank=True, - help_text=_("Additional Services account to track additional services revenue when an invoice is created."), + help_text=_( + "Additional Services account to track additional services revenue when an invoice is created." + ), verbose_name=_("Invoice Additional Services Account"), ) invoice_cost_of_good_sold_account = models.ForeignKey( @@ -3394,7 +3422,9 @@ class DealerSettings(models.Model): on_delete=models.SET_NULL, null=True, blank=True, - help_text=_("Cost of Goods Sold account to track the cost of goods sold when an invoice is created."), + help_text=_( + "Cost of Goods Sold account to track the cost of goods sold when an invoice is created." + ), verbose_name=_("Invoice Cost of Goods Sold Account"), ) @@ -3404,7 +3434,9 @@ class DealerSettings(models.Model): on_delete=models.SET_NULL, null=True, blank=True, - help_text=_("Inventory account to track the cost of goods sold when an invoice is created."), + help_text=_( + "Inventory account to track the cost of goods sold when an invoice is created." + ), verbose_name=_("Invoice Inventory Account"), ) @@ -3423,7 +3455,9 @@ class DealerSettings(models.Model): on_delete=models.SET_NULL, null=True, blank=True, - help_text=_("Prepaid account to track prepaid expenses when a bill is created."), + help_text=_( + "Prepaid account to track prepaid expenses when a bill is created." + ), verbose_name=_("Bill Prepaid Account"), ) bill_unearned_account = models.ForeignKey( @@ -3432,10 +3466,14 @@ class DealerSettings(models.Model): on_delete=models.SET_NULL, null=True, blank=True, - help_text=_("Unearned account to track unearned expenses when a bill is created."), + help_text=_( + "Unearned account to track unearned expenses when a bill is created." + ), verbose_name=_("Bill Unearned Account"), ) - additional_info = models.JSONField(default=dict, null=True, blank=True, help_text=_("Additional information")) + additional_info = models.JSONField( + default=dict, null=True, blank=True, help_text=_("Additional information") + ) def __str__(self): return f"Settings for {self.dealer}" @@ -3790,7 +3828,10 @@ class Ticket(models.Model): ] dealer = models.ForeignKey( - Dealer, on_delete=models.CASCADE, related_name="tickets", verbose_name=_("Dealer") + Dealer, + on_delete=models.CASCADE, + related_name="tickets", + verbose_name=_("Dealer"), ) subject = models.CharField( max_length=200, verbose_name=_("Subject"), help_text=_("Short description") @@ -3882,7 +3923,6 @@ class CarImage(models.Model): ) - class UserRegistration(models.Model): name = models.CharField(_("Name"), max_length=255) arabic_name = models.CharField(_("Arabic Name"), max_length=255) @@ -3892,15 +3932,24 @@ class UserRegistration(models.Model): verbose_name=_("Phone Number"), validators=[SaudiPhoneNumberValidator()], ) - crn = models.CharField(_("Commercial Registration Number"), max_length=10, unique=True) + crn = models.CharField( + _("Commercial Registration Number"), max_length=10, unique=True + ) vrn = models.CharField(_("Vehicle Registration Number"), max_length=15, unique=True) address = models.TextField(_("Address")) - password = models.CharField(_("Password"), max_length=255,null=True,blank=True) + password = models.CharField(_("Password"), max_length=255, null=True, blank=True) is_created = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) - REQUIRED_FIELDS = ["username", "arabic_name", "crn", "vrn", "address", "phone_number"] + REQUIRED_FIELDS = [ + "username", + "arabic_name", + "crn", + "vrn", + "address", + "phone_number", + ] def __str__(self): return self.email @@ -3924,7 +3973,7 @@ class UserRegistration(models.Model): phone=self.phone_number, crn=self.crn, vrn=self.vrn, - address=self.address + address=self.address, ) if dealer: @@ -3939,4 +3988,4 @@ class UserRegistration(models.Model): except Exception as e: logger.error(f"Error creating account for {self.email}: {e}") - return False \ No newline at end of file + return False diff --git a/inventory/override.py b/inventory/override.py index 7587fa2a..d89f95a9 100644 --- a/inventory/override.py +++ b/inventory/override.py @@ -1145,6 +1145,7 @@ class ChartOfAccountModelCreateView(ChartOfAccountModelModelBaseViewMixIn, Creat }, ) + class ChartOfAccountModelUpdateView(ChartOfAccountModelModelBaseViewMixIn, UpdateView): context_object_name = "coa_model" slug_url_kwarg = "coa_slug" diff --git a/inventory/signals.py b/inventory/signals.py index f3af7710..649af178 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -5,6 +5,7 @@ from django.urls import reverse from django.contrib.auth.models import Group from django.db.models.signals import post_save, post_delete from django.dispatch import receiver + # from appointment.models import Service from django.utils.translation import gettext_lazy as _ from django.contrib.contenttypes.models import ContentType @@ -21,7 +22,7 @@ from django_ledger.models import ( EstimateModel, BillModel, ChartOfAccountModel, - CustomerModel + CustomerModel, ) from . import models from django.utils.timezone import now @@ -136,7 +137,6 @@ def create_car_location(sender, instance, created, **kwargs): print(f"Failed to create CarLocation for car {instance.vin}: {e}") - @receiver(post_save, sender=models.Dealer) def create_ledger_entity(sender, instance, created, **kwargs): if not created: @@ -155,20 +155,22 @@ def create_ledger_entity(sender, instance, created, **kwargs): raise Exception("Entity creation failed") instance.entity = entity - instance.save(update_fields=['entity']) + instance.save(update_fields=["entity"]) # Create default COA entity.create_chart_of_accounts( - assign_as_default=True, - commit=True, - coa_name=f"{entity.name}-COA" + assign_as_default=True, commit=True, coa_name=f"{entity.name}-COA" ) - logger.info(f"✅ Setup complete for dealer {instance.id}: entity & COA ready.") + logger.info( + f"✅ Setup complete for dealer {instance.id}: entity & COA ready." + ) except Exception as e: logger.error(f"💥 Failed setup for dealer {instance.id}: {e}") # Optional: schedule retry or alert + + # Create Entity # @receiver(post_save, sender=models.Dealer) # def create_ledger_entity(sender, instance, created, **kwargs): @@ -218,10 +220,10 @@ def create_ledger_entity(sender, instance, created, **kwargs): # dealer=instance, # hook="inventory.hooks.check_create_coa_accounts", # ) - # async_task('inventory.tasks.check_create_coa_accounts', instance, schedule_type='O', schedule_time=timedelta(seconds=20)) +# async_task('inventory.tasks.check_create_coa_accounts', instance, schedule_type='O', schedule_time=timedelta(seconds=20)) - # create_settings(instance.pk) - # create_accounts_for_make(instance.pk) +# create_settings(instance.pk) +# create_accounts_for_make(instance.pk) @receiver(post_save, sender=models.Dealer) @@ -998,25 +1000,27 @@ def save_po(sender, instance, created, **kwargs): instance.itemtransactionmodel_set.first().po_model.save() except Exception as e: pass + + @receiver(post_save, sender=PurchaseOrderModel) def create_po_item_upload(sender, instance, created, **kwargs): - if instance.po_status == "fulfilled" or instance.po_status == 'approved': + if instance.po_status == "fulfilled" or instance.po_status == "approved": for item in instance.get_itemtxs_data()[0]: dealer = models.Dealer.objects.get(entity=instance.entity) if item.bill_model and item.bill_model.is_paid(): - models.PoItemsUploaded.objects.update_or_create( - dealer=dealer, po=instance, item=item, - defaults={ - "status":instance.po_status - } - ) + models.PoItemsUploaded.objects.update_or_create( + dealer=dealer, + po=instance, + item=item, + defaults={"status": instance.po_status}, + ) - # po_item = models.PoItemsUploaded.objects.get_or_create( - # dealer=dealer, po=instance, item=item, - # defaults={ - # "status":instance.po_status - # } - # ) + # po_item = models.PoItemsUploaded.objects.get_or_create( + # dealer=dealer, po=instance, item=item, + # defaults={ + # "status":instance.po_status + # } + # ) # @receiver(post_save, sender=models.Staff) @@ -1364,7 +1368,9 @@ def handle_car_image(sender, instance, created, **kwargs): # ) # Check for existing image with same hash - existing = os.path.exists(os.path.join(settings.MEDIA_ROOT, "car_images",car.get_hash + ".png")) + existing = os.path.exists( + os.path.join(settings.MEDIA_ROOT, "car_images", car.get_hash + ".png") + ) # existing = ( # models.CarImage.objects.filter( # image_hash=car.get_hash, image__isnull=False @@ -1406,7 +1412,7 @@ def handle_user_registration(sender, instance, created, **kwargs): """ Thank you for registering with us. We will contact you shortly to complete your application. شكرا لمراسلتنا. سوف نتصل بك قريبا لاستكمال طلبك. - """ + """, ) if instance.is_created: @@ -1430,7 +1436,8 @@ def handle_user_registration(sender, instance, created, **kwargs): يرجى تسجيل الدخول إلى الموقع لاستكمال الملف الشخصي والبدء في استخدام خدماتنا. شكرا لاختيارك لنا. - """) + """, + ) @receiver(post_save, sender=ChartOfAccountModel) @@ -1463,4 +1470,4 @@ def handle_chart_of_account(sender, instance, created, **kwargs): # sync=False # Explicitly set to async # ) except Exception as e: - logger.error(f"Error handling chart of account: {e}") \ No newline at end of file + logger.error(f"Error handling chart of account: {e}") diff --git a/inventory/tasks.py b/inventory/tasks.py index a0a3875a..d40683a3 100644 --- a/inventory/tasks.py +++ b/inventory/tasks.py @@ -12,12 +12,14 @@ from django.db import transaction from django_ledger.io import roles from django_q.tasks import async_task from django.core.mail import send_mail + # from appointment.models import StaffMember from django.utils.translation import activate from django.core.files.base import ContentFile from django.contrib.auth import get_user_model from allauth.account.models import EmailAddress from django.core.mail import EmailMultiAlternatives + # from .utils import get_accounts_data, create_account from django.template.loader import render_to_string from django.utils.translation import gettext_lazy as _ @@ -30,7 +32,7 @@ from inventory.models import ( CarReservation, CarStatusChoices, CarImage, - Car + Car, ) logger = logging.getLogger(__name__) @@ -63,17 +65,18 @@ def create_settings(pk): ) -def create_coa_accounts(dealer_id,**kwargs): +def create_coa_accounts(dealer_id, **kwargs): """ Idempotent: Creates only missing default accounts. Safe to retry. Returns True if all done. """ from .models import Dealer from .utils import get_accounts_data, create_account + try: dealer = Dealer.objects.get(pk=dealer_id) entity = dealer.entity - coa_slug = kwargs.get('coa_slug', None) + coa_slug = kwargs.get("coa_slug", None) if not entity: logger.error(f"❌ No entity for dealer {dealer_id}") return False @@ -82,7 +85,9 @@ def create_coa_accounts(dealer_id,**kwargs): try: coa = entity.get_coa_model_qs().get(slug=coa_slug) except Exception as e: - logger.error(f"COA with slug {coa_slug} not found for entity {entity.pk}: {e}") + logger.error( + f"COA with slug {coa_slug} not found for entity {entity.pk}: {e}" + ) return False else: coa = entity.get_default_coa() @@ -92,10 +97,13 @@ def create_coa_accounts(dealer_id,**kwargs): return False # Get missing accounts - existing_codes = set(entity.get_all_accounts().filter(coa_model=coa).values_list('code', flat=True)) + existing_codes = set( + entity.get_all_accounts() + .filter(coa_model=coa) + .values_list("code", flat=True) + ) accounts_to_create = [ - acc for acc in get_accounts_data() - if acc["code"] not in existing_codes + acc for acc in get_accounts_data() if acc["code"] not in existing_codes ] if not accounts_to_create: @@ -122,6 +130,7 @@ def create_coa_accounts(dealer_id,**kwargs): logger.error(f"💥 Task failed for dealer {dealer_id}: {e}") raise # Let Django-Q handle retry if configured + def retry_entity_creation(dealer_id, retry_count=0): """ Retry entity creation if initial attempt failed @@ -164,8 +173,10 @@ def retry_entity_creation(dealer_id, retry_count=0): async_task( "inventory.tasks.retry_entity_creation", dealer_id=dealer_id, - retry_count=retry_count + 1 + retry_count=retry_count + 1, ) + + # def create_coa_accounts(**kwargs): # logger.info("creating all accounts are created") # instance = kwargs.get("dealer") diff --git a/inventory/templatetags/custom_filters.py b/inventory/templatetags/custom_filters.py index fb73e6d3..e013f164 100644 --- a/inventory/templatetags/custom_filters.py +++ b/inventory/templatetags/custom_filters.py @@ -13,6 +13,7 @@ from django.db.models import Case, Value, When, IntegerField register = template.Library() + @register.filter def is_negative(value): """ @@ -23,6 +24,7 @@ def is_negative(value): except (ValueError, TypeError): return False + @register.filter def get_percentage(value, total): try: @@ -501,8 +503,16 @@ def bill_item_formset_table(context, item_formset): for item in item_formset: if item: print(item.fields["item_model"]) - item.initial["quantity"] = item.instance.po_quantity if item.instance.po_quantity else item.instance.quantity - item.initial["unit_cost"] = item.instance.po_unit_cost if item.instance.po_unit_cost else item.instance.unit_cost + item.initial["quantity"] = ( + item.instance.po_quantity + if item.instance.po_quantity + else item.instance.quantity + ) + item.initial["unit_cost"] = ( + item.instance.po_unit_cost + if item.instance.po_unit_cost + else item.instance.unit_cost + ) # print(item.instance.po_quantity) # print(item.instance.po_unit_cost) # print(item.instance.po_total_amount) diff --git a/inventory/urls.py b/inventory/urls.py index e2d76dcf..9236ab3b 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -2,7 +2,7 @@ from inventory.utils import get_user_type from . import views from django.urls import path from django.urls import reverse_lazy -from django.views.generic import RedirectView,TemplateView +from django.views.generic import RedirectView, TemplateView from django_tables2.export.export import TableExport from django.conf.urls import handler403, handler400, handler404, handler500 @@ -10,14 +10,17 @@ urlpatterns = [ # main URLs path("", views.WelcomeView, name="welcome"), # path("signup/", views.dealer_signup, name="account_signup"), - path('signup/', views.CarDealershipSignUpView.as_view(), name='account_signup'), - path('success/', TemplateView.as_view(template_name='account/success.html'), name='registration_success'), + path("signup/", views.CarDealershipSignUpView.as_view(), name="account_signup"), + path( + "success/", + TemplateView.as_view(template_name="account/success.html"), + name="registration_success", + ), path("", views.HomeView, name="home"), # path('refund-policy/',views.refund_policy,name='refund_policy'), path("/", views.HomeView, name="home"), # Tasks path("legal/", views.terms_and_privacy, name="terms_and_privacy"), - # path('tasks//detail/', views.task_detail, name='task_detail'), # Dashboards # path("user//settings/", views.UserSettingsView.as_view(), name="user_settings"), @@ -44,13 +47,18 @@ urlpatterns = [ views.assign_car_makes, name="assign_car_makes", ), - - - #dashboards for manager, dealer, inventory and accounatant - path("dashboards//general/", views.general_dashboard,name="general_dashboard"), - #dashboard for sales - path("dashboards//sales/", views.sales_dashboard, name="sales_dashboard"), - + # dashboards for manager, dealer, inventory and accounatant + path( + "dashboards//general/", + views.general_dashboard, + name="general_dashboard", + ), + # dashboard for sales + path( + "dashboards//sales/", + views.sales_dashboard, + name="sales_dashboard", + ), path( "/cars/aging-inventory/list", views.aging_inventory_list_view, @@ -786,7 +794,11 @@ urlpatterns = [ views.EstimateDetailView.as_view(), name="estimate_detail", ), - path('/sales/estimates/print//', views.EstimatePrintView.as_view(), name='estimate_print'), + path( + "/sales/estimates/print//", + views.EstimatePrintView.as_view(), + name="estimate_print", + ), path( "/sales/estimates/create/", views.create_estimate, @@ -943,7 +955,6 @@ urlpatterns = [ views.ItemServiceUpdateView.as_view(), name="item_service_update", ), - path( "/items/services//detail/", views.ItemServiceDetailView.as_view(), @@ -1112,32 +1123,47 @@ urlpatterns = [ name="entity-ic-date", ), # Chart of Accounts... - path('/chart-of-accounts//list/', + path( + "/chart-of-accounts//list/", views.ChartOfAccountModelListView.as_view(), - name='coa-list'), - path('/chart-of-accounts//list/inactive/', - views.ChartOfAccountModelListView.as_view(inactive=True), - name='coa-list-inactive'), - path('//create/', - views.ChartOfAccountModelCreateView.as_view(), - name='coa-create'), - path('//detail//', - views.ChartOfAccountModelListView.as_view(), - name='coa-detail'), - path('//update//', - views.ChartOfAccountModelUpdateView.as_view(), - name='coa-update'), - + name="coa-list", + ), + path( + "/chart-of-accounts//list/inactive/", + views.ChartOfAccountModelListView.as_view(inactive=True), + name="coa-list-inactive", + ), + path( + "//create/", + views.ChartOfAccountModelCreateView.as_view(), + name="coa-create", + ), + path( + "//detail//", + views.ChartOfAccountModelListView.as_view(), + name="coa-detail", + ), + path( + "//update//", + views.ChartOfAccountModelUpdateView.as_view(), + name="coa-update", + ), # ACTIONS.... - path('//action//mark-as-default/', - views.CharOfAccountModelActionView.as_view(action_name='mark_as_default'), - name='coa-action-mark-as-default'), - path('//action//mark-as-active/', - views.CharOfAccountModelActionView.as_view(action_name='mark_as_active'), - name='coa-action-mark-as-active'), - path('//action//mark-as-inactive/', - views.CharOfAccountModelActionView.as_view(action_name='mark_as_inactive'), - name='coa-action-mark-as-inactive'), + path( + "//action//mark-as-default/", + views.CharOfAccountModelActionView.as_view(action_name="mark_as_default"), + name="coa-action-mark-as-default", + ), + path( + "//action//mark-as-active/", + views.CharOfAccountModelActionView.as_view(action_name="mark_as_active"), + name="coa-action-mark-as-active", + ), + path( + "//action//mark-as-inactive/", + views.CharOfAccountModelActionView.as_view(action_name="mark_as_inactive"), + name="coa-action-mark-as-inactive", + ), # CASH FLOW STATEMENTS... # Entities... path( @@ -1313,42 +1339,80 @@ urlpatterns = [ views.PurchaseOrderMarkAsVoidView.as_view(), name="po-action-mark-as-void", ), - # reports - path( + path( "/purchase-report/", views.purchase_report_view, name="po-report", ), - path('purchase-report//csv/', views.purchase_report_csv_export, name='purchase-report-csv-export'), - - path( + path( + "purchase-report//csv/", + views.purchase_report_csv_export, + name="purchase-report-csv-export", + ), + path( "/car-sale-report/", views.car_sale_report_view, name="car-sale-report", ), - path('/car-sale-report/get_filtered_choices/',views.get_filtered_choices,name='get_filtered_choices'), - path('car-sale-report//csv/', views.car_sale_report_csv_export, name='car-sale-report-csv-export'), - - path('feature/recall/', views.RecallListView.as_view(), name='recall_list'), - path('feature/recall/filter/', views.RecallFilterView, name='recall_filter'), - path('feature/recall//view/', views.RecallDetailView.as_view(), name='recall_detail'), - path('feature/recall/create/', views.RecallCreateView.as_view(), name='recall_create'), - path('feature/recall/success/', views.RecallSuccessView.as_view(), name='recall_success'), - - path('/schedules/calendar/', views.schedule_calendar, name='schedule_calendar'), - + path( + "/car-sale-report/get_filtered_choices/", + views.get_filtered_choices, + name="get_filtered_choices", + ), + path( + "car-sale-report//csv/", + views.car_sale_report_csv_export, + name="car-sale-report-csv-export", + ), + path("feature/recall/", views.RecallListView.as_view(), name="recall_list"), + path("feature/recall/filter/", views.RecallFilterView, name="recall_filter"), + path( + "feature/recall//view/", + views.RecallDetailView.as_view(), + name="recall_detail", + ), + path( + "feature/recall/create/", views.RecallCreateView.as_view(), name="recall_create" + ), + path( + "feature/recall/success/", + views.RecallSuccessView.as_view(), + name="recall_success", + ), + path( + "/schedules/calendar/", + views.schedule_calendar, + name="schedule_calendar", + ), # staff profile - path('/staff/detail/', views.StaffDetailView.as_view(), name='staff_detail'), + path( + "/staff/detail/", + views.StaffDetailView.as_view(), + name="staff_detail", + ), # tickets - path('help_center/view/', views.help_center, name='help_center'), - path('/help_center/tickets/', views.ticket_list, name='ticket_list'), - path('help_center/tickets//create/', views.create_ticket, name='create_ticket'), - path('/help_center/tickets//', views.ticket_detail, name='ticket_detail'), - path('help_center/tickets//update/', views.ticket_update, name='ticket_update'), + path("help_center/view/", views.help_center, name="help_center"), + path( + "/help_center/tickets/", views.ticket_list, name="ticket_list" + ), + path( + "help_center/tickets//create/", + views.create_ticket, + name="create_ticket", + ), + path( + "/help_center/tickets//", + views.ticket_detail, + name="ticket_detail", + ), + path( + "help_center/tickets//update/", + views.ticket_update, + name="ticket_update", + ), # path('help_center/tickets//ticket_mark_resolved/', views.ticket_mark_resolved, name='ticket_mark_resolved'), - path('payment_results/', views.payment_result, name='payment_result'), - + path("payment_results/", views.payment_result, name="payment_result"), ] handler404 = "inventory.views.custom_page_not_found_view" diff --git a/inventory/utils.py b/inventory/utils.py index 4cd9aa08..89df3fd5 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -27,7 +27,7 @@ from django_ledger.models import ( VendorModel, AccountModel, EntityModel, - ChartOfAccountModel + ChartOfAccountModel, ) from django.core.files.base import ContentFile from django_ledger.models.items import ItemModel @@ -1329,9 +1329,9 @@ def get_finance_data(estimate, dealer): additional_services = car.get_additional_services() discounted_price = Decimal(car.marked_price) - discount vat_amount = discounted_price * vat.rate - total_services_amount=additional_services.get("total") + total_services_amount = additional_services.get("total") total_services_vat = sum([x[1] for x in additional_services.get("services")]) - total_services_amount_=additional_services.get("total_") + total_services_amount_ = additional_services.get("total_") total_vat = vat_amount + total_services_vat return { "car": car, @@ -1342,16 +1342,11 @@ def get_finance_data(estimate, dealer): "discount_amount": discount, "additional_services": additional_services, "final_price": discounted_price + vat_amount, - - - "total_services_vat": total_services_vat, - "total_services_amount":total_services_amount, - "total_services_amount_":total_services_amount_, - + "total_services_amount": total_services_amount, + "total_services_amount_": total_services_amount_, "total_vat": total_vat, "grand_total": discounted_price + total_vat + additional_services.get("total"), - } # totals = self.calculate_totals() @@ -1605,43 +1600,69 @@ def _post_sale_and_cogs(invoice, dealer): 1) Cash / A-R / VAT / Revenue journal 2) COGS / Inventory journal """ - entity:EntityModel = invoice.ledger.entity + entity: EntityModel = invoice.ledger.entity # calc = CarFinanceCalculator(invoice) data = get_finance_data(invoice, dealer) car = data.get("car") - coa:ChartOfAccountModel = entity.get_default_coa() + coa: ChartOfAccountModel = entity.get_default_coa() cash_acc = invoice.cash_account or dealer.settings.invoice_cash_account - vat_acc = dealer.settings.invoice_tax_payable_account or entity.get_default_coa_accounts().filter(role_default=True, role=roles.LIABILITY_CL_TAXES_PAYABLE).first() + vat_acc = ( + dealer.settings.invoice_tax_payable_account + or entity.get_default_coa_accounts() + .filter(role_default=True, role=roles.LIABILITY_CL_TAXES_PAYABLE) + .first() + ) - car_rev = dealer.settings.invoice_vehicle_sale_account or entity.get_default_coa_accounts().filter(role_default=True, role=roles.INCOME_OPERATIONAL).first() + car_rev = ( + dealer.settings.invoice_vehicle_sale_account + or entity.get_default_coa_accounts() + .filter(role_default=True, role=roles.INCOME_OPERATIONAL) + .first() + ) add_rev = dealer.settings.invoice_additional_services_account if not add_rev: try: - add_rev = entity.get_default_coa_accounts().filter(name="After-Sales Services", active=True).first() + add_rev = ( + entity.get_default_coa_accounts() + .filter(name="After-Sales Services", active=True) + .first() + ) if not add_rev: add_rev = coa.create_account( - code="4020", - name="After-Sales Services", - role=roles.INCOME_OPERATIONAL, - balance_type=roles.CREDIT, - active=True, + code="4020", + name="After-Sales Services", + role=roles.INCOME_OPERATIONAL, + balance_type=roles.CREDIT, + active=True, ) add_rev.role_default = False - add_rev.save(update_fields=['role_default']) + add_rev.save(update_fields=["role_default"]) dealer.settings.invoice_additional_services_account = add_rev dealer.settings.save() except Exception as e: logger.error(f"error find or create additional services account {e}") if car.get_additional_services_amount > 0 and not add_rev: - raise Exception("additional services exist but not account found,please create account for the additional services and set as default in the settings") - cogs_acc = dealer.settings.invoice_cost_of_good_sold_account or entity.get_default_coa_accounts().filter(role_default=True, role=roles.COGS).first() + raise Exception( + "additional services exist but not account found,please create account for the additional services and set as default in the settings" + ) + cogs_acc = ( + dealer.settings.invoice_cost_of_good_sold_account + or entity.get_default_coa_accounts() + .filter(role_default=True, role=roles.COGS) + .first() + ) - inv_acc = dealer.settings.invoice_inventory_account or entity.get_default_coa_accounts().filter(role_default=True, role=roles.ASSET_CA_INVENTORY).first() + inv_acc = ( + dealer.settings.invoice_inventory_account + or entity.get_default_coa_accounts() + .filter(role_default=True, role=roles.ASSET_CA_INVENTORY) + .first() + ) net_car_price = Decimal(data["discounted_price"]) net_additionals_price = Decimal(data["additional_services"]["total"]) @@ -1696,11 +1717,12 @@ def _post_sale_and_cogs(invoice, dealer): # tx_type='credit' # ) - if car.get_additional_services_amount > 0: # Cr Sales – Additional Services if not add_rev: - logger.warning(f"Additional Services account not set for dealer {dealer}. Skipping additional services revenue entry.") + logger.warning( + f"Additional Services account not set for dealer {dealer}. Skipping additional services revenue entry." + ) else: TransactionModel.objects.create( journal_entry=je_sale, @@ -1938,7 +1960,7 @@ def handle_payment(request, dealer): } # Get selected plan from session - selected_plan_id = request.session.get('pending_plan_id') + selected_plan_id = request.session.get("pending_plan_id") if not selected_plan_id: raise ValueError("No pending plan found in session") from plans.models import PlanPricing @@ -1956,25 +1978,27 @@ def handle_payment(request, dealer): "dealer_slug": dealer.slug, } - payload = json.dumps({ - "amount": total, - "currency": "SAR", - "description": f"Payment for plan {pp.plan.name}", - "callback_url": callback_url, - "source": { - "type": "creditcard", - "name": card_name, - "number": card_number, - "month": month, - "year": year, - "cvc": cvv, - "statement_descriptor": "Century Store", - "3ds": True, - "manual": False, - "save_card": False, - }, - "metadata": metadata, - }) + payload = json.dumps( + { + "amount": total, + "currency": "SAR", + "description": f"Payment for plan {pp.plan.name}", + "callback_url": callback_url, + "source": { + "type": "creditcard", + "name": card_name, + "number": card_number, + "month": month, + "year": year, + "cvc": cvv, + "statement_descriptor": "Century Store", + "3ds": True, + "manual": False, + "save_card": False, + }, + "metadata": metadata, + } + ) headers = {"Content-Type": "application/json", "Accept": "application/json"} auth = (settings.MOYASAR_SECRET_KEY, "") @@ -1998,7 +2022,9 @@ def handle_payment(request, dealer): gateway_response=data, ) logger.info(f"Payment initiated: {data}") - return data["source"]["transaction_url"],None + return data["source"]["transaction_url"], None + + # def handle_payment(request, order): # logger.info(f"Handling payment for order {order}") # url = "https://api.moyasar.com/v1/payments" @@ -2518,10 +2544,9 @@ def create_account(entity, coa, account_data): logger.info(f"Created account: {account}") if account: account.role_default = account_data.get("default", False) - account.save(update_fields=['role_default']) + account.save(update_fields=["role_default"]) return True - except IntegrityError: return True # Already created by race condition except Exception as e: @@ -2529,6 +2554,7 @@ def create_account(entity, coa, account_data): return False + # def create_account(entity, coa, account_data): # try: # account = entity.create_account( @@ -2810,7 +2836,9 @@ def generate_car_image_simple(car): # Save the resized image logger.info(f" {car.vin}") with open( - os.path.join(settings.MEDIA_ROOT, f"car_images/{car.get_hash}.{file_extension}"), + os.path.join( + settings.MEDIA_ROOT, f"car_images/{car.get_hash}.{file_extension}" + ), "wb", ) as f: f.write(resized_data) @@ -2826,9 +2854,7 @@ def generate_car_image_simple(car): return {"success": False, "error": error_msg} - - -def create_estimate_(dealer,car,customer): +def create_estimate_(dealer, car, customer): entity = dealer.entity title = f"Estimate for {car.vin}-{car.id_car_make.name}-{car.id_car_model.name}-{car.year} for customer {customer.first_name} {customer.last_name}" estimate = entity.create_estimate( @@ -2856,4 +2882,4 @@ def create_estimate_(dealer,car,customer): estimate.delete() raise e - return estimate \ No newline at end of file + return estimate diff --git a/inventory/validators.py b/inventory/validators.py index 791df1dc..856778ef 100644 --- a/inventory/validators.py +++ b/inventory/validators.py @@ -16,9 +16,10 @@ class SaudiPhoneNumberValidator(RegexValidator): cleaned_value = re.sub(r"[\s\-\(\)\.]", "", str(value)) super().__call__(cleaned_value) + def vat_rate_validator(value): if value < 0 or value > 1: raise ValidationError( - _('%(value)s is not a valid VAT rate. It must be between 0 and 1.'), - params={'value': value}, - ) \ No newline at end of file + _("%(value)s is not a valid VAT rate. It must be between 0 and 1."), + params={"value": value}, + ) diff --git a/inventory/views.py b/inventory/views.py index 220bb70a..27c904b9 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -19,7 +19,7 @@ from random import randint from decimal import Decimal from io import TextIOWrapper from django.apps import apps -from datetime import datetime, timedelta,date +from datetime import datetime, timedelta, date from calendar import month_name from pyzbar.pyzbar import decode from urllib.parse import urlparse, urlunparse @@ -37,6 +37,7 @@ from django.core.exceptions import PermissionDenied from django.contrib.contenttypes.models import ContentType from django.views.decorators.http import require_POST from django.template.loader import render_to_string + # Django from django.db.models import Q from django.conf import settings @@ -54,7 +55,7 @@ from django.forms import CharField, HiddenInput, ValidationError from django.shortcuts import HttpResponse from django.db.models import Sum, F, Count -from django.db.models.functions import ExtractMonth,Round +from django.db.models.functions import ExtractMonth, Round from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.contrib.auth.models import User, Group from django.db.models import Value @@ -109,7 +110,7 @@ from django_ledger.forms.bank_account import ( BankAccountUpdateForm, ) from django_ledger.views.chart_of_accounts import ( - ChartOfAccountModelListView as ChartOfAccountModelListViewBase + ChartOfAccountModelListView as ChartOfAccountModelListViewBase, ) from django_ledger.views.bill import ( BillModelCreateView, @@ -175,7 +176,7 @@ from django_ledger.models import ( BillModel, LedgerModel, PurchaseOrderModel, - ChartOfAccountModel + ChartOfAccountModel, ) from django_ledger.views.financial_statement import ( FiscalYearBalanceSheetView, @@ -198,6 +199,7 @@ from django_ledger.views.mixins import ( from . import models, forms, tables from django_tables2 import SingleTableView from django_tables2.export.views import ExportMixin + # from appointment.models import Appointment, AppointmentRequest, Service, StaffMember from .services import ( decodevin, @@ -291,8 +293,10 @@ def switch_language(request): logger.warning(f"Invalid language code: {language}") return redirect("/") + def dealer_signup(request): from django_q.tasks import async_task + """ Handles the dealer signup wizard process, including forms validation, user and group creation, permissions assignment, and dealer data storage. This view supports GET @@ -365,6 +369,7 @@ def dealer_signup(request): "account/signup-wizar.html", ) + # class HomeView(LoginRequiredMixin, TemplateView): # """ # HomeView class responsible for rendering the home page. @@ -383,13 +388,15 @@ def dealer_signup(request): # template_name = "index.html" + @login_required -def HomeView(request,dealer_slug=None): - dealer_slug=request.dealer.slug +def HomeView(request, dealer_slug=None): + dealer_slug = request.dealer.slug if request.is_sales and not request.is_manager and not request.is_dealer: - return redirect('sales_dashboard', dealer_slug=dealer_slug) + return redirect("sales_dashboard", dealer_slug=dealer_slug) else: - return redirect('general_dashboard',dealer_slug=dealer_slug) + return redirect("general_dashboard", dealer_slug=dealer_slug) + class TestView(TemplateView): """ @@ -407,25 +414,26 @@ class TestView(TemplateView): template_name = "inventory/cars_list_api.html" + @login_required -def general_dashboard(request,dealer_slug): +def general_dashboard(request, dealer_slug): """ Renders the dealer dashboard with key performance indicators and chart data. """ - dealer = get_object_or_404(models.Dealer,slug=dealer_slug) - vat = models.VatRate.objects.filter(dealer=dealer,is_active=True).first() - VAT_RATE=vat.rate + dealer = get_object_or_404(models.Dealer, slug=dealer_slug) + vat = models.VatRate.objects.filter(dealer=dealer, is_active=True).first() + VAT_RATE = vat.rate today_local = timezone.localdate() # ---------------------------------------------------- # 1. Date Filtering # ---------------------------------------------------- - start_date_str = request.GET.get('start_date') - end_date_str = request.GET.get('end_date') + start_date_str = request.GET.get("start_date") + end_date_str = request.GET.get("end_date") if start_date_str and end_date_str: - start_date = timezone.datetime.strptime(start_date_str, '%Y-%m-%d').date() - end_date = timezone.datetime.strptime(end_date_str, '%Y-%m-%d').date() + start_date = timezone.datetime.strptime(start_date_str, "%Y-%m-%d").date() + end_date = timezone.datetime.strptime(end_date_str, "%Y-%m-%d").date() else: start_date = today_local - timedelta(days=30) end_date = today_local @@ -433,104 +441,152 @@ def general_dashboard(request,dealer_slug): # ---------------------------------------------------- # 2. Inventory KPIs # ---------------------------------------------------- - active_cars = models.Car.objects.filter(dealer=dealer).exclude(status='sold') + active_cars = models.Car.objects.filter(dealer=dealer).exclude(status="sold") total_cars_in_inventory = active_cars.count() - total_inventory_value = active_cars.aggregate(total=Sum('cost_price'))['total'] or 0 - new_cars_qs = active_cars.filter(stock_type='new') + total_inventory_value = active_cars.aggregate(total=Sum("cost_price"))["total"] or 0 + new_cars_qs = active_cars.filter(stock_type="new") total_new_cars_in_inventory = new_cars_qs.count() - new_car_value = new_cars_qs.aggregate(total=Sum('cost_price'))['total'] or 0 - used_cars_qs = active_cars.filter(stock_type='used') + new_car_value = new_cars_qs.aggregate(total=Sum("cost_price"))["total"] or 0 + used_cars_qs = active_cars.filter(stock_type="used") total_used_cars_in_inventory = used_cars_qs.count() - used_car_value = used_cars_qs.aggregate(total=Sum('cost_price'))['total'] or 0 + used_car_value = used_cars_qs.aggregate(total=Sum("cost_price"))["total"] or 0 aging_threshold_days = 60 - aging_inventory_count = active_cars.filter(receiving_date__date__lte=today_local - timedelta(days=aging_threshold_days)).count() + aging_inventory_count = active_cars.filter( + receiving_date__date__lte=today_local - timedelta(days=aging_threshold_days) + ).count() # ---------------------------------------------------- # 3. Sales KPIs (filtered by date) # ---------------------------------------------------- cars_sold_filtered = models.Car.objects.filter( dealer=dealer, - status='sold', + status="sold", sold_date__date__gte=start_date, - sold_date__date__lte=end_date + sold_date__date__lte=end_date, ) # General sales KPIs total_cars_sold = cars_sold_filtered.count() - total_cost_of_cars_sold = cars_sold_filtered.aggregate(total=Sum('cost_price'))['total'] or 0 - total_revenue_from_cars = cars_sold_filtered.aggregate( - total=Sum(F('marked_price') - F('discount_amount')) - )['total'] or 0 + total_cost_of_cars_sold = ( + cars_sold_filtered.aggregate(total=Sum("cost_price"))["total"] or 0 + ) + total_revenue_from_cars = ( + cars_sold_filtered.aggregate( + total=Sum(F("marked_price") - F("discount_amount")) + )["total"] + or 0 + ) - total_vat_collected_from_cars = cars_sold_filtered.annotate( - final_price=F('marked_price') - F('discount_amount')).aggregate( - total=Sum(F('final_price') * VAT_RATE))['total'] or 0 + total_vat_collected_from_cars = ( + cars_sold_filtered.annotate( + final_price=F("marked_price") - F("discount_amount") + ).aggregate(total=Sum(F("final_price") * VAT_RATE))["total"] + or 0 + ) net_profit_from_cars = total_revenue_from_cars - total_cost_of_cars_sold - total_discount = cars_sold_filtered.aggregate(total=Sum('discount_amount'))['total'] or 0 + total_discount = ( + cars_sold_filtered.aggregate(total=Sum("discount_amount"))["total"] or 0 + ) # Sales breakdown by type - new_cars_sold = cars_sold_filtered.filter(stock_type='new') + new_cars_sold = cars_sold_filtered.filter(stock_type="new") total_new_cars_sold = new_cars_sold.count() - total_cost_of_new_cars_sold = new_cars_sold.aggregate(total=Sum('cost_price'))['total'] or 0 + total_cost_of_new_cars_sold = ( + new_cars_sold.aggregate(total=Sum("cost_price"))["total"] or 0 + ) # total_revenue_from_new_cars=sum([ car.final_price for car in new_cars_sold]) - total_revenue_from_new_cars = new_cars_sold.aggregate( - total=Sum(F('marked_price') - F('discount_amount')) - )['total'] or 0 + total_revenue_from_new_cars = ( + new_cars_sold.aggregate(total=Sum(F("marked_price") - F("discount_amount")))[ + "total" + ] + or 0 + ) - total_vat_collected_from_new_cars = new_cars_sold.annotate( - final_price=F('marked_price') - F('discount_amount')).aggregate( - total=Sum(F('final_price') * VAT_RATE))['total'] or 0 + total_vat_collected_from_new_cars = ( + new_cars_sold.annotate( + final_price=F("marked_price") - F("discount_amount") + ).aggregate(total=Sum(F("final_price") * VAT_RATE))["total"] + or 0 + ) net_profit_from_new_cars = total_revenue_from_new_cars - total_cost_of_new_cars_sold - - - used_cars_sold = cars_sold_filtered.filter(stock_type='used') + used_cars_sold = cars_sold_filtered.filter(stock_type="used") total_used_cars_sold = used_cars_sold.count() - total_cost_of_used_cars_sold = used_cars_sold.aggregate(total=Sum('cost_price'))['total'] or 0 - total_revenue_from_used_cars = used_cars_sold.aggregate( - total=Sum(F('marked_price') - F('discount_amount')) - )['total'] or 0 + total_cost_of_used_cars_sold = ( + used_cars_sold.aggregate(total=Sum("cost_price"))["total"] or 0 + ) + total_revenue_from_used_cars = ( + used_cars_sold.aggregate(total=Sum(F("marked_price") - F("discount_amount")))[ + "total" + ] + or 0 + ) - total_vat_collected_from_used_cars = used_cars_sold.annotate( - final_price=F('marked_price') - F('discount_amount')).aggregate( - total=Sum(F('final_price') * VAT_RATE))['total'] or 0 + total_vat_collected_from_used_cars = ( + used_cars_sold.annotate( + final_price=F("marked_price") - F("discount_amount") + ).aggregate(total=Sum(F("final_price") * VAT_RATE))["total"] + or 0 + ) - net_profit_from_used_cars = total_revenue_from_used_cars - total_cost_of_used_cars_sold + net_profit_from_used_cars = ( + total_revenue_from_used_cars - total_cost_of_used_cars_sold + ) # Service & Overall KPIs - total_revenue_from_services = sum([car.get_additional_services()['total'] for car in cars_sold_filtered]) - total_vat_collected_from_services = sum([car.get_additional_services()['services_vat'] for car in cars_sold_filtered]) - total_vat_collected = total_vat_collected_from_cars + total_vat_collected_from_services + total_revenue_from_services = sum( + [car.get_additional_services()["total"] for car in cars_sold_filtered] + ) + total_vat_collected_from_services = sum( + [car.get_additional_services()["services_vat"] for car in cars_sold_filtered] + ) + total_vat_collected = ( + total_vat_collected_from_cars + total_vat_collected_from_services + ) total_revenue_generated = total_revenue_from_cars + total_revenue_from_services # total_expenses=sum([x.amount_paid for x in dealer.entity.get_bills().filter(bill_items__item_role="expense")]) - total_expenses=dealer.entity.get_bills().filter(bill_items__item_role="expense").aggregate(total=Sum('amount_paid'))['total'] or 0 - gross_profit = net_profit_from_cars+total_revenue_from_services - total_expenses + total_expenses = ( + dealer.entity.get_bills() + .filter(bill_items__item_role="expense") + .aggregate(total=Sum("amount_paid"))["total"] + or 0 + ) + gross_profit = net_profit_from_cars + total_revenue_from_services - total_expenses # ---------------------------------------------------- # 4. Chart Data Aggregation # ---------------------------------------------------- - monthly_sales_data = cars_sold_filtered.annotate( - month=ExtractMonth('sold_date') - ).values('month').annotate( - total_cars=Count('pk'), - total_revenue=Sum(F('marked_price') - F('discount_amount')), - total_profit=Sum(F('marked_price') - F('discount_amount') - F('cost_price')) - ).order_by('month') + monthly_sales_data = ( + cars_sold_filtered.annotate(month=ExtractMonth("sold_date")) + .values("month") + .annotate( + total_cars=Count("pk"), + total_revenue=Sum(F("marked_price") - F("discount_amount")), + total_profit=Sum( + F("marked_price") - F("discount_amount") - F("cost_price") + ), + ) + .order_by("month") + ) monthly_cars_sold = [0] * 12 monthly_revenue = [0] * 12 monthly_net_profit = [0] * 12 for data in monthly_sales_data: - month_index = data['month'] - 1 - monthly_cars_sold[month_index] = data['total_cars'] - monthly_revenue[month_index] = float(data['total_revenue']) if data['total_revenue'] else 0 - monthly_net_profit[month_index] = float(data['total_profit']) if data['total_profit'] else 0 + month_index = data["month"] - 1 + monthly_cars_sold[month_index] = data["total_cars"] + monthly_revenue[month_index] = ( + float(data["total_revenue"]) if data["total_revenue"] else 0 + ) + monthly_net_profit[month_index] = ( + float(data["total_profit"]) if data["total_profit"] else 0 + ) monthly_cars_sold_json = json.dumps(monthly_cars_sold) monthly_revenue_json = json.dumps(monthly_revenue) @@ -539,224 +595,217 @@ def general_dashboard(request,dealer_slug): # ---------------------------------------------------- # Sales by MAKE # ---------------------------------------------------- - sales_by_make_data = cars_sold_filtered.values('id_car_make__name').annotate( - car_count=Count('id_car_make__name') - ).order_by('-car_count') - - sales_by_make_labels = [data['id_car_make__name'] for data in sales_by_make_data] - sales_by_make_counts = [data['car_count'] for data in sales_by_make_data] - + sales_by_make_data = ( + cars_sold_filtered.values("id_car_make__name") + .annotate(car_count=Count("id_car_make__name")) + .order_by("-car_count") + ) + sales_by_make_labels = [data["id_car_make__name"] for data in sales_by_make_data] + sales_by_make_counts = [data["car_count"] for data in sales_by_make_data] # ---------------------------------------------------- # DATA FOR CAR SALES BY MODELS (for the new interactive chart) # ---------------------------------------------------- - # Get the selected make from the URL query parameter - selected_make_sales= request.GET.get('make_sold', None) - + selected_make_sales = request.GET.get("make_sold", None) # Get a list of all unique makes for the dropdown - all_makes_sold = list(cars_sold_filtered.values_list('id_car_make__name', flat=True).distinct().order_by('id_car_make__name')) + all_makes_sold = list( + cars_sold_filtered.values_list("id_car_make__name", flat=True) + .distinct() + .order_by("id_car_make__name") + ) if selected_make_sales: # If a make is selected, filter the queryset - sales_data_by_model = cars_sold_filtered.filter( - id_car_make__name=selected_make_sales - ).values('id_car_model__name').annotate( - count=Count('id_car_model__name') - ).order_by('-count') + sales_data_by_model = ( + cars_sold_filtered.filter(id_car_make__name=selected_make_sales) + .values("id_car_model__name") + .annotate(count=Count("id_car_model__name")) + .order_by("-count") + ) else: # If no make is selected, pass an empty list or some default data sales_data_by_model = [] - - - # 1. Inventory by Make (Pie Chart) - inventory_by_make_data = active_cars.values('id_car_make__name').annotate( - car_count=Count('id_car_make__name') - ).order_by('-car_count') + inventory_by_make_data = ( + active_cars.values("id_car_make__name") + .annotate(car_count=Count("id_car_make__name")) + .order_by("-car_count") + ) - inventory_by_make_labels = [data['id_car_make__name'] for data in inventory_by_make_data] - inventory_by_make_counts = [data['car_count'] for data in inventory_by_make_data] + inventory_by_make_labels = [ + data["id_car_make__name"] for data in inventory_by_make_data + ] + inventory_by_make_counts = [data["car_count"] for data in inventory_by_make_data] # 2. Inventory by Model (Bar Chart) - selected_make_inventory = request.GET.get('make_inventory', None) + selected_make_inventory = request.GET.get("make_inventory", None) # Get all unique makes in inventory for the dropdown - all_makes_inventory = list(active_cars.values_list('id_car_make__name', flat=True).distinct().order_by('id_car_make__name')) + all_makes_inventory = list( + active_cars.values_list("id_car_make__name", flat=True) + .distinct() + .order_by("id_car_make__name") + ) if selected_make_inventory: - inventory_data_by_model = active_cars.filter( - id_car_make__name=selected_make_inventory - ).values('id_car_model__name').annotate( - count=Count('id_car_model__name') - ).order_by('-count') + inventory_data_by_model = ( + active_cars.filter(id_car_make__name=selected_make_inventory) + .values("id_car_model__name") + .annotate(count=Count("id_car_model__name")) + .order_by("-count") + ) else: # Default data inventory_data_by_model = [] context = { - 'start_date': start_date, - 'end_date': end_date, - 'today': today_local, - + "start_date": start_date, + "end_date": end_date, + "today": today_local, # Inventory KPIs - 'total_cars_in_inventory': total_cars_in_inventory, - 'total_inventory_value': total_inventory_value, - 'total_new_cars_in_inventory': total_new_cars_in_inventory, - 'total_used_cars_in_inventory': total_used_cars_in_inventory, - 'new_car_value': new_car_value, - 'used_car_value': used_car_value, - 'aging_inventory_count': aging_inventory_count, - + "total_cars_in_inventory": total_cars_in_inventory, + "total_inventory_value": total_inventory_value, + "total_new_cars_in_inventory": total_new_cars_in_inventory, + "total_used_cars_in_inventory": total_used_cars_in_inventory, + "new_car_value": new_car_value, + "used_car_value": used_car_value, + "aging_inventory_count": aging_inventory_count, # Sales KPIs - 'total_cars_sold': total_cars_sold, - 'total_cost_of_cars_sold': total_cost_of_cars_sold, - 'total_revenue_from_cars': total_revenue_from_cars, - 'net_profit_from_cars': net_profit_from_cars, - 'total_vat_collected_from_cars': total_vat_collected_from_cars, - 'total_discount_on_cars': total_discount, - + "total_cars_sold": total_cars_sold, + "total_cost_of_cars_sold": total_cost_of_cars_sold, + "total_revenue_from_cars": total_revenue_from_cars, + "net_profit_from_cars": net_profit_from_cars, + "total_vat_collected_from_cars": total_vat_collected_from_cars, + "total_discount_on_cars": total_discount, # Sales by Type - 'total_new_cars_sold': total_new_cars_sold, - 'total_used_cars_sold': total_used_cars_sold, - 'total_cost_of_new_cars_sold': total_cost_of_new_cars_sold, - 'total_revenue_from_new_cars': total_revenue_from_new_cars, - 'net_profit_from_new_cars': net_profit_from_new_cars, - 'total_vat_collected_from_new_cars': total_vat_collected_from_new_cars, - 'total_cost_of_used_cars_sold': total_cost_of_used_cars_sold, - 'total_revenue_from_used_cars': total_revenue_from_used_cars, - 'net_profit_from_used_cars': net_profit_from_used_cars, - 'total_vat_collected_from_used_cars': total_vat_collected_from_used_cars, - + "total_new_cars_sold": total_new_cars_sold, + "total_used_cars_sold": total_used_cars_sold, + "total_cost_of_new_cars_sold": total_cost_of_new_cars_sold, + "total_revenue_from_new_cars": total_revenue_from_new_cars, + "net_profit_from_new_cars": net_profit_from_new_cars, + "total_vat_collected_from_new_cars": total_vat_collected_from_new_cars, + "total_cost_of_used_cars_sold": total_cost_of_used_cars_sold, + "total_revenue_from_used_cars": total_revenue_from_used_cars, + "net_profit_from_used_cars": net_profit_from_used_cars, + "total_vat_collected_from_used_cars": total_vat_collected_from_used_cars, # Services and Overall KPIs - 'total_revenue_from_services': total_revenue_from_services, - 'total_vat_collected_from_services': total_vat_collected_from_services, - 'total_revenue_generated': total_revenue_generated, - 'total_vat_collected': total_vat_collected, - 'total_expenses': total_expenses, - 'gross_profit': gross_profit, - + "total_revenue_from_services": total_revenue_from_services, + "total_vat_collected_from_services": total_vat_collected_from_services, + "total_revenue_generated": total_revenue_generated, + "total_vat_collected": total_vat_collected, + "total_expenses": total_expenses, + "gross_profit": gross_profit, # Chart Data - - 'monthly_cars_sold_json': monthly_cars_sold_json, - 'monthly_revenue_json': monthly_revenue_json, - 'monthly_net_profit_json': monthly_net_profit_json, - - - # Sales Chart Data - 'sales_by_make_labels_json': json.dumps(sales_by_make_labels), - 'sales_by_make_counts_json': json.dumps(sales_by_make_counts), - 'all_makes_sold': all_makes_sold, - 'selected_make_sales': selected_make_sales, - 'sales_data_by_model_json': json.dumps(list(sales_data_by_model)), - + "monthly_cars_sold_json": monthly_cars_sold_json, + "monthly_revenue_json": monthly_revenue_json, + "monthly_net_profit_json": monthly_net_profit_json, + # Sales Chart Data + "sales_by_make_labels_json": json.dumps(sales_by_make_labels), + "sales_by_make_counts_json": json.dumps(sales_by_make_counts), + "all_makes_sold": all_makes_sold, + "selected_make_sales": selected_make_sales, + "sales_data_by_model_json": json.dumps(list(sales_data_by_model)), # New Inventory Chart Data - 'inventory_by_make_labels_json': json.dumps(inventory_by_make_labels), - 'inventory_by_make_counts_json': json.dumps(inventory_by_make_counts), - 'all_makes_inventory': all_makes_inventory, - 'selected_make_inventory': selected_make_inventory, - 'inventory_data_by_model_json': json.dumps(list(inventory_data_by_model)), - - + "inventory_by_make_labels_json": json.dumps(inventory_by_make_labels), + "inventory_by_make_counts_json": json.dumps(inventory_by_make_counts), + "all_makes_inventory": all_makes_inventory, + "selected_make_inventory": selected_make_inventory, + "inventory_data_by_model_json": json.dumps(list(inventory_data_by_model)), } - - return render(request, 'dashboards/general_dashboard.html', context) + return render(request, "dashboards/general_dashboard.html", context) @login_required -def sales_dashboard(request,dealer_slug): - dealer = get_object_or_404(models.Dealer,slug=dealer_slug) +def sales_dashboard(request, dealer_slug): + dealer = get_object_or_404(models.Dealer, slug=dealer_slug) today_local = timezone.localdate() # ---------------------------------------------------- # 1. Date Filtering # ---------------------------------------------------- - start_date_str = request.GET.get('start_date') - end_date_str = request.GET.get('end_date') + start_date_str = request.GET.get("start_date") + end_date_str = request.GET.get("end_date") if start_date_str and end_date_str: - start_date = timezone.datetime.strptime(start_date_str, '%Y-%m-%d').date() - end_date = timezone.datetime.strptime(end_date_str, '%Y-%m-%d').date() + start_date = timezone.datetime.strptime(start_date_str, "%Y-%m-%d").date() + end_date = timezone.datetime.strptime(end_date_str, "%Y-%m-%d").date() else: start_date = today_local - timedelta(days=30) end_date = today_local # Filter leads by date range and dealer leads_filtered = models.Lead.objects.filter( - dealer=dealer, - created__date__gte=start_date, - created__date__lte=end_date + dealer=dealer, created__date__gte=start_date, created__date__lte=end_date ) - total_leads=leads_filtered.count() - + total_leads = leads_filtered.count() # ---------------------------------------------------- # 2. Lead Sources Chart Logic # ---------------------------------------------------- # Group leads by source and count them # This generates a list of dictionaries like [{'source': 'Showroom', 'count': 45}, ...] - lead_sources_data = leads_filtered.values('source').annotate( - count=Count('source') - ).order_by('-count') + lead_sources_data = ( + leads_filtered.values("source") + .annotate(count=Count("source")) + .order_by("-count") + ) # Separate the labels and counts for the chart - lead_sources_labels = [item['source'] for item in lead_sources_data] - lead_sources_counts = [item['count'] for item in lead_sources_data] + lead_sources_labels = [item["source"] for item in lead_sources_data] + lead_sources_counts = [item["count"] for item in lead_sources_data] # ---------------------------------------------------- # 2. Lead Funnel Chart Logic # ---------------------------------------------------- opportunity_filtered = models.Opportunity.objects.filter( - dealer=dealer, - created__date__gte=start_date, - created__date__lte=end_date + dealer=dealer, created__date__gte=start_date, created__date__lte=end_date ) - opportunity_stage_data = opportunity_filtered.values('stage').annotate( - count=Count('stage') - ).order_by('-count') - # Separate the labels and counts for the chart - opportunity_stage_labels = [item['stage'] for item in opportunity_stage_data ] - opportunity_stage_counts = [item['count'] for item in opportunity_stage_data ] - + opportunity_stage_data = ( + opportunity_filtered.values("stage") + .annotate(count=Count("stage")) + .order_by("-count") + ) + # Separate the labels and counts for the chart + opportunity_stage_labels = [item["stage"] for item in opportunity_stage_data] + opportunity_stage_counts = [item["count"] for item in opportunity_stage_data] # 2. Inventory KPIs # ---------------------------------------------------- - active_cars = models.Car.objects.filter(dealer=dealer).exclude(status='sold') + active_cars = models.Car.objects.filter(dealer=dealer).exclude(status="sold") total_cars_in_inventory = active_cars.count() - new_cars_qs = active_cars.filter(stock_type='new') + new_cars_qs = active_cars.filter(stock_type="new") total_new_cars_in_inventory = new_cars_qs.count() - used_cars_qs = active_cars.filter(stock_type='used') + used_cars_qs = active_cars.filter(stock_type="used") total_used_cars_in_inventory = used_cars_qs.count() aging_threshold_days = 60 - aging_inventory_count = active_cars.filter(receiving_date__date__lte=today_local - timedelta(days=aging_threshold_days)).count() - + aging_inventory_count = active_cars.filter( + receiving_date__date__lte=today_local - timedelta(days=aging_threshold_days) + ).count() context = { - 'start_date': start_date, - 'end_date': end_date, - 'lead_sources_labels_json': json.dumps(lead_sources_labels), - 'lead_sources_counts_json': json.dumps(lead_sources_counts), - 'opportunity_stage_labels_json': json.dumps(opportunity_stage_labels), - 'opportunity_stage_counts_json': json.dumps(opportunity_stage_counts), - - # Inventory KPIs - 'total_cars_in_inventory': total_cars_in_inventory, - 'total_new_cars_in_inventory': total_new_cars_in_inventory, - 'total_used_cars_in_inventory': total_used_cars_in_inventory, - 'aging_inventory_count': aging_inventory_count, - 'total_leads':total_leads + "start_date": start_date, + "end_date": end_date, + "lead_sources_labels_json": json.dumps(lead_sources_labels), + "lead_sources_counts_json": json.dumps(lead_sources_counts), + "opportunity_stage_labels_json": json.dumps(opportunity_stage_labels), + "opportunity_stage_counts_json": json.dumps(opportunity_stage_counts), + # Inventory KPIs + "total_cars_in_inventory": total_cars_in_inventory, + "total_new_cars_in_inventory": total_new_cars_in_inventory, + "total_used_cars_in_inventory": total_used_cars_in_inventory, + "aging_inventory_count": aging_inventory_count, + "total_leads": total_leads, } - return render(request, 'dashboards/sales_dashboard.html', context) - - + return render(request, "dashboards/sales_dashboard.html", context) def aging_inventory_list_view(request, dealer_slug): @@ -768,51 +817,80 @@ def aging_inventory_list_view(request, dealer_slug): aging_threshold_days = 60 # Get filter parameters from the request - selected_make = request.GET.get('make') - selected_model = request.GET.get('model') - selected_series = request.GET.get('series') # Changed 'serie' to 'series' for consistency - selected_year = request.GET.get('year') - selected_stock_type = request.GET.get('stock_type') + selected_make = request.GET.get("make") + selected_model = request.GET.get("model") + selected_series = request.GET.get( + "series" + ) # Changed 'serie' to 'series' for consistency + selected_year = request.GET.get("year") + selected_stock_type = request.GET.get("stock_type") # Start with the base queryset for all aging cars. aging_cars_queryset = models.Car.objects.filter( dealer=dealer, - receiving_date__date__lt=today_local - timedelta(days=aging_threshold_days) - ).exclude(status='sold') + receiving_date__date__lt=today_local - timedelta(days=aging_threshold_days), + ).exclude(status="sold") # Apply filters to the queryset if they exist. Chaining is fine here. if selected_make: - aging_cars_queryset = aging_cars_queryset.filter(id_car_make__name=selected_make) + aging_cars_queryset = aging_cars_queryset.filter( + id_car_make__name=selected_make + ) if selected_model: - aging_cars_queryset = aging_cars_queryset.filter(id_car_model__name=selected_model) + aging_cars_queryset = aging_cars_queryset.filter( + id_car_model__name=selected_model + ) if selected_series: - aging_cars_queryset = aging_cars_queryset.filter(id_car_series__name=selected_series) + aging_cars_queryset = aging_cars_queryset.filter( + id_car_series__name=selected_series + ) if selected_year: - aging_cars_queryset = aging_cars_queryset.filter(id_car_year__year=selected_year) + aging_cars_queryset = aging_cars_queryset.filter( + id_car_year__year=selected_year + ) if selected_stock_type: aging_cars_queryset = aging_cars_queryset.filter(stock_type=selected_stock_type) - total_aging_inventory_value=aging_cars_queryset.aggregate(total=Sum('cost_price'))['total'] + total_aging_inventory_value = aging_cars_queryset.aggregate( + total=Sum("cost_price") + )["total"] count_of_aging_cars = aging_cars_queryset.count() - # Get distinct values for filter dropdowns based on the initial, unfiltered aging cars queryset. # This ensures all possible filter options are always available. aging_base_queryset = models.Car.objects.filter( dealer=dealer, - receiving_date__date__lt=today_local - timedelta(days=aging_threshold_days) - ).exclude(status='sold') + receiving_date__date__lt=today_local - timedelta(days=aging_threshold_days), + ).exclude(status="sold") - all_makes = aging_base_queryset.values_list('id_car_make__name', flat=True).distinct().order_by('id_car_make__name') - all_models = aging_base_queryset.values_list('id_car_model__name', flat=True).distinct().order_by('id_car_model__name') - all_series = aging_base_queryset.values_list('id_car_serie__name', flat=True).distinct().order_by('id_car_serie__name') - all_stock_types = aging_base_queryset.values_list('stock_type', flat=True).distinct().order_by('stock_type') - all_years = aging_base_queryset.values_list('year', flat=True).distinct().order_by('-year') + all_makes = ( + aging_base_queryset.values_list("id_car_make__name", flat=True) + .distinct() + .order_by("id_car_make__name") + ) + all_models = ( + aging_base_queryset.values_list("id_car_model__name", flat=True) + .distinct() + .order_by("id_car_model__name") + ) + all_series = ( + aging_base_queryset.values_list("id_car_serie__name", flat=True) + .distinct() + .order_by("id_car_serie__name") + ) + all_stock_types = ( + aging_base_queryset.values_list("stock_type", flat=True) + .distinct() + .order_by("stock_type") + ) + all_years = ( + aging_base_queryset.values_list("year", flat=True).distinct().order_by("-year") + ) # # Set up pagination paginator = Paginator(aging_cars_queryset, 10) - page_number = request.GET.get('page') + page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) # Iterate only on the cars for the current page to add the age attribute. @@ -822,23 +900,23 @@ def aging_inventory_list_view(request, dealer_slug): context = { "is_paginated": page_obj.has_other_pages, "cars": page_obj.object_list, - 'selected_make': selected_make, - 'selected_model': selected_model, - 'selected_series': selected_series, # Corrected variable name - 'selected_year': selected_year, - 'selected_stock_type': selected_stock_type, - 'all_makes': all_makes, - 'all_models': all_models, - 'all_series': all_series, - 'all_stock_types': all_stock_types, - 'all_years': all_years, - 'total_aging_inventory_value':total_aging_inventory_value, - 'page_obj':page_obj, - 'count_of_aging_cars':count_of_aging_cars - + "selected_make": selected_make, + "selected_model": selected_model, + "selected_series": selected_series, # Corrected variable name + "selected_year": selected_year, + "selected_stock_type": selected_stock_type, + "all_makes": all_makes, + "all_models": all_models, + "all_series": all_series, + "all_stock_types": all_stock_types, + "all_years": all_years, + "total_aging_inventory_value": total_aging_inventory_value, + "page_obj": page_obj, + "count_of_aging_cars": count_of_aging_cars, } - return render(request, 'dashboards/aging_inventory_list.html', context) + return render(request, "dashboards/aging_inventory_list.html", context) + def terms_and_privacy(request): return render(request, "terms_and_privacy.html") @@ -863,7 +941,9 @@ def WelcomeView(request): return render(request, "welcome.html", context) -class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView): +class CarCreateView( + LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView +): """ Manages the creation of a new car entry in the inventory system. @@ -885,7 +965,7 @@ class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMi form_class = forms.CarForm template_name = "inventory/car_form.html" permission_required = ["inventory.add_car"] - success_message=_("Car Added successfully to the inventory") + success_message = _("Car Added successfully to the inventory") def get_form(self, form_class=None): form = super().get_form(form_class) @@ -976,7 +1056,7 @@ class AjaxHandlerView(LoginRequiredMixin, View): def decode_vin(self, request): dealer = request.dealer vin_no = request.GET.get("vin_no") - car_existed = models.Car.objects.filter(dealer=dealer,vin=vin_no).exists() + car_existed = models.Car.objects.filter(dealer=dealer, vin=vin_no).exists() if car_existed: return JsonResponse({"error": _("VIN number exists")}, status=400) @@ -1284,7 +1364,9 @@ class CarInventory(LoginRequiredMixin, PermissionRequiredMixin, ListView): return context -class CarColorCreate(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView): +class CarColorCreate( + LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView +): """ View for creating a new car color. @@ -1308,7 +1390,7 @@ class CarColorCreate(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageM form_class = forms.CarColorsForm template_name = "inventory/add_colors.html" permission_required = ["inventory.add_carcolors"] - success_message=_("Car colors details added successfully") + success_message = _("Car colors details added successfully") def form_valid(self, form): car = get_object_or_404(models.Car, slug=self.kwargs["slug"]) @@ -1593,7 +1675,11 @@ def inventory_stats_view(request, dealer_slug): for make_data in inventory.values() ], } - return render(request, "inventory/inventory_stats.html", {"inventory": result,"empty_state_value":_("car")}) + return render( + request, + "inventory/inventory_stats.html", + {"inventory": result, "empty_state_value": _("car")}, + ) # @login_required @@ -1743,14 +1829,16 @@ class CarDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) form = forms.CarDetailsEstimateCreate() - form.fields["customer"].queryset = form.fields["customer"].queryset.filter(dealer=self.request.dealer) + form.fields["customer"].queryset = form.fields["customer"].queryset.filter( + dealer=self.request.dealer + ) context["estimate_form"] = form context["active_estimates"] = self.object.get_active_estimates() return context -def CarFinanceUpdateView(request,dealer_slug,slug): +def CarFinanceUpdateView(request, dealer_slug, slug): car = get_object_or_404(models.Car, slug=slug) dealer = get_object_or_404(models.Dealer, slug=dealer_slug) @@ -1764,7 +1852,12 @@ def CarFinanceUpdateView(request,dealer_slug,slug): else: form = forms.CarFinanceForm(instance=car) - return render(request, "inventory/car_finance_form.html", {"car": car, "dealer": dealer, "form": form}) + return render( + request, + "inventory/car_finance_form.html", + {"car": car, "dealer": dealer, "form": form}, + ) + class CarUpdateView( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView @@ -1795,7 +1888,10 @@ class CarUpdateView( permission_required = ["inventory.change_car"] def get_success_url(self): - return reverse("car_detail", kwargs={"dealer_slug": self.request.dealer.slug,"slug": self.object.slug}) + return reverse( + "car_detail", + kwargs={"dealer_slug": self.request.dealer.slug, "slug": self.object.slug}, + ) def get_form(self, form_class=None): form = super().get_form(form_class) @@ -2354,6 +2450,8 @@ class DealerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): from .forms import VatRateForm + + @login_required def dealer_vat_rate_update(request, slug): dealer = get_object_or_404(models.Dealer, slug=slug) @@ -2361,7 +2459,6 @@ def dealer_vat_rate_update(request, slug): vat_rate_instance, created = models.VatRate.objects.get_or_create(dealer=dealer) if request.method == "POST": - form = VatRateForm(request.POST, instance=vat_rate_instance) if form.is_valid(): @@ -2369,11 +2466,10 @@ def dealer_vat_rate_update(request, slug): messages.success(request, _("VAT rate updated successfully")) return redirect("dealer_detail", slug=slug) else: - messages.error(request, _("Please enter valid vat rate between 0 and 1.")) redirect("dealer_detail", slug=slug) - return redirect("dealer_detail", slug=slug) + return redirect("dealer_detail", slug=slug) class DealerUpdateView( @@ -2409,7 +2505,8 @@ class DealerUpdateView( def get_success_url(self): return reverse("dealer_detail", kwargs={"slug": self.object.slug}) -class StaffDetailView(LoginRequiredMixin, DetailView): + +class StaffDetailView(LoginRequiredMixin, DetailView): """ Represents a detailed view for a Dealer model. @@ -2431,9 +2528,6 @@ class StaffDetailView(LoginRequiredMixin, DetailView): context_object_name = "staff" - - - class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): """ View for displaying a list of customers. @@ -2525,6 +2619,7 @@ class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): # context["note_form"] = forms.NoteForm() # return context + class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): """ CustomerDetailView handles retrieving and presenting detailed information about @@ -2554,13 +2649,11 @@ class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView context = super().get_context_data(**kwargs) context["notes"] = models.Notes.objects.filter( - dealer=dealer, - content_type__model="customer", object_id=self.object.id + dealer=dealer, content_type__model="customer", object_id=self.object.id ) estimates = entity.get_estimates().filter(customer=self.object.customer_model) invoices = entity.get_invoices().filter(customer=self.object.customer_model) - context['leads']=self.object.customer_leads.all() - + context["leads"] = self.object.customer_leads.all() total = estimates.count() + invoices.count() @@ -2675,8 +2768,7 @@ class CustomerCreateView( def form_valid(self, form): dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) if customer := models.Customer.objects.filter( - dealer=dealer, - email=form.instance.email + dealer=dealer, email=form.instance.email ).first(): if not customer.active: messages.error( @@ -2855,16 +2947,25 @@ def vendorDetailView(request, dealer_slug, slug): :rtype: HttpResponse """ dealer = get_object_or_404(models.Dealer, slug=dealer_slug) - vendor = get_object_or_404(models.Vendor, slug=slug,dealer=dealer) - cars=vendor.cars.all() - total_cars_from_vendor=cars.count() - vendor_makes=cars.values('id_car_make__name').annotate(make_count=Count('id_car_make__name')) - vendor_bills=BillModel.objects.filter(vendor=vendor.vendor_model) - paginator=Paginator(vendor_bills,20) + vendor = get_object_or_404(models.Vendor, slug=slug, dealer=dealer) + cars = vendor.cars.all() + total_cars_from_vendor = cars.count() + vendor_makes = cars.values("id_car_make__name").annotate( + make_count=Count("id_car_make__name") + ) + vendor_bills = BillModel.objects.filter(vendor=vendor.vendor_model) + paginator = Paginator(vendor_bills, 20) page_number = request.GET.get("page") - page_obj=paginator.get_page(page_number) + page_obj = paginator.get_page(page_number) return render( - request, template_name="vendors/view_vendor.html", context={"vendor": vendor,"vendor_bills":page_obj,"total_cars_from_vendor":total_cars_from_vendor,"vendor_makes":vendor_makes} + request, + template_name="vendors/view_vendor.html", + context={ + "vendor": vendor, + "vendor_bills": page_obj, + "total_cars_from_vendor": total_cars_from_vendor, + "vendor_makes": vendor_makes, + }, ) @@ -2901,7 +3002,9 @@ class VendorCreateView( def form_valid(self, form): dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) - if vendor := models.Vendor.objects.filter(dealer=dealer,email=form.instance.email).first(): + if vendor := models.Vendor.objects.filter( + dealer=dealer, email=form.instance.email + ).first(): if not vendor.active: messages.error( self.request, @@ -3582,11 +3685,13 @@ class UserListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) staff = models.Staff.objects.filter(dealer=dealer, active=True).all() return apply_search_filters(staff, query) + def get_context_data(self, **kwargs): - context=super().get_context_data(**kwargs) - context['no_staff_message']=_("staff") + context = super().get_context_data(**kwargs) + context["no_staff_message"] = _("staff") return context + class UserDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): """ Represents a detailed view for displaying user-specific information. @@ -3667,7 +3772,10 @@ class UserCreateView( # return self.form_invalid(form) email = form.cleaned_data["email"] - if models.Staff.objects.filter(user__email=email).exists() or models.Dealer.objects.filter(user__email=email).exists(): + if ( + models.Staff.objects.filter(user__email=email).exists() + or models.Dealer.objects.filter(user__email=email).exists() + ): messages.error( self.request, _( @@ -3692,7 +3800,7 @@ class UserCreateView( # staff_member, _ = StaffMember.objects.get_or_create(user=user) # for service in form.cleaned_data["service_offered"]: - # staff_member.services_offered.add(service) + # staff_member.services_offered.add(service) staff.user = user staff.dealer = dealer staff.save() @@ -3702,7 +3810,9 @@ class UserCreateView( return super().form_valid(form) def get_success_url(self): - return reverse_lazy("staff_password_reset", args=[self.request.dealer.slug, self.staff_pk]) + return reverse_lazy( + "staff_password_reset", args=[self.request.dealer.slug, self.staff_pk] + ) # return reverse_lazy("user_list", args=[self.request.dealer.slug]) @@ -3832,11 +3942,13 @@ class OrganizationListView(LoginRequiredMixin, PermissionRequiredMixin, ListView organization = dealer.organizations.filter(active=True) return apply_search_filters(organization, query) + def get_context_data(self, **kwargs): - context=super().get_context_data(**kwargs) - context["empty_state_value"]=_("organization") + context = super().get_context_data(**kwargs) + context["empty_state_value"] = _("organization") return context + class OrganizationDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): """ Handles displaying detailed information about an organization. @@ -3888,8 +4000,7 @@ class OrganizationCreateView(LoginRequiredMixin, PermissionRequiredMixin, Create def form_valid(self, form): dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) if organization := models.Organization.objects.filter( - dealer=dealer, - email=form.instance.email + dealer=dealer, email=form.instance.email ).first(): if not organization.active: messages.error( @@ -4395,9 +4506,15 @@ class AccountListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): def get_queryset(self): query = self.request.GET.get("q") dealer = get_user_type(self.request) - coa = ChartOfAccountModel.objects.get(entity=dealer.entity,pk=self.kwargs["coa_pk"]) or self.request.entity.get_default_coa() + coa = ( + ChartOfAccountModel.objects.get( + entity=dealer.entity, pk=self.kwargs["coa_pk"] + ) + or self.request.entity.get_default_coa() + ) accounts = coa.get_coa_accounts() return apply_search_filters(accounts, query) + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["url_kwargs"] = self.kwargs @@ -4448,7 +4565,12 @@ class AccountCreateView( def form_valid(self, form): dealer = get_user_type(self.request) - coa = ChartOfAccountModel.objects.get(entity=dealer.entity,pk=self.kwargs["coa_pk"]) or self.request.entity.get_default_coa() + coa = ( + ChartOfAccountModel.objects.get( + entity=dealer.entity, pk=self.kwargs["coa_pk"] + ) + or self.request.entity.get_default_coa() + ) form.instance.entity_model = dealer.entity form.instance.coa_model = coa form.instance.depth = 0 @@ -4458,31 +4580,47 @@ class AccountCreateView( def get_form_kwargs(self): dealer = get_user_type(self.request) kwargs = super().get_form_kwargs() - coa = ChartOfAccountModel.objects.get(entity=dealer.entity,pk=self.kwargs["coa_pk"]) or self.request.entity.get_default_coa() + coa = ( + ChartOfAccountModel.objects.get( + entity=dealer.entity, pk=self.kwargs["coa_pk"] + ) + or self.request.entity.get_default_coa() + ) kwargs["coa_model"] = coa return kwargs def get_form(self, form_class=None): form = super().get_form(form_class) entity = get_user_type(self.request).entity - coa = ChartOfAccountModel.objects.get(entity=entity,pk=self.kwargs["coa_pk"]) or self.request.entity.get_default_coa() + coa = ( + ChartOfAccountModel.objects.get(entity=entity, pk=self.kwargs["coa_pk"]) + or self.request.entity.get_default_coa() + ) form.initial["coa_model"] = coa return form def get_success_url(self): return reverse( - "account_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"], "coa_pk": self.kwargs["coa_pk"]} + "account_list", + kwargs={ + "dealer_slug": self.kwargs["dealer_slug"], + "coa_pk": self.kwargs["coa_pk"], + }, ) - def get_context_data(self,**kwargs): + + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["url_kwargs"] = self.kwargs coa_pk = context["url_kwargs"]["coa_pk"] try: - context["coa_model"] = ChartOfAccountModel.objects.get(entity=self.request.entity,pk=coa_pk) + context["coa_model"] = ChartOfAccountModel.objects.get( + entity=self.request.entity, pk=coa_pk + ) except Exception: context["coa_model"] = self.request.entity.get_default_coa() return context + class AccountDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): """ Represents the detailed view for an account with additional context data related to account @@ -4583,26 +4721,38 @@ class AccountUpdateView( return form def form_valid(self, form): - form.instance.coa_model = ChartOfAccountModel.objects.get(pk=self.kwargs['coa_pk']) or self.request.entity.get_default_coa() + form.instance.coa_model = ( + ChartOfAccountModel.objects.get(pk=self.kwargs["coa_pk"]) + or self.request.entity.get_default_coa() + ) return super().form_valid(form) def get_success_url(self): return reverse_lazy( - "account_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"],"coa_pk":self.kwargs["coa_pk"]} + "account_list", + kwargs={ + "dealer_slug": self.kwargs["dealer_slug"], + "coa_pk": self.kwargs["coa_pk"], + }, ) - def get_context_data(self,**kwargs): + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["url_kwargs"] = self.kwargs coa_pk = context["url_kwargs"]["coa_pk"] try: - context["coa_model"] = ChartOfAccountModel.objects.get(pk=coa_pk) or self.request.entity.get_default_coa() + context["coa_model"] = ( + ChartOfAccountModel.objects.get(pk=coa_pk) + or self.request.entity.get_default_coa() + ) except Exception: context["coa_model"] = self.request.entity.get_default_coa() return context + + @login_required @permission_required("django_ledger.delete_accountmodel") -def account_delete(request, dealer_slug,coa_pk, pk): +def account_delete(request, dealer_slug, coa_pk, pk): """ Handles the deletion of an account object identified by its primary key (pk). Ensures that the user has the necessary permissions to perform the deletion. Successfully @@ -4647,17 +4797,19 @@ def sales_list_view(request, dealer_slug): qs = [] try: if any([request.is_dealer, request.is_manager, request.is_accountant]): - qs = models.ExtraInfo.get_sale_orders(staff=staff, is_dealer=True,dealer=dealer) + qs = models.ExtraInfo.get_sale_orders( + staff=staff, is_dealer=True, dealer=dealer + ) elif request.is_staff: - qs = models.ExtraInfo.get_sale_orders(staff=staff,dealer=dealer) + qs = models.ExtraInfo.get_sale_orders(staff=staff, dealer=dealer) except Exception as e: print(e) - search_query = request.GET.get('q', None) + search_query = request.GET.get("q", None) if search_query: qs = qs.filter( - Q(customer__phone_number__icontains=search_query)| - Q(customer__customer_name__icontains=search_query) + Q(customer__phone_number__icontains=search_query) + | Q(customer__customer_name__icontains=search_query) ).distinct() paginator = Paginator(qs, 30) @@ -4747,11 +4899,13 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): dealer=dealer, content_type=ContentType.objects.get_for_model(EstimateModel), related_content_type=ContentType.objects.get_for_model(models.Staff), - ).union(models.ExtraInfo.objects.filter( - dealer=dealer, - content_type=ContentType.objects.get_for_model(EstimateModel), - related_content_type=ContentType.objects.get_for_model(User), - )) + ).union( + models.ExtraInfo.objects.filter( + dealer=dealer, + content_type=ContentType.objects.get_for_model(EstimateModel), + related_content_type=ContentType.objects.get_for_model(User), + ) + ) elif self.request.is_staff and self.request.is_sales: qs = models.ExtraInfo.objects.filter( @@ -4761,11 +4915,11 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): related_object_id=self.request.staff.pk, ) qs = EstimateModel.objects.filter(pk__in=[x.content_object.pk for x in qs]) - search_query = self.request.GET.get('q', None) + search_query = self.request.GET.get("q", None) if search_query: qs = qs.filter( - Q(estimate_number__icontains=search_query)| - Q(customer__customer_name__icontains=search_query) + Q(estimate_number__icontains=search_query) + | Q(customer__customer_name__icontains=search_query) ).distinct() context["staff_estimates"] = qs return context @@ -4778,12 +4932,12 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): queryset = entity.get_estimates() if status: queryset = queryset.filter(status=status) - search_query = self.request.GET.get('q', None) + search_query = self.request.GET.get("q", None) if search_query: queryset = queryset.filter( - Q(estimate_number__icontains=search_query)| - Q(customer__customer_name__icontains=search_query) + Q(estimate_number__icontains=search_query) + | Q(customer__customer_name__icontains=search_query) ).distinct() return queryset @@ -4820,7 +4974,9 @@ def create_estimate(request, dealer_slug, slug=None): data = json.loads(request.body) title = data.get("title") customer_id = data.get("customer") - customer = models.Customer.objects.filter(pk=int(customer_id),dealer=dealer).first() + customer = models.Customer.objects.filter( + pk=int(customer_id), dealer=dealer + ).first() items = data.get("item", []) quantities = data.get("quantity", []) @@ -4911,7 +5067,9 @@ def create_estimate(request, dealer_slug, slug=None): "quantity": 1, "unit_cost": round(float(i.marked_price)), "unit_revenue": round(float(i.marked_price)), - "total_amount": round(float(i.final_price_plus_vat)),# TODO : check later + "total_amount": round( + float(i.final_price_plus_vat) + ), # TODO : check later } ) @@ -5055,9 +5213,13 @@ def create_estimate(request, dealer_slug, slug=None): ], "opportunity_id": slug if slug else None, "customer_count": entity.get_customers().count(), - "no_items_message": _("Please add at least one car or complete the car info before creating a quotation."), + "no_items_message": _( + "Please add at least one car or complete the car info before creating a quotation." + ), "no_items_button": _("Add car"), - "no_customers_message": _("Please add at least one customer before creating a quotation."), + "no_customers_message": _( + "Please add at least one customer before creating a quotation." + ), "no_customers_button": _("Add Customer"), } @@ -5096,19 +5258,21 @@ class EstimateDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView if estimate.get_itemtxs_data(): # calculator = CarFinanceCalculator(estimate) # finance_data = calculator.get_finance_data() - finance_data = get_finance_data(estimate,dealer) + finance_data = get_finance_data(estimate, dealer) invoice_obj = InvoiceModel.objects.all().filter(ce_model=estimate).first() kwargs["data"] = finance_data - kwargs["customer_obj"]=estimate.customer.customer_set.first() - kwargs['dealer_info']=dealer + kwargs["customer_obj"] = estimate.customer.customer_set.first() + kwargs["dealer_info"] = dealer kwargs["invoice"] = invoice_obj try: car = estimate.get_itemtxs_data()[0].first().item_model.car selected_items = car.additional_services.filter(dealer=dealer) form = forms.AdditionalFinancesForm() - form.fields["additional_finances"].queryset = form.fields["additional_finances"].queryset.filter(dealer=dealer) # + form.fields["additional_finances"].queryset = form.fields[ + "additional_finances" + ].queryset.filter(dealer=dealer) # form.initial["additional_finances"] = selected_items kwargs["additionals_form"] = form except Exception as e: @@ -5123,31 +5287,26 @@ class EstimatePrintView(EstimateDetailView): uses a dedicated, stripped-down print template. """ - - def get(self, request, *args, **kwargs): - self.object = self.get_object() context = self.get_context_data(object=self.object) - # lang = request.GET.get('lang', 'ar') - - if request.GET.get('lang')=='en': + if request.GET.get("lang") == "en": template_path = "sales/estimates/estimate_preview_en.html" else: template_path = "sales/estimates/estimate_preview_ar.html" - html_string = render_to_string(template_path, context) - base_url = request.build_absolute_uri('/') + base_url = request.build_absolute_uri("/") pdf_file = HTML(string=html_string, base_url=base_url).write_pdf() - - response = HttpResponse(pdf_file, content_type='application/pdf') - response['Content-Disposition'] = f'attachment; filename="estimate_{self.object.estimate_number}.pdf"' + response = HttpResponse(pdf_file, content_type="application/pdf") + response["Content-Disposition"] = ( + f'attachment; filename="estimate_{self.object.estimate_number}.pdf"' + ) return response @@ -5203,8 +5362,10 @@ def create_sale_order(request, dealer_slug, pk): f"KeyError: 'car_info' or 'status' key missing when attempting to update status to 'sold' for item.item_model PK: {getattr(item.item_model, 'pk', 'N/A')}." ) pass - item.item_model.car.sold_date=timezone.now() # to be checked added by faheed - item.item_model.car.save()# to be checked added byfaheed + item.item_model.car.sold_date = ( + timezone.now() + ) # to be checked added by faheed + item.item_model.car.save() # to be checked added byfaheed item.item_model.car.mark_as_sold() messages.success(request, "Sale Order created successfully") @@ -5225,7 +5386,7 @@ def create_sale_order(request, dealer_slug, pk): # form.fields["opportunity"].widget = HiddenInput() # calculator = CarFinanceCalculator(estimate) - finance_data = get_finance_data(estimate,dealer) + finance_data = get_finance_data(estimate, dealer) return render( request, "sales/estimates/sale_order_form.html", @@ -5246,14 +5407,21 @@ def update_estimate_discount(request, dealer_slug, pk): # calculator = CarFinanceCalculator(estimate) # finance_data = calculator.get_finance_data() discount_amount = request.POST.get("discount_amount", 0) - finance_data = get_finance_data(estimate,dealer) - car = finance_data.get('car') + finance_data = get_finance_data(estimate, dealer) + car = finance_data.get("car") if Decimal(discount_amount) >= car.marked_price: - messages.error(request, _("Discount amount cannot be greater than marked price")) + messages.error( + request, _("Discount amount cannot be greater than marked price") + ) return redirect("estimate_detail", dealer_slug=dealer_slug, pk=pk) - if Decimal(discount_amount) > car.marked_price * Decimal('0.5'): - messages.warning(request, _("Discount amount is greater than 50% of the marked price, proceed with caution.")) + if Decimal(discount_amount) > car.marked_price * Decimal("0.5"): + messages.warning( + request, + _( + "Discount amount is greater than 50% of the marked price, proceed with caution." + ), + ) else: messages.success(request, _("Discount updated successfully")) extra_info.data.update({"discount": Decimal(discount_amount)}) @@ -5270,9 +5438,7 @@ def update_estimate_additionals(request, dealer_slug, pk): if form.is_valid(): estimate = get_object_or_404(EstimateModel, pk=pk) car = estimate.get_itemtxs_data()[0].first().item_model.car - car.additional_services.set( - form.cleaned_data["additional_finances"] - ) + car.additional_services.set(form.cleaned_data["additional_finances"]) car.save() messages.success(request, "Additional Finances updated successfully") return redirect("estimate_detail", dealer_slug=dealer_slug, pk=pk) @@ -5298,7 +5464,7 @@ class SaleOrderDetail(LoginRequiredMixin, PermissionRequiredMixin, DetailView): if estimate.get_itemtxs_data(): # calculator = CarFinanceCalculator(estimate) # finance_data = calculator.get_finance_data() - finance_data = get_finance_data(estimate,dealer) + finance_data = get_finance_data(estimate, dealer) kwargs["data"] = finance_data return super().get_context_data(**kwargs) @@ -5396,10 +5562,9 @@ class EstimatePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailVie def get_context_data(self, **kwargs): estimate = kwargs.get("object") if estimate.get_itemtxs_data(): - # data = get_financial_values(estimate) # calculator = CarFinanceCalculator(estimate) - kwargs["data"] = get_finance_data(estimate,self.request.dealer) + kwargs["data"] = get_finance_data(estimate, self.request.dealer) return super().get_context_data(**kwargs) @@ -5529,7 +5694,7 @@ class InvoiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) entity = dealer.entity staff = getattr(self.request.user, "staff", None) - qs=None + qs = None try: if any( [ @@ -5538,9 +5703,11 @@ class InvoiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): self.request.is_accountant, ] ): - qs = models.ExtraInfo.get_invoices(staff=staff, is_dealer=True,dealer=dealer) + qs = models.ExtraInfo.get_invoices( + staff=staff, is_dealer=True, dealer=dealer + ) elif self.request.is_staff: - qs = models.ExtraInfo.get_invoices(staff=staff,dealer=dealer) + qs = models.ExtraInfo.get_invoices(staff=staff, dealer=dealer) except Exception as e: print(e) @@ -5581,7 +5748,7 @@ class InvoiceDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView) if invoice.get_itemtxs_data(): # calculator = CarFinanceCalculator(invoice) # finance_data = calculator.get_finance_data() - finance_data = get_finance_data(invoice,self.request.dealer) + finance_data = get_finance_data(invoice, self.request.dealer) kwargs["data"] = finance_data kwargs["payments"] = JournalEntryModel.objects.filter( ledger=invoice.ledger @@ -5677,7 +5844,11 @@ class ApprovedInvoiceModelUpdateFormView( def get_success_url(self): return reverse_lazy( "invoice_detail", - kwargs={"dealer_slug": self.kwargs["dealer_slug"],"entity_slug": self.kwargs["entity_slug"], "pk": self.object.pk}, + kwargs={ + "dealer_slug": self.kwargs["dealer_slug"], + "entity_slug": self.kwargs["entity_slug"], + "pk": self.object.pk, + }, ) @@ -5725,7 +5896,11 @@ class PaidInvoiceModelUpdateFormView( def get_success_url(self): return reverse_lazy( "invoice_detail", - kwargs={"dealer_slug": self.kwargs["dealer_slug"],"entity_slug": self.kwargs["entity_slug"], "pk": self.object.pk}, + kwargs={ + "dealer_slug": self.kwargs["dealer_slug"], + "entity_slug": self.kwargs["entity_slug"], + "pk": self.object.pk, + }, ) def form_valid(self, form): @@ -5733,7 +5908,12 @@ class PaidInvoiceModelUpdateFormView( if invoice.get_amount_open() > 0: messages.error(self.request, "Invoice is not fully paid") - return redirect("invoice_detail",dealer_slug=self.kwargs["dealer_slug"],entity_slug=self.kwargs["entity_slug"], pk=invoice.pk) + return redirect( + "invoice_detail", + dealer_slug=self.kwargs["dealer_slug"], + entity_slug=self.kwargs["entity_slug"], + pk=invoice.pk, + ) else: invoice.post_ledger() invoice.save() @@ -5765,12 +5945,22 @@ def invoice_mark_as(request, dealer_slug, pk): if mark and mark == "accept": if not invoice.can_approve(): messages.error(request, "invoice is not ready for approval") - return redirect("invoice_detail", dealer_slug=dealer_slug,entity_slug=request.entity.slug, pk=invoice.pk) + return redirect( + "invoice_detail", + dealer_slug=dealer_slug, + entity_slug=request.entity.slug, + pk=invoice.pk, + ) invoice.mark_as_approved( entity_slug=dealer.entity.slug, user_model=dealer.entity.admin ) invoice.save() - return redirect("invoice_detail", dealer_slug=dealer_slug,entity_slug=request.entity.slug, pk=invoice.pk) + return redirect( + "invoice_detail", + dealer_slug=dealer_slug, + entity_slug=request.entity.slug, + pk=invoice.pk, + ) @login_required @@ -5810,7 +6000,7 @@ def invoice_create(request, dealer_slug, pk): # calculator = CarFinanceCalculator(estimate) # finance_data = calculator.get_finance_data() - finance_data = get_finance_data(estimate,dealer) + finance_data = get_finance_data(estimate, dealer) car = finance_data.get("car") invoice_itemtxs = { car.item_model.item_number: { @@ -5834,7 +6024,12 @@ def invoice_create(request, dealer_slug, pk): estimate.save() invoice.save() messages.success(request, "Invoice created successfully") - return redirect("invoice_detail", dealer_slug=dealer.slug,entity_slug=entity.slug, pk=invoice.pk) + return redirect( + "invoice_detail", + dealer_slug=dealer.slug, + entity_slug=entity.slug, + pk=invoice.pk, + ) else: print(form.errors) form = forms.InvoiceModelCreateForm( @@ -5885,45 +6080,44 @@ class InvoicePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailView invoice = kwargs.get("object") if invoice.get_itemtxs_data(): # calculator = CarFinanceCalculator(invoice) - finance_data = get_finance_data(invoice,dealer) + finance_data = get_finance_data(invoice, dealer) kwargs["data"] = finance_data kwargs["dealer_info"] = dealer - kwargs["customer_obj"]=invoice.customer.customer_set.first() + kwargs["customer_obj"] = invoice.customer.customer_set.first() return super().get_context_data(**kwargs) - def get(self, request, *args, **kwargs): + def get(self, request, *args, **kwargs): self.object = self.get_object() context = self.get_context_data(object=self.object) - # lang = request.GET.get('lang', 'ar') - - if request.GET.get('lang')=='en': + if request.GET.get("lang") == "en": template_path = "sales/invoices/invoice_preview_en.html" - elif request.GET.get('lang')=='ar': + elif request.GET.get("lang") == "ar": template_path = "sales/invoices/invoice_preview_ar.html" else: # just for preview not for download - return render(request,'sales/invoices/invoice_preview.html',context) - + return render(request, "sales/invoices/invoice_preview.html", context) html_string = render_to_string(template_path, context) - base_url = request.build_absolute_uri('/') + base_url = request.build_absolute_uri("/") pdf_file = HTML(string=html_string, base_url=base_url).write_pdf() - - response = HttpResponse(pdf_file, content_type='application/pdf') - response['Content-Disposition'] = f'attachment; filename="invoice_{self.object.invoice_number}.pdf"' + response = HttpResponse(pdf_file, content_type="application/pdf") + response["Content-Disposition"] = ( + f'attachment; filename="invoice_{self.object.invoice_number}.pdf"' + ) return response # payments + class InvoiceModelUpdateView(InvoiceModelUpdateViewBase): - template_name = 'sales/invoices/invoice_update.html' + template_name = "sales/invoices/invoice_update.html" permission_required = ["django_ledger.change_invoicemodel"] @@ -5949,6 +6143,7 @@ class InvoiceModelUpdateView(InvoiceModelUpdateViewBase): # context = { "invoice": invoice, "form": form } # return render(request, "sales/payments/payment_form1.html", context) + @login_required @permission_required("inventory.add_payment", raise_exception=True) def PaymentCreateView(request, dealer_slug, pk): @@ -5995,7 +6190,12 @@ def PaymentCreateView(request, dealer_slug, pk): invoice = form.cleaned_data.get("invoice") # bill = form.cleaned_data.get("bill") payment_method = form.cleaned_data.get("payment_method") - response = redirect("invoice_detail", dealer_slug=dealer.slug,entity_slug=entity.slug, pk=model.pk)# if invoice else "bill_detail" + response = redirect( + "invoice_detail", + dealer_slug=dealer.slug, + entity_slug=entity.slug, + pk=model.pk, + ) # if invoice else "bill_detail" # model = invoice if invoice else bill if not model.is_approved(): @@ -6175,7 +6375,12 @@ def payment_mark_as_paid(request, dealer_slug, pk): exc_info=True, ) messages.error(request, f"Error: {str(e)}") - return redirect("invoice_detail", dealer_slug=dealer_slug,entity_slug=request.entity.slug, pk=invoice.pk) + return redirect( + "invoice_detail", + dealer_slug=dealer_slug, + entity_slug=request.entity.slug, + pk=invoice.pk, + ) # activity log @@ -6267,9 +6472,10 @@ class LeadListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): if self.request.is_staff: return qs.filter(staff=self.request.staff) return models.Lead.objects.none() + def get_context_data(self, **kwargs): - context=super().get_context_data(**kwargs) - context['empty_state_value']=_("lead") + context = super().get_context_data(**kwargs) + context["empty_state_value"] = _("lead") return context @@ -6384,8 +6590,7 @@ def lead_create(request, dealer_slug): if instance.lead_type == "customer": customer = models.Customer.objects.filter( - dealer=dealer, - email=instance.email + dealer=dealer, email=instance.email ).first() if not customer: customer = models.Customer( @@ -6404,8 +6609,7 @@ def lead_create(request, dealer_slug): if instance.lead_type == "organization": organization = models.Organization.objects.filter( - dealer=dealer, - email=instance.email + dealer=dealer, email=instance.email ).first() if not organization: organization = models.Organization( @@ -6426,7 +6630,9 @@ def lead_create(request, dealer_slug): f"lead created successfully for dealer {dealer_slug} by user:{user_username}" ) messages.success(request, _("Lead created successfully")) - return redirect("lead_detail",dealer_slug=dealer_slug,slug=instance.slug) + return redirect( + "lead_detail", dealer_slug=dealer_slug, slug=instance.slug + ) else: logger.error( f"error creating leading for dealer {dealer_slug} by user:{user_username}" @@ -6444,13 +6650,11 @@ def lead_create(request, dealer_slug): form.filter_qs(dealer=dealer) if make := request.GET.get("id_car_make", None): - qs = models.CarModel.objects.filter( - id_car_make=int(make) - ) + qs = models.CarModel.objects.filter(id_car_make=int(make)) form.fields["id_car_model"].queryset = qs form.fields["id_car_model"].choices = [ (obj.id_car_model, obj.get_local_name()) for obj in qs - ] + ] else: dealer_make_list = models.DealersMake.objects.filter(dealer=dealer).values_list( @@ -6508,7 +6712,7 @@ def lead_tracking(request, dealer_slug): qs = models.Lead.objects.filter(dealer=dealer, staff=staff) else: qs = models.Lead.objects.filter(dealer=dealer) - leads=qs + leads = qs won = qs.filter(status="won") new = qs.filter(status="new") lose = qs.filter(status="lose") @@ -6521,7 +6725,7 @@ def lead_tracking(request, dealer_slug): "won": won, "lose": lose, "negotiation": negotiation, - "leads":leads, + "leads": leads, "empty_state_value": _("lead"), } return render(request, "crm/leads/lead_tracking.html", context) @@ -6553,10 +6757,7 @@ def update_lead_actions(request, dealer_slug): f"User {user_username} submitted incomplete data to update lead actions " f"for dealer '{dealer_slug}'. Missing fields: lead_id='{lead_id}', current_action='{current_action}', next_action='{next_action}'." ) - messages.error( - request, - _("All fields are required") - ) + messages.error(request, _("All fields are required")) return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug) # return JsonResponse( # {"success": False, "message": "All fields are required"}, status=400 @@ -6564,7 +6765,6 @@ def update_lead_actions(request, dealer_slug): # Get the lead - # Update lead fields lead.status = current_action @@ -6593,10 +6793,7 @@ def update_lead_actions(request, dealer_slug): f"submitted invalid date format ('{next_action_date}') " f"for Lead ID: {lead.pk}. Error: {ve}" ) - messages.error( - request, - _("Invalid date format") - ) + messages.error(request, _("Invalid date format")) return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug) # return JsonResponse( # {"success": False, "message": "Invalid date format"}, status=400 @@ -6608,10 +6805,7 @@ def update_lead_actions(request, dealer_slug): f"User {user_username} successfully updated Lead ID: {lead.pk} ('{lead.slug}'). " f"New Status: '{lead.status}', Next Action: '{lead.next_action}', Next Action Date: '{lead.next_action_date}'." ) - messages.success( - request, - _("Actions updated successfully") - ) + messages.success(request, _("Actions updated successfully")) return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug) # return JsonResponse( # {"success": True, "message": "Actions updated successfully"} @@ -6623,10 +6817,7 @@ def update_lead_actions(request, dealer_slug): f"User {user_username} attempted to update non-existent Lead with ID: '{lead_id}' " f"for dealer '{dealer_slug}'. Returning 404." ) - messages.error( - request, - _("Lead not found") - ) + messages.error(request, _("Lead not found")) return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug) # return JsonResponse({"success": False, "message": "Lead not found"}, status=404) except Exception as e: @@ -6636,27 +6827,23 @@ def update_lead_actions(request, dealer_slug): f"for dealer '{dealer_slug}'. Error: {e}", exc_info=True, # CRUCIAL: Includes the full traceback ) - messages.error( - request, - _("An error occurred while updating lead actions") - ) + messages.error(request, _("An error occurred while updating lead actions")) return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug) # return JsonResponse({"success": False, "message": str(e)}, status=500) -def lead_update(request,dealer_slug,slug): + +def lead_update(request, dealer_slug, slug): dealer = get_object_or_404(models.Dealer, slug=dealer_slug) lead = get_object_or_404(models.Lead, slug=slug) form = forms.LeadForm(instance=lead) if "HX-Request" in request.headers: make_id = request.GET.get("id_car_make") make = models.CarMake.objects.get(pk=make_id) - form.fields[ - "id_car_model" - ].queryset = make.carmodel_set.all() + form.fields["id_car_model"].queryset = make.carmodel_set.all() else: form.fields[ - "id_car_model" - ].queryset = form.instance.id_car_make.carmodel_set.all() + "id_car_model" + ].queryset = form.instance.id_car_make.carmodel_set.all() form.fields["staff"].queryset = ( form.fields["staff"] .queryset.select_related("user") @@ -6666,10 +6853,9 @@ def lead_update(request,dealer_slug,slug): ) .distinct() ) - context = { - "form":form - } - return render(request,"crm/leads/lead_form.html",context) + context = {"form": form} + return render(request, "crm/leads/lead_form.html", context) + class LeadUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): """ @@ -6882,10 +7068,10 @@ def delete_note(request, dealer_slug, pk): note.delete() messages.success(request, _("Note deleted successfully.")) except Exception as e: - print("Errroooorrr: ",e) + print("Errroooorrr: ", e) print(url) print(dealer_slug) - return redirect(url, dealer_slug=dealer_slug,slug=slug) + return redirect(url, dealer_slug=dealer_slug, slug=slug) @login_required @@ -6976,7 +7162,7 @@ def schedule_event(request, dealer_slug, content_type, slug): form = forms.ScheduleForm(request.POST) if form.is_valid(): - reminder = form.cleaned_data['reminder'] + reminder = form.cleaned_data["reminder"] instance = form.save(commit=False) instance.dealer = dealer instance.content_object = obj @@ -7024,7 +7210,13 @@ def schedule_event(request, dealer_slug, content_type, slug): activity_type=instance.scheduled_type, ) if reminder: - scheduled_at_aware = timezone.make_aware(instance.scheduled_at, timezone.get_current_timezone()) if timezone.is_naive(instance.scheduled_at) else instance.scheduled_at + scheduled_at_aware = ( + timezone.make_aware( + instance.scheduled_at, timezone.get_current_timezone() + ) + if timezone.is_naive(instance.scheduled_at) + else instance.scheduled_at + ) reminder_time = scheduled_at_aware - timezone.timedelta(minutes=15) # Only schedule if the reminder time is in the future @@ -7032,15 +7224,17 @@ def schedule_event(request, dealer_slug, content_type, slug): if reminder_time > timezone.now(): DjangoQSchedule.objects.create( name=f"send_schedule_reminder_email_to_{instance.scheduled_by.email}_for_{content_type}_with_PK_{instance.pk}", - func='inventory.tasks.send_schedule_reminder_email', + func="inventory.tasks.send_schedule_reminder_email", args=f'"{instance.pk}"', schedule_type=DjangoQSchedule.ONCE, next_run=reminder_time, - hook='inventory.tasks.log_email_status', + hook="inventory.tasks.log_email_status", ) messages.success(request, _("Appointment Created Successfully")) - return redirect(f'{content_type}_detail',dealer_slug=dealer_slug, slug=slug) + return redirect( + f"{content_type}_detail", dealer_slug=dealer_slug, slug=slug + ) else: # Log for invalid form data @@ -7080,7 +7274,7 @@ def lead_transfer(request, dealer_slug, slug): messages.success(request, _("Lead transferred successfully")) else: messages.error(request, f"Invalid form data: {str(form.errors)}") - return redirect("lead_detail", dealer_slug=dealer.slug ,slug=lead.slug) + return redirect("lead_detail", dealer_slug=dealer.slug, slug=lead.slug) @login_required @@ -7166,7 +7360,7 @@ def send_lead_email(request, dealer_slug, slug, email_pk=None): # f"Lead's opportunity does not exist. Redirecting to lead list." # ) # return response - # return redirect("lead_list", dealer_slug=dealer.slug) + # return redirect("lead_list", dealer_slug=dealer.slug) if request.method == "POST": email_pk = request.POST.get("email_pk") @@ -7221,7 +7415,7 @@ def send_lead_email(request, dealer_slug, slug, email_pk=None): # f"Lead's opportunity does not exist. Redirecting to lead list." # ) # return response - # return redirect("lead_list", dealer_slug=dealer_slug) + # return redirect("lead_list", dealer_slug=dealer_slug) msg = f""" السلام عليكم {lead.full_name}, @@ -7321,9 +7515,7 @@ class OpportunityCreateView( dealer=dealer, status="available", marked_price__gt=0 ) if self.request.is_dealer: - form.fields["lead"].queryset = models.Lead.objects.filter( - dealer=dealer - ) + form.fields["lead"].queryset = models.Lead.objects.filter(dealer=dealer) elif self.request.is_staff: form.fields["lead"].queryset = models.Lead.objects.filter( dealer=dealer, staff=self.request.staff @@ -7371,13 +7563,12 @@ class OpportunityUpdateView( success_message = _("Opportunity updated successfully.") permission_required = ["inventory.change_opportunity"] - def get_form(self, form_class=None): form = super().get_form(form_class) dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug")) staff = getattr(self.request.user, "staff", None) form.fields["car"].queryset = models.Car.objects.filter( - dealer=dealer, status="available",marked_price__gt=0 + dealer=dealer, status="available", marked_price__gt=0 ) form.fields["lead"].queryset = models.Lead.objects.filter( dealer=dealer, staff=staff @@ -7393,6 +7584,7 @@ class OpportunityUpdateView( }, ) + class OpportunityStageUpdateView( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView ): @@ -7423,7 +7615,6 @@ class OpportunityStageUpdateView( success_message = _("Opportunity Stage updated successfully.") permission_required = ["inventory.change_opportunity"] - def get_success_url(self): return reverse_lazy( "opportunity_detail", @@ -7545,9 +7736,9 @@ class OpportunityListView(LoginRequiredMixin, PermissionRequiredMixin, ListView) queryset = models.Opportunity.objects.filter(dealer=dealer) elif self.request.is_staff: staff = self.request.staff - queryset = models.Opportunity.objects.filter(dealer=dealer, lead__staff=staff) - - + queryset = models.Opportunity.objects.filter( + dealer=dealer, lead__staff=staff + ) # Stage filter stage = self.request.GET.get("stage") @@ -7564,7 +7755,7 @@ class OpportunityListView(LoginRequiredMixin, PermissionRequiredMixin, ListView) elif sort == "closing": queryset = queryset.order_by("expected_close_date") - # Search filter + # Search filter search = self.request.GET.get("q") if search: queryset = queryset.filter( @@ -7682,14 +7873,13 @@ class NotificationListView(LoginRequiredMixin, ListView): return models.Notification.objects.filter(user=self.request.user) def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) user_notifications = self.get_queryset() # Calculate the number of total, read and unread notifications - context['total_count'] = user_notifications.count() - context['read_count'] = user_notifications.filter(is_read=True).count() - context['unread_count'] = user_notifications.filter(is_read=False).count() + context["total_count"] = user_notifications.count() + context["read_count"] = user_notifications.filter(is_read=True).count() + context["unread_count"] = user_notifications.filter(is_read=False).count() return context @@ -7832,12 +8022,14 @@ class ItemServiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView) query = self.request.GET.get("q") qs = models.AdditionalServices.objects.filter(dealer=dealer).all() if query: - qs = qs.filter(Q(name__icontains=query)| - Q(id__icontains=query)| - Q(uom__icontains=query) - ) + qs = qs.filter( + Q(name__icontains=query) + | Q(id__icontains=query) + | Q(uom__icontains=query) + ) return qs + class ItemServiceDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): model = models.AdditionalServices template_name = "items/service/service_detail.html" @@ -7845,13 +8037,19 @@ class ItemServiceDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV permission_required = ["inventory.view_additionalservices"] def get_context_data(self, **kwargs): - context=super().get_context_data(**kwargs) - sold_cars=models.Car.objects.filter(status='sold',) - context['total_services_price']=self.object.price*self.object.additionals.filter(status='sold').count() + context = super().get_context_data(**kwargs) + sold_cars = models.Car.objects.filter( + status="sold", + ) + context["total_services_price"] = ( + self.object.price * self.object.additionals.filter(status="sold").count() + ) return context -class ItemExpenseCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView): +class ItemExpenseCreateView( + LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView +): """ Represents a view for creating item expense entries. @@ -7942,9 +8140,6 @@ class ItemExpenseUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateV ) - - - class ItemExpenseListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): """ Handles the display of a list of item expenses. @@ -7970,7 +8165,7 @@ class ItemExpenseListView(LoginRequiredMixin, PermissionRequiredMixin, ListView) model = ItemModel template_name = "items/expenses/expenses_list.html" context_object_name = "expenses" - paginate_by =20 + paginate_by = 20 permission_required = ["django_ledger.view_itemmodel"] def get_queryset(self): @@ -7983,7 +8178,7 @@ class ItemExpenseListView(LoginRequiredMixin, PermissionRequiredMixin, ListView) class ItemExpenseDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): - queryset=ItemModel.objects.filter(item_role='expense') + queryset = ItemModel.objects.filter(item_role="expense") template_name = "items/expenses/expense_detail.html" context_object_name = "expense" permission_required = ["django_ledger.view_itemmodel"] @@ -7991,23 +8186,19 @@ class ItemExpenseDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # Get the related bills queryset - bills_list = self.object.billmodel_set.all().order_by('-created') + bills_list = self.object.billmodel_set.all().order_by("-created") # Paginate the bills paginator = Paginator(bills_list, 10) # Show 10 bills per page - page_number = self.request.GET.get('page') + page_number = self.request.GET.get("page") page_obj = paginator.get_page(page_number) # Add the paginated bills to the context - context['page_obj'] = page_obj + context["page_obj"] = page_obj context["entity"] = get_user_type(self.request).entity return context - - - - class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): """ Provides a view for listing bills. @@ -8030,7 +8221,7 @@ class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): model = BillModel template_name = "ledger/bills/bill_list.html" context_object_name = "bills" - paginate_by=20 + paginate_by = 20 permission_required = ["django_ledger.view_billmodel"] def get_queryset(self): @@ -8038,8 +8229,10 @@ class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): qs = dealer.entity.get_bills() query = self.request.GET.get("q") if query: - qs = qs.filter(Q(bill_number__icontains=query)| - Q(vendor__vendor_name__icontains=query)) + qs = qs.filter( + Q(bill_number__icontains=query) + | Q(vendor__vendor_name__icontains=query) + ) return qs def get_context_data(self, **kwargs): @@ -8048,7 +8241,9 @@ class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): return context -class BillModelCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView): +class BillModelCreateView( + LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView +): template_name = "bill/bill_create.html" PAGE_TITLE = _("Create Bill") permission_required = "django_ledger.add_billmodel" @@ -8180,9 +8375,24 @@ class BillModelCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMes # form = super().get_form(form_class) dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) form = BillModelCreateForm(entity_model=dealer.entity, **self.get_form_kwargs()) - form.initial['prepaid_account'] = models.DealerSettings.objects.filter(dealer=dealer).first().bill_prepaid_account or None - form.initial['unearned_account'] = models.DealerSettings.objects.filter(dealer=dealer).first().bill_unearned_account or None - form.initial['cash_account'] = models.DealerSettings.objects.filter(dealer=dealer).first().bill_cash_account or None + form.initial["prepaid_account"] = ( + models.DealerSettings.objects.filter(dealer=dealer) + .first() + .bill_prepaid_account + or None + ) + form.initial["unearned_account"] = ( + models.DealerSettings.objects.filter(dealer=dealer) + .first() + .bill_unearned_account + or None + ) + form.initial["cash_account"] = ( + models.DealerSettings.objects.filter(dealer=dealer) + .first() + .bill_cash_account + or None + ) return form def form_valid(self, form): @@ -8286,6 +8496,7 @@ class BillModelCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMes "bill_pk": bill_model.uuid, }, ) + def get_queryset(self): qs = super().get_queryset() return qs.select_related( @@ -8300,6 +8511,7 @@ class BillModelCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMes "unearned_account__coa_model", ) + class BillModelDetailView(BillModelDetailViewBase): template_name = "bill/bill_detail.html" permission_required = ["django_ledger.view_billmodel"] @@ -9350,7 +9562,9 @@ def DealerSettingsView(request, slug): form = forms.DealerSettingsForm(instance=dealer_setting, initial={"dealer": dealer}) form.fields[ "invoice_cash_account" - ].queryset = dealer.entity.get_default_coa_accounts().filter(role=roles.ASSET_CA_CASH) + ].queryset = dealer.entity.get_default_coa_accounts().filter( + role=roles.ASSET_CA_CASH + ) form.fields[ "invoice_prepaid_account" ].queryset = dealer.entity.get_default_coa_accounts().filter( @@ -9373,21 +9587,23 @@ def DealerSettingsView(request, slug): ) form.fields[ "invoice_cost_of_good_sold_account" - ].queryset = dealer.entity.get_default_coa_accounts().filter( - role=roles.COGS - ) + ].queryset = dealer.entity.get_default_coa_accounts().filter(role=roles.COGS) form.fields[ "invoice_inventory_account" ].queryset = dealer.entity.get_default_coa_accounts().filter( role=roles.ASSET_CA_INVENTORY ) - form.fields["bill_cash_account"].queryset = dealer.entity.get_default_coa_accounts().filter( + form.fields[ + "bill_cash_account" + ].queryset = dealer.entity.get_default_coa_accounts().filter( role=roles.ASSET_CA_CASH ) form.fields[ "bill_prepaid_account" - ].queryset = dealer.entity.get_default_coa_accounts().filter(role=roles.ASSET_CA_PREPAID) + ].queryset = dealer.entity.get_default_coa_accounts().filter( + role=roles.ASSET_CA_PREPAID + ) form.fields[ "bill_unearned_account" ].queryset = dealer.entity.get_default_coa_accounts().filter( @@ -9423,7 +9639,6 @@ def schedule_cancel(request, dealer_slug, pk): @login_required @permission_required("inventory.change_dealer", raise_exception=True) def assign_car_makes(request, dealer_slug): - """ Assigns car makes to a dealer. @@ -9551,7 +9766,7 @@ class LedgerModelDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV permission_required = "django_ledger.view_ledgermodel" -class LedgerModelCreateView(LedgerModelCreateViewBase,SuccessMessageMixin): +class LedgerModelCreateView(LedgerModelCreateViewBase, SuccessMessageMixin): """ Handles the creation of LedgerModel entities. @@ -9615,16 +9830,17 @@ class LedgerModelModelActionView(LedgerModelModelActionViewBase): ) - @login_required @permission_required("django_ledger.delete_ledgermodel", raise_exception=True) -def LedgerModelDeleteView(request, dealer_slug,entity_slug,ledger_pk): +def LedgerModelDeleteView(request, dealer_slug, entity_slug, ledger_pk): ledger = LedgerModel.objects.filter(pk=ledger_pk).first() if request.method == "POST": ledger.delete() messages.success(request, _("Ledger deleted successfully")) return redirect("ledger_list", dealer_slug=dealer_slug, entity_slug=entity_slug) - return render(request,"ledger/ledger/ledger_delete.html",{"ledger_model":ledger}) + return render(request, "ledger/ledger/ledger_delete.html", {"ledger_model": ledger}) + + # class LedgerModelDeleteView(DeleteView, SuccessMessageMixin): # """ # Handles the deletion of a Ledger model instance. @@ -9749,7 +9965,7 @@ class JournalEntryCreateView( @login_required @permission_required("django_ledger.delete_journalentrymodel", raise_exception=True) -def JournalEntryDeleteView(request,dealer_slug, pk): +def JournalEntryDeleteView(request, dealer_slug, pk): """ Handles the deletion of a specific journal entry. This view facilitates the deletion of a journal entry identified by its primary key (pk). If the @@ -9770,10 +9986,10 @@ def JournalEntryDeleteView(request,dealer_slug, pk): ledger = journal_entry.ledger if not journal_entry.can_delete(): messages.error(request, _("Journal Entry cannot be deleted")) - return redirect("journalentry_list",dealer_slug=dealer_slug, pk=ledger.pk) + return redirect("journalentry_list", dealer_slug=dealer_slug, pk=ledger.pk) journal_entry.delete() messages.success(request, "Journal Entry deleted") - return redirect("journalentry_list",dealer_slug=dealer_slug, pk=ledger.pk) + return redirect("journalentry_list", dealer_slug=dealer_slug, pk=ledger.pk) return render( request, "ledger/journal_entry/journal_entry_delete.html", @@ -9962,20 +10178,25 @@ def ledger_unpost_all_journals(request, dealer_slug, entity_slug, pk): @login_required @permission_required("inventory.change_dealer", raise_exception=True) def pricing_page(request, dealer_slug): - dealer=get_object_or_404(models.Dealer, slug=dealer_slug) + dealer = get_object_or_404(models.Dealer, slug=dealer_slug) vat = models.VatRate.objects.filter(dealer=dealer).first() now = datetime.now().date() + timedelta(days=15) - if not hasattr(dealer.user,'userplan') or dealer.is_plan_expired or dealer.user.userplan.expire <= now: - plan_list = PlanPricing.objects.annotate( - price_with_tax=Round(F('price') * vat.rate + F('price'), 2) - ).all() + if ( + not hasattr(dealer.user, "userplan") + or dealer.is_plan_expired + or dealer.user.userplan.expire <= now + ): + plan_list = PlanPricing.objects.annotate( + price_with_tax=Round(F("price") * vat.rate + F("price"), 2) + ).all() - form = forms.PaymentPlanForm() - return render(request, "pricing_page.html", {"plan_list": plan_list, "form": form}) + form = forms.PaymentPlanForm() + return render( + request, "pricing_page.html", {"plan_list": plan_list, "form": form} + ) else: - messages.info(request,_("You already have an plan!!")) - return redirect('home',dealer_slug=dealer_slug) - + messages.info(request, _("You already have an plan!!")) + return redirect("home", dealer_slug=dealer_slug) # @login_required @@ -10024,11 +10245,11 @@ def submit_plan(request, dealer_slug): return redirect("pricing_page", dealer_slug=dealer_slug) # Store plan & dealer info in session for use in callback - request.session['pending_plan_id'] = selected_plan_id - request.session['pending_dealer_slug'] = dealer_slug + request.session["pending_plan_id"] = selected_plan_id + request.session["pending_dealer_slug"] = dealer_slug # Initiate payment WITHOUT creating order - transaction_url,error = handle_payment(request, dealer) + transaction_url, error = handle_payment(request, dealer) if not transaction_url: messages.error(request, _(f"Payment initiation failed. {error}")) return redirect("pricing_page", dealer_slug=dealer_slug) @@ -10042,19 +10263,25 @@ def payment_callback(request, dealer_slug): payment_status = request.GET.get("status") message = request.GET.get("message", "") - logger.info(f"Received payment callback for dealer_slug: {dealer_slug}, payment_id: {payment_id}, status: {payment_status}") + logger.info( + f"Received payment callback for dealer_slug: {dealer_slug}, payment_id: {payment_id}, status: {payment_status}" + ) history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first() if not history: logger.error(f"No PaymentHistory found for transaction_id: {payment_id}") - return render(request, "payment_failed.html", {"message": "Invalid transaction"}) + return render( + request, "payment_failed.html", {"message": "Invalid transaction"} + ) if history.status == "paid": logger.info("Payment already processed. Redirecting to home.") - return redirect('home') + return redirect("home") if payment_status == "paid": - logger.info(f"Payment successful for transaction ID {payment_id}. Creating order...") + logger.info( + f"Payment successful for transaction ID {payment_id}. Creating order..." + ) # Get metadata from PaymentHistory (passed during handle_payment) metadata = history.user_data @@ -10071,7 +10298,9 @@ def payment_callback(request, dealer_slug): logger.error("Invalid metadata in payment callback") history.status = "failed" history.save() - return render(request, "payment_failed.html", {"message": "Invalid payment data"}) + return render( + request, "payment_failed.html", {"message": "Invalid payment data"} + ) dealer = get_object_or_404(models.Dealer, slug=dealer_slug) pp = get_object_or_404(PlanPricing, pk=plan_pricing_id) @@ -10092,19 +10321,21 @@ def payment_callback(request, dealer_slug): logger.exception(f"Failed to create order: {e}") history.status = "failed" history.save() - return render(request, "payment_failed.html", {"message": "Order creation failed"}) + return render( + request, "payment_failed.html", {"message": "Order creation failed"} + ) # Create or get BillingInfo billing_info, created = BillingInfo.objects.get_or_create( user=dealer.user, defaults={ - 'tax_number': dealer.vrn, - 'name': dealer.arabic_name, - 'street': dealer.address, - 'zipcode': dealer.entity.zip_code or " ", - 'city': dealer.entity.city or " ", - 'country': dealer.entity.country or " ", - } + "tax_number": dealer.vrn, + "name": dealer.arabic_name, + "street": dealer.address, + "zipcode": dealer.entity.zip_code or " ", + "city": dealer.entity.city or " ", + "country": dealer.entity.country or " ", + }, ) if created: logger.info(f"Created new billing info for user {dealer.user}.") @@ -10112,13 +10343,15 @@ def payment_callback(request, dealer_slug): logger.debug(f"Billing info already exists for user {dealer.user}.") # Create or update UserPlan - if not hasattr(order.user, 'userplan'): + if not hasattr(order.user, "userplan"): UserPlan.objects.create( user=order.user, plan=order.plan, # expire=datetime.now().date() + timedelta(days=order.get_plan_pricing().pricing.period) ) - logger.info(f"Created new UserPlan for user {order.user} with plan {order.plan}.") + logger.info( + f"Created new UserPlan for user {order.user} with plan {order.plan}." + ) else: # Optional: upgrade existing plan # user_plan = order.user.userplan @@ -10137,16 +10370,16 @@ def payment_callback(request, dealer_slug): logger.info(f"Order {order.id} completed. Rendering success page.") return render( - request, - "payment_success.html", - {"order": order, "invoice": invoice} + request, "payment_success.html", {"order": order, "invoice": invoice} ) except Exception as e: logger.exception(f"Error completing order {order.id}: {e}") history.status = "failed" history.save() - return render(request, "payment_failed.html", {"message": "Plan activation error"}) + return render( + request, "payment_failed.html", {"message": "Plan activation error"} + ) finally: # Activate dealer & staff if needed @@ -10159,12 +10392,16 @@ def payment_callback(request, dealer_slug): staff.activate_account() elif payment_status == "failed": - logger.warning(f"Payment failed for transaction ID {payment_id}. Message: {message}") + logger.warning( + f"Payment failed for transaction ID {payment_id}. Message: {message}" + ) history.status = "failed" history.save() return render(request, "payment_failed.html", {"message": message}) return render(request, "payment_failed.html", {"message": "Unknown payment status"}) + + # @login_required # @permission_required("inventory.change_dealer", raise_exception=True) # def payment_callback(request, dealer_slug): @@ -10289,9 +10526,11 @@ def payment_callback(request, dealer_slug): # return render(request, "payment_failed.html", {"message": message}) + @login_required async def sse_stream(request): import asyncio + def event_generator(): last_id = int(request.GET.get("last_id", 0)) @@ -10299,11 +10538,13 @@ async def sse_stream(request): async def fetch_notifications(): while True: # 🔥 Fully async ORM query - notifications = models.Notification.objects.filter( - user=request.user, - id__gt=last_id, - is_read=False - ).order_by("created").values("id", "message", "created") + notifications = ( + models.Notification.objects.filter( + user=request.user, id__gt=last_id, is_read=False + ) + .order_by("created") + .values("id", "message", "created") + ) # 🔥 Async iteration over queryset async for notification in notifications: @@ -10333,7 +10574,7 @@ async def sse_stream(request): "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no", - } + }, ) @@ -10485,7 +10726,6 @@ def add_task(request, dealer_slug, content_type, slug): return redirect(f"{content_type}_detail", dealer_slug=dealer_slug, slug=slug) - @login_required @permission_required("inventory.change_tasks", raise_exception=True) def update_task(request, dealer_slug, pk): @@ -10600,10 +10840,12 @@ def management_view(request, dealer_slug): def user_management(request, dealer_slug): dealer = get_object_or_404(models.Dealer, slug=dealer_slug) context = { - "customers": models.Customer.objects.filter(active=False,dealer=dealer), - "organizations": models.Organization.objects.filter(active=False,dealer=dealer), - "vendors": models.Vendor.objects.filter(active=False,dealer=dealer), - "staff": models.Staff.objects.filter(active=False,dealer=dealer), + "customers": models.Customer.objects.filter(active=False, dealer=dealer), + "organizations": models.Organization.objects.filter( + active=False, dealer=dealer + ), + "vendors": models.Vendor.objects.filter(active=False, dealer=dealer), + "staff": models.Staff.objects.filter(active=False, dealer=dealer), } return render(request, "admin_management/user_management.html", context) @@ -10974,7 +11216,8 @@ def InventoryItemCreateView(request, dealer_slug): "year": year, "exterior": exterior, "interior": interior, - }) + } + ) item.save() messages.success(request, _("Inventory item created successfully")) return response @@ -11059,7 +11302,13 @@ class PurchaseOrderDetailView(LoginRequiredMixin, PermissionRequiredMixin, Detai title = f"Purchase Order {po_model.po_number}" context["page_title"] = title context["header_title"] = title - context["po_ready_to_fulfill"] = all([item for item in po_model.get_itemtxs_data()[0] if item.po_item_status == 'received']) + context["po_ready_to_fulfill"] = all( + [ + item + for item in po_model.get_itemtxs_data()[0] + if item.po_item_status == "received" + ] + ) po_model: PurchaseOrderModel = self.object po_items_qs, item_data = po_model.get_itemtxs_data( queryset=po_model.itemtransactionmodel_set.all().select_related( @@ -11072,10 +11321,14 @@ class PurchaseOrderDetailView(LoginRequiredMixin, PermissionRequiredMixin, Detai for i in po_items_qs.values("po_total_amount", "po_item_status") if i["po_item_status"] != "cancelled" ) - items = [{"total": x.total_amount, "q": x.quantity} for x in po_model.get_itemtxs_data()[0].all()] + items = [ + {"total": x.total_amount, "q": x.quantity} + for x in po_model.get_itemtxs_data()[0].all() + ] po_quantity = sum(item["q"] for item in items) - context['po_quantity']=po_quantity + context["po_quantity"] = po_quantity return context + def get(self, request, *args, **kwargs): self.object = self.get_object() context = self.get_context_data(object=self.object) @@ -11085,36 +11338,36 @@ class PurchaseOrderDetailView(LoginRequiredMixin, PermissionRequiredMixin, Detai ) ) - if self.object.po_status == 'fulfilled': - context['po_items_list']=po_items_qs - context['vendor']=po_items_qs.first().bill_model.vendor - context['dealer']=request.dealer + if self.object.po_status == "fulfilled": + context["po_items_list"] = po_items_qs + context["vendor"] = po_items_qs.first().bill_model.vendor + context["dealer"] = request.dealer # Check if PDF format is requested - if request.GET.get('format') == 'pdf': + if request.GET.get("format") == "pdf": # Use a separate, print-friendly template for the PDF - if request.GET.get('lang')=='en': + if request.GET.get("lang") == "en": html_string = render_to_string( - "purchase_orders/po_detail_en_pdf.html", - context - ) + "purchase_orders/po_detail_en_pdf.html", context + ) else: - html_string=render_to_string( - "purchase_orders/po_detail_ar_pdf.html", - context + html_string = render_to_string( + "purchase_orders/po_detail_ar_pdf.html", context ) - - base_url = request.build_absolute_uri('/') + base_url = request.build_absolute_uri("/") pdf = HTML(string=html_string, base_url=base_url).write_pdf() response = HttpResponse(pdf, content_type="application/pdf") - response["Content-Disposition"] = f'attachment; filename="PO_{self.object.po_number}.pdf"' + response["Content-Disposition"] = ( + f'attachment; filename="PO_{self.object.po_number}.pdf"' + ) return response # If not a PDF request, return the standard HTML response return self.render_to_response(context) + class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): model = PurchaseOrderModel context_object_name = "purchase_orders" @@ -11127,14 +11380,18 @@ class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListVie query = self.request.GET.get("q") qs = self.model.objects.filter(entity=dealer.entity) if query: - qs=qs.filter(Q(po_number__icontains=query)|Q(po_status__icontains=query)|Q(po_title__icontains=query)) + qs = qs.filter( + Q(po_number__icontains=query) + | Q(po_status__icontains=query) + | Q(po_title__icontains=query) + ) return qs return qs def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) dealer = get_user_type(self.request) - vendors=models.Vendor.objects.filter(dealer=dealer) + vendors = models.Vendor.objects.filter(dealer=dealer) context = super().get_context_data(**kwargs) context["entity_slug"] = dealer.entity.slug context["vendors"] = vendors @@ -11161,11 +11418,14 @@ class PurchaseOrderModelDeleteView(PurchaseOrderModelDeleteViewBase): level=messages.SUCCESS, ) return reverse( - "purchase_order_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"], "entity_slug": self.kwargs["entity_slug"]} + "purchase_order_list", + kwargs={ + "dealer_slug": self.kwargs["dealer_slug"], + "entity_slug": self.kwargs["entity_slug"], + }, ) - class PurchaseOrderMarkAsDraftView(BasePurchaseOrderActionActionView): action_name = "mark_as_draft" @@ -11274,7 +11534,9 @@ def upload_cars(request, dealer_slug, pk=None): response = redirect("upload_cars", dealer_slug=dealer_slug, pk=pk) if po_item.status == "uploaded": - messages.add_message(request, messages.SUCCESS, "Item uploaded Sucessfully.") + messages.add_message( + request, messages.SUCCESS, "Item uploaded Sucessfully." + ) return redirect( "view_items_inventory", dealer_slug=dealer_slug, @@ -11298,8 +11560,12 @@ def upload_cars(request, dealer_slug, pk=None): trim = models.CarTrim.objects.get(pk=item.addition_info.get("trim")) serie = models.CarSerie.objects.get(pk=item.addition_info.get("serie")) year = item.addition_info.get("year") - exterior = models.ExteriorColors.objects.get(pk=item.addition_info.get("exterior")) - interior = models.InteriorColors.objects.get(pk=item.addition_info.get("interior")) + exterior = models.ExteriorColors.objects.get( + pk=item.addition_info.get("exterior") + ) + interior = models.InteriorColors.objects.get( + pk=item.addition_info.get("interior") + ) receiving_date = timezone.now() vendor_model = item.bill_model.vendor vendor = models.Vendor.objects.get(vendor_model=vendor_model) @@ -11486,23 +11752,24 @@ class InventoryListView(InventoryListViewBase): template_name = "inventory/list.html" permission_required = ["django_ledger.view_purchaseordermodel"] + @login_required def purchase_report_view(request, dealer_slug): - start_date_str = request.GET.get('start_date') - end_date_str = request.GET.get('end_date') + start_date_str = request.GET.get("start_date") + end_date_str = request.GET.get("end_date") pos = request.entity.get_purchase_orders() if start_date_str: try: - start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date() + start_date = datetime.strptime(start_date_str, "%Y-%m-%d").date() pos = pos.filter(created__date__gte=start_date) except (ValueError, TypeError): pass if end_date_str: try: - end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date() + end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date() pos = pos.filter(created__date__lte=end_date) except (ValueError, TypeError): pass @@ -11512,7 +11779,10 @@ def purchase_report_view(request, dealer_slug): total_po_cars = 0 for po in pos: - items = [{"total": x.total_amount, "q": x.quantity} for x in po.get_itemtxs_data()[0].all()] + items = [ + {"total": x.total_amount, "q": x.quantity} + for x in po.get_itemtxs_data()[0].all() + ] po_amount = sum(item["total"] for item in items) po_quantity = sum(item["q"] for item in items) @@ -11524,15 +11794,17 @@ def purchase_report_view(request, dealer_slug): vendors = set([bill.vendor.vendor_name for bill in bills]) vendors_str = ", ".join(sorted(list(vendors))) if vendors else "N/A" - data.append({ - "po_number": po.po_number, - "po_created": po.created, - "po_status": po.po_status, - "po_fulfilled_date": po.date_fulfilled, - "po_amount": po_amount, - "po_quantity": po_quantity, - "vendors_str": vendors_str - }) + data.append( + { + "po_number": po.po_number, + "po_created": po.created, + "po_status": po.po_status, + "po_fulfilled_date": po.date_fulfilled, + "po_amount": po_amount, + "po_quantity": po_quantity, + "vendors_str": vendors_str, + } + ) current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S") context = { @@ -11546,42 +11818,42 @@ def purchase_report_view(request, dealer_slug): "end_date": end_date_str, } - return render(request, 'ledger/reports/purchase_report.html', context) + return render(request, "ledger/reports/purchase_report.html", context) def purchase_report_csv_export(request, dealer_slug): - response = HttpResponse(content_type='text/csv') + response = HttpResponse(content_type="text/csv") current_time = timezone.now().strftime("%Y-%m-%d_%H%M%S") filename = f"purchase_report_{dealer_slug}_{current_time}.csv" - response['Content-Disposition'] = f'attachment; filename="{filename}"' + response["Content-Disposition"] = f'attachment; filename="{filename}"' writer = csv.writer(response) header = [ - 'PO Number', - 'Created Date', - 'Status', - 'Fulfilled Date', - 'PO Amount', - 'PO Quantity', - 'Vendors' + "PO Number", + "Created Date", + "Status", + "Fulfilled Date", + "PO Amount", + "PO Quantity", + "Vendors", ] writer.writerow(header) pos = request.entity.get_purchase_orders() - start_date_str = request.GET.get('start_date') - end_date_str = request.GET.get('end_date') + start_date_str = request.GET.get("start_date") + end_date_str = request.GET.get("end_date") if start_date_str: try: - start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date() + start_date = datetime.strptime(start_date_str, "%Y-%m-%d").date() pos = pos.filter(created__date__gte=start_date) except (ValueError, TypeError): pass if end_date_str: try: - end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date() + end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date() pos = pos.filter(created__date__lte=end_date) except (ValueError, TypeError): pass @@ -11589,7 +11861,10 @@ def purchase_report_csv_export(request, dealer_slug): for po in pos: po_amount = 0 po_quantity = 0 - items = [{"total": x.total_amount, "q": x.quantity} for x in po.get_itemtxs_data()[0].all()] + items = [ + {"total": x.total_amount, "q": x.quantity} + for x in po.get_itemtxs_data()[0].all() + ] for item in items: po_amount += item["total"] @@ -11599,15 +11874,17 @@ def purchase_report_csv_export(request, dealer_slug): vendors = set([bill.vendor.vendor_name for bill in bills]) vendors_str = ", ".join(sorted(list(vendors))) if vendors else "N/A" - writer.writerow([ - po.po_number, - po.created.strftime("%Y-%m-%d %H:%M:%S") if po.created else '', - po.get_po_status_display(), - po.date_fulfilled.strftime("%Y-%m-%d") if po.date_fulfilled else '', - f"{po_amount:.2f}", - po_quantity, - vendors_str - ]) + writer.writerow( + [ + po.po_number, + po.created.strftime("%Y-%m-%d %H:%M:%S") if po.created else "", + po.get_po_status_display(), + po.date_fulfilled.strftime("%Y-%m-%d") if po.date_fulfilled else "", + f"{po_amount:.2f}", + po_quantity, + vendors_str, + ] + ) return response @@ -11617,16 +11894,16 @@ def car_sale_report_view(request, dealer_slug): vat = models.VatRate.objects.filter(dealer=dealer, is_active=True).first() VAT_RATE = vat.rate if vat else 0 - cars_sold = models.Car.objects.filter(dealer=dealer, status='sold') + cars_sold = models.Car.objects.filter(dealer=dealer, status="sold") # Get filter parameters from the request - selected_make = request.GET.get('make') - selected_model = request.GET.get('model') - selected_serie = request.GET.get('serie') - selected_year = request.GET.get('year') - selected_stock_type = request.GET.get('stock_type') - start_date_str = request.GET.get('start_date') - end_date_str = request.GET.get('end_date') + selected_make = request.GET.get("make") + selected_model = request.GET.get("model") + selected_serie = request.GET.get("serie") + selected_year = request.GET.get("year") + selected_stock_type = request.GET.get("stock_type") + start_date_str = request.GET.get("start_date") + end_date_str = request.GET.get("end_date") # Apply filters to the queryset if selected_make: @@ -11643,81 +11920,96 @@ def car_sale_report_view(request, dealer_slug): # Corrected: Apply date filters using the 'sold_date' field if start_date_str: try: - start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date() + start_date = datetime.strptime(start_date_str, "%Y-%m-%d").date() cars_sold = cars_sold.filter(sold_date__gte=start_date) except (ValueError, TypeError): pass if end_date_str: try: - end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date() + end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date() cars_sold = cars_sold.filter(sold_date__lte=end_date) except (ValueError, TypeError): pass # Calculate summary data for the filtered results total_cars_sold = cars_sold.count() - total_revenue_from_cars = cars_sold.aggregate( - total=Sum(F('marked_price') - F('discount_amount')) - )['total'] or 0 + total_revenue_from_cars = ( + cars_sold.aggregate(total=Sum(F("marked_price") - F("discount_amount")))[ + "total" + ] + or 0 + ) - total_vat_on_cars = cars_sold.annotate( - final_price=F('marked_price') - F('discount_amount')).aggregate( - total=Sum(F('final_price') * VAT_RATE))['total'] or 0 + total_vat_on_cars = ( + cars_sold.annotate( + final_price=F("marked_price") - F("discount_amount") + ).aggregate(total=Sum(F("final_price") * VAT_RATE))["total"] + or 0 + ) - total_revenue_from_additonals = sum([car.get_additional_services()['total'] for car in cars_sold]) - total_vat_from_additonals = sum([car.get_additional_services()['services_vat'] for car in cars_sold]) + total_revenue_from_additonals = sum( + [car.get_additional_services()["total"] for car in cars_sold] + ) + total_vat_from_additonals = sum( + [car.get_additional_services()["services_vat"] for car in cars_sold] + ) total_vat_collected = total_vat_on_cars + total_vat_from_additonals total_revenue_collected = total_revenue_from_cars + total_revenue_from_additonals - total_discount = cars_sold.aggregate(total=Sum('discount_amount'))['total'] or 0 + total_discount = cars_sold.aggregate(total=Sum("discount_amount"))["total"] or 0 current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S") # Get distinct makes for the initial dropdown, other dropdowns will be populated via AJAX - base_sold_cars_queryset = models.Car.objects.filter(dealer=dealer, status='sold') - makes = base_sold_cars_queryset.values_list('id_car_make__name', flat=True).distinct().order_by('id_car_make__name') + base_sold_cars_queryset = models.Car.objects.filter(dealer=dealer, status="sold") + makes = ( + base_sold_cars_queryset.values_list("id_car_make__name", flat=True) + .distinct() + .order_by("id_car_make__name") + ) context = { - 'cars_sold': cars_sold, - 'total_cars_sold': total_cars_sold, - 'current_time': current_time, - 'dealer': dealer, - 'total_revenue_from_cars': total_revenue_from_cars, - 'total_revenue_from_additonals': total_revenue_from_additonals, - 'total_revenue_collected': total_revenue_collected, - 'total_vat_on_cars': total_vat_on_cars, - 'total_vat_from_additonals': total_vat_from_additonals, - 'total_vat_collected': total_vat_collected, - 'total_discount': total_discount, - 'makes': makes, - 'selected_make': selected_make, - 'selected_model': selected_model, - 'selected_serie': selected_serie, - 'selected_year': selected_year, - 'selected_stock_type': selected_stock_type, - 'start_date': start_date_str, - 'end_date': end_date_str, + "cars_sold": cars_sold, + "total_cars_sold": total_cars_sold, + "current_time": current_time, + "dealer": dealer, + "total_revenue_from_cars": total_revenue_from_cars, + "total_revenue_from_additonals": total_revenue_from_additonals, + "total_revenue_collected": total_revenue_collected, + "total_vat_on_cars": total_vat_on_cars, + "total_vat_from_additonals": total_vat_from_additonals, + "total_vat_collected": total_vat_collected, + "total_discount": total_discount, + "makes": makes, + "selected_make": selected_make, + "selected_model": selected_model, + "selected_serie": selected_serie, + "selected_year": selected_year, + "selected_stock_type": selected_stock_type, + "start_date": start_date_str, + "end_date": end_date_str, } - return render(request, 'ledger/reports/car_sale_report.html', context) + return render(request, "ledger/reports/car_sale_report.html", context) ### 2. Updated `get_filtered_choices` + @login_required def get_filtered_choices(request, dealer_slug): dealer = get_object_or_404(models.Dealer, slug=dealer_slug) # Get all filter parameters from the request - selected_make = request.GET.get('make') - selected_model = request.GET.get('model') - selected_serie = request.GET.get('serie') - start_date_str = request.GET.get('start_date') - end_date_str = request.GET.get('end_date') + selected_make = request.GET.get("make") + selected_model = request.GET.get("model") + selected_serie = request.GET.get("serie") + start_date_str = request.GET.get("start_date") + end_date_str = request.GET.get("end_date") # Start with the base queryset - queryset = models.Car.objects.filter(dealer=dealer, status='sold') + queryset = models.Car.objects.filter(dealer=dealer, status="sold") # Apply filters based on what is selected if selected_make: @@ -11732,59 +12024,88 @@ def get_filtered_choices(request, dealer_slug): # Corrected: Apply date filters to the AJAX queryset if start_date_str: try: - start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date() + start_date = datetime.strptime(start_date_str, "%Y-%m-%d").date() queryset = queryset.filter(sold_date__gte=start_date) except (ValueError, TypeError): pass if end_date_str: try: - end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date() + end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date() queryset = queryset.filter(sold_date__lte=end_date) except (ValueError, TypeError): pass data = { - 'models': list(queryset.values_list('id_car_model__name', flat=True).distinct().order_by('id_car_model__name')), - 'series': list(queryset.values_list('id_car_serie__name', flat=True).distinct().order_by('id_car_serie__name')), - 'years': list(queryset.values_list('year', flat=True).distinct().order_by('-year')), - 'stock_types': list(queryset.values_list('stock_type', flat=True).distinct().order_by('stock_type')) + "models": list( + queryset.values_list("id_car_model__name", flat=True) + .distinct() + .order_by("id_car_model__name") + ), + "series": list( + queryset.values_list("id_car_serie__name", flat=True) + .distinct() + .order_by("id_car_serie__name") + ), + "years": list( + queryset.values_list("year", flat=True).distinct().order_by("-year") + ), + "stock_types": list( + queryset.values_list("stock_type", flat=True) + .distinct() + .order_by("stock_type") + ), } return JsonResponse(data) ### 3. Updated `car_sale_report_csv_export` + @login_required def car_sale_report_csv_export(request, dealer_slug): - response = HttpResponse(content_type='text/csv') + response = HttpResponse(content_type="text/csv") current_time = timezone.now().strftime("%Y-%m-%d_%H-%M-%S") filename = f"sales_report_{dealer_slug}_{current_time}.csv" - response['Content-Disposition'] = f'attachment; filename="{filename}"' + response["Content-Disposition"] = f'attachment; filename="{filename}"' writer = csv.writer(response) # Define the CSV header based on your HTML table headers header = [ - 'VIN', 'Make', 'Model', 'Year', 'Serie', 'Trim', 'Mileage', - 'Stock Type', 'Created Date', 'Sold Date', 'Cost Price', - 'Marked Price', 'Discount Amount', 'Selling Price', - 'VAT on Car', 'Services Price', 'VAT on Services', 'Final Total', - 'Invoice Number' + "VIN", + "Make", + "Model", + "Year", + "Serie", + "Trim", + "Mileage", + "Stock Type", + "Created Date", + "Sold Date", + "Cost Price", + "Marked Price", + "Discount Amount", + "Selling Price", + "VAT on Car", + "Services Price", + "VAT on Services", + "Final Total", + "Invoice Number", ] writer.writerow(header) dealer = get_object_or_404(models.Dealer, slug=dealer_slug) - cars_sold = models.Car.objects.filter(dealer=dealer, status='sold') + cars_sold = models.Car.objects.filter(dealer=dealer, status="sold") # Apply filters from the request, just like in your HTML view - selected_make = request.GET.get('make') - selected_model = request.GET.get('model') - selected_serie = request.GET.get('serie') - selected_year = request.GET.get('year') - selected_stock_type = request.GET.get('stock_type') - start_date_str = request.GET.get('start_date') - end_date_str = request.GET.get('end_date') + selected_make = request.GET.get("make") + selected_model = request.GET.get("model") + selected_serie = request.GET.get("serie") + selected_year = request.GET.get("year") + selected_stock_type = request.GET.get("stock_type") + start_date_str = request.GET.get("start_date") + end_date_str = request.GET.get("end_date") if selected_make: cars_sold = cars_sold.filter(id_car_make__name=selected_make) @@ -11800,14 +12121,14 @@ def car_sale_report_csv_export(request, dealer_slug): # Corrected: Apply date filters for CSV export if start_date_str: try: - start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date() + start_date = datetime.strptime(start_date_str, "%Y-%m-%d").date() cars_sold = cars_sold.filter(sold_date__gte=start_date) except (ValueError, TypeError): pass if end_date_str: try: - end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date() + end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date() cars_sold = cars_sold.filter(sold_date__lte=end_date) except (ValueError, TypeError): pass @@ -11815,8 +12136,8 @@ def car_sale_report_csv_export(request, dealer_slug): # Write the data for the filtered cars for car in cars_sold: additional_services = car.get_additional_services() - services_total_price = additional_services['total'] - services_vat_amount = additional_services['services_vat'] + services_total_price = additional_services["total"] + services_vat_amount = additional_services["services_vat"] invoice_number = None sold_date = None @@ -11824,124 +12145,142 @@ def car_sale_report_csv_export(request, dealer_slug): invoice_number = car.invoice.invoice_number sold_date = car.invoice.date_paid - writer.writerow([ - car.vin, - car.id_car_make.name, - car.id_car_model.name, - car.year, - car.id_car_serie.name, - car.id_car_trim.name, - car.mileage if car.mileage else '0', - car.stock_type, - car.created_at.strftime("%Y-%m-%d %H:%M:%S") if car.created_at else '', - sold_date.strftime("%Y-%m-%d %H:%M:%S") if sold_date else '', - car.cost_price, - car.marked_price, - car.discount_amount, - car.final_price, - car.vat_amount, - services_total_price, - services_vat_amount, - car.final_price_plus_services_plus_vat, - invoice_number, - ]) + writer.writerow( + [ + car.vin, + car.id_car_make.name, + car.id_car_model.name, + car.year, + car.id_car_serie.name, + car.id_car_trim.name, + car.mileage if car.mileage else "0", + car.stock_type, + car.created_at.strftime("%Y-%m-%d %H:%M:%S") if car.created_at else "", + sold_date.strftime("%Y-%m-%d %H:%M:%S") if sold_date else "", + car.cost_price, + car.marked_price, + car.discount_amount, + car.final_price, + car.vat_amount, + services_total_price, + services_vat_amount, + car.final_price_plus_services_plus_vat, + invoice_number, + ] + ) return response + @login_required # @permission_required('inventory.view_staff') def staff_password_reset_view(request, dealer_slug, user_pk): dealer = get_object_or_404(models.Dealer, slug=dealer_slug) staff = models.Staff.objects.filter(dealer=dealer, pk=user_pk).first() - if request.method == 'POST': + if request.method == "POST": form = forms.CustomSetPasswordForm(staff.user, request.POST) if form.is_valid(): form.save() - messages.success(request, _('Your password has been set. You may go ahead and log in now.')) - return redirect('user_detail',dealer_slug=dealer_slug,slug=staff.slug) + messages.success( + request, + _("Your password has been set. You may go ahead and log in now."), + ) + return redirect("user_detail", dealer_slug=dealer_slug, slug=staff.slug) else: - messages.error(request, _(f'Invalid password. {str(form.errors)}')) + messages.error(request, _(f"Invalid password. {str(form.errors)}")) form = forms.CustomSetPasswordForm(staff.user) - return render(request, 'users/user_password_reset.html', {'form': form}) + return render(request, "users/user_password_reset.html", {"form": form}) + class RecallListView(ListView): model = models.Recall - template_name = 'recalls/recall_list.html' - context_object_name = 'recalls' + template_name = "recalls/recall_list.html" + context_object_name = "recalls" paginate_by = 20 def get_queryset(self): - queryset = super().get_queryset().annotate( - dealer_count=Count('notifications', distinct=True), - car_count=Count('notifications__cars_affected', distinct=True) + queryset = ( + super() + .get_queryset() + .annotate( + dealer_count=Count("notifications", distinct=True), + car_count=Count("notifications__cars_affected", distinct=True), + ) ) - return queryset.select_related('make', 'model', 'serie', 'trim') + return queryset.select_related("make", "model", "serie", "trim") class RecallDetailView(DetailView): model = models.Recall - template_name = 'recalls/recall_detail.html' - context_object_name = 'recall' + template_name = "recalls/recall_detail.html" + context_object_name = "recall" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['notifications'] = self.object.notifications.select_related('dealer') + context["notifications"] = self.object.notifications.select_related("dealer") return context def RecallFilterView(request): - context = {'make_data': models.CarMake.objects.all()} + context = {"make_data": models.CarMake.objects.all()} if request.method == "POST": - make = request.POST.get('make') - model = request.POST.get('model') - serie = request.POST.get('serie') - trim = request.POST.get('trim') - year = request.POST.get('year') - url = reverse('recall_create') + make = request.POST.get("make") + model = request.POST.get("model") + serie = request.POST.get("serie") + trim = request.POST.get("trim") + year = request.POST.get("year") + url = reverse("recall_create") url += f"?make={make}&model={model}&serie={serie}&trim={trim}&year={year}" - cars = models.Car.objects.filter(id_car_make=make,id_car_model=model,id_car_serie=serie,id_car_trim=trim,year=year) - context['url'] = url - context['cars'] = cars - return render(request,'recalls/recall_filter.html',context) + cars = models.Car.objects.filter( + id_car_make=make, + id_car_model=model, + id_car_serie=serie, + id_car_trim=trim, + year=year, + ) + context["url"] = url + context["cars"] = cars + return render(request, "recalls/recall_filter.html", context) + class RecallCreateView(FormView): - template_name = 'recalls/recall_create.html' + template_name = "recalls/recall_create.html" form_class = forms.RecallCreateForm - success_url = reverse_lazy('recall_success') + success_url = reverse_lazy("recall_success") def get_form(self, form_class=None): form = super().get_form(form_class) - make = self.request.GET.get('make') - model = self.request.GET.get('model') - serie = self.request.GET.get('serie') - trim = self.request.GET.get('trim') - year = self.request.GET.get('year') + make = self.request.GET.get("make") + model = self.request.GET.get("model") + serie = self.request.GET.get("serie") + trim = self.request.GET.get("trim") + year = self.request.GET.get("year") if make: qs = models.CarMake.objects.filter(pk=make) - form.fields['make'].queryset = qs - form.initial['make'] = qs.first() + form.fields["make"].queryset = qs + form.initial["make"] = qs.first() if model: qs = models.CarModel.objects.filter(pk=model) - form.fields['model'].queryset = qs - form.initial['model'] = qs.first() + form.fields["model"].queryset = qs + form.initial["model"] = qs.first() if serie: qs = models.CarSerie.objects.filter(pk=serie) - form.fields['serie'].queryset = qs - form.initial['serie'] = qs.first() + form.fields["serie"].queryset = qs + form.initial["serie"] = qs.first() if trim: qs = models.CarTrim.objects.filter(pk=trim) - form.fields['trim'].queryset = qs - form.initial['trim'] = qs.first() + form.fields["trim"].queryset = qs + form.initial["trim"] = qs.first() if year: - form.fields['year_from'].initial = year - form.fields['year_to'].initial = year + form.fields["year_from"].initial = year + form.fields["year_to"].initial = year return form def get_initial(self): initial = super().get_initial() - if self.request.method == 'GET': + if self.request.method == "GET": initial.update(self.request.GET.dict()) return initial @@ -11977,175 +12316,195 @@ class RecallCreateView(FormView): for dealer in dealers: dealer_cars = cars.filter(dealer=dealer) notification = models.RecallNotification.objects.create( - recall=recall, - dealer=dealer + recall=recall, dealer=dealer ) notification.cars_affected.set(dealer_cars) # Send email self.send_notification_email(dealer, recall, dealer_cars) - messages.success(self.request, _("Recall created and notifications sent successfully")) + messages.success( + self.request, _("Recall created and notifications sent successfully") + ) return super().form_valid(form) def send_notification_email(self, dealer, recall, cars): subject = f"Recall Notification: {recall.title}" - message = render_to_string('recalls/email/recall_notification.txt', { - 'dealer': dealer, - 'recall': recall, - 'cars': cars, - }) + message = render_to_string( + "recalls/email/recall_notification.txt", + { + "dealer": dealer, + "recall": recall, + "cars": cars, + }, + ) send_email( subject, message, - 'noreply@yourdomain.com', + "noreply@yourdomain.com", [dealer.user.email], ) + class RecallSuccessView(TemplateView): - template_name = 'recalls/recall_success.html' + template_name = "recalls/recall_success.html" @login_required -def schedule_calendar(request,dealer_slug): +def schedule_calendar(request, dealer_slug): dealer = get_object_or_404(models.Dealer, slug=dealer_slug) - user_schedules = models.Schedule.objects.filter(dealer=dealer,scheduled_by=request.user).order_by('scheduled_at') - upcoming_schedules = user_schedules.filter(scheduled_at__gte=timezone.now()).order_by('scheduled_at') - context = { - 'schedules': user_schedules, - 'upcoming_schedules':upcoming_schedules - } - return render(request, 'schedule_calendar.html', context) + user_schedules = models.Schedule.objects.filter( + dealer=dealer, scheduled_by=request.user + ).order_by("scheduled_at") + upcoming_schedules = user_schedules.filter( + scheduled_at__gte=timezone.now() + ).order_by("scheduled_at") + context = {"schedules": user_schedules, "upcoming_schedules": upcoming_schedules} + return render(request, "schedule_calendar.html", context) # Support @login_required def help_center(request): - return render(request, 'support/help_center.html') + return render(request, "support/help_center.html") + @login_required -@permission_required('inventory.add_ticket') -def create_ticket(request,dealer_slug): +@permission_required("inventory.add_ticket") +def create_ticket(request, dealer_slug): if not request.is_dealer: - return redirect('home') + return redirect("home") dealer = get_object_or_404(models.Dealer, slug=dealer_slug) - if request.method == 'POST': + if request.method == "POST": form = forms.TicketForm(request.POST) if form.is_valid(): instance = form.save(commit=False) instance.dealer = dealer instance.save() - messages.success(request, 'Your support ticket has been submitted successfully!') - return redirect('ticket_list',dealer_slug=dealer.slug) + messages.success( + request, "Your support ticket has been submitted successfully!" + ) + return redirect("ticket_list", dealer_slug=dealer.slug) else: form = forms.TicketForm() - return render(request, 'support/create_ticket.html', {'form': form}) + return render(request, "support/create_ticket.html", {"form": form}) + @login_required -@permission_required('inventory.view_ticket') -def ticket_list(request,dealer_slug): - dealer= get_object_or_404(models.Dealer, slug=dealer_slug) - tickets = models.Ticket.objects.filter(dealer=dealer).order_by('-created_at') - query=request.GET.get('q') - if query: - tickets=tickets.filter(Q(id__icontains=query)| Q(subject__icontains=query)) - - return render(request, 'support/ticket_list.html', {'tickets': tickets}) - -@login_required -@permission_required('inventory.change_ticket') -def ticket_detail(request, dealer_slug,ticket_id): +@permission_required("inventory.view_ticket") +def ticket_list(request, dealer_slug): dealer = get_object_or_404(models.Dealer, slug=dealer_slug) - ticket = models.Ticket.objects.get(dealer=dealer,id=ticket_id) - return render(request, 'support/ticket_detail.html', {'ticket': ticket}) + tickets = models.Ticket.objects.filter(dealer=dealer).order_by("-created_at") + query = request.GET.get("q") + if query: + tickets = tickets.filter(Q(id__icontains=query) | Q(subject__icontains=query)) + + return render(request, "support/ticket_list.html", {"tickets": tickets}) + @login_required -@permission_required('inventory.change_ticket') +@permission_required("inventory.change_ticket") +def ticket_detail(request, dealer_slug, ticket_id): + dealer = get_object_or_404(models.Dealer, slug=dealer_slug) + ticket = models.Ticket.objects.get(dealer=dealer, id=ticket_id) + return render(request, "support/ticket_detail.html", {"ticket": ticket}) + + +@login_required +@permission_required("inventory.change_ticket") def ticket_mark_resolved(request, ticket_id): ticket = models.Ticket.objects.get(id=ticket_id) - ticket.status = 'resolved' + ticket.status = "resolved" ticket.save() - messages.success(request, 'Ticket marked as resolved successfully!') - subject = 'Ticket Resolved' + messages.success(request, "Ticket marked as resolved successfully!") + subject = "Ticket Resolved" message = f"Your support ticket has been resolved. Please check the details below:\n\nTicket ID: {ticket.id}\nSubject: {ticket.subject}\nDescription: {ticket.description}" - send_email( - settings.SUPPORT_EMAIL, - ticket.dealer.user.email, - subject, - message - ) - return render(request, 'support/ticket_detail.html', {'ticket': ticket}) + send_email(settings.SUPPORT_EMAIL, ticket.dealer.user.email, subject, message) + return render(request, "support/ticket_detail.html", {"ticket": ticket}) + @login_required -@permission_required('inventory.change_ticket') +@permission_required("inventory.change_ticket") def ticket_update(request, ticket_id): ticket = models.Ticket.objects.get(id=ticket_id) - if request.method == 'POST': + if request.method == "POST": form = forms.TicketResolutionForm(request.POST, instance=ticket) if form.is_valid(): form.save() - messages.success(request, f'Ticket has been marked as {ticket.get_status_display()}.') - return redirect('ticket_detail',dealer_slug=ticket.dealer.slug, ticket_id=ticket.id) + messages.success( + request, f"Ticket has been marked as {ticket.get_status_display()}." + ) + return redirect( + "ticket_detail", dealer_slug=ticket.dealer.slug, ticket_id=ticket.id + ) else: form = forms.TicketResolutionForm(instance=ticket) - return render(request, 'support/ticket_update.html', { - 'ticket': ticket, - 'form': form - }) + return render( + request, "support/ticket_update.html", {"ticket": ticket, "form": form} + ) # class ChartOfAccountModelListView(ChartOfAccountModelListViewBase): # template_name = 'chart_of_accounts/coa_list.html' # permission_required = 'django_ledger.view_chartofaccountmodel' class ChartOfAccountModelCreateView(ChartOfAccountModelCreateViewBase): - template_name = 'chart_of_accounts/coa_create.html' - permission_required = 'django_ledger.add_chartofaccountmodel' -class ChartOfAccountModelListView(ChartOfAccountModelListViewBase): - template_name = 'chart_of_accounts/coa_list.html' - permission_required = 'django_ledger.view_chartofaccountmodel' -class ChartOfAccountModelUpdateView(ChartOfAccountModelUpdateViewBase): - template_name = 'chart_of_accounts/coa_update.html' - permission_required = 'django_ledger.change_chartofaccountmodel' -class CharOfAccountModelActionView(CharOfAccountModelActionViewBase): - permission_required = 'django_ledger.change_chartofaccountmodel' + template_name = "chart_of_accounts/coa_create.html" + permission_required = "django_ledger.add_chartofaccountmodel" +class ChartOfAccountModelListView(ChartOfAccountModelListViewBase): + template_name = "chart_of_accounts/coa_list.html" + permission_required = "django_ledger.view_chartofaccountmodel" + + +class ChartOfAccountModelUpdateView(ChartOfAccountModelUpdateViewBase): + template_name = "chart_of_accounts/coa_update.html" + permission_required = "django_ledger.change_chartofaccountmodel" + + +class CharOfAccountModelActionView(CharOfAccountModelActionViewBase): + permission_required = "django_ledger.change_chartofaccountmodel" + class CarDealershipSignUpView(CreateView): model = models.UserRegistration form_class = forms.CarDealershipRegistrationForm - template_name = 'account/signup-wizard.html' - success_url = reverse_lazy('registration_success') + template_name = "account/signup-wizard.html" + success_url = reverse_lazy("registration_success") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['title'] = _('Car Dealership Registration') + context["title"] = _("Car Dealership Registration") return context def form_valid(self, form): response = super().form_valid(form) - messages.success(self.request, _('Your request has been submitted. We will contact you soon.')) + messages.success( + self.request, + _("Your request has been submitted. We will contact you soon."), + ) return response + def payment_result(request): s = request.GET.get("status") if s == "success": - return render(request, 'plans/payment_success.html') - return render(request, 'plans/payment_failed.html') + return render(request, "plans/payment_success.html") + return render(request, "plans/payment_failed.html") @require_POST -def create_estimate_for_car(request,dealer_slug,slug): +def create_estimate_for_car(request, dealer_slug, slug): car = get_object_or_404(models.Car, slug=slug) dealer = get_object_or_404(models.Dealer, slug=dealer_slug) - if request.method == 'POST': + if request.method == "POST": form = forms.CarDetailsEstimateCreate(request.POST) if form.is_valid(): - customer = form.cleaned_data['customer'] + customer = form.cleaned_data["customer"] estimate = create_estimate_(dealer, car, customer) if request.is_staff: @@ -12170,16 +12529,20 @@ def create_estimate_for_car(request,dealer_slug,slug): else: messages.error(request, "Please correct the errors below.") return redirect("car_detail", dealer_slug=dealer.slug, slug=car.slug) + + @require_POST -def estimate_create_from_opportunity(request,dealer_slug,slug): +def estimate_create_from_opportunity(request, dealer_slug, slug): opportunity = get_object_or_404(models.Opportunity, slug=slug) dealer = get_object_or_404(models.Dealer, slug=dealer_slug) car = opportunity.car customer = opportunity.customer # TODO: set safe guard, so it doesnt recreate it - if not all([dealer,car,customer]): + if not all([dealer, car, customer]): messages.error(request, "Please correct the errors below.") - return redirect("opportunity_detail", dealer_slug=dealer.slug, slug=opportunity.slug) + return redirect( + "opportunity_detail", dealer_slug=dealer.slug, slug=opportunity.slug + ) estimate = create_estimate_(dealer, car, customer) @@ -12201,4 +12564,4 @@ def estimate_create_from_opportunity(request,dealer_slug,slug): ) messages.success(request, "Estimate created successfully.") - return redirect("estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk) \ No newline at end of file + return redirect("estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk) diff --git a/requirements_prod.txt b/requirements_prod.txt index 7592a4f2..c236da77 100644 --- a/requirements_prod.txt +++ b/requirements_prod.txt @@ -7,7 +7,9 @@ autobahn==24.4.2 Automat==25.4.16 Babel==2.15.0 beautifulsoup4==4.13.4 +blacknoise==1.2.0 blessed==1.21.0 +Brotli==1.1.0 cattrs==25.1.1 certifi==2025.7.9 cffi==1.17.1 @@ -19,15 +21,13 @@ constantly==23.10.4 crispy-bootstrap5==2025.6 cryptography==45.0.5 cssbeautifier==1.15.4 -daphne==4.2.1 +cssselect2==0.8.0 defusedxml==0.7.1 diff-match-patch==20241021 distro==1.9.0 Django==5.2.4 django-allauth==65.10.0 django-appconf==1.1.0 -django-appointment==3.8.0 -django-background-tasks==1.2.8 django-bootstrap5==25.1 django-ckeditor==6.7.3 django-cors-headers==4.7.0 @@ -35,6 +35,7 @@ django-countries==7.6.1 django-crispy-forms==2.4 django-debug-toolbar==5.2.0 django-easy-audit==1.3.7 +django-encrypted-model-fields==0.6.5 django-extensions==4.1 django-filter==25.1 django-imagekit==5.0.0 @@ -47,7 +48,6 @@ django-ordered-model==3.7.4 django-phonenumber-field==8.0.0 django-picklefield==3.3 django-plans==2.0.0 -django-prometheus==2.4.1 django-q2==1.8.0 django-query-builder==3.2.0 django-schema-graph==3.1.0 @@ -56,8 +56,6 @@ django-tables2==2.7.5 django-treebeard==4.7.1 django-widget-tweaks==1.5.0 djangorestframework==3.16.0 -djhtml==3.0.8 -djlint==1.36.4 dnspython==2.7.0 docopt==0.6.2 EditorConfig==0.17.1 @@ -78,8 +76,6 @@ hyperlink==21.0.0 icalendar==6.3.1 idna==3.10 incremental==24.7.2 -iron-core==1.2.1 -iron-mq==0.9 jiter==0.10.0 jsbeautifier==1.15.4 json5==0.12.0 @@ -109,16 +105,17 @@ phonenumbers==8.13.42 pilkit==3.0 pillow==10.4.0 priority==1.3.0 -prometheus_client==0.22.1 psycopg2-binary==2.9.10 pyasn1==0.6.1 pyasn1_modules==0.4.2 pycparser==2.22 pydantic==2.11.7 pydantic_core==2.33.2 +pydyf==0.11.0 Pygments==2.19.2 pymongo==4.14.1 pyOpenSSL==25.1.0 +pyphen==0.17.2 python-dateutil==2.9.0.post0 python-dotenv==1.1.1 python-slugify==8.0.4 @@ -131,8 +128,6 @@ redis==6.2.0 regex==2024.11.6 requests==2.32.4 requests-toolbelt==1.0.0 -rich==14.0.0 -ruff==0.12.2 service-identity==24.2.0 setuptools==80.9.0 six==1.17.0 @@ -140,11 +135,15 @@ sniffio==1.3.1 soupsieve==2.7 SQLAlchemy==2.0.41 sqlparse==0.5.3 +starlette==0.47.3 +static3==0.7.0 suds==1.2.0 swapper==1.3.0 tablib==3.8.0 tenacity==9.1.2 text-unidecode==1.3 +tinycss2==1.4.0 +tinyhtml5==2.0.0 tqdm==4.67.1 Twisted==25.5.0 txaio==25.6.1 @@ -156,6 +155,8 @@ urllib3==2.5.0 uvicorn==0.35.0 uvicorn-worker==0.3.0 wcwidth==0.2.13 -whitenoise==6.9.0 +weasyprint==66.0 +webencodings==0.5.1 zope.interface==7.2 +zopfli==0.2.3.post1 zstandard==0.23.0 diff --git a/templates/account/account_inactive.html b/templates/account/account_inactive.html index fa1311ca..4ace5cad 100644 --- a/templates/account/account_inactive.html +++ b/templates/account/account_inactive.html @@ -6,9 +6,9 @@ {% endblock head_title %} {% block content %} {% element h1 %} - {% translate "Account Inactive" %} -{% endelement %} -{% element p %} -{% translate "This account is inactive." %} -{% endelement %} + {% translate "Account Inactive" %} + {% endelement %} + {% element p %} + {% translate "This account is inactive." %} + {% endelement %} {% endblock content %} diff --git a/templates/account/confirm_email_verification_code.html b/templates/account/confirm_email_verification_code.html index 08f0d893..84e37256 100644 --- a/templates/account/confirm_email_verification_code.html +++ b/templates/account/confirm_email_verification_code.html @@ -6,43 +6,43 @@ {% endblock head_title %} {% block content %} {% element h1 %} - {% translate "Enter Email Verification Code" %} -{% endelement %} -{% setvar email_link %} -{{ email }} -{% endsetvar %} -{% element p %} -{% blocktranslate %}We’ve sent a code to {{ email_link }}. The code expires shortly, so please enter it soon.{% endblocktranslate %} -{% endelement %} -{% url 'account_email_verification_sent' as action_url %} -{% element form form=form method="post" action=action_url tags="entrance,email,verification" %} -{% slot body %} -{% csrf_token %} -{% element fields form=form unlabeled=True %} -{% endelement %} -{{ redirect_field }} -{% endslot %} -{% slot actions %} -{% element button type="submit" tags="prominent,confirm" %} -{% translate "Confirm" %} -{% endelement %} -{% if cancel_url %} - {% element button href=cancel_url tags="link,cancel" %} - {% translate "Cancel" %} -{% endelement %} -{% else %} -{% element button type="submit" form="logout-from-stage" tags="link,cancel" %} -{% translate "Cancel" %} -{% endelement %} -{% endif %} -{% endslot %} -{% endelement %} -{% if not cancel_url %} - - - {% csrf_token %} - -{% endif %} + {% translate "Enter Email Verification Code" %} + {% endelement %} + {% setvar email_link %} + {{ email }} + {% endsetvar %} + {% element p %} + {% blocktranslate %}We’ve sent a code to {{ email_link }}. The code expires shortly, so please enter it soon.{% endblocktranslate %} + {% endelement %} + {% url 'account_email_verification_sent' as action_url %} + {% element form form=form method="post" action=action_url tags="entrance,email,verification" %} + {% slot body %} + {% csrf_token %} + {% element fields form=form unlabeled=True %} + {% endelement %} + {{ redirect_field }} + {% endslot %} + {% slot actions %} + {% element button type="submit" tags="prominent,confirm" %} + {% translate "Confirm" %} + {% endelement %} + {% if cancel_url %} + {% element button href=cancel_url tags="link,cancel" %} + {% translate "Cancel" %} + {% endelement %} + {% else %} + {% element button type="submit" form="logout-from-stage" tags="link,cancel" %} + {% translate "Cancel" %} + {% endelement %} + {% endif %} + {% endslot %} + {% endelement %} + {% if not cancel_url %} +
    + + {% csrf_token %} +
    + {% endif %} {% endblock content %} diff --git a/templates/account/confirm_login_code..html b/templates/account/confirm_login_code..html index a7ffaaa1..ee6519c3 100644 --- a/templates/account/confirm_login_code..html +++ b/templates/account/confirm_login_code..html @@ -28,30 +28,30 @@

    {% translate "Enter Sign-In Code" %}

    {% setvar email_link %} - {{ email }} - {% endsetvar %} -

    - {% blocktranslate %}We’ve sent a code to {{ email_link }}. The code expires shortly, so please enter it soon.{% endblocktranslate %} -

    -
    - {% csrf_token %} - {{ redirect_field }} - {{ form|crispy }} - -
    - {% element button type="submit" form="logout-from-stage" tags="link" %} - {% translate "Cancel" %} - {% endelement %} -
    - - {% csrf_token %} -
    + {{ email }} + {% endsetvar %} +

    + {% blocktranslate %}We’ve sent a code to {{ email_link }}. The code expires shortly, so please enter it soon.{% endblocktranslate %} +

    +
    + {% csrf_token %} + {{ redirect_field }} + {{ form|crispy }} + +
    + {% element button type="submit" form="logout-from-stage" tags="link" %} + {% translate "Cancel" %} + {% endelement %} +
    + + {% csrf_token %} +
    + + - - {% endblock content %} diff --git a/templates/account/email.html b/templates/account/email.html index 8faf8174..1a1fba70 100644 --- a/templates/account/email.html +++ b/templates/account/email.html @@ -50,7 +50,7 @@ {% if emailaddress.primary %} {% endif %} - {% endwith %} + {% endwith %} {% endfor %} diff --git a/templates/account/email_change.html b/templates/account/email_change.html index 94576c23..0ee140c6 100644 --- a/templates/account/email_change.html +++ b/templates/account/email_change.html @@ -6,63 +6,63 @@ {% endblock head_title %} {% block content %} {% element h1 %} - {% trans "Email Address" %} -{% endelement %} -{% if not emailaddresses %} - {% include "account/snippets/warn_no_email.html" %} -{% endif %} -{% url 'account_email' as action_url %} -{% element form method="post" action=action_url %} -{% slot body %} -{% csrf_token %} -{% if current_emailaddress %} - {% element field id="current_email" disabled=True type="email" value=current_emailaddress.email %} - {% slot label %} - {% translate "Current email" %}: -{% endslot %} -{% endelement %} -{% endif %} -{% if new_emailaddress %} - {% element field id="new_email" value=new_emailaddress.email disabled=True type="email" %} - {% slot label %} - {% if not current_emailaddress %} - {% translate "Current email" %}: - {% else %} - {% translate "Changing to" %}: + {% trans "Email Address" %} + {% endelement %} + {% if not emailaddresses %} + {% include "account/snippets/warn_no_email.html" %} + {% endif %} + {% url 'account_email' as action_url %} + {% element form method="post" action=action_url %} + {% slot body %} + {% csrf_token %} + {% if current_emailaddress %} + {% element field id="current_email" disabled=True type="email" value=current_emailaddress.email %} + {% slot label %} + {% translate "Current email" %}: + {% endslot %} + {% endelement %} + {% endif %} + {% if new_emailaddress %} + {% element field id="new_email" value=new_emailaddress.email disabled=True type="email" %} + {% slot label %} + {% if not current_emailaddress %} + {% translate "Current email" %}: + {% else %} + {% translate "Changing to" %}: + {% endif %} + {% endslot %} + {% slot help_text %} + {% blocktranslate %}Your email address is still pending verification.{% endblocktranslate %} + {% element button form="pending-email" type="submit" name="action_send" tags="minor,secondary" %} + {% trans 'Re-send Verification' %} + {% endelement %} + {% if current_emailaddress %} + {% element button form="pending-email" type="submit" name="action_remove" tags="danger,minor" %} + {% trans 'Cancel Change' %} + {% endelement %} + {% endif %} + {% endslot %} + {% endelement %} + {% endif %} + {% element field id=form.email.auto_id name="email" value=form.email.value errors=form.email.errors type="email" %} + {% slot label %} + {% translate "Change to" %}: + {% endslot %} + {% endelement %} + {% endslot %} + {% slot actions %} + {% element button name="action_add" type="submit" %} + {% trans "Change Email" %} + {% endelement %} + {% endslot %} + {% endelement %} + {% if new_emailaddress %} + {% endif %} -{% endslot %} -{% slot help_text %} -{% blocktranslate %}Your email address is still pending verification.{% endblocktranslate %} -{% element button form="pending-email" type="submit" name="action_send" tags="minor,secondary" %} -{% trans 'Re-send Verification' %} -{% endelement %} -{% if current_emailaddress %} - {% element button form="pending-email" type="submit" name="action_remove" tags="danger,minor" %} - {% trans 'Cancel Change' %} -{% endelement %} -{% endif %} -{% endslot %} -{% endelement %} -{% endif %} -{% element field id=form.email.auto_id name="email" value=form.email.value errors=form.email.errors type="email" %} -{% slot label %} -{% translate "Change to" %}: -{% endslot %} -{% endelement %} -{% endslot %} -{% slot actions %} -{% element button name="action_add" type="submit" %} -{% trans "Change Email" %} -{% endelement %} -{% endslot %} -{% endelement %} -{% if new_emailaddress %} - -{% endif %} {% endblock content %} diff --git a/templates/account/lock-screen.html b/templates/account/lock-screen.html index 5447cb7a..5d04e1e5 100644 --- a/templates/account/lock-screen.html +++ b/templates/account/lock-screen.html @@ -153,437 +153,437 @@

    I need help with something

    -

    I can’t reorder a product I previously ordered

    - -
    -

    How do I place an order?

    - -
    -

    My payment method not working

    - -
    - -
    -
    - + href="#!"> +

    I can’t reorder a product I previously ordered

    + + +

    How do I place an order?

    + +
    +

    My payment method not working

    + +
    +
    +
    +
    + +
    +
    Eric
    +

    + Ask us anything – we’ll get back to you here or by email within 24 hours. +

    +
    +
    + + -
    Eric
    -

    - Ask us anything – we’ll get back to you here or by email within 24 hours. -

    - - - - - - - + -
    -
    -
    -
    -
    - Theme Customizer -
    -

    Explore different styles according to your preferences

    +
    +
    +
    +
    +
    + Theme Customizer +
    +

    Explore different styles according to your preferences

    +
    + +
    +
    - -
    - -
    -
    -
    -
    Color Scheme
    -
    -
    - - +
    +
    +
    Color Scheme
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    -
    - - +
    +
    +
    RTL
    +
    + +
    +
    +

    Change text direction

    -
    - - +
    +
    +
    Support Chat
    +
    + +
    +
    +

    Toggle support chat

    +
    +
    Navigation Type
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +

    + You can't update navigation type in this page +

    +
    +
    +
    Vertical Navbar Appearance
    +
    +
    + + +
    +
    + + +
    +
    +

    + You can't update vertical navbar appearance in this page +

    +
    +
    +
    Horizontal Navbar Shape
    +
    +
    + + +
    +
    + + +
    +
    +

    + You can't update horizontal navbar shape in this page +

    +
    +
    +
    Horizontal Navbar Appearance
    +
    +
    + + +
    +
    + + +
    +
    +

    + You can't update horizontal navbar appearance in this page +

    +
    + Purchase template
    - - -
    -
    -
    - - - - - - -
    -
    - customize -
    -
    + - - - - - - - - - - - + + + + + + + + + + + diff --git a/templates/account/login.html b/templates/account/login.html index e2cb0db6..09d91e06 100644 --- a/templates/account/login.html +++ b/templates/account/login.html @@ -77,25 +77,25 @@
    - + {% if LOGIN_BY_CODE_ENABLED or PASSKEY_LOGIN_ENABLED %}
    {% element button_group vertical=True %} - {% if PASSKEY_LOGIN_ENABLED %} - {% element button type="submit" form="mfa_login" id="passkey_login" tags="prominent,login,outline,primary" %} - {% trans "Sign in with a passkey" %} + {% if PASSKEY_LOGIN_ENABLED %} + {% element button type="submit" form="mfa_login" id="passkey_login" tags="prominent,login,outline,primary" %} + {% trans "Sign in with a passkey" %} + {% endelement %} + {% endif %} + {% if LOGIN_BY_CODE_ENABLED %} + {% element button href=request_login_code_url tags="prominent,login,outline,primary" %} + {% trans "Mail me a sign-in code" %} + {% endelement %} + {% endif %} {% endelement %} {% endif %} - {% if LOGIN_BY_CODE_ENABLED %} - {% element button href=request_login_code_url tags="prominent,login,outline,primary" %} - {% trans "Mail me a sign-in code" %} - {% endelement %} - {% endif %} -{% endelement %} -{% endif %} -{% if SOCIALACCOUNT_ENABLED %} - {% include "socialaccount/snippets/login.html" with page_layout="entrance" %} -{% endif %} + {% if SOCIALACCOUNT_ENABLED %} + {% include "socialaccount/snippets/login.html" with page_layout="entrance" %} + {% endif %} {% endblock content %} {% block extra_body %} {{ block.super }} diff --git a/templates/account/password_change.html b/templates/account/password_change.html index 36546bb3..4d6e51e5 100644 --- a/templates/account/password_change.html +++ b/templates/account/password_change.html @@ -27,7 +27,7 @@

    {% trans "Change Password" %}

    diff --git a/templates/account/password_set.html b/templates/account/password_set.html index e2325905..2cb77a42 100644 --- a/templates/account/password_set.html +++ b/templates/account/password_set.html @@ -6,20 +6,20 @@ {% endblock head_title %} {% block content %} {% element h1 %} - {% trans "Set Password" %} -{% endelement %} -{% url 'account_set_password' as action_url %} -{% element form method="post" action=action_url %} -{% slot body %} -{% csrf_token %} -{{ redirect_field }} -{% element fields form=form %} -{% endelement %} -{% endslot %} -{% slot actions %} -{% element button type="submit" name="action" %} -{% trans 'Set Password' %} -{% endelement %} -{% endslot %} -{% endelement %} + {% trans "Set Password" %} + {% endelement %} + {% url 'account_set_password' as action_url %} + {% element form method="post" action=action_url %} + {% slot body %} + {% csrf_token %} + {{ redirect_field }} + {% element fields form=form %} + {% endelement %} + {% endslot %} + {% slot actions %} + {% element button type="submit" name="action" %} + {% trans 'Set Password' %} + {% endelement %} + {% endslot %} + {% endelement %} {% endblock content %} diff --git a/templates/account/reauthenticate.html b/templates/account/reauthenticate.html index 5e7b3e58..d98c86a5 100644 --- a/templates/account/reauthenticate.html +++ b/templates/account/reauthenticate.html @@ -3,20 +3,20 @@ {% load i18n %} {% block reauthenticate_content %} {% element p %} - {% blocktranslate %}Enter your password:{% endblocktranslate %} -{% endelement %} -{% url 'account_reauthenticate' as action_url %} -{% element form form=form method="post" action=action_url %} -{% slot body %} -{% csrf_token %} -{% element fields form=form unlabeled=True %} -{% endelement %} -{{ redirect_field }} -{% endslot %} -{% slot actions %} -{% element button type="submit" tags="primary,reauthenticate" %} -{% trans "Confirm" %} -{% endelement %} -{% endslot %} -{% endelement %} + {% blocktranslate %}Enter your password:{% endblocktranslate %} + {% endelement %} + {% url 'account_reauthenticate' as action_url %} + {% element form form=form method="post" action=action_url %} + {% slot body %} + {% csrf_token %} + {% element fields form=form unlabeled=True %} + {% endelement %} + {{ redirect_field }} + {% endslot %} + {% slot actions %} + {% element button type="submit" tags="primary,reauthenticate" %} + {% trans "Confirm" %} + {% endelement %} + {% endslot %} + {% endelement %} {% endblock %} diff --git a/templates/account/request_login_code.html b/templates/account/request_login_code.html index 6d331faf..8b9b0152 100644 --- a/templates/account/request_login_code.html +++ b/templates/account/request_login_code.html @@ -6,27 +6,27 @@ {% endblock head_title %} {% block content %} {% element h1 %} - {% translate "Mail me a sign-in code" %} -{% endelement %} -{% element p %} -{% blocktranslate %}You will receive an email containing a special code for a password-free sign-in.{% endblocktranslate %} -{% endelement %} -{% url 'account_request_login_code' as login_url %} -{% element form form=form method="post" action=login_url tags="entrance,login" %} -{% slot body %} -{% csrf_token %} -{% element fields form=form unlabeled=True %} -{% endelement %} -{{ redirect_field }} -{% endslot %} -{% slot actions %} -{% element button type="submit" tags="prominent,login" %} -{% translate "Request Code" %} -{% endelement %} -{% endslot %} -{% endelement %} -{% url 'account_login' as login_url %} -{% element button href=login_url tags="link" %} -{% translate "Other sign-in options" %} -{% endelement %} + {% translate "Mail me a sign-in code" %} + {% endelement %} + {% element p %} + {% blocktranslate %}You will receive an email containing a special code for a password-free sign-in.{% endblocktranslate %} + {% endelement %} + {% url 'account_request_login_code' as login_url %} + {% element form form=form method="post" action=login_url tags="entrance,login" %} + {% slot body %} + {% csrf_token %} + {% element fields form=form unlabeled=True %} + {% endelement %} + {{ redirect_field }} + {% endslot %} + {% slot actions %} + {% element button type="submit" tags="prominent,login" %} + {% translate "Request Code" %} + {% endelement %} + {% endslot %} + {% endelement %} + {% url 'account_login' as login_url %} + {% element button href=login_url tags="link" %} + {% translate "Other sign-in options" %} + {% endelement %} {% endblock content %} diff --git a/templates/account/signup-wizard.html b/templates/account/signup-wizard.html index 3dc3d472..53e2af1f 100644 --- a/templates/account/signup-wizard.html +++ b/templates/account/signup-wizard.html @@ -25,7 +25,7 @@

    {% trans 'Create your dealership account today' %}

    -
    +
    {% csrf_token %}
    @@ -55,16 +55,16 @@ - +
    - - + + {% endblock content %} - + {% block customJS %} - + - - {% endblock %} +{% endblock %} +{% block customJS %} + +{% endblock %} diff --git a/templates/bill/bill_detail.html b/templates/bill/bill_detail.html index c4df53a6..dcd01e13 100644 --- a/templates/bill/bill_detail.html +++ b/templates/bill/bill_detail.html @@ -94,7 +94,7 @@
    {% trans 'PO' %}
    - - - - - - - - - - - - - - {% for f in item_formset %} - +
    +
    {% trans 'Item' %}{% trans 'PO Qty' %}{% trans 'PO Amount' %}{% trans 'Quantity' %}{% trans 'Unit Cost' %}{% trans 'Unit' %}{% trans 'Total' %}{% trans 'Delete' %}
    + + + + + + + + + + + + + + {% for f in item_formset %} + - + - + - + - + - + - + - + - - - {% endfor %} - - - - - - - - - -
    {% trans 'Item' %}{% trans 'PO Qty' %}{% trans 'PO Amount' %}{% trans 'Quantity' %}{% trans 'Unit Cost' %}{% trans 'Unit' %}{% trans 'Total' %}{% trans 'Delete' %}
    -
    - {% for hidden_field in f.hidden_fields %}{{ hidden_field }}{% endfor %} - {{ f.item_model|add_class:"form-control" }} - {% if f.errors %}{{ f.errors }}{% endif %} -
    -
    +
    + {% for hidden_field in f.hidden_fields %}{{ hidden_field }}{% endfor %} + {{ f.item_model|add_class:"form-control" }} + {% if f.errors %}{{ f.errors }}{% endif %} +
    +
    - - {% if f.instance.po_quantity %}{{ f.instance.po_quantity }}{% endif %} - - + + {% if f.instance.po_quantity %}{{ f.instance.po_quantity }}{% endif %} + + - {% if f.instance.po_total_amount %} -
    - {{ f.instance.po_total_amount | currency_format }} - - {% trans 'View PO' %} - -
    - {% endif %} -
    + {% if f.instance.po_total_amount %} +
    + {{ f.instance.po_total_amount | currency_format }} + + {% trans 'View PO' %} + +
    + {% endif %} +
    -
    {{ f.quantity|add_class:"form-control" }}
    -
    +
    {{ f.quantity|add_class:"form-control" }}
    +
    -
    {{ f.unit_cost|add_class:"form-control" }}
    -
    +
    {{ f.unit_cost|add_class:"form-control" }}
    +
    {{ f.entity_unit|add_class:"form-control" }}{{ f.entity_unit|add_class:"form-control" }} - - {{ f.instance.total_amount | currency_format }} - - + + {{ f.instance.total_amount | currency_format }} + + - {% if item_formset.can_delete %}
    {{ f.DELETE }}
    {% endif %} -
    - {% trans 'Total' %} + + {% if item_formset.can_delete %}
    {{ f.DELETE }}
    {% endif %}
    - {{ total_amount__sum | currency_format }} -
    -
    + {% endfor %} + + + + + + + {% trans 'Total' %} + + + {{ total_amount__sum | currency_format }} + + + + + + -
    -
    -
    +
    +
    +
    {% comment %} {% if not item_formset.has_po %} @@ -122,12 +122,12 @@ {% trans 'New Item' %} {% endif %} {% endcomment %} - -
    +
    - +
    + diff --git a/templates/bill/transactions/tags/txs_table.html b/templates/bill/transactions/tags/txs_table.html index 9a9c6e45..1699ddfe 100644 --- a/templates/bill/transactions/tags/txs_table.html +++ b/templates/bill/transactions/tags/txs_table.html @@ -38,7 +38,7 @@ {% trans 'Total' %} - {{ total_credits | currency_format }} + {{ total_credits | currency_format }} {{ total_debits | currency_format }} diff --git a/templates/chart_of_accounts/coa_create.html b/templates/chart_of_accounts/coa_create.html index c7c57ac6..8e9863bc 100644 --- a/templates/chart_of_accounts/coa_create.html +++ b/templates/chart_of_accounts/coa_create.html @@ -3,7 +3,7 @@ {% load django_ledger %} {% load widget_tweaks %} {% block title %} - {% trans "Create Chart of Accounts" %} + {% trans "Create Chart of Accounts" %} {% endblock %} {% block content %}
    @@ -12,7 +12,7 @@

    {% trans 'Create Chart of Accounts' %} - +

    diff --git a/templates/chart_of_accounts/coa_list.html b/templates/chart_of_accounts/coa_list.html index 715a565a..8a2b3a3c 100644 --- a/templates/chart_of_accounts/coa_list.html +++ b/templates/chart_of_accounts/coa_list.html @@ -3,7 +3,7 @@ {% load static %} {% load icon from django_ledger %} {% block title %} - {% trans "Chart of Accounts" %} + {% trans "Chart of Accounts" %} {% endblock %} {% block content %}
    diff --git a/templates/chart_of_accounts/coa_update.html b/templates/chart_of_accounts/coa_update.html index d1cce73e..2d568ce6 100644 --- a/templates/chart_of_accounts/coa_update.html +++ b/templates/chart_of_accounts/coa_update.html @@ -3,7 +3,7 @@ {% load static %} {% load widget_tweaks %} {% block title %} - {% trans "Update chart of Account"%} + {% trans "Update chart of Account"%} {% endblock %} {% block content %}
    diff --git a/templates/chart_of_accounts/includes/coa_card.html b/templates/chart_of_accounts/includes/coa_card.html index e62bf7aa..1bbe41d8 100644 --- a/templates/chart_of_accounts/includes/coa_card.html +++ b/templates/chart_of_accounts/includes/coa_card.html @@ -9,7 +9,7 @@
    - + {% if coa_model.is_default %} {% trans 'DEFAULT' %} {% endif %} diff --git a/templates/chat_support.html b/templates/chat_support.html index 680752d4..3c1a59c3 100644 --- a/templates/chat_support.html +++ b/templates/chat_support.html @@ -30,55 +30,55 @@

    I need help with something

    -

    I can’t reorder a product I previously ordered

    - -
    -

    How do I place an order?

    - -
    -

    My payment method not working

    - -
    -
    -
    -
    - + href="#!"> +

    I can’t reorder a product I previously ordered

    + + +

    How do I place an order?

    + +
    +

    My payment method not working

    + +
    +
    +
    +
    + +
    +
    Eric
    +

    + Ask us anything – we’ll get back to you here or by email within 24 hours. +

    +
    +
    +
    + -
    Eric
    -

    - Ask us anything – we’ll get back to you here or by email within 24 hours. -

    - - - - - - diff --git a/templates/components/note_modal.html b/templates/components/note_modal.html index 7934052c..2985095a 100644 --- a/templates/components/note_modal.html +++ b/templates/components/note_modal.html @@ -16,7 +16,7 @@ +
    +
    + {% if is_paginated %} + {% include 'partials/pagination.html' %} + {% endif %}
    -
    -
    - {% if is_paginated %} - {% include 'partials/pagination.html' %} - {% endif %} -
    -
    - {% endif %} -
    - {% include 'modal/delete_modal.html' %} - {% else %} - {% url "vendor_create" request.dealer.slug as create_vendor_url %} - {% include "empty-illustration-page.html" with value="vendor" url=create_vendor_url %} - {% endif %} - {% endblock %} + + {% endif %} + + {% include 'modal/delete_modal.html' %} + {% else %} + {% url "vendor_create" request.dealer.slug as create_vendor_url %} + {% include "empty-illustration-page.html" with value="vendor" url=create_vendor_url %} + {% endif %} +{% endblock %} diff --git a/templates/welcome.html b/templates/welcome.html index 288ff21d..6f5a92b2 100644 --- a/templates/welcome.html +++ b/templates/welcome.html @@ -40,199 +40,199 @@ style="background-image:url({% static 'images/bg/bg-left-27.png' %}); background-size:auto; background-position:left"> -
    -
    -
    -
    -
    -

    {{ _("Because Inventory Needs Order") }}

    -

    - {% blocktrans %}Haikal empowers car dealers with a seamless, structured system to manage their inventory effortlessly, ensuring every vehicle is tracked, accounted for, and ready for sale with precision and efficiency.{% endblocktrans %} -

    -
    -
    -
    - - - -
    - {% trans 'Inventory Management' %} -

    - {% trans 'Effortlessly manage your car inventory with real-time updates and intuitive tools.' %} -

    -
    -
    -
    -
    -
    - - - -
    - {% trans 'Seamless Accounting' %} -

    {% trans 'Integrated double-entry accounting tailored for car dealers.' %}

    -
    -
    -
    -
    -
    - - - -
    - {% trans 'Advanced Analytics' %} -

    {% trans 'Gain insights and make data-driven decisions for your business.' %}

    -
    -
    -
    +
    +
    +
    +
    +
    +

    {{ _("Because Inventory Needs Order") }}

    +

    + {% blocktrans %}Haikal empowers car dealers with a seamless, structured system to manage their inventory effortlessly, ensuring every vehicle is tracked, accounted for, and ready for sale with precision and efficiency.{% endblocktrans %} +

    -
    -
    -
    +
    - +
    - {% trans 'CRM' %} + {% trans 'Inventory Management' %}

    - {% trans 'Specialized customer relationship management system designed for car dealers, offering streamlined sales.' %} + {% trans 'Effortlessly manage your car inventory with real-time updates and intuitive tools.' %}

    -
    -
    -
    - -
    -
    -
    -

    {{ _("Pricing") }}

    -
    - {% for plan in plan_list %} -
    - - -
    - {% endfor %} -
    -
    -
    -
    -
    -
    -
    -
    -

    {{ _("Other features") }}

    -

    {{ _("Find out other features included in Haikal") }}

    -
    -
    -
    -
    -
    -
    - -
    - {{ _("Manage Everything from one place") }} +
    +
    + + + +
    + {% trans 'Seamless Accounting' %} +

    {% trans 'Integrated double-entry accounting tailored for car dealers.' %}

    - - -
    -
    - -
    - {{ _("Advanced Dashboards for better decisions") }} +
    +
    + + + +
    + {% trans 'Advanced Analytics' %} +

    {% trans 'Gain insights and make data-driven decisions for your business.' %}

    -
    -
    - -
    -
    - +
    +
    +
    + + + +
    + {% trans 'CRM' %} +

    + {% trans 'Specialized customer relationship management system designed for car dealers, offering streamlined sales.' %} +

    +
    +
    -
    +
    + +
    +
    +
    +

    {{ _("Pricing") }}

    +
    + {% for plan in plan_list %} +
    + + +
    + {% endfor %} +
    +
    +
    +
    +
    +
    +
    +
    +

    {{ _("Other features") }}

    +

    {{ _("Find out other features included in Haikal") }}

    +
    + +
    {% endblock %} \ No newline at end of file diff --git a/templates/welcome_base.html b/templates/welcome_base.html index 8a62667d..af487458 100644 --- a/templates/welcome_base.html +++ b/templates/welcome_base.html @@ -43,7 +43,7 @@ - + {% if LANGUAGE_CODE == 'ar' %} - + {% block extraCSS %} {% endblock extraCSS %} @@ -76,11 +76,11 @@ {% block content %} {% endblock content %} - + - + @@ -88,7 +88,7 @@ - + {% block customJS %} {% endblock customJS %} From 417475ad54611f9c1db72eb7486e2dcba458c044 Mon Sep 17 00:00:00 2001 From: ismail Date: Wed, 24 Sep 2025 12:09:11 +0300 Subject: [PATCH 16/22] small fixes --- .../commands/set_custom_permissions.py | 20 +- inventory/models.py | 4 + inventory/views.py | 218 +++++++++--------- 3 files changed, 126 insertions(+), 116 deletions(-) diff --git a/inventory/management/commands/set_custom_permissions.py b/inventory/management/commands/set_custom_permissions.py index 91d7a577..c74db16f 100644 --- a/inventory/management/commands/set_custom_permissions.py +++ b/inventory/management/commands/set_custom_permissions.py @@ -7,16 +7,16 @@ from django_ledger.models import EstimateModel, BillModel, AccountModel, LedgerM class Command(BaseCommand): def handle(self, *args, **kwargs): - Permission.objects.get_or_create( - name="Can view crm", - codename="can_view_crm", - content_type=ContentType.objects.get_for_model(Lead), - ) - Permission.objects.get_or_create( - name="Can reassign lead", - codename="can_reassign_lead", - content_type=ContentType.objects.get_for_model(Lead), - ) + # Permission.objects.get_or_create( + # name="Can view crm", + # codename="can_view_crm", + # content_type=ContentType.objects.get_for_model(Lead), + # ) + # Permission.objects.get_or_create( + # name="Can reassign lead", + # codename="can_reassign_lead", + # content_type=ContentType.objects.get_for_model(Lead), + # ) Permission.objects.get_or_create( name="Can view sales", codename="can_view_sales", diff --git a/inventory/models.py b/inventory/models.py index 71fc8dfa..b2b2e1d8 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -2134,6 +2134,10 @@ class Lead(models.Model): slug = RandomCharField(length=8, unique=True) class Meta: + permissions = [ + ("can_view_crm", _("Can view CRM")), + ("can_reassign_lead", _("Can reassign lead")), + ] verbose_name = _("Lead") verbose_name_plural = _("Leads") indexes = [ diff --git a/inventory/views.py b/inventory/views.py index 27c904b9..cc435bfa 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -10259,31 +10259,52 @@ def submit_plan(request, dealer_slug): # @login_required def payment_callback(request, dealer_slug): + from django.db import transaction + payment_id = request.GET.get("id") payment_status = request.GET.get("status") message = request.GET.get("message", "") + if not payment_id: + logger.error("Missing payment ID in callback") + return render(request, "payment_failed.html", {"message": "Invalid request"}) + logger.info( f"Received payment callback for dealer_slug: {dealer_slug}, payment_id: {payment_id}, status: {payment_status}" ) - history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first() - if not history: - logger.error(f"No PaymentHistory found for transaction_id: {payment_id}") - return render( - request, "payment_failed.html", {"message": "Invalid transaction"} + with transaction.atomic(): + history = ( + models.PaymentHistory.objects + .select_for_update() + .filter(transaction_id=payment_id) + .first() ) - if history.status == "paid": - logger.info("Payment already processed. Redirecting to home.") - return redirect("home") + if not history: + logger.error(f"No PaymentHistory found for transaction_id: {payment_id}") + return render( + request, "payment_failed.html", {"message": "Invalid transaction"} + ) + + if history.status == "paid": + logger.info("Payment already processed. Redirecting to home.") + return redirect("home") + + if history.status == "processing": + logger.warning(f"Payment {payment_id} is already being processed. Skipping.") + return redirect("home") + + if history.status == "failed" and payment_status != "paid": + logger.warning(f"Payment {payment_id} already failed. Ignoring.") + return render(request, "payment_failed.html", {"message": message or "Payment failed"}) + + history.status = "processing" + history.save(update_fields=["status"]) if payment_status == "paid": - logger.info( - f"Payment successful for transaction ID {payment_id}. Creating order..." - ) + logger.info(f"Payment successful for transaction ID {payment_id}. Creating order...") - # Get metadata from PaymentHistory (passed during handle_payment) metadata = history.user_data if isinstance(metadata, str): try: @@ -10291,116 +10312,101 @@ def payment_callback(request, dealer_slug): except json.JSONDecodeError: logger.error(f"Failed to decode metadata JSON: {metadata}") metadata = {} + plan_pricing_id = metadata.get("plan_pricing_id") dealer_slug_from_meta = metadata.get("dealer_slug") if not plan_pricing_id or dealer_slug_from_meta != dealer_slug: logger.error("Invalid metadata in payment callback") history.status = "failed" - history.save() - return render( - request, "payment_failed.html", {"message": "Invalid payment data"} - ) - - dealer = get_object_or_404(models.Dealer, slug=dealer_slug) - pp = get_object_or_404(PlanPricing, pk=plan_pricing_id) - - # ✅ CREATE ORDER HERE - try: - order = Order.objects.create( - user=dealer.user, - plan=pp.plan, - pricing=pp.pricing, - amount=pp.price, - currency="SAR", # Fixed typo: was "SA" - tax=15, - status=Order.STATUS.NEW, # Use constant if available - ) - logger.info(f"Order {order.id} created for user {dealer.user}") - except Exception as e: - logger.exception(f"Failed to create order: {e}") - history.status = "failed" - history.save() - return render( - request, "payment_failed.html", {"message": "Order creation failed"} - ) - - # Create or get BillingInfo - billing_info, created = BillingInfo.objects.get_or_create( - user=dealer.user, - defaults={ - "tax_number": dealer.vrn, - "name": dealer.arabic_name, - "street": dealer.address, - "zipcode": dealer.entity.zip_code or " ", - "city": dealer.entity.city or " ", - "country": dealer.entity.country or " ", - }, - ) - if created: - logger.info(f"Created new billing info for user {dealer.user}.") - else: - logger.debug(f"Billing info already exists for user {dealer.user}.") - - # Create or update UserPlan - if not hasattr(order.user, "userplan"): - UserPlan.objects.create( - user=order.user, - plan=order.plan, - # expire=datetime.now().date() + timedelta(days=order.get_plan_pricing().pricing.period) - ) - logger.info( - f"Created new UserPlan for user {order.user} with plan {order.plan}." - ) - else: - # Optional: upgrade existing plan - # user_plan = order.user.userplan - # user_plan.plan = order.plan - # user_plan.save() - logger.info(f"UserPlan already exists for user {order.user}.") + history.save(update_fields=["status"]) + return render(request, "payment_failed.html", {"message": "Invalid payment data"}) try: - # Complete the order (this may generate invoice, etc.) - order.complete_order() - history.status = "paid" - history.order = order # Link payment to order - history.save() + dealer = get_object_or_404(models.Dealer, slug=dealer_slug) + pp = get_object_or_404(PlanPricing, pk=plan_pricing_id) + + with transaction.atomic(): + history.refresh_from_db() + if history.status == "paid": + logger.info("Payment was already completed by another request. Skipping.") + return redirect("home") + + order = Order.objects.create( + user=dealer.user, + plan=pp.plan, + pricing=pp.pricing, + amount=pp.price, + currency="SAR", + tax=15, + status=Order.STATUS.NEW, + ) + logger.info(f"Order {order.id} created for user {dealer.user}") + + billing_info, created = BillingInfo.objects.get_or_create( + user=dealer.user, + defaults={ + "tax_number": dealer.vrn, + "name": dealer.arabic_name, + "street": dealer.address, + "zipcode": dealer.entity.zip_code or " ", + "city": dealer.entity.city or " ", + "country": dealer.entity.country or " ", + }, + ) + + # Create UserPlan if missing + if not hasattr(order.user, "userplan"): + UserPlan.objects.create( + user=order.user, + plan=order.plan, + ) + logger.info(f"Created new UserPlan for user {order.user} with plan {order.plan}.") + else: + logger.info(f"UserPlan already exists for user {order.user}.") + + order.complete_order() + + history.status = "paid" + history.order = order + history.save(update_fields=["status", "order"]) invoice = order.get_invoices().first() - - logger.info(f"Order {order.id} completed. Rendering success page.") + logger.info(f"Order {order.id} completed successfully.") return render( request, "payment_success.html", {"order": order, "invoice": invoice} ) except Exception as e: - logger.exception(f"Error completing order {order.id}: {e}") + logger.exception(f"Error processing paid payment {payment_id}: {e}") + # Mark as failed history.status = "failed" - history.save() - return render( - request, "payment_failed.html", {"message": "Plan activation error"} - ) + history.save(update_fields=["status"]) + return render(request, "payment_failed.html", {"message": "Payment processing error"}) finally: - # Activate dealer & staff if needed - if dealer := getattr(order.user, "dealer", None): - if not dealer.user.is_active: - dealer.user.is_active = True - dealer.user.save() - for staff in dealer.get_staff(): - if not staff.user.is_active: - staff.activate_account() + try: + if dealer := getattr(order.user, "dealer", None): + if not dealer.user.is_active: + dealer.user.is_active = True + dealer.user.save() + for staff in dealer.get_staff(): + if not staff.user.is_active: + staff.activate_account() + except Exception as ex: + logger.warning(f"Failed to activate dealer/staff: {ex}") elif payment_status == "failed": - logger.warning( - f"Payment failed for transaction ID {payment_id}. Message: {message}" - ) + logger.warning(f"Payment failed for transaction ID {payment_id}. Message: {message}") history.status = "failed" - history.save() + history.save(update_fields=["status"]) return render(request, "payment_failed.html", {"message": message}) - return render(request, "payment_failed.html", {"message": "Unknown payment status"}) - + else: + logger.warning(f"Unknown payment status: {payment_status}") + history.status = "failed" + history.save(update_fields=["status"]) + return render(request, "payment_failed.html", {"message": "Unknown payment status"}) # @login_required # @permission_required("inventory.change_dealer", raise_exception=True) @@ -11555,16 +11561,16 @@ def upload_cars(request, dealer_slug, pk=None): try: if item: # data = [x.strip() for x in item.item_model.name.split("||")] - make = models.CarMake.objects.get(pk=item.addition_info.get("make")) - model = models.CarModel.objects.get(pk=item.addition_info.get("model")) - trim = models.CarTrim.objects.get(pk=item.addition_info.get("trim")) - serie = models.CarSerie.objects.get(pk=item.addition_info.get("serie")) - year = item.addition_info.get("year") + make = models.CarMake.objects.get(pk=item.additional_info.get("make")) + model = models.CarModel.objects.get(pk=item.additional_info.get("model")) + trim = models.CarTrim.objects.get(pk=item.additional_info.get("trim")) + serie = models.CarSerie.objects.get(pk=item.additional_info.get("serie")) + year = item.additional_info.get("year") exterior = models.ExteriorColors.objects.get( - pk=item.addition_info.get("exterior") + pk=item.additional_info.get("exterior") ) interior = models.InteriorColors.objects.get( - pk=item.addition_info.get("interior") + pk=item.additional_info.get("interior") ) receiving_date = timezone.now() vendor_model = item.bill_model.vendor From 82a8ccc44d4419f9a6d5ae7cb44401fd172ccaed Mon Sep 17 00:00:00 2001 From: ismail Date: Wed, 24 Sep 2025 12:14:57 +0300 Subject: [PATCH 17/22] small fixes --- inventory/views.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/inventory/views.py b/inventory/views.py index cc435bfa..580eac71 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -11561,16 +11561,16 @@ def upload_cars(request, dealer_slug, pk=None): try: if item: # data = [x.strip() for x in item.item_model.name.split("||")] - make = models.CarMake.objects.get(pk=item.additional_info.get("make")) - model = models.CarModel.objects.get(pk=item.additional_info.get("model")) - trim = models.CarTrim.objects.get(pk=item.additional_info.get("trim")) - serie = models.CarSerie.objects.get(pk=item.additional_info.get("serie")) - year = item.additional_info.get("year") + make = models.CarMake.objects.get(pk=item.item_mode.additional_info.get("make")) + model = models.CarModel.objects.get(pk=item.item_mode.additional_info.get("model")) + trim = models.CarTrim.objects.get(pk=item.item_mode.additional_info.get("trim")) + serie = models.CarSerie.objects.get(pk=item.item_mode.additional_info.get("serie")) + year = item.item_mode.additional_info.get("year") exterior = models.ExteriorColors.objects.get( - pk=item.additional_info.get("exterior") + pk=item.item_mode.additional_info.get("exterior") ) interior = models.InteriorColors.objects.get( - pk=item.additional_info.get("interior") + pk=item.item_mode.additional_info.get("interior") ) receiving_date = timezone.now() vendor_model = item.bill_model.vendor From 1fc4ff706a69ec5fe98c2d16d642c9f3e1dbe03c Mon Sep 17 00:00:00 2001 From: ismail Date: Wed, 24 Sep 2025 12:18:11 +0300 Subject: [PATCH 18/22] small fixes --- inventory/views.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/inventory/views.py b/inventory/views.py index 580eac71..a79df747 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -10369,7 +10369,7 @@ def payment_callback(request, dealer_slug): history.status = "paid" history.order = order - history.save(update_fields=["status", "order"]) + history.save(update_fields=["status"]) invoice = order.get_invoices().first() logger.info(f"Order {order.id} completed successfully.") @@ -11561,16 +11561,16 @@ def upload_cars(request, dealer_slug, pk=None): try: if item: # data = [x.strip() for x in item.item_model.name.split("||")] - make = models.CarMake.objects.get(pk=item.item_mode.additional_info.get("make")) - model = models.CarModel.objects.get(pk=item.item_mode.additional_info.get("model")) - trim = models.CarTrim.objects.get(pk=item.item_mode.additional_info.get("trim")) - serie = models.CarSerie.objects.get(pk=item.item_mode.additional_info.get("serie")) - year = item.item_mode.additional_info.get("year") + make = models.CarMake.objects.get(pk=item.item_model.additional_info.get("make")) + model = models.CarModel.objects.get(pk=item.item_model.additional_info.get("model")) + trim = models.CarTrim.objects.get(pk=item.item_model.additional_info.get("trim")) + serie = models.CarSerie.objects.get(pk=item.item_model.additional_info.get("serie")) + year = item.item_model.additional_info.get("year") exterior = models.ExteriorColors.objects.get( - pk=item.item_mode.additional_info.get("exterior") + pk=item.item_model.additional_info.get("exterior") ) interior = models.InteriorColors.objects.get( - pk=item.item_mode.additional_info.get("interior") + pk=item.item_model.additional_info.get("interior") ) receiving_date = timezone.now() vendor_model = item.bill_model.vendor From 227b4932e51e51639bc1e6ea3ea533c1cfba9382 Mon Sep 17 00:00:00 2001 From: Faheed Date: Wed, 24 Sep 2025 13:13:59 +0300 Subject: [PATCH 19/22] new chnages: --- templates/groups/group_list.html | 133 ++++++++++----------- templates/sales/invoices/invoice_list.html | 109 +++++++++-------- 2 files changed, 125 insertions(+), 117 deletions(-) diff --git a/templates/groups/group_list.html b/templates/groups/group_list.html index 7e7ad146..b858b3b6 100644 --- a/templates/groups/group_list.html +++ b/templates/groups/group_list.html @@ -2,78 +2,71 @@ {% load i18n %} {% load custom_filters %} {% load render_table from django_tables2 %} + {% block title %} {% trans "Groups" %} {% endblock title %} + {% block content %} -
    -
    - {% if groups or request.GET.q %} - -
    - -
    -
    - - - - - - - - - - - {% for group in groups %} - - - - - - - {% endfor %} - -
    {% trans 'name'|capfirst %}{% trans 'total Users'|capfirst %}{% trans 'total permission'|capfirst %} - {% trans 'actions'|capfirst %} -
    {{ group.name }} - {{ group.users.count }} - - {{ group.permissions.count }} - - - - {% trans 'view Permissions'|capfirst %} - -
    -
    -
    - {% if page_obj.paginator.num_pages > 1 %} - - {% endif %} +
    + {% if groups or request.GET.q %} +
    +

    + + {% trans "Groups" %} +

    + - {% else %} - {% url "group_create" request.dealer.slug as create_group_url %} - {% include "empty-illustration-page.html" with value="group" url=create_group_url %} - {% endif %} -
    -
    -{% endblock %} +
    +
    +
    +
    + + + + + + + + + + + {% for group in groups %} + + + + + + + {% endfor %} + +
    {% trans 'Name'|capfirst %}{% trans 'Total Users'|capfirst %}{% trans 'Total Permissions'|capfirst %}{% trans 'Actions'|capfirst %}
    {{ group.name }} + {{ group.users.count }} + + {{ group.permissions.count }} + + + {% trans 'View' %} + +
    +
    +
    + {% if page_obj.paginator.num_pages > 1 %} +
    {% include 'partials/pagination.html' %}
    + {% endif %} +
    + {% else %} + {% url "group_create" request.dealer.slug as create_group_url %} + {% include "empty-illustration-page.html" with value="group" url=create_group_url %} + {% endif %} +
    +{% endblock %} \ No newline at end of file diff --git a/templates/sales/invoices/invoice_list.html b/templates/sales/invoices/invoice_list.html index 8f674b6f..f3eb4ae1 100644 --- a/templates/sales/invoices/invoice_list.html +++ b/templates/sales/invoices/invoice_list.html @@ -4,69 +4,80 @@ {{ _("Invoices") }} {% endblock title %} {% block content %} +
    {% if invoices or request.GET.q %} -
    -
    -
    -
    -

    - {% trans "Invoices" %} -

    -
    +
    +
    + +
    +
    +

    + {% trans "Invoices" %} + +

    +

    + {% trans "Manage and track all your customer invoices." %} +

    - {% comment %}
    -
    {% include 'partials/search_box.html' %}
    -
    {% endcomment %}
    -
    - - - - - - - - - + + {% comment %} +
    + {% include 'partials/search_box.html' %} +
    + {% endcomment %} + +
    +
    {% trans "Invoice Number" %}{% trans "Customer" %}{% trans "Status" %}{% trans "Status Date" %}{% trans "Created" %}{% trans "Actions" %}
    + + + + + + + + - + {% 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_number }}{{ invoice.customer }} {% if invoice.is_past_due %} - {% trans "Past Due" %} + {% trans "Past Due" %} {% elif invoice.is_approved %} - {% trans "Approved" %} + {% trans "Approved" %} {% elif invoice.is_canceled %} - {% trans "Canceled" %} + {% trans "Canceled" %} {% elif invoice.is_draft %} - {% trans "Draft" %} + {% trans "Draft" %} {% elif invoice.is_review %} - {% trans "In Review" %} + {% trans "In Review" %} {% elif invoice.is_paid %} - {% trans "Paid" %} + {% trans "Paid" %} {% endif %} + {% if invoice.invoice_status == "in_review" %} - {{ invoice.date_in_review }} + {{ invoice.date_in_review|date:"M d, Y"|default:"N/A" }} {% elif invoice.invoice_status == "approved" %} - {{ invoice.date_approved }} + {{ invoice.date_approved|date:"M d, Y"|default:"N/A" }} {% elif invoice.invoice_status == "canceled" %} - {{ invoice.date_canceled }} + {{ invoice.date_canceled|date:"M d, Y"|default:"N/A" }} {% elif invoice.invoice_status == "draft" %} - {{ invoice.date_draft }} + {{ invoice.date_draft|date:"M d, Y"|default:"N/A" }} {% elif invoice.invoice_status == "paid" %} - {{ invoice.date_paid }} + {{ invoice.date_paid|date:"M d, Y"|default:"N/A" }} {% endif %} {{ invoice.created }} + + {{ invoice.created|date:"M d, Y" }} + + class="btn btn-sm btn-primary"> {% trans "View" %} @@ -74,20 +85,24 @@
    {% trans "No Invoice Found" %}{% trans "No invoices found." %}
    + {% if page_obj.paginator.num_pages > 1 %} -
    -
    {% include 'partials/pagination.html' %}
    +
    + {% include 'partials/pagination.html' %}
    {% endif %} +
    +
    {% else %} - {% url 'estimate_create' request.dealer.slug as url %} - {% include "empty-illustration-page.html" with value=_("invoice") url=url %} + {% url 'estimate_create' request.dealer.slug as create_url %} + {% include "empty-illustration-page.html" with title=_("No Invoices Yet") subtitle=_("Looks like you haven't created any invoices. Start by creating a new invoice or quotation.") url=create_url button_text=_("Create First Invoice") %} {% endif %} -{% endblock %} +
    +{% endblock %} \ No newline at end of file From 7dbf4c4bd619240e9d6c88039f6c9ecc9360e51e Mon Sep 17 00:00:00 2001 From: Faheed Date: Wed, 24 Sep 2025 13:36:02 +0300 Subject: [PATCH 20/22] new update --- templates/groups/group_list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/groups/group_list.html b/templates/groups/group_list.html index b858b3b6..15d448aa 100644 --- a/templates/groups/group_list.html +++ b/templates/groups/group_list.html @@ -51,7 +51,7 @@ - {% trans 'View' %} + {% trans 'View Permissions' %} From 57aff69153e9f5f75e6bfe072473d8eda9052e58 Mon Sep 17 00:00:00 2001 From: ismail Date: Wed, 24 Sep 2025 13:53:38 +0300 Subject: [PATCH 21/22] update the opportunity --- inventory/views.py | 26 +++++++++++++++++-- .../crm/opportunities/opportunity_detail.html | 2 +- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/inventory/views.py b/inventory/views.py index a79df747..dc37e09f 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -5438,7 +5438,21 @@ def update_estimate_additionals(request, dealer_slug, pk): if form.is_valid(): estimate = get_object_or_404(EstimateModel, pk=pk) car = estimate.get_itemtxs_data()[0].first().item_model.car - car.additional_services.set(form.cleaned_data["additional_finances"]) + additionals = form.cleaned_data["additional_finances"] + car.additional_services.set(additionals) + additionals = [additional.pk for additional in additionals] + extra_info = models.ExtraInfo.objects.get( + dealer=dealer, + content_type=ContentType.objects.get_for_model(EstimateModel), + object_id=estimate.pk, + ) + extra_info.data.update({"additionals": additionals}) + extra_info.save() + # for i in additionals: + + # if ex: + # ex.delete() + car.save() messages.success(request, "Additional Finances updated successfully") return redirect("estimate_detail", dealer_slug=dealer_slug, pk=pk) @@ -12540,10 +12554,18 @@ def create_estimate_for_car(request, dealer_slug, slug): @require_POST def estimate_create_from_opportunity(request, dealer_slug, slug): opportunity = get_object_or_404(models.Opportunity, slug=slug) + if opportunity.estimate: + messages.error( + request, + "An estimate has already been created for this opportunity.", + ) + return redirect( + "opportunity_detail", dealer_slug=dealer_slug, slug=opportunity.slug + ) dealer = get_object_or_404(models.Dealer, slug=dealer_slug) car = opportunity.car customer = opportunity.customer - # TODO: set safe guard, so it doesnt recreate it + if not all([dealer, car, customer]): messages.error(request, "Please correct the errors below.") return redirect( diff --git a/templates/crm/opportunities/opportunity_detail.html b/templates/crm/opportunities/opportunity_detail.html index 6006563d..7538b364 100644 --- a/templates/crm/opportunities/opportunity_detail.html +++ b/templates/crm/opportunities/opportunity_detail.html @@ -28,7 +28,7 @@ href="{% url 'estimate_detail' request.dealer.slug opportunity.estimate.pk %}">{{ _("View Quotation") }} {% endif %} {% else %} - {% if perms.django_ledger.add_estimatemodel %} + {% if perms.django_ledger.add_estimatemodel and not opportunity.estimate %} Date: Wed, 24 Sep 2025 14:55:36 +0300 Subject: [PATCH 22/22] update the estimate details additionals --- inventory/utils.py | 2 +- inventory/views.py | 48 +++++++++++++++---- .../sales/estimates/estimate_detail.html | 7 +-- 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/inventory/utils.py b/inventory/utils.py index 89df3fd5..350b18ac 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -1325,8 +1325,8 @@ def get_finance_data(estimate, dealer): ) discount = extra_info.data.get("discount", 0) discount = Decimal(discount) - additional_services = car.get_additional_services() + discounted_price = Decimal(car.marked_price) - discount vat_amount = discounted_price * vat.rate total_services_amount = additional_services.get("total") diff --git a/inventory/views.py b/inventory/views.py index dc37e09f..5d8256c8 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -5268,13 +5268,31 @@ class EstimateDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView kwargs["invoice"] = invoice_obj try: car = estimate.get_itemtxs_data()[0].first().item_model.car - selected_items = car.additional_services.filter(dealer=dealer) + extra_info = models.ExtraInfo.objects.get( + dealer=dealer, + content_type=ContentType.objects.get_for_model(EstimateModel), + object_id=estimate.pk + ) + try: + additionals = extra_info.data.get("additionals") + if additionals: + selected_items = models.AdditionalServices.objects.filter(dealer=dealer,pk__in=additionals) + else: + selected_items = [] + except Exception as e: + selected_items = [] + if estimate.is_draft() or estimate.is_review(): + kwargs["grand_total"] = finance_data.get("final_price") + sum([x.price_ for x in selected_items]) + else: + kwargs["grand_total"] = finance_data.get("grand_total") form = forms.AdditionalFinancesForm() form.fields["additional_finances"].queryset = form.fields[ "additional_finances" ].queryset.filter(dealer=dealer) # form.initial["additional_finances"] = selected_items kwargs["additionals_form"] = form + kwargs["additional_finances"] = selected_items + except Exception as e: logger.error(e) return super().get_context_data(**kwargs) @@ -5439,8 +5457,9 @@ def update_estimate_additionals(request, dealer_slug, pk): estimate = get_object_or_404(EstimateModel, pk=pk) car = estimate.get_itemtxs_data()[0].first().item_model.car additionals = form.cleaned_data["additional_finances"] - car.additional_services.set(additionals) + # car.additional_services.set(additionals) additionals = [additional.pk for additional in additionals] + extra_info = models.ExtraInfo.objects.get( dealer=dealer, content_type=ContentType.objects.get_for_model(EstimateModel), @@ -5448,11 +5467,6 @@ def update_estimate_additionals(request, dealer_slug, pk): ) extra_info.data.update({"additionals": additionals}) extra_info.save() - # for i in additionals: - - # if ex: - # ex.delete() - car.save() messages.success(request, "Additional Finances updated successfully") return redirect("estimate_detail", dealer_slug=dealer_slug, pk=pk) @@ -5609,7 +5623,7 @@ def estimate_mark_as(request, dealer_slug, pk): dealer = get_object_or_404(models.Dealer, slug=dealer_slug) estimate = get_object_or_404(EstimateModel, pk=pk) mark = request.GET.get("mark") - print(mark) + if mark: if mark == "review": if not estimate.can_review(): @@ -5629,6 +5643,24 @@ def estimate_mark_as(request, dealer_slug, pk): # Reserve The Car car = estimate.get_itemtxs_data()[0].first().item_model.car reserve_car(car, request) + extra_info = models.ExtraInfo.objects.get( + dealer=dealer, + content_type=ContentType.objects.get_for_model(EstimateModel), + object_id=estimate.pk + ) + try: + additionals = extra_info.data.get("additionals") + if additionals: + selected_items = models.AdditionalServices.objects.filter(dealer=dealer,pk__in=additionals) + else: + selected_items = [] + except Exception as e: + logger.error(e) + selected_items = [] + if selected_items: + car.additional_services.clear() + car.additional_services.set(selected_items) + messages.success(request, _("Quotation approved successfully")) return redirect("estimate_list", dealer_slug=dealer.slug) elif mark == "rejected": diff --git a/templates/sales/estimates/estimate_detail.html b/templates/sales/estimates/estimate_detail.html index a849012a..9dc14e6d 100644 --- a/templates/sales/estimates/estimate_detail.html +++ b/templates/sales/estimates/estimate_detail.html @@ -293,8 +293,9 @@ {% trans "Additional Services" %} - {% for service in data.additional_services.services %} - + {{ service.0.name }} - {{ service.0.price_|floatformat }} + + {% for service in additional_finances %} + + {{ service.name }} - {{ service.price_|floatformat }}
    {% endfor %} {% if estimate.is_draft %} @@ -310,7 +311,7 @@ {% trans "Grand Total" %} - {{ data.grand_total|floatformat }} + {{ grand_total|floatformat }}