main #196

Merged
ismail merged 2 commits from main into frontend 2025-08-25 12:22:53 +03:00
15 changed files with 94 additions and 67 deletions

View File

@ -71,6 +71,7 @@ admin.site.register(models.Notes)
admin.site.register(models.UserActivityLog)
admin.site.register(models.DealersMake)
admin.site.register(models.ExtraInfo)
admin.site.register(models.Ticket)
@admin.register(models.Car)

View File

@ -2214,15 +2214,10 @@ class TicketForm(forms.ModelForm):
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']
fields = ['status', 'resolution_notes']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@ -3220,7 +3220,7 @@ class CustomGroup(models.Model):
"activity",
"payment",
"vendor",
],
other_perms=[
"view_car",
@ -3232,7 +3232,7 @@ class CustomGroup(models.Model):
"view_leads",
"view_opportunity",
'view_customer'
],
)
self.set_permissions(
@ -3679,6 +3679,7 @@ class Ticket(models.Model):
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='tickets')
subject = models.CharField(max_length=200)
description = models.TextField()
resolution_notes = models.TextField(blank=True, null=True)
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)

View File

@ -67,9 +67,12 @@ class PurchaseOrderModelUpdateView(
def get_context_data(self, itemtxs_formset=None, **kwargs):
dealer = get_object_or_404(Dealer, slug=self.kwargs["dealer_slug"])
po_model: PurchaseOrderModel = self.object
context = super().get_context_data(**kwargs)
context["entity_slug"] = dealer.entity.slug
po_model: PurchaseOrderModel = self.object
context["po_ready_to_fulfill"] = [item for item in po_model.get_itemtxs_data()[0] if item.po_item_status == 'received']
if not itemtxs_formset:
itemtxs_qs = self.get_po_itemtxs_qs(po_model)
itemtxs_qs, itemtxs_agg = po_model.get_itemtxs_data(queryset=itemtxs_qs)
@ -530,6 +533,7 @@ class BillModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateVie
context["page_title"] = title
context["header_title"] = title
context["header_subtitle"] = bill_model.get_bill_status_display()
context["can_mark_as_paid"] = bill_model.amount_paid == bill_model.amount_due
if not bill_model.is_configured():
messages.add_message(

View File

@ -1244,4 +1244,20 @@ def send_ticket_notification(sender, instance, created, **kwargs):
[settings.SUPPORT_EMAIL],
subject,
message,
)
)
else:
models.Notification.objects.create(
user=instance.dealer.user,
message=_(
"""
Support Ticket #{ticket_number} has been updated.
<a href="{url}" target="_blank">View</a>.
"""
).format(
ticket_number=instance.pk,
url=reverse(
"ticket_detail",
kwargs={"dealer_slug": instance.dealer.slug, "ticket_id": instance.pk},
),
),
)

View File

