From 223f11133c61a500c0ae977a55373a02a7040eb3 Mon Sep 17 00:00:00 2001 From: ismail Date: Thu, 7 Aug 2025 15:40:09 +0300 Subject: [PATCH] update the customer and vendor --- inventory/forms.py | 37 +++++++++++-- inventory/middleware.py | 7 ++- inventory/models.py | 62 ++++++++++++++++++++- inventory/signals.py | 30 ++++++++++- inventory/tasks.py | 2 +- inventory/urls.py | 10 +++- inventory/views.py | 75 +++++++++++++++++++++++++- templates/header.html | 23 ++++---- templates/support/create_ticket.html | 21 ++++++++ templates/support/help_center.html | 13 +++++ templates/support/ticket_detail.html | 59 ++++++++++++++++++++ templates/support/ticket_list.html | 80 ++++++++++++++++++++++++++++ templates/support/ticket_update.html | 20 +++++++ 13 files changed, 411 insertions(+), 28 deletions(-) create mode 100644 templates/support/create_ticket.html create mode 100644 templates/support/help_center.html create mode 100644 templates/support/ticket_detail.html create mode 100644 templates/support/ticket_list.html create mode 100644 templates/support/ticket_update.html diff --git a/inventory/forms.py b/inventory/forms.py index 3286cee3..4bdd23c6 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -56,6 +56,7 @@ from .models import ( DealerSettings, Tasks, Recall, + Ticket ) from django_ledger import models as ledger_models from django.forms import ( @@ -1304,15 +1305,15 @@ class OpportunityStageForm(forms.ModelForm): :type Meta.fields: list """ - + class Meta: model = Opportunity fields = [ "stage", - + ] - - + + class InvoiceModelCreateForm(InvoiceModelCreateFormBase): @@ -2202,3 +2203,31 @@ class RecallCreateForm(forms.ModelForm): 'year_to': forms.NumberInput(attrs={'class': 'form-control'}), } + +class TicketForm(forms.ModelForm): + class Meta: + model = Ticket + fields = ['subject', 'description', 'priority'] + widgets = { + 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}), + } + + +class TicketResolutionForm(forms.ModelForm): + resolution_notes = forms.CharField( + widget=forms.Textarea(attrs={'rows': 3}), + required=False, + help_text="Optional notes about how the issue was resolved." + ) + + class Meta: + model = Ticket + fields = ['status'] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Limit status choices to resolution options + self.fields['status'].choices = [ + ('resolved', 'Resolved'), + ('closed', 'Closed') + ] \ No newline at end of file diff --git a/inventory/middleware.py b/inventory/middleware.py index e9b1ccc6..4de7968a 100644 --- a/inventory/middleware.py +++ b/inventory/middleware.py @@ -155,9 +155,12 @@ class DealerSlugMiddleware: "/ar/signup/", "/en/signup/", "/ar/login/", "/en/login/", "/ar/logout/", "/en/logout/", "/en/ledger/", "/ar/ledger/", "/en/notifications/", "/ar/notifications/", "/en/appointment/", - "/ar/appointment/", "/en/feature/recall/","ar/feature/recall/" + "/ar/appointment/", "/en/feature/recall/","/ar/feature/recall/", + "/ar/help_center/", "/en/help_center/", ] - if any(request.path_info.startswith(path) for path in paths): + print("------------------------------------") + print(request.path in paths) + if request.path in paths: return None if not request.user.is_authenticated: diff --git a/inventory/models.py b/inventory/models.py index e3700eb9..f443af86 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -3506,4 +3506,64 @@ class RecallNotification(models.Model): verbose_name_plural = _("Recall Notifications") def __str__(self): - return f"Notification for {self.dealer} about {self.recall}" \ No newline at end of file + return f"Notification for {self.dealer} about {self.recall}" + +class Ticket(models.Model): + STATUS_CHOICES = [ + ('open', 'Open'), + ('in_progress', 'In Progress'), + ('resolved', 'Resolved'), + ('closed', 'Closed'), + ] + + PRIORITY_CHOICES = [ + ('low', 'Low'), + ('medium', 'Medium'), + ('high', 'High'), + ('critical', 'Critical'), + ] + + dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='tickets') + subject = models.CharField(max_length=200) + description = models.TextField() + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='open') + priority = models.CharField(max_length=20, choices=PRIORITY_CHOICES, default='medium') + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + @property + def time_to_resolution(self): + """ + Calculate the time taken to resolve the ticket. + Returns None if ticket isn't resolved/closed. + Returns timedelta if resolved/closed. + """ + if self.status in ['resolved', 'closed'] and self.created_at: + return self.updated_at - self.created_at + return None + + @property + def time_to_resolution_display(self): + """ + Returns a human-readable version of time_to_resolution + """ + resolution_time = self.time_to_resolution + if not resolution_time: + return "Not resolved yet" + + days = resolution_time.days + hours, remainder = divmod(resolution_time.seconds, 3600) + minutes, _ = divmod(remainder, 60) + + parts = [] + if days > 0: + parts.append(f"{days} day{'s' if days != 1 else ''}") + if hours > 0: + parts.append(f"{hours} hour{'s' if hours != 1 else ''}") + if minutes > 0 or not parts: + parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}") + + return ", ".join(parts) + + def __str__(self): + return f"#{self.id} - {self.subject} ({self.status})" \ No newline at end of file diff --git a/inventory/signals.py b/inventory/signals.py index 874d1e33..21947d35 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -27,7 +27,8 @@ from django.db import transaction from django_q.tasks import async_task from plans.models import UserPlan from plans.signals import order_completed, activate_user_plan - +from inventory.tasks import send_email +from django.conf import settings # logging import logging @@ -1215,4 +1216,29 @@ def bill_model_in_approve_notification(sender, instance, created, **kwargs): # kwargs={"dealer_slug": dealer.slug, "entity_slug": dealer.entity.slug, "bill_pk": instance.pk}, # ), # ), -# ) \ No newline at end of file +# ) + + + +@receiver(post_save, sender=models.Ticket) +def send_ticket_notification(sender, instance, created, **kwargs): + if created: + subject = f"New Support Ticket: {instance.subject}" + message = f""" + A new support ticket has been created: + + Ticket ID: #{instance.id} + Subject: {instance.subject} + Priority: {instance.get_priority_display()} + Description: + {instance.description} + + Please log in to the admin panel to respond. + """ + + send_email( + settings.DEFAULT_FROM_EMAIL, + [settings.SUPPORT_EMAIL], + subject, + message, + ) \ No newline at end of file diff --git a/inventory/tasks.py b/inventory/tasks.py index fecf7fda..b04b49af 100644 --- a/inventory/tasks.py +++ b/inventory/tasks.py @@ -69,7 +69,7 @@ def create_coa_accounts(instance): "role": roles.ASSET_CA_CASH, "balance_type": roles.DEBIT, "locked": False, - "default": False, + "default": False, }, { "code": "1030", diff --git a/inventory/urls.py b/inventory/urls.py index c92f9901..2fae0256 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -933,7 +933,7 @@ urlpatterns = [ views.ItemServiceUpdateView.as_view(), name="item_service_update", ), - + # Expanese path( "/items/expeneses/", @@ -1292,7 +1292,13 @@ urlpatterns = [ # staff profile 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/tickets//ticket_mark_resolved/', views.ticket_mark_resolved, name='ticket_mark_resolved'), ] diff --git a/inventory/views.py b/inventory/views.py index fac1f7c5..8179a9ab 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -2466,7 +2466,9 @@ class CustomerCreateView( success_message = _("Customer created successfully") def form_valid(self, form): + dealer = self.request.kwargs.get("dealer_slug") if customer := models.Customer.objects.filter( + dealer=dealer, email=form.instance.email ).first(): if not customer.active: @@ -2683,7 +2685,8 @@ class VendorCreateView( permission_required = ["inventory.add_vendor"] def form_valid(self, form): - if vendor := models.Vendor.objects.filter(email=form.instance.email).first(): + dealer = self.request.kwargs["dealer_slug"] + if vendor := models.Vendor.objects.filter(dealer=dealer,email=form.instance.email).first(): if not vendor.active: messages.error( self.request, @@ -11132,4 +11135,72 @@ def schedule_calendar(request,dealer_slug): 'schedules': user_schedules, 'upcoming_schedules':upcoming_schedules } - return render(request, 'schedule_calendar.html', context) \ No newline at end of file + return render(request, 'schedule_calendar.html', context) + + +# Support +@login_required +def help_center(request): + return render(request, 'support/help_center.html') + +@login_required +def create_ticket(request): + if not request.is_dealer: + return redirect('home') + + if request.method == 'POST': + form = forms.TicketForm(request.POST) + if form.is_valid(): + instance = form.save(commit=False) + instance.dealer = request.dealer + instance.save() + messages.success(request, 'Your support ticket has been submitted successfully!') + return redirect('ticket_list') + else: + form = forms.TicketForm() + + return render(request, 'support/create_ticket.html', {'form': form}) + +@login_required +def ticket_list(request): + tickets = models.Ticket.objects.all().order_by('-created_at') + if request.is_dealer: + tickets = tickets = tickets.filter(dealer=request.dealer) + return render(request, 'support/ticket_list.html', {'tickets': tickets}) + +@login_required +def ticket_detail(request, ticket_id): + ticket = models.Ticket.objects.get(id=ticket_id) + return render(request, 'support/ticket_detail.html', {'ticket': ticket}) +@login_required +def ticket_mark_resolved(request, ticket_id): + ticket = models.Ticket.objects.get(id=ticket_id) + ticket.status = 'resolved' + ticket.save() + 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}) + +def ticket_update(request, ticket_id): + ticket = models.Ticket.objects.get(id=ticket_id) + + 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', ticket_id=ticket.id) + else: + form = forms.TicketResolutionForm(instance=ticket) + + return render(request, 'support/ticket_update.html', { + 'ticket': ticket, + 'form': form + }) \ No newline at end of file diff --git a/templates/header.html b/templates/header.html index ce1e99c2..89219412 100644 --- a/templates/header.html +++ b/templates/header.html @@ -498,39 +498,34 @@