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 inventory.mixins import AddDealerInstanceMixin from .services import elm, decodevin,get_make,get_model,normalize_name from . import models, forms from django_tables2.export.views import ExportMixin from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.decorators import user_passes_test from django.contrib.messages.views import SuccessMessageMixin from django.contrib.auth.models import Group from django.contrib.auth import get_user_model User = get_user_model() 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 any(hasattr(request.user, attr) for attr in ['dealer', 'subdealer']) 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 get_success_url(self): """Determine the redirect URL based on user choice.""" if self.request.POST.get('add_another'): return reverse('car_add') return reverse('inventory_stats') def form_valid(self, form): form.instance.dealer = self.request.user.dealer.get_parent_or_self 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 = '' manufacturer_name = model_name = year_model = None if not (result :=decodevin(vin_no)): return JsonResponse({'success': False, 'error': 'VIN not found in all sources.'}, status=404) manufacturer_name,model_name,year_model = result.values() make = get_make(manufacturer_name) model = get_model(model_name,make) logger.info( f"VIN decoded using {decoding_method}: Make={manufacturer_name}, Model={model_name}, Year={year_model}" ) car_model = model car_make = make 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 if not car_model: vin_data['model_id'] = "" else: 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=self.request.user.dealer.get_parent_or_self, 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 class CarColorCreate(LoginRequiredMixin, CreateView): model = models.CarColors form_class = forms.CarColorsForm template_name = "inventory/add_colors.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_success_url(self): return reverse_lazy('car_detail', kwargs={'pk': self.kwargs['car_pk']}) 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 @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.get_parent_or_self) .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,SuccessMessageMixin, UpdateView): model = models.CarFinance form_class = forms.CarFinanceForm template_name = 'inventory/car_finance_form.html' success_message = _('Car finance details updated successfully.') def get_success_url(self): return reverse('car_detail', kwargs={'pk': self.object.car.pk}) class CarUpdateView(LoginRequiredMixin, SuccessMessageMixin,UpdateView): model = models.Car form_class = forms.CarUpdateForm template_name = 'inventory/car_edit.html' success_message = _('Car updated successfully.') def get_success_url(self): return reverse('car_detail', kwargs={'pk': self.object.pk}) class CarDeleteView(LoginRequiredMixin,SuccessMessageMixin, DeleteView): model = models.Car template_name = 'inventory/car_confirm_delete.html' success_url = reverse_lazy('inventory_stats') 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']}) @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, SuccessMessageMixin,CreateView): model = models.Dealer form_class = forms.DealerForm template_name = 'dealer_form.html' success_url = reverse_lazy('dealer_list') success_message = _('Dealer created successfully.') class DealerUpdateView(LoginRequiredMixin,SuccessMessageMixin, UpdateView): model = models.Dealer form_class = forms.DealerForm template_name = 'dealers/dealer_form.html' success_url = reverse_lazy('dealer_detail') success_message = _('Dealer updated successfully.') def get_success_url(self): return reverse('dealer_detail', kwargs={'pk': self.object.pk}) def get_form(self, form_class=None): form = super().get_form(form_class) form.fields.pop('dealer_type') return form def get_form_class(self): if self.request.user.dealer.is_parent: return forms.DealerForm else: return forms.UserForm class DealerDeleteView(LoginRequiredMixin,SuccessMessageMixin, DeleteView): model = models.Dealer template_name = 'dealer_confirm_delete.html' success_url = reverse_lazy('dealer_list') success_message = _('Dealer deleted successfully.') class CustomerListView(LoginRequiredMixin,PermissionRequiredMixin, ListView): model = models.Customer home_label = _('customers') context_object_name = 'customers' paginate_by = 10 template_name = "customers/customer_list.html" permission_required = ('inventory.view_customer',) def get_queryset(self): query = self.request.GET.get('q') customers = models.Customer.objects.filter(dealer=self.request.user.dealer.get_parent_or_self) 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,PermissionRequiredMixin, DetailView): model = models.Customer template_name = 'customers/view_customer.html' context_object_name = 'customer' permission_required = ('inventory.view_customer',) class CustomerCreateView(LoginRequiredMixin,PermissionRequiredMixin,SuccessMessageMixin,AddDealerInstanceMixin, CreateView): model = models.Customer form_class = forms.CustomerForm template_name = 'customers/customer_form.html' success_url = reverse_lazy('customer_list') permission_required = ('inventory.add_customer',) success_message = _('Customer created successfully.') class CustomerUpdateView(LoginRequiredMixin,PermissionRequiredMixin,SuccessMessageMixin,AddDealerInstanceMixin, UpdateView): model = models.Customer form_class = forms.CustomerForm template_name = 'customers/customer_form.html' success_url = reverse_lazy('customer_list') permission_required = ('inventory.change_customer',) success_message = _('Customer updated successfully.') @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') class VendorListView(LoginRequiredMixin,PermissionRequiredMixin, ListView): model = models.Vendor context_object_name = 'vendors' paginate_by = 10 template_name = "vendors/vendors_list.html" permission_required = ('inventory.view_vendor',) class VendorDetailView(LoginRequiredMixin,PermissionRequiredMixin, DetailView): model = models.Vendor template_name = "vendors/view_vendor.html" permission_required = ('inventory.view_vendor',) class VendorCreateView(LoginRequiredMixin,PermissionRequiredMixin,SuccessMessageMixin,AddDealerInstanceMixin, CreateView): model = models.Vendor form_class = forms.VendorForm template_name = 'vendors/vendor_form.html' success_url = reverse_lazy('vendor_list') permission_required = ('inventory.add_vendor',) success_message = _('Vendor created successfully.') class VendorUpdateView(LoginRequiredMixin,PermissionRequiredMixin,SuccessMessageMixin,AddDealerInstanceMixin, UpdateView): model = models.Vendor form_class = forms.VendorForm template_name = 'vendors/vendor_form.html' success_url = reverse_lazy('vendor_list') permission_required = ('inventory.change_vendor',) success_message = _('Vendor updated successfully.') @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') class QuotationCreateView(LoginRequiredMixin,PermissionRequiredMixin, CreateView): model = models.SaleQuotation form_class = forms.QuotationForm template_name = 'sales/quotation_form.html' permission_required = ('inventory.add_salequotation',) def form_valid(self, form): quotation = form.save() selected_cars = form.cleaned_data.get("cars") for car in selected_cars: car_finance = car.finances.first() if car_finance: models.SaleQuotationCar.objects.create( quotation=quotation, car=car, ) messages.success(self.request, _("Quotation created successfully.")) return redirect('quotation_list') class QuotationListView(LoginRequiredMixin,PermissionRequiredMixin, ListView): model = models.SaleQuotation template_name = "sales/quotation_list.html" context_object_name = "quotations" paginate_by = 10 permission_required = ('inventory.view_salequotation',) def get_queryset(self): status = self.request.GET.get("status") queryset = models.SaleQuotation.objects.all() if status: queryset = queryset.filter(status=status) return queryset class QuotationDetailView(LoginRequiredMixin,PermissionRequiredMixin, DetailView): model = models.SaleQuotation template_name = "sales/quotation_detail.html" context_object_name = "quotation" permission_required = ('inventory.view_salequotation',) @login_required def confirm_quotation(request, pk): quotation = get_object_or_404(models.SaleQuotation, pk=pk) try: quotation.confirm() models.SalesOrder.objects.create( quotation=quotation, total_amount=quotation.quotation_cars.aggregate(Sum("total_amount"))["total_amount__sum"], ) messages.success(request, _("Quotation confirmed and sales order created.")) except ValueError as e: messages.error(request, str(e)) return redirect("quotation_detail", pk=pk) class SalesOrderDetailView(LoginRequiredMixin,PermissionRequiredMixin, DetailView): model = models.SalesOrder template_name = "sales/sales_order_detail.html" context_object_name = "sales_order" permission_required = ('inventory.view_salequotation',) slug_field = 'order_id' slug_url_kwarg = 'order_id' #Users class UserListView(LoginRequiredMixin,PermissionRequiredMixin, ListView): model = models.Dealer context_object_name = 'users' paginate_by = 10 template_name = "users/user_list.html" permission_required = ('inventory.view_dealer',) def get_queryset(self): query = self.request.GET.get('q') users = self.request.user.dealer.sub_dealers if query: users = users.filter( Q(name__icontains=query) | Q(arabic_name__icontains=query) | Q(phone_number__icontains=query)| Q(address__icontains=query) ) return users.all() class UserDetailView(LoginRequiredMixin,PermissionRequiredMixin, DetailView): model = models.Dealer template_name = "users/user_detail.html" context_object_name = "user" permission_required = ('inventory.view_dealer',) class UserCreateView(LoginRequiredMixin,PermissionRequiredMixin,SuccessMessageMixin,AddDealerInstanceMixin, CreateView): model = models.Dealer form_class = forms.UserForm template_name = 'users/user_form.html' success_url = reverse_lazy('user_list') permission_required = ('inventory.add_dealer',) success_message = _('User created successfully.') def get_form(self, form_class = None): form = super().get_form(form_class) form.fields['dealer_type'].choices = [t for t in form.fields['dealer_type'].choices if t[0] != 'Owner'] return form def form_valid(self, form): dealer = self.request.user.dealer.get_parent_or_self if dealer.sub_dealers.count() >= dealer.get_active_plan.max_users: messages.error(self.request, _("You have reached the maximum number of users.")) return redirect('user_list') user = User.objects.create_user(username=form.cleaned_data['name']) user.set_password("Tenhal@123") user.save() form.instance.user = user form.instance.parent_dealer = dealer for group in user.groups.all(): group.user_set.remove(user) Group.objects.get(name=form.cleaned_data['dealer_type'].lower()).user_set.add(user) form.save() return super().form_valid(form) class UserUpdateView(LoginRequiredMixin,PermissionRequiredMixin,SuccessMessageMixin,AddDealerInstanceMixin, UpdateView): model = models.Dealer form_class = forms.UserForm template_name = 'users/user_form.html' success_url = reverse_lazy('user_list') permission_required = ('inventory.change_dealer',) success_message = _('User updated successfully.') def get_form(self, form_class = None): form = super().get_form(form_class) if not self.request.user.has_perms(["inventory.change_dealer_type"]): field = form.fields['dealer_type'] field.widget = field.hidden_widget() form.fields['dealer_type'].choices = [t for t in form.fields['dealer_type'].choices if t[0] != 'Owner'] return form def form_valid(self, form): user = form.instance.user for group in user.groups.all(): group.user_set.remove(user) Group.objects.get(name=form.cleaned_data['dealer_type'].lower()).user_set.add(user) form.save() return super().form_valid(form) def UserDeleteview(request, pk): user = get_object_or_404(models.Dealer, pk=pk) user.delete() messages.success(request, _('User deleted successfully.')) return redirect('user_list') #errors def custom_page_not_found_view(request, exception): return render(request, "errors/404.html", {}) def custom_error_view(request, exception=None): return render(request, "errors/500.html", {}) def custom_permission_denied_view(request, exception=None): return render(request, "errors/403.html", {}) def custom_bad_request_view(request, exception=None): return render(request, "errors/400.html", {})