import logging import json from django.views.decorators.csrf import csrf_exempt from vin import VIN from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required from django.http import JsonResponse from django.shortcuts import render, get_object_or_404, redirect from django.utils.translation import gettext_lazy as _ from django.db.models import Q from django.views.generic import ( View, ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView, ) from django.utils import timezone, translation from django.conf import settings from urllib.parse import urlparse, urlunparse from django.forms import ChoiceField, ModelForm, RadioSelect from django.urls import reverse, reverse_lazy from django.contrib import messages from django.db.models import Sum, F, Count from .services import elm, fetch_colors, translate, decode_vin_pyvin, normalize_name from . import models, tables, forms from django_tables2.export.views import ExportMixin logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) def switch_language(request): language = request.GET.get("language", "en") referer = request.META.get("HTTP_REFERER", "/") parsed_url = urlparse(referer) path_parts = parsed_url.path.split("/") if path_parts[1] in dict(settings.LANGUAGES): path_parts.pop(1) new_path = "/".join(path_parts) new_url = urlunparse( ( parsed_url.scheme, parsed_url.netloc, new_path, parsed_url.params, parsed_url.query, parsed_url.fragment, ) ) if language in dict(settings.LANGUAGES): logger.debug(f"Switching language to: {language}") response = redirect(new_url) response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language) translation.activate(language) request.session[settings.LANGUAGE_COOKIE_NAME] = language logger.debug( f"Language switched to: {language}, Session: {request.session[settings.LANGUAGE_COOKIE_NAME]}" ) return response else: logger.warning(f"Invalid language code: {language}") return redirect("/") class HomeView(LoginRequiredMixin, TemplateView): template_name = "index.html" def dispatch(self, request, *args, **kwargs): if not hasattr(request.user, "dealer") or not request.user.is_authenticated: messages.error(request, _("You are not associated with any dealer.")) return redirect("welcome") return super().dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) total_cars = models.Car.objects.count() total_reservations = models.CarReservation.objects.filter( reserved_until__gte=timezone.now() ).count() stats = models.CarFinance.objects.aggregate( total_cost_price=Sum("cost_price"), total_selling_price=Sum("selling_price"), ) total_cost_price = stats["total_cost_price"] or 0 total_selling_price = stats["total_selling_price"] or 0 total_profit = total_selling_price - total_cost_price context["total_cars"] = total_cars context["total_reservations"] = total_reservations context["total_cost_price"] = total_cost_price context["total_selling_price"] = total_selling_price context["total_profit"] = total_profit return context class WelcomeView(TemplateView): template_name = "welcome.html" def dispatch(self, request, *args, **kwargs): if hasattr(request.user, "dealer") and request.user.is_authenticated: return redirect("landing_page") return super().dispatch(request, *args, **kwargs) class CarCreateView(LoginRequiredMixin, CreateView): model = models.Car form_class = forms.CarForm template_name = "inventory/car_form.html" success_url = reverse_lazy("inventory_stats") def form_valid(self, form): form.instance.dealer = self.request.user.dealer form.save() messages.success(self.request, "Car saved successfully.") return super().form_valid(form) class AjaxHandlerView(LoginRequiredMixin, View): def get(self, request, *args, **kwargs): action = request.GET.get("action") handlers = { "decode_vin": self.decode_vin, "get_models": self.get_models, "get_series": self.get_series, "get_trims": self.get_trims, "get_specifications": self.get_specifications, } handler = handlers.get(action) if handler: return handler(request) else: return JsonResponse({"error": "Invalid action"}, status=400) def decode_vin(self, request): vin_no = request.GET.get("vin_no") if not vin_no or len(vin_no.strip()) != 17: return JsonResponse( {"success": False, "error": "Invalid VIN number provided."}, status=400 ) vin_no = vin_no.strip() vin_data = {} decoding_method = "" decoding_methods = [("PYVIN", decode_vin_pyvin), ("VIN", VIN), ("ELM", elm)] manufacturer_name = model_name_before = model_name = year_model = None for method_name, decode_function in decoding_methods: try: vin_info = decode_function(vin_no) if vin_info: if method_name == "PYVIN": manufacturer_name = vin_info.Make.strip() model_name_before = vin_info.Model.strip() year_model = vin_info.ModelYear if not manufacturer_name or not year_model: raise ValueError("PYVIN returned incomplete data.") elif method_name == "VIN": manufacturer_name = vin_info.make.strip() model_name_before = vin_info.model.strip() year_model = vin_info.model_year if ( not manufacturer_name or not model_name_before or not year_model ): raise ValueError("VIN returned incomplete data.") elif method_name == "ELM": elm_data = vin_info.get("data", {}) manufacturer_name = elm_data.get("maker", "").strip() model_name_before = elm_data.get("model", "").strip() year_model = elm_data.get("modelYear", "").strip() if ( not manufacturer_name or not model_name_before or not year_model ): raise ValueError("ELM returned incomplete data.") model_name = normalize_name(model_name_before) decoding_method = method_name print(f"decoded by {method_name}") break else: logger.warning(f"{method_name} returned no data for {vin_no}.") except Exception as e: logger.warning( f"VIN decoding with {method_name} failed for {vin_no}: {e}" ) if not manufacturer_name or not model_name or not year_model: return JsonResponse( {"success": False, "error": "VIN not found in all sources."}, status=404 ) logger.info( f"VIN decoded using {decoding_method}: Make={manufacturer_name}, Model={model_name}, Year={year_model}" ) car_make = models.CarMake.objects.filter( name__icontains=manufacturer_name ).first() if not car_make: return JsonResponse( {"success": False, "error": "Manufacturer not found in the database."}, status=404, ) vin_data["make_id"] = car_make.id_car_make vin_data["name"] = car_make.name vin_data["arabic_name"] = car_make.arabic_name car_model = models.CarModel.objects.filter( id_car_make=car_make.id_car_make, name__icontains=model_name ).first() if not car_model: return JsonResponse( { "success": False, "error": "Model not found for the given manufacturer.", }, status=404, ) vin_data["model_id"] = car_model.id_car_model vin_data["year"] = year_model return JsonResponse({"success": True, "data": vin_data}) def get_models(self, request): make_id = request.GET.get("make_id") car_models = models.CarModel.objects.filter(id_car_make=make_id).values( "id_car_model", "name", "arabic_name" ) return JsonResponse(list(car_models), safe=False) def get_series(self, request): model_id = request.GET.get("model_id") series = models.CarSerie.objects.filter( id_car_model=model_id, ).values("id_car_serie", "name", "arabic_name") return JsonResponse(list(series), safe=False) def get_trims(self, request): serie_id = request.GET.get("serie_id") trims = models.CarTrim.objects.filter(id_car_serie=serie_id).values( "id_car_trim", "name", "arabic_name" ) return JsonResponse(list(trims), safe=False) def get_specifications(self, request): trim_id = request.GET.get("trim_id") car_spec_values = models.CarSpecificationValue.objects.filter( id_car_trim=trim_id ) lang = translation.get_language() specs_by_parent = {} for value in car_spec_values: specification = value.id_car_specification parent = specification.id_parent parent_id = parent.id_car_specification if parent else 0 if lang == "ar": parent_name = parent.arabic_name if parent else "Root" else: parent_name = parent.name if parent else "Root" if parent_id not in specs_by_parent: specs_by_parent[parent_id] = { "parent_name": parent_name, "specifications": [], } spec_data = { "specification_id": specification.id_car_specification, "s_name": specification.arabic_name if lang == "ar" else specification.name, "s_value": value.value, "s_unit": value.unit if value.unit else "", "trim_name": value.id_car_trim.name, } specs_by_parent[parent_id]["specifications"].append(spec_data) serialized_specs = [ {"parent_name": v["parent_name"], "specifications": v["specifications"]} for v in specs_by_parent.values() ] return JsonResponse(serialized_specs, safe=False) class CarInventory(LoginRequiredMixin, ListView): model = models.Car home_label = _("inventory") template_name = "inventory/car_inventory.html" context_object_name = "cars" paginate_by = 10 ordering = ["receiving_date"] def get_queryset(self, *args, **kwargs): query = self.request.GET.get("q") make_id = self.kwargs["make_id"] model_id = self.kwargs["model_id"] trim_id = self.kwargs["trim_id"] cars = models.Car.objects.filter( dealer__user=self.request.user, id_car_make=make_id, id_car_model=model_id, id_car_trim=trim_id, ).order_by("receiving_date") if query: cars = cars.filter(Q(vin__icontains=query)) return cars def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["query"] = self.request.GET.get("q", "") context["make_id"] = self.kwargs["make_id"] context["model_id"] = self.kwargs["model_id"] context["trim_id"] = self.kwargs["trim_id"] return context @login_required def inventory_stats_view(request): dealer = request.user.dealer # Annotate total cars by make, model, and trim cars = ( models.Car.objects.filter(dealer=dealer) .select_related("id_car_make", "id_car_model", "id_car_trim") .annotate( make_total=Count("id_car_make"), model_total=Count("id_car_model"), trim_total=Count("id_car_trim"), ) ) # Prepare the nested structure inventory = {} for car in cars: # Make Level make = car.id_car_make if make.id_car_make not in inventory: inventory[make.id_car_make] = { "make_id": make.id_car_make, "make_name": make.get_local_name(), "total_cars": 0, "models": {}, } inventory[make.id_car_make]["total_cars"] += 1 # Model Level model = car.id_car_model if model and model.id_car_model not in inventory[make.id_car_make]["models"]: inventory[make.id_car_make]["models"][model.id_car_model] = { "model_id": model.id_car_model, "model_name": model.get_local_name(), "total_cars": 0, "trims": {}, } inventory[make.id_car_make]["models"][model.id_car_model]["total_cars"] += 1 # Trim Level trim = car.id_car_trim if ( trim and trim.id_car_trim not in inventory[make.id_car_make]["models"][model.id_car_model]["trims"] ): inventory[make.id_car_make]["models"][model.id_car_model]["trims"][ trim.id_car_trim ] = {"trim_id": trim.id_car_trim, "trim_name": trim.name, "total_cars": 0} inventory[make.id_car_make]["models"][model.id_car_model]["trims"][ trim.id_car_trim ]["total_cars"] += 1 # Convert to a list for easier template rendering result = { "total_cars": cars.count(), "makes": [ { "make_id": make_data["make_id"], "make_name": make_data["make_name"], "total_cars": make_data["total_cars"], "models": [ { "model_id": model_data["model_id"], "model_name": model_data["model_name"], "total_cars": model_data["total_cars"], "trims": list(model_data["trims"].values()), } for model_data in make_data["models"].values() ], } for make_data in inventory.values() ], } return render(request, "inventory/inventory_stats.html", {"inventory": result}) class CarDetailView(LoginRequiredMixin, DetailView): model = models.Car template_name = "inventory/car_detail.html" context_object_name = "car" class CarFinanceCreateView(LoginRequiredMixin, CreateView): model = models.CarFinance form_class = forms.CarFinanceForm template_name = "inventory/car_finance_form.html" def dispatch(self, request, *args, **kwargs): self.car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) return super().dispatch(request, *args, **kwargs) def form_valid(self, form): form.instance.car = self.car messages.success(self.request, _("Car finance details saved successfully.")) return super().form_valid(form) def get_success_url(self): return reverse("car_detail", kwargs={"pk": self.car.pk}) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["car"] = self.car return context class CarFinanceUpdateView(LoginRequiredMixin, UpdateView): model = models.CarFinance form_class = forms.CarFinanceForm template_name = "inventory/car_finance_form.html" def form_valid(self, form): messages.success(self.request, _("Car finance updated successfully.")) return super().form_valid(form) def get_success_url(self): return reverse("car_detail", kwargs={"pk": self.object.car.pk}) class CarUpdateView(LoginRequiredMixin, UpdateView): model = models.Car form_class = forms.CarUpdateForm template_name = "inventory/car_edit.html" def form_valid(self, form): messages.success(self.request, _("Car updated successfully.")) return super().form_valid(form) def get_success_url(self): return reverse("car_detail", kwargs={"pk": self.object.pk}) class CarDeleteView(LoginRequiredMixin, DeleteView): model = models.Car template_name = "inventory/car_confirm_delete.html" success_url = reverse_lazy("inventory_stats") def delete(self, request, *args, **kwargs): messages.success(request, _("Car deleted successfully.")) return super().delete(request, *args, **kwargs) class CustomCardCreateView(LoginRequiredMixin, CreateView): model = models.CustomCard form_class = forms.CustomCardForm template_name = "inventory/add_custom_card.html" def form_valid(self, form): car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) form.instance.car = car return super().form_valid(form) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["car"] = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) return context def get_success_url(self): messages.success(self.request, _("Custom Card added successfully.")) return reverse_lazy("car_detail", kwargs={"pk": self.kwargs["car_pk"]}) class CarColorCreateView(LoginRequiredMixin, CreateView): model = models.CarColors template_name = "inventory/color_palette.html" def dispatch(self, request, *args, **kwargs): self.car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) self.available_colors = self.fetch_available_colors() return super().dispatch(request, *args, **kwargs) def get_form_class(self): class ColorPickerForm(ModelForm): color = ChoiceField( choices=self.get_color_choices(), widget=RadioSelect(attrs={"class": "color-picker"}), label=_("Select a Color"), ) color_type = ChoiceField( choices=models.CarColors.ColorType.choices, widget=RadioSelect(attrs={"class": "color-type-picker"}), label=_("Select Color Type"), ) class Meta: model = models.CarColors fields = ["color", "color_type"] return ColorPickerForm def fetch_available_colors(self): car_data = { "make": self.car.id_car_make.name, "model": self.car.id_car_model.name, "year": str(self.car.year), } return fetch_colors(car_data) or [] def get_color_choices(self): return [(color["rgb"], color["name"]) for color in self.available_colors] def form_valid(self, form): selected_rgb = form.cleaned_data["color"] selected_name = next( ( color["name"] for color in self.available_colors if color["rgb"] == selected_rgb ), None, ) if not selected_name: messages.error(self.request, _("Invalid color selection.")) return self.form_invalid(form) # Assign the car and selected color details form.instance.car = self.car form.instance.rgb = selected_rgb form.instance.name = selected_name form.instance.arabic_name = translate(selected_name) form.instance.color_type = form.cleaned_data["color_type"] messages.success(self.request, _("Color added successfully.")) return super().form_valid(form) def get_success_url(self): return reverse("car_detail", kwargs={"pk": self.car.pk}) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["car"] = self.car return context class CarColorUpdateView(LoginRequiredMixin, UpdateView): model = forms.CarColors template_name = "inventory/color_palette.html" def dispatch(self, request, *args, **kwargs): self.car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) self.available_colors = self.fetch_available_colors() return super().dispatch(request, *args, **kwargs) def get_form_class(self): class ColorPickerForm(ModelForm): color = ChoiceField( choices=self.get_color_choices(), widget=RadioSelect(attrs={"class": "color-picker"}), label=_("Select a Color"), ) class Meta: model = forms.CarColors fields = ["color"] return ColorPickerForm def fetch_available_colors(self): car_data = { "make": self.car.id_car_make.name, "model": self.car.id_car_model.name, "year": str(self.car.year), } return fetch_colors(car_data) or [] def get_color_choices(self): return [(color["rgb"], color["name"]) for color in self.available_colors] def form_valid(self, form): selected_rgb = form.cleaned_data["color"] selected_name = next( ( color["name"] for color in self.available_colors if color["rgb"] == selected_rgb ), None, ) if not selected_name: messages.error(self.request, _("Invalid color selection.")) return self.form_invalid(form) form.instance.rgb = selected_rgb form.instance.name = selected_name form.instance.arabic_name = translate(selected_name) messages.success(self.request, _("Exterior color updated successfully.")) return super().form_valid(form) def get_success_url(self): return reverse("car_detail", kwargs={"pk": self.car.pk}) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["car"] = self.car return context @login_required() def reserve_car_view(request, car_id): if request.method == "POST": car = get_object_or_404(models.Car, pk=car_id) if car.is_reserved(): messages.error(request, _("This car is already reserved.")) return redirect("car_detail", pk=car.pk) try: reserved_until = timezone.now() + timezone.timedelta(hours=24) models.CarReservation.objects.create( car=car, reserved_by=request.user, reserved_until=reserved_until ) messages.success(request, _("Car reserved successfully.")) except Exception as e: messages.error(request, f"Error reserving car: {e}") return redirect("car_detail", pk=car.pk) return JsonResponse( {"success": False, "message": "Invalid request method."}, status=400 ) @login_required def manage_reservation(request, reservation_id): reservation = get_object_or_404( models.CarReservation, pk=reservation_id, reserved_by=request.user ) if request.method == "POST": action = request.POST.get("action") if action == "renew": reservation.reserved_until = timezone.now() + timezone.timedelta(hours=24) reservation.save() messages.success(request, _("Reservation renewed successfully.")) return redirect("car_detail", pk=reservation.car.pk) elif action == "cancel": reservation.delete() messages.success(request, _("Reservation canceled successfully.")) return redirect("car_detail", pk=reservation.car.pk) else: return JsonResponse( {"success": False, "message": _("Invalid action.")}, status=400 ) return JsonResponse( {"success": False, "message": _("Invalid request method.")}, status=400 ) class DealerListView(LoginRequiredMixin, ListView): model = models.Dealer template_name = "dealer_list.html" context_object_name = "dealers" class DealerDetailView(LoginRequiredMixin, DetailView): model = models.Dealer template_name = "dealers/dealer_detail.html" context_object_name = "dealer" class DealerCreateView(LoginRequiredMixin, CreateView): model = models.Dealer form_class = forms.DealerForm template_name = "dealer_form.html" success_url = reverse_lazy("dealer_list") def form_valid(self, form): messages.success(self.request, _("Dealer created successfully.")) return super().form_valid(form) class DealerUpdateView(LoginRequiredMixin, UpdateView): model = models.Dealer form_class = forms.DealerForm template_name = "dealers/dealer_form.html" success_url = reverse_lazy("dealer_detail") def form_valid(self, form): messages.success(self.request, _("Dealer updated successfully.")) return super().form_valid(form) class DealerDeleteView(LoginRequiredMixin, DeleteView): model = models.Dealer template_name = "dealer_confirm_delete.html" success_url = reverse_lazy("dealer_list") def delete(self, request, *args, **kwargs): messages.success(request, _("Dealer deleted successfully.")) return super().delete(request, *args, **kwargs) class CustomerListView(LoginRequiredMixin, ListView): model = models.Customer home_label = _("customers") context_object_name = "customers" paginate_by = 10 template_name = "customers/customer_list.html" def get_queryset(self): query = self.request.GET.get("q") customers = models.Customer.objects.filter(dealer__user=self.request.user) if query: customers = customers.filter( Q(national_id__icontains=query) | Q(first_name__icontains=query) | Q(last_name__icontains=query) ) return customers def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["query"] = self.request.GET.get("q", "") return context class CustomerDetailView(LoginRequiredMixin, DetailView): model = models.Customer template_name = "customers/view_customer.html" context_object_name = "customer" class CustomerCreateView(LoginRequiredMixin, CreateView): model = models.Customer form_class = forms.CustomerForm template_name = "customers/customer_form.html" success_url = reverse_lazy("customer_list") def form_valid(self, form): if form.is_valid(): form.instance.dealer = self.request.user.dealer form.save() messages.success(self.request, _("Customer created successfully.")) return super().form_valid(form) else: return form.errors class CustomerUpdateView(LoginRequiredMixin, UpdateView): model = models.Customer form_class = forms.CustomerForm template_name = "customers/customer_form.html" success_url = reverse_lazy("customer_list") def form_valid(self, form): if form.is_valid(): form.instance.dealer = self.request.user.dealer form.save() messages.success(self.request, _("Customer updated successfully.")) return super().form_valid(form) else: return form.errors @login_required def delete_customer(request, pk): customer = get_object_or_404(models.Customer, pk=pk) customer.delete() <<<<<<< HEAD messages.success(request, _('Customer deleted successfully.')) return redirect('customer_list') class VendorListView(LoginRequiredMixin, ListView): model = models.Vendor context_object_name = 'vendors' paginate_by = 10 template_name = "vendors/vendors_list.html" class VendorDetailView(LoginRequiredMixin, DetailView): model = models.Vendor template_name = "vendors/view_vendor.html" class VendorCreateView(LoginRequiredMixin, CreateView): model = models.Vendor form_class = forms.VendorForm template_name = 'vendors/vendor_form.html' success_url = reverse_lazy('vendor_list') def form_valid(self, form): if form.is_valid(): form.instance.dealer = self.request.user.dealer form.save() messages.success(self.request, _('Vendor created successfully.')) return super().form_valid(form) else: return form.errors class VendorUpdateView(LoginRequiredMixin, UpdateView): model = models.Vendor form_class = forms.VendorForm template_name = 'vendors/vendor_form.html' success_url = reverse_lazy('vendor_list') def form_valid(self, form): if form.is_valid(): form.instance.dealer = self.request.user.dealer form.save() messages.success(self.request, _('Vendor updated successfully.')) return super().form_valid(form) else: return form.errors @login_required def delete_vendor(request, pk): vendor = get_object_or_404(models.Vendor, pk=pk) vendor.delete() messages.success(request, _('Vendor deleted successfully.')) return redirect('vendor_list') ======= messages.success(request, _("Customer deleted successfully.")) return redirect("customer_list") >>>>>>> c96865f (add search to select fields, redirect logged in user to landingpage if they are authenticated)