update merge
This commit is contained in:
parent
2c6249d74c
commit
6a2911cea4
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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},
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -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'),
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
{{ 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 %}
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user