Merge branch 'main' of http://10.10.1.136:3000/ismail/haikal into frontend
This commit is contained in:
commit
d4844a80c4
@ -30,7 +30,6 @@ urlpatterns += i18n_patterns(
|
||||
path("plans/", include("plans.urls")),
|
||||
path("schema/", Schema.as_view()),
|
||||
path("tours/", include("tours.urls")),
|
||||
|
||||
# path('', include(tf_urls)),
|
||||
)
|
||||
|
||||
|
||||
@ -55,6 +55,7 @@ from .models import (
|
||||
Organization,
|
||||
DealerSettings,
|
||||
Tasks,
|
||||
Recall,
|
||||
)
|
||||
from django_ledger import models as ledger_models
|
||||
from django.forms import (
|
||||
@ -2113,4 +2114,63 @@ class CustomSetPasswordForm(SetPasswordForm):
|
||||
new_password2 = forms.CharField(
|
||||
label="Confirm New Password",
|
||||
widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': 'Confirm New Password'})
|
||||
)
|
||||
)
|
||||
|
||||
# forms.py
|
||||
class RecallFilterForm(forms.Form):
|
||||
make = forms.ModelChoiceField(
|
||||
queryset=CarMake.objects.all(),
|
||||
required=False,
|
||||
label=_("Make"),
|
||||
widget=forms.Select(attrs={'class': 'form-control'})
|
||||
)
|
||||
model = forms.ModelChoiceField(
|
||||
queryset=CarModel.objects.none(),
|
||||
required=False,
|
||||
label=_("Model"),
|
||||
widget=forms.Select(attrs={'class': 'form-control'})
|
||||
)
|
||||
serie = forms.ModelChoiceField(
|
||||
queryset=CarSerie.objects.none(),
|
||||
required=False,
|
||||
label=_("Series"),
|
||||
widget=forms.Select(attrs={'class': 'form-control'})
|
||||
)
|
||||
trim = forms.ModelChoiceField(
|
||||
queryset=CarTrim.objects.none(),
|
||||
required=False,
|
||||
label=_("Trim"),
|
||||
widget=forms.Select(attrs={'class': 'form-control'})
|
||||
)
|
||||
year_from = forms.IntegerField(required=False, label=_("From Year"),
|
||||
widget=forms.NumberInput(attrs={'class': 'form-control'}))
|
||||
year_to = forms.IntegerField(required=False, label=_("To Year"),
|
||||
widget=forms.NumberInput(attrs={'class': 'form-control'}))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
make_id = kwargs.pop('make_id', None)
|
||||
model_id = kwargs.pop('model_id', None)
|
||||
serie_id = kwargs.pop('serie_id', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if make_id:
|
||||
self.fields['model'].queryset = CarModel.objects.filter(id_car_make_id=make_id)
|
||||
if model_id:
|
||||
self.fields['serie'].queryset = CarSerie.objects.filter(id_car_model_id=model_id)
|
||||
if serie_id:
|
||||
self.fields['trim'].queryset = CarTrim.objects.filter(id_car_serie_id=serie_id)
|
||||
class RecallCreateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Recall
|
||||
fields = ['title', 'description', 'make', 'model', 'serie', 'trim', 'year_from', 'year_to']
|
||||
widgets = {
|
||||
'make': forms.Select(attrs={'class': 'form-control'}),
|
||||
'model': forms.Select(attrs={'class': 'form-control'}),
|
||||
'serie': forms.Select(attrs={'class': 'form-control'}),
|
||||
'trim': forms.Select(attrs={'class': 'form-control'}),
|
||||
'title': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'description': forms.Textarea(attrs={'class': 'form-control'}),
|
||||
'year_from': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||
'year_to': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||
}
|
||||
|
||||
|
||||
@ -151,20 +151,13 @@ class DealerSlugMiddleware:
|
||||
return response
|
||||
|
||||
def process_view(self, request, view_func, view_args, view_kwargs):
|
||||
if (
|
||||
request.path_info.startswith("/ar/signup/")
|
||||
or request.path_info.startswith("/en/signup/")
|
||||
or request.path_info.startswith("/ar/login/")
|
||||
or request.path_info.startswith("/en/login/")
|
||||
or request.path_info.startswith("/ar/logout/")
|
||||
or request.path_info.startswith("/en/logout/")
|
||||
or request.path_info.startswith("/en/ledger/")
|
||||
or request.path_info.startswith("/ar/ledger/")
|
||||
or request.path_info.startswith("/en/notifications/")
|
||||
or request.path_info.startswith("/ar/notifications/")
|
||||
or request.path_info.startswith("/en/appointment/")
|
||||
or request.path_info.startswith("/ar/appointment/")
|
||||
):
|
||||
paths = [
|
||||
"/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/"
|
||||
]
|
||||
if any(request.path_info.startswith(path) for path in paths):
|
||||
return None
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
|
||||
@ -3411,3 +3411,66 @@ class ExtraInfo(models.Model):
|
||||
for x in qs
|
||||
if x.content_object.invoicemodel_set.first()
|
||||
]
|
||||
|
||||
|
||||
class Recall(models.Model):
|
||||
title = models.CharField(max_length=200, verbose_name=_("Recall Title"))
|
||||
description = models.TextField(verbose_name=_("Description"))
|
||||
make = models.ForeignKey(
|
||||
CarMake,
|
||||
models.DO_NOTHING,
|
||||
verbose_name=_("Make"),
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
model = models.ForeignKey(
|
||||
CarModel,
|
||||
models.DO_NOTHING,
|
||||
verbose_name=_("Model"),
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
serie = models.ForeignKey(
|
||||
CarSerie,
|
||||
models.DO_NOTHING,
|
||||
verbose_name=_("Series"),
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
trim = models.ForeignKey(
|
||||
CarTrim,
|
||||
models.DO_NOTHING,
|
||||
verbose_name=_("Trim"),
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
year_from = models.IntegerField(verbose_name=_("From Year"), null=True, blank=True)
|
||||
year_to = models.IntegerField(verbose_name=_("To Year"), null=True, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
|
||||
created_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Created By")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Recall")
|
||||
verbose_name_plural = _("Recalls")
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class RecallNotification(models.Model):
|
||||
recall = models.ForeignKey(Recall, on_delete=models.CASCADE, related_name='notifications')
|
||||
dealer = models.ForeignKey("Dealer", on_delete=models.CASCADE, related_name='recall_notifications')
|
||||
sent_at = models.DateTimeField(auto_now_add=True)
|
||||
cars_affected = models.ManyToManyField(Car, related_name='recall_notifications')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Recall Notification")
|
||||
verbose_name_plural = _("Recall Notifications")
|
||||
|
||||
def __str__(self):
|
||||
return f"Notification for {self.dealer} about {self.recall}"
|
||||
@ -1214,6 +1214,11 @@ urlpatterns = [
|
||||
views.inventory_items_filter,
|
||||
name="inventory_items_filter",
|
||||
),
|
||||
path(
|
||||
"inventory_items_filter/",
|
||||
views.inventory_items_filter,
|
||||
name="inventory_items_filter",
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/purchase_orders/<slug:entity_slug>/delete/<uuid:po_pk>/",
|
||||
views.PurchaseOrderModelDeleteView.as_view(),
|
||||
@ -1271,6 +1276,11 @@ urlpatterns = [
|
||||
),
|
||||
path('car-sale-report/<slug:dealer_slug>/csv/', views.car_sale_report_csv_export, name='car-sale-report-csv-export'),
|
||||
|
||||
path('feature/recalls/', views.RecallListView.as_view(), name='recall_list'),
|
||||
path('feature/recall/', views.RecallFilterView, name='recall_filter'),
|
||||
path('feature/recall/<int:pk>/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'),
|
||||
]
|
||||
|
||||
handler404 = "inventory.views.custom_page_not_found_view"
|
||||
|
||||
@ -34,7 +34,7 @@ from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||
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
|
||||
@ -10152,10 +10152,49 @@ def InventoryItemCreateView(request, dealer_slug):
|
||||
)
|
||||
|
||||
|
||||
# def inventory_items_filter(request):
|
||||
# year = request.GET.get("year")
|
||||
# make = request.GET.get("make")
|
||||
# model = request.GET.get("model")
|
||||
# serie = request.GET.get("serie")
|
||||
|
||||
# # Get all makes for initial dropdown
|
||||
# makes = models.CarMake.objects.all()
|
||||
# print(make)
|
||||
# model_data = models.CarModel.objects.none()
|
||||
# serie_data = models.CarSerie.objects.none()
|
||||
# trim_data = models.CarTrim.objects.none()
|
||||
|
||||
# if make:
|
||||
# make_obj = models.CarMake.objects.get(pk=int(make))
|
||||
# model_data = make_obj.carmodel_set.all()
|
||||
# if model:
|
||||
# model_obj = models.CarModel.objects.get(pk=model)
|
||||
# serie_data = model_obj.carserie_set.all()
|
||||
# if year:
|
||||
# serie_data = serie_data.filter(year_begin__lte=year, year_end__gte=year)
|
||||
# if serie:
|
||||
# serie_obj = models.CarSerie.objects.get(pk=serie)
|
||||
# trim_data = serie_obj.cartrim_set.all()
|
||||
|
||||
# # Generate year choices (adjust range as needed)
|
||||
# current_year = datetime.now().year
|
||||
# year_choices = range(current_year, current_year - 10, -1)
|
||||
|
||||
# context = {
|
||||
# "makes": makes,
|
||||
# "model_data": model_data,
|
||||
# "serie_data": serie_data,
|
||||
# "trim_data": trim_data,
|
||||
# "year_choices": year_choices,
|
||||
# "selected_make": make,
|
||||
# "selected_model": model,
|
||||
# "selected_serie": serie,
|
||||
# "selected_year": year,
|
||||
# }
|
||||
# return render(request, "cars/partials/recall_filter_form.html", context)
|
||||
@login_required
|
||||
@permission_required("django_ledger.view_purchaseordermodel", raise_exception=True)
|
||||
def inventory_items_filter(request, dealer_slug):
|
||||
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
||||
def inventory_items_filter(request,dealer_slug=None):
|
||||
year = request.GET.get("year", None)
|
||||
make = request.GET.get("make")
|
||||
model = request.GET.get("model")
|
||||
@ -10766,4 +10805,148 @@ def staff_password_reset_view(request, dealer_slug, user_pk):
|
||||
else:
|
||||
messages.error(request, _('Invalid password. Please try again.'))
|
||||
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'
|
||||
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)
|
||||
)
|
||||
return queryset.select_related('make', 'model', 'serie', 'trim')
|
||||
|
||||
|
||||
class RecallDetailView(DetailView):
|
||||
model = models.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')
|
||||
return context
|
||||
|
||||
|
||||
|
||||
def RecallFilterView(request):
|
||||
context = {'make_data': models.CarMake.objects.all()}
|
||||
if request.method == "POST":
|
||||
print(request.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')
|
||||
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)
|
||||
|
||||
class RecallCreateView(FormView):
|
||||
template_name = 'recalls/recall_create.html'
|
||||
form_class = forms.RecallCreateForm
|
||||
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')
|
||||
if make:
|
||||
qs = models.CarMake.objects.filter(pk=make)
|
||||
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()
|
||||
if serie:
|
||||
qs = models.CarSerie.objects.filter(pk=serie)
|
||||
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()
|
||||
if 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':
|
||||
initial.update(self.request.GET.dict())
|
||||
return initial
|
||||
|
||||
def form_valid(self, form):
|
||||
recall = form.save(commit=False)
|
||||
recall.created_by = self.request.user
|
||||
recall.save()
|
||||
|
||||
# Get affected cars based on recall criteria
|
||||
cars = models.Car.objects.all()
|
||||
|
||||
if recall.make:
|
||||
cars = cars.filter(id_car_make=recall.make)
|
||||
|
||||
if recall.model:
|
||||
cars = cars.filter(id_car_model=recall.model)
|
||||
|
||||
if recall.serie:
|
||||
cars = cars.filter(id_car_serie=recall.serie)
|
||||
|
||||
if recall.trim:
|
||||
cars = cars.filter(id_car_trim=recall.trim)
|
||||
|
||||
if recall.year_from:
|
||||
cars = cars.filter(year__gte=recall.year_from)
|
||||
|
||||
if recall.year_to:
|
||||
cars = cars.filter(year__lte=recall.year_to)
|
||||
|
||||
# Group cars by dealer and send notifications
|
||||
dealers = models.Dealer.objects.filter(cars__in=cars).distinct()
|
||||
|
||||
for dealer in dealers:
|
||||
dealer_cars = cars.filter(dealer=dealer)
|
||||
notification = models.RecallNotification.objects.create(
|
||||
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"))
|
||||
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,
|
||||
})
|
||||
send_email(
|
||||
subject,
|
||||
message,
|
||||
'noreply@yourdomain.com',
|
||||
[dealer.user.email],
|
||||
)
|
||||
|
||||
class RecallSuccessView(TemplateView):
|
||||
template_name = 'recalls/recall_success.html'
|
||||
@ -84,7 +84,7 @@
|
||||
{% include "plans/expiration_messages.html" %}
|
||||
{% block period_navigation %}
|
||||
{% endblock period_navigation %}
|
||||
<div id="main_content" class="fade-me-in" hx-boost="false" hx-target="#main_content" hx-select="#main_content" hx-swap="innerHTML transition:true" hx-select-oob="#toast-container" hx-history-elt>
|
||||
<div id="main_content" class="fade-me-in" hx-boost="true" hx-target="#main_content" hx-select="#main_content" hx-swap="outerHTML transition:true" hx-select-oob="#toast-container" hx-history-elt>
|
||||
<div id="spinner" class="htmx-indicator spinner-bg">
|
||||
<img src="{% static 'spinner.svg' %}" width="100" height="100" alt="">
|
||||
</div>
|
||||
|
||||
@ -8,17 +8,16 @@
|
||||
{% block content%}
|
||||
|
||||
<div class="row mt-4">
|
||||
|
||||
<div class="col-12 mb-3">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
{% include 'bill/includes/card_bill.html' with dealer_slug=request.dealer.slug bill=bill entity_slug=view.kwargs.entity_slug style='bill-detail' %}
|
||||
</div>
|
||||
{% if bill.is_configured %}
|
||||
|
||||
|
||||
<div class="row text-center g-3 mb-3">
|
||||
<div class="col-12 col-md-3">
|
||||
|
||||
|
||||
<h6 class="text-uppercase text-xs text-muted mb-2">
|
||||
{% trans 'Cash Account' %}:
|
||||
<a href="{% url 'account_detail' request.dealer.slug bill.cash_account.uuid %}"
|
||||
@ -27,11 +26,11 @@
|
||||
<h4 class="mb-0" id="djl-bill-detail-amount-paid">
|
||||
{% currency_symbol %}{{ bill.get_amount_cash | absolute | currency_format }}
|
||||
</h4>
|
||||
|
||||
|
||||
</div>
|
||||
{% if bill.accrue %}
|
||||
<div class="col-12 col-md-3">
|
||||
|
||||
|
||||
<h6 class="text-uppercase text-xs text-muted mb-2">
|
||||
{% trans 'Prepaid Account' %}:
|
||||
<a href="{% url 'account_detail' request.dealer.slug bill.prepaid_account.uuid %}"
|
||||
@ -42,10 +41,10 @@
|
||||
<h4 class="text-success mb-0" id="djl-bill-detail-amount-prepaid">
|
||||
{% currency_symbol %}{{ bill.get_amount_prepaid | currency_format }}
|
||||
</h4>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
|
||||
|
||||
<h6 class="text-uppercase text-xs text-muted mb-2">
|
||||
{% trans 'Accounts Payable' %}:
|
||||
<a href="{% url 'account_detail' request.dealer.slug bill.unearned_account.uuid %}"
|
||||
@ -56,26 +55,26 @@
|
||||
<h4 class="text-danger mb-0" id="djl-bill-detail-amount-unearned">
|
||||
{% currency_symbol %}{{ bill.get_amount_unearned | currency_format }}
|
||||
</h4>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
|
||||
|
||||
<h6 class="text-uppercase text-xs text-muted mb-2">{% trans 'Accrued' %} {{ bill.get_progress | percentage }}</h6>
|
||||
<h4 class="mb-0">{% currency_symbol %}{{ bill.get_amount_earned | currency_format }}</h4>
|
||||
|
||||
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-12 col-md-3 offset-md-6">
|
||||
|
||||
|
||||
<h6 class="text-uppercase text-xs text-muted mb-2">{% trans 'You Still Owe' %}</h6>
|
||||
<h4 class="text-danger mb-0" id="djl-bill-detail-amount-owed">
|
||||
{% currency_symbol %}{{ bill.get_amount_open | currency_format }}
|
||||
</h4>
|
||||
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{% load django_ledger %}
|
||||
{% load i18n %}
|
||||
<div id="djl-bill-card-widget" class="">
|
||||
<div id="djl-bill-card-widget" class="" hx-boost="false">
|
||||
{% if not create_bill %}
|
||||
{% if style == 'dashboard' %}
|
||||
<!-- Dashboard Style Card -->
|
||||
@ -54,7 +54,7 @@
|
||||
<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>
|
||||
{% if perms.django_ledger.change_billmodel %}
|
||||
<a href="{% url 'django_ledger:bill-update' entity_slug=entity_slug bill_pk=bill.uuid %}"
|
||||
<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 }}')"
|
||||
@ -198,66 +198,71 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-footer p-0">
|
||||
<div class="d-flex flex-wrap gap-2 mt-2" hx-boost="false">
|
||||
<div class="d-flex flex-wrap gap-2 mt-2">
|
||||
<!-- Update Button -->
|
||||
{% if perms.django_ledger.change_billmodel %}
|
||||
<button class="btn btn-phoenix-primary"
|
||||
<a hx-boost="true" href="{% url 'bill-update' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill.uuid %}">
|
||||
<button class="btn btn-phoenix-primary"
|
||||
{% if not request.is_accountant %}disabled{% endif %}>
|
||||
<a href="{% url 'bill-update' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill.uuid %}">
|
||||
<i class="fas fa-edit me-2"></i>{% trans 'Update' %}
|
||||
</a>
|
||||
</button>
|
||||
<!-- Mark as Draft -->
|
||||
{% if bill.can_draft %}
|
||||
<button class="btn btn-phoenix-success"
|
||||
{% if not request.is_accountant %}disabled{% endif %}
|
||||
onclick="showPOModal('Mark as Draft', '{% url 'bill-action-mark-as-draft' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Draft')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Draft' %}
|
||||
</button>
|
||||
</a>
|
||||
{% if "detail" not in request.path %}
|
||||
<!-- Mark as Draft -->
|
||||
{% if bill.can_draft %}
|
||||
<button class="btn btn-phoenix-success"
|
||||
{% if not request.is_accountant %}disabled{% endif %}
|
||||
onclick="showPOModal('Mark as Draft', '{% url 'bill-action-mark-as-draft' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Draft')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Draft' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<!-- Mark as Review -->
|
||||
{% if bill.can_review %}
|
||||
<button class="btn btn-phoenix-warning"
|
||||
{% if not request.is_accountant %}disabled{% endif %}
|
||||
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' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<!-- Mark as Approved -->
|
||||
{% endif %}
|
||||
<!-- Mark as Review -->
|
||||
{% if bill.can_review %}
|
||||
<button class="btn btn-phoenix-warning"
|
||||
{% if not request.is_accountant %}disabled{% endif %}
|
||||
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' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<!-- Mark as Approved -->
|
||||
{% if bill.can_approve and perms.django_ledger.can_approve_billmodel %}
|
||||
<button class="btn btn-phoenix-success"
|
||||
onclick="showPOModal('Mark as Approved', '{% url 'bill-action-mark-as-approved' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Approved')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Approved' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if bill.can_approve and not request.is_manager %}
|
||||
<button class="btn btn-phoenix-warning" disabled>
|
||||
<i class="fas fa-hourglass-start me-2"></i><span class="text-warning">{% trans 'Waiting for Manager Approval' %}</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
<!-- Mark as Paid -->
|
||||
{% if bill.can_pay %}
|
||||
<button class="btn btn-phoenix-success"
|
||||
onclick="showPOModal('Mark as Paid', '{% url 'bill-action-mark-as-paid' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Paid')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Paid' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<!-- Void Button -->
|
||||
{% if bill.can_void %}
|
||||
<button class="btn btn-phoenix-danger"
|
||||
onclick="showPOModal('Mark as Void', '{% url 'bill-action-mark-as-void' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Void')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Void' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<!-- Cancel Button -->
|
||||
{% if bill.can_cancel %}
|
||||
<button class="btn btn-phoenix-danger"
|
||||
{% if not request.is_accountant %}disabled{% endif %}
|
||||
onclick="showPOModal('Mark as Canceled', '{% url 'bill-action-mark-as-canceled' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Canceled')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Canceled' %}
|
||||
</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 %}
|
||||
{% if bill.can_approve and not request.is_manager or not request.is_dealer %}
|
||||
<button class="btn btn-phoenix-warning" disabled>
|
||||
<i class="fas fa-hourglass-start me-2"></i><span class="text-warning">{% trans 'Waiting for Manager Approval' %}</span>
|
||||
</button>
|
||||
{% else %}
|
||||
{% if bill.can_approve and perms.django_ledger.can_approve_billmodel %}
|
||||
<button class="btn btn-phoenix-success"
|
||||
onclick="showPOModal('Mark as Approved', '{% url 'bill-action-mark-as-approved' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Approved')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Approved' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<!-- Mark as Paid -->
|
||||
{% if "detail" not in request.path %}
|
||||
{% if bill.can_pay %}
|
||||
<button class="btn btn-phoenix-success"
|
||||
onclick="showPOModal('Mark as Paid', '{% url 'bill-action-mark-as-paid' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Paid')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Paid' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<!-- Void Button -->
|
||||
{% if bill.can_void %}
|
||||
<button class="btn btn-phoenix-danger"
|
||||
onclick="showPOModal('Mark as Void', '{% url 'bill-action-mark-as-void' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Void')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Void' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<!-- Cancel Button -->
|
||||
{% if bill.can_cancel %}
|
||||
<button class="btn btn-phoenix-danger"
|
||||
{% if not request.is_accountant %}disabled{% endif %}
|
||||
onclick="showPOModal('Mark as Canceled', '{% url 'bill-action-mark-as-canceled' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Canceled')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Canceled' %}
|
||||
</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 %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@ -289,7 +294,10 @@
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('DOMContentLoaded',processElements);
|
||||
//document.addEventListener('htmx:afterSwap',processElements);
|
||||
|
||||
function processElements() {
|
||||
window.showPOModal = function(title, actionUrl, buttonText) {
|
||||
const modalEl = document.getElementById('POModal');
|
||||
if (!modalEl) {
|
||||
@ -300,7 +308,8 @@
|
||||
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
||||
document.getElementById('POModalTitle').textContent = title;
|
||||
|
||||
document.getElementById('POModalBody').innerHTML = `
|
||||
const modalBody = document.getElementById('POModalBody')
|
||||
modalBody.innerHTML = `
|
||||
<div class="d-flex justify-content-center gap-3 py-3">
|
||||
<a class="btn btn-phoenix-primary px-4" href="${actionUrl}">
|
||||
<i class="fas fa-check-circle me-2"></i>${buttonText}
|
||||
@ -310,8 +319,8 @@
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
//htmx.process(modalBody);
|
||||
modal.show();
|
||||
};
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -2,7 +2,10 @@
|
||||
{% load django_ledger %}
|
||||
{% load widget_tweaks %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('DOMContentLoaded',processElements);
|
||||
//document.addEventListener('htmx:afterSwap',processElements);
|
||||
|
||||
function processElements() {
|
||||
window.showPOModal = function(title, actionUrl, buttonText) {
|
||||
const modalEl = document.getElementById('POModal');
|
||||
if (!modalEl) {
|
||||
@ -13,7 +16,8 @@
|
||||
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
||||
document.getElementById('POModalTitle').textContent = title;
|
||||
|
||||
document.getElementById('POModalBody').innerHTML = `
|
||||
const modalBody = document.getElementById('POModalBody')
|
||||
modalBody.innerHTML = `
|
||||
<div class="d-flex justify-content-center gap-3 py-3">
|
||||
<a hx-boost="true" class="btn btn-phoenix-primary px-4" href="${actionUrl}">
|
||||
<i class="fas fa-check-circle me-2"></i>${buttonText}
|
||||
@ -23,10 +27,10 @@
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
//htmx.process(modalBody)
|
||||
modal.show();
|
||||
};
|
||||
});
|
||||
};
|
||||
</script>
|
||||
{% if not create_po %}
|
||||
{% if style == 'po-detail' %}
|
||||
@ -106,52 +110,54 @@
|
||||
class="btn btn-phoenix-primary">
|
||||
<i class="fas fa-edit me-2"></i>{% trans 'Update' %}
|
||||
</a>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% if po_model.can_draft %}
|
||||
<button class="btn btn-phoenix-secondary"
|
||||
onclick="showPOModal('Draft PO', '{% url 'po-action-mark-as-draft' request.dealer.slug entity_slug po_model.pk %}', 'Mark As Draft')">
|
||||
<i class="fas fa-file me-2"></i>{% trans 'Mark as Draft' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if po_model.can_review %}
|
||||
<button class="btn btn-phoenix-warning"
|
||||
onclick="showPOModal('Review PO', '{% url 'po-action-mark-as-review' request.dealer.slug entity_slug po_model.pk %}', 'Mark As Review')">
|
||||
<i class="fas fa-search me-2"></i>{% trans 'Mark as Review' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if po_model.can_approve %}
|
||||
<button class="btn btn-phoenix-success"
|
||||
onclick="showPOModal('Approve PO', '{% url 'po-action-mark-as-approved' request.dealer.slug entity_slug po_model.pk %}', 'Mark As Approved')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Approved' %}
|
||||
</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')">
|
||||
<i class="fas fa-truck me-2"></i>{% trans 'Mark as Fulfilled' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if po_model.can_delete %}
|
||||
{% if perms.django_ledger.delete_purchaseordermodel %}
|
||||
<button class="btn btn-phoenix-danger"
|
||||
onclick="showPOModal('Delete PO', '{% url 'po-delete' request.dealer.slug entity_slug po_model.pk %}', 'Delete')">
|
||||
<i class="fas fa-trash me-2"></i>{% trans 'Delete' %}
|
||||
{% if "detail" not in request.path %}
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% if po_model.can_draft %}
|
||||
<button class="btn btn-phoenix-secondary"
|
||||
onclick="showPOModal('Draft PO', '{% url 'po-action-mark-as-draft' request.dealer.slug entity_slug po_model.pk %}', 'Mark As Draft')">
|
||||
<i class="fas fa-file me-2"></i>{% trans 'Mark as Draft' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if po_model.can_void %}
|
||||
<button class="btn btn-phoenix-danger"
|
||||
onclick="showPOModal('Void PO', '{% url 'po-action-mark-as-void' request.dealer.slug entity_slug po_model.pk %}', 'Mark As Void')">
|
||||
<i class="fas fa-times-circle me-2"></i>{% trans 'Void' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if po_model.can_cancel %}
|
||||
<button class="btn btn-phoenix-secondary"
|
||||
onclick="showPOModal('Cancel PO', '{% url 'po-action-mark-as-canceled' request.dealer.slug entity_slug po_model.pk %}', 'Mark As Cancelled')">
|
||||
<i class="fas fa-ban me-2"></i>{% trans 'Cancel' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if po_model.can_review %}
|
||||
<button class="btn btn-phoenix-warning"
|
||||
onclick="showPOModal('Review PO', '{% url 'po-action-mark-as-review' request.dealer.slug entity_slug po_model.pk %}', 'Mark As Review')">
|
||||
<i class="fas fa-search me-2"></i>{% trans 'Mark as Review' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if po_model.can_approve %}
|
||||
<button class="btn btn-phoenix-success"
|
||||
onclick="showPOModal('Approve PO', '{% url 'po-action-mark-as-approved' request.dealer.slug entity_slug po_model.pk %}', 'Mark As Approved')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Approved' %}
|
||||
</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')">
|
||||
<i class="fas fa-truck me-2"></i>{% trans 'Mark as Fulfilled' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if po_model.can_delete %}
|
||||
{% if perms.django_ledger.delete_purchaseordermodel %}
|
||||
<button class="btn btn-phoenix-danger"
|
||||
onclick="showPOModal('Delete PO', '{% url 'po-delete' request.dealer.slug entity_slug po_model.pk %}', 'Delete')">
|
||||
<i class="fas fa-trash me-2"></i>{% trans 'Delete' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if po_model.can_void %}
|
||||
<button class="btn btn-phoenix-danger"
|
||||
onclick="showPOModal('Void PO', '{% url 'po-action-mark-as-void' request.dealer.slug entity_slug po_model.pk %}', 'Mark As Void')">
|
||||
<i class="fas fa-times-circle me-2"></i>{% trans 'Void' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if po_model.can_cancel %}
|
||||
<button class="btn btn-phoenix-secondary"
|
||||
onclick="showPOModal('Cancel PO', '{% url 'po-action-mark-as-canceled' request.dealer.slug entity_slug po_model.pk %}', 'Mark As Cancelled')">
|
||||
<i class="fas fa-ban me-2"></i>{% trans 'Cancel' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
24
templates/recalls/email/recall_notification.txt
Normal file
24
templates/recalls/email/recall_notification.txt
Normal file
@ -0,0 +1,24 @@
|
||||
<!-- templates/recalls/email/recall_notification_formal.txt -->
|
||||
{% load i18n %}
|
||||
السادة/ شركة {{ dealer.name }} المحترمين،
|
||||
السلام عليكم ورحمة الله وبركاته،
|
||||
|
||||
نفيدكم علماً بأنه تم إصدار استدعاء سلامة لبعض المركبات من الموديلات الموجودة لديكم في المخزون.
|
||||
|
||||
بيانات الاستدعاء:
|
||||
- عنوان الاستدعاء: {{ recall.title }}
|
||||
- وصف المشكلة: {{ recall.description }}
|
||||
|
||||
المركبات المتأثرة في مخزونكم:
|
||||
{% for car in cars %}
|
||||
- رقم الشاصي: {{ car.vin }}
|
||||
- الماركة: {{ car.id_car_make }}
|
||||
- الموديل: {{ car.id_car_model }} - {{ car.year }}
|
||||
- الفئة: {{ car.id_car_trim }}
|
||||
{% endfor %}
|
||||
|
||||
يرجى التواصل مع فريق خدمة العملاء لدينا على أرقام الهاتف التالية [أدخل الأرقام هنا] أو عبر البريد الإلكتروني [أدخل البريد هنا] لترتيب موعد لإجراء الإصلاحات اللازمة مجاناً.
|
||||
|
||||
وتفضلوا بقبول فائق الاحترام والتقدير،
|
||||
إدارة خدمة العملاء
|
||||
شركة [تنحل]
|
||||
35
templates/recalls/partials/recall_cars_table.html
Normal file
35
templates/recalls/partials/recall_cars_table.html
Normal file
@ -0,0 +1,35 @@
|
||||
{% load i18n %}
|
||||
{% if cars %}
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "VIN" %}</th>
|
||||
<th>{% trans "Make" %}</th>
|
||||
<th>{% trans "Model" %}</th>
|
||||
<th>{% trans "Series" %}</th>
|
||||
<th>{% trans "Trim" %}</th>
|
||||
<th>{% trans "Year" %}</th>
|
||||
<th>{% trans "Dealer" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for car in cars %}
|
||||
<tr>
|
||||
<td>{{ car.vin }}</td>
|
||||
<td>{{ car.id_car_make|default:"-" }}</td>
|
||||
<td>{{ car.id_car_model|default:"-" }}</td>
|
||||
<td>{{ car.id_car_serie|default:"-" }}</td>
|
||||
<td>{{ car.id_car_trim|default:"-" }}</td>
|
||||
<td>{{ car.year }}</td>
|
||||
<td>{{ car.dealer.name }}</td>
|
||||
<td>{{ car.get_status_display }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<a href="{{ url }}" class="btn btn-primary">
|
||||
<i class="fas fa-plus mr-2"></i>
|
||||
{% trans "Recall Request" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
79
templates/recalls/partials/recall_filter_form.html
Normal file
79
templates/recalls/partials/recall_filter_form.html
Normal file
@ -0,0 +1,79 @@
|
||||
{% load i18n %}
|
||||
<div class="row">
|
||||
<!-- Make Dropdown -->
|
||||
<div class="col-md-3">
|
||||
<label for="id_make">{% trans "Make" %}</label>
|
||||
<select name="make" id="id_make" class="form-select"
|
||||
hx-get="{% url 'inventory_items_filter' %}"
|
||||
hx-select="#id_model"
|
||||
hx-target="#id_model"
|
||||
hx-swap="outerHTML"
|
||||
hx-include="[name='year']">
|
||||
<option value="">{% trans "Select Make" %}</option>
|
||||
{% for make in makes %}
|
||||
<option value="{{ make.pk }}" {% if selected_make == make.pk %}selected{% endif %}>{{ make.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Model Dropdown Container -->
|
||||
<div class="col-md-3" id="model-container">
|
||||
<label for="id_model">{% trans "Model" %}</label>
|
||||
<select name="model" id="id_model" class="form-select"
|
||||
{% if not model_data %}disabled{% endif %}
|
||||
hx-get="{% url 'inventory_items_filter' %}"
|
||||
hx-select="#serie-container"
|
||||
hx-target="#serie-container"
|
||||
hx-trigger="change"
|
||||
hx-include="[name='year']">
|
||||
<option value="">{% trans "Select Model" %}</option>
|
||||
{% for model in model_data %}
|
||||
<option value="{{ model.pk }}" {% if selected_model == model.pk %}selected{% endif %}>{{ model.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Series Dropdown Container -->
|
||||
<div class="col-md-3" id="serie-container">
|
||||
<label for="id_serie">{% trans "Series" %}</label>
|
||||
<select name="serie" id="id_serie" class="form-select"
|
||||
{% if not serie_data %}disabled{% endif %}
|
||||
hx-get="{% url 'inventory_items_filter' %}"
|
||||
hx-select="#trim-container"
|
||||
hx-target="#trim-container"
|
||||
hx-trigger="change"
|
||||
hx-include="[name='year']">
|
||||
<option value="">{% trans "Select Series" %}</option>
|
||||
{% for serie in serie_data %}
|
||||
<option value="{{ serie.pk }}" {% if selected_serie == serie.pk %}selected{% endif %}>{{ serie.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Trim Dropdown Container -->
|
||||
<div class="col-md-3" id="trim-container">
|
||||
<label for="id_trim">{% trans "Trim" %}</label>
|
||||
<select name="trim" id="id_trim" class="form-select" {% if not trim_data %}disabled{% endif %}>
|
||||
<option value="">{% trans "Select Trim" %}</option>
|
||||
{% for trim in trim_data %}
|
||||
<option value="{{ trim.pk }}" {% if selected_trim == trim.pk %}selected{% endif %}>{{ trim.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Year Filter -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-3">
|
||||
<label for="id_year">{% trans "Year" %}</label>
|
||||
<select name="year" id="id_year" class="form-select"
|
||||
hx-get="{% url 'inventory_items_filter' %}"
|
||||
hx-trigger="change"
|
||||
hx-target="#serie-container">
|
||||
<option value="">{% trans "Any Year" %}</option>
|
||||
{% for year in year_choices %}
|
||||
<option value="{{ year }}" {% if selected_year == year %}selected{% endif %}>{{ year }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
80
templates/recalls/recall_create.html
Normal file
80
templates/recalls/recall_create.html
Normal file
@ -0,0 +1,80 @@
|
||||
<!-- templates/recalls/recall_create.html -->
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h2>{% trans "Create New Recall" %}</h2>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>{% trans "Recall Details" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
{{ form.title.label_tag }}
|
||||
{{ form.title }}
|
||||
{% if form.title.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.title.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-12">
|
||||
{{ form.description.label_tag }}
|
||||
{{ form.description }}
|
||||
{% if form.description.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.description.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-2">
|
||||
{{ form.make.label_tag }}
|
||||
{{ form.make }}
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
{{ form.model.label_tag }}
|
||||
{{ form.model }}
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
{{ form.serie.label_tag }}
|
||||
{{ form.serie }}
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
{{ form.trim.label_tag }}
|
||||
{{ form.trim }}
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
{{ form.year_from.label_tag }}
|
||||
{{ form.year_from }}
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
{{ form.year_to.label_tag }}
|
||||
{{ form.year_to }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn btn-danger">
|
||||
{% trans "Confirm and Send Recall Notifications" %}
|
||||
</button>
|
||||
<a href="{% url 'recall_filter' %}" class="btn btn-secondary">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
82
templates/recalls/recall_detail.html
Normal file
82
templates/recalls/recall_detail.html
Normal file
@ -0,0 +1,82 @@
|
||||
<!-- templates/recalls/recall_detail.html -->
|
||||
{% extends "base.html" %}
|
||||
{% load i18n humanize %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<img src="{{ recall.make.logo.url }}" width="80" height="80">
|
||||
<h2>{{ recall.title }}</h2>
|
||||
</div>
|
||||
<a href="{% url 'recall_list' %}" class="btn btn-secondary">
|
||||
{% trans "Back to Recall List" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5>{% trans "Recall Details" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<strong>{% trans "Description:" %}</strong>
|
||||
<p>{{ recall.description }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>{% trans "Sent:" %}</strong>
|
||||
<p>{{ recall.created_at|date:"DATETIME_FORMAT" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<strong>{% trans "Make:" %}</strong>
|
||||
<p>{{ recall.make|default:"-" }}</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<strong>{% trans "Model:" %}</strong>
|
||||
<p>{{ recall.model|default:"-" }}</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<strong>{% trans "Series:" %}</strong>
|
||||
<p>{{ recall.serie|default:"-" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>{% trans "Notifications Sent" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Dealer" %}</th>
|
||||
<th>{% trans "Cars Affected" %}</th>
|
||||
<th>{% trans "Notification Date" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for notification in notifications %}
|
||||
<tr>
|
||||
<td>{{ notification.dealer.name }}</td>
|
||||
<td>{{ notification.cars_affected.count }}</td>
|
||||
<td>{{ notification.sent_at|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-center">{% trans "No notifications sent" %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
48
templates/recalls/recall_filter.html
Normal file
48
templates/recalls/recall_filter.html
Normal file
@ -0,0 +1,48 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
{% block title %}
|
||||
{% trans 'Recall' %}
|
||||
{% endblock %}
|
||||
|
||||
<form hx-boost="true" action="{% url 'recall_filter' %}" method="post" hx-target=".table-responsive" hx-select=".table-responsive" hx-swap="outerHTML" id="recall-filter-form"
|
||||
hx-on::after-request="resetSubmitButton(document.querySelector('#recall-filter-form button[type=submit]'))"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-md-3"">
|
||||
{% include "purchase_orders/partials/po-select.html" with name="make" target="model" data=make_data pk=po_model.pk %}
|
||||
</div>
|
||||
<div class="col-md-3"">
|
||||
{% include "purchase_orders/partials/po-select.html" with name="model" target="serie" data=model_data pk=po_model.pk %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="id_year">{% trans "Year" %}</label>
|
||||
<input type="number" name="year" id="id_year" class="form-control" hx-get="{% url 'inventory_items_filter' request.dealer.slug %}"
|
||||
hx-target="#serie"
|
||||
hx-select="#serie"
|
||||
hx-include="#model"
|
||||
hx-trigger="input delay:500ms"
|
||||
hx-swap="outerHTML" required>
|
||||
</div>
|
||||
<div class="col-md-3"">
|
||||
{% include "purchase_orders/partials/po-select.html" with name="serie" target="trim" data=serie_data pk=po_model.pk %}
|
||||
</div>
|
||||
<div class="col-md-3"">
|
||||
{% include "purchase_orders/partials/po-select.html" with name="trim" target="none" data=trim_data pk=po_model.pk %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 mt-3">
|
||||
<button type="submit" class="btn btn-lg btn-phoenix-primary md-me-2"><i class="fa fa-filter" aria-hidden="true"></i> {% trans "Filter" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Results table -->
|
||||
<div class="table-responsive mt-3">
|
||||
{% include "recalls/partials/recall_cars_table.html" %}
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
92
templates/recalls/recall_list.html
Normal file
92
templates/recalls/recall_list.html
Normal file
@ -0,0 +1,92 @@
|
||||
<!-- templates/recalls/recall_list.html -->
|
||||
{% extends "base.html" %}
|
||||
{% load i18n humanize %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h2>{% trans "Recall History" %}</h2>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th>{% trans "Title" %}</th>
|
||||
<th>{% trans "Sent" %}</th>
|
||||
<th>{% trans "Make" %}</th>
|
||||
<th>{% trans "Model" %}</th>
|
||||
<th>{% trans "Series" %}</th>
|
||||
<th>{% trans "Affected Dealers" %}</th>
|
||||
<th>{% trans "Affected Cars" %}</th>
|
||||
<th>{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for recall in recalls %}
|
||||
<tr>
|
||||
<td>{{ recall.title }}</td>
|
||||
<td>
|
||||
<span title="{{ recall.created_at }}">
|
||||
{{ recall.created_at|naturaltime }}
|
||||
</span>
|
||||
</td>
|
||||
<td><img src="{{ recall.make.logo.url }}" width="50" height="50"> {{ recall.make|default:"-" }}</td>
|
||||
<td>{{ recall.model|default:"-" }}</td>
|
||||
<td>{{ recall.serie|default:"-" }}</td>
|
||||
<td>{{ recall.dealer_count }}</td>
|
||||
<td>{{ recall.car_count }}</td>
|
||||
<td>
|
||||
<a href="{% url 'recall_detail' recall.id %}"
|
||||
class="btn btn-sm btn-info"
|
||||
title="{% trans 'View details' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="8" class="text-center">{% trans "No recalls found" %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center mt-4">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">
|
||||
{% trans "Previous" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ num }}</span>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}">
|
||||
{% trans "Next" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
16
templates/recalls/recall_success.html
Normal file
16
templates/recalls/recall_success.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!-- templates/recalls/recall_success.html -->
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="alert alert-success">
|
||||
<h4 class="alert-heading">{% trans "Recall Initiated Successfully!" %}</h4>
|
||||
<p>{% trans "The recall has been created and notifications have been sent to all affected dealers." %}</p>
|
||||
<hr>
|
||||
<a href="{% url 'recall_filter' %}" class="btn btn-primary">
|
||||
{% trans "Back to Recall Management" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Loading…
x
Reference in New Issue
Block a user