diff --git a/inventory/forms.py b/inventory/forms.py index bfc563b7..6019c38c 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -127,17 +127,17 @@ class StaffForm(forms.ModelForm): queryset=Service.objects.all(), required=False, ) - phone_number = SaudiPhoneNumberField( - required=False, - widget=forms.TextInput( - attrs={ - "class": "form-control", - "placeholder": _("Phone Number"), - "id": "phone", - } - ), - label=_("Phone Number"), - ) + # phone_number = SaudiPhoneNumberField( + # required=False, + # widget=forms.TextInput( + # attrs={ + # "class": "form-control", + # "placeholder": _("Phone Number"), + # "id": "phone", + # } + # ), + # label=_("Phone Number"), + # ) group = forms.ModelMultipleChoiceField( label=_("Group"), widget=forms.CheckboxSelectMultiple(attrs={"class": "form-check-input"}), diff --git a/inventory/models.py b/inventory/models.py index 8f3af378..e162ed3a 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -1467,7 +1467,7 @@ class Staff(models.Model): first_name = models.CharField(max_length=255, verbose_name=_("First Name")) last_name = models.CharField(max_length=255, verbose_name=_("Last Name")) - arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) + arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"),null=True,blank=True) phone_number = models.CharField( max_length=255, verbose_name=_("Phone Number"), diff --git a/inventory/urls.py b/inventory/urls.py index c7e8dabc..b1f29988 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -130,7 +130,7 @@ urlpatterns = [ ), path( "/crm/leads//update/", - views.LeadUpdateView.as_view(), + views.lead_update, name="lead_update", ), path( diff --git a/inventory/views.py b/inventory/views.py index 322f3b6d..cd6613ec 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -18,6 +18,7 @@ from decimal import Decimal from io import TextIOWrapper from django.apps import apps from datetime import datetime, timedelta,date +from datetime import datetime, timedelta,date from calendar import month_name from pyzbar.pyzbar import decode from urllib.parse import urlparse, urlunparse @@ -108,6 +109,7 @@ from django_ledger.forms.bank_account import ( ) from django_ledger.views.chart_of_accounts import ( ChartOfAccountModelListView as ChartOfAccountModelListViewBase + ChartOfAccountModelListView as ChartOfAccountModelListViewBase ) from django_ledger.views.bill import ( BillModelCreateView, @@ -174,6 +176,7 @@ from django_ledger.models import ( LedgerModel, PurchaseOrderModel, ChartOfAccountModel + ChartOfAccountModel ) from django_ledger.views.financial_statement import ( FiscalYearBalanceSheetView, @@ -405,6 +408,7 @@ class TestView(TemplateView): template_name = "inventory/cars_list_api.html" @login_required +def general_dashboard(request,dealer_slug): def general_dashboard(request,dealer_slug): """ Renders the dealer dashboard with key performance indicators and chart data. @@ -412,6 +416,9 @@ def general_dashboard(request,dealer_slug): dealer = get_object_or_404(models.Dealer,slug=dealer_slug) vat = models.VatRate.objects.filter(dealer=dealer,is_active=True).first() VAT_RATE=vat.rate + dealer = get_object_or_404(models.Dealer,slug=dealer_slug) + vat = models.VatRate.objects.filter(dealer=dealer,is_active=True).first() + VAT_RATE=vat.rate today_local = timezone.localdate() # ---------------------------------------------------- @@ -419,10 +426,14 @@ def general_dashboard(request,dealer_slug): # ---------------------------------------------------- start_date_str = request.GET.get('start_date') end_date_str = request.GET.get('end_date') + start_date_str = request.GET.get('start_date') + end_date_str = request.GET.get('end_date') if start_date_str and end_date_str: start_date = timezone.datetime.strptime(start_date_str, '%Y-%m-%d').date() end_date = timezone.datetime.strptime(end_date_str, '%Y-%m-%d').date() + start_date = timezone.datetime.strptime(start_date_str, '%Y-%m-%d').date() + end_date = timezone.datetime.strptime(end_date_str, '%Y-%m-%d').date() else: start_date = today_local - timedelta(days=30) end_date = today_local @@ -431,18 +442,25 @@ def general_dashboard(request,dealer_slug): # 2. Inventory KPIs # ---------------------------------------------------- active_cars = models.Car.objects.filter(dealer=dealer).exclude(status='sold') + active_cars = models.Car.objects.filter(dealer=dealer).exclude(status='sold') total_cars_in_inventory = active_cars.count() total_inventory_value = active_cars.aggregate(total=Sum('cost_price'))['total'] or 0 new_cars_qs = active_cars.filter(stock_type='new') + total_inventory_value = active_cars.aggregate(total=Sum('cost_price'))['total'] or 0 + new_cars_qs = active_cars.filter(stock_type='new') total_new_cars_in_inventory = new_cars_qs.count() new_car_value = new_cars_qs.aggregate(total=Sum('cost_price'))['total'] or 0 used_cars_qs = active_cars.filter(stock_type='used') + new_car_value = new_cars_qs.aggregate(total=Sum('cost_price'))['total'] or 0 + used_cars_qs = active_cars.filter(stock_type='used') total_used_cars_in_inventory = used_cars_qs.count() used_car_value = used_cars_qs.aggregate(total=Sum('cost_price'))['total'] or 0 + used_car_value = used_cars_qs.aggregate(total=Sum('cost_price'))['total'] or 0 aging_threshold_days = 60 aging_inventory_count = active_cars.filter(receiving_date__date__lte=today_local - timedelta(days=aging_threshold_days)).count() + aging_inventory_count = active_cars.filter(receiving_date__date__lte=today_local - timedelta(days=aging_threshold_days)).count() # ---------------------------------------------------- # 3. Sales KPIs (filtered by date) @@ -450,8 +468,10 @@ def general_dashboard(request,dealer_slug): cars_sold_filtered = models.Car.objects.filter( dealer=dealer, status='sold', + status='sold', sold_date__date__gte=start_date, sold_date__date__lte=end_date + sold_date__date__lte=end_date ) # General sales KPIs @@ -460,23 +480,39 @@ def general_dashboard(request,dealer_slug): total_revenue_from_cars = cars_sold_filtered.aggregate( total=Sum(F('marked_price') - F('discount_amount')) )['total'] or 0 + total_cost_of_cars_sold = cars_sold_filtered.aggregate(total=Sum('cost_price'))['total'] or 0 + total_revenue_from_cars = cars_sold_filtered.aggregate( + total=Sum(F('marked_price') - F('discount_amount')) + )['total'] or 0 + total_vat_collected_from_cars = cars_sold_filtered.annotate( + final_price=F('marked_price') - F('discount_amount')).aggregate( + total=Sum(F('final_price') * VAT_RATE))['total'] or 0 total_vat_collected_from_cars = cars_sold_filtered.annotate( final_price=F('marked_price') - F('discount_amount')).aggregate( total=Sum(F('final_price') * VAT_RATE))['total'] or 0 net_profit_from_cars = total_revenue_from_cars - total_cost_of_cars_sold total_discount = cars_sold_filtered.aggregate(total=Sum('discount_amount'))['total'] or 0 + total_discount = cars_sold_filtered.aggregate(total=Sum('discount_amount'))['total'] or 0 # Sales breakdown by type new_cars_sold = cars_sold_filtered.filter(stock_type='new') + new_cars_sold = cars_sold_filtered.filter(stock_type='new') total_new_cars_sold = new_cars_sold.count() total_cost_of_new_cars_sold = new_cars_sold.aggregate(total=Sum('cost_price'))['total'] or 0 + total_cost_of_new_cars_sold = new_cars_sold.aggregate(total=Sum('cost_price'))['total'] or 0 # total_revenue_from_new_cars=sum([ car.final_price for car in new_cars_sold]) total_revenue_from_new_cars = new_cars_sold.aggregate( total=Sum(F('marked_price') - F('discount_amount')) )['total'] or 0 + total_revenue_from_new_cars = new_cars_sold.aggregate( + total=Sum(F('marked_price') - F('discount_amount')) + )['total'] or 0 + total_vat_collected_from_new_cars = new_cars_sold.annotate( + final_price=F('marked_price') - F('discount_amount')).aggregate( + total=Sum(F('final_price') * VAT_RATE))['total'] or 0 total_vat_collected_from_new_cars = new_cars_sold.annotate( final_price=F('marked_price') - F('discount_amount')).aggregate( total=Sum(F('final_price') * VAT_RATE))['total'] or 0 @@ -485,25 +521,41 @@ def general_dashboard(request,dealer_slug): + used_cars_sold = cars_sold_filtered.filter(stock_type='used') + + used_cars_sold = cars_sold_filtered.filter(stock_type='used') total_used_cars_sold = used_cars_sold.count() total_cost_of_used_cars_sold = used_cars_sold.aggregate(total=Sum('cost_price'))['total'] or 0 total_revenue_from_used_cars = used_cars_sold.aggregate( total=Sum(F('marked_price') - F('discount_amount')) )['total'] or 0 + total_cost_of_used_cars_sold = used_cars_sold.aggregate(total=Sum('cost_price'))['total'] or 0 + total_revenue_from_used_cars = used_cars_sold.aggregate( + total=Sum(F('marked_price') - F('discount_amount')) + )['total'] or 0 + total_vat_collected_from_used_cars = used_cars_sold.annotate( + final_price=F('marked_price') - F('discount_amount')).aggregate( + total=Sum(F('final_price') * VAT_RATE))['total'] or 0 total_vat_collected_from_used_cars = used_cars_sold.annotate( final_price=F('marked_price') - F('discount_amount')).aggregate( total=Sum(F('final_price') * VAT_RATE))['total'] or 0 net_profit_from_used_cars = total_revenue_from_used_cars - total_cost_of_used_cars_sold + net_profit_from_used_cars = total_revenue_from_used_cars - total_cost_of_used_cars_sold # Service & Overall KPIs total_revenue_from_services = sum([car.get_additional_services()['total'] for car in cars_sold_filtered]) total_vat_collected_from_services = sum([car.get_additional_services()['services_vat'] for car in cars_sold_filtered]) total_vat_collected = total_vat_collected_from_cars + total_vat_collected_from_services + total_revenue_from_services = sum([car.get_additional_services()['total'] for car in cars_sold_filtered]) + total_vat_collected_from_services = sum([car.get_additional_services()['services_vat'] for car in cars_sold_filtered]) + total_vat_collected = total_vat_collected_from_cars + total_vat_collected_from_services total_revenue_generated = total_revenue_from_cars + total_revenue_from_services + expenses = models.ItemModel.objects.filter(entity__admin__dealer=dealer, item_role='expense') + total_expenses = expenses.aggregate(total=Sum('default_amount'))['total'] or 0 expenses = models.ItemModel.objects.filter(entity__admin__dealer=dealer, item_role='expense') total_expenses = expenses.aggregate(total=Sum('default_amount'))['total'] or 0 gross_profit = net_profit_from_cars - total_expenses @@ -518,6 +570,13 @@ def general_dashboard(request,dealer_slug): total_revenue=Sum(F('marked_price') - F('discount_amount')), total_profit=Sum(F('marked_price') - F('discount_amount') - F('cost_price')) ).order_by('month') + monthly_sales_data = cars_sold_filtered.annotate( + month=ExtractMonth('sold_date') + ).values('month').annotate( + total_cars=Count('pk'), + total_revenue=Sum(F('marked_price') - F('discount_amount')), + total_profit=Sum(F('marked_price') - F('discount_amount') - F('cost_price')) + ).order_by('month') monthly_cars_sold = [0] * 12 monthly_revenue = [0] * 12 @@ -528,6 +587,10 @@ def general_dashboard(request,dealer_slug): monthly_cars_sold[month_index] = data['total_cars'] monthly_revenue[month_index] = float(data['total_revenue']) if data['total_revenue'] else 0 monthly_net_profit[month_index] = float(data['total_profit']) if data['total_profit'] else 0 + month_index = data['month'] - 1 + monthly_cars_sold[month_index] = data['total_cars'] + monthly_revenue[month_index] = float(data['total_revenue']) if data['total_revenue'] else 0 + monthly_net_profit[month_index] = float(data['total_profit']) if data['total_profit'] else 0 monthly_cars_sold_json = json.dumps(monthly_cars_sold) monthly_revenue_json = json.dumps(monthly_revenue) @@ -539,6 +602,13 @@ def general_dashboard(request,dealer_slug): sales_by_make_data = cars_sold_filtered.values('id_car_make__name').annotate( car_count=Count('id_car_make__name') ).order_by('-car_count') + sales_by_make_data = cars_sold_filtered.values('id_car_make__name').annotate( + car_count=Count('id_car_make__name') + ).order_by('-car_count') + + sales_by_make_labels = [data['id_car_make__name'] for data in sales_by_make_data] + sales_by_make_counts = [data['car_count'] for data in sales_by_make_data] + sales_by_make_labels = [data['id_car_make__name'] for data in sales_by_make_data] sales_by_make_counts = [data['car_count'] for data in sales_by_make_data] @@ -550,12 +620,16 @@ def general_dashboard(request,dealer_slug): # ---------------------------------------------------- + # Get the selected make from the URL query parameter selected_make_sales= request.GET.get('make_sold', None) + selected_make_sales= request.GET.get('make_sold', None) + # Get a list of all unique makes for the dropdown all_makes_sold = list(cars_sold_filtered.values_list('id_car_make__name', flat=True).distinct().order_by('id_car_make__name')) + all_makes_sold = list(cars_sold_filtered.values_list('id_car_make__name', flat=True).distinct().order_by('id_car_make__name')) if selected_make_sales: # If a make is selected, filter the queryset @@ -564,6 +638,11 @@ def general_dashboard(request,dealer_slug): ).values('id_car_model__name').annotate( count=Count('id_car_model__name') ).order_by('-count') + sales_data_by_model = cars_sold_filtered.filter( + id_car_make__name=selected_make_sales + ).values('id_car_model__name').annotate( + count=Count('id_car_model__name') + ).order_by('-count') else: # If no make is selected, pass an empty list or some default data sales_data_by_model = [] @@ -571,20 +650,30 @@ def general_dashboard(request,dealer_slug): + + + # 1. Inventory by Make (Pie Chart) + inventory_by_make_data = active_cars.values('id_car_make__name').annotate( + car_count=Count('id_car_make__name') + ).order_by('-car_count') inventory_by_make_data = active_cars.values('id_car_make__name').annotate( car_count=Count('id_car_make__name') ).order_by('-car_count') + inventory_by_make_labels = [data['id_car_make__name'] for data in inventory_by_make_data] + inventory_by_make_counts = [data['car_count'] for data in inventory_by_make_data] inventory_by_make_labels = [data['id_car_make__name'] for data in inventory_by_make_data] inventory_by_make_counts = [data['car_count'] for data in inventory_by_make_data] # 2. Inventory by Model (Bar Chart) selected_make_inventory = request.GET.get('make_inventory', None) + selected_make_inventory = request.GET.get('make_inventory', None) # Get all unique makes in inventory for the dropdown all_makes_inventory = list(active_cars.values_list('id_car_make__name', flat=True).distinct().order_by('id_car_make__name')) + all_makes_inventory = list(active_cars.values_list('id_car_make__name', flat=True).distinct().order_by('id_car_make__name')) if selected_make_inventory: inventory_data_by_model = active_cars.filter( @@ -592,6 +681,11 @@ def general_dashboard(request,dealer_slug): ).values('id_car_model__name').annotate( count=Count('id_car_model__name') ).order_by('-count') + inventory_data_by_model = active_cars.filter( + id_car_make__name=selected_make_inventory + ).values('id_car_model__name').annotate( + count=Count('id_car_model__name') + ).order_by('-count') else: # Default data inventory_data_by_model = [] @@ -601,6 +695,10 @@ def general_dashboard(request,dealer_slug): 'end_date': end_date, 'today': today_local, + 'start_date': start_date, + 'end_date': end_date, + 'today': today_local, + # Inventory KPIs 'total_cars_in_inventory': total_cars_in_inventory, 'total_inventory_value': total_inventory_value, @@ -610,6 +708,14 @@ def general_dashboard(request,dealer_slug): 'used_car_value': used_car_value, 'aging_inventory_count': aging_inventory_count, + 'total_cars_in_inventory': total_cars_in_inventory, + 'total_inventory_value': total_inventory_value, + 'total_new_cars_in_inventory': total_new_cars_in_inventory, + 'total_used_cars_in_inventory': total_used_cars_in_inventory, + 'new_car_value': new_car_value, + 'used_car_value': used_car_value, + 'aging_inventory_count': aging_inventory_count, + # Sales KPIs 'total_cars_sold': total_cars_sold, 'total_cost_of_cars_sold': total_cost_of_cars_sold, @@ -618,6 +724,13 @@ def general_dashboard(request,dealer_slug): 'total_vat_collected_from_cars': total_vat_collected_from_cars, 'total_discount_on_cars': total_discount, + 'total_cars_sold': total_cars_sold, + 'total_cost_of_cars_sold': total_cost_of_cars_sold, + 'total_revenue_from_cars': total_revenue_from_cars, + 'net_profit_from_cars': net_profit_from_cars, + 'total_vat_collected_from_cars': total_vat_collected_from_cars, + 'total_discount_on_cars': total_discount, + # Sales by Type 'total_new_cars_sold': total_new_cars_sold, 'total_used_cars_sold': total_used_cars_sold, @@ -630,6 +743,17 @@ def general_dashboard(request,dealer_slug): 'net_profit_from_used_cars': net_profit_from_used_cars, 'total_vat_collected_from_used_cars': total_vat_collected_from_used_cars, + 'total_new_cars_sold': total_new_cars_sold, + 'total_used_cars_sold': total_used_cars_sold, + 'total_cost_of_new_cars_sold': total_cost_of_new_cars_sold, + 'total_revenue_from_new_cars': total_revenue_from_new_cars, + 'net_profit_from_new_cars': net_profit_from_new_cars, + 'total_vat_collected_from_new_cars': total_vat_collected_from_new_cars, + 'total_cost_of_used_cars_sold': total_cost_of_used_cars_sold, + 'total_revenue_from_used_cars': total_revenue_from_used_cars, + 'net_profit_from_used_cars': net_profit_from_used_cars, + 'total_vat_collected_from_used_cars': total_vat_collected_from_used_cars, + # Services and Overall KPIs 'total_revenue_from_services': total_revenue_from_services, 'total_vat_collected_from_services': total_vat_collected_from_services, @@ -638,8 +762,28 @@ def general_dashboard(request,dealer_slug): 'total_expenses': total_expenses, 'gross_profit': gross_profit, + 'total_revenue_from_services': total_revenue_from_services, + 'total_vat_collected_from_services': total_vat_collected_from_services, + 'total_revenue_generated': total_revenue_generated, + 'total_vat_collected': total_vat_collected, + 'total_expenses': total_expenses, + 'gross_profit': gross_profit, + # Chart Data + 'monthly_cars_sold_json': monthly_cars_sold_json, + 'monthly_revenue_json': monthly_revenue_json, + 'monthly_net_profit_json': monthly_net_profit_json, + + + # Sales Chart Data + 'sales_by_make_labels_json': json.dumps(sales_by_make_labels), + 'sales_by_make_counts_json': json.dumps(sales_by_make_counts), + 'all_makes_sold': all_makes_sold, + 'selected_make_sales': selected_make_sales, + 'sales_data_by_model_json': json.dumps(list(sales_data_by_model)), + + 'monthly_cars_sold_json': monthly_cars_sold_json, 'monthly_revenue_json': monthly_revenue_json, 'monthly_net_profit_json': monthly_net_profit_json, @@ -660,13 +804,24 @@ def general_dashboard(request,dealer_slug): 'inventory_data_by_model_json': json.dumps(list(inventory_data_by_model)), + 'inventory_by_make_labels_json': json.dumps(inventory_by_make_labels), + 'inventory_by_make_counts_json': json.dumps(inventory_by_make_counts), + 'all_makes_inventory': all_makes_inventory, + 'selected_make_inventory': selected_make_inventory, + 'inventory_data_by_model_json': json.dumps(list(inventory_data_by_model)), + + } + + return render(request, 'dashboards/general_dashboard.html', context) return render(request, 'dashboards/general_dashboard.html', context) @login_required +def sales_dashboard(request,dealer_slug): + dealer = get_object_or_404(models.Dealer,slug=dealer_slug) def sales_dashboard(request,dealer_slug): dealer = get_object_or_404(models.Dealer,slug=dealer_slug) today_local = timezone.localdate() @@ -676,10 +831,14 @@ def sales_dashboard(request,dealer_slug): # ---------------------------------------------------- start_date_str = request.GET.get('start_date') end_date_str = request.GET.get('end_date') + start_date_str = request.GET.get('start_date') + end_date_str = request.GET.get('end_date') if start_date_str and end_date_str: start_date = timezone.datetime.strptime(start_date_str, '%Y-%m-%d').date() end_date = timezone.datetime.strptime(end_date_str, '%Y-%m-%d').date() + start_date = timezone.datetime.strptime(start_date_str, '%Y-%m-%d').date() + end_date = timezone.datetime.strptime(end_date_str, '%Y-%m-%d').date() else: start_date = today_local - timedelta(days=30) end_date = today_local @@ -689,9 +848,13 @@ def sales_dashboard(request,dealer_slug): dealer=dealer, created__date__gte=start_date, created__date__lte=end_date + dealer=dealer, + created__date__gte=start_date, + created__date__lte=end_date ) + # ---------------------------------------------------- # 2. Lead Sources Chart Logic # ---------------------------------------------------- @@ -700,10 +863,15 @@ def sales_dashboard(request,dealer_slug): lead_sources_data = leads_filtered.values('source').annotate( count=Count('source') ).order_by('-count') + lead_sources_data = leads_filtered.values('source').annotate( + count=Count('source') + ).order_by('-count') # Separate the labels and counts for the chart lead_sources_labels = [item['source'] for item in lead_sources_data] lead_sources_counts = [item['count'] for item in lead_sources_data] + lead_sources_labels = [item['source'] for item in lead_sources_data] + lead_sources_counts = [item['count'] for item in lead_sources_data] # ---------------------------------------------------- # 2. Lead Funnel Chart Logic @@ -712,6 +880,9 @@ def sales_dashboard(request,dealer_slug): dealer=dealer, created__date__gte=start_date, created__date__lte=end_date + dealer=dealer, + created__date__gte=start_date, + created__date__lte=end_date ) opportunity_stage_data = opportunity_filtered.values('stage').annotate( count=Count('stage') @@ -720,19 +891,31 @@ def sales_dashboard(request,dealer_slug): opportunity_stage_labels = [item['stage'] for item in opportunity_stage_data ] opportunity_stage_counts = [item['count'] for item in opportunity_stage_data ] + opportunity_stage_data = opportunity_filtered.values('stage').annotate( + count=Count('stage') + ).order_by('-count') + # Separate the labels and counts for the chart + opportunity_stage_labels = [item['stage'] for item in opportunity_stage_data ] + opportunity_stage_counts = [item['count'] for item in opportunity_stage_data ] + # 2. Inventory KPIs # ---------------------------------------------------- active_cars = models.Car.objects.filter(dealer=dealer).exclude(status='sold') + active_cars = models.Car.objects.filter(dealer=dealer).exclude(status='sold') total_cars_in_inventory = active_cars.count() new_cars_qs = active_cars.filter(stock_type='new') + new_cars_qs = active_cars.filter(stock_type='new') total_new_cars_in_inventory = new_cars_qs.count() used_cars_qs = active_cars.filter(stock_type='used') + used_cars_qs = active_cars.filter(stock_type='used') total_used_cars_in_inventory = used_cars_qs.count() aging_threshold_days = 60 aging_inventory_count = active_cars.filter(receiving_date__date__lte=today_local - timedelta(days=aging_threshold_days)).count() + aging_inventory_count = active_cars.filter(receiving_date__date__lte=today_local - timedelta(days=aging_threshold_days)).count() + context = { 'start_date': start_date, @@ -747,11 +930,26 @@ def sales_dashboard(request,dealer_slug): 'total_new_cars_in_inventory': total_new_cars_in_inventory, 'total_used_cars_in_inventory': total_used_cars_in_inventory, 'aging_inventory_count': aging_inventory_count, + 'start_date': start_date, + 'end_date': end_date, + 'lead_sources_labels_json': json.dumps(lead_sources_labels), + 'lead_sources_counts_json': json.dumps(lead_sources_counts), + 'opportunity_stage_labels_json': json.dumps(opportunity_stage_labels), + 'opportunity_stage_counts_json': json.dumps(opportunity_stage_counts), + + # Inventory KPIs + 'total_cars_in_inventory': total_cars_in_inventory, + 'total_new_cars_in_inventory': total_new_cars_in_inventory, + 'total_used_cars_in_inventory': total_used_cars_in_inventory, + 'aging_inventory_count': aging_inventory_count, } return render(request, 'dashboards/sales_dashboard.html', context) + return render(request, 'dashboards/sales_dashboard.html', context) + + def aging_inventory_list_view(request, dealer_slug): @@ -768,34 +966,54 @@ def aging_inventory_list_view(request, dealer_slug): selected_series = request.GET.get('series') # Changed 'serie' to 'series' for consistency selected_year = request.GET.get('year') selected_stock_type = request.GET.get('stock_type') + selected_make = request.GET.get('make') + selected_model = request.GET.get('model') + selected_series = request.GET.get('series') # Changed 'serie' to 'series' for consistency + selected_year = request.GET.get('year') + selected_stock_type = request.GET.get('stock_type') # Start with the base queryset for all aging cars. aging_cars_queryset = models.Car.objects.filter( dealer=dealer, receiving_date__date__lt=today_local - timedelta(days=aging_threshold_days) ).exclude(status='sold') + total_aging_inventory_value=aging_cars_queryset.aggregate(total=Sum('cost_price'))['total'] + receiving_date__date__lt=today_local - timedelta(days=aging_threshold_days) + ).exclude(status='sold') total_aging_inventory_value=aging_cars_queryset.aggregate(total=Sum('cost_price'))['total'] # Apply filters to the queryset if they exist. Chaining is fine here. if selected_make: aging_cars_queryset = aging_cars_queryset.filter(id_car_make__name=selected_make) + aging_cars_queryset = aging_cars_queryset.filter(id_car_make__name=selected_make) if selected_model: aging_cars_queryset = aging_cars_queryset.filter(id_car_model__name=selected_model) + aging_cars_queryset = aging_cars_queryset.filter(id_car_model__name=selected_model) if selected_series: aging_cars_queryset = aging_cars_queryset.filter(id_car_series__name=selected_series) + aging_cars_queryset = aging_cars_queryset.filter(id_car_series__name=selected_series) if selected_year: aging_cars_queryset = aging_cars_queryset.filter(id_car_year__year=selected_year) + aging_cars_queryset = aging_cars_queryset.filter(id_car_year__year=selected_year) if selected_stock_type: aging_cars_queryset = aging_cars_queryset.filter(stock_type=selected_stock_type) + # Get distinct values for filter dropdowns based on the initial, unfiltered aging cars queryset. # This ensures all possible filter options are always available. aging_base_queryset = models.Car.objects.filter( dealer=dealer, receiving_date__date__lt=today_local - timedelta(days=aging_threshold_days) ).exclude(status='sold') + receiving_date__date__lt=today_local - timedelta(days=aging_threshold_days) + ).exclude(status='sold') + all_makes = aging_base_queryset.values_list('id_car_make__name', flat=True).distinct().order_by('id_car_make__name') + all_models = aging_base_queryset.values_list('id_car_model__name', flat=True).distinct().order_by('id_car_model__name') + all_series = aging_base_queryset.values_list('id_car_serie__name', flat=True).distinct().order_by('id_car_serie__name') + all_stock_types = aging_base_queryset.values_list('stock_type', flat=True).distinct().order_by('stock_type') + all_years = aging_base_queryset.values_list('year', flat=True).distinct().order_by('-year') all_makes = aging_base_queryset.values_list('id_car_make__name', flat=True).distinct().order_by('id_car_make__name') all_models = aging_base_queryset.values_list('id_car_model__name', flat=True).distinct().order_by('id_car_model__name') all_series = aging_base_queryset.values_list('id_car_serie__name', flat=True).distinct().order_by('id_car_serie__name') @@ -806,6 +1024,7 @@ def aging_inventory_list_view(request, dealer_slug): # Set up pagination paginator = Paginator(aging_cars_queryset, 10) page_number = request.GET.get('page') + page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) # Iterate only on the cars for the current page to add the age attribute. @@ -827,9 +1046,22 @@ def aging_inventory_list_view(request, dealer_slug): 'all_years': all_years, 'total_aging_inventory_value':total_aging_inventory_value + 'selected_make': selected_make, + 'selected_model': selected_model, + 'selected_series': selected_series, # Corrected variable name + 'selected_year': selected_year, + 'selected_stock_type': selected_stock_type, + 'all_makes': all_makes, + 'all_models': all_models, + 'all_series': all_series, + 'all_stock_types': all_stock_types, + 'all_years': all_years, + 'total_aging_inventory_value':total_aging_inventory_value + } return render(request, 'dashboards/aging_inventory_list.html', context) + return render(request, 'dashboards/aging_inventory_list.html', context) def terms_and_privacy(request): return render(request, "terms_and_privacy.html") @@ -854,6 +1086,7 @@ def WelcomeView(request): return render(request, "welcome.html", context) +class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView): class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView): """ Manages the creation of a new car entry in the inventory system. @@ -877,6 +1110,7 @@ class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMi template_name = "inventory/car_form.html" permission_required = ["inventory.add_car"] success_message=_("Car Added successfully to the inventory") + success_message=_("Car Added successfully to the inventory") def get_form(self, form_class=None): form = super().get_form(form_class) @@ -1274,6 +1508,7 @@ class CarInventory(LoginRequiredMixin, PermissionRequiredMixin, ListView): return context +class CarColorCreate(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView): class CarColorCreate(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView): """ View for creating a new car color. @@ -1299,6 +1534,7 @@ class CarColorCreate(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageM template_name = "inventory/add_colors.html" permission_required = ["inventory.add_carcolors"] success_message=_("Car colors details added successfully") + success_message=_("Car colors details added successfully") def form_valid(self, form): car = get_object_or_404(models.Car, slug=self.kwargs["slug"]) @@ -1731,6 +1967,7 @@ class CarDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): permission_required = ["inventory.view_car"] +def CarFinanceUpdateView(request,dealer_slug,slug): def CarFinanceUpdateView(request,dealer_slug,slug): car = get_object_or_404(models.Car, slug=slug) dealer = get_object_or_404(models.Dealer, slug=dealer_slug) @@ -1746,6 +1983,7 @@ def CarFinanceUpdateView(request,dealer_slug,slug): form = forms.CarFinanceForm(instance=car) return render(request, "inventory/car_finance_form.html", {"car": car, "dealer": dealer, "form": form}) + return render(request, "inventory/car_finance_form.html", {"car": car, "dealer": dealer, "form": form}) class CarUpdateView( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView @@ -1777,6 +2015,7 @@ class CarUpdateView( def get_success_url(self): return reverse("car_detail", kwargs={"dealer_slug": self.request.dealer.slug,"slug": self.object.slug}) + return reverse("car_detail", kwargs={"dealer_slug": self.request.dealer.slug,"slug": self.object.slug}) def get_form(self, form_class=None): form = super().get_form(form_class) @@ -2374,6 +2613,7 @@ class DealerUpdateView( def get_success_url(self): return reverse("dealer_detail", kwargs={"slug": self.object.slug}) +class StaffDetailView(LoginRequiredMixin, DetailView): class StaffDetailView(LoginRequiredMixin, DetailView): """ Represents a detailed view for a Dealer model. @@ -2397,6 +2637,7 @@ class StaffDetailView(LoginRequiredMixin, DetailView): + def dealer_vat_rate_update(request, slug): dealer = get_object_or_404(models.Dealer, slug=slug) models.VatRate.objects.filter(dealer=dealer).update(rate=request.POST.get("rate")) @@ -2525,11 +2766,15 @@ class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView context["notes"] = models.Notes.objects.filter( dealer=dealer, content_type__model="customer", object_id=self.object.id + dealer=dealer, + content_type__model="customer", object_id=self.object.id ) estimates = entity.get_estimates().filter(customer=self.object.customer_model) invoices = entity.get_invoices().filter(customer=self.object.customer_model) context['leads']=self.object.customer_leads.all() + context['leads']=self.object.customer_leads.all() + total = estimates.count() + invoices.count() @@ -2646,6 +2891,8 @@ class CustomerCreateView( if customer := models.Customer.objects.filter( dealer=dealer, email=form.instance.email + dealer=dealer, + email=form.instance.email ).first(): if not customer.active: messages.error( @@ -2830,10 +3077,18 @@ def vendorDetailView(request, dealer_slug, slug): vendor_makes=cars.values('id_car_make__name').annotate(make_count=Count('id_car_make__name')) vendor_bills=BillModel.objects.filter(vendor=vendor.vendor_model) paginator=Paginator(vendor_bills,20) + vendor = get_object_or_404(models.Vendor, slug=slug,dealer=dealer) + cars=vendor.cars.all() + total_cars_from_vendor=cars.count() + vendor_makes=cars.values('id_car_make__name').annotate(make_count=Count('id_car_make__name')) + vendor_bills=BillModel.objects.filter(vendor=vendor.vendor_model) + paginator=Paginator(vendor_bills,20) page_number = request.GET.get("page") page_obj=paginator.get_page(page_number) + page_obj=paginator.get_page(page_number) return render( request, template_name="vendors/view_vendor.html", context={"vendor": vendor,"vendor_bills":page_obj,"total_cars_from_vendor":total_cars_from_vendor,"vendor_makes":vendor_makes} + request, template_name="vendors/view_vendor.html", context={"vendor": vendor,"vendor_bills":page_obj,"total_cars_from_vendor":total_cars_from_vendor,"vendor_makes":vendor_makes} ) @@ -2870,6 +3125,7 @@ class VendorCreateView( def form_valid(self, form): dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) + if vendor := models.Vendor.objects.filter(dealer=dealer,email=form.instance.email).first(): if vendor := models.Vendor.objects.filter(dealer=dealer,email=form.instance.email).first(): if not vendor.active: messages.error( @@ -3659,6 +3915,7 @@ class UserCreateView( # staff_member, _ = StaffMember.objects.get_or_create(user=user) # for service in form.cleaned_data["service_offered"]: # staff_member.services_offered.add(service) + # staff_member.services_offered.add(service) staff.user = user staff.dealer = dealer staff.save() @@ -3668,6 +3925,7 @@ class UserCreateView( return super().form_valid(form) def get_success_url(self): + return reverse_lazy("staff_password_reset", args=[self.request.dealer.slug, self.staff_pk]) return reverse_lazy("staff_password_reset", args=[self.request.dealer.slug, self.staff_pk]) # return reverse_lazy("user_list", args=[self.request.dealer.slug]) @@ -3854,6 +4112,8 @@ class OrganizationCreateView(LoginRequiredMixin, PermissionRequiredMixin, Create if organization := models.Organization.objects.filter( dealer=dealer, email=form.instance.email + dealer=dealer, + email=form.instance.email ).first(): if not organization.active: messages.error( @@ -4431,13 +4691,16 @@ class AccountCreateView( def get_success_url(self): return reverse( "account_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"], "coa_pk": self.kwargs["coa_pk"]} + "account_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"], "coa_pk": self.kwargs["coa_pk"]} ) + def get_context_data(self,**kwargs): def get_context_data(self,**kwargs): context = super().get_context_data(**kwargs) context["url_kwargs"] = self.kwargs coa_pk = context["url_kwargs"]["coa_pk"] try: kwargs["coa_model"] = ChartOfAccountModel.objects.get(pk=coa_pk) or self.request.entity.get_default_coa() + kwargs["coa_model"] = ChartOfAccountModel.objects.get(pk=coa_pk) or self.request.entity.get_default_coa() except Exception: kwargs["coa_model"] = self.request.entity.get_default_coa() return context @@ -4543,19 +4806,23 @@ class AccountUpdateView( def get_success_url(self): return reverse_lazy( "account_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"],"coa_pk":self.kwargs["coa_pk"]} + "account_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"],"coa_pk":self.kwargs["coa_pk"]} ) + def get_context_data(self,**kwargs): def get_context_data(self,**kwargs): context = super().get_context_data(**kwargs) context["url_kwargs"] = self.kwargs coa_pk = context["url_kwargs"]["coa_pk"] try: kwargs["coa_model"] = ChartOfAccountModel.objects.get(pk=coa_pk) or self.request.entity.get_default_coa() + kwargs["coa_model"] = ChartOfAccountModel.objects.get(pk=coa_pk) or self.request.entity.get_default_coa() except Exception: kwargs["coa_model"] = self.request.entity.get_default_coa() return context @login_required @permission_required("django_ledger.delete_accountmodel") +def account_delete(request, dealer_slug,coa_pk, pk): def account_delete(request, dealer_slug,coa_pk, pk): """ Handles the deletion of an account object identified by its primary key (pk). Ensures @@ -4602,16 +4869,21 @@ def sales_list_view(request, dealer_slug): try: if any([request.is_dealer, request.is_manager, request.is_accountant]): qs = models.ExtraInfo.get_sale_orders(staff=staff, is_dealer=True,dealer=dealer) + qs = models.ExtraInfo.get_sale_orders(staff=staff, is_dealer=True,dealer=dealer) elif request.is_staff: qs = models.ExtraInfo.get_sale_orders(staff=staff,dealer=dealer) + qs = models.ExtraInfo.get_sale_orders(staff=staff,dealer=dealer) except Exception as e: print(e) + search_query = request.GET.get('q', None) search_query = request.GET.get('q', None) if search_query: qs = qs.filter( Q(order_number__icontains=search_query)| Q(customer__customer_name__icontains=search_query) + Q(order_number__icontains=search_query)| + Q(customer__customer_name__icontains=search_query) ).distinct() paginator = Paginator(qs, 30) @@ -4704,6 +4976,11 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): content_type=ContentType.objects.get_for_model(EstimateModel), related_content_type=ContentType.objects.get_for_model(User), )) + ).union(models.ExtraInfo.objects.filter( + dealer=dealer, + content_type=ContentType.objects.get_for_model(EstimateModel), + related_content_type=ContentType.objects.get_for_model(User), + )) elif self.request.is_staff and self.request.is_sales: qs = models.ExtraInfo.objects.filter( @@ -4714,10 +4991,13 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): ) qs = EstimateModel.objects.filter(pk__in=[x.content_object.pk for x in qs]) search_query = self.request.GET.get('q', None) + search_query = self.request.GET.get('q', None) if search_query: qs = qs.filter( Q(estimate_number__icontains=search_query)| Q(customer__customer_name__icontains=search_query) + Q(estimate_number__icontains=search_query)| + Q(customer__customer_name__icontains=search_query) ).distinct() context["staff_estimates"] = qs return context @@ -4731,11 +5011,14 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): if status: queryset = queryset.filter(status=status) search_query = self.request.GET.get('q', None) + search_query = self.request.GET.get('q', None) if search_query: queryset = queryset.filter( Q(estimate_number__icontains=search_query)| Q(customer__customer_name__icontains=search_query) + Q(estimate_number__icontains=search_query)| + Q(customer__customer_name__icontains=search_query) ).distinct() return queryset @@ -4773,6 +5056,7 @@ def create_estimate(request, dealer_slug, slug=None): title = data.get("title") customer_id = data.get("customer") customer = models.Customer.objects.filter(pk=int(customer_id),dealer=dealer).first() + customer = models.Customer.objects.filter(pk=int(customer_id),dealer=dealer).first() items = data.get("item", []) quantities = data.get("quantity", []) @@ -4864,6 +5148,7 @@ def create_estimate(request, dealer_slug, slug=None): "unit_cost": round(float(i.marked_price)), "unit_revenue": round(float(i.marked_price)), "total_amount": round(float(i.final_price_plus_vat)),# TODO : check later + "total_amount": round(float(i.final_price_plus_vat)),# TODO : check later } ) @@ -5042,6 +5327,7 @@ class EstimateDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView # calculator = CarFinanceCalculator(estimate) # finance_data = calculator.get_finance_data() finance_data = get_finance_data(estimate,dealer) + finance_data = get_finance_data(estimate,dealer) invoice_obj = InvoiceModel.objects.all().filter(ce_model=estimate).first() kwargs["data"] = finance_data @@ -5052,6 +5338,7 @@ class EstimateDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView selected_items = car.additional_services.filter(dealer=dealer) form = forms.AdditionalFinancesForm() form.fields["additional_finances"].queryset = form.fields["additional_finances"].queryset.filter(dealer=dealer) # + form.fields["additional_finances"].queryset = form.fields["additional_finances"].queryset.filter(dealer=dealer) # form.initial["additional_finances"] = selected_items kwargs["additionals_form"] = form except Exception as e: @@ -5069,6 +5356,7 @@ class EstimatePrintView(EstimateDetailView): + @login_required @permission_required("inventory.add_saleorder", raise_exception=True) def create_sale_order(request, dealer_slug, pk): @@ -5122,6 +5410,8 @@ def create_sale_order(request, dealer_slug, pk): pass item.item_model.car.sold_date=timezone.now() # to be checked added by faheed item.item_model.car.save()# to be checked added byfaheed + item.item_model.car.sold_date=timezone.now() # to be checked added by faheed + item.item_model.car.save()# to be checked added byfaheed item.item_model.car.mark_as_sold() messages.success(request, "Sale Order created successfully") @@ -5143,6 +5433,7 @@ def create_sale_order(request, dealer_slug, pk): # calculator = CarFinanceCalculator(estimate) finance_data = get_finance_data(estimate,dealer) + finance_data = get_finance_data(estimate,dealer) return render( request, "sales/estimates/sale_order_form.html", @@ -5165,10 +5456,15 @@ def update_estimate_discount(request, dealer_slug, pk): discount_amount = request.POST.get("discount_amount", 0) finance_data = get_finance_data(estimate,dealer) car = finance_data.get('car') + finance_data = get_finance_data(estimate,dealer) + car = finance_data.get('car') if Decimal(discount_amount) >= car.marked_price: + messages.error(request, _("Discount amount cannot be greater than marked price")) messages.error(request, _("Discount amount cannot be greater than marked price")) return redirect("estimate_detail", dealer_slug=dealer_slug, pk=pk) + if Decimal(discount_amount) > car.marked_price * Decimal('0.5'): + messages.warning(request, _("Discount amount is greater than 50% of the marked price, proceed with caution.")) if Decimal(discount_amount) > car.marked_price * Decimal('0.5'): messages.warning(request, _("Discount amount is greater than 50% of the marked price, proceed with caution.")) else: @@ -5190,6 +5486,9 @@ def update_estimate_additionals(request, dealer_slug, pk): car.additional_services.set( form.cleaned_data["additional_finances"] ) + car.additional_services.set( + form.cleaned_data["additional_finances"] + ) car.save() messages.success(request, "Additional Finances updated successfully") return redirect("estimate_detail", dealer_slug=dealer_slug, pk=pk) @@ -5215,6 +5514,7 @@ class SaleOrderDetail(LoginRequiredMixin, PermissionRequiredMixin, DetailView): # calculator = CarFinanceCalculator(estimate) # finance_data = calculator.get_finance_data() finance_data = get_finance_data(estimate,dealer) + finance_data = get_finance_data(estimate,dealer) kwargs["data"] = finance_data return super().get_context_data(**kwargs) @@ -5313,9 +5613,11 @@ class EstimatePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailVie estimate = kwargs.get("object") if estimate.get_itemtxs_data(): + # data = get_financial_values(estimate) # calculator = CarFinanceCalculator(estimate) kwargs["data"] = get_finance_data(estimate,self.request.dealer) + kwargs["data"] = get_finance_data(estimate,self.request.dealer) return super().get_context_data(**kwargs) @@ -5455,8 +5757,10 @@ class InvoiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): ] ): qs = models.ExtraInfo.get_invoices(staff=staff, is_dealer=True,dealer=dealer) + qs = models.ExtraInfo.get_invoices(staff=staff, is_dealer=True,dealer=dealer) elif self.request.is_staff: qs = models.ExtraInfo.get_invoices(staff=staff,dealer=dealer) + qs = models.ExtraInfo.get_invoices(staff=staff,dealer=dealer) except Exception as e: print(e) @@ -5498,6 +5802,7 @@ class InvoiceDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView) # calculator = CarFinanceCalculator(invoice) # finance_data = calculator.get_finance_data() finance_data = get_finance_data(invoice,self.request.dealer) + finance_data = get_finance_data(invoice,self.request.dealer) kwargs["data"] = finance_data kwargs["payments"] = JournalEntryModel.objects.filter( ledger=invoice.ledger @@ -5594,6 +5899,7 @@ class ApprovedInvoiceModelUpdateFormView( return reverse_lazy( "invoice_detail", kwargs={"dealer_slug": self.kwargs["dealer_slug"],"entity_slug": self.kwargs["entity_slug"], "pk": self.object.pk}, + kwargs={"dealer_slug": self.kwargs["dealer_slug"],"entity_slug": self.kwargs["entity_slug"], "pk": self.object.pk}, ) @@ -5642,6 +5948,7 @@ class PaidInvoiceModelUpdateFormView( return reverse_lazy( "invoice_detail", kwargs={"dealer_slug": self.kwargs["dealer_slug"],"entity_slug": self.kwargs["entity_slug"], "pk": self.object.pk}, + kwargs={"dealer_slug": self.kwargs["dealer_slug"],"entity_slug": self.kwargs["entity_slug"], "pk": self.object.pk}, ) def form_valid(self, form): @@ -5650,6 +5957,7 @@ class PaidInvoiceModelUpdateFormView( if invoice.get_amount_open() > 0: messages.error(self.request, "Invoice is not fully paid") return redirect("invoice_detail",dealer_slug=self.kwargs["dealer_slug"],entity_slug=self.kwargs["entity_slug"], pk=invoice.pk) + return redirect("invoice_detail",dealer_slug=self.kwargs["dealer_slug"],entity_slug=self.kwargs["entity_slug"], pk=invoice.pk) else: invoice.post_ledger() invoice.save() @@ -5682,11 +5990,13 @@ def invoice_mark_as(request, dealer_slug, pk): if not invoice.can_approve(): messages.error(request, "invoice is not ready for approval") return redirect("invoice_detail", dealer_slug=dealer_slug,entity_slug=request.entity.slug, pk=invoice.pk) + return redirect("invoice_detail", dealer_slug=dealer_slug,entity_slug=request.entity.slug, pk=invoice.pk) invoice.mark_as_approved( entity_slug=dealer.entity.slug, user_model=dealer.entity.admin ) invoice.save() return redirect("invoice_detail", dealer_slug=dealer_slug,entity_slug=request.entity.slug, pk=invoice.pk) + return redirect("invoice_detail", dealer_slug=dealer_slug,entity_slug=request.entity.slug, pk=invoice.pk) @login_required @@ -5726,6 +6036,7 @@ def invoice_create(request, dealer_slug, pk): # calculator = CarFinanceCalculator(estimate) # finance_data = calculator.get_finance_data() + finance_data = get_finance_data(estimate,dealer) finance_data = get_finance_data(estimate,dealer) car = finance_data.get("car") invoice_itemtxs = { @@ -5751,6 +6062,7 @@ def invoice_create(request, dealer_slug, pk): invoice.save() messages.success(request, "Invoice created successfully") return redirect("invoice_detail", dealer_slug=dealer.slug,entity_slug=entity.slug, pk=invoice.pk) + return redirect("invoice_detail", dealer_slug=dealer.slug,entity_slug=entity.slug, pk=invoice.pk) else: print(form.errors) form = forms.InvoiceModelCreateForm( @@ -5803,6 +6115,7 @@ class InvoicePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailView if invoice.get_itemtxs_data(): # calculator = CarFinanceCalculator(invoice) finance_data = get_finance_data(invoice,dealer) + finance_data = get_finance_data(invoice,dealer) kwargs["data"] = finance_data kwargs["dealer"] = dealer return super().get_context_data(**kwargs) @@ -5811,6 +6124,7 @@ class InvoicePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailView # payments class InvoiceModelUpdateView(InvoiceModelUpdateViewBase): + template_name = 'sales/invoices/invoice_update.html' template_name = 'sales/invoices/invoice_update.html' permission_required = ["django_ledger.change_invoicemodel"] @@ -5884,6 +6198,7 @@ def PaymentCreateView(request, dealer_slug, pk): # bill = form.cleaned_data.get("bill") payment_method = form.cleaned_data.get("payment_method") response = redirect("invoice_detail", dealer_slug=dealer.slug,entity_slug=entity.slug, pk=model.pk)# if invoice else "bill_detail" + response = redirect("invoice_detail", dealer_slug=dealer.slug,entity_slug=entity.slug, pk=model.pk)# if invoice else "bill_detail" # model = invoice if invoice else bill if not model.is_approved(): @@ -6061,6 +6376,7 @@ def payment_mark_as_paid(request, dealer_slug, pk): ) messages.error(request, f"Error: {str(e)}") return redirect("invoice_detail", dealer_slug=dealer_slug,entity_slug=request.entity.slug, pk=invoice.pk) + return redirect("invoice_detail", dealer_slug=dealer_slug,entity_slug=request.entity.slug, pk=invoice.pk) # activity log @@ -6267,6 +6583,8 @@ def lead_create(request, dealer_slug): customer = models.Customer.objects.filter( dealer=dealer, email=instance.email + dealer=dealer, + email=instance.email ).first() if not customer: customer = models.Customer( @@ -6287,6 +6605,8 @@ def lead_create(request, dealer_slug): organization = models.Organization.objects.filter( dealer=dealer, email=instance.email + dealer=dealer, + email=instance.email ).first() if not organization: organization = models.Organization( @@ -6308,6 +6628,7 @@ def lead_create(request, dealer_slug): ) messages.success(request, _("Lead created successfully")) return redirect("lead_detail",dealer_slug=dealer_slug,slug=instance.slug) + return redirect("lead_detail",dealer_slug=dealer_slug,slug=instance.slug) else: logger.error( f"error creating leading for dealer {dealer_slug} by user:{user_username}" @@ -6335,7 +6656,6 @@ def lead_create(request, dealer_slug): qs = form.fields["id_car_make"].queryset.filter( is_sa_import=True, pk__in=dealer_make_list ) - # print(qs) form.fields["staff"].queryset = ( form.fields["staff"] .queryset.select_related("user") @@ -6354,7 +6674,6 @@ def lead_create(request, dealer_slug): form.fields["staff"].queryset = models.Staff.objects.filter( dealer=dealer, pk=request.staff.pk ) - qs = qs.order_by("name") form.fields["id_car_make"].queryset = qs form.fields["id_car_make"].choices = [ (obj.id_car_make, obj.get_local_name()) for obj in qs @@ -6381,6 +6700,7 @@ def lead_tracking(request, dealer_slug): else: qs = models.Lead.objects.filter(dealer=dealer) leads=qs + leads=qs won = qs.filter(status="won") new = qs.filter(status="new") lose = qs.filter(status="lose") @@ -6394,6 +6714,7 @@ def lead_tracking(request, dealer_slug): "lose": lose, "negotiation": negotiation, "leads":leads + "leads":leads } return render(request, "crm/leads/lead_tracking.html", context) @@ -6428,6 +6749,10 @@ def update_lead_actions(request, dealer_slug): request, _("All fields are required") ) + messages.error( + request, + _("All fields are required") + ) return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug) # return JsonResponse( # {"success": False, "message": "All fields are required"}, status=400 @@ -6436,6 +6761,7 @@ def update_lead_actions(request, dealer_slug): # Get the lead + # Update lead fields lead.status = current_action @@ -6468,6 +6794,10 @@ def update_lead_actions(request, dealer_slug): request, _("Invalid date format") ) + messages.error( + request, + _("Invalid date format") + ) return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug) # return JsonResponse( # {"success": False, "message": "Invalid date format"}, status=400 @@ -6483,6 +6813,10 @@ def update_lead_actions(request, dealer_slug): request, _("Actions updated successfully") ) + messages.success( + request, + _("Actions updated successfully") + ) return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug) # return JsonResponse( # {"success": True, "message": "Actions updated successfully"} @@ -6498,6 +6832,10 @@ def update_lead_actions(request, dealer_slug): request, _("Lead not found") ) + messages.error( + request, + _("Lead not found") + ) return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug) # return JsonResponse({"success": False, "message": "Lead not found"}, status=404) except Exception as e: @@ -6511,10 +6849,42 @@ def update_lead_actions(request, dealer_slug): request, _("An error occurred while updating lead actions") ) + messages.error( + request, + _("An error occurred while updating lead actions") + ) return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug) # return JsonResponse({"success": False, "message": str(e)}, status=500) + +def lead_update(request,dealer_slug,slug): + dealer = get_object_or_404(models.Dealer, slug=dealer_slug) + lead = get_object_or_404(models.Lead, slug=slug) + form = forms.LeadForm(instance=lead) + if "HX-Request" in request.headers: + make_id = request.GET.get("id_car_make") + make = models.CarMake.objects.get(pk=make_id) + form.fields[ + "id_car_model" + ].queryset = make.carmodel_set.all() + else: + form.fields[ + "id_car_model" + ].queryset = form.instance.id_car_make.carmodel_set.all() + form.fields["staff"].queryset = ( + form.fields["staff"] + .queryset.select_related("user") + .filter( + dealer=dealer, + user__groups__permissions__codename__contains="add_lead", + ) + .distinct() + ) + context = { + "form":form + } + return render(request,"crm/leads/lead_form.html",context) class LeadUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): """ Handles the update view for Lead objects. @@ -6804,6 +7174,7 @@ def schedule_event(request, dealer_slug, content_type, slug): form = forms.ScheduleForm(request.POST) if form.is_valid(): + reminder = form.cleaned_data['reminder'] reminder = form.cleaned_data['reminder'] instance = form.save(commit=False) instance.dealer = dealer @@ -6853,6 +7224,7 @@ def schedule_event(request, dealer_slug, content_type, slug): ) if reminder: scheduled_at_aware = timezone.make_aware(instance.scheduled_at, timezone.get_current_timezone()) if timezone.is_naive(instance.scheduled_at) else instance.scheduled_at + scheduled_at_aware = timezone.make_aware(instance.scheduled_at, timezone.get_current_timezone()) if timezone.is_naive(instance.scheduled_at) else instance.scheduled_at reminder_time = scheduled_at_aware - timezone.timedelta(minutes=15) # Only schedule if the reminder time is in the future @@ -6861,14 +7233,17 @@ def schedule_event(request, dealer_slug, content_type, slug): DjangoQSchedule.objects.create( name=f"send_schedule_reminder_email_to_{instance.scheduled_by.email}_for_{content_type}_with_PK_{instance.pk}", func='inventory.tasks.send_schedule_reminder_email', + func='inventory.tasks.send_schedule_reminder_email', args=f'"{instance.pk}"', schedule_type=DjangoQSchedule.ONCE, next_run=reminder_time, hook='inventory.tasks.log_email_status', + hook='inventory.tasks.log_email_status', ) messages.success(request, _("Appointment Created Successfully")) return redirect(f'{content_type}_detail',dealer_slug=dealer_slug, slug=slug) + return redirect(f'{content_type}_detail',dealer_slug=dealer_slug, slug=slug) else: # Log for invalid form data @@ -6909,6 +7284,7 @@ def lead_transfer(request, dealer_slug, slug): else: messages.error(request, f"Invalid form data: {str(form.errors)}") return redirect("lead_detail", dealer_slug=dealer.slug ,slug=lead.slug) + return redirect("lead_detail", dealer_slug=dealer.slug ,slug=lead.slug) @login_required @@ -6995,6 +7371,7 @@ def send_lead_email(request, dealer_slug, slug, email_pk=None): # ) # return response # return redirect("lead_list", dealer_slug=dealer.slug) + # return redirect("lead_list", dealer_slug=dealer.slug) if request.method == "POST": email_pk = request.POST.get("email_pk") @@ -7050,6 +7427,7 @@ def send_lead_email(request, dealer_slug, slug, email_pk=None): # ) # return response # return redirect("lead_list", dealer_slug=dealer_slug) + # return redirect("lead_list", dealer_slug=dealer_slug) msg = f""" السلام عليكم {lead.full_name}, @@ -7151,6 +7529,9 @@ class OpportunityCreateView( form.fields["lead"].queryset = models.Lead.objects.filter( dealer=dealer ) + form.fields["lead"].queryset = models.Lead.objects.filter( + dealer=dealer + ) elif self.request.is_staff: form.fields["lead"].queryset = models.Lead.objects.filter( dealer=dealer, staff=self.request.staff @@ -7199,12 +7580,14 @@ class OpportunityUpdateView( permission_required = ["inventory.change_opportunity"] + def get_form(self, form_class=None): form = super().get_form(form_class) dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug")) staff = getattr(self.request.user, "staff", None) form.fields["car"].queryset = models.Car.objects.filter( dealer=dealer, status="available",marked_price__gt=0 + dealer=dealer, status="available",marked_price__gt=0 ) form.fields["lead"].queryset = models.Lead.objects.filter( dealer=dealer, staff=staff @@ -7251,6 +7634,7 @@ class OpportunityStageUpdateView( permission_required = ["inventory.change_opportunity"] + def get_success_url(self): return reverse_lazy( "opportunity_detail", @@ -7376,6 +7760,9 @@ class OpportunityListView(LoginRequiredMixin, PermissionRequiredMixin, ListView) queryset = models.Opportunity.objects.filter(dealer=dealer, lead__staff=staff) + queryset = models.Opportunity.objects.filter(dealer=dealer, lead__staff=staff) + + # Stage filter stage = self.request.GET.get("stage") @@ -7393,6 +7780,7 @@ class OpportunityListView(LoginRequiredMixin, PermissionRequiredMixin, ListView) queryset = queryset.order_by("expected_close_date") # Search filter + # Search filter search = self.request.GET.get("q") if search: queryset = queryset.filter( @@ -7651,11 +8039,18 @@ class ItemServiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView) Q(id__icontains=query)| Q(uom__icontains=query) ) + qs = qs.filter(Q(name__icontains=query)| + Q(id__icontains=query)| + Q(uom__icontains=query) + ) return qs +class ItemExpenseCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView): + + class ItemExpenseCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView): """ Represents a view for creating item expense entries. @@ -7750,6 +8145,9 @@ class ItemExpenseUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateV + + + class ItemExpenseListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): """ Handles the display of a list of item expenses. @@ -7818,6 +8216,8 @@ class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): if query: qs = qs.filter(Q(bill_number__icontains=query)| Q(vendor__vendor_name__icontains=query)) + qs = qs.filter(Q(bill_number__icontains=query)| + Q(vendor__vendor_name__icontains=query)) return qs def get_context_data(self, **kwargs): @@ -7826,6 +8226,7 @@ class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): return context +class BillModelCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView): class BillModelCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView): template_name = "bill/bill_create.html" PAGE_TITLE = _("Create Bill") @@ -8445,7 +8846,7 @@ class FiscalYearIncomeStatementViewBase( """ template_name = "ledger/reports/income_statement.html" - permission_required = ["django_ledger.view_ledgermodel"] + permission_required = ["inventory.view_carfinance"] def get_login_url(self): return reverse("account_login") @@ -8584,7 +8985,7 @@ class FiscalYearCashFlowStatementViewBase( """ template_name = "ledger/reports/cash_flow_statement.html" - permission_required = ["django_ledger.view_ledgermodel"] + permission_required = ["inventory.view_carfinance"] def get_login_url(self): return reverse("account_login") @@ -8773,7 +9174,7 @@ class FiscalYearEntityModelDashboardView( :type permission_required: list """ - permission_required = ["django_ledger.view_ledgermodel"] + permission_required = ["inventory.view_carfinance"] def get_login_url(self): return reverse("account_login") @@ -9165,6 +9566,7 @@ def schedule_cancel(request, dealer_slug, pk): @permission_required("inventory.change_dealer", raise_exception=True) def assign_car_makes(request, dealer_slug): + """ Assigns car makes to a dealer. @@ -9292,6 +9694,7 @@ class LedgerModelDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV permission_required = "django_ledger.view_ledgermodel" +class LedgerModelCreateView(LedgerModelCreateViewBase,SuccessMessageMixin): class LedgerModelCreateView(LedgerModelCreateViewBase,SuccessMessageMixin): """ Handles the creation of LedgerModel entities. @@ -9357,8 +9760,10 @@ class LedgerModelModelActionView(LedgerModelModelActionViewBase): + @login_required @permission_required("django_ledger.delete_ledgermodel", raise_exception=True) +def LedgerModelDeleteView(request, dealer_slug,entity_slug,ledger_pk): def LedgerModelDeleteView(request, dealer_slug,entity_slug,ledger_pk): ledger = LedgerModel.objects.filter(pk=ledger_pk).first() if request.method == "POST": @@ -9366,6 +9771,7 @@ def LedgerModelDeleteView(request, dealer_slug,entity_slug,ledger_pk): messages.success(request, _("Ledger deleted successfully")) return redirect("ledger_list", dealer_slug=dealer_slug, entity_slug=entity_slug) return render(request,"ledger/ledger/ledger_delete.html",{"ledger_model":ledger}) + return render(request,"ledger/ledger/ledger_delete.html",{"ledger_model":ledger}) # class LedgerModelDeleteView(DeleteView, SuccessMessageMixin): # """ # Handles the deletion of a Ledger model instance. @@ -9490,6 +9896,7 @@ class JournalEntryCreateView( @login_required @permission_required("django_ledger.delete_journalentrymodel", raise_exception=True) +def JournalEntryDeleteView(request,dealer_slug, pk): def JournalEntryDeleteView(request,dealer_slug, pk): """ Handles the deletion of a specific journal entry. This view facilitates @@ -9512,9 +9919,11 @@ def JournalEntryDeleteView(request,dealer_slug, pk): if not journal_entry.can_delete(): messages.error(request, _("Journal Entry cannot be deleted")) return redirect("journalentry_list",dealer_slug=dealer_slug, pk=ledger.pk) + return redirect("journalentry_list",dealer_slug=dealer_slug, pk=ledger.pk) journal_entry.delete() messages.success(request, "Journal Entry deleted") return redirect("journalentry_list",dealer_slug=dealer_slug, pk=ledger.pk) + return redirect("journalentry_list",dealer_slug=dealer_slug, pk=ledger.pk) return render( request, "ledger/journal_entry/journal_entry_delete.html", @@ -9703,15 +10112,22 @@ def ledger_unpost_all_journals(request, dealer_slug, entity_slug, pk): @login_required @permission_required("inventory.change_dealer", raise_exception=True) def pricing_page(request, dealer_slug): + dealer=get_object_or_404(models.Dealer, slug=dealer_slug) dealer=get_object_or_404(models.Dealer, slug=dealer_slug) if not dealer.active_plan: plan_list = PlanPricing.objects.all() form = forms.PaymentPlanForm() return render(request, "pricing_page.html", {"plan_list": plan_list, "form": form}) + plan_list = PlanPricing.objects.all() + form = forms.PaymentPlanForm() + return render(request, "pricing_page.html", {"plan_list": plan_list, "form": form}) else: messages.info(request,_("You already have an plan!!")) return redirect('home',dealer_slug=dealer_slug) + messages.info(request,_("You already have an plan!!")) + return redirect('home',dealer_slug=dealer_slug) + @login_required @@ -9752,11 +10168,13 @@ def payment_callback(request, dealer_slug): history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first() payment_status = request.GET.get("status") logger.info(f"Received payment callback for dealer_slug: {dealer_slug}, payment_id: {payment_id}, status: {payment_status}") + logger.info(f"Received payment callback for dealer_slug: {dealer_slug}, payment_id: {payment_id}, status: {payment_status}") order = Order.objects.filter(user=dealer.user, status=1).first() # Status 1 = NEW print(order) if payment_status == "paid": logger.info(f"Payment successful for transaction ID {payment_id}. Processing order completion.") + logger.info(f"Payment successful for transaction ID {payment_id}. Processing order completion.") billing_info, created = BillingInfo.objects.get_or_create( user=dealer.user, @@ -9768,24 +10186,35 @@ def payment_callback(request, dealer_slug): 'city': dealer.entity.city or " ", 'country': dealer.entity.country or " ", } + 'tax_number': dealer.vrn, + 'name': dealer.arabic_name, + 'street': dealer.address, + 'zipcode': dealer.entity.zip_code or " ", + 'city': dealer.entity.city or " ", + 'country': dealer.entity.country or " ", + } ) if created: logger.info(f"Created new billing info for user {dealer.user}.") else: logger.debug(f"Billing info already exists for user {dealer.user}.") + if not hasattr(order.user, 'userplan'): if not hasattr(order.user, 'userplan'): UserPlan.objects.create( user=order.user, plan=order.plan, expire=datetime.now().date() + timedelta(days=order.get_plan_pricing().pricing.period) + expire=datetime.now().date() + timedelta(days=order.get_plan_pricing().pricing.period) ) logger.info(f"Created new UserPlan for user {order.user} with plan {order.plan}.") + logger.info(f"Created new UserPlan for user {order.user} with plan {order.plan}.") else: logger.info(f"UserPlan already exists for user {order.user}.") try: + # if order.user.userplan: # user = order.user # pricing = order.get_plan_pricing().pricing @@ -9801,21 +10230,28 @@ def payment_callback(request, dealer_slug): history.status = "paid" history.save() logger.info(f"Order {order.id} for user {order.user} completed successfully. Payment history updated.") + logger.info(f"Order {order.id} for user {order.user} completed successfully. Payment history updated.") invoice = order.get_invoices().first() return render( request, "payment_success.html", {"order": order, "invoice": invoice} + request, + "payment_success.html", + {"order": order, "invoice": invoice} ) except Exception as e: + logger.exception(f"Error completing order {order.id} for user {order.user}: {e}") logger.exception(f"Error completing order {order.id} for user {order.user}: {e}") logger.error(f"Plan activation failed: {str(e)}") history.status = "failed" history.save() return render(request, "payment_failed.html", {"message": "Plan activation error"}) + return render(request, "payment_failed.html", {"message": "Plan activation error"}) elif payment_status == "failed": + logger.warning(f"Payment failed for transaction ID {payment_id}. Message: {message}") logger.warning(f"Payment failed for transaction ID {payment_id}. Message: {message}") history.status = "failed" history.save() @@ -10043,6 +10479,7 @@ def add_task(request, dealer_slug, content_type, slug): + @login_required @permission_required("inventory.change_tasks", raise_exception=True) def update_task(request, dealer_slug, pk): @@ -10160,6 +10597,10 @@ def user_management(request, dealer_slug): "organizations": models.Organization.objects.filter(active=False,dealer=dealer), "vendors": models.Vendor.objects.filter(active=False,dealer=dealer), "staff": models.Staff.objects.filter(active=False,dealer=dealer), + "customers": models.Customer.objects.filter(active=False,dealer=dealer), + "organizations": models.Organization.objects.filter(active=False,dealer=dealer), + "vendors": models.Vendor.objects.filter(active=False,dealer=dealer), + "staff": models.Staff.objects.filter(active=False,dealer=dealer), } return render(request, "admin_management/user_management.html", context) @@ -10600,6 +11041,7 @@ class PurchaseOrderDetailView(LoginRequiredMixin, PermissionRequiredMixin, Detai context["page_title"] = title context["header_title"] = title context["po_ready_to_fulfill"] = all([item for item in po_model.get_itemtxs_data()[0] if item.po_item_status == 'received']) + context["po_ready_to_fulfill"] = all([item for item in po_model.get_itemtxs_data()[0] if item.po_item_status == 'received']) po_model: PurchaseOrderModel = self.object po_items_qs, item_data = po_model.get_itemtxs_data( queryset=po_model.itemtransactionmodel_set.all().select_related( @@ -10627,6 +11069,7 @@ class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListVie query = self.request.GET.get("q") qs = self.model.objects.filter(entity=dealer.entity) if query: + qs=qs.filter(Q(po_number__icontains=query)|Q(po_status__icontains=query)|Q(po_title__icontains=query)) qs=qs.filter(Q(po_number__icontains=query)|Q(po_status__icontains=query)|Q(po_title__icontains=query)) return qs return qs @@ -10635,6 +11078,7 @@ class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListVie context = super().get_context_data(**kwargs) dealer = get_user_type(self.request) vendors=models.Vendor.objects.filter(dealer=dealer) + vendors=models.Vendor.objects.filter(dealer=dealer) context = super().get_context_data(**kwargs) context["entity_slug"] = dealer.entity.slug context["vendors"] = vendors @@ -10773,6 +11217,7 @@ def upload_cars(request, dealer_slug, pk=None): response = redirect("upload_cars", dealer_slug=dealer_slug, pk=pk) if po_item.status == "uploaded": + messages.add_message(request, messages.SUCCESS, "Item uploaded Sucessfully.") messages.add_message(request, messages.SUCCESS, "Item uploaded Sucessfully.") return redirect( "view_items_inventory", @@ -10909,6 +11354,12 @@ def upload_cars(request, dealer_slug, pk=None): # marked_price=0, # selling_price=0, # ) + # models.CarFinance.objects.create( + # car=car, + # cost_price=po_item.item.unit_cost, + # marked_price=0, + # selling_price=0, + # ) car.add_colors(exterior=exterior, interior=interior) cars_created += 1 logger.debug( @@ -10994,20 +11445,32 @@ class InventoryListView(InventoryListViewBase): permission_required = ["django_ledger.view_purchaseordermodel"] @login_required +def purchase_report_view(request,dealer_slug): def purchase_report_view(request,dealer_slug): pos = request.entity.get_purchase_orders() data = [] total_po_amount=0 total_po_cars=0 + total_po_amount=0 + total_po_cars=0 for po in pos: items = [{"total":x.total_amount,"q":x.quantity} for x in po.get_itemtxs_data()[0].all()] + items = [{"total":x.total_amount,"q":x.quantity} for x in po.get_itemtxs_data()[0].all()] + po_amount=0 + po_quantity=0 po_amount=0 po_quantity=0 for item in items: po_amount+=item["total"] po_quantity+=item["q"] + po_amount+=item["total"] + po_quantity+=item["q"] + total_po_amount+=po_amount + total_po_cars+=po_quantity + bills=po.get_po_bill_queryset() + vendors=set([bill.vendor.vendor_name for bill in bills]) total_po_amount+=po_amount total_po_cars+=po_quantity bills=po.get_po_bill_queryset() @@ -11015,6 +11478,8 @@ def purchase_report_view(request,dealer_slug): vendors_str = ", ".join(sorted(list(vendors))) if vendors else "N/A" data.append({"po_number":po.po_number,"po_created":po.created,"po_status":po.po_status,"po_fulfilled_date":po.date_fulfilled,"po_amount":po_amount, "po_quantity":po_quantity,"vendors_str":vendors_str}) + data.append({"po_number":po.po_number,"po_created":po.created,"po_status":po.po_status,"po_fulfilled_date":po.date_fulfilled,"po_amount":po_amount, + "po_quantity":po_quantity,"vendors_str":vendors_str}) current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S") context={ @@ -11025,13 +11490,27 @@ def purchase_report_view(request,dealer_slug): "total_po_cars":total_po_cars, "current_time":current_time + context={ + "dealer":request.entity.name, + "time":current_time, + "data":data, + "total_po_amount":total_po_amount, + "total_po_cars":total_po_cars, + "current_time":current_time + } + + + return render(request,'ledger/reports/purchase_report.html',context) return render(request,'ledger/reports/purchase_report.html',context) +def purchase_report_csv_export(request,dealer_slug): + response = HttpResponse(content_type='text/csv') + def purchase_report_csv_export(request,dealer_slug): response = HttpResponse(content_type='text/csv') @@ -11039,10 +11518,12 @@ def purchase_report_csv_export(request,dealer_slug): current_time = timezone.now().strftime("%Y-%m-%d_%H%M%S") filename = f"purchase_report_{dealer_slug}_{current_time}.csv" response['Content-Disposition'] = f'attachment; filename="{filename}"' + response['Content-Disposition'] = f'attachment; filename="{filename}"' writer = csv.writer(response) + header = [ 'PO Number', 'Created Date', @@ -11051,6 +11532,13 @@ def purchase_report_csv_export(request,dealer_slug): 'PO Amount', 'PO Quantity', 'Vendors' + 'PO Number', + 'Created Date', + 'Status', + 'Fulfilled Date', + 'PO Amount', + 'PO Quantity', + 'Vendors' ] writer.writerow(header) pos = request.entity.get_purchase_orders() @@ -11059,6 +11547,7 @@ def purchase_report_csv_export(request,dealer_slug): po_amount = 0 po_quantity = 0 items = [{"total":x.total_amount,"q":x.quantity} for x in po.get_itemtxs_data()[0].all()] + items = [{"total":x.total_amount,"q":x.quantity} for x in po.get_itemtxs_data()[0].all()] for item in items: po_amount += item["total"] @@ -11066,9 +11555,20 @@ def purchase_report_csv_export(request,dealer_slug): bills = po.get_po_bill_queryset() vendors = set([bill.vendor.vendor_name for bill in bills ]) + vendors = set([bill.vendor.vendor_name for bill in bills ]) vendors_str = ", ".join(sorted(list(vendors))) if vendors else "N/A" + writer.writerow([ + po.po_number, + po.created.strftime("%Y-%m-%d %H:%M:%S") if po.created else '', + po.get_po_status_display(), + po.date_fulfilled.strftime("%Y-%m-%d") if po.date_fulfilled else '', + f"{po_amount:.2f}", + po_quantity, + vendors_str + ]) + writer.writerow([ po.po_number, po.created.strftime("%Y-%m-%d %H:%M:%S") if po.created else '', @@ -11082,11 +11582,16 @@ def purchase_report_csv_export(request,dealer_slug): + @login_required def car_sale_report_view(request, dealer_slug): dealer = get_object_or_404(models.Dealer, slug=dealer_slug) vat = models.VatRate.objects.filter(dealer=dealer,is_active=True).first() VAT_RATE=vat.rate + vat = models.VatRate.objects.filter(dealer=dealer,is_active=True).first() + VAT_RATE=vat.rate + + cars_sold = models.Car.objects.filter(dealer=dealer, status='sold') cars_sold = models.Car.objects.filter(dealer=dealer, status='sold') @@ -11097,6 +11602,11 @@ def car_sale_report_view(request, dealer_slug): selected_serie = request.GET.get('serie') selected_year = request.GET.get('year') selected_stock_type=request.GET.get('stock_type') + selected_make = request.GET.get('make') + selected_model = request.GET.get('model') + selected_serie = request.GET.get('serie') + selected_year = request.GET.get('year') + selected_stock_type=request.GET.get('stock_type') # Apply filters to the queryset if selected_make: @@ -11111,16 +11621,28 @@ def car_sale_report_view(request, dealer_slug): cars_sold = cars_sold.filter(stock_type=selected_stock_type) + # # Calculate summary data for the filtered results total_cars_sold=cars_sold.count() total_revenue_from_cars = cars_sold.aggregate( total=Sum(F('marked_price') - F('discount_amount')) )['total'] or 0 + total_cars_sold=cars_sold.count() + total_revenue_from_cars = cars_sold.aggregate( + total=Sum(F('marked_price') - F('discount_amount')) + )['total'] or 0 + total_vat_on_cars=cars_sold.annotate( + final_price=F('marked_price') - F('discount_amount')).aggregate( + total=Sum(F('final_price') * VAT_RATE))['total'] or 0 total_vat_on_cars=cars_sold.annotate( final_price=F('marked_price') - F('discount_amount')).aggregate( total=Sum(F('final_price') * VAT_RATE))['total'] or 0 + total_revenue_from_additonals=sum([car.get_additional_services()['total'] for car in cars_sold]) + total_vat_from_additonals=sum([car.get_additional_services()['services_vat'] for car in cars_sold]) + total_vat_collected = total_vat_on_cars+total_vat_from_additonals + total_revenue_collected=total_revenue_from_cars+total_revenue_from_additonals total_revenue_from_additonals=sum([car.get_additional_services()['total'] for car in cars_sold]) total_vat_from_additonals=sum([car.get_additional_services()['services_vat'] for car in cars_sold]) total_vat_collected = total_vat_on_cars+total_vat_from_additonals @@ -11133,7 +11655,13 @@ def car_sale_report_view(request, dealer_slug): base_sold_cars_queryset = models.Car.objects.filter(dealer=dealer, status='sold') makes =base_sold_cars_queryset.values_list('id_car_make__name', flat=True).distinct() models_qs =base_sold_cars_queryset.values_list('id_car_model__name', flat=True).distinct() + base_sold_cars_queryset = models.Car.objects.filter(dealer=dealer, status='sold') + makes =base_sold_cars_queryset.values_list('id_car_make__name', flat=True).distinct() + models_qs =base_sold_cars_queryset.values_list('id_car_model__name', flat=True).distinct() + series =base_sold_cars_queryset.values_list('id_car_serie__name', flat=True).distinct() + stock_types=base_sold_cars_queryset.values_list('stock_type', flat=True).distinct() + years = base_sold_cars_queryset.values_list('year', flat=True).distinct().order_by('-year') series =base_sold_cars_queryset.values_list('id_car_serie__name', flat=True).distinct() stock_types=base_sold_cars_queryset.values_list('stock_type', flat=True).distinct() years = base_sold_cars_queryset.values_list('year', flat=True).distinct().order_by('-year') @@ -11160,17 +11688,41 @@ def car_sale_report_view(request, dealer_slug): 'selected_serie': selected_serie, 'selected_year': selected_year, 'selected_stock_type':selected_stock_type, + 'cars_sold': cars_sold, + 'total_cars_sold':total_cars_sold, + 'current_time': current_time, + 'dealer': dealer, + 'total_revenue_from_cars': total_revenue_from_cars, + 'total_revenue_from_additonals':total_revenue_from_additonals, + 'total_revenue_collected': total_revenue_collected, + 'total_vat_on_cars':total_vat_on_cars, + 'total_vat_from_additonals':total_vat_from_additonals, + 'total_vat_collected':total_vat_collected, + 'total_discount': total_discount, + 'makes': makes, + 'models': models_qs, + 'series': series, + 'years': years, + 'stock_types':stock_types, + 'selected_make': selected_make, + 'selected_model': selected_model, + 'selected_serie': selected_serie, + 'selected_year': selected_year, + 'selected_stock_type':selected_stock_type, } return render(request, 'ledger/reports/car_sale_report.html', context) + return render(request, 'ledger/reports/car_sale_report.html', context) @login_required def car_sale_report_csv_export(request, dealer_slug): + response = HttpResponse(content_type='text/csv') response = HttpResponse(content_type='text/csv') current_time = timezone.now().strftime("%Y-%m-%d_%H-%M-%S") filename = f"sales_report_{dealer_slug}_{current_time}.csv" response['Content-Disposition'] = f'attachment; filename="{filename}"' + response['Content-Disposition'] = f'attachment; filename="{filename}"' writer = csv.writer(response) @@ -11181,11 +11733,17 @@ def car_sale_report_csv_export(request, dealer_slug): 'Marked Price', 'Discount Amount', 'Selling Price', 'VAT on Car', 'Services Price', 'VAT on Services', 'Final Total', 'Invoice Number' + 'VIN', 'Make', 'Model', 'Year', 'Serie', 'Trim', 'Mileage', + 'Stock Type', 'Created Date', 'Sold Date', 'Cost Price', + 'Marked Price', 'Discount Amount', 'Selling Price', + 'VAT on Car', 'Services Price', 'VAT on Services', 'Final Total', + 'Invoice Number' ] writer.writerow(header) dealer = get_object_or_404(models.Dealer, slug=dealer_slug) cars_sold = models.Car.objects.filter(dealer=dealer, status='sold') + cars_sold = models.Car.objects.filter(dealer=dealer, status='sold') # Apply filters from the request, just like in your HTML view selected_make = request.GET.get('make') @@ -11193,6 +11751,11 @@ def car_sale_report_csv_export(request, dealer_slug): selected_serie = request.GET.get('serie') selected_year = request.GET.get('year') selected_stock_type = request.GET.get('stock_type') + selected_make = request.GET.get('make') + selected_model = request.GET.get('model') + selected_serie = request.GET.get('serie') + selected_year = request.GET.get('year') + selected_stock_type = request.GET.get('stock_type') if selected_make: cars_sold = cars_sold.filter(id_car_make__name=selected_make) @@ -11211,6 +11774,8 @@ def car_sale_report_csv_export(request, dealer_slug): additional_services = car.get_additional_services() services_total_price = additional_services['total'] services_vat_amount = additional_services['services_vat'] + services_total_price = additional_services['total'] + services_vat_amount = additional_services['services_vat'] # Checking for the invoice number to avoid errors on cars without one invoice_number = None @@ -11240,6 +11805,27 @@ def car_sale_report_csv_export(request, dealer_slug): car.final_price_plus_services_plus_vat, invoice_number, ]) + writer.writerow([ + car.vin, + car.id_car_make.name, + car.id_car_model.name, + car.year, + car.id_car_serie.name, + car.id_car_trim.name, + car.mileage if car.mileage else '0', + car.stock_type, + car.created_at.strftime("%Y-%m-%d %H:%M:%S") if car.created_at else '', + sold_date.strftime("%Y-%m-%d %H:%M:%S") if sold_date else '', + car.cost_price, + car.marked_price, + car.discount, # Ensure this property returns a number + car.final_price, # Selling Price without VAT + car.vat_amount, # VAT on the car + services_total_price, # Total services without VAT + services_vat_amount, # VAT on services + car.final_price_plus_services_plus_vat, + invoice_number, + ]) return response @@ -11250,45 +11836,66 @@ def staff_password_reset_view(request, dealer_slug, user_pk): dealer = get_object_or_404(models.Dealer, slug=dealer_slug) staff = models.Staff.objects.filter(dealer=dealer, pk=user_pk).first() + if request.method == 'POST': if request.method == 'POST': form = forms.CustomSetPasswordForm(staff.user, request.POST) if form.is_valid(): form.save() messages.success(request, _('Your password has been set. You may go ahead and log in now.')) return redirect('user_detail',dealer_slug=dealer_slug,slug=staff.slug) + messages.success(request, _('Your password has been set. You may go ahead and log in now.')) + return redirect('user_detail',dealer_slug=dealer_slug,slug=staff.slug) else: messages.error(request, _('Invalid password. Please try again.')) + 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' + 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) 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') + return queryset.select_related('make', 'model', 'serie', 'trim') class RecallDetailView(DetailView): model = models.Recall template_name = 'recalls/recall_detail.html' context_object_name = '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') + context['notifications'] = self.object.notifications.select_related('dealer') return context def RecallFilterView(request): + context = {'make_data': models.CarMake.objects.all()} context = {'make_data': models.CarMake.objects.all()} if request.method == "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') make = request.POST.get('make') model = request.POST.get('model') serie = request.POST.get('serie') @@ -11300,11 +11907,17 @@ def RecallFilterView(request): context['url'] = url context['cars'] = cars return render(request,'recalls/recall_filter.html',context) + 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' template_name = 'recalls/recall_create.html' form_class = forms.RecallCreateForm success_url = reverse_lazy('recall_success') + success_url = reverse_lazy('recall_success') def get_form(self, form_class=None): form = super().get_form(form_class) @@ -11313,30 +11926,46 @@ class RecallCreateView(FormView): serie = self.request.GET.get('serie') trim = self.request.GET.get('trim') year = self.request.GET.get('year') + 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() + 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() + 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() + 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() + form.fields['trim'].queryset = qs + form.initial['trim'] = qs.first() if year: form.fields['year_from'].initial = year form.fields['year_to'].initial = 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': if self.request.method == 'GET': initial.update(self.request.GET.dict()) return initial @@ -11375,12 +12004,15 @@ class RecallCreateView(FormView): notification = models.RecallNotification.objects.create( recall=recall, dealer=dealer + 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")) messages.success(self.request, _("Recall created and notifications sent successfully")) return super().form_valid(form) @@ -11391,18 +12023,26 @@ class RecallCreateView(FormView): 'recall': recall, 'cars': cars, }) + message = render_to_string('recalls/email/recall_notification.txt', { + 'dealer': dealer, + 'recall': recall, + 'cars': cars, + }) send_email( subject, message, 'noreply@yourdomain.com', + 'noreply@yourdomain.com', [dealer.user.email], ) class RecallSuccessView(TemplateView): template_name = 'recalls/recall_success.html' + template_name = 'recalls/recall_success.html' @login_required +def schedule_calendar(request,dealer_slug): def schedule_calendar(request,dealer_slug): dealer = get_object_or_404(models.Dealer, slug=dealer_slug) user_schedules = models.Schedule.objects.filter(dealer=dealer,scheduled_by=request.user).order_by('scheduled_at') @@ -11412,19 +12052,31 @@ def schedule_calendar(request,dealer_slug): 'upcoming_schedules':upcoming_schedules } return render(request, 'schedule_calendar.html', context) + user_schedules = models.Schedule.objects.filter(dealer=dealer,scheduled_by=request.user).order_by('scheduled_at') + upcoming_schedules = user_schedules.filter(scheduled_at__gte=timezone.now()).order_by('scheduled_at') + context = { + 'schedules': user_schedules, + 'upcoming_schedules':upcoming_schedules + } + return render(request, 'schedule_calendar.html', context) # Support @login_required def help_center(request): return render(request, 'support/help_center.html') + return render(request, 'support/help_center.html') @login_required @permission_required('inventory.add_ticket') +def create_ticket(request,dealer_slug): +@permission_required('inventory.add_ticket') def create_ticket(request,dealer_slug): if not request.is_dealer: return redirect('home') + return redirect('home') dealer = get_object_or_404(models.Dealer, slug=dealer_slug) + if request.method == 'POST': if request.method == 'POST': form = forms.TicketForm(request.POST) if form.is_valid(): @@ -11433,10 +12085,13 @@ def create_ticket(request,dealer_slug): instance.save() messages.success(request, 'Your support ticket has been submitted successfully!') return redirect('ticket_list',dealer_slug=dealer.slug) + messages.success(request, 'Your support ticket has been submitted successfully!') + return redirect('ticket_list',dealer_slug=dealer.slug) else: form = forms.TicketForm() return render(request, 'support/create_ticket.html', {'form': form}) + return render(request, 'support/create_ticket.html', {'form': form}) @login_required @permission_required('inventory.view_ticket') @@ -11451,19 +12106,27 @@ def ticket_list(request,dealer_slug): @login_required @permission_required('inventory.change_ticket') +def ticket_detail(request, dealer_slug,ticket_id): +@permission_required('inventory.change_ticket') def ticket_detail(request, dealer_slug,ticket_id): dealer = get_object_or_404(models.Dealer, slug=dealer_slug) ticket = models.Ticket.objects.get(dealer=dealer,id=ticket_id) return render(request, 'support/ticket_detail.html', {'ticket': ticket}) + ticket = models.Ticket.objects.get(dealer=dealer,id=ticket_id) + return render(request, 'support/ticket_detail.html', {'ticket': ticket}) @login_required @permission_required('inventory.change_ticket') +@permission_required('inventory.change_ticket') def ticket_mark_resolved(request, ticket_id): ticket = models.Ticket.objects.get(id=ticket_id) ticket.status = 'resolved' + ticket.status = 'resolved' ticket.save() messages.success(request, 'Ticket marked as resolved successfully!') subject = 'Ticket Resolved' + messages.success(request, 'Ticket marked as resolved successfully!') + subject = 'Ticket Resolved' message = f"Your support ticket has been resolved. Please check the details below:\n\nTicket ID: {ticket.id}\nSubject: {ticket.subject}\nDescription: {ticket.description}" send_email( settings.SUPPORT_EMAIL, @@ -11472,18 +12135,29 @@ def ticket_mark_resolved(request, ticket_id): message ) return render(request, 'support/ticket_detail.html', {'ticket': ticket}) + send_email( + settings.SUPPORT_EMAIL, + ticket.dealer.user.email, + subject, + message + ) + return render(request, 'support/ticket_detail.html', {'ticket': ticket}) @login_required @permission_required('inventory.change_ticket') +@permission_required('inventory.change_ticket') def ticket_update(request, ticket_id): ticket = models.Ticket.objects.get(id=ticket_id) + if request.method == 'POST': if request.method == 'POST': form = forms.TicketResolutionForm(request.POST, instance=ticket) if form.is_valid(): form.save() messages.success(request, f'Ticket has been marked as {ticket.get_status_display()}.') return redirect('ticket_detail',dealer_slug=ticket.dealer.slug, ticket_id=ticket.id) + messages.success(request, f'Ticket has been marked as {ticket.get_status_display()}.') + return redirect('ticket_detail',dealer_slug=ticket.dealer.slug, ticket_id=ticket.id) else: form = forms.TicketResolutionForm(instance=ticket) @@ -11491,19 +12165,32 @@ def ticket_update(request, ticket_id): 'ticket': ticket, 'form': form }) + return render(request, 'support/ticket_update.html', { + 'ticket': ticket, + 'form': form + }) class ChartOfAccountModelListView(ChartOfAccountModelListViewBase): template_name = 'chart_of_accounts/coa_list.html' permission_required = 'django_ledger.view_chartofaccountmodel' + template_name = 'chart_of_accounts/coa_list.html' + permission_required = 'django_ledger.view_chartofaccountmodel' class ChartOfAccountModelCreateView(ChartOfAccountModelCreateViewBase): template_name = 'chart_of_accounts/coa_create.html' permission_required = 'django_ledger.add_chartofaccountmodel' + template_name = 'chart_of_accounts/coa_create.html' + permission_required = 'django_ledger.add_chartofaccountmodel' class ChartOfAccountModelListView(ChartOfAccountModelListViewBase): template_name = 'chart_of_accounts/coa_list.html' permission_required = 'django_ledger.view_chartofaccountmodel' + template_name = 'chart_of_accounts/coa_list.html' + permission_required = 'django_ledger.view_chartofaccountmodel' class ChartOfAccountModelUpdateView(ChartOfAccountModelUpdateViewBase): template_name = 'chart_of_accounts/coa_update.html' permission_required = 'django_ledger.change_chartofaccountmodel' + template_name = 'chart_of_accounts/coa_update.html' + permission_required = 'django_ledger.change_chartofaccountmodel' class CharOfAccountModelActionView(CharOfAccountModelActionViewBase): permission_required = 'django_ledger.change_chartofaccountmodel' + permission_required = 'django_ledger.change_chartofaccountmodel' diff --git a/static/images/car_images/66f997b4e17b94a1ce42c3caa83f01d521db3b78b3cf730fe26c405baa33d599.png b/static/images/car_images/66f997b4e17b94a1ce42c3caa83f01d521db3b78b3cf730fe26c405baa33d599.png new file mode 100644 index 00000000..f49c35fd Binary files /dev/null and b/static/images/car_images/66f997b4e17b94a1ce42c3caa83f01d521db3b78b3cf730fe26c405baa33d599.png differ diff --git a/templates/users/user_form.html b/templates/users/user_form.html index 096a7204..69fc33b8 100644 --- a/templates/users/user_form.html +++ b/templates/users/user_form.html @@ -26,7 +26,16 @@
{% csrf_token %} {{ redirect_field }} - {{ form|crispy }} + + {{ form.first_name|as_crispy_field }} + {{ form.last_name|as_crispy_field }} + {{ form.arabic_name|as_crispy_field }} + {{ form.email|as_crispy_field }} + {{ form.phone_number|as_crispy_field }} + {{ form.address|as_crispy_field }} + {{ form.logo|as_crispy_field }} + {{ form.group|as_crispy_field }} + {% if form.errors %}