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" 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() messages.success(request, _('Customer deleted successfully.')) return redirect('customer_list')