Merge branch 'main' of http://10.10.1.136:3000/ismail/haikal into frontend

This commit is contained in:
Faheedkhan 2025-08-03 13:10:49 +03:00
commit d4844a80c4
18 changed files with 921 additions and 143 deletions

View File

@ -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)),
)

View File

@ -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'}),
}

View File

@ -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:

View File

@ -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}"

View File

@ -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"

View File

@ -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'

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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 %}
يرجى التواصل مع فريق خدمة العملاء لدينا على أرقام الهاتف التالية [أدخل الأرقام هنا] أو عبر البريد الإلكتروني [أدخل البريد هنا] لترتيب موعد لإجراء الإصلاحات اللازمة مجاناً.
وتفضلوا بقبول فائق الاحترام والتقدير،
إدارة خدمة العملاء
شركة [تنحل]

View 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 %}

View 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>

View 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 %}

View 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 %}

View 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 %}

View 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"> &nbsp;{{ 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 %}

View 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 %}