@ -1322,9 +1322,9 @@ urlpatterns = [
path('<slug:dealer_slug>/staff/<slug:slug>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/<int:ticket_id>/', views.ticket_detail, name='ticket_detail'),
path('<slug:dealer_slug>/help_center/tickets/', views.ticket_list, name='ticket_list'),
path('help_center/tickets/<slug:dealer_slug>/create/', views.create_ticket, name='create_ticket'),
path('<slug:dealer_slug>/help_center/tickets/<int:ticket_id>/', views.ticket_detail, name='ticket_detail'),
path('help_center/tickets/<int:ticket_id>/update/', views.ticket_update, name='ticket_update'),
# path('help_center/tickets/<int:ticket_id>/ticket_mark_resolved/', views.ticket_mark_resolved, name='ticket_mark_resolved'),

View File

@ -457,10 +457,10 @@ def general_dashboard(request,dealer_slug):
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
# Sales breakdown by type
new_cars_sold = cars_sold_filtered.filter(stock_type='new')
total_new_cars_sold = new_cars_sold.count()
@ -469,13 +469,13 @@ def general_dashboard(request,dealer_slug):
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
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')
@ -488,7 +488,7 @@ def general_dashboard(request,dealer_slug):
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
# Service & Overall KPIs
@ -1483,7 +1483,7 @@ def inventory_stats_view(request, dealer_slug):
"""
# Base queryset for cars belonging to the dealer
cars = models.Car.objects.filter(dealer=request.dealer)
cars = models.Car.objects.filter(dealer=request.dealer)
# Count for total, reserved, showroom, and unreserved cars
total_cars = cars.count()
reserved_cars = models.CarReservation.objects.count()
@ -10589,7 +10589,7 @@ 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'])
po_model: PurchaseOrderModel = self.object
po_items_qs, item_data = po_model.get_itemtxs_data(
queryset=po_model.itemtransactionmodel_set.all().select_related(
@ -10890,6 +10890,7 @@ def upload_cars(request, dealer_slug, pk=None):
year=int(year_model),
vendor=vendor,
receiving_date=receiving_date,
cost_price=po_item.item.unit_cost,
)
# if po_item: #TODO:update
# models.CarFinance.objects.create(
@ -11076,7 +11077,7 @@ def car_sale_report_view(request, dealer_slug):
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
cars_sold = models.Car.objects.filter(dealer=dealer, status='sold')
@ -11410,18 +11411,18 @@ def help_center(request):
@login_required
@permission_required('inventory.add_ticket')
def create_ticket(request):
def create_ticket(request,dealer_slug):
if not request.is_dealer:
return redirect('home')
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
if request.method == 'POST':
form = forms.TicketForm(request.POST)
if form.is_valid():
instance = form.save(commit=False)
instance.dealer = request.dealer
instance.dealer = dealer
instance.save()
messages.success(request, 'Your support ticket has been submitted successfully!')
return redirect('ticket_list')
return redirect('ticket_list',dealer_slug=dealer.slug)
else:
form = forms.TicketForm()
@ -11429,16 +11430,16 @@ def create_ticket(request):
@login_required
@permission_required('inventory.view_ticket')
def ticket_list(request):
tickets = models.Ticket.objects.all().order_by('-created_at')
if request.is_dealer:
tickets = tickets = tickets.filter(dealer=request.dealer)
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')
return render(request, 'support/ticket_list.html', {'tickets': tickets})
@login_required
@permission_required('inventory.change_ticket')
def ticket_detail(request, ticket_id):
ticket = models.Ticket.objects.get(id=ticket_id)
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
@ -11468,7 +11469,7 @@ def ticket_update(request, ticket_id):
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)
return redirect('ticket_detail',dealer_slug=ticket.dealer.slug, ticket_id=ticket.id)
else:
form = forms.TicketResolutionForm(instance=ticket)

View File

@ -4,6 +4,7 @@
{% if not create_bill %}
{% if style == 'dashboard' %}
<!-- Dashboard Style Card -->
<div class="">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3 text-primary">
@ -50,6 +51,7 @@
</div>
<!-- Modal Action -->
{% modal_action bill 'get' entity_slug %}
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{% url 'django_ledger:bill-detail' entity_slug=entity_slug bill_pk=bill.uuid %}"
class="btn btn-sm btn-phoenix-primary me-md-2">{% trans 'View' %}</a>
@ -57,8 +59,7 @@
<a hx-boost="true" 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' %}</a>
{% if bill.can_pay %}
<button onclick="djLedger.toggleModal('{{ bill.get_html_id }}')"
class="btn btn-sm btn-phoenix-info">{% trans 'Mark as Paid' %}</button>
<button onclick="djLedger.toggleModal('{{ bill.get_html_id }}')" class="btn btn-sm btn-phoenix-info">{% trans 'Mark as Paid' %}</button>
{% endif %}
{% if bill.can_cancel %}
<button onclick="djLedger.toggleModal('{{ bill.get_html_id }}')"
@ -218,7 +219,7 @@
{% endif %}
<!-- Mark as Review -->
{% if bill.can_review %}
<button class="btn btn-phoenix-warning"
onclick="showPOModal('Mark as Review', '{% url 'bill-action-mark-as-review' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Review')">
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Review' %}
@ -260,7 +261,7 @@
</button>
{% modal_action_v2 bill bill.get_mark_as_canceled_url bill.get_mark_as_canceled_message bill.get_mark_as_canceled_html_id %}
{% endif %}
{% endif %}
</div>
</div>

View File

@ -533,7 +533,7 @@
{% endif %}
</li>
<li class="nav-item">
<a class="nav-link px-3 d-block" href="#"> <span class="me-2 text-body align-bottom" data-feather="help-circle"></span>{{ _("Help Center") }}</a>
<a class="nav-link px-3 d-block" href="{% url 'ticket_list' request.dealer.slug %}"> <span class="me-2 text-body align-bottom" data-feather="help-circle"></span>{{ _("Help Center") }}</a>
</li>
{% if request.is_staff %}
<li class="nav-item">

View File

@ -131,10 +131,17 @@
</button>
{% endif %}
{% if po_model.can_fulfill %}
<button class="btn btn-phoenix-primary"
onclick="showPOModal('Fulfill PO', '{% url 'po-action-mark-as-fulfilled' request.dealer.slug entity_slug po_model.pk %}', 'Mark As Fulfilled')">
{% if not po_ready_to_fulfill %}
<button disabled class="btn btn-phoenix-primary">
<i class="fas fa-truck me-2"></i>{% trans 'Mark as Fulfilled' %}
</button>
{% else %}
<button class="btn btn-phoenix-primary"
onclick="showPOModal('Fulfill PO', '{% url 'po-action-mark-as-fulfilled' request.dealer.slug entity_slug po_model.pk %}', 'Mark As Fulfilled')"
>
<i class="fas fa-truck me-2"></i>{% trans 'Mark as Fulfilled' %}
</button>
{% endif %}
{% endif %}
{% if po_model.can_delete %}
{% if perms.django_ledger.delete_purchaseordermodel %}

View File

@ -14,7 +14,7 @@
{{form|crispy}}
<button type="submit" class="btn btn-primary">Submit Ticket</button>
<a href="{% url 'ticket_list' %}" class="btn btn-secondary">Cancel</a>
<a href="{% url 'ticket_list' request.dealer.slug %}" class="btn btn-secondary">Cancel</a>
</form>
</div>
</div>

View File

@ -6,7 +6,7 @@
<div class="card-body">
<h5 class="card-title">Need help?</h5>
<p class="card-text">Raise a ticket and we will get back to you as soon as possible.</p>
<a href="{% url 'create_ticket' %}" class="btn btn-phoenix-primary">Raise a Ticket</a>
<a href="{% url 'create_ticket' request.dealer.slug %}" class="btn btn-phoenix-primary">Raise a Ticket</a>
</div>
</div>
</div>

View File

@ -8,12 +8,7 @@
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h4 mb-0">Ticket #{{ ticket.id }}: {{ ticket.subject }}</h2>
<div>
{% if ticket.status != 'resolved' %}
<a href="{% url 'ticket_update' ticket.id %}" class="btn btn-sm btn-outline-success">
Update Ticket Status
</a>
{% endif %}
<a href="{% url 'ticket_list' %}" class="btn btn-sm btn-outline-secondary">
<a href="{% url 'ticket_list' request.dealer.slug %}" class="btn btn-sm btn-outline-secondary">
Back to List
</a>
</div>
@ -48,10 +43,16 @@
<div class="mb-4">
<h3 class="h5">Description</h3>
<div class="p-3 bg-light rounded">
<div class="p-3 rounded">
{{ ticket.description|linebreaks }}
</div>
</div>
<div class="mb-4">
<h3 class="h5">Resolution Notes</h3>
<div class="p-3 rounded">
{{ ticket.resolution_notes|linebreaks }}
</div>
</div>
<!-- You can add comments/replies section here later -->
</div>

View File

@ -3,12 +3,13 @@
{% load i18n %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0">My Support Tickets</h1>
<a href="{% url 'create_ticket' %}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> New Ticket
</a>
</div>
<div class="card">
<div class="card-body">
<h5 class="card-title">Need help?</h5>
<p class="card-text">Raise a ticket and we will get back to you as soon as possible.</p>
<a href="{% url 'create_ticket' request.dealer.slug %}" class="btn btn-phoenix-primary">Raise a Ticket</a>
</div>
</div>
{% if messages %}
{% for message in messages %}
@ -19,17 +20,19 @@
{% endfor %}
{% endif %}
<div class="card">
<div class="card-body">
<h5 class="card-title">Tickets</h5>
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<table class="table table-striped table-hover">
<thead class="">
<tr>
<th>ID</th>
<th>Subject</th>
<th>Status</th>
<th>Priority</th>
<th>Created</th>
<th>Resolved At</th>
<th>Time To Resolution</th>
<th>Actions</th>
</tr>
</thead>
@ -57,14 +60,9 @@
</span>
</td>
<td>{{ ticket.created_at|date:"M d, Y H:i" }}</td>
<td>{{ ticket.updated_at|date:"M d, Y H:i" }}</td>
<td>
<p>
<i class="fa fa-clock"></i>&nbsp;
{{ ticket.time_to_resolution_display }}
</p>
<td>
<a href="{% url 'ticket_detail' ticket.id %}" class="btn btn-sm btn-outline-primary">
<a href="{% url 'ticket_detail' request.dealer.slug ticket.id %}" class="btn btn-sm btn-outline-primary">
View
</a>
</td>
@ -77,4 +75,6 @@
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -13,7 +13,7 @@
{% csrf_token %}
{{form|crispy}}
<button type="submit" class="btn btn-primary">Save</button>
<a href="{% url 'ticket_list' %}" class="btn btn-secondary">Cancel</a>
<a href="{% url 'ticket_list' request.dealer.slug %}" class="btn btn-secondary">Cancel</a>
</form>
</div>
</div>