haikal/inventory/views.py
2025-09-09 18:52:22 +03:00

11743 lines
477 KiB
Python

# Standard
from django.core.files.base import ContentFile
import base64
import os
import re
import io
import csv
import cv2
import json
import logging
import tempfile
import numpy as np
from time import sleep
# from rich import print
from random import randint
from decimal import Decimal
from io import TextIOWrapper
from django.apps import apps
from datetime import datetime, timedelta,date
from calendar import month_name
from pyzbar.pyzbar import decode
from urllib.parse import urlparse, urlunparse
#####################################################################
from inventory.mixins import AuthorizedEntityMixin, DealerSlugMixin
from inventory.models import Status as LeadStatus
from django.db import IntegrityError
from django.views.generic import FormView
from django.views.decorators.http import require_http_methods
from django.db.models.deletion import RestrictedError
from django.http.response import StreamingHttpResponse
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.core.exceptions import PermissionDenied
from django.contrib.contenttypes.models import ContentType
from django.views.decorators.http import require_POST
from django.template.loader import render_to_string
# Django
from django.db.models import Q
from django.conf import settings
from django.db.models import Func
from django.contrib import messages
from django.http import (
Http404,
HttpResponseBadRequest,
HttpResponseNotFound,
HttpResponseRedirect,
JsonResponse,
HttpResponseForbidden,
)
from django.forms import CharField, HiddenInput, ValidationError
from django.shortcuts import HttpResponse
from django.db.models import Sum, F, Count
from django.db.models.functions import ExtractMonth,Round
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.contrib.auth.models import User, Group
from django.db.models import Value
from django.urls import reverse, reverse_lazy
from django.utils import timezone, translation
from django.db.models.functions import Coalesce
from django.contrib.auth.models import Permission
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import DetailView, RedirectView
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.auth.decorators import permission_required
from django.shortcuts import render, get_object_or_404, redirect
from plans.models import Plan, Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo
from django.views.generic import (
View,
ListView,
CreateView,
UpdateView,
DeleteView,
TemplateView,
ArchiveIndexView,
)
from django.db.models import Case, Value, IntegerField, When
# Django Ledger
from django_ledger.io import roles
from django_ledger.utils import accruable_net_summary
from django_ledger.views import (
JournalEntryModelTXSDetailView as JournalEntryModelTXSDetailViewBase,
LedgerModelModelActionView as LedgerModelModelActionViewBase,
LedgerModelDeleteView as LedgerModelDeleteViewBase,
LedgerModelCreateView as LedgerModelCreateViewBase,
)
from django_ledger.forms.account import AccountModelCreateForm, AccountModelUpdateForm
from django_ledger.views.entity import (
EntityModelDetailBaseView,
EntityModelDetailHandlerView,
)
from django_ledger.forms.ledger import LedgerModelCreateForm
from django_ledger.forms.item import (
ExpenseItemCreateForm,
ExpenseItemUpdateForm,
)
from django_ledger.forms.bank_account import (
BankAccountCreateForm,
BankAccountUpdateForm,
)
from django_ledger.views.chart_of_accounts import (
ChartOfAccountModelListView as ChartOfAccountModelListViewBase
)
from django_ledger.views.bill import (
BillModelCreateView,
BillModelDetailView,
BillModelUpdateView,
BaseBillActionView as BaseBillActionViewBase,
BillModelModelBaseView,
)
from django_ledger.forms.bill import (
ApprovedBillModelUpdateForm,
InReviewBillModelUpdateForm,
get_bill_itemtxs_formset_class,
BillModelCreateForm,
)
from django_ledger.forms.invoice import (
DraftInvoiceModelUpdateForm,
ApprovedInvoiceModelUpdateForm,
PaidInvoiceModelUpdateForm,
)
from django_ledger.forms.item import (
InventoryItemCreateForm,
)
from django_ledger.forms.purchase_order import (
PurchaseOrderModelCreateForm,
BasePurchaseOrderModelUpdateForm,
DraftPurchaseOrderModelUpdateForm,
ReviewPurchaseOrderModelUpdateForm,
ApprovedPurchaseOrderModelUpdateForm,
get_po_itemtxs_formset_class,
)
from django_ledger.views.purchase_order import (
PurchaseOrderModelDetailView as PurchaseOrderModelDetailViewBase,
# PurchaseOrderModelUpdateView as PurchaseOrderModelUpdateViewBase,
# BasePurchaseOrderActionActionView as BasePurchaseOrderActionActionViewBase,
PurchaseOrderModelDeleteView as PurchaseOrderModelDeleteViewBase,
)
from .override import (
PurchaseOrderModelUpdateView as PurchaseOrderModelUpdateViewBase,
BasePurchaseOrderActionActionView as BasePurchaseOrderActionActionViewBase,
BillModelDetailView as BillModelDetailViewBase,
BillModelUpdateView as BillModelUpdateViewBase,
BaseBillActionView as BaseBillActionViewBase,
InventoryListView as InventoryListViewBase,
InvoiceModelUpdateView as InvoiceModelUpdateViewBase,
ChartOfAccountModelCreateView as ChartOfAccountModelCreateViewBase,
ChartOfAccountModelListView as ChartOfAccountModelListViewBase,
ChartOfAccountModelUpdateView as ChartOfAccountModelUpdateViewBase,
CharOfAccountModelActionView as CharOfAccountModelActionViewBase,
)
from django_ledger.models import (
ItemTransactionModel,
EntityModel,
InvoiceModel,
BankAccountModel,
AccountModel,
JournalEntryModel,
TransactionModel,
EstimateModel,
CustomerModel,
ItemModel,
BillModel,
LedgerModel,
PurchaseOrderModel,
ChartOfAccountModel
)
from django_ledger.views.financial_statement import (
FiscalYearBalanceSheetView,
BaseIncomeStatementRedirectView,
FiscalYearIncomeStatementView,
BaseCashFlowStatementRedirectView,
FiscalYearCashFlowStatementView,
)
from django_ledger.io.io_core import get_localdate
from django_ledger.views.mixins import (
QuarterlyReportMixIn,
MonthlyReportMixIn,
DateReportMixIn,
DjangoLedgerSecurityMixIn,
EntityUnitMixIn,
)
# Other
from . import models, forms, tables
from django_tables2 import SingleTableView
from django_tables2.export.views import ExportMixin
# from appointment.models import Appointment, AppointmentRequest, Service, StaffMember
from .services import (
decodevin,
get_make,
get_model,
)
from .utils import (
CarFinanceCalculator,
get_car_finance_data,
get_finance_data,
get_item_transactions,
handle_payment,
reserve_car,
# send_email,
get_user_type,
set_bill_payment,
set_invoice_payment,
CarTransfer,
)
from .tasks import create_accounts_for_make, create_user_dealer, send_email
# djago easy audit log
from easyaudit.models import RequestEvent, CRUDEvent, LoginEvent
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
class Hash(Func):
"""
Represents a function used to compute a hash value.
This class serves as a placeholder to specify a particular hash
computation function. It extends the `Func` base class and is used
primarily to work with hash-related operations within the underlying
implementation. The specific hash algorithm can be modified or accessed
through the `function` attribute.
:ivar function: Specifies the hash computation function.
:type function: str
"""
function = "get_hash"
def switch_language(request):
"""
Switches the current language context for the user based on a request parameter, modifies the URL path
accordingly, and updates session and cookies with the new language preference.
:param request: The HTTP request object containing information about the user request, including
the desired language to switch to and the referring URL.
- "GET" dictionary is accessed to retrieve the desired language parameter.
- "META" dictionary is used to extract the referring URL via "HTTP_REFERER".
:return: A redirect response object pointing to the modified URL with the updated language
preference, if the requested language is valid. Otherwise, redirects to the default URL.
"""
language = request.GET.get("language", "en")
referer = request.META.get("HTTP_REFERER", "/")
parsed_url = urlparse(referer)
path_parts = parsed_url.path.split("/")
if path_parts[1] in dict(settings.LANGUAGES):
path_parts.pop(1)
new_path = "/".join(path_parts)
new_url = urlunparse(
(
parsed_url.scheme,
parsed_url.netloc,
new_path,
parsed_url.params,
parsed_url.query,
parsed_url.fragment,
)
)
if language in dict(settings.LANGUAGES):
logger.debug(f"Switching language to: {language}")
response = redirect(new_url)
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
translation.activate(language)
request.session[settings.LANGUAGE_COOKIE_NAME] = language
logger.debug(
f"Language switched to: {language}, Session: {request.session[settings.LANGUAGE_COOKIE_NAME]}"
)
return response
else:
logger.warning(f"Invalid language code: {language}")
return redirect("/")
def dealer_signup(request):
from django_q.tasks import async_task
"""
Handles the dealer signup wizard process, including forms validation, user and group
creation, permissions assignment, and dealer data storage. This view supports GET
requests for rendering the signup wizard page, and POST requests for processing
submitted data. The function also differentiates between requests sent with the
"Hx-Request" header for partial form validations in the wizard.
:param request: The HTTP request object representing the client request. It contains
metadata about the request and the POST data for creating the dealer.
:type request: django.http.HttpRequest
:param args: Optional positional arguments passed to the view during the call.
:type args: tuple
:param kwargs: Optional keyword arguments passed to the view during the call.
:type kwargs: dict
:return: A rendered signup wizard page or a JSON response indicating operation success
or failure.
:rtype: Union[django.http.HttpResponse, django.http.JsonResponse]
"""
if request.method == "POST":
try:
data = json.loads(request.body)
email = data.get("email")
password = data.get("password")
password_confirm = data.get("confirm_password")
name = data.get("name")
arabic_name = data.get("arabic_name")
phone = data.get("phone_number")
crn = data.get("crn")
vrn = data.get("vrn")
address = data.get("address")
except json.JSONDecodeError:
pass
if User.objects.filter(email=email).exists():
return JsonResponse({"error": _("Email already exists")}, status=400)
if not re.match(
r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$",
email,
):
return JsonResponse(
{"error": _("Please enter a valid email address")}, status=400
)
if len(password) < 8:
return JsonResponse(
{"error": _("Password must be at least 8 characters")}, status=400
)
if password != password_confirm:
return JsonResponse({"error": _("Passwords do not match")}, status=400)
try:
async_task(
create_user_dealer,
email,
password,
name,
arabic_name,
phone,
crn,
vrn,
address,
)
logger.info(f"Delear created succesfully with emailID {email}")
return JsonResponse({"message": _("User created successfully")}, status=200)
except Exception as e:
logger.error(f"Error creating dealer with email id:{email} error:{e}")
return JsonResponse({"error": str(e)}, status=400)
return render(
request,
"account/signup-wizar.html",
)
# class HomeView(LoginRequiredMixin, TemplateView):
# """
# HomeView class responsible for rendering the home page.
# This class ensures that only authenticated users can access the home page.
# Unauthenticated users are redirected to the welcome page. It is built on top of
# Django's TemplateView and includes additional functionality by inheriting from
# LoginRequiredMixin. The purpose of this class is to control the accessibility
# of the main index page of the application and manage context data for frontend
# rendering.
# :ivar template_name: The path to the template used for rendering the view's
# output.
# :type template_name: str
# """
# template_name = "index.html"
@login_required
def HomeView(request,dealer_slug=None):
dealer_slug=request.dealer.slug
if request.is_sales and not request.is_manager and not request.is_dealer:
return redirect('sales_dashboard', dealer_slug=dealer_slug)
else:
return redirect('general_dashboard',dealer_slug=dealer_slug)
class TestView(TemplateView):
"""
Represents a view for displaying a list of cars.
This class is a Django TemplateView-based view that renders a specific template
to display a list of cars. It uses a pre-defined template file and can be
extended or customized to provide additional functionality for rendering
content in a Django application.
:ivar template_name: Specifies the path to the HTML template file used for
rendering the cars list view.
:type template_name: str
"""
template_name = "inventory/cars_list_api.html"
@login_required
def general_dashboard(request,dealer_slug):
"""
Renders the dealer dashboard with key performance indicators and chart data.
"""
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()
# ----------------------------------------------------
# 1. Date Filtering
# ----------------------------------------------------
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()
else:
start_date = today_local - timedelta(days=30)
end_date = today_local
# ----------------------------------------------------
# 2. Inventory KPIs
# ----------------------------------------------------
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_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')
total_used_cars_in_inventory = used_cars_qs.count()
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()
# ----------------------------------------------------
# 3. Sales KPIs (filtered by date)
# ----------------------------------------------------
cars_sold_filtered = models.Car.objects.filter(
dealer=dealer,
status='sold',
sold_date__date__gte=start_date,
sold_date__date__lte=end_date
)
# General sales KPIs
total_cars_sold = cars_sold_filtered.count()
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
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
# Sales breakdown by type
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_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_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
net_profit_from_new_cars = total_revenue_from_new_cars - total_cost_of_new_cars_sold
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_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
# 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_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
gross_profit = net_profit_from_cars - total_expenses
# ----------------------------------------------------
# 4. Chart Data Aggregation
# ----------------------------------------------------
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
monthly_net_profit = [0] * 12
for data in monthly_sales_data:
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)
monthly_net_profit_json = json.dumps(monthly_net_profit)
# ----------------------------------------------------
# Sales by MAKE
# ----------------------------------------------------
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]
# ----------------------------------------------------
# DATA FOR CAR SALES BY MODELS (for the new interactive chart)
# ----------------------------------------------------
# Get the selected make from the URL query parameter
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'))
if selected_make_sales:
# If a make is selected, filter the queryset
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 = []
# 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_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)
# 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'))
if selected_make_inventory:
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 = []
context = {
'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,
'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,
'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,
'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,
'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)),
# New Inventory Chart Data
'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)
@login_required
def sales_dashboard(request,dealer_slug):
dealer = get_object_or_404(models.Dealer,slug=dealer_slug)
today_local = timezone.localdate()
# ----------------------------------------------------
# 1. Date Filtering
# ----------------------------------------------------
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()
else:
start_date = today_local - timedelta(days=30)
end_date = today_local
# Filter leads by date range and dealer
leads_filtered = models.Lead.objects.filter(
dealer=dealer,
created__date__gte=start_date,
created__date__lte=end_date
)
total_leads=leads_filtered.count()
# ----------------------------------------------------
# 2. Lead Sources Chart Logic
# ----------------------------------------------------
# Group leads by source and count them
# This generates a list of dictionaries like [{'source': 'Showroom', 'count': 45}, ...]
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]
# ----------------------------------------------------
# 2. Lead Funnel Chart Logic
# ----------------------------------------------------
opportunity_filtered = models.Opportunity.objects.filter(
dealer=dealer,
created__date__gte=start_date,
created__date__lte=end_date
)
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')
total_cars_in_inventory = active_cars.count()
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')
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()
context = {
'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,
'total_leads':total_leads
}
return render(request, 'dashboards/sales_dashboard.html', context)
def aging_inventory_list_view(request, dealer_slug):
"""
Renders a paginated list of aging inventory for a specific dealer, with filters.
"""
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
today_local = timezone.localdate()
aging_threshold_days = 60
# Get filter parameters from the request
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']
# 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)
if 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)
if 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')
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')
#
# Set up pagination
paginator = Paginator(aging_cars_queryset, 10)
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.
for car in page_obj.object_list:
car.age_in_days = (today_local - car.receiving_date.date()).days
context = {
"is_paginated": page_obj.has_other_pages,
"cars": page_obj.object_list,
'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)
def terms_and_privacy(request):
return render(request, "terms_and_privacy.html")
def WelcomeView(request):
"""
Handles the rendering and context data for the Welcome view.
This class serves as a Django TemplateView for the "welcome.html" template. It
is primarily responsible for providing the necessary context data, including
user-specific information and the list of plans, to the template for rendering
the welcome page.
:ivar template_name: Path to the template used by the view.
:type template_name: str
"""
if request.user.is_authenticated:
return redirect("home", dealer_slug=request.dealer.slug)
plan_list = Plan.objects.all()
context = {"plan_list": plan_list}
return render(request, "welcome.html", context)
class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView):
"""
Manages the creation of a new car entry in the inventory system.
This class is responsible for handling the creation of a new car, including form
display and data submission. It ensures the user has appropriate permissions and
customizes the available vendors for the user depending on their dealer entity.
:ivar model: Specifies the Car model that this view interacts with.
:type model: models.Car
:ivar form_class: Defines the form class to be used for creating or editing `Car` instances.
:type form_class: forms.CarForm
:ivar template_name: Name of the template to render the car creation form.
:type template_name: str
:ivar permission_required: Permissions required to add a car.
:type permission_required: list
"""
model = models.Car
form_class = forms.CarForm
template_name = "inventory/car_form.html"
permission_required = ["inventory.add_car"]
success_message=_("Car Added successfully to the inventory")
def get_form(self, form_class=None):
form = super().get_form(form_class)
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
form.fields["vendor"].queryset = dealer.vendors.filter(active=True)
return form
def get_success_url(self):
"""Determine the redirect URL based on user choice."""
if self.request.POST.get("add_another"):
return reverse(
"car_add", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
)
return reverse(
"inventory_stats", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
)
def form_valid(self, form):
dealer = get_user_type(self.request)
form.instance.dealer = dealer
form.save()
messages.success(self.request, _("Car saved successfully"))
return super().form_valid(form)
def get_context_data(self, **kwargs):
dealer = get_user_type(self.request)
context = super().get_context_data(**kwargs)
context["vendor_exists"] = dealer.vendors.filter(active=True).exists()
return context
def car_history(request, slug):
"""
Fetch and display the history of activities related to a specific car.
This view retrieves a car object based on its primary key (pk) and gathers
all related activity records where the content type corresponds to the car
model. The retrieved data is then rendered into a specified HTML template
for presentation.
:param request: The HTTP request object that contains metadata about the
request made by the client.
:type request: HttpRequest
:param pk: The primary key of the car object to retrieve.
:type pk: int
:return: An HTTP response with the rendered car history HTML template,
including the car and its associated activities as context data.
:rtype: HttpResponse
"""
car = get_object_or_404(models.Car, slug=slug)
activities = models.Activity.objects.filter(
content_type__model="car", object_id=car.id
)
return render(
request, "inventory/car_history.html", {"car": car, "activities": activities}
)
class AjaxHandlerView(LoginRequiredMixin, View):
"""
Handles AJAX requests for various car-related operations.
This class enables dynamic handling of AJAX requests related to vehicle
data, such as retrieving models, series, trims, specifications, and other
related details. It checks the requested action, delegates it to the
corresponding handler method, and returns an appropriate response.
:ivar request: Django request object containing HTTP request details.
"""
def get(self, request, dealer_slug, *args, **kwargs):
action = request.GET.get("action")
handlers = {
"decode_vin": self.decode_vin,
"get_models": self.get_models,
"get_series": self.get_series,
"get_trims": self.get_trims,
"get_specifications": self.get_specifications,
"get_equipments": self.get_equipments,
"get_options": self.get_options,
}
handler = handlers.get(action)
if handler:
return handler(request)
else:
return JsonResponse({"error": "Invalid action"}, status=400)
def decode_vin(self, request):
vin_no = request.GET.get("vin_no")
car_existed = models.Car.objects.filter(vin=vin_no).exists()
if car_existed:
return JsonResponse({"error": _("VIN number exists")}, status=400)
if not vin_no or len(vin_no.strip()) != 17:
return JsonResponse(
{"success": False, "error": _("Invalid VIN number provided")},
status=400,
)
vin_no = vin_no.strip()
vin_data = {}
decoding_method = ""
if result := decodevin(vin_no):
manufacturer_name, model_name, year_model = result.values()
car_make = get_make(manufacturer_name)
if not car_make:
logger.info(
f"VIN decoded using {decoding_method}: Make={manufacturer_name}, Model={model_name}, Year={year_model}"
)
return JsonResponse(
{
"success": False,
"error": _("Manufacturer not found in the database"),
},
status=404,
)
car_model = get_model(model_name, car_make)
logger.info(
f"VIN decoded using {decoding_method}: Make={manufacturer_name}, Model={model_name}, Year={year_model}"
)
if not car_make:
return JsonResponse(
{
"success": False,
"error": _("Manufacturer not found in the database"),
},
status=404,
)
vin_data["make_id"] = car_make.id_car_make
vin_data["name"] = car_make.name
vin_data["arabic_name"] = car_make.arabic_name
if not car_model:
vin_data["model_id"] = ""
else:
vin_data["model_id"] = car_model.id_car_model
vin_data["year"] = year_model
return JsonResponse({"success": True, "data": vin_data})
# manufacturer_name = model_name = year_model = None
return JsonResponse(
{"success": False, "error": _("VIN not found in all sources")},
status=404,
)
def get_models(self, request):
make_id = request.GET.get("make_id")
car_models = (
models.CarModel.objects.filter(id_car_make=make_id)
.values("id_car_model", "name", "arabic_name")
.order_by("name")
)
return JsonResponse(list(car_models), safe=False)
def get_series(self, request):
model_id = request.GET.get("model_id")
year = request.GET.get("year")
model_id = int(model_id)
year = int(year)
query = Q(id_car_model=model_id) & (
Q(year_begin__lte=year, year_end__gte=year)
| Q(year_end__isnull=True)
| Q(year_begin__isnull=True)
)
try:
series = models.CarSerie.objects.filter(query).values(
"id_car_serie", "name", "arabic_name", "generation_name"
)
logger.debug(
f"Successfully fetched car series with query: {query}. Found {len(series)} results."
)
except Exception as e:
logger.error(
f"Error fetching car series with query '{query}'. Details: {e}",
exc_info=True,
)
return JsonResponse({"error": _("Server error occurred")}, status=500)
return JsonResponse(list(series), safe=False)
def get_trims(self, request):
serie_id = request.GET.get("serie_id")
# model_id = request.GET.get('model_id')
trims = models.CarTrim.objects.filter(id_car_serie=serie_id).values(
"id_car_trim", "name", "arabic_name"
)
return JsonResponse(list(trims), safe=False)
def get_specifications(self, request):
trim_id = request.GET.get("trim_id")
car_spec_values = models.CarSpecificationValue.objects.filter(
id_car_trim=trim_id
)
lang = translation.get_language()
specs_by_parent = {}
for value in car_spec_values:
specification = value.id_car_specification
parent = specification.id_parent
parent_id = parent.id_car_specification if parent else 0
if lang == "ar":
parent_name = parent.arabic_name if parent else "Root"
else:
parent_name = parent.name if parent else "Root"
if parent_id not in specs_by_parent:
specs_by_parent[parent_id] = {
"parent_name": parent_name,
"specifications": [],
}
spec_data = {
"specification_id": specification.id_car_specification,
"s_name": specification.arabic_name
if lang == "ar"
else specification.name,
"s_value": value.value,
"s_unit": value.unit if value.unit else "",
"trim_name": value.id_car_trim.name,
}
specs_by_parent[parent_id]["specifications"].append(spec_data)
serialized_specs = [
{"parent_name": v["parent_name"], "specifications": v["specifications"]}
for v in specs_by_parent.values()
]
return JsonResponse(serialized_specs, safe=False)
def get_equipments(self, request):
trim_id = request.GET.get("trim_id")
equipments = (
models.CarEquipment.objects.filter(id_car_trim=trim_id)
.values("id_car_equipment", "name")
.order_by("name")
)
return JsonResponse(list(equipments), safe=False)
def get_options(self, request):
equipment_id = request.GET.get("equipment_id")
car_option_values = models.CarOptionValue.objects.filter(
id_car_equipment=equipment_id
)
options_by_parent = {}
for value in car_option_values:
option = value.id_car_option
parent = option.id_parent
parent_id = parent.id_car_option if parent else 0
parent_name = parent.name if parent else "Root"
if parent_id not in options_by_parent:
options_by_parent[parent_id] = {
"parent_name": parent_name,
"options": [],
}
option_data = {
"option_id": option.id_car_option,
"option_name": option.name,
"is_base": value.is_base,
"equipment_name": value.id_car_equipment.name,
}
options_by_parent[parent_id]["options"].append(option_data)
serialized_options = [
{"parent_name": v["parent_name"], "options": v["options"]}
for v in options_by_parent.values()
]
return JsonResponse(serialized_options, safe=False)
@method_decorator(csrf_exempt, name="dispatch")
class SearchCodeView(LoginRequiredMixin, View):
template_name = "inventory/scan_vin.html"
def get(self, request, *args, **kwargs):
return render(request, self.template_name)
def post(self, request, *args, **kwargs):
image_file = request.FILES.get("image")
if not image_file:
return JsonResponse({"success": False, "error": _("No image provided")})
try:
# --- Logging the start of image processing ---
logger.debug(
f"Received image '{image_file.name}' ({image_file.size} bytes) for barcode/QR scan from user: {request.user.username}."
)
np_arr = np.frombuffer(image_file.read(), np.uint8)
image = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
if image is None:
logger.error(
f"Invalid image format or corrupted file received for scan from user: {request.user.username}. "
f"Filename: {image_file.name}."
)
raise ValueError("Invalid image format")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
decoded_objects = decode(gray)
if not decoded_objects:
# --- Logging for no QR/Barcode detected ---
logger.info(
f"No QR/Barcode detected in image '{image_file.name}' "
f"from user: {request.user.username}."
)
return JsonResponse(
{"success": False, "error": _("No QR/Barcode detected")}
)
code = decoded_objects[0].data.decode("utf-8").strip()
# Use INFO or WARNING, as it's a common user-facing scenario, not necessarily a server error.
logger.info(
f"Scanned VIN '{code}' from image '{image_file.name}' not found in database for user: {request.user.username}."
)
car = get_object_or_404(models.Car, vin=code)
logger.info(
f"Successfully found car (VIN: {code}, ID: {car.pk}) based on scanned code for user: {request.user.username}."
)
return JsonResponse(
{
"success": True,
"code": code,
"redirect_url": reverse("car_detail", args=[car.slug]),
}
)
except Exception as e:
logger.error(
f"An unexpected error occurred during barcode/QR scan process for {image_file}"
f"from user: {request.user.username}. Error: {e}",
exc_info=True, # CRUCIAL: Get the full traceback
)
return JsonResponse({"success": False, "error": str(e)})
class CarInventory(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"""
Represents the car inventory listing view for a dealership.
This class is a Django ListView-based implementation to display a list of cars
filtered by specific criteria such as make, model, and trim, while supporting user
authentication and permissions. It includes pagination and ordering features and
integrates search filtering functionality.
:ivar model: The model associated with the list view.
:type model: models.Car
:ivar home_label: The label of the home section of the application, used for UI
or breadcrumbs. Defined as a translatable string.
:type home_label: str
:ivar template_name: The template path used for rendering the car inventory
list view.
:type template_name: str
:ivar context_object_name: The name of the context variable containing the
car objects in the template.
:type context_object_name: str
:ivar paginate_by: The number of car objects displayed per page.
:type paginate_by: int
:ivar ordering: The default ordering of car objects in the list view.
:type ordering: list
:ivar permission_required: The permission(s) required to access this view.
:type permission_required: list
"""
model = models.Car
home_label = _("inventory")
template_name = "inventory/car_inventory.html"
context_object_name = "cars"
paginate_by = 20
ordering = ["receiving_date"]
permission_required = ["inventory.view_car"]
def get_queryset(self, *args, **kwargs):
query = self.request.GET.get("q")
make = models.CarMake.objects.get(slug=self.kwargs["make_id"])
model = models.CarModel.objects.get(slug=self.kwargs["model_id"])
trim = models.CarTrim.objects.get(slug=self.kwargs["trim_id"])
dealer = get_user_type(self.request)
cars = models.Car.objects.filter(
dealer=dealer,
id_car_make=make,
id_car_model=model,
id_car_trim=trim,
).order_by("receiving_date")
return apply_search_filters(cars, query)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["query"] = self.request.GET.get("q", "")
context["make_id"] = self.kwargs["make_id"]
context["model_id"] = self.kwargs["model_id"]
context["trim_id"] = self.kwargs["trim_id"]
return context
class CarColorCreate(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView):
"""
View for creating a new car color.
This view is used to add new colors to a specific car by associating
the color with a car instance. It ensures that the user is logged in
and has the necessary permissions to add car colors. The form used in
this view is populated dynamically with respect to the related car
object that is retrieved based on the provided car primary key.
:ivar model: Reference to the model representing car colors.
:type model: models.CarColors
:ivar form_class: The form class used for car color creation.
:type form_class: forms.CarColorsForm
:ivar template_name: Path to the template used while rendering the view.
:type template_name: str
:ivar permission_required: List of permissions required by the view.
:type permission_required: list
"""
model = models.CarColors
form_class = forms.CarColorsForm
template_name = "inventory/add_colors.html"
permission_required = ["inventory.add_carcolors"]
success_message=_("Car colors details added successfully")
def form_valid(self, form):
car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
form.instance.car = car
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy(
"car_detail",
kwargs={
"dealer_slug": self.request.dealer.slug,
"slug": self.kwargs["slug"],
},
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"])
return context
class CarColorsUpdateView(
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
):
model = models.CarColors
form_class = forms.CarColorsForm
template_name = "inventory/add_colors.html"
success_message = _("Car Colors details updated successfully")
permission_required = ["inventory.change_carcolors"]
def get_object(self, queryset=None):
"""
Retrieves the CarColors instance associated with the Car slug from the URL.
This ensures we are updating the colors for the correct car.
"""
# Get the car_slug from the URL keywords arguments
slug = self.kwargs.get("slug")
# If no car_slug is provided, it's an invalid request
if not slug:
# You might want to raise Http404 or a more specific error here
raise ValueError("Car slug is required to identify the colors to update.")
return get_object_or_404(models.CarColors, car__slug=slug)
def get_success_url(self):
"""
Redirects to the car's detail page using its slug after a successful update.
"""
# self.object refers to the CarColors instance that was just updated.
# self.object.car then refers to the associated Car instance.
return reverse(
"car_detail",
kwargs={
"dealer_slug": self.request.dealer.slug,
"slug": self.object.car.slug,
},
)
def get_context_data(self, **kwargs):
"""
Adds the related Car object to the template context.
"""
context = super().get_context_data(**kwargs)
# self.object is already available here from get_object()
context["car"] = self.object.car
context["page_title"] = _("Update Colors for %(car_name)s") % {
"car_name": context["car"]
}
return context
class CarListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"""
Represents a view for listing and managing a collection of cars.
This class is used to display a paginated list of cars according to specific
filters and permissions. It ensures that only users with the required
permissions can access the view and handles dynamic filtering of cars based
on user input. The view also populates additional context data,
such as car statistics and related make, model, and year information,
to support detailed customization in templates.
:ivar model: Specifies the model associated with this view.
:type model: models.Car
:ivar template_name: The name of the template used for this view.
:type template_name: str
:ivar context_object_name: The name of the variable used in the template to
represent the objects being displayed.
:type context_object_name: str
:ivar paginate_by: The number of items to display per page in the paginated list.
:type paginate_by: int
:ivar permission_required: The permission required to access this view.
:type permission_required: str
"""
model = models.Car
template_name = "inventory/car_list_view.html"
context_object_name = "cars"
paginate_by = 10
permission_required = "inventory.view_car"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
dealer = get_user_type(self.request)
cars = models.Car.objects.filter(dealer=dealer).order_by("receiving_date")
context["stats"] = {
"all": cars.count(),
"available": cars.filter(status="available").count(),
"reserved": cars.filter(status="reserved").count(),
"sold": cars.filter(status="sold").count(),
"transfer": cars.filter(status="transfer").count(),
}
context["make"] = models.CarMake.objects.filter(
is_sa_import=True, car__in=cars
).distinct()
context["model"] = models.CarModel.objects.none()
context["year"] = models.Car.objects.none()
make = self.request.GET.get("make")
model = self.request.GET.get("model")
if make:
make_ = models.CarMake.objects.get(id_car_make=int(make))
context["model"] = make_.carmodel_set.filter(car__in=cars).distinct()
if make and model:
make_ = models.CarMake.objects.get(id_car_make=int(make))
model_ = models.CarModel.objects.get(id_car_model=int(model))
context["year"] = (
models.Car.objects.filter(id_car_make=make_, id_car_model=model_)
.values_list("year")
.distinct()
)
return context
def get_queryset(self):
dealer = get_user_type(self.request)
qs = super().get_queryset()
qs = qs.filter(dealer=dealer)
# status = self.request.GET.get("status")
# status = self.request.GET.get("status")
search = self.request.GET.get("search")
make = self.request.GET.get("make", None)
model = self.request.GET.get("model", None)
year = self.request.GET.get("year", None)
car_status = self.request.GET.get("car_status", None)
print(car_status)
if car_status:
qs = qs.filter(status=car_status)
if search:
query = (
Q(vin__icontains=search)
| Q(id_car_make__name__icontains=search)
| Q(id_car_model__name__icontains=search)
| Q(id_car_trim__name__icontains=search)
| Q(vin=search)
)
qs = qs.filter(query)
if any([make, model, year, car_status]):
query = Q()
if make:
query &= Q(id_car_make=int(make))
if model:
query &= Q(id_car_model=model)
if year:
query &= Q(year=year)
if car_status:
query &= Q(status=car_status)
qs = qs.filter(query)
return qs
@login_required
@permission_required("inventory.view_car")
def inventory_stats_view(request, dealer_slug):
"""
Handle the inventory stats view for a dealer, displaying detailed information
about the cars, including counts grouped by make, model, and trim.
The function fetches all cars associated with the authenticated dealer, calculates
the inventory statistics (e.g., total cars, reserved cars, and cars categorized
by make, model, and trim levels), and prepares the data to be rendered in a
template.
:param request: The HTTP request object from the client.
:type request: HttpRequest
:return: An HTTP response containing structured inventory data rendered in the
"inventory/inventory_stats.html" template.
:rtype: HttpResponse
"""
# Base queryset for cars belonging to the dealer
cars = models.Car.objects.filter(dealer=request.dealer)
# Count for total, reserved, showroom, and unreserved cars
total_cars = cars.count()
reserved_cars = models.CarReservation.objects.count()
# showroom_cars = cars.filter(location='showroom').count()
# unreserved_cars = total_cars - reserved_cars
# Annotate total cars by make, model, and trim
cars = cars.select_related("id_car_make", "id_car_model", "id_car_trim").annotate(
make_total=Count("id_car_make"),
model_total=Count("id_car_model"),
trim_total=Count("id_car_trim"),
)
inventory = {}
for car in cars:
make = car.id_car_make
if make.id_car_make not in inventory:
inventory[make.id_car_make] = {
"make_id": make.id_car_make,
"slug": make.slug,
"make_name": make.get_local_name(),
"total_cars": 0,
"models": {},
}
inventory[make.id_car_make]["total_cars"] += 1
model = car.id_car_model
if model and model.id_car_model not in inventory[make.id_car_make]["models"]:
inventory[make.id_car_make]["models"][model.id_car_model] = {
"model_id": model.id_car_model,
"slug": model.slug,
"model_name": model.get_local_name(),
"total_cars": 0,
"trims": {},
}
try:
logger.debug(
f"Attempting to update inventory for Car ID: {car.id}, Make: {make.name} ({make.id_car_make}), "
f"Model: {model.name} ({model.id_car_model})."
)
inventory[make.id_car_make]["models"][model.id_car_model]["total_cars"] += 1
trim = car.id_car_trim
if (
trim
and trim.id_car_trim
not in inventory[make.id_car_make]["models"][model.id_car_model][
"trims"
]
):
inventory[make.id_car_make]["models"][model.id_car_model]["trims"][
trim.id_car_trim
] = {
"trim_id": trim.id_car_trim,
"slug": trim.slug,
"trim_name": trim.name,
"total_cars": 0,
}
inventory[make.id_car_make]["models"][model.id_car_model]["trims"][
trim.id_car_trim
]["total_cars"] += 1
except Exception as e:
logger.error(
f"Error updating inventory counts for car ID {getattr(car, 'id', 'N/A')}. "
f"Make ID: {getattr(make, 'id_car_make', 'N/A')}, Model ID: {getattr(model, 'id_car_model', 'N/A')}, "
f"Trim ID: {getattr(trim, 'id_car_trim', 'N/A')}. Error: {e}",
exc_info=True,
)
print(e)
result = {
"total_cars": total_cars,
"reserved_cars": reserved_cars,
"makes": [
{
"make_id": make_data["make_id"],
"slug": make_data["slug"],
"make_name": make_data["make_name"],
"total_cars": make_data["total_cars"],
"models": [
{
"model_id": model_data["model_id"],
"slug": model_data["slug"],
"model_name": model_data["model_name"],
"total_cars": model_data["total_cars"],
"trims": list(model_data["trims"].values()),
}
for model_data in make_data["models"].values()
],
}
for make_data in inventory.values()
],
}
return render(request, "inventory/inventory_stats.html", {"inventory": result,"empty_state_value":_("car")})
# @login_required
# def inventory_stats_view(request):
# """
# Handle the inventory stats view for a dealer, displaying detailed information
# about the cars, including counts grouped by make, model, and trim.
# The function fetches all cars associated with the authenticated dealer, calculates
# the inventory statistics (e.g., total cars, reserved cars, and cars categorized
# by make, model, and trim levels), and prepares the data to be rendered in a
# template.
# :param request: The HTTP request object from the client.
# :type request: HttpRequest
# :return: An HTTP response containing structured inventory data rendered in the
# "inventory/inventory_stats.html" template.
# :rtype: HttpResponse
# """
# dealer = get_user_type(request)
# # Base queryset for cars belonging to the dealer
# # Ordering here is important for consistent pagination results
# cars = models.Car.objects.filter(dealer=dealer).order_by(
# 'id_car_make__name', 'id_car_model__name', 'id_car_trim__name'
# ) # Added ordering for consistent pagination
# # Count for total, reserved, showroom, and unreserved cars
# total_cars = cars.count()
# reserved_cars = models.CarReservation.objects.filter(car__dealer=dealer).count() # Filter reservations by dealer's cars
# # We need to process the cars into the inventory structure FIRST,
# # then paginate the list of makes.
# inventory_data = {}
# for car in cars:
# make = car.id_car_make
# if make.id_car_make not in inventory_data:
# inventory_data[make.id_car_make] = {
# "make_id": make.id_car_make,
# "slug": make.slug,
# "make_name": make.get_local_name(),
# "total_cars": 0,
# "models": {},
# }
# inventory_data[make.id_car_make]["total_cars"] += 1
# model = car.id_car_model
# # Ensure model exists before trying to access its attributes
# if model:
# if model.id_car_model not in inventory_data[make.id_car_make]["models"]:
# inventory_data[make.id_car_make]["models"][model.id_car_model] = {
# "model_id": model.id_car_model,
# "slug": model.slug,
# "model_name": model.get_local_name(),
# "total_cars": 0,
# "trims": {},
# }
# inventory_data[make.id_car_make]["models"][model.id_car_model]["total_cars"] += 1
# trim = car.id_car_trim
# if trim: # Ensure trim exists
# if trim.id_car_trim not in inventory_data[make.id_car_make]["models"][model.id_car_model]["trims"]:
# inventory_data[make.id_car_make]["models"][model.id_car_model]["trims"][
# trim.id_car_trim
# ] = {
# "trim_id": trim.id_car_trim,
# "slug": trim.slug,
# "trim_name": trim.name,
# "total_cars": 0,
# }
# inventory_data[make.id_car_make]["models"][model.id_car_model]["trims"][
# trim.id_car_trim
# ]["total_cars"] += 1
# # Convert the inventory dictionary into a list of makes for pagination
# # Sort the makes by name for consistent pagination
# all_makes = sorted(inventory_data.values(), key=lambda x: x['make_name'])
# # --- Pagination Logic ---
# items_per_page = 10 # You can adjust this number
# paginator = Paginator(all_makes, items_per_page)
# page_number = request.GET.get('page')
# try:
# page_obj = paginator.get_page(page_number)
# except PageNotAnInteger:
# page_obj = paginator.get_page(1)
# except EmptyPage:
# page_obj = paginator.get_page(paginator.num_pages)
# # The 'makes' list for the current page
# # Ensure models and trims are also converted to lists within each make on the current page
# paginated_makes_list = []
# for make_data in page_obj.object_list:
# make_data_copy = make_data.copy() # Avoid modifying the original dict during iteration
# make_data_copy['models'] = []
# for model_id, model_data in make_data['models'].items():
# model_data_copy = model_data.copy()
# model_data_copy['trims'] = list(model_data['trims'].values())
# make_data_copy['models'].append(model_data_copy)
# paginated_makes_list.append(make_data_copy)
# result = {
# "total_cars": total_cars,
# "reserved_cars": reserved_cars,
# "makes": paginated_makes_list, # This is now the paginated list of makes
# }
# # print(result["makes"]) # For debugging
# context = {
# "inventory": result,
# "page_obj": page_obj, # Pass the page_obj to the template
# "is_paginated": page_obj.has_other_pages(), # To use in template for conditional rendering
# }
# return render(request, "inventory/inventory_stats.html", context)
class CarDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""
Provides a detailed view of a single car instance.
This class-based view is designed to display detailed information about a
single car instance from the inventory. It utilizes Django's built-in
DetailView along with mixins to enforce authentication and permission
requirements. The view ensures that only authenticated users with the proper
permissions can access car details.
:ivar model: The model associated with this view, representing the Car model.
:type model: Model
:ivar template_name: The path to the template used to render the detailed
car view.
:type template_name: str
:ivar context_object_name: The name of the context variable that contains
the car object.
:type context_object_name: str
:ivar permission_required: A list of permissions required to access the
view.
:type permission_required: list
"""
model = models.Car
template_name = "inventory/car_detail.html"
context_object_name = "car"
permission_required = ["inventory.view_car"]
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)
if request.method == "POST":
form = forms.CarFinanceForm(request.POST, instance=car)
if form.is_valid():
form.save()
return redirect("car_detail", dealer_slug=dealer.slug, slug=car.slug)
else:
messages.error(request, "Please correct the errors below.")
else:
form = forms.CarFinanceForm(instance=car)
return render(request, "inventory/car_finance_form.html", {"car": car, "dealer": dealer, "form": form})
class CarUpdateView(
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
):
"""
Represents a Django view dedicated to updating Car model instances.
This view facilitates secure and authorized updates to car-related information
through a web interface. It ensures permission checks, requires user login for
access, and communicates feedback upon successful updates.
:ivar model: The model associated with this view.
:type model: models.Model
:ivar form_class: The form class used for updating the model instance.
:type form_class: django.forms.ModelForm
:ivar template_name: The path to the template used for rendering the view.
:type template_name: str
:ivar success_message: Message displayed upon successful update.
:type success_message: str
:ivar permission_required: List of permissions required to access this view.
:type permission_required: list[str]
"""
model = models.Car
form_class = forms.CarUpdateForm
template_name = "inventory/car_edit.html"
success_message = _("Car updated successfully")
permission_required = ["inventory.change_car"]
def get_success_url(self):
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)
dealer = get_user_type(self.request)
form.fields["vendor"].queryset = dealer.vendors.filter(active=True)
return form
class CarDeleteView(
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, DeleteView
):
"""
Handles the deletion of Car objects in the inventory application.
This view ensures that only authorized users can delete a car. It requires users to be logged in
and have the appropriate permissions. Upon successful deletion, a success message is displayed,
and the user is redirected to the inventory statistics page.
:ivar model: The model associated with this view.
:type model: models.Car
:ivar template_name: The path to the HTML template used for confirming a car deletion.
:type template_name: str
:ivar success_url: The URL to redirect to after successful deletion.
:type success_url: str
:ivar permission_required: A list of permission strings required to access this view.
:type permission_required: list[str]
"""
model = models.Car
template_name = "inventory/car_confirm_delete.html"
permission_required = ["inventory.delete_car"]
def delete(self, request, *args, **kwargs):
messages.success(request, _("Car deleted successfully"))
return super().delete(request, *args, **kwargs)
def get_success_url(self):
"""
Returns the URL to redirect to after a successful car deletion.
It dynamically includes the dealer_slug from the URL.
"""
dealer_slug = self.kwargs.get("dealer_slug")
if dealer_slug:
return reverse_lazy("car_list", kwargs={"dealer_slug": dealer_slug})
else:
messages.error(
self.request, _("Could not determine dealer for redirection.")
)
return reverse_lazy("home")
class CarLocationCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
"""
Handles the creation of new car locations.
This view allows authenticated and authorized users to add new car locations
to the system. The form allows users to specify details regarding the car's
location. Permissions are required to ensure only authorized users are
permitted to perform this action. Upon successful form submission, the
user is redirected to the car's detail page and a success message is displayed.
:ivar model: The model associated with this view.
:type model: models.CarLocation
:ivar form_class: The form class used to create or update the model.
:type form_class: forms.CarLocationForm
:ivar template_name: Path to the template used by the view.
:type template_name: str
:ivar permission_required: List of permissions required to create a car location.
:type permission_required: list
"""
model = models.CarLocation
form_class = forms.CarLocationForm
template_name = "inventory/car_location_form.html"
permission_required = ["inventory.add_carlocation"]
def get_success_url(self):
return reverse_lazy("car_detail", kwargs={"slug": self.object.car.slug})
def form_valid(self, form):
form.instance.car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
dealer = get_user_type(self.request)
form.instance.owner = dealer
form.save()
messages.success(self.request, _("Location saved successfully"))
return super().form_valid(form)
class CarLocationUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
"""
Provides functionality to update the location of a car.
This view is a subclass of `LoginRequiredMixin` and `PermissionRequiredMixin`, ensuring
that only authenticated and authorized users with the appropriate permissions can access it.
It provides a form to update car location details and handles the necessary updates to the
location record, associating it with the appropriate car and owner. Upon successful update,
a success message is displayed to the user, and the user is redirected to the car detail page.
:ivar model: The model associated with this view. Represents car location.
:type model: models.CarLocation
:ivar form_class: The form class used for updating car location.
:type form_class: forms.CarLocationForm
:ivar template_name: The path to the template used to render the view.
:type template_name: str
:ivar permission_required: Permissions required to access this view.
:type permission_required: list
"""
model = models.CarLocation
form_class = forms.CarLocationForm
template_name = "inventory/car_location_form.html"
permission_required = ["inventory.update_carlocation"]
# def get_initial(self):
# initial = super().get_initial()
# initial["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"])
# return initial
def form_valid(self, form):
form.instance.car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
dealer = get_user_type(self.request)
form.instance.owner = dealer
form.save()
messages.success(self.request, _("Location updated successfully"))
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("car_detail", kwargs={"slug": self.object.car.slug})
class CarTransferCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
"""
A view for creating car transfers.
This class-based view allows authenticated users to create car transfers. It handles the
selection of cars and dealers involved in the transfer process. The view inherits from
`LoginRequiredMixin` to ensure only logged-in users can access it, and `CreateView` to
simplify the implementation of the creation logic.
:ivar model: The model class associated with the view.
:type model: models.CarTransfer
:ivar form_class: The form class used to render and validate the input data.
:type form_class: forms.CarTransferForm
:ivar template_name: The path to the template used to render the view.
:type template_name: str
"""
model = models.CarTransfer
form_class = forms.CarTransferForm
template_name = "inventory/car_transfer_form.html"
permission_required = ["inventory.add_cartransfer"]
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.fields["to_dealer"].queryset = models.Dealer.objects.exclude(
pk=get_user_type(self.request).pk
).all()
form.fields["car"].queryset = models.Car.objects.filter(
slug=self.kwargs["slug"]
)
return form
def get_initial(self):
initial = super().get_initial()
initial["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"])
return initial
def form_valid(self, form):
form.instance.from_dealer = get_user_type(self.request)
form.instance.car.status = "transfer"
form.instance.car.save()
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("car_detail", kwargs={"slug": self.object.car.slug})
class CarTransferDetailView(
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, DetailView
):
"""
Provides a detailed view of a specific car transfer record.
This class-based view is used to display details of a car transfer for
authenticated users. It ensures that only authorized users can access this
information and renders the data along with additional context such as
specific actions tied to the transfer. The view is associated with a Django
model for car transfers and uses a specific template to display information.
:ivar model: The model associated with this view, which represents car
transfer records.
:type model: Type[models.CarTransfer]
:ivar template_name: The path to the template used for rendering the car
transfer details.
:type template_name: str
:ivar context_object_name: The name of the context object used in the template
to reference the car transfer record.
:type context_object_name: str
"""
model = models.CarTransfer
template_name = "inventory/transfer_details.html"
context_object_name = "transfer"
permission_required = ["inventory.view_cartransfer"]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["action"] = self.request.GET.get("action")
return context
@login_required
@permission_required("inventory.change_cartransfer")
def car_transfer_approve(request, slug, transfer_pk):
"""
Approves or cancels a car transfer request based on the action parameter. This view
handles the workflow of updating the transfer status and notifying the involved parties
accordingly. If the transfer is canceled, it reverts the car status to "available" and
deactivates the transfer record. If approved, it notifies the recipient dealer and allows
the request to proceed for further actions.
:param request: The HTTP request object containing metadata and the action parameter.
:param car_pk: Primary key of the car involved in the transfer.
:param transfer_pk: Primary key of the transfer request to approve or cancel.
:return: An HTTP response redirecting to the car detail page of the specified car.
"""
car = get_object_or_404(models.Car, slug=slug)
transfer = get_object_or_404(models.CarTransfer, pk=transfer_pk)
action = request.GET.get("action")
if action == "cancel":
transfer.status = "cancel"
transfer.active = False
transfer.save()
transfer.car.status = "available"
transfer.car.save()
messages.success(request, _("Car transfer canceled successfully"))
models.Notification.objects.create(
user=transfer.from_dealer.user,
message=f"Car transfer request from {transfer.to_dealer} is canceled.",
)
return redirect("car_detail", slug=car.slug)
transfer.status = "approved"
transfer.save()
url = request.build_absolute_uri(
reverse(
"transfer_preview", kwargs={"slug": car.slug, "transfer_pk": transfer.pk}
)
)
models.Notification.objects.create(
user=transfer.to_dealer.user,
message=f"Car transfer request from {transfer.from_dealer} is waiting for your acceptance. <a href='{url}'> Accept</a>",
)
messages.success(request, _("Car transfer approved successfully"))
return redirect("car_detail", slug=car.slug)
@login_required
@permission_required("inventory.change_cartransfer")
def car_transfer_accept_reject(request, slug, transfer_pk):
"""
Handles the acceptance or rejection of a car transfer request. Based on the
`status` parameter obtained from the query string, the function updates the
transfer status to either 'accept' or 'reject'. If the transfer is accepted, it
initiates the car transfer process. Appropriate notifications are sent, and
activity records are created for both acceptance and rejection actions.
:param request: The HTTP request object which contains metadata about
the request made by the user, including session and user information.
:param car_pk: The primary key of the car to be transferred.
:param transfer_pk: The primary key of the car transfer request to be processed.
:return: An HTTP redirect response to the 'inventory_stats' view.
"""
car = get_object_or_404(models.Car, slug=slug)
transfer = get_object_or_404(models.CarTransfer, pk=transfer_pk)
status = request.GET.get("status")
if status == "rejected":
transfer.status = "reject"
transfer.active = False
messages.success(request, _("Car transfer rejected successfully"))
models.Notification.objects.create(
user=transfer.from_dealer.user,
message=f"Car transfer request from {transfer.to_dealer} is rejected.",
)
transfer.save()
elif status == "accepted":
transfer.status = "accept"
transfer.save()
transfer_process = CarTransfer(car, transfer)
success = transfer_process.transfer_car()
if success:
messages.success(request, _("Car Transfer Completed successfully."))
models.Activity.objects.create(
content_object=car,
notes=f"Transfered from {transfer.from_dealer} to {transfer.to_dealer}",
created_by=request.user,
)
models.Notification.objects.create(
user=transfer.from_dealer.user,
message=f"Car transfer request from {transfer.to_dealer} is completed.",
)
return redirect("inventory_stats")
@login_required
@permission_required("inventory.view_cartransfer")
def CarTransferPreviewView(request, slug, transfer_pk):
"""
Handles the preview of car transfer details and ensures that a user has appropriate
permissions to view the transfer based on their associated dealer.
This view checks if the car transfer's destination dealer matches the current user's
associated dealer type. If not, the user is redirected to the car detail page. Otherwise,
it renders the transfer preview page with the relevant transfer details.
:param request: The HTTP request object
:type request: django.http.HttpRequest
:param car_pk: The primary key of the car related to the transfer
:type car_pk: int
:param transfer_pk: The primary key of the car transfer to preview
:type transfer_pk: int
:return: An HTTP response rendering the transfer preview page or redirecting
to the car detail page
:rtype: django.http.HttpResponse
"""
transfer = get_object_or_404(models.CarTransfer, pk=transfer_pk)
if transfer.to_dealer != get_user_type(request):
return redirect("car_detail", slug=slug)
return render(request, "inventory/transfer_preview.html", {"transfer": transfer})
class CustomCardCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
"""
Represents a view for creating a custom card associated with a specific car.
This view ensures that the user is authenticated before allowing access and
associates the created custom card with a specific car. It handles form validation,
context data injection, and determines the success URL upon successful form submission.
:ivar model: The model associated with the view, which is `CustomCard`.
:type model: models.CustomCard
:ivar form_class: The form class used to create a new instance of `CustomCard`.
:type form_class: forms.CustomCardForm
:ivar template_name: The name of the template used to render the view.
:type template_name: str
"""
model = models.CustomCard
form_class = forms.CustomCardForm
template_name = "inventory/add_custom_card.html"
permission_required = "inventory.add_customcard"
def form_valid(self, form):
car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
form.instance.car = car
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"])
return context
def get_success_url(self):
messages.success(self.request, _("Custom Card added successfully"))
return reverse_lazy(
"car_detail",
kwargs={
"dealer_slug": self.request.dealer.slug,
"slug": self.kwargs["slug"],
},
)
class CarRegistrationCreateView(
LoginRequiredMixin, PermissionRequiredMixin, CreateView
):
"""
Handles the creation of new car registration records.
This view is responsible for rendering the car registration creation form,
validating input, and saving the data to the database. It ensures that the
current user is authenticated and associates the registration with a specific
car, identified by its primary key.
Inherits from:
- LoginRequiredMixin: Ensures the user is logged in to access the view.
- CreateView: Provides built-in functionality for creating and saving model
instances.
:ivar model: The model linked to this view, representing car registrations.
:type model: models.CarRegistration
:ivar form_class: The form class used for creating car registration instances.
:type form_class: forms.CarRegistrationForm
:ivar template_name: The path to the HTML template used for rendering the car
registration form.
:type template_name: str
"""
model = models.CarRegistration
form_class = forms.CarRegistrationForm
template_name = "inventory/car_registration_form.html"
permission_required = "inventory.add_carregistration"
def form_valid(self, form):
car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
form.instance.car = car
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"])
return context
def get_success_url(self):
messages.success(self.request, _("Registration added successfully"))
return reverse_lazy(
"car_detail",
kwargs={
"dealer_slug": self.request.dealer.slug,
"slug": self.kwargs["slug"],
},
)
@login_required()
def reserve_car_view(request, dealer_slug, slug):
"""
Handles car reservation requests. This view requires the user to be logged in
and processes only POST requests. When invoked, it checks if the specified car
is already reserved. If not, it proceeds to reserve the car for the user and
sends an appropriate response. If the car is already reserved or if the request
method is invalid, it provides corresponding error messages or responses.
:param request: The HTTP request object.
:type request: HttpRequest
:param car_id: The unique identifier of the car to be reserved.
:type car_id: int
:return: A response indicating the result of the reservation process.
:rtype: HttpResponse or JsonResponse
"""
if request.method == "POST":
car = get_object_or_404(models.Car, slug=slug)
if car.is_reserved():
messages.error(request, _("This car is already reserved"))
return redirect("car_detail", slug=car.slug)
response = reserve_car(car, request)
return response
return JsonResponse(
{"success": False, "message": "Invalid request method"}, status=400
)
@login_required
def manage_reservation(request, dealer_slug, reservation_id):
"""
Handles the management of a car reservation, providing options to renew or
cancel an existing reservation associated with the logged-in user.
Renewing a reservation extends the reservation period by an additional
24 hours. Canceling a reservation deletes it and updates the car's status
to AVAILABLE. All actions require a valid reservation and are performed
based on the current user's authentication and request type.
:param request: Django HttpRequest object representing the client's request.
:type request: HttpRequest
:param reservation_id: The unique identifier of the car reservation to manage.
:type reservation_id: int
:return: On POST requests, returns an HTTP redirect or JSON response
based on the action performed. On other request methods,
returns a JSON response with an error message.
:rtype: JsonResponse or HttpResponseRedirect
"""
reservation = get_object_or_404(
models.CarReservation, pk=reservation_id, reserved_by=request.user
)
if request.method == "POST":
action = request.POST.get("action")
if action == "renew":
reservation.reserved_until = timezone.now() + timezone.timedelta(hours=24)
reservation.save()
messages.success(request, _("Reservation renewed successfully"))
return redirect(
"car_detail", dealer_slug=request.dealer.slug, slug=reservation.car.slug
)
elif action == "cancel":
car = reservation.car
reservation.delete()
car.status = models.CarStatusChoices.AVAILABLE
car.save()
messages.success(request, _("Reservation canceled successfully"))
return redirect(
"car_detail", dealer_slug=request.dealer.slug, slug=reservation.car.slug
)
else:
return JsonResponse(
{"success": False, "message": _("Invalid action")}, status=400
)
return JsonResponse(
{"success": False, "message": _("Invalid request method")}, status=400
)
class DealerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""
Represents a detailed view for a Dealer model.
This class extends Django's `DetailView` to provide a detailed view of a dealer.
It includes additional context data such as the count of staff members, cars
associated with the dealer, available car makes, and dynamically fetched quotas.
The class also ensures that users must be logged in to access the detailed view.
:ivar model: The model associated with this view (Dealer model).
:type model: django.db.models.Model
:ivar template_name: Path to the template used to render the view.
:type template_name: str
:ivar context_object_name: The name used to refer to the object in the template context.
:type context_object_name: str
"""
model = models.Dealer
template_name = "dealers/dealer_detail.html"
context_object_name = "dealer"
permission_required = "inventory.view_dealer"
def get_queryset(self):
return models.Dealer.objects.annotate(
staff_count=Coalesce(
Count("staff"), Value(0)
) # Get the number of staff members
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
dealer = self.object
car_makes = models.CarMake.objects.filter(
car_dealers__dealer=dealer, is_sa_import=True
)
staff_count = dealer.staff_count
cars_count = models.Car.objects.filter(dealer=dealer).count()
context["car_makes"] = car_makes
context["staff_count"] = staff_count
context["cars_count"] = cars_count
context["allowed_users"] = dealer.user_quota
context["allowed_cars"] = dealer.car_quota
context["vatform"] = forms.VatRateForm(initial={"rate": dealer.vat_rate})
context["quota_display"] = (
f"{staff_count}/{dealer.user_quota}" if dealer.user_quota else "0"
)
return context
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"))
messages.success(request, _("VAT rate updated successfully"))
return redirect("dealer_detail", slug=slug)
class DealerUpdateView(
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
):
"""
Handles the update functionality for the Dealer model.
This class-based view allows authenticated users to update an existing
Dealer instance using a form. Upon successful update, a success message
is displayed, and the user is redirected to the detail view of the updated
Dealer.
:ivar model: The model class associated with this view.
:type model: models.Dealer
:ivar form_class: The form class used for this view.
:type form_class: forms.DealerForm
:ivar template_name: The template used to render the form for this view.
:type template_name: str
:ivar success_url: The URL to redirect to after a successful form submission.
:type success_url: str
:ivar success_message: The message displayed upon a successful update.
:type success_message: str
"""
model = models.Dealer
form_class = forms.DealerForm
template_name = "dealers/dealer_form.html"
success_url = reverse_lazy("dealer_detail")
success_message = _("Dealer updated successfully")
permission_required = ["inventory.change_dealer"]
def get_success_url(self):
return reverse("dealer_detail", kwargs={"slug": self.object.slug})
class StaffDetailView(LoginRequiredMixin, DetailView):
"""
Represents a detailed view for a Dealer model.
This class extends Django's `DetailView` to provide a detailed view of a dealer.
It includes additional context data such as the count of staff members, cars
associated with the dealer, available car makes, and dynamically fetched quotas.
The class also ensures that users must be logged in to access the detailed view.
:ivar model: The model associated with this view (Dealer model).
:type model: django.db.models.Model
:ivar template_name: Path to the template used to render the view.
:type template_name: str
:ivar context_object_name: The name used to refer to the object in the template context.
:type context_object_name: str
"""
model = models.Staff
template_name = "staff/staff_detail.html"
context_object_name = "staff"
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"))
messages.success(request, _("VAT rate updated successfully"))
return redirect("dealer_detail", slug=slug)
class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"""
View for displaying a list of customers.
This class-based view is used to display a paginated and searchable list of
customers. It ensures that the user has the required permissions and is
logged in before they can access the view. The view fetches a list of
customers related to the current user's entity and applies any search
filters based on the user's query. The data is rendered using a specified
template.
:ivar model: The model associated with the view, representing customer data.
:type model: type[CustomerModel]
:ivar home_label: The label used for navigation purposes in the UI.
:type home_label: str
:ivar context_object_name: The name of the context variable in the template.
:type context_object_name: str
:ivar paginate_by: Number of items displayed per page for pagination.
:type paginate_by: int
:ivar template_name: The path of the template used for rendering the view.
:type template_name: str
:ivar ordering: The default ordering applied to the queryset of customers.
:type ordering: list
:ivar permission_required: A list of permissions required to access the view.
:type permission_required: list
"""
model = models.Customer
home_label = _("customers")
context_object_name = "customers"
paginate_by = 30
template_name = "customers/customer_list.html"
ordering = ["-created"]
permission_required = ["inventory.view_customer"]
def get_queryset(self):
query = self.request.GET.get("q")
dealer = get_user_type(self.request)
customers = dealer.customers.filter(active=True)
return apply_search_filters(customers, query)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["query"] = self.request.GET.get("q", "")
context["empty_state_value"] = _("customers")
return context
# class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
# """
# CustomerDetailView handles retrieving and presenting detailed information about
# a specific customer. It ensures that the user is authenticated and has the
# necessary permissions before accessing the customer's details. This view
# provides context data including estimates and invoices related to the customer.
# :ivar model: The model associated with the view.
# :type model: CustomerModel
# :ivar template_name: The path to the template used for rendering the view.
# :type template_name: str
# :ivar context_object_name: The name of the variable in the template context
# for the object being viewed.
# :type context_object_name: str
# :ivar permission_required: The list of permissions required to access this view.
# :type permission_required: list[str]
# """
# model = models.Customer
# template_name = "customers/view_customer.html"
# context_object_name = "customer"
# permission_required = ["inventory.view_customer"]
# def get_context_data(self, **kwargs):
# dealer = get_user_type(self.request)
# entity = dealer.entity
# context = super().get_context_data(**kwargs)
# context["notes"] = models.Notes.objects.filter(
# 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)
# total = estimates.count() + invoices.count()
# context["estimates"] = estimates
# context["invoices"] = invoices
# context["total"] = total
# context["note_form"] = forms.NoteForm()
# return context
class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""
CustomerDetailView handles retrieving and presenting detailed information about
a specific customer. It ensures that the user is authenticated and has the
necessary permissions before accessing the customer's details. This view
provides context data including estimates and invoices related to the customer.
:ivar model: The model associated with the view.
:type model: CustomerModel
:ivar template_name: The path to the template used for rendering the view.
:type template_name: str
:ivar context_object_name: The name of the variable in the template context
for the object being viewed.
:type context_object_name: str
:ivar permission_required: The list of permissions required to access this view.
:type permission_required: list[str]
"""
model = models.Customer
template_name = "customers/view_customer.html"
context_object_name = "customer"
permission_required = ["inventory.view_customer"]
def get_context_data(self, **kwargs):
dealer = get_user_type(self.request)
entity = dealer.entity
context = super().get_context_data(**kwargs)
context["notes"] = models.Notes.objects.filter(
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()
total = estimates.count() + invoices.count()
context["estimates"] = estimates
context["invoices"] = invoices
context["total"] = total
context["note_form"] = forms.NoteForm()
return context
@login_required
@permission_required("inventory.add_notes", raise_exception=True)
def add_note_to_customer(request, dealer_slug, slug):
"""
This function allows authenticated users to add a note to a specific customer. The
note creation is handled by a form, which is validated after submission. If the form
is valid, the note is saved and associated with the specified customer. On successful
submission, the user is redirected to the customer detail page. If the request method
is not POST, an empty form is rendered.
:param request: The HTTP request object containing metadata and the method type
(e.g., GET, POST). Should be an HttpRequest instance.
:param customer_id: The unique identifier (UUID) of the customer to whom the note
is to be added.
:return: An HTTP response. In the case of a successful POST request, the function
returns a redirect response to the customer detail page. For a GET or invalid
POST request, it renders the note form template with context including
the form and customer.
"""
# get_object_or_404(models.Dealer, slug=dealer_slug)
customer = get_object_or_404(models.Customer, slug=slug)
if request.method == "POST":
form = forms.NoteForm(request.POST)
if form.is_valid():
note = form.save(commit=False)
note.content_object = customer
note.created_by = request.user
note.save()
return redirect(
"customer_detail", dealer_slug=dealer_slug, slug=customer.slug
)
form = forms.NoteForm()
return render(
request, "customers/note_form.html", {"form": form, "customer": customer}
)
@login_required
@permission_required("inventory.add_activity", raise_exception=True)
def add_activity_to_customer(request, pk):
"""
Adds an activity to a specific customer.
This function allows adding a new activity to a customer identified by their
primary key (`pk`). It retrieves the customer object, processes the form for
activity creation, and saves it. If the request method is POST, it validates
the form and associates the activity with the respective customer. Upon
successful save, it redirects to the customer detail view. If the request
method is GET, it renders a form for activity submission.
:param request: The HTTP request object containing metadata about the request.
:type request: HttpRequest
:param pk: The primary key of the customer to which the activity will be added.
:type pk: int
:return: An HTTP response rendered with the activity form in the context of
the customer, or a redirect response to the customer detail view upon
successful activity creation.
:rtype: HttpResponse
"""
customer = get_object_or_404(CustomerModel, pk=pk)
if request.method == "POST":
form = forms.ActivityForm(request.POST)
if form.is_valid():
activity = form.save(commit=False)
activity.content_object = customer
activity.created_by = request.user
activity.save()
return redirect("customer_detail", pk=pk)
else:
form = forms.ActivityForm()
return render(
request, "crm/add_activity.html", {"form": form, "customer": customer}
)
class CustomerCreateView(
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView
):
"""
# Handles the creation of a new customer within the system. This view ensures that proper permissions
# and request methods are utilized. It provides feedback to the user about the success or failure of
# the customer creation process. When the form is submitted and valid, it checks for duplicate
# customers based on the email provided before proceeding with the customer creation.
# :param request: The HTTP request object containing metadata about the request initiated by the user.
# :type request: HttpRequest
# :return: The rendered form page or a redirect to the customer list page upon successful creation.
# :rtype: HttpResponse
# :raises PermissionDenied: If the user does not have the required permissions to access the view.
#"""
model = models.Customer
form_class = forms.CustomerForm
permission_required = ["inventory.add_customer"]
permission_required = ["inventory.add_customer"]
template_name = "customers/customer_form.html"
success_url = reverse_lazy("customer_list")
success_message = _("Customer created successfully")
def form_valid(self, form):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
if customer := models.Customer.objects.filter(
dealer=dealer,
email=form.instance.email
).first():
if not customer.active:
messages.error(
self.request,
_(
"Customer Account with this email is Deactivated,Please Contact Admin"
),
)
else:
messages.error(
self.request, _("Customer with this email already exists")
)
return redirect("customer_create")
dealer = get_user_type(self.request)
form.instance.dealer = dealer
# try:
# user = form.instance.create_user_model()
# logger.info(
# f"Successfully created Customer with '{user.username}' (ID: {user.id}) "
# f"with email '{user.email}' for dealer '{dealer.name}'."
# )
# except IntegrityError as e:
# if "UNIQUE constraint" in str(e):
# messages.error(self.request, _("Email already exists"))
# logger.info(
# f"Attempted to create user with existing email '{form.instance.email}' "
# f"for dealer '{dealer.name}'. Message: '{e}'"
# )
# else:
# logger.error(
# f"An unexpected IntegrityError occurred while creating user(customer) with email '{form.instance.email}' "
# f"for dealer '{dealer.name}'. Error: {e}",
# exc_info=True,
# )
# messages.error(self.request, str(e))
# return redirect("customer_create")
customer = form.instance.create_customer_model()
# form.instance.user = user
form.instance.customer_model = customer
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy(
"customer_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
)
class CustomerUpdateView(
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
):
"""
# Updates the details of an existing customer in the database. This view is
# accessible only to logged-in users with the appropriate permissions. It
# handles both GET (form rendering with pre-filled customer data) and POST
# (submitting updates) requests. Data validation and customer updates are
# conducted based on the received form data.
# :param request: The HTTP request object used to determine the request method,
# access user session details, and provide request data such as POST content.
# Expected to contain the updated customer data if request method is POST.
# :type request: HttpRequest
# :param pk: The primary key of the CustomerModel object that is to be updated.
# :type pk: int
# :return: A rendered HTML template displaying the customer form pre-filled
# with existing data if a GET request is received. On successful form
# submission (POST request), redirects to the customer list page
# and displays a success message. In case of invalid data or errors,
# returns the rendered form template with the validation errors.
# :rtype: HttpResponse
#"""
model = models.Customer
form_class = forms.CustomerForm
permission_required = ["inventory.change_customer"]
permission_required = ["inventory.change_customer"]
template_name = "customers/customer_form.html"
success_url = reverse_lazy("customer_list")
success_message = _("Customer updated successfully")
def form_valid(self, form):
# form.instance.update_user_model()
form.instance.update_customer_model()
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy(
"customer_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
)
@permission_required("inventory.delete_customer", raise_exception=True)
@login_required
def delete_customer(request, dealer_slug, slug):
"""
Deletes a customer from the system and deactivates the corresponding user account.
This function retrieves a customer object based on the primary key (pk),
sets their active status to False, and deactivates the linked user account
(using the email associated with the customer). After saving these changes,
it displays a success message to the user and redirects to the customer list page.
:param request: A HttpRequest object containing metadata about the request.
:type request: HttpRequest
:param pk: Primary key of the customer to be deleted.
:type pk: int
:return: A redirect response to the customer list page.
:rtype: HttpResponseRedirect
"""
customer = get_object_or_404(models.Customer, slug=slug)
customer.deactivate_account()
messages.success(request, _("Customer deactivated successfully"))
return redirect("customer_list", dealer_slug=dealer_slug)
class VendorListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"""
Represents a view for displaying a paginated list of vendors, accessible only
to authenticated users.
This class inherits from ``LoginRequiredMixin`` and ``ListView`` and is
designed to display a paginated list of vendors associated with a user. It
utilizes filters and search capabilities based on user input and is rendered
using a specified template. The list is ordered by the creation date of the
vendors in descending order.
:ivar model: The model that this view interacts with.
:type model: VendorModel
:ivar context_object_name: The name of the context variable used in the
template for the list of vendors.
:type context_object_name: str
:ivar paginate_by: The number of vendors to display per page in the paginated
view.
:type paginate_by: int
:ivar template_name: The path to the template used to render the list of
vendors.
:type template_name: str
:ivar ordering: The default ordering applied to the queryset. Vendors are
ordered by their creation date in descending order.
:type ordering: list
"""
model = models.Vendor
context_object_name = "vendors"
paginate_by = 30
template_name = "vendors/vendors_list.html"
permission_required = ["inventory.view_vendor"]
def get_queryset(self):
query = self.request.GET.get("q")
dealer = get_user_type(self.request)
vendors = super().get_queryset().filter(dealer=dealer, active=True)
if query:
return apply_search_filters(vendors, query)
return vendors
@login_required
@permission_required("inventory.view_vendor", raise_exception=True)
def vendorDetailView(request, dealer_slug, slug):
"""
Fetches and renders the detail view for a specific vendor.
This function retrieves a vendor object based on the primary key (pk)
provided in the URL, ensures the user is logged in to access the
view, and renders the vendor detail template with the vendor's context.
:param request: The HTTP request object.
:type request: HttpRequest
:param pk: The primary key of the vendor to retrieve.
:type pk: int
:return: An HttpResponse object containing the rendered vendor detail page.
:rtype: HttpResponse
"""
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
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)
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}
)
class VendorCreateView(
LoginRequiredMixin,
PermissionRequiredMixin,
SuccessMessageMixin,
CreateView,
):
"""
Handles the creation of a new vendor.
This view allows authenticated users to create a new vendor entry. It uses the
Vendor model and its corresponding form for creation. A success message is displayed
upon successful creation, and the user is redirected to the list of vendors.
:ivar model: The model associated with this view.
:type model: models.Vendor
:ivar form_class: The form class used for creating a vendor.
:type form_class: forms.VendorForm
:ivar template_name: The name of the template used to render the view.
:type template_name: str
:ivar success_url: The URL to redirect to after successful vendor creation.
:type success_url: str
:ivar success_message: The message displayed upon successful creation.
:type success_message: str
"""
model = models.Vendor
form_class = forms.VendorForm
template_name = "vendors/vendor_form.html"
success_message = _("Vendor created successfully")
permission_required = ["inventory.add_vendor"]
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 not vendor.active:
messages.error(
self.request,
_(
"Vendor Account with this email is Deactivated,Please Contact Admin"
),
)
else:
messages.error(self.request, _("Vendor with this email already exists"))
return redirect("vendor_create", dealer_slug=self.kwargs["dealer_slug"])
dealer = get_user_type(self.request)
form.instance.dealer = dealer
form.instance.save()
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy(
"vendor_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
)
class VendorUpdateView(
LoginRequiredMixin,
PermissionRequiredMixin,
SuccessMessageMixin,
UpdateView,
):
"""
View for updating vendor information.
This class-based view is used to handle the update of vendor information.
It ensures that only authenticated users can access this page, provides
form handling functionality, and includes a success message upon completion.
The form fields are dynamically populated and validated before vendor
information is updated in the database.
:ivar model: The model that this view is based on.
:type model: models.Vendor
:ivar form_class: The form class used to validate and process vendor data.
:type form_class: forms.VendorForm
:ivar template_name: The path to the HTML template used to render this view.
:type template_name: str
:ivar success_url: The URL to redirect to after successful data submission.
:type success_url: str
:ivar success_message: The message to display upon successful data update.
:type success_message: str
"""
model = models.Vendor
form_class = forms.VendorForm
template_name = "vendors/vendor_form.html"
success_message = _("Vendor updated successfully")
permission_required = ["inventory.change_vendor"]
# def get_initial(self):
# initial = super().get_initial()
# initial = self.object.additional_info
# return initial
def form_valid(self, form):
# instance = form.save(commit=False)
print(self.request.POST)
# instance.vendor_name = self.request.POST["name"]
# instance.vendor_number = self.request.POST["crn"]
# instance.address_1 = self.request.POST["address"]
# instance.phone = self.request.POST["phone_number"]
# instance.email = self.request.POST["email"]
# instance.tax_id_number = self.request.POST["vrn"]
# additionals = form.cleaned_data
# additionals["phone_number"] = str(additionals["phone_number"])
# instance.additional_info = additionals
# instance.save()
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy(
"vendor_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
)
@login_required
@permission_required("inventory.delete_vendor")
def delete_vendor(request, dealer_slug, slug):
"""
Deletes an existing vendor record from the database.
This function allows users with valid authentication to delete a specified
vendor object by its primary key. Upon successful deletion, a success message
is displayed, and the user is redirected to the vendor list page.
:param request: HttpRequest object containing metadata about the request.
:type request: HttpRequest
:param pk: Primary key of the vendor object to be deleted.
:type pk: int
:return: HttpResponseRedirect object for redirecting to the vendor list page.
:rtype: HttpResponseRedirect
"""
vendor = get_object_or_404(models.Vendor, slug=slug)
vendor.active = False
vendor.vendor_model.active = False
vendor.save()
messages.success(request, _("Vendor deleted successfully"))
return redirect("vendor_list", dealer_slug=dealer_slug)
# group
class GroupListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"""
Represents a view for listing groups for a logged-in user.
This view is designed for authenticated users, inheriting functionalities
from `LoginRequiredMixin` to enforce authentication checks and `ListView`
to handle the display of a list of groups. It queries the groups related
to the user type (dealer) and displays the results in a paginated format
using a specified template.
:ivar model: The model used for retrieving group data.
:type model: type
:ivar context_object_name: The name of the context variable used to contain
the queryset of groups.
:type context_object_name: str
:ivar paginate_by: The number of groups listed on each page.
:type paginate_by: int
:ivar template_name: The path to the template used for rendering the group list.
:type template_name: str
"""
model = models.CustomGroup
context_object_name = "groups"
paginate_by = 10
template_name = "groups/group_list.html"
permission_required = ["inventory.view_customgroup"]
def get_queryset(self):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
return dealer.groups.all()
class GroupDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""
Represents the detail view for a specific group.
Handles the display of detailed information about a specific CustomGroup
instance. Requires the user to be logged in to access this view. The class
is designed to fetch a specific group instance and render its details using
the specified template.
:ivar model: The model that represents the group details being viewed.
:type model: models.CustomGroup
:ivar template_name: The name of the template used to render the group detail
view.
:type template_name: str
:ivar context_object_name: The context variable name under which the group
instance will be available in the template.
:type context_object_name: str
"""
model = models.CustomGroup
template_name = "groups/group_detail.html"
context_object_name = "group"
permission_required = ["inventory.view_customgroup"]
class GroupCreateView(
LoginRequiredMixin,
PermissionRequiredMixin,
SuccessMessageMixin,
CreateView,
):
"""
Represents a view for creating a new group in the application.
This class provides a form-based interface for authenticated users to create
new group entities. It utilizes mixins to enforce login requirements and display
success messages upon successful group creation.
:ivar model: The model associated with the view.
:type model: models.CustomGroup
:ivar form_class: The form class used to create a new group.
:type form_class: forms.GroupForm
:ivar template_name: The template used to render the form for creating a group.
:type template_name: str
:ivar success_url: The URL to be redirected to when the group creation is successful.
:type success_url: str
:ivar success_message: A message displayed upon successful creation of a group.
:type success_message: str
"""
model = models.CustomGroup
form_class = forms.GroupForm
template_name = "groups/group_form.html"
success_message = _("Group created successfully")
permission_required = ["inventory.add_customgroup"]
def form_valid(self, form):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
instance = form.save(commit=False)
group_name = f"{dealer.slug}_{instance.name}"
logger.debug(
f"Attempting to create or get Django Group '{group_name}' "
f"for dealer '{dealer.name}' (ID: {dealer.id})."
)
try:
group, created = Group.objects.get_or_create(name=group_name)
if created:
logger.info(f"Successfully created new Django Group: '{group_name}'.")
else:
logger.info(
f"Django Group '{group_name}' already exists and was retrieved."
)
instance.dealer = dealer
instance.group = group
instance.save()
logger.info(
f"Successfully created CustomGroup '{instance.name}' (ID: {instance.id}) "
f"linked to Django Group '{group.name}' for dealer '{dealer.name}'."
)
except IntegrityError as e:
from django.utils.translation import gettext_lazy as _
print(e)
messages.error(self.request, _("Group name already exists"))
logger.error(
f"An unexpected IntegrityError occurred during CustomGroup creation "
f"for name '{instance.name}' (derived Django Group name '{group_name}') "
f"for dealer '{dealer.name}'. Error: {e}",
exc_info=True, # CRUCIAL: Get the full traceback for unexpected errors
)
return redirect("group_create", dealer_slug=dealer.slug)
if created:
group_manager, _ = models.CustomGroup.objects.get_or_create(
name=instance.name, dealer=dealer, group=group
)
# group_manager.set_default_permissions()
dealer.user.groups.add(group)
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("group_list", args=[self.request.dealer.slug])
class GroupUpdateView(
LoginRequiredMixin,
PermissionRequiredMixin,
SuccessMessageMixin,
UpdateView,
):
"""
Handles the update of group objects with permission control and custom behavior.
This view allows users with login credentials to update existing group
objects. It ensures that changes made to groups adhere to default
permissions and adds necessary naming conventions based on user type.
Upon successful update, it redirects the user to the group list view and
displays a success message.
:ivar model: Specifies the model to be used for the update operation.
:type model: models.CustomGroup
:ivar form_class: Specifies the form class to be used for validation
and data manipulation.
:type form_class: forms.GroupForm
:ivar template_name: File path to the template that renders the form view.
:type template_name: str
:ivar success_url: URL to redirect upon successful form submission.
:type success_url: str
:ivar success_message: Message displayed upon successful update of a group.
:type success_message: str
"""
model = models.CustomGroup
form_class = forms.GroupForm
template_name = "groups/group_form.html"
success_message = _("Group updated successfully")
permission_required = ["inventory.change_customgroup"]
def form_valid(self, form):
dealer = get_user_type(self.request)
instance = form.save(commit=False)
instance.set_default_permissions()
instance.group.name = f"{dealer.slug}_{instance.name}"
instance.save()
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("group_list", args=[self.request.dealer.slug])
@login_required
def GroupDeleteview(request, dealer_slug, pk):
"""
Handles the deletion of a specific group instance. This view ensures that only
authenticated users can perform the deletion. Upon successful deletion, a
success message is displayed, and the user is redirected to the group list page.
:param request: The HTTP request object that contains metadata about the
request context and user information. Must be an authenticated user.
:param pk: The primary key of the group instance to be deleted.
It specifies which group to retrieve and delete.
:return: The HTTP response that redirects the user to the group list page
after the group is successfully deleted.
"""
get_object_or_404(models.Dealer, slug=dealer_slug)
group = get_object_or_404(models.CustomGroup, pk=pk)
group.delete()
messages.success(request, _("Group deleted successfully"))
return redirect("group_list", dealer_slug=dealer_slug)
# @login_required
# def GroupPermissionView(request, dealer_slug, pk):
# # Verify dealer and group exist
# get_object_or_404(models.Dealer, slug=dealer_slug)
# customgroup = get_object_or_404(models.CustomGroup, pk=pk)
# if request.method == "POST":
# form = forms.PermissionForm(request.POST, instance=customgroup)
# if form.is_valid():
# # Clear existing permissions
# customgroup.clear_permissions()
# # Add new permissions from form
# permissions = form.cleaned_data.get('permissions', [])
# for permission in permissions:
# customgroup.add_permission(permission)
# messages.success(request, _("Permissions updated successfully"))
# return redirect("group_detail", dealer_slug=dealer_slug, pk=customgroup.pk)
# else:
# # Initial form with current permissions
# form = forms.PermissionForm(instance=customgroup)
# group_permission_ids = set(customgroup.permissions.values_list('id', flat=True))
# # Mark permissions as checked in the form data
# for app_label, model in form.grouped_permissions.items():
# print(app_label, model)
# for mo, perms in model.items():
# for perm in perms:
# perm.is_checked = perm.id in group_permission_ids
# return render(request,"groups/group_permission_form.html", {
# "group": customgroup,
# "form": form,
# "group_permission_apps": set(customgroup.group.permissions.values_list('content_type__app_label', flat=True)),
# "group_permission_models": set(customgroup.group.permissions.values_list('content_type__model', flat=True))
# })
@login_required
@permission_required("inventory.change_customgroup")
def GroupPermissionView(request, dealer_slug, pk):
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from django.db.models import Q
get_object_or_404(models.Dealer, slug=dealer_slug)
customgroup = get_object_or_404(models.CustomGroup, pk=pk)
group = customgroup.group
# Define ALL permissions you want to manage
MODEL_LIST = [
("inventory", "car"),
# ("inventory", "carfinance"),
("inventory", "carlocation"),
("inventory", "customcard"),
("inventory", "cartransfer"),
("inventory", "carcolors"),
("inventory", "carequipment"),
("inventory", "interiorcolors"),
("inventory", "exteriorcolors"),
("inventory", "lead"),
("inventory", "customgroup"),
("inventory", "saleorder"),
("inventory", "payment"),
("inventory", "staff"),
("inventory", "schedule"),
("inventory", "activity"),
("inventory", "opportunity"),
("inventory", "carreservation"),
("inventory", "customer"),
("inventory", "organization"),
("inventory", "additionalservices"),
("inventory", "notes"),
("inventory", "tasks"),
("inventory", "activity"),
("inventory", "vendor"),
("inventory", "poitemsuploaded"),
("django_ledger", "purchaseordermodel"),
("django_ledger", "bankaccountmodel"),
("django_ledger", "estimatemodel"),
("django_ledger", "accountmodel"),
("django_ledger", "chartofaccountmodel"),
("django_ledger", "billmodel"),
("django_ledger", "itemmodel"),
("django_ledger", "invoicemodel"),
("django_ledger", "vendormodel"),
("django_ledger", "journalentrymodel"),
("django_ledger", "ledgermodel"),
("django_ledger", "transactionmodel"),
]
CUSTOM_PERMISSIONS = [
("django_ledger", "can_approve_estimatemodel"),
("django_ledger", "can_approve_billmodel"),
]
if request.method == "POST":
# Get user info for logging
user_id = request.user.id if request.user.is_authenticated else "Anonymous"
user_username = (
request.user.username if request.user.is_authenticated else "anonymous"
)
try:
selected_ids = [int(id) for id in request.POST.getlist("permissions", [])]
# Get content types for model permissions
model_content_types = ContentType.objects.filter(
app_label__in=[m[0] for m in MODEL_LIST],
model__in=[m[1] for m in MODEL_LIST],
)
# Get all valid permissions (model CRUD + custom)
valid_perms = Permission.objects.filter(
# Model CRUD permissions
Q(content_type__in=model_content_types)
|
# Custom permissions
Q(
content_type__app_label__in=[p[0] for p in CUSTOM_PERMISSIONS],
codename__in=[p[1] for p in CUSTOM_PERMISSIONS],
),
id__in=selected_ids,
)
with transaction.atomic():
group.permissions.clear()
if valid_perms.exists():
group.permissions.add(*valid_perms)
# --- Logging for successful permission update ---
logger.info(
f"Permissions for Group '{group.name}' (ID: {group.id}) "
f"successfully updated by user {user_username} (ID: {user_id}). "
f"Total permissions granted: {valid_perms.count()}."
)
messages.success(request, _("Permissions updated successfully"))
return redirect("group_detail", dealer_slug=dealer_slug, pk=customgroup.pk)
except Exception as e:
# --- Logging for error during permission update ---
logger.error(
f"Error updating permissions for Group '{group.name}' (ID: {group.id}) "
f"by user {user_username} (ID: {user_id}). Error: {e}",
exc_info=True, # CRUCIAL: Includes the full traceback
)
messages.error(request, _("Error updating permissions: ") + str(e))
# GET request handling
# Get permissions for models (CRUD)
model_perms = Permission.objects.filter(
content_type__in=ContentType.objects.filter(
app_label__in=[m[0] for m in MODEL_LIST],
model__in=[m[1] for m in MODEL_LIST],
)
)
# Get custom permissions
custom_perms = Permission.objects.filter(
content_type__app_label__in=[p[0] for p in CUSTOM_PERMISSIONS],
codename__in=[p[1] for p in CUSTOM_PERMISSIONS],
)
# Combine all permissions
all_permissions = model_perms | custom_perms
all_permissions = all_permissions.select_related("content_type").order_by(
"content_type__app_label", "content_type__model", "codename"
)
# Group permissions with custom ones in a special section
grouped_permissions = {}
for perm in all_permissions:
app_label = perm.content_type.app_label
# Check if this is a custom permission
is_custom = any(
p[0] == app_label and p[1] == perm.codename for p in CUSTOM_PERMISSIONS
)
if is_custom:
# Group custom permissions under "Custom" model name
model = "Custom"
else:
model = perm.content_type.model
if app_label not in grouped_permissions:
grouped_permissions[app_label] = {}
if model not in grouped_permissions[app_label]:
grouped_permissions[app_label][model] = []
grouped_permissions[app_label][model].append(perm)
# Get currently assigned permission IDs
group_permission_ids = set(
group.permissions.filter(
id__in=all_permissions.values_list("id", flat=True)
).values_list("id", flat=True)
)
return render(
request,
"groups/group_permission_form.html",
{
"group": customgroup,
"grouped_permissions": grouped_permissions,
"group_permission_ids": group_permission_ids,
"group_permission_apps": set(
group.permissions.filter(
content_type__app_label__in=grouped_permissions.keys()
).values_list("content_type__app_label", flat=True)
),
"group_permission_models": set(
group.permissions.filter(
content_type__app_label__in=grouped_permissions.keys()
).values_list("content_type__model", flat=True)
),
},
)
# if request.method == "POST":
# try:
# selected_ids = [int(id) for id in request.POST.getlist('permissions', [])]
# # Get all permission types for our models
# content_types = ContentType.objects.filter(
# app_label__in=[m[0] for m in MODEL_LIST],
# model__in=[m[1] for m in MODEL_LIST]
# )
# # Get valid permissions that exist for these models
# valid_perms = Permission.objects.filter(
# content_type__in=content_types,
# id__in=selected_ids
# )
# # Atomic transaction to ensure data consistency
# with transaction.atomic():
# # Clear current permissions
# group.permissions.clear()
# # Add new permissions if any were selected
# if valid_perms.exists():
# group.permissions.add(*valid_perms)
# messages.success(request, _("Permissions updated successfully"))
# return redirect("group_detail", dealer_slug=dealer_slug, pk=customgroup.pk)
# except Exception as e:
# messages.error(request, _("Error updating permissions: ") + str(e))
# # GET request handling
# content_types = ContentType.objects.filter(
# app_label__in=[m[0] for m in MODEL_LIST],
# model__in=[m[1] for m in MODEL_LIST]
# )
# # Get all permissions for these content types
# all_permissions = Permission.objects.filter(
# content_type__in=content_types
# ).select_related('content_type').order_by('content_type__app_label', 'content_type__model', 'codename')
# # Group permissions by app and model
# grouped_permissions = {}
# for perm in all_permissions:
# app_label = perm.content_type.app_label
# model = perm.content_type.model
# if app_label not in grouped_permissions:
# grouped_permissions[app_label] = {}
# if model not in grouped_permissions[app_label]:
# grouped_permissions[app_label][model] = []
# grouped_permissions[app_label][model].append(perm)
# # Get currently assigned permission IDs
# group_permission_ids = set(
# group.permissions.filter(
# content_type__in=content_types
# ).values_list('id', flat=True)
# )
# return render(request, "groups/group_permission_form.html", {
# "group": customgroup,
# "grouped_permissions": grouped_permissions,
# "group_permission_ids": group_permission_ids,
# "group_permission_apps": set(
# group.permissions.filter(
# content_type__app_label__in=[m[0] for m in MODEL_LIST]
# ).values_list('content_type__app_label', flat=True)
# ),
# "group_permission_models": set(
# group.permissions.filter(
# content_type__model__in=[m[1] for m in MODEL_LIST]
# ).values_list('content_type__model', flat=True)
# )
# })
# Users
@login_required
@permission_required("inventory.change_staff")
def UserGroupView(request, dealer_slug, slug):
"""
Handles the assignment of user groups to a specific staff member. This view
allows updating the groups a staff member belongs to via a form submission.
It processes both GET and POST requests, ensuring appropriate group
assignments are managed and feedback is provided to the user via messages.
:param request: HttpRequest object representing the HTTP request.
:type request: HttpRequest
:param pk: Primary key of the staff member whose groups are being updated.
:type pk: int
:return: Renders the user group form for GET requests or redirects to the
user detail page after successful submission for POST requests.
:rtype: HttpResponse or HttpResponseRedirect
"""
get_object_or_404(models.Dealer, slug=dealer_slug)
staff = get_object_or_404(models.Staff, slug=slug)
if request.method == "POST":
form = forms.UserGroupForm(request.POST)
groups = request.POST.getlist("name")
staff.clear_groups()
for i in groups:
cg = models.CustomGroup.objects.get(id=int(i))
staff.add_group(cg.group)
messages.success(request, _("Group added successfully"))
return redirect("user_detail", dealer_slug=dealer_slug, slug=staff.slug)
form = forms.UserGroupForm(initial={"name": staff.groups})
form.fields["name"].queryset = models.CustomGroup.objects.filter(
dealer=staff.dealer
)
return render(request, "users/user_group_form.html", {"staff": staff, "form": form})
class UserListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"""
Represents a view for listing users with pagination and filters.
This class is designed to display a list view of staff users for a specific
dealer. It supports pagination and search filtering functionality. This view
requires the user to be logged in, as it inherits from `LoginRequiredMixin`.
:ivar model: The model that the view will work with.
:type model: Type[models.Staff]
:ivar context_object_name: The name of the context variable that will contain
the list of users.
:type context_object_name: str
:ivar paginate_by: The number of items to display per page.
:type paginate_by: int
:ivar template_name: The path to the template used for rendering the view's
page.
:type template_name: str
"""
model = models.Staff
context_object_name = "users"
paginate_by = 10
template_name = "users/user_list.html"
permission_required = ["inventory.view_staff"]
def get_queryset(self):
query = self.request.GET.get("q")
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
staff = models.Staff.objects.filter(dealer=dealer, active=True).all()
return apply_search_filters(staff, query)
def get_context_data(self, **kwargs):
context=super().get_context_data(**kwargs)
context['no_staff_message']=_("staff")
return context
class UserDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""
Represents a detailed view for displaying user-specific information.
This class-based view is used to display detailed information for
a specific user. It ensures that only logged-in users can access
this view, leveraging the ``LoginRequiredMixin`` for authentication.
The data for the view comes from the ``models.Staff`` model and
is rendered using the specified HTML template.
:ivar model: The data model associated with this view.
:type model: models.Staff
:ivar template_name: Path to the HTML template used for rendering the view.
:type template_name: str
:ivar context_object_name: Name of the context variable available in the template.
:type context_object_name: str
"""
model = models.Staff
template_name = "users/user_detail.html"
context_object_name = "user_"
permission_required = ["inventory.view_staff"]
class UserCreateView(
LoginRequiredMixin,
PermissionRequiredMixin,
SuccessMessageMixin,
CreateView,
):
"""
Manages the creation of a new staff user and their associated details.
This view is responsible for handling the creation of a new staff user in the
system. It ensures quota limits are adhered to, validates form input, saves user
details, and associates them with services and permissions.
:ivar model: The database model associated with this view.
:type model: django.db.models.Model
:ivar form_class: The form class used to validate and create a staff user.
:type form_class: type
:ivar template_name: The template used to render the user creation form.
:type template_name: str
:ivar success_url: The URL to redirect to after successfully creating a user.
:type success_url: str
:ivar success_message: The success message displayed upon user creation.
:type success_message: str
"""
model = models.Staff
form_class = forms.StaffForm
template_name = "users/user_form.html"
success_url = reverse_lazy("user_list")
success_message = _("User created successfully")
permission_required = ["inventory.add_staff"]
staff_pk = None
def get_form(self, form_class=None):
form = super().get_form(form_class)
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
form.fields["group"].queryset = models.CustomGroup.objects.filter(dealer=dealer)
return form
def form_valid(self, form):
staff = form.save(commit=False)
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
if dealer.is_staff_exceed_quota_limit:
messages.error(
self.request,
_(
"You have reached the maximum number of staff users allowed for your plan"
),
)
return self.form_invalid(form)
email = form.cleaned_data["email"]
if models.Staff.objects.filter(user__email=email).exists():
messages.error(
self.request,
_(
"A user with this email already exists. Please use a different email."
),
)
return redirect("user_create", dealer_slug=dealer.slug)
password = "Tenhal@123"
user, created = User.objects.get_or_create(
email=email,
defaults={
"first_name": staff.first_name,
"last_name": staff.last_name,
"username": email,
"password": password,
},
)
user.is_staff = True
user.save()
# staff_member, _ = StaffMember.objects.get_or_create(user=user)
# for service in form.cleaned_data["service_offered"]:
# staff_member.services_offered.add(service)
staff.user = user
staff.dealer = dealer
staff.save()
self.staff_pk = staff.pk
for customgroup in form.cleaned_data["group"]:
staff.add_group(customgroup.group)
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("user_list", args=[self.request.dealer.slug])
class UserUpdateView(
LoginRequiredMixin,
PermissionRequiredMixin,
SuccessMessageMixin,
UpdateView,
):
"""
UserUpdateView updates information for a user with specific details.
This view handles updating user details such as email, name, phone number, and services
offered. It leverages Django's `UpdateView` to manage the update operation. The view
disables editing of email addresses, initializes specific fields with data associated
with the user, and processes input to manage related services. Validation of the form and
saving changes are customized to align with specific business logic.
:ivar model: The model class associated with the view, used for retrieving the object to update.
:type model: models.Staff
:ivar form_class: The form class used to render and process the form for updating the user.
:type form_class: forms.StaffForm
:ivar template_name: The template used for rendering the form in the UI.
:type template_name: str
:ivar success_url: URL to redirect to after a successful form submission.
:type success_url: str
:ivar success_message: Message displayed to the user after a successful update.
:type success_message: str
"""
model = models.Staff
form_class = forms.StaffForm
template_name = "users/user_form.html"
success_url = reverse_lazy("user_list")
success_message = _("User updated successfully")
permission_required = ["inventory.change_staff"]
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["instance"] = self.get_object() # Pass the Staff instance to the form
return kwargs
def get_form(self, form_class=None):
form = super().get_form(form_class)
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
form.fields["group"].queryset = models.CustomGroup.objects.filter(dealer=dealer)
form.fields["email"].disabled = True
return form
def get_initial(self):
initial = super().get_initial()
initial["email"] = self.object.user.email
initial["group"] = self.object.groups
return initial
def form_valid(self, form):
# services = form.cleaned_data["service_offered"]
# if not services:
# self.object.staff_member.services_offered.clear()
# else:
# for service in services:
# self.object.staff_member.services_offered.add(service)
staff = form.save(commit=False)
# staff.name = form.cleaned_data["name"]
staff.arabic_name = form.cleaned_data["arabic_name"]
staff.phone_number = form.cleaned_data["phone_number"]
for customgroup in form.cleaned_data["group"]:
staff.add_group(customgroup.group, True)
staff.save()
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("user_list", args=[self.request.dealer.slug])
@login_required
@permission_required("inventory.delete_staff", login_url="login")
def UserDeleteview(request, dealer_slug, slug):
"""
Deletes a user and its associated staff member from the database and redirects
to the user list page. Displays a success message upon successful deletion
of the user.
:param request: The HTTP request object representing the incoming request.
:param pk: The primary key (ID) of the staff member to be deleted.
:return: An HTTP redirect to the user list page.
"""
get_object_or_404(models.Dealer, slug=dealer_slug)
staff = get_object_or_404(models.Staff, slug=slug)
staff.deactivate_account()
messages.success(request, _("User deleted successfully"))
return redirect("user_list", dealer_slug=dealer_slug)
class OrganizationListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"""
Represents a view to display a paginated list of organizations for a dealer.
This class inherits from `LoginRequiredMixin` to ensure that only
authenticated users can access the list, and from `ListView` to provide
a generic implementation to render lists of database objects. It is designed
specifically to show organizations related to a dealer entity and includes
search functionality based on a query parameter.
:ivar model: Specifies the model to fetch data from.
:type model: type[CustomerModel]
:ivar template_name: The template used to render the organization list page.
:type template_name: str
:ivar context_object_name: The name of the context variable for the organization list.
:type context_object_name: str
:ivar paginate_by: The number of organizations displayed per page.
:type paginate_by: int
"""
model = models.Organization
template_name = "organizations/organization_list.html"
context_object_name = "organizations"
paginate_by = 20
permission_required = ["inventory.view_organization"]
def get_queryset(self):
query = self.request.GET.get("q")
dealer = get_user_type(self.request)
organization = dealer.organizations.filter(active=True)
return apply_search_filters(organization, query)
def get_context_data(self, **kwargs):
context=super().get_context_data(**kwargs)
context["empty_state_value"]=_("organization")
return context
class OrganizationDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""
Handles displaying detailed information about an organization.
This view is a detailed representation of an individual organization, which
provides the necessary data for templates to render the information. It is
intended to be used for displaying details of a `CustomerModel` instance.
Requires the user to be logged in to access this view.
:ivar model: Specifies the model that this view will interact with.
:type model: type[CustomerModel]
:ivar template_name: The name of the template to be used for rendering the
organization's detail information.
:type template_name: str
:ivar context_object_name: The context variable name to be used in the
template for accessing the organization's data.
:type context_object_name: str
"""
model = models.Organization
template_name = "organizations/organization_detail.html"
context_object_name = "organization"
permission_required = ["inventory.view_organization"]
class OrganizationCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
"""
# Handles the creation of a new organization via a web form. This view allows the
# authenticated user to submit data for creating an organization. If a POST request
# is received, it validates the data, checks for duplicate organizations, and
# creates a customer linked to the organization, including its associated
# information such as address, phone number, and logo. Upon success, the user
# is redirected to the organization list, and a success message is displayed.
# :param request: The HTTP request object containing data for creating an organization.
# :type request: HttpRequest
# :return: An HTTP response object rendering the organization create form page or
# redirecting the user after a successful creation.
# :rtype: HttpResponse
#"""
model = models.Organization
form_class = forms.OrganizationForm
permission_required = ["inventory.add_organization"]
template_name = "organizations/organization_form.html"
success_url = reverse_lazy("organization_list")
success_message = _("Organization created successfully")
def form_valid(self, form):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
if organization := models.Organization.objects.filter(
dealer=dealer,
email=form.instance.email
).first():
if not organization.active:
messages.error(
self.request,
_(
"Organization Account with this email is Deactivated,Please Contact Admin"
),
)
else:
messages.error(
self.request, _("Organization with this email already exists")
)
return redirect("organization_create")
dealer = get_user_type(self.request)
form.instance.dealer = dealer
# user = form.instance.create_user_model()
customer = form.instance.create_customer_model()
# form.instance.user = user
form.instance.customer_model = customer
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy(
"organization_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
)
class OrganizationUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
"""
# Handles the update of an organization instance. This view fetches the organization
# based on the provided primary key (pk) and renders a form for editing the
# organization attributes. When a POST request is made, this view validates and
# processes the form data, updates the organization instance, and saves the changes.
# If the request method is not POST, it initializes the form with existing organization
# data for rendering.
# :param request: The HTTP request object. Must be authenticated via login.
# :type request: HttpRequest
# :param pk: The primary key of the organization to be updated.
# :type pk: int
# :return: An HTTP response object. Either renders the organization form or redirects
# to the organization list upon successful update.
# :rtype: HttpResponse
#"""
model = models.Organization
form_class = forms.OrganizationForm
permission_required = ["inventory.change_organization"]
template_name = "organizations/organization_form.html"
success_url = reverse_lazy("organization_list")
success_message = _("Organization updated successfully")
def form_valid(self, form):
# form.instance.update_user_model()
form.instance.update_customer_model()
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy(
"organization_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
)
@login_required
@permission_required("inventory.delete_organization")
def OrganizationDeleteView(request, dealer_slug, slug):
"""
Handles the deletion of an organization based on the provided primary key (pk). Looks up
the organization and its corresponding user by email, attempts to delete both, and provides
appropriate success or error feedback to the user. In case of failure, an error message is shown,
while successful deletion redirects to the organization list.
:param request: The HTTP request object containing metadata about the request.
:type request: HttpRequest
:param pk: The primary key of the organization to be deleted.
:type pk: int
:return: An HTTP response redirecting to the organization list view.
:rtype: HttpResponseRedirect
"""
organization = get_object_or_404(models.Organization, slug=slug)
organization.deactivate_account()
messages.success(request, _("Organization Deactivated successfully"))
return redirect(
reverse_lazy("organization_list", kwargs={"dealer_slug": dealer_slug})
)
class RepresentativeListView(LoginRequiredMixin, ListView):
"""
Represents a view for displaying a paginated list of representatives.
This view handles the functionality of displaying and paginating a list
of representatives for the logged-in user. It utilizes search filters to
allow querying representatives based on the search term provided in the
request. The view restricts access to logged-in users only.
:ivar model: The model associated with this view.
:type model: models.Representative
:ivar template_name: Name of the template used to render the view.
:type template_name: str
:ivar context_object_name: Name of the context variable used to access
representatives in the template.
:type context_object_name: str
:ivar paginate_by: The number of representatives displayed per page.
:type paginate_by: int
"""
model = models.Representative
template_name = "representatives/representative_list.html"
context_object_name = "representatives"
paginate_by = 10
def get_queryset(self):
query = self.request.GET.get("q")
dealer = get_user_type(self.request)
representative = models.Representative.objects.filter(dealer=dealer)
return apply_search_filters(representative, query)
class RepresentativeDetailView(LoginRequiredMixin, DetailView):
"""
Represents a detailed view for Representative instances.
This class-based view is used to provide detailed representation for
``Representative`` model instances. It ensures that only authenticated
users can access the view by utilizing ``LoginRequiredMixin``. The
template used to render the view and the context name for the object
are also specified for use in a Django template.
:ivar model: The model associated with this view.
:type model: Type of the model (models.Representative)
:ivar template_name: The path to the template used to render this view.
:type template_name: str
:ivar context_object_name: The name of the context variable containing
the object.
:type context_object_name: str
"""
model = models.Representative
template_name = "representatives/representative_detail.html"
context_object_name = "representative"
class RepresentativeCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
"""
Handles the creation of a Representative object.
This class is a view that provides the interface and functionality to create
a new representative in the application. It is designed to ensure that only
authenticated users with a valid dealer association can create representatives.
A success message is displayed upon successful creation of a representative.
:ivar model: The model that this view will work with, which is Representative.
:type model: django.db.models.Model
:ivar form_class: The form class used for creating a representative.
:type form_class: django.forms.ModelForm
:ivar template_name: The name of the template used to render the create view.
:type template_name: str
:ivar success_url: The URL to redirect to upon successful form submission.
:type success_url: str
:ivar success_message: The success message displayed after creating a representative.
:type success_message: str
"""
model = models.Representative
form_class = forms.RepresentativeForm
template_name = "representatives/representative_form.html"
success_url = reverse_lazy("representative_list")
success_message = _("Representative created successfully")
def form_valid(self, form):
if form.is_valid():
form.instance.dealer = self.request.dealer
form.save()
return super().form_valid(form)
else:
return form.errors
class RepresentativeUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
"""
Provides functionality for updating a representative's details.
This class-based view allows authenticated users, equipped with
the required permissions, to update information for a specific
representative. It uses a predefined form for input, renders the
appropriate update template, and provides success messaging upon
successful completion. It also redirects to a predefined success
URL once the update operation is complete.
:ivar model: The model representing a representative that is being
updated.
:type model: Type[models.Representative]
:ivar form_class: The form class used for providing input fields to
update representative details.
:type form_class: Type[forms.RepresentativeForm]
:ivar template_name: The template used to render the representative
update page.
:type template_name: str
:ivar success_url: The URL to which the user is redirected following
a successful update.
:type success_url: str
:ivar success_message: The message displayed upon a successful update
operation.
:type success_message: str
"""
model = models.Representative
form_class = forms.RepresentativeForm
template_name = "representatives/representative_form.html"
success_url = reverse_lazy("representative_list")
success_message = _("Representative updated successfully")
class RepresentativeDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
"""
Handles the deletion of a representative.
This view provides functionality to delete a representative from the system.
It ensures that only authenticated users can perform the deletion and displays
a success message upon successful deletion. The deletion is confirmed via a
template, and upon success, the user is redirected to the representative list page.
:ivar model: The model representing the representative.
:type model: models.Representative
:ivar template_name: The template used to confirm the deletion of a representative.
:type template_name: str
:ivar success_url: The URL to redirect to after successful deletion.
:type success_url: str
:ivar success_message: The success message displayed after a representative is deleted.
:type success_message: str
"""
model = models.Representative
template_name = "representatives/representative_confirm_delete.html"
success_url = reverse_lazy("representative_list")
success_message = _("Representative deleted successfully")
# BANK ACCOUNT
class BankAccountListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"""
Provides a view for listing bank accounts associated with a specific entity
and applying search filters if provided. Ensures user authentication and
required permissions are implemented.
This view is used to display the list of bank accounts in a paginated format.
It filters the list of bank accounts based on the associated entity of the
dealer (user type) and applies search filters when needed. It requires the
user to be logged in and have the specified permissions to view the resource.
:ivar model: The model to fetch data for the bank account listing.
:type model: type[BankAccountModel]
:ivar template_name: The template used to render the bank account list.
:type template_name: str
:ivar context_object_name: The name of the context variable containing the
list of bank accounts.
:type context_object_name: str
:ivar paginate_by: The number of records displayed per page in pagination.
:type paginate_by: int
:ivar permission_required: The required permissions to access the view.
:type permission_required: list[str]
"""
model = BankAccountModel
template_name = "ledger/bank_accounts/bank_account_list.html"
context_object_name = "bank_accounts"
paginate_by = 30
permission_required = ["django_ledger.view_bankaccountmodel"]
def get_queryset(self):
query = self.request.GET.get("q")
dealer = self.request.dealer
bank_accounts = BankAccountModel.objects.filter(entity_model=dealer.entity)
return apply_search_filters(bank_accounts, query)
class BankAccountCreateView(
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView
):
"""
Represents a view for creating a new bank account.
This class is a Django CreateView that handles the creation of a new bank account
within the system. It integrates functionalities for login requirements, permission
enforcement, and success messages upon successful creation. The view is initialized
with a specific model, form class, and template to render the form. A success message
and redirection URL are specified for after the account creation. Additionally, it
checks permissions required to access this view.
:ivar model: The model to be used for the creation of a bank account.
:type model: BankAccountModel
:ivar form_class: The form class to be used for validating and handling bank account
creation.
:type form_class: BankAccountCreateForm
:ivar template_name: The template to render the form for bank account creation.
:type template_name: str
:ivar success_url: The URL to redirect to after successful bank account creation.
:type success_url: str
:ivar success_message: The success message to display upon successful creation.
:type success_message: str
:ivar permission_required: List of permissions required to access the view.
:type permission_required: list
"""
model = BankAccountModel
form_class = BankAccountCreateForm
template_name = "ledger/bank_accounts/bank_account_form.html"
success_message = _("Bank account created successfully")
permission_required = ["django_ledger.view_bankaccountmodel"]
def form_valid(self, form):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
form.instance.entity_model = dealer.entity
return super().form_valid(form)
def get_form_kwargs(self):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
entity = dealer.entity
kwargs = super().get_form_kwargs()
kwargs["entity_slug"] = entity.slug
kwargs["user_model"] = entity.admin
return kwargs
def get_form(self, form_class=None):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
form = super().get_form(form_class)
account_qs = dealer.entity.get_all_accounts().filter(
role__in=[
roles.ASSET_CA_CASH,
roles.LIABILITY_CL_ACC_PAYABLE,
roles.LIABILITY_LTL_MORTGAGE_PAYABLE,
]
)
form.fields["account_model"].queryset = account_qs
return form
def get_success_url(self):
return reverse_lazy(
"bank_account_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
)
class BankAccountDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""
Manages the detailed view of a bank account.
Provides a detailed view for a specific bank account by leveraging Django's
DetailView. This class ensures that only authenticated users with the relevant
permissions can access the view. It is tailored for displaying necessary
details about the bank account.
:ivar model: The model associated with the view.
:type model: type[BankAccountModel]
:ivar template_name: Path to the HTML template used to render the view.
:type template_name: str
:ivar context_object_name: The name of the context variable used to represent
the specific bank account instance in the template.
:type context_object_name: str
:ivar permission_required: List of permissions required to access the view.
:type permission_required: list[str]
"""
model = BankAccountModel
template_name = "ledger/bank_accounts/bank_account_detail.html"
context_object_name = "bank_account"
permission_required = ["django_ledger.view_bankaccountmodel"]
def get_queryset(self):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
query = self.request.GET.get("q")
qs = self.model.objects.filter(entity_model=dealer.entity)
if query:
qs = apply_search_filters(qs, query)
return qs
return qs
class BankAccountUpdateView(
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
):
"""
Represents a view for updating bank account details in the system.
This class is responsible for providing functionality to update existing
bank account information within the system. It ensures that only logged-in
users with the appropriate permissions can access and update the data. The
view also provides user feedback regarding successful updates using a
success message and redirects to the bank account list upon completion.
:ivar model: Defines the model associated with the view.
:type model: BankAccountModel
:ivar form_class: Specifies the form class used for updating bank account information.
:type form_class: BankAccountUpdateForm
:ivar template_name: Path to the template used for rendering the update form.
:type template_name: str
:ivar success_url: URL to redirect to upon successful update of a bank account.
:type success_url: str
:ivar success_message: Message displayed to the user upon successful update.
:type success_message: str
:ivar permission_required: List of permissions required to access the update view.
:type permission_required: list
"""
model = BankAccountModel
form_class = BankAccountUpdateForm
template_name = "ledger/bank_accounts/bank_account_form.html"
success_message = _("Bank account updated successfully")
permission_required = ["django_ledger.view_bankaccountmodel"]
def get_form_kwargs(self):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
entity = dealer.entity
kwargs = super().get_form_kwargs()
kwargs["entity_slug"] = entity.slug
kwargs["user_model"] = entity.admin
return kwargs
def get_form(self, form_class=None):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
form = super().get_form(form_class)
account_qs = dealer.entity.get_all_accounts().filter(
role__in=[
roles.ASSET_CA_CASH,
roles.LIABILITY_CL_ACC_PAYABLE,
roles.LIABILITY_LTL_MORTGAGE_PAYABLE,
]
)
form.fields["account_model"].queryset = account_qs
return form
def get_success_url(self):
return reverse_lazy(
"bank_account_detail",
kwargs={"dealer_slug": self.kwargs["dealer_slug"], "pk": self.object.pk},
)
@login_required
@permission_required("django_ledger.delete_bankaccountmodel", raise_exception=True)
def bank_account_delete(request, dealer_slug, pk):
"""
Delete a bank account entry from the database.
This view handles the deletion of a bank account record specified by its
primary key (pk). It renders a deletion confirmation page and processes the
deletion if the request method is POST. Upon successful deletion, the user is
redirected to the list of bank accounts and a success message is displayed.
:param request: The HTTP request object representing the client's request.
It contains data such as request type (GET or POST) and session
information.
:type request: HttpRequest
:param pk: The primary key of the bank account model instance to be deleted.
:type pk: int
:return: Returns an HttpResponse object. This can be an HTTP redirect to the
bank account list page upon successful deletion, or an HTML response
rendering the confirmation template if accessed via GET.
:rtype: HttpResponse
"""
get_object_or_404(models.Dealer, slug=dealer_slug)
bank_account = get_object_or_404(BankAccountModel, pk=pk)
bank_account.delete()
messages.success(request, _("Bank account deleted successfully"))
return redirect("bank_account_list", dealer_slug=dealer_slug)
# Accounts
class AccountListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"""
View for displaying a list of accounts.
AccountListView is responsible for displaying a paginated list of accounts
that users can view based on their permissions. This view is restricted
to users who are logged in and have the required permissions. It also
provides functionality to filter and search the available accounts.
:ivar model: The Django model used for this view. Determines which
database records are displayed in the account list.
:type model: type[Model]
:ivar template_name: Path to the template file used for rendering the
account list view.
:type template_name: str
:ivar context_object_name: Name of the variable containing the accounts
list passed to the template.
:type context_object_name: str
:ivar paginate_by: Number of accounts to display per page.
:type paginate_by: int
:ivar permission_required: Permissions required to access this view.
:type permission_required: list[str]
"""
model = AccountModel
template_name = "ledger/coa_accounts/account_list.html"
context_object_name = "accounts"
permission_required = ["django_ledger.view_accountmodel"]
def get_queryset(self):
query = self.request.GET.get("q")
dealer = get_user_type(self.request)
coa = ChartOfAccountModel.objects.get(entity=dealer.entity,pk=self.kwargs["coa_pk"]) or self.request.entity.get_default_coa()
accounts = coa.get_coa_accounts()
return apply_search_filters(accounts, query)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["url_kwargs"] = self.kwargs
context["coa_pk"] = self.kwargs["coa_pk"]
return context
class AccountCreateView(
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView
):
"""
View for creating an account in the ledger system.
This class provides functionality for rendering a form to create a new account,
validating the form, setting default account properties based on the current
user's entity, and saving the new account. It is designed to ensure that only
authorized users with the required permissions can create accounts. The view
also provides feedback to the user upon successful account creation.
:ivar model: Defines the model associated with this view. In this case, the model
represents accounts in the ledger system.
:type model: type
:ivar form_class: Defines the form class used for creating new accounts. This
ensures data validation and captures input for new account creation.
:type form_class: type
:ivar template_name: Specifies the template used to render the account creation form.
:type template_name: str
:ivar success_url: URL to which the user is redirected upon successful account creation.
:type success_url: str
:ivar success_message: Feedback message displayed to the user upon successfully
creating an account.
:type success_message: str
:ivar permission_required: List of permissions required to access this view. Enforces
the permission checking to prevent unauthorized access.
:type permission_required: list
"""
model = AccountModel
form_class = AccountModelCreateForm
template_name = "ledger/coa_accounts/account_form.html"
success_message = _("Account created successfully")
permission_required = ["django_ledger.add_accountmodel"]
def form_valid(self, form):
dealer = get_user_type(self.request)
coa = ChartOfAccountModel.objects.get(entity=dealer.entity,pk=self.kwargs["coa_pk"]) or self.request.entity.get_default_coa()
form.instance.entity_model = dealer.entity
form.instance.coa_model = coa
form.instance.depth = 0
form.instance.path = form.instance.code
return super().form_valid(form)
def get_form_kwargs(self):
dealer = get_user_type(self.request)
kwargs = super().get_form_kwargs()
coa = ChartOfAccountModel.objects.get(entity=dealer.entity,pk=self.kwargs["coa_pk"]) or self.request.entity.get_default_coa()
kwargs["coa_model"] = coa
return kwargs
def get_form(self, form_class=None):
form = super().get_form(form_class)
entity = get_user_type(self.request).entity
coa = ChartOfAccountModel.objects.get(entity=entity,pk=self.kwargs["coa_pk"]) or self.request.entity.get_default_coa()
form.initial["coa_model"] = coa
return form
def get_success_url(self):
return reverse(
"account_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"], "coa_pk": self.kwargs["coa_pk"]}
)
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:
context["coa_model"] = ChartOfAccountModel.objects.get(entity=self.request.entity,pk=coa_pk)
except Exception:
context["coa_model"] = self.request.entity.get_default_coa()
return context
class AccountDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""
Represents the detailed view for an account with additional context data related to account
transactions and permissions.
This class provides a detailed view for an account in the system. It includes functionality
for generating contextual data, managing permissions, and customizing rendering templates.
The view calculates total debits, credits, and provides transaction details for the account.
:ivar model: The Django model class representing the account data.
:type model: Type[AccountModel]
:ivar template_name: The path to the template used to render this view.
:type template_name: str
:ivar context_object_name: The context variable name representing the account object.
:type context_object_name: str
:ivar slug_field: The field in the model used to retrieve the account instance based on a slug.
:type slug_field: str
:ivar DEFAULT_TXS_DAYS: Default number of days to filter transactions.
:type DEFAULT_TXS_DAYS: int
:ivar permission_required: Permissions required to access this view.
:type permission_required: list[str]
:ivar extra_context: Additional context data passed to the template.
:type extra_context: dict
"""
model = AccountModel
template_name = "ledger/coa_accounts/account_detail.html"
context_object_name = "account"
slug_field = "uuid"
DEFAULT_TXS_DAYS = 30
permission_required = ["django_ledger.view_accountmodel"]
extra_context = {
"DEFAULT_TXS_DAYS": DEFAULT_TXS_DAYS,
"header_subtitle_icon": "ic:round-account-tree",
}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
account_model: AccountModel = context["object"]
context["header_title"] = f"Account {account_model.code} - {account_model.name}"
context["page_title"] = f"Account {account_model.code} - {account_model.name}"
context["total_debits"] = sum(
x.amount for x in account_model.transactionmodel_set.filter(tx_type="debit")
)
context["total_credits"] = sum(
x.amount
for x in account_model.transactionmodel_set.filter(tx_type="credit")
)
account_model.transactionmodel_set.all().posted().order_by(
"journal_entry__timestamp"
).select_related(
"journal_entry",
"journal_entry__entity_unit",
"journal_entry__ledger__billmodel",
"journal_entry__ledger__invoicemodel",
)
context["url_kwargs"] = self.kwargs
return context
class AccountUpdateView(
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
):
"""
Represents a view for updating an existing account.
This class provides functionality to update an account's details using a form.
The user must be logged in and have the necessary permissions to access this
view. Upon successful update of the account, a success message is displayed
and the user is redirected to the account list page.
:ivar model: The model associated with this view which represents the account.
:type model: AccountModel
:ivar form_class: The form class used for updating account details.
:type form_class: AccountModelUpdateForm
:ivar template_name: The path to the template used for rendering the update view.
:type template_name: str
:ivar success_url: The URL to redirect to upon success.
:type success_url: str
:ivar success_message: The success message displayed after updating an account
successfully.
:type success_message: str
:ivar permission_required: List of permissions required to access the view.
:type permission_required: list of str
"""
model = AccountModel
form_class = AccountModelUpdateForm
template_name = "ledger/coa_accounts/account_form.html"
success_message = _("Account updated successfully")
permission_required = ["django_ledger.view_accountmodel"]
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.fields["_ref_node_id"].widget = HiddenInput()
form.fields["_position"].widget = HiddenInput()
return form
def form_valid(self, form):
form.instance.coa_model = ChartOfAccountModel.objects.get(pk=self.kwargs['coa_pk']) or self.request.entity.get_default_coa()
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy(
"account_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"],"coa_pk":self.kwargs["coa_pk"]}
)
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:
context["coa_model"] = ChartOfAccountModel.objects.get(pk=coa_pk) or self.request.entity.get_default_coa()
except Exception:
context["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):
"""
Handles the deletion of an account object identified by its primary key (pk). Ensures
that the user has the necessary permissions to perform the deletion. Successfully
deletes the account and redirects to the account list view with a success message.
:param request: The HTTP request object representing the current user and request data.
:type request: HttpRequest
:param pk: The primary key of the account to be deleted.
:type pk: int
:return: An HTTP redirect response to the account list page.
:rtype: HttpResponse
"""
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
account = get_object_or_404(AccountModel, pk=pk)
account.delete()
messages.success(request, _("Account deleted successfully"))
return redirect("account_list", dealer_slug=dealer_slug, coa_pk=coa_pk)
# Sales list
@login_required
@permission_required("inventory.view_saleorder", raise_exception=True)
def sales_list_view(request, dealer_slug):
"""
Handles the retrieval and presentation of a paginated list of item transactions for
sales, specific to the logged-in user's entity. Requires the user to have appropriate
permissions to view the list.
:param request: The HTTP request object containing metadata about the request,
such as HTTP method, user credentials, and sent data.
:type request: HttpRequest
:return: An HTTP response with the rendered sales list page containing the paginated
item transactions specific to the user's entity.
:rtype: HttpResponse
"""
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
staff = getattr(request.user, "staff", None)
qs = []
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)
elif request.is_staff:
qs = models.ExtraInfo.get_sale_orders(staff=staff,dealer=dealer)
except Exception as e:
print(e)
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)
).distinct()
paginator = Paginator(qs, 30)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
context = {"txs": page_obj, "page_obj": page_obj}
return render(request, "sales/sales_list.html", context)
class SaleOrderDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
model = models.SaleOrder
template_name = "sales/saleorder_detail.html"
context_object_name = "sale_order"
permission_required = ["inventory.view_saleorder"]
def get_context_data(self, **kwargs):
dealer = self.request.dealer
context = super().get_context_data(**kwargs)
sale_order = self.get_object()
data = get_finance_data(sale_order.estimate, dealer)
print(data)
# Add additional context data
context["status_choices"] = dict(models.SaleOrder.STATUS_CHOICES)
context["page_title"] = _("Sales Order Details")
# Calculate any additional properties you want to display
context["is_delivered"] = sale_order.status == "DELIVERED"
context["is_cancelled"] = sale_order.status == "CANCELLED"
context["is_pending_approval"] = sale_order.status == "PENDING_APPROVAL"
context["data"] = data
return context
def post(self, request, *args, **kwargs):
sale_order = self.get_object()
status = request.POST.get("status")
if status:
sale_order.status = status
sale_order.save()
messages.success(request, _("Sale order status updated"))
return redirect(
"order_detail", dealer_slug=sale_order.dealer.slug, pk=sale_order.pk
)
# Estimates
class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"""
Handles the display of a paginated list of estimates for a specific entity.
This class-based view displays estimates related to an entity associated
with the logged-in user. It renders a paginated list of estimates on a
template and allows filtering of estimates based on their status. Access
to this view is restricted to users with the required permissions.
:ivar model: The database model associated with the view.
:type model: Model
:ivar template_name: The path to the template used for rendering the view.
:type template_name: str
:ivar context_object_name: The name of the context variable representing
the list of estimates.
:type context_object_name: str
:ivar paginate_by: The number of estimates displayed per page.
:type paginate_by: int
:ivar permission_required: List of permissions required to view this page.
:type permission_required: list
"""
model = EstimateModel
template_name = "sales/estimates/estimate_list.html"
context_object_name = "estimates"
paginate_by = 20
permission_required = ["django_ledger.view_estimatemodel"]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
if any(
[
self.request.is_dealer,
self.request.is_manager,
self.request.is_accountant,
]
):
qs = models.ExtraInfo.objects.filter(
dealer=dealer,
content_type=ContentType.objects.get_for_model(EstimateModel),
related_content_type=ContentType.objects.get_for_model(models.Staff),
).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(
dealer=dealer,
content_type=ContentType.objects.get_for_model(EstimateModel),
related_content_type=ContentType.objects.get_for_model(models.Staff),
related_object_id=self.request.staff.pk,
)
qs = EstimateModel.objects.filter(pk__in=[x.content_object.pk for x in qs])
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)
).distinct()
context["staff_estimates"] = qs
return context
def get_queryset(self):
dealer = get_user_type(self.request)
entity = dealer.entity
status = self.request.GET.get("status")
queryset = entity.get_estimates()
if status:
queryset = queryset.filter(status=status)
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)
).distinct()
return queryset
# @csrf_exempt
@login_required
@permission_required("django_ledger.add_estimatemodel", raise_exception=True)
def create_estimate(request, dealer_slug, slug=None):
"""
Creates a new estimate based on the provided data and saves it. This function processes
a POST request and expects a JSON payload containing details of the estimate such as
title, customer, terms, items, and quantities. It validates the input data, ensures
availability of stocks, and updates or creates the corresponding estimate in the database.
If `pk` is provided, it links the created estimate with an existing opportunity. It handles
the reservation of cars and updates the stock information accordingly.
:param request: The HttpRequest object containing user-specific data and state.
:type request: HttpRequest
:param pk: An optional primary key of the existing opportunity to associate with
the created estimate.
:type pk: int, optional
:return: A JsonResponse object with status and either the created quotation URL
or an error message. If the request method is not POST, it renders the
estimate creation form.
:rtype: JsonResponse or HttpResponse
"""
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
entity = dealer.entity
if request.method == "POST":
# try:
data = json.loads(request.body)
title = data.get("title")
customer_id = data.get("customer")
customer = models.Customer.objects.filter(pk=int(customer_id),dealer=dealer).first()
items = data.get("item", [])
quantities = data.get("quantity", [])
if not all([items, quantities]):
return JsonResponse(
{"status": "error", "message": _("Items and Quantities are required")},
status=400,
)
if isinstance(quantities, list):
if "0" in quantities:
return JsonResponse(
{
"status": "error",
"message": _("Quantity must be greater than zero"),
}
)
else:
if int(quantities) <= 0:
return JsonResponse(
{
"status": "error",
"message": _("Quantity must be greater than zero"),
}
)
if isinstance(items, list):
for item, quantity in zip(items, quantities):
if (
int(quantity)
> models.Car.objects.filter(hash=item, status="available").count()
):
return JsonResponse(
{
"status": "error",
"message": _(
"Quantity must be less than or equal to the number of cars in stock"
),
},
)
else:
if (
int(quantities)
> models.Car.objects.filter(hash=items, status="available").count()
):
return JsonResponse(
{
"status": "error",
"message": _(
"Quantity must be less than or equal to the number of cars in stock"
),
},
)
estimate = entity.create_estimate(
estimate_title=title,
customer_model=customer.customer_model,
contract_terms="fixed",
)
if isinstance(items, list):
item_quantity_map = {}
for item, quantity in zip(items, quantities):
if item in item_quantity_map:
item_quantity_map[item] += int(quantity)
else:
item_quantity_map[item] = int(quantity)
item_list = list(item_quantity_map.keys())
quantity_list = list(item_quantity_map.values())
items_list = [
{"item_id": item_list[i], "quantity": quantity_list[i]}
for i in range(len(item_list))
]
items_txs = []
for item in items_list:
car_instance = models.Car.objects.filter(
hash=item.get("item_id"),
# finances__is_sold=False,
colors__isnull=False,
marked_price__gt=1,
status="available",
).all()
for i in car_instance[: int(quantities[0])]:
print(i)
items_txs.append(
{
"item_number": i.item_model.item_number,
"quantity": 1,
"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
}
)
estimate_itemtxs = {
item.get("item_number"): {
"unit_cost": item.get("unit_cost"),
"unit_revenue": item.get("unit_revenue"),
"quantity": item.get("quantity"),
"total_amount": item.get("total_amount"),
}
for item in items_txs
}
# else:
# item = entity.get_items_all().filter(pk=items).first()
# instance = models.Car.objects.get(vin=item.name)
# estimate_itemtxs = {
# item.item_number: {
# "unit_cost": instance.cost_price,
# "unit_revenue": instance.marked_price,
# "quantity": Decimal(quantities),
# "total_amount": instance.final_price_plus_vat * int(quantities),# TODO : check later
# }
# }
try:
estimate.migrate_itemtxs(
itemtxs=estimate_itemtxs,
commit=True,
operation=EstimateModel.ITEMIZE_APPEND,
)
except Exception as e:
estimate.delete()
return JsonResponse(
{
"status": "error",
"message": e,
}
)
if isinstance(items, list):
for item in estimate_itemtxs.keys():
item_instance = entity.get_items_all().filter(item_number=item).first()
# else:
# item_instance = ItemModel.objects.filter(
# additioinal_info__car_info__hash=items
# ).first()
# instance = models.Car.objects.get(hash=item)
# reserve_car(instance, request) #TODO
opportunity_id = data.get("opportunity_id")
if opportunity_id != "None":
opportunity = models.Opportunity.objects.get(slug=opportunity_id)
opportunity.estimate = estimate
opportunity.save()
if request.is_staff:
models.ExtraInfo.objects.create(
dealer=dealer,
content_object=estimate,
related_object=request.staff,
created_by=request.user,
)
else:
models.ExtraInfo.objects.create(
dealer=dealer,
content_object=estimate,
related_object=request.user,
created_by=request.user,
data={"vat_rate": 0.15, "discount": 0},
)
url = reverse(
"estimate_detail", kwargs={"dealer_slug": dealer.slug, "pk": estimate.pk}
)
return JsonResponse(
{
"status": "success",
"message": _("Quotation created successfully"),
"url": f"{url}",
}
)
#######################################
##GET
form = forms.EstimateModelCreateForm()
form.fields["customer"].queryset = models.Customer.objects.filter(
dealer=dealer, active=True
)
if slug:
opportunity = models.Opportunity.objects.get(slug=slug)
customer = opportunity.customer
print(customer)
form.fields["customer"].queryset = models.Customer.objects.filter(
pk=customer.pk
)
form.initial["customer"] = customer
car_list = (
models.Car.objects.filter(
dealer=dealer,
colors__isnull=False,
marked_price__gt=1,
status="available",
)
.annotate(
exterior_color=F("colors__exterior__rgb"),
interior_color=F("colors__interior__rgb"),
color_name=F("colors__exterior__arabic_name"),
)
.values_list(
"id_car_make__arabic_name",
"id_car_model__arabic_name",
"id_car_serie__arabic_name",
"id_car_trim__arabic_name",
"exterior_color",
"interior_color",
"color_name",
"hash",
"id_car_make__logo",
)
.annotate(hash_count=Count("hash"))
.distinct()
)
print(car_list)
context = {
"form": form,
"items": [
{
"make": x[0],
"model": x[1],
"serie": x[2],
"trim": x[3],
"exterior_color": x[4],
"interior_color": x[5],
"color_name": x[6],
"hash": x[7],
"logo": settings.MEDIA_URL + x[8],
"hash_count": x[9],
}
for x in car_list
],
"opportunity_id": slug if slug else None,
"customer_count": entity.get_customers().count(),
"no_items_message": _("Please add at least one car or complete the car info before creating a quotation."),
"no_items_button": _("Add car"),
"no_customers_message": _("Please add at least one customer before creating a quotation."),
"no_customers_button": _("Add Customer"),
}
return render(request, "sales/estimates/estimate_form.html", context)
class EstimateDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""
Represents the detailed view for an EstimateModel instance.
This class provides functionality to display detailed information about
a specific EstimateModel instance. It ensures the user is authenticated
and has the required permissions to access the estimate details. The class
also integrates additional financial and invoice-related data into the
context for more comprehensive display and functionality.
:ivar model: Specifies the model associated with the view.
:type model: ModelBase
:ivar template_name: Path to the template used for rendering the detailed view.
:type template_name: str
:ivar context_object_name: Name used to refer to the EstimateModel instance in the context.
:type context_object_name: str
:ivar permission_required: List of permissions required to access the view.
:type permission_required: list
"""
model = EstimateModel
template_name = "sales/estimates/estimate_detail.html"
context_object_name = "estimate"
permission_required = ["django_ledger.view_estimatemodel"]
def get_context_data(self, **kwargs):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
estimate = kwargs.get("object")
if estimate.get_itemtxs_data():
# calculator = CarFinanceCalculator(estimate)
# finance_data = calculator.get_finance_data()
finance_data = get_finance_data(estimate,dealer)
invoice_obj = InvoiceModel.objects.all().filter(ce_model=estimate).first()
kwargs["data"] = finance_data
kwargs["invoice"] = invoice_obj
try:
car = estimate.get_itemtxs_data()[0].first().item_model.car
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.initial["additional_finances"] = selected_items
kwargs["additionals_form"] = form
except Exception as e:
logger.error(e)
return super().get_context_data(**kwargs)
class EstimatePrintView(EstimateDetailView):
"""
A view to render a printer-friendly version of the estimate.
It reuses the data-fetching logic from EstimateDetailView but
uses a dedicated, stripped-down print template.
"""
template_name = "sales/estimates/estimate_preview.html"
@login_required
@permission_required("inventory.add_saleorder", raise_exception=True)
def create_sale_order(request, dealer_slug, pk):
"""
Creates a sale order for a given estimate and updates associated item and car data.
This view is responsible for handling the submission of a sale order form linked to
a specific estimate. It ensures that the estimate is approved if not already, updates
the status of the related car items as sold, and redirects to the estimate's detailed
view upon successful creation of the sale order. If the request method is not POST, it
renders the form for the user to input sale order details, along with other contextual
information like estimate data and car finance details.
:param request: HTTP request object.
:type request: HttpRequest
:param pk: Primary key of the estimate to create a sale order for.
:type pk: int
:return: An HTTP response rendering the sale order form if the method is GET or invalid
POST data, or redirects to the estimate detail view upon successful creation.
:rtype: HttpResponse
"""
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
estimate = get_object_or_404(EstimateModel, pk=pk)
items = estimate.get_itemtxs_data()[0].all()
if request.method == "POST":
form = forms.SaleOrderForm(request.POST)
if form.is_valid():
instance = form.save(commit=False)
instance.dealer = dealer
instance.estimate = estimate
instance.customer = estimate.customer.customer_set.first()
instance.created_by = request.user
instance.last_modified_by = request.user
instance.save()
if not estimate.is_approved():
estimate.mark_as_approved()
estimate.save()
for item in estimate.get_itemtxs_data()[0].all():
try:
# item.item_model.additional_info["car_info"]["status"] = "sold"
item.item_model.save()
logger.debug(
f"Car status updated to 'sold' for item.item_model PK: {getattr(item.item_model, 'pk', 'N/A')}."
)
except KeyError:
logger.warning(
f"KeyError: 'car_info' or 'status' key missing when attempting to update status to 'sold' for item.item_model PK: {getattr(item.item_model, 'pk', 'N/A')}."
)
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.mark_as_sold()
messages.success(request, "Sale Order created successfully")
else:
print(form.errors)
messages.error(request, "Invalid form data")
return redirect("estimate_detail", dealer_slug=dealer_slug, pk=estimate.pk)
form = forms.SaleOrderForm()
# customer = estimate.customer.customer_set.first()
# form.fields["estimate"].queryset = EstimateModel.objects.filter(pk=pk)
# form.initial["estimate"] = estimate
# form.fields["customer"].queryset = models.Customer.objects.filter(pk=customer.pk)
# form.initial["customer"] = customer
# if hasattr(estimate, "opportunity"):
# form.initial["opportunity"] = estimate.opportunity
# else:
# form.fields["opportunity"].widget = HiddenInput()
# calculator = CarFinanceCalculator(estimate)
finance_data = get_finance_data(estimate,dealer)
return render(
request,
"sales/estimates/sale_order_form.html",
{"form": form, "estimate": estimate, "items": items, "data": finance_data},
)
@login_required
@require_POST
def update_estimate_discount(request, dealer_slug, pk):
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
estimate = get_object_or_404(EstimateModel, pk=pk)
extra_info = models.ExtraInfo.objects.get(
dealer=dealer,
content_type=ContentType.objects.get_for_model(EstimateModel),
object_id=estimate.pk,
)
# calculator = CarFinanceCalculator(estimate)
# finance_data = calculator.get_finance_data()
discount_amount = request.POST.get("discount_amount", 0)
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"))
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."))
else:
messages.success(request, _("Discount updated successfully"))
extra_info.data.update({"discount": Decimal(discount_amount)})
extra_info.save()
return redirect("estimate_detail", dealer_slug=dealer_slug, pk=pk)
@login_required
@require_POST
def update_estimate_additionals(request, dealer_slug, pk):
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
form = forms.AdditionalFinancesForm(request.POST)
if request.method == "POST":
if form.is_valid():
estimate = get_object_or_404(EstimateModel, pk=pk)
car = estimate.get_itemtxs_data()[0].first().item_model.car
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)
class SaleOrderDetail(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
model = models.SaleOrder
template_name = "sales/orders/order_details.html"
context_object_name = "saleorder"
permission_required = ["inventory.view_saleorder"]
def get_object(self, queryset=None):
order_pk = self.kwargs.get("order_pk")
print("hi")
return models.SaleOrder.objects.get(
pk=order_pk,
)
def get_context_data(self, **kwargs):
saleorder = kwargs.get("object")
dealer = self.request.dealer
estimate = saleorder.estimatep
if estimate.get_itemtxs_data():
# calculator = CarFinanceCalculator(estimate)
# finance_data = calculator.get_finance_data()
finance_data = get_finance_data(estimate,dealer)
kwargs["data"] = finance_data
return super().get_context_data(**kwargs)
@login_required
@permission_required("inventory.view_saleorder", raise_exception=True)
def preview_sale_order(request, dealer_slug, pk):
"""
Handles rendering of the sale order preview page for a specific estimate.
This view retrieves an `EstimateModel` object based on the provided primary
key (`pk`), fetches related car finance data, and renders the preview of the
sale order associated with the given estimate.
:param request: The HTTP request object
:type request: HttpRequest
:param pk: The primary key of the `EstimateModel` to retrieve
:type pk: int
:return: HTTP response containing the rendered sale order preview page
:rtype: HttpResponse
"""
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
estimate = get_object_or_404(EstimateModel, pk=pk)
data = get_car_finance_data(estimate)
return render(
request,
"sales/estimates/sale_order_preview.html",
{"order": estimate.sale_orders.first(), "data": data, "estimate": estimate},
)
class PaymentRequest(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""
Represents a detailed view of a payment request for an estimate.
This class is a Django DetailView that leverages mixins for login
and permission requirements. It displays details of an estimate
and fetches related car data based on the estimate's items.
It ensures only authorized users can access the payment request details.
:ivar model: The Django model associated with this view. It is used
to fetch and display detailed information for an estimate.
:type model: EstimateModel
:ivar template_name: The template utilized to render the detailed
payment request page for estimates.
:type template_name: str
:ivar context_object_name: The name of the context object to be
accessible in the template for the detailed view.
:type context_object_name: str
:ivar permission_required: Permissions required for accessing
this view. The user must have the specified permissions.
:type permission_required: list
"""
model = EstimateModel
template_name = "sales/estimates/payment_request_detail.html"
context_object_name = "estimate"
permission_required = ["django_ledger.view_invoicemodel"]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["cars"] = [
models.Car.objects.get(vin=car.item_model.name)
for car in context["estimate"].get_itemtxs_data()[0].all()
]
return context
class EstimatePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""
Represents a view for previewing an estimate with user-specific permissions and
context data processing. This class provides functionality to render a detailed
view of an estimate while ensuring user authentication and permission verification.
This view is primarily used in a sales module to preview the detailed financial
breakdown of an estimate, including tax, discount, additional services, and total
amount.
:ivar model: The model associated with this view.
:type model: Type[models.Model]
:ivar context_object_name: The name of the context variable containing the object.
:type context_object_name: str
:ivar template_name: The path to the template used for rendering this view.
:type template_name: str
:ivar permission_required: List of permissions required to access this view.
:type permission_required: List[str]
"""
model = EstimateModel
context_object_name = "estimate"
template_name = "sales/estimates/estimate_preview.html"
permission_required = ["django_ledger.view_estimatemodel"]
def get_context_data(self, **kwargs):
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)
return super().get_context_data(**kwargs)
@login_required
@permission_required("django_ledger.change_estimatemodel", raise_exception=True)
def estimate_mark_as(request, dealer_slug, pk):
"""
Marks an estimate with a specified status based on the requested action and
permissions. The marking possibilities include review, approval, rejection,
completion, and cancellation. The function validates whether the estimate
can transition to the desired status before updating it. It also handles
notifications and updates related entities if required, such as car status
changes upon cancellation.
:param request: The HTTP request object containing metadata about the request.
:type request: HttpRequest
:param pk: The primary key of the estimate to be marked.
:type pk: int
:return: A redirect response to the estimate detail view.
:rtype: HttpResponseRedirect
"""
if not (
request.user.has_perm("django_ledger.can_approve_estimatemodel")
or request.user.has_perm("django_ledger.change_estimatemodel")
):
raise PermissionDenied
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
estimate = get_object_or_404(EstimateModel, pk=pk)
mark = request.GET.get("mark")
print(mark)
if mark:
if mark == "review":
if not estimate.can_review():
messages.error(request, _("Quotation is not ready for review"))
return redirect(
"estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk
)
estimate.mark_as_review()
elif mark == "approved":
if not estimate.can_approve():
messages.error(request, _("Quotation is not ready for approval"))
return redirect(
"estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk
)
estimate.mark_as_approved()
estimate.save()
# Reserve The Car
car = estimate.get_itemtxs_data()[0].first().item_model.car
reserve_car(car, request)
messages.success(request, _("Quotation approved successfully"))
return redirect("estimate_list", dealer_slug=dealer.slug)
elif mark == "rejected":
if not estimate.can_cancel():
messages.error(request, _("Quotation is not ready for rejection"))
return redirect(
"estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk
)
estimate.mark_as_canceled()
messages.success(request, _("Quotation canceled successfully"))
elif mark == "completed":
if not estimate.can_complete():
messages.error(request, _("Quotation is not ready for completion"))
return redirect(
"estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk
)
elif mark == "canceled":
if not estimate.can_cancel():
messages.error(request, _("Quotation is not ready for cancellation"))
return redirect(
"estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk
)
estimate.mark_as_canceled()
try:
car = models.Car.objects.get(
vin=estimate.get_itemtxs_data()[0].first().item_model.name
)
car.status = "available"
car.save()
logger.debug(
f"Car VIN '{car.vin}' status updated to 'available' for Estimate ID: {getattr(estimate, 'pk', 'N/A')}."
)
except Exception as e:
logger.error(
f"Failed to update car status to 'available' for Estimate ID: {getattr(estimate, 'pk', 'N/A')}. "
f"Attempted VIN: '{car.vin}'. Error: {e}",
exc_info=True,
)
messages.success(request, _("Quotation canceled successfully"))
estimate.save()
messages.success(request, _("Quotation marked as ") + mark.upper())
return redirect("estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk)
# Invoice
class InvoiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"""
Handles the display and management of a list of invoices.
This class-based view provides functionality for displaying a paginated list of invoices
while ensuring that only authenticated and authorized users can access the view. It allows
for applying search filters to the displayed invoices, based on user inputs. The view is
designed to work as part of the Django framework, and utilizes models, templates, and
permissions specific to the application.
:ivar model: The model representing invoices.
:type model: type
:ivar template_name: Path to the template used to render the view.
:type template_name: str
:ivar context_object_name: Name used to reference the list of invoices in the template.
:type context_object_name: str
:ivar paginate_by: The number of invoices to display per page.
:type paginate_by: int
:ivar permission_required: List of permissions required for accessing this view.
:type permission_required: list
"""
model = InvoiceModel
template_name = "sales/invoices/invoice_list.html"
context_object_name = "invoices"
paginate_by = 20
permission_required = ["django_ledger.view_invoicemodel"]
def get_queryset(self):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
entity = dealer.entity
staff = getattr(self.request.user, "staff", None)
qs = []
try:
if any(
[
self.request.is_dealer,
self.request.is_manager,
self.request.is_accountant,
]
):
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)
except Exception as e:
print(e)
query = self.request.GET.get("q")
invoices = qs
# invoices = dealer.entity.get_invoices()
return apply_search_filters(invoices, query)
class InvoiceDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""
Handles the detailed view for an invoice.
This class is responsible for displaying detailed information about a specific
invoice. It uses Django's DetailView to render the details, requires the user
to be logged in, and enforces specific permissions for viewing invoices. The
class also processes and includes additional invoice-specific data into the
context provided to the template.
:ivar model: Specifies the model to be used for the detail view.
:type model: Type[InvoiceModel]
:ivar template_name: Path to the template used for rendering the invoice details.
:type template_name: str
:ivar context_object_name: The name of the context variable representing the object.
:type context_object_name: str
:ivar permission_required: List of permissions required to access the view.
:type permission_required: list
"""
model = InvoiceModel
template_name = "sales/invoices/invoice_detail.html"
context_object_name = "invoice"
permission_required = ["django_ledger.view_invoicemodel"]
def get_context_data(self, **kwargs):
invoice = kwargs.get("object")
if invoice.get_itemtxs_data():
# calculator = CarFinanceCalculator(invoice)
# finance_data = calculator.get_finance_data()
finance_data = get_finance_data(invoice,self.request.dealer)
kwargs["data"] = finance_data
kwargs["payments"] = JournalEntryModel.objects.filter(
ledger=invoice.ledger
).all()
return super().get_context_data(**kwargs)
class DraftInvoiceModelUpdateFormView(
LoginRequiredMixin, PermissionRequiredMixin, UpdateView
):
"""
Representation of a form view for updating draft invoices.
This class inherits from Django's login and permission mixins as well as
UpdateView, providing the functionality required to update an existing
instance of `InvoiceModel` using the associated form class. It enforces
that the user logged in has the required permissions to view invoices
and redirects to the invoice list upon successful update. This form
view specifically customizes the form initialization logic to include
additional data based on the requesting user's type and associated
entity.
:ivar model: The Django model that this view will operate upon.
:type model: Type[InvoiceModel]
:ivar form_class: The form class to be used for updating `InvoiceModel`
instances.
:type form_class: Type[DraftInvoiceModelUpdateForm]
:ivar template_name: The path to the template used to render this view.
:type template_name: str
:ivar success_url: The URL to redirect to upon successful form submission.
:type success_url: str
:ivar permission_required: The list of permissions required to access
this view.
:type permission_required: List[str]
"""
model = InvoiceModel
form_class = DraftInvoiceModelUpdateForm
template_name = "sales/invoices/draft_invoice_update.html"
success_url = reverse_lazy("invoice_list")
permission_required = ["django_ledger.change_invoicemodel"]
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
kwargs["entity_slug"] = dealer.entity
kwargs["user_model"] = dealer.entity.admin
return kwargs
class ApprovedInvoiceModelUpdateFormView(
LoginRequiredMixin, PermissionRequiredMixin, UpdateView
):
"""
Handles the view for updating approved invoice models.
This class-based view is used to update the details of an approved invoice. It
inherits from ``LoginRequiredMixin``, ``PermissionRequiredMixin``, and
``UpdateView`` to ensure secured access to the functionality and integrates
with Django's permission system. Users must have the required permission to
access this view. It utilizes a custom update form and is configured with
specific success URLs.
:ivar model: The model associated with this view, which is ``InvoiceModel``.
:type model: type
:ivar form_class: The form class used for handling updates, which is
``ApprovedInvoiceModelUpdateForm``.
:type form_class: type
:ivar template_name: The path to the template used for rendering the view.
:type template_name: str
:ivar success_url: URL to redirect upon successful operation. This uses
``reverse_lazy`` to point to the invoice list by default.
:type success_url: django.urls.reverse_lazy
:ivar permission_required: The permission required to access this view. It is
set to ``django_ledger.view_invoicemodel`` by default.
:type permission_required: list of str
"""
model = InvoiceModel
form_class = ApprovedInvoiceModelUpdateForm
template_name = "sales/invoices/approved_invoice_update.html"
success_url = reverse_lazy("invoice_list")
permission_required = ["django_ledger.change_invoicemodel"]
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
kwargs["entity_slug"] = dealer.entity
kwargs["user_model"] = dealer.entity.admin
return kwargs
def get_success_url(self):
return reverse_lazy(
"invoice_detail",
kwargs={"dealer_slug": self.kwargs["dealer_slug"],"entity_slug": self.kwargs["entity_slug"], "pk": self.object.pk},
)
class PaidInvoiceModelUpdateFormView(
LoginRequiredMixin, PermissionRequiredMixin, UpdateView
):
"""
Handles the update process for paid invoices.
This view allows updating invoice details for paid invoices within the system.
The user must be logged in and have the necessary permissions for accessing
and modifying invoice-related data. Additionally, the view ensures that any
required validation is performed and only executes updates when the desired
business logic conditions are met. It inherits from `UpdateView` and includes
custom behavior for handling form validation, success URLs, and associated
permissions.
:ivar model: The model class associated with the view. Represents the invoice
data being managed.
:type model: type
:ivar form_class: The form class used to render and validate the update form.
:type form_class: type
:ivar template_name: Path to the template used to render the update view.
:type template_name: str
:ivar success_url: Default URL to redirect to after a successful update. This
can be overridden in specific cases.
:type success_url: str
:ivar permission_required: List of permissions required to access the view.
:type permission_required: list of str
"""
model = InvoiceModel
form_class = PaidInvoiceModelUpdateForm
template_name = "sales/invoices/paid_invoice_update.html"
success_url = reverse_lazy("invoice_list")
permission_required = ["django_ledger.change_invoicemodel"]
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
kwargs["entity_slug"] = dealer.entity
kwargs["user_model"] = dealer.entity.admin
return kwargs
def get_success_url(self):
return reverse_lazy(
"invoice_detail",
kwargs={"dealer_slug": self.kwargs["dealer_slug"],"entity_slug": self.kwargs["entity_slug"], "pk": self.object.pk},
)
def form_valid(self, form):
invoice = form.save()
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)
else:
invoice.post_ledger()
invoice.save()
return super().form_valid(form)
@login_required
@permission_required("django_ledger.change_invoicemodel", raise_exception=True)
def invoice_mark_as(request, dealer_slug, pk):
"""
Marks an invoice as approved if it meets the required conditions.
This view is responsible for marking an invoice as approved based on the provided
`mark` parameter. If the `mark` parameter is specified as "accept" and the invoice
is eligible for approval, it gets approved and saved. Otherwise, an error message
is displayed. The function requires the user to be logged in and to have the
appropriate permission to change the InvoiceModel.
:param request: The HTTP request object containing metadata about the request.
:type request: django.http.HttpRequest
:param pk: The primary key of the invoice to be processed.
:type pk: int
:return: An HTTP redirect response to the invoice detail page after processing.
:rtype: django.http.HttpResponse
"""
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
invoice = get_object_or_404(InvoiceModel, pk=pk)
mark = request.GET.get("mark")
if mark and mark == "accept":
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)
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)
@login_required
@permission_required("django_ledger.add_invoicemodel", raise_exception=True)
def invoice_create(request, dealer_slug, pk):
"""
Handles the creation of a new invoice associated with a given estimate. It validates
the submitted data through a form, processes the invoice, updates related models, and
finalizes the estimate. If successful, redirects to the detailed view of the created
invoice. If the submitted data is invalid or the request is not a POST request, renders
the invoice creation form.
:param request: The HTTP request object.
:type request: HttpRequest
:param pk: The primary key of the estimate associated with the invoice.
:type pk: int
:return: An HTTP response. Redirects to the "invoice_detail" view upon successful invoice
creation or renders the invoice creation form template otherwise.
:rtype: HttpResponse
"""
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
estimate = get_object_or_404(EstimateModel, pk=pk)
entity = dealer.entity
if request.method == "POST":
form = forms.InvoiceModelCreateForm(
request.POST, entity_slug=entity.slug, user_model=entity.admin
)
if form.is_valid():
invoice = form.save(commit=False)
ledger = entity.create_ledger(name=str(invoice.pk))
invoice.ledgar = ledger
ledger.invoicemodel = invoice
ledger.save()
invoice.save()
# calculator = CarFinanceCalculator(estimate)
# finance_data = calculator.get_finance_data()
finance_data = get_finance_data(estimate,dealer)
car = finance_data.get("car")
invoice_itemtxs = {
car.item_model.item_number: {
"unit_cost": finance_data.get("grand_total"),
"quantity": 1,
"total_amount": finance_data.get("grand_total"),
}
}
invoice_itemtxs = invoice.migrate_itemtxs(
itemtxs=invoice_itemtxs,
commit=True,
operation=InvoiceModel.ITEMIZE_APPEND,
)
sale_order = estimate.sale_orders.first()
sale_order.invoice = invoice
invoice.bind_estimate(estimate)
invoice.mark_as_review()
estimate.mark_as_completed()
sale_order.save()
estimate.save()
invoice.save()
messages.success(request, "Invoice created successfully")
return redirect("invoice_detail", dealer_slug=dealer.slug,entity_slug=entity.slug, pk=invoice.pk)
else:
print(form.errors)
form = forms.InvoiceModelCreateForm(
entity_slug=entity.slug, user_model=entity.admin
)
form.initial.update(
{
"customer": estimate.customer,
"cash_account": dealer.settings.invoice_cash_account,
"prepaid_account": dealer.settings.invoice_prepaid_account,
"unearned_account": dealer.settings.invoice_unearned_account,
}
)
context = {
"form": form,
"estimate": estimate,
}
return render(request, "sales/invoices/invoice_create.html", context)
class InvoicePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""
Represents a detailed view for previewing an invoice.
This class provides a mechanism to render a preview of an invoice for authorized
users. It utilizes Django's class-based views by extending `DetailView` and includes
necessary mixins for login and permission checks. The purpose of this class is to ensure
secured access to the invoice preview while generating additional context data needed
for rendering finance-related information.
:ivar model: The Django model class this view will represent.
:type model: InvoiceModel
:ivar context_object_name: Name of the context object used in the template.
:type context_object_name: str
:ivar template_name: Path to the template used for rendering the view.
:type template_name: str
:ivar permission_required: List of permissions required to access the view.
:type permission_required: list
"""
model = InvoiceModel
context_object_name = "invoice"
template_name = "sales/invoices/invoice_preview.html"
permission_required = ["django_ledger.view_invoicemodel"]
def get_context_data(self, **kwargs):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
invoice = kwargs.get("object")
if invoice.get_itemtxs_data():
# calculator = CarFinanceCalculator(invoice)
finance_data = get_finance_data(invoice,dealer)
kwargs["data"] = finance_data
kwargs["dealer"] = dealer
return super().get_context_data(**kwargs)
# payments
class InvoiceModelUpdateView(InvoiceModelUpdateViewBase):
template_name = 'sales/invoices/invoice_update.html'
permission_required = ["django_ledger.change_invoicemodel"]
# def PaymentCreateView(request,dealer_slug,entity_slug,invoice_pk):
# from django_ledger.forms.invoice import AccruedAndApprovedInvoiceModelUpdateForm
# invoice = get_object_or_404(InvoiceModel,pk=invoice_pk)
# form = AccruedAndApprovedInvoiceModelUpdateForm(entity_slug=entity_slug,user_model=request.dealer.user)
# if request.method == "POST":
# if form.is_valid():
# invoice_model: InvoiceModel = form.save(commit=False)
# if invoice_model.can_migrate():
# invoice_model.migrate_state(
# user_model=request.dealer.user,
# entity_slug=entity_slug
# )
# invoice_model.save()
# messages.success(request, "Invoice updated successfully")
# return redirect("invoice_detail", dealer_slug=dealer_slug,entity_slug=entity_slug, invoice_pk=invoice_model.pk)
# else:
# print(form.errors)
# context = { "invoice": invoice, "form": form }
# return render(request, "sales/payments/payment_form1.html", context)
@login_required
@permission_required("inventory.add_payment", raise_exception=True)
def PaymentCreateView(request, dealer_slug, pk):
"""
Handles the creation of a payment entry associated with an invoice or bill. Validates
the payment data against the model's current state and reflects the changes in
invoice or bill records. Provides appropriate error messages for invalid conditions
such as exceeding payable amounts or attempting payment for already fully paid models.
If successfully processed, the payment details are saved, and the model is updated
accordingly. This view regulates payment for dealer-associated entities while
ensuring the model consistency.
The view renders a form to submit payment details, and pre-populates the form fields
with default data for the associated model if necessary.
:param request: The HTTP request object containing user request data and session
information. This is required to handle the request and apply the appropriate
processing rules.
:param pk: The primary key of the invoice or bill being processed. It is used to
load the appropriate model instance for payment processing.
:return: An HTTP response object. Depending on the circumstances, the response may
redirect to the detail view of the processed invoice or bill, re-render the
payment form with error messages or indicate success in payment creation.
"""
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
invoice = InvoiceModel.objects.filter(pk=pk).first()
# bill = BillModel.objects.filter(pk=pk).first()
model = invoice
entity = dealer.entity
form = forms.PaymentForm()
if request.method == "POST":
form = forms.PaymentForm(request.POST)
user_id = request.user.id if request.user.is_authenticated else "Anonymous"
user_username = (
request.user.username if request.user.is_authenticated else "anonymous"
)
if form.is_valid():
amount = form.cleaned_data.get("amount")
invoice = form.cleaned_data.get("invoice")
# 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"
# model = invoice if invoice else bill
if not model.is_approved():
model.mark_as_approved(user_model=entity.admin)
if model.amount_paid == model.amount_due:
messages.error(request, _("fully paid"))
return response
if model.amount_paid + amount > model.amount_due:
messages.error(request, _("Amount exceeds due amount"))
return response
try:
if invoice:
set_invoice_payment(dealer, entity, invoice, amount, payment_method)
logger.info(
f"User {user_username} (ID: {user_id}) successfully processed payment for Invoice ID: {invoice.pk} (Dealer: {dealer.slug}) Amount: {amount}, Method: {payment_method}."
)
# elif bill:
# set_bill_payment(dealer, entity, bill, amount, payment_method)
# logger.info(
# f"User {user_username} (ID: {user_id}) successfully processed payment for Bill ID: {bill.pk} (Dealer: {dealer.slug}) Amount: {amount}, Method: {payment_method}."
# )
messages.success(request, _("Payment created successfully"))
return response
except Exception as e:
logger.error(
f"User {user_username} (ID: {user_id}) encountered error creating payment "
f"for (Dealer: {dealer.slug}). "
f"Attempted Amount: {amount}, Method: {payment_method}. Error: {e}",
exc_info=True,
)
messages.error(request, f"Error creating payment: {str(e)}")
else:
messages.error(request, f"Invalid form data: {str(form.errors)}")
form = forms.PaymentForm()
if model:
form.initial["amount"] = model.amount_due - model.amount_paid
if isinstance(model, InvoiceModel):
form.initial["invoice"] = model
form.fields["bill"].widget = HiddenInput()
elif isinstance(model, BillModel):
form.initial["bill"] = model
form.fields["invoice"].widget = HiddenInput()
return render(
request, "sales/payments/payment_form1.html", {"model": model, "form": form}
)
@login_required
@permission_required("inventory.view_payment", raise_exception=True)
def PaymentListView(request, dealer_slug):
"""
Handles the view for listing payment information associated with the journals of a specific
entity. This view is protected to ensure only authenticated and authorized users can
access it.
The function retrieves the related dealer object based on the current user session, extracts
the associated entity, and fetches all journal entries linked to the entity. This data is
then passed into the template for rendering.
:param request: The HTTP request object containing user context.
:type request: HttpRequest
:return: The rendered HTML response displaying the list of payments.
:rtype: HttpResponse
"""
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
entity = dealer.entity
journals = JournalEntryModel.objects.filter(ledger__entity=entity).all()
paginator = Paginator(journals, 30)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
return render(request, "sales/payments/payment_list.html", {"page_obj": page_obj})
@login_required
@permission_required("inventory.view_payment", raise_exception=True)
def PaymentDetailView(request, dealer_slug, pk):
"""
This function handles the detail view for a payment by fetching a journal entry
and its associated transactions. It ensures that the request is authenticated
and the user has permission to view the journal entry model.
:param request: The HTTP request object.
:type request: HttpRequest
:param pk: The primary key of the journal entry for which details are to be fetched.
:type pk: int
:return: An HTTP response rendering the payment details template with the journal
entry and its associated transactions.
:rtype: HttpResponse
"""
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
journal = JournalEntryModel.objects.filter(pk=pk).first()
transactions = (
TransactionModel.objects.filter(journal_entry=journal)
.order_by("account__code")
.all()
)
return render(
request,
"sales/payments/payment_details.html",
{"journal": journal, "transactions": transactions},
)
@login_required
@permission_required("inventory.change_payment", raise_exception=True)
def payment_mark_as_paid(request, dealer_slug, pk):
"""
Marks an invoice as paid if it meets the conditions of being fully paid and eligible
for payment. Also ensures that related ledger journal entries are locked and posted
when the payment is marked successfully.
This function is protected with both `login_required` and
`permission_required` decorators, ensuring that only logged-in users with
appropriate permissions can execute it.
:param request: HttpRequest object containing metadata about the request.
:type request: HttpRequest
:param pk: Primary key of the invoice to mark as paid.
:type pk: int
:return: Redirect response to the invoice detail page.
:rtype: HttpResponseRedirect
:raises: In case of an exception during the process, an error message is
displayed to the user through Django's messaging framework.
"""
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
invoice = get_object_or_404(InvoiceModel, pk=pk)
if request.method == "POST":
# Get user info for logging
user_id = request.user.id if request.user.is_authenticated else "Anonymous"
user_username = (
request.user.username if request.user.is_authenticated else "anonymous"
)
try:
if invoice.amount_due == invoice.amount_paid:
if not invoice.is_paid() and invoice.can_pay():
invoice.mark_as_paid(
entity_slug=invoice.ledger.entity.slug,
user_model=invoice.ledger.entity.admin,
)
invoice.save()
invoice.ledger.lock_journal_entries()
invoice.ledger.post_journal_entries()
# invoice.ledger.post()
invoice.ledger.save()
# --- Log successful operation ---
logger.info(
f"User {user_username} (ID: {user_id}) successfully marked Invoice ID: {invoice.pk} "
f"as paid and processed ledger entries for Dealer: {dealer_slug}."
)
messages.success(request, _("Payment created successfully"))
else:
logger.warning(
f"User {user_username} (ID: {user_id}) attempted to mark Invoice ID: {invoice.pk} "
f"as paid, but it is not fully paid (Due: {invoice.amount_due}, Paid: {invoice.amount_paid}). "
f"Operation halted for Dealer: {dealer_slug}."
)
messages.error(
request,
_("Invoice is not fully paid, Payment cannot be marked as paid"),
)
except Exception as e:
logger.error(
f"User {user_username} (ID: {user_id}) encountered an error while marking Invoice ID: {invoice.pk} "
f"as paid or processing ledger entries for Dealer: {dealer_slug}. Error: {e}",
exc_info=True,
)
messages.error(request, f"Error: {str(e)}")
return redirect("invoice_detail", dealer_slug=dealer_slug,entity_slug=request.entity.slug, pk=invoice.pk)
# activity log
class UserActivityLogListView(LoginRequiredMixin, ListView):
"""
Represents a view to display a paginated list of user activity logs.
This class is used to display a list of user activity logs in a paginated
manner. It retrieves the logs from the `UserActivityLog` model and allows
basic filtering of logs by user email through the URL query parameters.
The view requires the user to be authenticated and utilizes the
`LoginRequiredMixin` to enforce this.
:ivar model: The model associated with the view.
:type model: models.UserActivityLog
:ivar template_name: The template used for rendering the view.
:type template_name: str
:ivar context_object_name: The name of the context variable representing
the list of logs.
:type context_object_name: str
:ivar paginate_by: The number of logs displayed per page.
:type paginate_by: int
"""
model = models.UserActivityLog
template_name = "dealers/activity_log.html"
context_object_name = "logs"
paginate_by = 20
def get_queryset(self):
queryset = super().get_queryset()
if "user" in self.request.GET:
queryset = queryset.filter(user__email=self.request.GET["user"])
return queryset[:100] # will update later with better pagination
def lead_view(request):
return render(request, "crm/leads/lead_view.html")
# CRM RELATED VIEWS
class LeadListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"""
Represents a view to display a paginated list of leads, ensuring user permissions
and type-specific filtering.
This class is used for rendering a list of leads associated with a logged-in user
based on their type (dealer or staff member). It combines login and permission
controls with pagination to ensure proper access and management of leads
in a CRM context.
:ivar model: The model to be used for retrieving leads.
:type model: type[Lead]
:ivar template_name: Path to the template for rendering the lead list.
:type template_name: str
:ivar context_object_name: The name of the context variable to contain the leads.
:type context_object_name: str
:ivar paginate_by: Number of items to display per page.
:type paginate_by: int
:ivar permission_required: List of permissions required to access the view.
:type permission_required: list[str]
"""
model = models.Lead
template_name = "crm/leads/lead_list.html"
context_object_name = "leads"
paginate_by = 30
permission_required = ["inventory.view_lead"]
def get_queryset(self):
# print(self.request.is_dealer)
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
query = self.request.GET.get("q")
qs = models.Lead.objects.filter(dealer=dealer).exclude(status="converted")
if query:
qs = qs.filter(
Q(first_name__icontains=query)
| Q(last_name__icontains=query)
| Q(id_car_make__name__icontains=query)
| Q(id_car_model__name__icontains=query)
| Q(email__icontains=query)
| Q(phone_number__icontains=query)
| Q(next_action__icontains=query)
| Q(staff__name__icontains=query)
)
if self.request.is_dealer: # or self.request.is_manager:
return qs
if self.request.is_staff:
return qs.filter(staff=self.request.staff)
return models.Lead.objects.none()
def get_context_data(self, **kwargs):
context=super().get_context_data(**kwargs)
context['empty_state_value']=_("lead")
return context
class LeadDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""
View that provides detailed information about a specific lead.
This class-based view is designed for displaying detailed information associated with a
particular lead, including related notes, emails, activities, status history, and a lead
transfer form. It combines multiple mixins to enforce login and permission requirements
for accessing the data. This view is tailored to the CRM module for managing leads.
:ivar model: The model associated with this view, which is the Lead model.
:type model: models.Lead
:ivar template_name: Path to the template used to render detailed lead information.
:type template_name: str
:ivar permission_required: List of permissions required to access this view.
:type permission_required: list
"""
model = models.Lead
template_name = "crm/leads/lead_detail.html"
permission_required = ["inventory.view_lead"]
def get_context_data(self, **kwargs):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
context = super().get_context_data(**kwargs)
context["notes"] = models.Notes.objects.filter(
content_type__model="lead", object_id=self.object.id
)
email_qs = models.Email.objects.filter(
content_type__model="lead", object_id=self.object.id
)
context["emails"] = {
"sent": email_qs.filter(status="SENT"),
"draft": email_qs.filter(status="DRAFT"),
}
context["activities"] = models.Activity.objects.filter(
content_type__model="lead", object_id=self.object.id
)
context["tasks"] = models.Tasks.objects.filter(
content_type__model="lead", object_id=self.object.id
)
context["schedules"] = models.Schedule.objects.filter(
dealer=dealer,
content_type__model="lead",
object_id=self.object.id,
scheduled_by=self.request.user,
).order_by("-created_at")
context["status_history"] = models.LeadStatusHistory.objects.filter(
lead=self.object
)
context["transfer_form"] = forms.LeadTransferForm()
context["transfer_form"].fields["transfer_to"].queryset = (
models.Staff.objects.select_related("user")
.filter(
dealer=dealer,
user__groups__permissions__codename__contains="can_reassign_lead",
)
.exclude(user=self.request.user)
.distinct()
)
context["activity_form"] = forms.ActivityForm()
context["staff_task_form"] = forms.StaffTaskForm()
context["note_form"] = forms.NoteForm()
context["schedule_form"] = forms.ScheduleForm()
return context
@login_required
@permission_required("inventory.add_lead", raise_exception=True)
def lead_create(request, dealer_slug):
"""
Handles the creation of a new lead in the inventory system.
This function manages the rendering and processing of a form for creating a new
lead. It filters options for car models in the form based on the selected car
make. For POST requests, it validates and processes the submitted form data
and creates the lead if valid. It also creates a corresponding customer in the
ledger system if one does not exist for the provided email.
:param request: The HTTP request object containing request data.
:type request: HttpRequest
:return: An HTTP response object rendering the lead creation form or redirecting to the lead list page upon success.
:rtype: HttpResponse
"""
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
if request.method == "POST":
# get username for logging
user_username = (
request.user.username if request.user.is_authenticated else "anonymous"
)
# Log intent before trying to mark as paid
form = forms.LeadForm(request.POST)
# Filter car models based on the selected make (for POST requests)
if "id_car_make" in request.POST:
form.fields["id_car_model"].queryset = models.CarModel.objects.filter(
id_car_make=int(request.POST["id_car_make"])
)
try:
if form.is_valid():
instance = form.save(commit=False)
instance.dealer = dealer
instance.staff = form.cleaned_data.get("staff")
if instance.lead_type == "customer":
customer = models.Customer.objects.filter(
dealer=dealer,
email=instance.email
).first()
if not customer:
customer = models.Customer(
dealer=dealer,
address=instance.address,
phone_number=instance.phone_number,
email=instance.email,
first_name=instance.first_name,
last_name=instance.last_name,
active=False,
)
# customer.create_user_model(for_lead=True)
customer.create_customer_model(for_lead=True)
customer.save()
instance.customer = customer
if instance.lead_type == "organization":
organization = models.Organization.objects.filter(
dealer=dealer,
email=instance.email
).first()
if not organization:
organization = models.Organization(
dealer=dealer,
address=instance.address,
phone_number=instance.phone_number,
email=instance.email,
name=instance.first_name + " " + instance.last_name,
active=False,
)
# organization.create_user_model(for_lead=True)
organization.create_customer_model(for_lead=True)
organization.save()
instance.organization = organization
instance.next_action = LeadStatus.NEW
instance.save()
logger.info(
f"lead created successfully for dealer {dealer_slug} by user:{user_username}"
)
messages.success(request, _("Lead created successfully"))
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}"
)
messages.error(
request, f"Lead was not created ... : {str(form.errors)}"
)
return render(request, "crm/leads/lead_form.html", {"form": form})
except Exception as e:
messages.error(request, f"Lead was not created ... : {str(e)}")
form = forms.LeadForm()
form.fields["staff"].queryset = form.fields["staff"].queryset.filter(active=True)
form.filter_qs(dealer=dealer)
if make := request.GET.get("id_car_make", None):
qs = models.CarModel.objects.filter(
id_car_make=int(make)
)
form.fields["id_car_model"].queryset = qs
form.fields["id_car_model"].choices = [
(obj.id_car_model, obj.get_local_name()) for obj in qs
]
else:
dealer_make_list = models.DealersMake.objects.filter(dealer=dealer).values_list(
"car_make", flat=True
)
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")
.filter(
dealer=dealer,
user__groups__permissions__codename__contains="add_lead",
)
.distinct()
)
if request.is_staff:
form.initial["staff"] = request.staff
form.fields["staff"].widget.attrs.update(
{"readonly": "true", "required": "true"}
)
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
]
if first_make := qs.first():
qs = first_make.carmodel_set.all()
form.fields["id_car_model"].queryset = qs
form.fields["id_car_model"].choices = [
(obj.id_car_model, obj.get_local_name()) for obj in qs
]
return render(request, "crm/leads/lead_form.html", {"form": form})
@login_required
@permission_required("inventory.view_lead", raise_exception=True)
def lead_tracking(request, dealer_slug):
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
staff = (
models.Staff.objects.select_related("user")
.filter(dealer=dealer, user=request.user)
.first()
)
if staff:
qs = models.Lead.objects.filter(dealer=dealer, staff=staff)
else:
qs = models.Lead.objects.filter(dealer=dealer)
leads=qs
won = qs.filter(status="won")
new = qs.filter(status="new")
lose = qs.filter(status="lose")
negotiation = qs.filter(status="negotiation")
follow_up = qs.filter(next_action__in=["call", "meeting"])
context = {
"new": new,
"follow_up": follow_up,
"won": won,
"lose": lose,
"negotiation": negotiation,
"leads":leads,
"empty_state_value": _("lead"),
}
return render(request, "crm/leads/lead_tracking.html", context)
# @require_POST
@login_required
@permission_required("inventory.change_lead", raise_exception=True)
def update_lead_actions(request, dealer_slug):
# get the user info for logging
user_username = (
request.user.username if request.user.is_authenticated else "anonymous"
)
logger.debug(
f"User {user_username} is attempting to update lead actions "
f"for dealer '{dealer_slug}'. Request POST data: {request.POST.dict()}"
)
try:
lead_id = request.POST.get("lead_id")
current_action = request.POST.get("current_action")
next_action = request.POST.get("next_action")
next_action_date = request.POST.get("next_action_date", None)
lead = models.Lead.objects.get(id=lead_id)
if not all([lead_id, current_action, next_action]):
# Log for missing required fields
logger.warning(
f"User {user_username} submitted incomplete data to update lead actions "
f"for dealer '{dealer_slug}'. Missing fields: lead_id='{lead_id}', current_action='{current_action}', next_action='{next_action}'."
)
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
# )
# Get the lead
# Update lead fields
lead.status = current_action
lead.next_action = next_action
# Log before updating lead fields
logger.debug(
f"User {user_username} found Lead ID: {lead.pk} ('{lead.slug}') "
f"for update. Current action: '{current_action}', Next action: '{next_action}', Next action date: '{next_action_date}'."
)
# Parse the datetime string
try:
if next_action_date:
lead.next_action_date = next_action_date
next_action_datetime = datetime.strptime(
next_action_date, "%Y-%m-%dT%H:%M"
)
lead.next_action_date = timezone.make_aware(next_action_datetime)
logger.debug(
f"Lead ID: {lead.pk} next_action_date parsed to {lead.next_action_date}."
)
except ValueError as ve:
# Log for invalid date format
logger.warning(
f"submitted invalid date format ('{next_action_date}') "
f"for Lead ID: {lead.pk}. Error: {ve}"
)
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
# )
# Save the lead
lead.save()
# --- Logging for successful update (main try block success) ---
logger.info(
f"User {user_username} successfully updated Lead ID: {lead.pk} ('{lead.slug}'). "
f"New Status: '{lead.status}', Next Action: '{lead.next_action}', Next Action Date: '{lead.next_action_date}'."
)
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"}
# )
except models.Lead.DoesNotExist:
# --- Logging for Lead not found ---
logger.warning(
f"User {user_username} attempted to update non-existent Lead with ID: '{lead_id}' "
f"for dealer '{dealer_slug}'. Returning 404."
)
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:
involved_lead_id = request.POST.get("lead_id", "N/A")
logger.error(
f"User {user_username} encountered an unexpected error while updating Lead ID: '{involved_lead_id}' "
f"for dealer '{dealer_slug}'. Error: {e}",
exc_info=True, # CRUCIAL: Includes the full traceback
)
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.
This class is used to manage the process of updating Lead objects within the
application. It provides integration with Django's authentication and
permissions systems, ensuring that only authorized users can access this view.
Additionally, it customizes the behavior of the form to dynamically filter
available options for the 'id_car_model' field based on the related
'id_car_make' field.
:ivar model: Model class representing a Lead object.
:type model: models.Lead
:ivar form_class: Form class used for the Lead update form.
:type form_class: forms.LeadForm
:ivar template_name: Path to the template used for rendering the Lead update
form.
:type template_name: str
:ivar success_url: URL to redirect to upon successful Lead update.
:type success_url: str
:ivar permission_required: List of permissions required for accessing this view.
:type permission_required: list
"""
model = models.Lead
form_class = forms.LeadForm
template_name = "crm/leads/lead_form.html"
success_url = reverse_lazy("lead_list")
permission_required = ["inventory.change_lead"]
def get_form(self, form_class=None):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug"))
form = super().get_form(form_class)
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()
)
return form
def get_success_url(self):
return reverse_lazy(
"lead_list", kwargs={"dealer_slug": self.kwargs.get("dealer_slug")}
)
@login_required
@permission_required("inventory.delete_lead", raise_exception=True)
def LeadDeleteView(request, dealer_slug, slug):
"""
Handles the deletion of a Lead along with its associated customer and potentially
a related user account in the system. Ensures proper permissions and login
are required before the operation is performed. Provides a success message
after the deletion is complete.
:param request: The HTTP request object specific to the current user.
:param pk: The primary key identifier of the Lead to be deleted.
:return: An HTTP redirect response to the lead list page.
"""
# get the user info for logging
user_username = (
request.user.username if request.user.is_authenticated else "anonymous"
)
lead = get_object_or_404(models.Lead, slug=slug)
# Log intent before attempting deletion
logger.debug(
f"User {user_username} attempting to delete Lead ID: {lead.pk} ('{lead.slug}') "
f"and its associated customer/user for dealer '{dealer_slug}'."
)
try:
User.objects.get(email=lead.customer.email).delete()
lead.customer.delete()
# --- Single-line log for successful associated user/customer deletion ---
logger.info(
f"User {user_username} successfully deleted associated user and customer "
f"for Lead ID: {lead.pk} ('{lead.slug}') (Email: {lead.customer.email})."
)
except Exception as e:
# --- Single-line log for error during associated user/customer deletion ---
logger.error(
f"User {user_username} encountered an error deleting associated user/customer "
f"for Lead ID: {lead.slug} ('{lead.slug}') (Email: {getattr(lead.customer, 'email', 'N/A')}). " # Safely get email
f"Error: {e}",
exc_info=True,
)
print(e)
lead_id_final = lead.pk # Capture before deletion
lead_name_final = lead.slug
lead.delete()
# Log the final lead deletion, which happens unconditionally after the try-except
logger.info(
f"User {user_username} successfully deleted Lead ID: {lead_id_final} ('{lead_name_final}') "
f"for dealer '{dealer_slug}'."
)
messages.success(request, _("Lead deleted successfully"))
return redirect("lead_list", dealer_slug=dealer_slug)
# @login_required #TODO:delete later
# @permission_required("inventory.add_notes", raise_exception=True)
# def add_note_to_lead(request, dealer_slug, slug):
# """
# Adds a note to a specific lead. This view is accessible only to authenticated
# users. The function handles the POST request to create a new note associated
# with a lead. Upon successful submission, the note is linked to the provided lead,
# and the user is redirected to the lead's detail page. If the request method is
# not POST, it initializes an empty form to add a note.
# :param request: The HTTP request object
# :type request: HttpRequest
# :param pk: The primary key of the lead to which the note will be added
# :type pk: int
# :return: HTTP response object. Redirects to the lead detail page on successful
# note creation or renders the note form template for GET or invalid POST requests.
# :rtype: HttpResponse
# """
# lead = get_object_or_404(models.Lead, slug=slug)
# if request.method == "POST":
# form = forms.NoteForm(request.POST)
# if form.is_valid():
# note = form.save(commit=False)
# note.content_object = lead
# note.created_by = request.user
# note.save()
# messages.success(request, _("Note added successfully"))
# return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug)
# else:
# form = forms.NoteForm()
# return render(request, "crm/note_form.html", {"form": form, "lead": lead})
# @login_required
# @permission_required("inventory.add_notes", raise_exception=True)
# def add_note_to_opportunity(request, dealer_slug, slug):
# """
# Add a note to a specific opportunity identified by its primary key.
# This function handles the addition of a note to an existing opportunity
# by processing a POST request that includes the note content. If the note
# content is missing, an error message will be displayed. If the operation
# is successful, the note is saved, and a success message is returned.
# :param request: The HTTP request object representing the client's request.
# :param pk: The primary key of the Opportunity to which the note is to be added.
# :type pk: int
# :return: A redirect response to the detailed view of the opportunity.
# """
# opportunity = get_object_or_404(models.Opportunity, slug=slug)
# dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
# if request.method == "POST":
# notes = request.POST.get("notes")
# if not notes:
# messages.error(request, _("Notes field is required"))
# else:
# models.Notes.objects.create(
# dealer=dealer,
# content_object=opportunity,
# created_by=request.user,
# note=notes,
# )
# messages.success(request, _("Note added successfully"))
# return redirect(
# "opportunity_detail", dealer_slug=dealer_slug, slug=opportunity.slug
# )
@login_required
@permission_required("inventory.delete_notes", raise_exception=True)
def delete_note(request, dealer_slug, pk):
"""
Deletes a specific note created by the currently logged-in user and redirects
to the lead detail page. If the note does not exist or the user is not the creator,
a 404 error will be raised. A success message is displayed upon successful deletion
of the note.
:param request: The HTTP request object associated with the current request.
:type request: HttpRequest
:param pk: The primary key of the note to be deleted.
:type pk: int
:return: An HTTP redirection to the lead detail page of the corresponding note's lead.
:rtype: HttpResponseRedirect
"""
try:
note = get_object_or_404(models.Notes, pk=pk, created_by=request.user)
if isinstance(note.content_object, models.Customer):
url = "customer_detail"
slug = note.content_object.slug
elif isinstance(note.content_object, models.Lead):
url = "lead_detail"
slug = note.content_object.slug
if hasattr(note.content_object, "opportunity"):
url = "opportunity_detail"
slug = note.content_object.opportunity.slug
elif isinstance(note.content_object, models.Opportunity):
url = "opportunity_detail"
slug = note.content_object.slug
note.delete()
messages.success(request, _("Note deleted successfully."))
except Exception as e:
print("Errroooorrr: ",e)
print(url)
print(dealer_slug)
return redirect(url, dealer_slug=dealer_slug,slug=slug)
@login_required
@permission_required("inventory.change_lead", raise_exception=True)
def lead_convert(request, dealer_slug, slug):
"""
Converts a lead into a customer and creates a corresponding opportunity.
The function ensures that leads are not converted to customers more than once.
If the lead has already been converted, an error is displayed. Otherwise,
the lead is converted into a customer and a new opportunity is created.
Upon successful conversion, the user is redirected to the lead list view
and a success message is displayed.
:param request: The HTTP request object representing the user's request.
:type request: HttpRequest
:param pk: The primary key of the lead to be converted.
:type pk: int
:return: An HTTP response redirecting to the lead list view.
:rtype: HttpResponse
"""
lead = get_object_or_404(models.Lead, slug=slug)
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
if hasattr(lead, "opportunity"):
messages.error(request, _("Lead is already converted to customer"))
else:
customer = lead.convert_to_customer()
models.Opportunity.objects.create(
dealer=dealer,
customer=customer,
lead=lead,
probability=50,
stage=models.Stage.NEGOTIATION,
staff=lead.staff,
)
messages.success(request, _("Lead converted to customer successfully"))
return redirect("lead_list", dealer_slug=dealer_slug)
@login_required
@require_POST
@permission_required("inventory.add_schedule", raise_exception=True)
def schedule_event(request, dealer_slug, content_type, slug):
"""
Handles the scheduling of a lead for an appointment.
This function ensures that only staff members with the appropriate permissions
can schedule leads. If the user is not a staff member or does not have the
required inventory permissions, they are redirected with an appropriate error
message. The function creates an appointment request and an associated appointment
record upon successful submission of the scheduling form.
:param request: The HTTP request object containing metadata about the request.
:type request: HttpRequest
:param pk: The primary key of the lead to be scheduled.
:type pk: int
:return: A rendered template or a redirection to another view based on the request
method and validity of the form submission.
:rtype: HttpResponse
"""
user_username = (
request.user.username if request.user.is_authenticated else "anonymous"
)
# Log the attempt to retrieve the model dynamically
logger.debug(
f"User {user_username} attempting to retrieve model "
f"for content_type '{content_type}' for dealer '{dealer_slug}'."
)
try:
model = apps.get_model(f"inventory.{content_type}")
except LookupError:
# --- Single-line log for LookupError (Model not found) ---
logger.warning(
f"User {user_username} requested an invalid model content_type: '{content_type}'. "
f"Raising Http404 for dealer '{dealer_slug}'."
)
raise Http404("Model not found")
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
obj = get_object_or_404(model, slug=slug)
# if not request.is_staff:
# messages.error(request, _("You do not have permission to schedule."))
# return redirect(f"{content_type}_detail", dealer_slug=dealer_slug, slug=slug)
if request.method == "POST":
from django_q.models import Schedule as DjangoQSchedule
form = forms.ScheduleForm(request.POST)
if form.is_valid():
reminder = form.cleaned_data['reminder']
instance = form.save(commit=False)
instance.dealer = dealer
instance.content_object = obj
instance.scheduled_by = request.user
if obj.customer:
instance.customer = obj.customer.customer_model
elif obj.organization:
instance.cutsomer = obj.organization.customer_model
# service = Service.objects.get(name=instance.scheduled_type)
# # Log attempt to create AppointmentRequest
# logger.debug(
# f"User {user_username} attempting to create AppointmentRequest "
# f"for {content_type} ID: {obj.pk} ('{obj.slug}'). Service: '{service.name}', "
# f"Scheduled At: '{instance.scheduled_at}', Duration: '{instance.duration}'."
# )
# try:
# appointment_request = AppointmentRequest.objects.create(
# date=instance.scheduled_at.date(),
# service=service,
# staff_member=request.user.staffmember,
# )
# except ValidationError as e:
# messages.error(request, str(e))
# return redirect(request.META.get("HTTP_REFERER"))
# client = get_object_or_404(User, email=instance.customer.email)
# Create Appointment
# Appointment.objects.create(
# client=client,
# appointment_request=appointment_request,
# phone=instance.customer.phone,
# address=instance.customer.address_1,
# )
instance.save()
models.Activity.objects.create(
dealer=dealer,
content_object=obj,
notes=instance.notes,
created_by=request.user,
activity_type=instance.scheduled_type,
)
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
reminder_time = scheduled_at_aware - timezone.timedelta(minutes=15)
# Only schedule if the reminder time is in the future
# Reminder emails are scheduled to be sent 15 minutes before the scheduled time
if reminder_time > timezone.now():
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',
args=f'"{instance.pk}"',
schedule_type=DjangoQSchedule.ONCE,
next_run=reminder_time,
hook='inventory.tasks.log_email_status',
)
messages.success(request, _("Appointment Created Successfully"))
return redirect(f'{content_type}_detail',dealer_slug=dealer_slug, slug=slug)
else:
# Log for invalid form data
logger.warning(
f"User {user_username} submitted invalid schedule form data "
f"for Lead ID: {obj.pk} ('{obj.slug}'). Errors: {form.errors.as_json()}"
)
messages.error(request, f"Invalid form data: {str(form.errors)}")
return redirect(request.META.get("HTTP_REFERER"))
@login_required
@permission_required("inventory.change_lead", raise_exception=True)
def lead_transfer(request, dealer_slug, slug):
"""
Handles the transfer of a lead to a different staff member. This view is accessible
only to authenticated users with the 'inventory.change_lead' permission. If the
request method is POST and the form data is valid, the lead's staff is updated
accordingly, saved, and a success message is displayed. Otherwise, an error message
is shown. In both cases, the user is redirected to the lead listing page.
:param request: The HTTP request object.
:param pk: The primary key of the lead to be transferred.
:return: An HTTP redirect response to the lead list view.
"""
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
lead = get_object_or_404(models.Lead, slug=slug)
if request.method == "POST":
form = forms.LeadTransferForm(request.POST)
if form.is_valid():
lead.staff = form.cleaned_data["transfer_to"]
lead.save()
models.Notification.objects.create(
user=lead.staff.user,
message=f"You have been assigned a new lead: {lead.full_name}.",
)
messages.success(request, _("Lead transferred successfully"))
else:
messages.error(request, f"Invalid form data: {str(form.errors)}")
return redirect("lead_detail", dealer_slug=dealer.slug ,slug=lead.slug)
@login_required
def send_lead_email(request, dealer_slug, slug, email_pk=None):
"""
Handles sending emails related to a lead. Supports creating drafts, sending emails, and rendering
an email composition page. Changes on the lead or email objects, such as marking a lead as contacted
or an email as sent, are recorded in associated activity logs.
:param request: The HTTP request object. This contains user information, method type, and data such as
GET or POST payload used to draft or send the email appropriately.
Type: HttpRequest
:param pk: The primary key of the lead to which the email action is associated. It's used to retrieve
the lead object from the database.
Type: int
:param email_pk: Optional parameter representing the primary key of an email template. When provided,
the respective email content is pre-populated into the email composition form.
Defaults to None.
Type: Optional[int]
:return: When successfully sending an email, redirects the user to the lead list page. On draft mode
or email composition rendering, a response object is returned to render the respective page.
Type: HttpResponse
"""
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
lead = get_object_or_404(models.Lead, slug=slug)
status = request.GET.get("status")
user_username = (
request.user.username if request.user.is_authenticated else "anonymous"
)
if status == "draft":
models.Email.objects.create(
content_object=lead,
created_by=request.user,
from_email="manager@tenhal.com",
to_email=request.GET.get("to"),
subject=request.GET.get("subject"),
message=request.GET.get("message"),
status=models.EmailStatus.DRAFT,
)
models.Activity.objects.create(
dealer=dealer,
content_object=lead,
notes="Email Draft",
created_by=request.user,
activity_type=models.ActionChoices.EMAIL,
)
messages.success(request, _("Email Draft successfully"))
# try:
# if getattr(lead, "opportunity", None):
# # Log success when opportunity exists and redirecting
# logger.info(
# f"User {user_username} successfully drafted email for Lead ID: {lead.pk} ('{lead.slug}'). "
# f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail."
# )
# # response = HttpResponse(
# # redirect(
# # "opportunity_detail",
# # dealer_slug=dealer_slug,
# # slug=lead.opportunity.slug,
# # )
# # )
# # response["HX-Redirect"] = reverse(
# # "opportunity_detail", args=[lead.opportunity.slug]
# # )
# else:
# # Log success when no opportunity and redirecting to lead detail
# logger.info(
# f"User {user_username} successfully drafted email for Lead ID: {lead.pk} ('{lead.slug}'). "
# f"Lead has no Opportunity, redirecting to lead detail."
# )
# # response = HttpResponse()
# # response["HX-Redirect"] = reverse(
# # "lead_detail", dealer_slug=dealer_slug, slug=lead.slug
# # )
# return response
# except models.Lead.opportunity.RelatedObjectDoesNotExist:
# # --- Log when Lead.opportunity does not exist (Draft status) ---
# logger.info(
# f"User {user_username} drafted email for Lead ID: {lead.pk} ('{lead.slug}'). "
# f"Lead's opportunity does not exist. Redirecting to lead list."
# )
# return response
# return redirect("lead_list", dealer_slug=dealer.slug)
if request.method == "POST":
email_pk = request.POST.get("email_pk")
if email_pk not in [None, "None", ""]:
email = get_object_or_404(models.Email, pk=int(email_pk))
email.status = models.EmailStatus.SENT
email.save()
else:
models.Email.objects.create(
content_object=lead,
created_by=request.user,
from_email="manager@tenhal.com",
to_email=request.POST.get("to"),
subject=request.POST.get("subject"),
message=request.POST.get("message"),
status=models.EmailStatus.SENT,
)
send_email(
"manager@tenhal.com",
request.POST.get("to"),
request.POST.get("subject"),
request.POST.get("message"),
)
models.Activity.objects.create(
dealer=dealer,
content_object=lead,
notes="Email sent",
created_by=request.user,
activity_type=models.ActionChoices.EMAIL,
)
messages.success(request, _("Email sent successfully"))
response = HttpResponse()
response["HX-Refresh"] = "true"
return response
# try:
# if lead.opportunity:
# # Log success when opportunity exists and redirecting after sending email
# logger.info(
# f"User {user_username} successfully sent email for Lead ID: {lead.pk} ('{lead.slug}'). "
# f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail."
# )
# return response
# # return redirect(
# # "opportunity_detail",
# # dealer_slug=dealer_slug,
# # slug=lead.opportunity.slug,
# # )
# except models.Lead.opportunity.RelatedObjectDoesNotExist:
# # --- Log when Lead.opportunity does not exist (POST request for sending) ---
# logger.info(
# f"User {user_username} sent email for Lead ID: {lead.pk} ('{lead.slug}'). "
# f"Lead's opportunity does not exist. Redirecting to lead list."
# )
# return response
# return redirect("lead_list", dealer_slug=dealer_slug)
msg = f"""
السلام عليكم {lead.full_name},
شكراً لزيارتك لـ {lead.dealer.name}! لقد كان من دواعي سرورنا مساعدتك اليوم.
لقد أنشأنا ملفاً شخصياً لك في نظامنا لتتبع تفضيلاتك والسيارات التي تهتم بها. سنتواصل معك قريباً للمتابعة والإجابة على أي أسئلة أخرى قد تكون لديك.
في هذه الأثناء، لا تتردد في الاتصال بنا مباشرة أو زيارتنا مرة أخرى في أي وقت يناسبك.
نتطلع إلى مساعدتك في العثور على سيارتك القادمة!
تحياتي،
{lead.dealer.arabic_name}
{lead.dealer.address}
{lead.dealer.phone_number}
-----
Dear {lead.full_name},
Thank you for visiting {lead.dealer.name}! It was a pleasure to assist you today.
We've created a profile for you in our system to keep track of your preferences and the vehicles you're interested in. We'll be in touch shortly to follow up and answer any further questions you may have.
In the meantime, feel free to contact us directly at {lead.dealer.phone_number} or visit us again at your convenience.
We look forward to helping you find your next car!
Best regards,
{lead.dealer.name}
{lead.dealer.address}
{lead.dealer.phone_number}
"""
subject = ""
if email_pk:
email = get_object_or_404(models.Email, pk=email_pk)
msg = email.message
subject = email.subject
return render(
request,
"crm/leads/lead_send.html",
{"lead": lead, "message": msg, "subject": subject, "email_pk": email_pk},
)
class OpportunityCreateView(
LoginRequiredMixin, PermissionRequiredMixin, CreateView, SuccessMessageMixin
):
"""
Handles the creation of Opportunity instances through a form while enforcing
specific user access control and initial data population. This view ensures
an authenticated user can create opportunities tied to their dealer type and
allows pre-filling data when linked to an existing Lead.
Allows authenticated users to create a new Opportunity instance, associating it
with the requesting user's dealer type and prefilling some fields based on
existing Lead data, if applicable.
:ivar model: The database model associated with this view, which is used to
represent Opportunity data.
:type model: models.Opportunity
:ivar form_class: The form class used for creating new Opportunity instances.
:type form_class: forms.OpportunityForm
:ivar template_name: The template used to render the Opportunity creation form.
:type template_name: str
"""
model = models.Opportunity
form_class = forms.OpportunityForm
template_name = "crm/opportunities/opportunity_form.html"
success_message = _("Opportunity created successfully.")
permission_required = ["inventory.add_opportunity"]
def get_initial(self):
initial = super().get_initial()
dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug"))
if self.kwargs.get("slug", None):
lead = models.Lead.objects.get(slug=self.kwargs.get("slug"), dealer=dealer)
initial["lead"] = lead
initial["stage"] = models.Stage.QUALIFICATION
return initial
def form_valid(self, form):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug"))
instance = form.save(commit=False)
instance.dealer = dealer
instance.staff = instance.lead.staff
instance.save()
instance.lead.convert_to_customer()
instance.lead.save()
return super().form_valid(form)
def get_form(self, form_class=None):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug"))
form = super().get_form(form_class)
form.fields["car"].queryset = models.Car.objects.filter(
dealer=dealer, status="available", marked_price__gt=0
)
if self.request.is_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
)
return form
def get_success_url(self):
return reverse_lazy(
"opportunity_detail",
kwargs={
"dealer_slug": self.kwargs.get("dealer_slug"),
"slug": self.object.slug,
},
)
class OpportunityUpdateView(
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
):
"""
Handles the update functionality for Opportunity objects.
This class-based view is responsible for handling the update of existing
Opportunity instances. It uses a Django form that is specified by the
`form_class` attribute and renders a template to display and process the
update form. Access to this view is restricted to authenticated users, as
it inherits from `LoginRequiredMixin`.
It defines the model to be updated and the form template to be used. Upon
successful update, it redirects the user to the detail page of the updated
opportunity instance.
:ivar model: The model associated with this view. Represents the Opportunity model.
:type model: django.db.models.Model
:ivar form_class: The form class used to manage the Opportunity update process.
:type form_class: django.forms.ModelForm
:ivar template_name: The path to the template used to render the opportunity
update form.
:type template_name: str
"""
model = models.Opportunity
form_class = forms.OpportunityForm
template_name = "crm/opportunities/opportunity_form.html"
success_message = _("Opportunity updated successfully.")
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
)
form.fields["lead"].queryset = models.Lead.objects.filter(
dealer=dealer, staff=staff
)
return form
def get_success_url(self):
return reverse_lazy(
"opportunity_detail",
kwargs={
"dealer_slug": self.kwargs.get("dealer_slug"),
"slug": self.object.slug,
},
)
class OpportunityStageUpdateView(
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
):
"""
Handles the update functionality for Opportunity objects.
This class-based view is responsible for handling the update of existing
Opportunity instances. It uses a Django form that is specified by the
`form_class` attribute and renders a template to display and process the
update form. Access to this view is restricted to authenticated users, as
it inherits from `LoginRequiredMixin`.
It defines the model to be updated and the form template to be used. Upon
successful update, it redirects the user to the detail page of the updated
opportunity instance.
:ivar model: The model associated with this view. Represents the Opportunity model.
:type model: django.db.models.Model
:ivar form_class: The form class used to manage the Opportunity update process.
:type form_class: django.forms.ModelForm
:ivar template_name: The path to the template used to render the opportunity
update form.
:type template_name: str
"""
model = models.Opportunity
form_class = forms.OpportunityStageForm
success_message = _("Opportunity Stage updated successfully.")
permission_required = ["inventory.change_opportunity"]
def get_success_url(self):
return reverse_lazy(
"opportunity_detail",
kwargs={
"dealer_slug": self.kwargs.get("dealer_slug"),
"slug": self.object.slug,
},
)
class OpportunityDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""
Handles the detailed view of an Opportunity object.
Displays detailed information about a specific opportunity, including its
status, stage, notes, activities, and related emails. This view utilizes
a form to manage and update the status and stage of the opportunity
through HTTPX requests. Suitable for use in CRM applications.
:ivar model: The model for which this view is being implemented.
:type model: models.Opportunity
:ivar template_name: The template used to render this view.
:type template_name: str
:ivar context_object_name: The context variable name for the model object in the template.
:type context_object_name: str
"""
model = models.Opportunity
template_name = "crm/opportunities/opportunity_detail.html"
context_object_name = "opportunity"
permission_required = ["inventory.view_opportunity"]
def get_context_data(self, **kwargs):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug"))
context = super().get_context_data(**kwargs)
form = forms.OpportunityStatusForm()
url = reverse(
"opportunity_update_status",
kwargs={
"dealer_slug": self.kwargs["dealer_slug"],
"slug": self.object.slug,
},
)
form.fields["status"].widget.attrs["hx-get"] = url
form.fields["stage"].widget.attrs["hx-get"] = url
form.fields["stage"].initial = self.object.stage
context["status_form"] = form
context["lead_notes"] = models.Notes.objects.filter(
content_type__model="lead", object_id=self.object.id
).order_by("-created")
context["lead_notes"] = models.Notes.objects.filter(
content_type__model="lead", object_id=self.object.lead.pk
).order_by("-created")
context["notes"] = models.Notes.objects.filter(
content_type__model="opportunity", object_id=self.object.id
).order_by("-created")
context["lead_activities"] = models.Activity.objects.filter(
content_type__model="lead", object_id=self.object.lead.pk
)
context["activities"] = models.Activity.objects.filter(
content_type__model="opportunity", object_id=self.object.id
)
lead_email_qs = models.Email.objects.filter(
content_type__model="lead", object_id=self.object.lead.pk
)
email_qs = models.Email.objects.filter(
content_type__model="opportunity", object_id=self.object.id
)
context["emails"] = {
"sent": email_qs.filter(status="SENT"),
"draft": email_qs.filter(status="DRAFT"),
}
context["lead_emails"] = {
"sent": lead_email_qs.filter(status="SENT"),
"draft": lead_email_qs.filter(status="DRAFT"),
}
context["staff_task_form"] = forms.StaffTaskForm()
context["note_form"] = forms.NoteForm()
context["lead_tasks"] = models.Tasks.objects.filter(
content_type__model="lead", object_id=self.object.lead.pk
)
context["tasks"] = models.Tasks.objects.filter(
content_type__model="opportunity", object_id=self.object.id
)
qs = models.Schedule.objects.filter(
dealer=dealer,
content_type__model="lead",
object_id=self.object.lead.id,
scheduled_by=self.request.user,
)
context["schedules"] = qs | models.Schedule.objects.filter(
dealer=dealer,
content_type__model="opportunity",
object_id=self.object.id,
scheduled_by=self.request.user,
)
context["schedules"] = context["schedules"].order_by("-created_at")
context["upcoming_events"] = {
"schedules": qs.filter(scheduled_at__gt=timezone.now())[:5],
}
context["schedule_form"] = forms.ScheduleForm()
context["stage_form"] = forms.OpportunityStageForm()
return context
class OpportunityListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
model = models.Opportunity
template_name = "crm/opportunities/opportunity_list.html"
context_object_name = "opportunities"
paginate_by = 30
permission_required = ["inventory.view_opportunity"]
permission_required = ["inventory.view_opportunity"]
def get_queryset(self):
dealer = get_user_type(self.request)
if self.request.is_dealer:
queryset = models.Opportunity.objects.filter(dealer=dealer)
elif self.request.is_staff:
staff = self.request.staff
queryset = models.Opportunity.objects.filter(dealer=dealer, lead__staff=staff)
# Stage filter
stage = self.request.GET.get("stage")
print(stage)
if stage:
queryset = queryset.filter(stage=stage)
# Sorting
sort = self.request.GET.get("sort", "newest")
if sort == "newest":
queryset = queryset.order_by("-created")
elif sort == "highest":
queryset = queryset.order_by("-expected_revenue")
elif sort == "closing":
queryset = queryset.order_by("expected_close_date")
# Search filter
search = self.request.GET.get("q")
if search:
queryset = queryset.filter(
Q(customer__first_name__icontains=search)
| Q(customer__last_name__icontains=search)
| Q(customer__email__icontains=search)
)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["stage_choices"] = models.Stage.choices
context["empty_state_value"] = _("opportunity")
return context
def get_template_names(self):
return self.template_name
@login_required
@permission_required("inventory.delete_opportunity", raise_exception=True)
def delete_opportunity(request, dealer_slug, pk):
"""
Deletes an opportunity object from the database and redirects to the opportunity
list view. If the opportunity with the specified primary key is not found, a 404
error will be raised. Displays a success message after deletion.
:param request: The HTTP request object containing metadata about the request.
:type request: HttpRequest
:param pk: The primary key of the opportunity object to be deleted.
:type pk: int
:return: An HTTP response redirecting to the opportunity list view.
:rtype: HttpResponse
"""
get_object_or_404(models.Dealer, slug=dealer_slug)
opportunity = get_object_or_404(models.Opportunity, pk=pk)
opportunity.delete()
messages.success(request, _("Opportunity deleted successfully"))
return redirect("opportunity_list", dealer_slug=dealer_slug)
@login_required
@permission_required("inventory.change_opportunity", raise_exception=True)
def opportunity_update_status(request, dealer_slug, slug):
"""
Update the status and/or stage of a specific Opportunity instance. This is a
view function, which is generally tied to a URL endpoint in a Django application.
The function is accessible only to authenticated users due to the
`@login_required` decorator.
The function retrieves the `Opportunity` instance based on the primary key
in the URL, updates the status or stage of the instance based on query
parameters in the request, saves the changes, and then redirects to the
detail view of the `Opportunity`. Additionally, a success message is displayed,
and a custom header (`HX-Refresh`) is added to the response.
:param request: The HTTP request object containing details of the user's
request, such as query parameters for status and stage. This is a
mandatory parameter provided by Django's view function framework.
:type request: HttpRequest
:param pk: The primary key of the `Opportunity` instance to be updated.
:type pk: int
:return: An HTTP response object that redirects to the updated Opportunity's
detail page, with a success message and the "HX-Refresh" header to trigger
frontend behavior.
:rtype: HttpResponse
"""
opportunity = get_object_or_404(models.Opportunity, slug=slug)
status = request.GET.get("status")
stage = request.GET.get("stage")
if status:
opportunity.status = status
if stage:
opportunity.stage = stage
opportunity.save()
messages.success(request, _("Opportunity status updated successfully"))
response = HttpResponse(
redirect("opportunity_detail", dealer_slug=dealer_slug, slug=opportunity.slug)
)
response["HX-Refresh"] = "true"
return response
class NotificationListView(LoginRequiredMixin, ListView):
"""
Handles the display and management of notification history for a user.
This class provides functionality to display a paginated list of user notifications
sorted by their creation time in descending order. It ensures that the user is logged
in before accessing the notifications and filters the displayed notifications specific
to the logged-in user.
:ivar model: Specifies the model to retrieve data from.
:type model: models.Notification
:ivar template_name: Specifies the template to render the notifications page.
:type template_name: str
:ivar context_object_name: The name of the variable accessible in the template to
refer to the list of notifications.
:type context_object_name: str
:ivar paginate_by: Defines the number of notifications to display per page.
:type paginate_by: int
:ivar ordering: Defines the default ordering of notifications.
:type ordering: str
"""
model = models.Notification
template_name = "crm/notifications_history.html"
context_object_name = "notifications"
paginate_by = 20
ordering = "-created"
def get_queryset(self):
return models.Notification.objects.filter(user=self.request.user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
user_notifications = self.get_queryset()
# Calculate the number of total, read and unread notifications
context['total_count'] = user_notifications.count()
context['read_count'] = user_notifications.filter(is_read=True).count()
context['unread_count'] = user_notifications.filter(is_read=False).count()
return context
class ItemServiceCreateView(
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView
):
"""
Manages the creation of additional services with permission checks, success messages,
and validation of VAT and dealer-specific pricing.
This class-based view facilitates the creation of additional service items within
a custom Django application framework. It includes mixins for login-required access,
permission control, and success message handling. The form is validated to apply VAT
rates to taxable services and associates the service with the dealer determined from
the logged-in user's context.
:ivar model: The model representing additional services.
:type model: models.AdditionalServices
:ivar form_class: The form class used for creating a service.
:type form_class: forms.AdditionalServiceForm
:ivar template_name: The template used to render the service creation page.
:type template_name: str
:ivar success_url: The redirect URL upon successful form submission.
:type success_url: str
:ivar success_message: The success message displayed upon successful form submission.
:type success_message: str
:ivar context_object_name: The context name used to refer to the service object in templates.
:type context_object_name: str
:ivar permission_required: The permissions required for accessing this view.
:type permission_required: list
"""
model = models.AdditionalServices
form_class = forms.AdditionalServiceForm
template_name = "items/service/service_create.html"
success_message = _("Service created successfully")
context_object_name = "service"
permission_required = ["inventory.add_additionalservices"]
def form_valid(self, form):
dealer = get_user_type(self.request)
vat = models.VatRate.objects.get(dealer=dealer, is_active=True)
form.instance.dealer = dealer
# if form.instance.taxable:
# form.instance.price = (form.instance.price * vat.rate) + form.instance.price
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy(
"item_service_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
)
class ItemServiceUpdateView(
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
):
"""
Handles update functionality for additional services in the system.
This view enables authenticated and authorized users to update instances of
the `AdditionalServices` model. It requires the user to have the necessary
permissions and provides success feedback upon successful update. Additionally,
it calculates and updates the price of the service based on the tax rate if
the service is taxable.
:ivar model: The model associated with this view.
:type model: models.AdditionalServices
:ivar form_class: The form class used for handling AdditionalService updates.
:type form_class: forms.AdditionalServiceForm
:ivar template_name: The template used for rendering the service update page.
:type template_name: str
:ivar success_url: The URL to redirect after a successful update.
:type success_url: str
:ivar success_message: The success message displayed after a successful update.
:type success_message: str
:ivar context_object_name: The context variable name for the object to be displayed.
:type context_object_name: str
:ivar permission_required: Permissions required for accessing this view.
:type permission_required: list[str]
"""
model = models.AdditionalServices
form_class = forms.AdditionalServiceForm
template_name = "items/service/service_create.html"
success_message = _("Service updated successfully")
context_object_name = "service"
permission_required = ["inventory.change_additionalservices"]
def form_valid(self, form):
dealer = get_user_type(self.request)
vat = models.VatRate.objects.get(dealer=dealer, is_active=True)
uom = dealer.entity.get_uom_all().filter(unit_abbr=form.instance.uom).first()
form.instance.dealer = dealer
form.instance.uom = uom.name
form.instance.item.uom = uom
# if form.instance.taxable:
# form.instance.price = (form.instance.price * vat.rate) + form.instance.price
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy(
"item_service_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
)
class ItemServiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"""
Handles the listing of additional services for a dealer.
This class is responsible for displaying a paginated list of additional
services specific to a dealer. It enforces login, permission checking,
and customizes the queryset to return only services associated with the
currently logged-in dealer.
:ivar model: The model representing the additional services data.
:type model: Model
:ivar template_name: Path to the template used for rendering the view.
:type template_name: str
:ivar context_object_name: The name of the context variable for the
object list.
:type context_object_name: str
:ivar paginate_by: Number of items displayed per page.
:type paginate_by: int
:ivar permission_required: List of permissions required to access the
view.
:type permission_required: list
"""
model = models.AdditionalServices
template_name = "items/service/service_list.html"
context_object_name = "services"
paginate_by = 30
permission_required = ["inventory.view_additionalservices"]
def get_queryset(self):
dealer = get_user_type(self.request)
query = self.request.GET.get("q")
qs = models.AdditionalServices.objects.filter(dealer=dealer).all()
if query:
qs = qs.filter(Q(name__icontains=query)|
Q(id__icontains=query)|
Q(uom__icontains=query)
)
return qs
class ItemExpenseCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView):
"""
Represents a view for creating item expense entries.
This class is responsible for handling the creation of expense items in the
application. It integrates with Django's class-based views and includes
customizations specific to the current user and their associated entity.
Permission control ensures only authorized users can create expenses.
:ivar model: The database model used for this view.
:type model: ItemModel
:ivar form_class: The form class used to render and validate input.
:type form_class: ExpenseItemCreateForm
:ivar template_name: The path to the template used to render the view.
:type template_name: str
:ivar success_url: The URL to redirect to upon successful form submission.
:type success_url: str
:ivar permission_required: A list of permissions required to access the view.
:type permission_required: list
"""
model = ItemModel
form_class = ExpenseItemCreateForm
template_name = "items/expenses/expense_create.html"
success_url = reverse_lazy("item_expense_list")
success_message = _("Expense created successfully")
permission_required = ["django_ledger.add_itemmodel"]
def get_form_kwargs(self):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
kwargs = super().get_form_kwargs()
kwargs["entity_slug"] = dealer.entity.slug
kwargs["user_model"] = dealer.entity.admin
return kwargs
def form_valid(self, form):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
form.instance.entity = dealer.entity
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy(
"item_expense_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
)
class ItemExpenseUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
"""
Manages the update view for item expenses.
This class enables authenticated users with the necessary permissions to update
information related to item expenses. It integrates form handling and permission
requirements, ensuring entity-specific contextual data is appended to the form
during updates.
:ivar model: The model associated with the update operation.
:type model: ItemModel
:ivar form_class: The form used to validate and process updates.
:type form_class: ExpenseItemUpdateForm
:ivar template_name: Path to the template used for rendering the update view.
:type template_name: str
:ivar success_url: URL where the user is redirected after a successful update.
:type success_url: str
:ivar permission_required: List of permissions required to access this view.
:type permission_required: list[str]
"""
model = ItemModel
form_class = ExpenseItemUpdateForm
template_name = "items/expenses/expense_update.html"
success_url = reverse_lazy("item_expense_list")
permission_required = ["django_ledger.change_itemmodel"]
def get_form_kwargs(self):
dealer = get_user_type(self.request)
kwargs = super().get_form_kwargs()
kwargs["entity_slug"] = dealer.entity.slug
kwargs["user_model"] = dealer.entity.admin
return kwargs
def form_valid(self, form):
dealer = get_user_type(self.request)
form.instance.entity = dealer.entity
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy(
"item_expense_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
)
class ItemExpenseListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"""
Handles the display of a list of item expenses.
This class-based view is responsible for displaying a paginated list of item
expenses related to a specific entity. It uses the Django generic `ListView`
class and integrates with permissions and login requirements. It retrieves
data through the `get_queryset` method by invoking a specific function on
the user's associated entity object.
:ivar model: The model associated with the view.
:type model: type[ItemModel]
:ivar template_name: The template used to render the view.
:type template_name: str
:ivar context_object_name: The context variable name used in the template.
:type context_object_name: str
:ivar paginate_by: The number of items displayed per page.
:type paginate_by: int
:ivar permission_required: The list of required permissions to access the view.
:type permission_required: list[str]
"""
model = ItemModel
template_name = "items/expenses/expenses_list.html"
context_object_name = "expenses"
paginate_by = 4
permission_required = ["django_ledger.view_itemmodel"]
def get_queryset(self):
dealer = get_user_type(self.request)
query = self.request.GET.get("q")
qs = dealer.entity.get_items_expenses()
if query:
qs = apply_search_filters(qs, query)
return qs
class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"""
Provides a view for listing bills.
This class-based view is used to display a list of bills. It ensures that
the user is logged in and has the appropriate permissions to view the list
of bills. The view retrieves the bills associated with the dealer's entity
and displays them using the specified template.
:ivar model: The model associated with the view.
:type model: django.db.models.Model
:ivar template_name: The template used to render the view.
:type template_name: str
:ivar context_object_name: Name of the context variable containing the list of bills.
:type context_object_name: str
:ivar permission_required: List of permissions required to access the view.
:type permission_required: list[str]
"""
model = BillModel
template_name = "ledger/bills/bill_list.html"
context_object_name = "bills"
permission_required = ["django_ledger.view_billmodel"]
def get_queryset(self):
dealer = get_user_type(self.request)
qs = dealer.entity.get_bills()
query = self.request.GET.get("q")
if query:
qs = qs.filter(Q(bill_number__icontains=query)|
Q(vendor__vendor_name__icontains=query))
return qs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["entity"] = get_user_type(self.request).entity
return context
class BillModelCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView):
template_name = "bill/bill_create.html"
PAGE_TITLE = _("Create Bill")
permission_required = "django_ledger.add_billmodel"
extra_context = {
"page_title": PAGE_TITLE,
"header_title": PAGE_TITLE,
"header_subtitle_icon": "uil:bill",
}
for_purchase_order = False
for_estimate = False
success_message = _("Bill created successfully")
# Get user info for logging
def get(self, request, dealer_slug, **kwargs):
user_username = (
request.user.username if request.user.is_authenticated else "anonymous"
)
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
if not request.user.is_authenticated:
return HttpResponseForbidden()
if self.for_estimate and "ce_pk" in self.kwargs:
estimate_qs = EstimateModel.objects.for_entity(
entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin
)
estimate_model: EstimateModel = get_object_or_404(
estimate_qs, uuid__exact=self.kwargs["ce_pk"]
)
if not estimate_model.can_bind():
logger.warning(
f"User {user_username} attempted to create bill from Estimate ID: {estimate_model.uuid}, "
f"but estimate cannot be bound (already bound/invalid state). Returning 404."
)
return HttpResponseNotFound("404 Not Found")
logger.debug(
f"User {user_username} confirmed Estimate ID: {estimate_model.uuid} "
f"can be bound for bill creation."
)
return super(BillModelCreateView, self).get(request, dealer_slug, **kwargs)
def get_context_data(self, **kwargs):
context = super(BillModelCreateView, self).get_context_data(**kwargs)
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
user_username = (
self.request.user.username
if self.request.user.is_authenticated
else "anonymous"
)
if self.for_purchase_order:
po_pk = self.kwargs["po_pk"]
po_item_uuids_qry_param = self.request.GET.get("item_uuids")
if po_item_uuids_qry_param:
try:
po_item_uuids = po_item_uuids_qry_param.split(",")
# --- Single-line log for successful parsing ---
logger.debug(
f"User {user_username} successfully parsed item_uuids from query param "
f"for Purchase Order ID: {po_pk} "
)
except:
# --- Single-line log for error during parsing ---
logger.warning(
f"User {user_username}submitted invalid item_uuids query parameter "
f"'{po_item_uuids_qry_param}' for Purchase Order ID: {po_pk} "
f"Returning BadRequest."
)
return HttpResponseBadRequest()
else:
logger.warning(
f"User {user_username} attempted to create bill from Purchase Order ID: {po_pk} "
f"Returning BadRequest."
)
return HttpResponseBadRequest()
po_qs = PurchaseOrderModel.objects.for_entity(
entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin
).prefetch_related("itemtransactionmodel_set")
po_model: PurchaseOrderModel = get_object_or_404(po_qs, uuid__exact=po_pk)
po_itemtxs_qs = po_model.itemtransactionmodel_set.filter(
bill_model__isnull=True, uuid__in=po_item_uuids
)
context["po_model"] = po_model
context["po_itemtxs_qs"] = po_itemtxs_qs
form_action = (
reverse(
"bill-create-po",
kwargs={
"dealer_slug": self.kwargs["dealer_slug"],
"entity_slug": self.kwargs["entity_slug"],
"po_pk": po_model.uuid,
},
)
+ f"?item_uuids={po_item_uuids_qry_param}"
)
elif self.for_estimate:
estimate_qs = EstimateModel.objects.for_entity(
entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin
)
estimate_uuid = self.kwargs["ce_pk"]
estimate_model: EstimateModel = get_object_or_404(
estimate_qs, uuid__exact=estimate_uuid
)
form_action = reverse(
"bill-create-estimate",
kwargs={
"dealer_slug": self.kwargs["dealer_slug"],
"entity_slug": self.kwargs["entity_slug"],
"ce_pk": estimate_model.uuid,
},
)
else:
form_action = reverse(
"bill-create",
kwargs={
"dealer_slug": self.kwargs["dealer_slug"],
"entity_slug": self.kwargs["entity_slug"],
},
)
context["form_action_url"] = form_action
return context
def get_initial(self):
return {"date_draft": get_localdate()}
def get_form(self, form_class=None):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
return BillModelCreateForm(entity_model=dealer.entity, **self.get_form_kwargs())
def form_valid(self, form):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
bill_model: BillModel = form.save(commit=False)
ledger_model, bill_model = bill_model.configure(
entity_slug=dealer.entity,
user_model=dealer.entity.admin,
commit_ledger=True,
)
if self.for_estimate:
ce_pk = self.kwargs["ce_pk"]
estimate_model_qs = EstimateModel.objects.for_entity(
entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin
)
estimate_model = get_object_or_404(estimate_model_qs, uuid__exact=ce_pk)
bill_model.bind_estimate(estimate_model=estimate_model, commit=False)
elif self.for_purchase_order:
po_pk = self.kwargs["po_pk"]
item_uuids = self.request.GET.get("item_uuids")
if not item_uuids:
return HttpResponseBadRequest()
item_uuids = item_uuids.split(",")
po_qs = PurchaseOrderModel.objects.for_entity(
entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin
)
po_model: PurchaseOrderModel = get_object_or_404(po_qs, uuid__exact=po_pk)
user_username = (
self.request.user.username
if self.request.user.is_authenticated
else "anonymous"
)
try:
bill_model.can_bind_po(po_model, raise_exception=True)
logger.info(
f"User {user_username} successfully validated binding of Bill ID: {getattr(bill_model, 'pk', 'N/A')} "
f"to Purchase Order ID: {getattr(po_model, 'pk', 'N/A')}."
)
except ValidationError as e:
messages.add_message(
self.request,
message=e.message,
level=messages.ERROR,
extra_tags="is-danger",
)
# --- Single-line log for ValidationError during binding validation ---
logger.warning(
f"User {user_username} encountered a validation error "
f"while attempting to bind Bill ID: {getattr(bill_model, 'pk', 'N/A')} "
f"to Purchase Order ID: {getattr(po_model, 'pk', 'N/A')}. Error: {e.message}"
)
return self.render_to_response(self.get_context_data(form=form))
po_model_items_qs = po_model.itemtransactionmodel_set.filter(
uuid__in=item_uuids
)
if po_model.is_contract_bound():
bill_model.ce_model_id = po_model.ce_model_id
bill_model.update_amount_due()
bill_model.get_state(commit=True)
bill_model.clean()
bill_model.save()
po_model_items_qs.update(bill_model=bill_model)
return HttpResponseRedirect(self.get_success_url())
return super(BillModelCreateView, self).form_valid(form)
def get_success_url(self):
entity_slug = self.kwargs["entity_slug"]
if self.for_purchase_order:
po_pk = self.kwargs["po_pk"]
return reverse(
"purchase_order_update",
kwargs={
"dealer_slug": self.kwargs["dealer_slug"],
"entity_slug": entity_slug,
"po_pk": po_pk,
},
)
elif self.for_estimate:
return reverse(
"customer-estimate-detail",
kwargs={
"dealer_slug": self.kwargs["dealer_slug"],
"entity_slug": entity_slug,
"ce_pk": self.kwargs["ce_pk"],
},
)
bill_model: BillModel = self.object
return reverse(
"bill-detail",
kwargs={
"dealer_slug": self.kwargs["dealer_slug"],
"entity_slug": entity_slug,
"bill_pk": bill_model.uuid,
},
)
class BillModelDetailView(BillModelDetailViewBase):
template_name = "bill/bill_detail.html"
permission_required = ["django_ledger.view_billmodel"]
def get_context_data(self, **kwargs):
context = super(BillModelDetailView, self).get_context_data(**kwargs)
context["dealer"] = self.request.dealer
return context
def get_queryset(self):
qs = super().get_queryset()
return qs.select_related(
"ledger",
"ledger__entity",
"vendor",
"cash_account",
"prepaid_account",
"unearned_account",
"cash_account__coa_model",
"prepaid_account__coa_model",
"unearned_account__coa_model",
)
class BillModelUpdateView(BillModelUpdateViewBase):
permission_required = ["django_ledger.change_billmodel"]
class OrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"""
Represents a view for listing sale orders.
This class provides the functionality to display a list of sale orders
within a web template. It includes permission-based access control, and
retrieves sale orders corresponding to the entity of the logged-in dealer.
:ivar model: The model associated with this view.
:type model: models.SaleOrder
:ivar template_name: The name of the template to be rendered.
:type template_name: str
:ivar context_object_name: The context variable name to be used for the object list.
:type context_object_name: str
:ivar permission_required: A list of permissions required to access this view.
:type permission_required: list[str]
"""
model = models.SaleOrder
template_name = "sales/orders/order_list.html"
context_object_name = "orders"
paginate_by = 30
permission_required = ["inventory.view_saleorder"]
def get_queryset(self):
dealer = get_user_type(self.request)
qs = super().get_queryset().select_related("customer", "estimate", "invoice")
return qs.filter(estimate__entity=dealer.entity)
@login_required
@permission_required("django_ledger.change_estimatemodel", raise_exception=True)
def send_email_view(request, dealer_slug, pk):
"""
View function to send an email for an estimate. This function allows authenticated and
authorized users to send an email containing the estimate details to the customer.
The email includes a link to preview the estimate and a message template in multiple
languages. If the estimate does not have any associated items, the user will receive
an error message. Upon successfully sending the email, the estimate is marked as reviewed.
:param request: The HttpRequest object containing metadata about the request.
:type request: HttpRequest
:param pk: The primary key of the estimate to be sent via email.
:type pk: int
:return: An HttpResponseRedirect to the estimate detail view.
:rtype: HttpResponseRedirect
:raises Http404: If the estimate with the given primary key does not exist.
"""
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
estimate = get_object_or_404(EstimateModel, pk=pk)
if not estimate.get_itemtxs_data()[0]:
messages.error(request, _("Quotation has no items"))
return redirect("estimate_detail", dealer_slug=dealer_slug, pk=estimate.pk)
link = request.build_absolute_uri(
reverse_lazy(
"estimate_preview", kwargs={"dealer_slug": dealer_slug, "pk": estimate.pk}
)
)
msg = f"""
السلام عليكم،
عزيزي {estimate.customer.customer_name}،
يسعدني أن أشارككم عرض السعر الذي طلبتموه. يرجى الاطلاع على التفاصيل الكاملة والأسعار من خلال الرابط أدناه.
حرصنا على أن يكون عرضنا مناسباً وشفافاً. إذا كانت لديكم أي استفسارات أو ملاحظات، فلا تترددوا في التواصل معنا.
رابط عرض السعر:
{link}
نأمل أن ينال العرض إعجابكم ونتطلع إلى بدء العمل قريباً!
تحياتي،
{dealer.get_local_name()}
{dealer.phone_number}
Haikal | هيكل
-----
Dear {estimate.customer.customer_name},
I hope this email finds you well.
Following up on our conversation, I'm excited to share the quotation for your review. Please find the detailed pricing and information by clicking on the link below.
We've done our best to provide you with a fair and competitive offer. If you have any questions or would like to discuss it further, please don't hesitate to reach out.
Quotation Link:
{link}
We look forward to hearing from you and hopefully moving forward with your project!
Best regards,
{dealer.get_local_name()}
{dealer.phone_number}
Haikal
"""
# subject = _("Quotation")
send_email(
str(settings.DEFAULT_FROM_EMAIL),
estimate.customer.email,
"عرض سعر - Quotation",
msg,
)
# estimate.mark_as_review()
messages.success(request, _("Email sent successfully"))
return redirect("estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk)
# errors
def custom_page_not_found_view(request, exception=None):
"""
Custom handler for 404 errors that renders a custom HTML page
upon encountering a Page Not Found error.
:param request: The HttpRequest object associated with the request.
:type request: HttpRequest
:param exception: The exception that triggered the 404 error, if any.
:type exception: Exception, optional
:return: An HttpResponse object rendering the "errors/404.html" template.
:rtype: HttpResponse
"""
return render(request, "errors/404.html", {})
def custom_error_view(request, exception=None):
"""
Handles rendering the custom error page for HTTP 500 errors.
This function is called when an unhandled exception occurs in the application. It renders
a predefined template for server errors, providing a consistent error page layout
to the users.
:param request: The HTTP request instance which triggered the error.
:type request: HttpRequest
:param exception: The exception that caused the error to trigger. Defaults to None.
:type exception: Exception, optional
:return: An HttpResponse object representing the rendered error page.
:rtype: HttpResponse
"""
return render(request, "errors/500.html", {})
def custom_permission_denied_view(request, exception=None):
"""
Handles the custom view for 403 Forbidden permission denied errors. This
function renders a custom template for the error page when users are denied
permission to access a particular resource or view.
:param request: Django HttpRequest object containing metadata about the request.
:type request: HttpRequest
:param exception: Optional exception that caused the 403 error.
Defaults to None.
:type exception: Exception | None
:return: HttpResponse object rendering the 403.html error page.
:rtype: HttpResponse
"""
return render(request, "errors/403.html", {})
def custom_bad_request_view(request, exception=None):
"""
Handles custom bad request responses by rendering a specified HTML
template for 400 status errors.
:return: Rendered HTTP response rendering the error page.
:rtype: HttpResponse
"""
return render(request, "errors/400.html", {})
# BALANCE SHEET
class BaseBalanceSheetRedirectView(RedirectView):
"""
Handles redirection for balance sheet views of entities.
This class is specifically designed to redirect users to the
appropriate year's balance sheet for an entity. It determines
the URL based on the current year and the provided entity slug
in the request. Additionally, it provides a URL for login redirection
if required.
:ivar url: The original URL or base URL to redirect users.
:type url: str
"""
def get_redirect_url(self, *args, **kwargs):
year = get_localdate().year
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
return reverse(
"entity-bs-year",
kwargs={
"dealer_slug": self.kwargs["dealer_slug"],
"entity_slug": self.kwargs["entity_slug"],
"year": year,
},
)
def get_login_url(self):
return reverse("account_login")
class FiscalYearBalanceSheetViewBase(AuthorizedEntityMixin, FiscalYearBalanceSheetView):
"""
Defines a base view for the fiscal year balance sheet.
This class serves as a base view for displaying the balance sheet of a fiscal
year. It provides functionality for rendering the associated template and
managing user login requirements. It integrates with Django's security system
to ensure appropriate access control.
:ivar template_name: The template used for rendering the balance sheet view.
:type template_name: str
"""
template_name = "ledger/reports/balance_sheet.html"
def get_login_url(self):
return reverse("account_login")
class QuarterlyBalanceSheetView(FiscalYearBalanceSheetViewBase, QuarterlyReportMixIn):
"""
Represents a quarterly balance sheet view.
The purpose of this class is to provide a representation and handling
for the balance sheet data specific to fiscal quarters. It extends from
`FiscalYearBalanceSheetViewBase` to inherit year-level balance sheet
operations and incorporates functionality from `QuarterlyReportMixIn`
to manage quarterly-specific logic. Additionally, the `DjangoLedgerSecurityMixIn`
is used to handle security features related to Django ledger operations.
:ivar fiscal_year: The fiscal year associated with the quarterly balance sheet.
:type fiscal_year: int
:ivar quarter: The specific quarter (e.g., Q1, Q2, Q3, Q4) for the balance sheet.
:type quarter: int
:ivar balance_data: The data structure representing the detailed financial
information for the balance sheet of the quarter.
:type balance_data: dict
"""
class MonthlyBalanceSheetView(
FiscalYearBalanceSheetViewBase,
MonthlyReportMixIn,
):
"""
Represents the view for the monthly balance sheet.
This class is responsible for handling the presentation and processing of
monthly balance sheet data. It incorporates necessary mix-ins to provide
functionality specific to fiscal year balance sheet viewing, monthly reporting,
and security validation within the Django ledger context.
:ivar fiscal_year: Stores the fiscal year information associated with the
balance sheet.
:type fiscal_year: int
:ivar monthly_reports: Holds a collection of monthly report instances that
pertain to the balance sheet.
:type monthly_reports: List[MonthlyReport]
:ivar user_permissions: Manages user-specific permissions for accessing and
modifying the balance sheet data.
:type user_permissions: Dict[str, Any]
"""
class DateBalanceSheetView(
FiscalYearBalanceSheetViewBase,
DateReportMixIn,
):
"""
Represents a balance sheet view for a specific date.
DateBalanceSheetView extends functionality for displaying balance sheets
specific to certain dates. This class combines features from various mixins
to enforce security, handle date-based reporting, and provide fiscal year
context. It is used in scenarios where financial reports for a particular
date are required to ensure accurate and filtered viewing of accounting
data.
:ivar fiscal_year: Represents the fiscal year context for the balance sheet.
:type fiscal_year: FiscalYear
:ivar report_date: The specific date for which the balance sheet report
is generated.
:type report_date: datetime.date
:ivar is_authenticated: Indicates whether the user accessing the
balance sheet has been authenticated.
:type is_authenticated: bool
:ivar account_data: Contains processed account data used in the balance sheet
for computations or display purposes.
:type account_data: dict
"""
# Income Statement -----------
class BaseIncomeStatementRedirectViewBase(
AuthorizedEntityMixin, BaseIncomeStatementRedirectView
):
"""
The BaseIncomeStatementRedirectViewBase class provides functionality for handling
redirects in the context of income statements. This class combines features from
both BaseIncomeStatementRedirectView and DjangoLedgerSecurityMixIn to ensure
secure handling of data and user authentication.
This class operates by calculating the current year, determining the user's dealer
type, and generating a URL to redirect to an income statement view associated with a
specific entity for the calculated year. Additionally, it defines a method to generate
the login URL for unauthenticated users.
:ivar request: The HTTP request object containing user session and other request-related data.
:type request: HttpRequest
"""
def get_redirect_url(self, *args, **kwargs):
year = get_localdate().year
dealer = get_object_or_404(models.Dealer, slug=kwargs["dealer_slug"])
return reverse(
"entity-ic-year",
kwargs={
"dealer_slug": dealer.slug,
"entity_slug": dealer.entity.slug,
"year": year,
},
)
def get_login_url(self):
return reverse("account_login")
class FiscalYearIncomeStatementViewBase(
AuthorizedEntityMixin, FiscalYearIncomeStatementView
):
"""
Represents a base view for fiscal year income statement.
This class serves as a base view for generating and rendering the fiscal
year income statement within the application. It combines multiple
functionalities, including template rendering and permission handling,
to provide a secure and user-friendly interface.
:ivar template_name: Path to the HTML template used for rendering the
income statement view.
:type template_name: str
:ivar permission_required: List of permissions required to access the view.
:type permission_required: list[str]
"""
template_name = "ledger/reports/income_statement.html"
permission_required = ["django_ledger.view_ledgermodel"]
def get_login_url(self):
return reverse("account_login")
class QuarterlyIncomeStatementView(
FiscalYearIncomeStatementViewBase, QuarterlyReportMixIn
):
"""
Represents a detailed view for a quarterly income statement.
This class is responsible for providing a structured view of a company's
quarterly income statement. It combines the functionality of multiple
base classes to deliver a comprehensive representation of quarterly report
data while ensuring integration with Django Ledger security protocols.
The class serves as a specialized view tied to specific fiscal data,
enabling efficient management and rendering of quarterly financial reports.
:ivar fiscal_year: The fiscal year associated with the quarterly income
statement.
:type fiscal_year: int
:ivar quarter: The quarter number (e.g., 1 for Q1, 2 for Q2, etc.) of the
income statement.
:type quarter: int
:ivar revenue: The total revenue recorded in the quarterly income statement.
:type revenue: float
:ivar net_income: The net income calculated for the quarter.
:type net_income: float
:ivar expenses: The total expenses incurred during the quarter.
:type expenses: float
"""
class MonthlyIncomeStatementView(
FiscalYearIncomeStatementViewBase,
MonthlyReportMixIn,
):
"""
Represents the view for a monthly income statement in the financial
application.
This class is designed to support the presentation and display of
financial income statement data on a monthly basis. It incorporates
functionality from multiple mixins to include specific fiscal year
operations, monthly report behaviors, and security aspects tailored
to the Django Ledger system.
:ivar fiscal_year: The fiscal year associated with the income
statement.
:type fiscal_year: int
:ivar month: The month for which the income statement is viewed.
:type month: int
:ivar data: The financial data contained in the income statement.
:type data: dict
:ivar authorized_user: The user authorized to access this income
statement view.
:type authorized_user: str
"""
class DateModelIncomeStatementView(
FiscalYearIncomeStatementViewBase,
DateReportMixIn,
):
"""
Represents a detailed view of an income statement for a fiscal year with additional
capabilities for handling dates and security integrations.
This class is a specialized combination of views and mixins designed to manage income
statements with added functionality for date-based operations and Django-based ledger
security. It inherits behaviors for fiscal year reports, date handling, and security
enforcements, providing a cohesive interface for managing income statement data.
:ivar fiscal_year_data: Contains detailed income statement data for a specific fiscal year.
:type fiscal_year_data: dict
:ivar report_date: Represents the date of the associated report.
:type report_date: datetime.date
:ivar user_permissions: Tracks security permissions associated with the income statement for
authenticated users.
:type user_permissions: dict
"""
# Cash Flow -----------
class BaseCashFlowStatementRedirectViewBase(
AuthorizedEntityMixin, BaseCashFlowStatementRedirectView
):
"""
Base class for handling cash flow statement redirection views.
This class is designed to manage the redirection of users to the
appropriate cash flow statement view for a particular year and entity.
It incorporates security features and functionalities specific to dealers
or entities by extending relevant mixins.
:ivar request: The HTTP request object associated with the view.
:type request: HttpRequest
"""
def get_redirect_url(self, *args, **kwargs):
year = get_localdate().year
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
return reverse(
"entity-cf-year",
kwargs={
"dealer_slug": dealer.slug,
"entity_slug": dealer.entity.slug,
"year": year,
},
)
def get_login_url(self):
return reverse("account_login")
class FiscalYearCashFlowStatementViewBase(
AuthorizedEntityMixin, FiscalYearCashFlowStatementView
):
"""
Represents a base view for fiscal year cash flow statements.
This class is utilized to handle the rendering and permissions control
for fiscal year cash flow statement views. It inherits functionality
from FiscalYearCashFlowStatementView and DjangoLedgerSecurityMixIn
to integrate with the Django framework's security and reporting
mechanisms.
:ivar template_name: Template path to render the view.
:type template_name: str
:ivar permission_required: List of permissions required for accessing
the view.
:type permission_required: list
"""
template_name = "ledger/reports/cash_flow_statement.html"
permission_required = ["django_ledger.view_ledgermodel"]
def get_login_url(self):
return reverse("account_login")
class QuarterlyCashFlowStatementView(
FiscalYearCashFlowStatementViewBase,
QuarterlyReportMixIn,
):
"""
Represents a view model for quarterly cash flow statements.
This class is designed to encapsulate and handle data related to
quarterly cash flow statements, extending the functionality from
base classes and mix-ins for specific behaviors. It combines features
from fiscal year cash flow statements, quarterly reporting utilities,
and Django ledger security mechanisms to provide comprehensive support
for managing and displaying financial data for quarterly cash flows.
:ivar fiscal_year: The fiscal year associated with the cash flow
statement.
:type fiscal_year: int
:ivar quarter: The quarter associated with the cash flow statement,
typically denoted as an integer from 1 to 4.
:type quarter: int
:ivar cash_flow_data: A structure containing detailed cash flow
information for the given quarter and fiscal year.
:type cash_flow_data: dict
:ivar is_audited: Boolean flag indicating whether the quarterly cash
flow statement has been audited.
:type is_audited: bool
"""
class MonthlyCashFlowStatementView(
FiscalYearCashFlowStatementViewBase,
MonthlyReportMixIn,
):
"""
Represents a view for monthly cash flow statements.
This class combines the functionality of fiscal year cash flow statement views,
monthly reporting capabilities, and security features related to Django Ledger.
It is utilized to generate or retrieve cash flow statements on a monthly basis,
ensuring seamless integration with other components that extend or implement
financial reporting logic.
:ivar attribute1: Description of attribute1.
:type attribute1: type
:ivar attribute2: Description of attribute2.
:type attribute2: type
"""
class DateCashFlowStatementView(
FiscalYearCashFlowStatementViewBase,
DateReportMixIn,
):
"""
Representation of a detailed view for a cash flow statement associated with a specific fiscal year,
including reporting capabilities and security features. This class extends functionality from
multiple mix-in classes to provide a comprehensive cash flow statement representation with date-specific
reporting.
:ivar field1: Description of field1.
:type field1: type
:ivar field2: Description of field2.
:type field2: type
"""
# Dashboard
class EntityModelDetailHandlerViewBase(
EntityModelDetailHandlerView,
):
"""
Handles detailed views for Entity Models along with redirection logic
customized for different users and units.
This class extends functionality from `EntityModelDetailHandlerView` and
integrates additional security measures using `DjangoLedgerSecurityMixIn`.
It provides a mechanism to construct redirection URLs based on the specifics
of the current request, such as the user's dealer type, the unit slug, and the
current localized date. Typically used for redirecting to appropriate dashboards
with monthly data filtered by either entity or unit.
:ivar request: The HTTP request object associated with the current view.
:type request: HttpRequest
"""
def get_redirect_url(self, *args, **kwargs):
loc_date = get_localdate()
dealer = get_user_type(self.request)
unit_slug = self.get_unit_slug()
if unit_slug:
return reverse(
"unit-dashboard-month",
kwargs={
"entity_slug": dealer.entity.slug,
"unit_slug": unit_slug,
"year": loc_date.year,
"month": loc_date.month,
},
)
return reverse(
"entity-dashboard-month",
kwargs={
"entity_slug": dealer.entity.slug,
"year": loc_date.year,
"month": loc_date.month,
},
)
class EntityModelDetailBaseViewBase(
EntityModelDetailBaseView,
):
"""
Represents a base view that extends functionality for displaying detailed
dashboard information about an entity model. Handles context population for
dynamic elements like charts and titles specific to the entity or an optional
unit.
This class is designed as part of a Django application, utilizing mixins for
enhanced security and inheriting a basic entity model detail view. It is
customized to render a specific dashboard template and manage context data
for charts and other display elements relevant to the entity.
:ivar template_name: Path to the HTML template used for rendering the
dashboard.
:type template_name: str
"""
template_name = "ledger/reports/dashboard.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
dealer = get_user_type(self.request)
entity_model: EntityModel = dealer.entity
context["page_title"] = entity_model.name
context["header_title"] = entity_model.name
context["header_subtitle"] = _("Dashboard")
context["header_subtitle_icon"] = "mdi:monitor-dashboard"
unit_slug = context.get("unit_slug", self.get_unit_slug())
KWARGS = dict(entity_slug=self.kwargs["entity_slug"])
if unit_slug:
KWARGS["unit_slug"] = unit_slug
url_pointer = "entity" if not unit_slug else "unit"
context["pnl_chart_id"] = f"djl-entity-pnl-chart-{randint(10000, 99999)}"
context["pnl_chart_endpoint"] = reverse(
f"django_ledger:{url_pointer}-json-pnl", kwargs=KWARGS
)
context["payables_chart_id"] = (
f"djl-entity-payables-chart-{randint(10000, 99999)}"
)
context["payables_chart_endpoint"] = reverse(
f"django_ledger:{url_pointer}-json-net-payables", kwargs=KWARGS
)
context["receivables_chart_id"] = (
f"djl-entity-receivables-chart-{randint(10000, 99999)}"
)
context["receivables_chart_endpoint"] = reverse(
f"django_ledger:{url_pointer}-json-net-receivables", kwargs=KWARGS
)
return context
class FiscalYearEntityModelDashboardView(
EntityModelDetailBaseViewBase,
):
"""
Represents a dashboard view for fiscal year entity models.
This class serves as a view to display details related to fiscal year
entity models while enforcing permissions and login requirements.
It integrates security features and provides functionalities specific
to managing fiscal year entity models within the dashboard.
:ivar permission_required: List of permissions required to access the view.
:type permission_required: list
"""
permission_required = ["django_ledger.view_ledgermodel"]
def get_login_url(self):
return reverse("account_login")
class QuarterlyEntityDashboardView(
FiscalYearEntityModelDashboardView,
QuarterlyReportMixIn,
):
"""
Represents a dashboard view for quarterly entities.
This class integrates the functionalities of `FiscalYearEntityModelDashboardView`,
`QuarterlyReportMixIn`, and `DjangoLedgerSecurityMixIn` to provide a dashboard
view that displays and organizes quarterly financial metrics and reports for a
specific fiscal year entity. It facilitates secure access to financial data and
provides interfaces to analyze quarterly reports.
:ivar quarters: A list of quarters included in the dashboard view.
:type quarters: list
:ivar entity: The entity for which the dashboard view is generated.
:type entity: object
:ivar fiscal_year: The fiscal year associated with the dashboard.
:type fiscal_year: int
:ivar reporting_data: A collection of data specifically prepared for
quarterly reporting.
:type reporting_data: dict
"""
class MonthlyEntityDashboardView(
FiscalYearEntityModelDashboardView,
MonthlyReportMixIn,
):
"""
Represents a dashboard view for a specific entity's monthly report.
This class combines functionalities from the base dashboard view for
fiscal year entities, as well as specific monthly report and security
features, to provide a comprehensive dashboard handler. It is designed
to work within the Django Ledger system, facilitating secured and
contextual data display tailored for monthly reporting of financial
entities.
:ivar fiscal_year: Represents the fiscal year associated with the
dashboard view.
:type fiscal_year: int
:ivar entity_id: The unique identifier for the financial entity
being reported on in the dashboard.
:type entity_id: int
:ivar report_month: The specific month for which the report is
generated and displayed.
:type report_month: str
:ivar user_permissions: Permissions associated with the currently
authenticated user to enforce security on
sensitive actions or data.
:type user_permissions: list
"""
class DateEntityDashboardView(
FiscalYearEntityModelDashboardView,
DateReportMixIn,
):
"""
Represents a dashboard view for date-based entity data visualization.
This class integrates date-based information into the dashboard view,
extending the functionality of `FiscalYearEntityModelDashboardView`.
It utilizes mixins to incorporate report generation and security features
specific to the Django Ledger framework.
:ivar fiscal_year: The fiscal year associated with the dashboard data.
:type fiscal_year: FiscalYear
:ivar date_report_data: Data pertaining to the date-specific reports displayed.
:type date_report_data: dict
:ivar user_permissions: The security permissions granted to the user for viewing
or interacting with the dashboard.
:type user_permissions: set
"""
class PayableNetAPIView(EntityUnitMixIn, View):
"""
Handles the retrieval of net payable data for authenticated users.
The PayableNetAPIView is designed to process GET requests for retrieving
a summary of unpaid bills, specifically for authenticated users associated
with an entity. This view ensures that only authorized users, based on their
authentication state and type, can access this sensitive financial data. It
leverages the DjangoLedgerSecurityMixIn and EntityUnitMixIn for security
enhancements and entity-specific processing.
:ivar http_method_names: HTTP methods supported by this view.
:type http_method_names: list[str]
"""
http_method_names = ["get"]
def get(self, request, *args, **kwargs):
if request.user.is_authenticated:
dealer = get_user_type(request)
bill_qs = BillModel.objects.for_entity(
entity_slug=dealer.entity.slug,
user_model=dealer.entity.admin,
).unpaid()
net_summary = accruable_net_summary(bill_qs)
net_payables = {"net_payable_data": net_summary}
return JsonResponse({"results": net_payables})
return JsonResponse({"message": _("Unauthorized")}, status=401)
class ReceivableNetAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View):
"""
Handles the retrieval of net receivables summary for authenticated users.
This view is part of a ledger system and is designed to provide a net summary
of unpaid invoices for an authenticated user. If the user is not authenticated,
the view will return an unauthorized response. The view supports only GET
requests.
Class Attributes:
:ivar http_method_names: Restricts the allowed HTTP methods for this view.
:type http_method_names: list[str]
"""
http_method_names = ["get"]
def get(self, request, *args, **kwargs):
if request.user.is_authenticated:
dealer = get_user_type(request)
invoice_qs = InvoiceModel.objects.for_entity(
entity_slug=dealer.entity.slug,
user_model=dealer.entity.admin,
).unpaid()
net_summary = accruable_net_summary(invoice_qs)
net_receivable = {"net_receivable_data": net_summary}
return JsonResponse({"results": net_receivable})
return JsonResponse({"message": _("Unauthorized")}, status=401)
class PnLAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View):
"""
APIView for handling Profit and Loss (PnL) data retrieval.
This class provides an endpoint to handle the retrieval of Profit and Loss data
for an entity. It uses authentication to validate the user's access and retrieves
PnL data based on the user's entity and optional query parameters such as dates.
The retrieved data is organized and returned in a JSON format.
:ivar http_method_names: Restricts HTTP methods that can be used with this view.
:type http_method_names: list[str]
"""
http_method_names = ["get"]
def get(self, request, *args, **kwargs):
if request.user.is_authenticated:
dealer = get_user_type(request)
entity = EntityModel.objects.for_user(user_model=dealer.entity.admin).get(
slug__exact=dealer.entity.slug
)
unit_slug = self.get_unit_slug()
io_digest = entity.digest(
user_model=self.request.user,
unit_slug=unit_slug,
equity_only=True,
signs=False,
by_period=True,
process_groups=True,
from_date=self.request.GET.get("fromDate"),
to_date=self.request.GET.get("toDate"),
# todo: For PnL to display proper period values must not use closing entries.
use_closing_entries=False,
)
io_data = io_digest.get_io_data()
group_balance_by_period = io_data["group_balance_by_period"]
group_balance_by_period = dict(
sorted((k, v) for k, v in group_balance_by_period.items())
)
entity_data = {
f"{month_name[k[1]]} {k[0]}": {d: float(f) for d, f in v.items()}
for k, v in group_balance_by_period.items()
}
entity_pnl = {
"entity_slug": entity.slug,
"entity_name": entity.name,
"pnl_data": entity_data,
}
return JsonResponse({"results": entity_pnl})
return JsonResponse({"message": _("Unauthorized")}, status=401)
# class EmployeeCalendarView(LoginRequiredMixin, ListView):
# """
# Provides a view for displaying the employee's calendar in a list format.
# Displays a list of appointments for logged-in users. This view ensures that
# only appointments relevant to the logged-in user as a dealer or staff member
# are displayed. Supports search functionality to filter displayed appointments
# based on provided query parameters.
# :ivar template_name: Path to the HTML template used to render the view.
# :type template_name: str
# :ivar model: Model object to interact with appointments data.
# :type model: Appointment
# :ivar context_object_name: Name of the context variable that contains the
# list of appointments in the template.
# :type context_object_name: str
# """
# template_name = "crm/employee_calendar.html"
# model = Appointment
# context_object_name = "appointments"
# def get_queryset(self):
# query = self.request.GET.get("q")
# staff = getattr(self.request, "staff", None)
# if staff:
# appointments = Appointment.objects.filter(
# appointment_request__staff_member=staff,
# ppointment_request__date__gt=timezone.now(),
# )
# appointments = Appointment.objects.filter(
# appointment_request__date__gt=timezone.now()
# )
# return apply_search_filters(appointments, query)
def apply_search_filters(queryset, query):
"""
Apply search filters to a queryset based on a query string.
This function filters the provided queryset by applying a Q object for all
CharField, TextField, or EmailField fields in the model. It checks if the
query exists within these fields using case-insensitive containment (icontains).
If the query string is empty or None, the original queryset is returned
without any modifications.
:param queryset: The initial queryset to apply the search filters on.
:param query: The search string to match against the model fields. If None
or an empty string, no filtering is applied.
:return: A filtered queryset that satisfies the search condition.
"""
if not query:
return queryset
search_filters = Q()
model = queryset.model
for field in model._meta.get_fields():
if hasattr(field, "attname") and field.get_internal_type() in [
"CharField",
"TextField",
"EmailField",
]:
search_filters |= Q(**{f"{field.name}__icontains": query})
return queryset.filter(search_filters).distinct()
class CarListViewTable(LoginRequiredMixin, ExportMixin, SingleTableView):
"""
Displays a table view of cars for a user with proper permissions.
This class-based view leverages functionality for login-required access, data
exporting, and integration with a single table representation. It is designed
to fetch and display a list of car objects related to a specific dealer in
a tabular format. The view also renders a predefined template to display
the data appropriately.
:ivar model: Specifies the model for the table view (Car).
:type model: models.Car
:ivar table_class: Represents the table class to be used for rendering
the data in tabular format.
:type table_class: tables.CarTable
:ivar template_name: Defines the template to be used for rendering the view.
:type template_name: str
"""
model = models.Car
table_class = tables.CarTable
template_name = "inventory/car_list_table.html"
def get_queryset(self):
dealer = get_user_type(self.request)
return models.Car.objects.select_related(
"finances", "colors__exterior", "colors__interior"
).filter(dealer=dealer)
@login_required
def DealerSettingsView(request, slug):
"""
Handles dealer settings view where dealers can update their financial and
payment account settings. This view ensures validation and reassigns form
fields dynamically based on the dealer's account roles.
:param request: The HTTP request object received from the client. This parameter
contains data including HTTP headers, session, and POST data if applicable.
:type request: HttpRequest
:param pk: Primary key representing the dealer for whom the settings are being
retrieved or modified. This identifier is used to fetch or update dealer-
specific settings from the database.
:type pk: int
:return: An HTTP response rendering the dealer settings form or redirecting
to the dealer detail view after successful form submission.
:rtype: HttpResponse
"""
dealer_setting = get_object_or_404(models.DealerSettings, dealer__slug=slug)
dealer = get_user_type(request)
if request.method == "POST":
form = forms.DealerSettingsForm(request.POST, instance=dealer_setting)
if form.is_valid():
instance = form.save(commit=False)
instance.dealer = dealer
instance.save()
messages.success(request, _("Settings updated"))
return redirect("dealer_detail", slug=dealer.slug)
else:
print(form.errors)
form = forms.DealerSettingsForm(instance=dealer_setting, initial={"dealer": dealer})
form.fields[
"invoice_cash_account"
].queryset = dealer.entity.get_all_accounts().filter(role=roles.ASSET_CA_CASH)
form.fields[
"invoice_prepaid_account"
].queryset = dealer.entity.get_all_accounts().filter(
role=roles.ASSET_CA_RECEIVABLES
)
form.fields[
"invoice_unearned_account"
].queryset = dealer.entity.get_all_accounts().filter(
role=roles.LIABILITY_CL_DEFERRED_REVENUE
)
form.fields["bill_cash_account"].queryset = dealer.entity.get_all_accounts().filter(
role=roles.ASSET_CA_CASH
)
form.fields[
"bill_prepaid_account"
].queryset = dealer.entity.get_all_accounts().filter(role=roles.ASSET_CA_PREPAID)
form.fields[
"bill_unearned_account"
].queryset = dealer.entity.get_all_accounts().filter(
role=roles.LIABILITY_CL_ACC_PAYABLE
)
return render(request, "account/user_settings.html", {"form": form})
@login_required
def schedule_cancel(request, dealer_slug, pk):
"""
Cancel a schedule by updating its status to "Canceled". The function is protected
by a login requirement, ensuring only authenticated users can execute it. It
retrieves the schedule object by its primary key, updates its status, saves the
changes, and returns an HTTP response with status code 200.
:param request: The HTTP request object representing the user's request.
:type request: HttpRequest
:param pk: The primary key of the schedule to be canceled.
:type pk: int
:return: An HTTP response object with a 200 status code upon successful execution.
:rtype: HttpResponse
"""
get_object_or_404(models.Dealer, slug=dealer_slug)
schedule = get_object_or_404(models.Schedule, pk=pk)
schedule.status = "Canceled"
schedule.save()
response = HttpResponse()
response.status_code = 200
return response
@login_required
@permission_required("inventory.change_dealer", raise_exception=True)
def assign_car_makes(request, dealer_slug):
"""
Assigns car makes to a dealer.
This function handles both the display and processing of a form that allows
a dealer to assign or modify their associated car makes. If the request
method is POST, it validates and saves the submitted form data. If the
method is not POST, it displays a form prefilled with the existing car makes
associated with the dealer.
:param request: The HTTP request object containing information about the
current request.
:type request: HttpRequest
:return: A rendered HTML response for GET requests, or a redirect to the
dealer detail page after successful form submission.
:rtype: HttpResponse
"""
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
if request.method == "POST":
form = forms.DealersMakeForm(request.POST, dealer=dealer)
if form.is_valid():
makes = form.cleaned_data["car_makes"]
create_accounts_for_make(dealer, makes)
form.save()
return redirect("dealer_detail", slug=dealer.slug)
else:
print(form.errors)
else:
# Pre-fill the form with existing selections
existing_car_makes = models.DealersMake.objects.filter(
dealer=dealer
).values_list("car_make", flat=True)
form = forms.DealersMakeForm(
initial={"car_makes": existing_car_makes}, dealer=dealer
)
for choice in form.fields["car_makes"].choices:
print(choice[0].instance)
break
return render(request, "dealers/assign_car_makes.html", {"form": form})
class LedgerModelListView(
LoginRequiredMixin, PermissionRequiredMixin, ListView, ArchiveIndexView
):
"""
Provides a view for listing ledger entries in the system.
This class-based view combines the functionality of LoginRequiredMixin,
ListView, and ArchiveIndexView to display a filtered and ordered
list of LedgerModel entries based on the user's type and entity. It
also allows toggling between different subsets of data, such as
all entries, current entries, or visible entries.
:ivar model: The model that this view displays.
:type model: type[LedgerModel]
:ivar context_object_name: The name of the context variable containing the list of LedgerModel instances.
:type context_object_name: str
:ivar template_name: The template used to render this view.
:type template_name: str
:ivar date_field: The date field used for ordering and archive functionality.
:type date_field: str
:ivar ordering: Default ordering for the queryset.
:type ordering: str
:ivar show_all: Determines whether all ledger entries should be shown.
:type show_all: bool
:ivar show_current: Determines whether only current ledger entries should be shown.
:type show_current: bool
:ivar show_visible: Determines whether only visible ledger entries should be shown.
:type show_visible: bool
:ivar allow_empty: Allows rendering of the page even if the queryset is empty.
:type allow_empty: bool
"""
model = LedgerModel
context_object_name = "ledgers"
template_name = "ledger/ledger/ledger_list.html"
permission_required = ["django_ledger.view_ledgermodel"]
date_field = "created"
ordering = "-created"
show_all = False
show_current = False
show_visible = False
allow_empty = True
paginate_by = 30
def get_queryset(self):
qs = super().get_queryset()
qs = qs.filter(entity=self.request.entity)
qs = qs.select_related("billmodel", "invoicemodel")
qs = qs.order_by("-created")
if self.show_all:
return qs
if self.show_current:
qs = qs.current()
if self.show_visible:
qs = qs.visible()
return qs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["entity_slug"] = self.request.dealer.entity.slug
return context
class LedgerModelDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""
This class provides a detailed view of a specific ledger entry.
The LedgerModelDetailView is implemented as a Django DetailView, enabling it to
retrieve and display detailed information about a single ledger instance.
It ensures that only authenticated users have access to this view by using the
LoginRequiredMixin.
:ivar model: The model associated with this view, representing ledger entries.
:type model: type[LedgerModel]
:ivar context_object_name: The name of the context variable available in the
template for the detailed ledger.
:type context_object_name: str
:ivar template_name: The path to the template used to render the detailed ledger view.
:type template_name: str
"""
model = LedgerModel
context_object_name = "ledger"
template_name = "ledger/ledger/ledger_detail.html"
permission_required = "django_ledger.view_ledgermodel"
class LedgerModelCreateView(LedgerModelCreateViewBase,SuccessMessageMixin):
"""
Handles the creation of LedgerModel entities.
This class provides the logic to manage the creation process of
LedgerModel instances. It determines the appropriate form for creating
a LedgerModel, validates the submitted form, and redirects the user
upon success. The view is specific to a particular user type (dealer).
:ivar template_name: Specifies the path to the template used for the
form rendering.
:type template_name: str
"""
template_name = "ledger/ledger/ledger_form.html"
permission_required = ["django_ledger.add_ledgermodel"]
success_message = _("Ledger created successfully")
def get_form(self, form_class=None):
return LedgerModelCreateForm(
entity_slug=self.request.dealer.entity.slug,
user_model=self.request.entity.admin,
**self.get_form_kwargs(),
)
def form_valid(self, form):
form.fields["entity"] = self.request.dealer.entity
return super().form_valid(form)
def get_success_url(self):
messages.success(self.request, self.success_message)
return reverse(
"ledger_list",
kwargs={
"dealer_slug": self.kwargs["dealer_slug"],
"entity_slug": self.kwargs["entity_slug"],
},
)
class LedgerModelModelActionView(LedgerModelModelActionViewBase):
"""
Represents a view for handling actions related to the Ledger model.
This class extends the LedgerModelModelActionViewBase and is responsible for
providing functionality to determine the redirection URL after performing
specific actions on a Ledger model instance.
:ivar template_name: Name of the template used for rendering the view.
:type template_name: str
:ivar model: The model associated with this view.
:type model: type[Model]
"""
def get_redirect_url(self, *args, **kwargs):
return reverse(
"ledger_list",
kwargs={
"dealer_slug": self.kwargs["dealer_slug"],
"entity_slug": self.kwargs["entity_slug"],
},
)
@login_required
@permission_required("django_ledger.delete_ledgermodel", raise_exception=True)
def LedgerModelDeleteView(request, dealer_slug,entity_slug,ledger_pk):
ledger = LedgerModel.objects.filter(pk=ledger_pk).first()
if request.method == "POST":
ledger.delete()
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})
# class LedgerModelDeleteView(DeleteView, SuccessMessageMixin):
# """
# Handles the deletion of a Ledger model instance.
# Provides functionality for rendering a confirmation template and deleting a
# ledger instance from the system. Extends functionality for managing success
# messages and redirections upon successful deletion.
# :ivar template_name: Path to the template used for rendering the delete
# confirmation view.
# :type template_name: str
# :ivar success_message: Success message displayed upon successful deletion
# of the ledger instance.
# :type success_message: str
# """
# template_name = "ledger/ledger/ledger_delete.html"
# pk_url_kwarg = 'ledger_pk'
# context_object_name = 'ledger_model'
# success_message = _("Ledger deleted successfully")
# permission_required = ["django_ledger.delete_ledgermodel"]
# def get_success_url(self):
# return reverse(
# "ledger_list",
# kwargs={
# "dealer_slug": self.kwargs["dealer_slug"],
# "entity_slug": self.kwargs["entity_slug"],
# },
# )
class JournalEntryListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"""
Represents a view to list all journal entries for a specific ledger.
This class inherits from Django's `ListView` and `LoginRequiredMixin` to provide
a secure and paginated list of journal entries associated with a specific ledger.
It ensures the user is authenticated before access to the journal entries is
granted. The view customizes the queryset and context data to include only
entries tied to the given ledger specified by its primary key.
:ivar model: The model associated with the view.
:type model: JournalEntryModel
:ivar context_object_name: The name of the context variable which will hold the
list of journal entries in the template.
:type context_object_name: str
:ivar template_name: The path to the HTML template that renders the list view.
:type template_name: str
"""
model = JournalEntryModel
context_object_name = "journal_entries"
template_name = "ledger/journal_entry/journal_entry_list.html"
permission_required = ["django_ledger.view_journalentrymodel"]
ordering = ["-timestamp"]
def get_queryset(self):
qs = super().get_queryset()
ledger = LedgerModel.objects.filter(pk=self.kwargs["pk"]).first()
qs = qs.filter(ledger=ledger)
return qs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["ledger"] = LedgerModel.objects.filter(pk=self.kwargs["pk"]).first()
return context
class JournalEntryCreateView(
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView
):
"""
View for creating a new journal entry.
This class-based view allows authenticated users to create new journal entries
associated with a specific ledger. It includes functionality for handling
context data, form initialization, and redirection upon successful submission
of the form.
:ivar model: The model used for creating the journal entry.
:type model: JournalEntryModel
:ivar template_name: The template used to render the form.
:type template_name: str
:ivar form_class: The form class used to create the journal entry.
:type form_class: forms.JournalEntryModelCreateForm
:ivar ledger_model: The ledger model associated with this journal entry.
:type ledger_model: LedgerModel or None
:ivar success_message: The message displayed upon successfully creating a journal entry.
:type success_message: str
"""
model = JournalEntryModel
template_name = "ledger/journal_entry/journal_entry_form.html"
permission_required = ["django_ledger.add_journalentrymodel"]
form_class = forms.JournalEntryModelCreateForm
ledger_model = None
success_message = _("Journal Entry created successfully")
def get_form(self, form_class=None):
dealer = get_user_type(self.request)
ledger = LedgerModel.objects.filter(pk=self.kwargs["pk"]).first()
form = forms.JournalEntryModelCreateForm(
entity_model=dealer.entity, ledger_model=ledger, **self.get_form_kwargs()
)
form.fields.pop("entity_unit")
return form
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["ledger"] = LedgerModel.objects.filter(pk=self.kwargs["pk"]).first()
return context
def get_success_url(self):
ledger = LedgerModel.objects.filter(pk=self.kwargs["pk"]).first()
return reverse(
"journalentry_list",
kwargs={"dealer_slug": self.kwargs["dealer_slug"], "pk": ledger.pk},
)
@login_required
@permission_required("django_ledger.delete_journalentrymodel", raise_exception=True)
def JournalEntryDeleteView(request,dealer_slug, pk):
"""
Handles the deletion of a specific journal entry. This view facilitates
the deletion of a journal entry identified by its primary key (pk). If the
deletion is successful, the user is redirected to the list of journal entries
for the associated ledger. If the deletion cannot proceed, an error message
is displayed, and the user is redirected back to the journal entry list.
:param request: The HTTP request object.
:type request: HttpRequest
:param pk: The primary key (pk) of the journal entry to be deleted.
:type pk: int
:return: A rendered HTML response for GET requests or a redirect upon
successful/failed deletion.
:rtype: HttpResponse
"""
journal_entry = get_object_or_404(JournalEntryModel, pk=pk)
if request.method == "POST":
ledger = journal_entry.ledger
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)
journal_entry.delete()
messages.success(request, "Journal Entry deleted")
return redirect("journalentry_list",dealer_slug=dealer_slug, pk=ledger.pk)
return render(
request,
"ledger/journal_entry/journal_entry_delete.html",
{"journal_entry": journal_entry},
)
@login_required
@permission_required("django_ledger.view_transactionmodel", raise_exception=True)
def JournalEntryTransactionsView(request, dealer_slug, pk):
"""
Handles the retrieval and display of journal entry transactions for a specific journal
entry instance. It retrieves the journal entry and its associated transactions, ordering
the transactions by account code. The data is then rendered using the specified template.
:param request: The HTTP request object.
:type request: django.http.HttpRequest
:param pk: The primary key of the journal entry to be retrieved.
:type pk: int
:return: An HTTP response with the rendered template, including the journal entry and
its transactions.
:rtype: django.http.HttpResponse
"""
get_object_or_404(models.Dealer, slug=dealer_slug)
journal = JournalEntryModel.objects.filter(pk=pk).first()
qs = TransactionModel.objects.filter(journal_entry=journal).all()
transactions = qs.annotate(
debit_credit_sort_order=Case(
When(tx_type="debit", then=Value(0)),
When(tx_type="credit", then=Value(1)),
output_field=IntegerField(),
)
).order_by("debit_credit_sort_order")
return render(
request,
"ledger/journal_entry/journal_entry_transactions.html",
{"journal_entry": journal, "transactions": transactions},
)
class JournalEntryModelTXSDetailView(JournalEntryModelTXSDetailViewBase):
"""
Represents a detailed view of journal entry transactions in the ledger.
This class is used to render the transactions associated with a specific
journal entry. It extends the base view functionality and is tailored for a
transaction details page.
:ivar template_name: The path to the template used to render the journal
entry transactions view.
:type template_name: str
"""
template_name = "ledger/journal_entry/journal_entry_txs.html"
@login_required
@permission_required("django_ledger.change_ledgermodel", raise_exception=True)
def ledger_lock_all_journals(request, dealer_slug, entity_slug, pk):
"""
Locks all journals associated with a specific ledger. If the ledger is already locked,
it will notify the user through an error message. Otherwise, it initiates the locking of
related journal entries, locks the ledger itself, and saves the changes to the database.
After the operation, it redirects the user to the journal entry list associated with the
ledger.
:param request: HttpRequest object representing the current request.
:type request: HttpRequest
:param entity_slug: The slug identifier of the entity.
:type entity_slug: str
:param pk: The primary key of the ledger to be locked.
:type pk: int
:return: HttpResponse redirecting to the journal entry list page of the locked ledger.
:rtype: HttpResponse
"""
get_object_or_404(models.Dealer, slug=dealer_slug)
ledger = LedgerModel.objects.filter(pk=pk).first()
if ledger.is_locked():
messages.error(request, _("Ledger is already locked"))
return redirect("journalentry_list", dealer_slug=dealer_slug, pk=ledger.pk)
ledger.lock_journal_entries()
ledger.lock()
ledger.save()
return redirect("journalentry_list", dealer_slug=dealer_slug, pk=ledger.pk)
@login_required
@permission_required("django_ledger.change_ledgermodel", raise_exception=True)
def ledger_unlock_all_journals(request, dealer_slug, entity_slug, pk):
"""
Unlocks all journal entries associated with a specific ledger. This function first checks if the
ledger is locked. If it is already unlocked, it shows an error message and redirects the user
to the journal entry list page. If the ledger is locked, it unlocks the ledger, saves changes,
and iterates through all the locked journal entries within the ledger to unlock them as well.
Afterward, it redirects the user to the journal entry list page.
:param request: The HTTP request object containing user session and request metadata.
:type request: HttpRequest
:param entity_slug: A unique string slug representing the specific entity or organization context.
:type entity_slug: str
:param pk: The primary key of the ledger record to be unlocked.
:type pk: int
:return: A redirection to the journal entry list page for the specified ledger.
:rtype: HttpResponseRedirect
"""
get_object_or_404(models.Dealer, slug=dealer_slug)
ledger = LedgerModel.objects.filter(pk=pk).first()
if not ledger.is_locked():
messages.error(request, _("Ledger is already Unlocked"))
return redirect("journalentry_list", dealer_slug=dealer_slug, pk=ledger.pk)
ledger.unlock()
ledger.save()
qs = ledger.journal_entries.locked()
for je in qs:
je.unlock()
je.save()
return redirect("journalentry_list", dealer_slug=dealer_slug, pk=ledger.pk)
@login_required
@permission_required("django_ledger.change_ledgermodel", raise_exception=True)
def ledger_post_all_journals(request, dealer_slug, entity_slug, pk):
"""
Posts all journal entries associated with a ledger. This function updates the ledger's
state to reflect that its journal entries have been posted. If the ledger is already
posted, an error message is displayed and the user is redirected.
:param request: The HTTP request object used for processing the action.
:type request: HttpRequest
:param entity_slug: A string representing the specific entity slug for the ledger context.
:type entity_slug: str
:param pk: The primary key of the ledger to be posted.
:type pk: int
:return: A redirect to the journal entry list view for the specified ledger.
:rtype: HttpResponseRedirect
"""
get_object_or_404(models.Dealer, slug=dealer_slug)
ledger = LedgerModel.objects.filter(pk=pk).first()
if ledger.is_posted():
messages.error(request, _("Ledger is already posted"))
return redirect("journalentry_list", dealer_slug=dealer_slug, pk=ledger.pk)
ledger.post_journal_entries()
ledger.post()
ledger.save()
return redirect("journalentry_list", dealer_slug=dealer_slug, pk=ledger.pk)
@login_required
@permission_required("django_ledger.change_ledgermodel", raise_exception=True)
def ledger_unpost_all_journals(request, dealer_slug, entity_slug, pk):
"""
Unposts all journal entries for a specified ledger and marks the ledger as unposted.
This function identifies a ledger by its primary key (pk) and checks if it is
already posted. If the ledger is not posted, an error message is displayed.
If it is posted, the function iterates through its posted journal entries,
marks them as unposted, and saves the changes. Finally, it marks the ledger itself
as unposted and saves it.
:param request: The HTTP request object from the client.
:type request: HttpRequest
:param entity_slug: A slug identifying the entity associated with the ledger.
:type entity_slug: str
:param pk: The primary key of the ledger whose journal entries are being unposted.
:type pk: int
:return: An HTTP redirect response object directing the client to the journal entry list
page for the specified ledger.
:rtype: HttpResponseRedirect
"""
get_object_or_404(models.Dealer, slug=dealer_slug)
ledger = LedgerModel.objects.filter(pk=pk).first()
if not ledger.is_posted():
messages.error(request, _("Ledger is already Unposted"))
return redirect("journalentry_list", dealer_slug=dealer_slug, pk=ledger.pk)
qs = ledger.journal_entries.posted()
for je in qs:
je.mark_as_unposted()
je.save()
ledger.unpost()
ledger.save()
return redirect("journalentry_list", dealer_slug=dealer_slug, pk=ledger.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)
vat = models.VatRate.objects.filter(dealer=dealer).first()
if not dealer.active_plan:
plan_list = PlanPricing.objects.annotate(
price_with_tax=Round(F('price') * vat.rate + F('price'), 2)
).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)
@login_required
@permission_required("inventory.change_dealer", raise_exception=True)
@require_POST
def submit_plan(request, dealer_slug):
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
selected_plan_id = request.POST.get("selected_plan")
pp = PlanPricing.objects.get(pk=selected_plan_id)
order = None
try:
order = Order.objects.create(
user=dealer.user,
plan=pp.plan,
pricing=pp.pricing,
amount=pp.price,
currency="SA",
tax=15,
status=1,
)
logger.info(f"order created {order}")
except Exception as e:
logger.error(e)
if not order:
messages.error(request, _("Error creating order"))
logger.error("unable to create order")
return redirect("pricing_page", dealer_slug=dealer_slug)
transaction_url = handle_payment(request, order)
return redirect(transaction_url)
@login_required
@permission_required("inventory.change_dealer", raise_exception=True)
def payment_callback(request, dealer_slug):
message = request.GET.get("message")
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
payment_id = request.GET.get("id")
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}")
order = Order.objects.filter(user=dealer.user, status=1).first() # Status 1 = NEW
if payment_status == "paid":
logger.info(f"Payment successful for transaction ID {payment_id}. Processing order completion.")
billing_info, created = BillingInfo.objects.get_or_create(
user=dealer.user,
defaults={
'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'):
print(order.get_plan_pricing().pricing.period)
UserPlan.objects.create(
user=order.user,
plan=order.plan,
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}.")
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
# logger.info(f"Processing order completion for {user} - upgrading to {order.plan}")
# user.userplan.plan = order.plan
# user.userplan.expire = datetime.now() + timedelta(days=pricing.period)
# user.userplan.save()
# user.save()
# logger.info(f"User {user} upgraded to {order.plan} plan successfully")
order.complete_order()
history.status = "paid"
history.save()
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}
)
except Exception as 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"})
elif payment_status == "failed":
logger.warning(f"Payment failed for transaction ID {payment_id}. Message: {message}")
history.status = "failed"
history.save()
return render(request, "payment_failed.html", {"message": message})
return render(request, "payment_failed.html", {"message": "Unknown payment status"})
# def payment_callback(request, dealer_slug):
# message = request.GET.get("message")
# dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
# payment_id = request.GET.get("id")
# history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first()
# payment_status = request.GET.get("status")
# order = Order.objects.filter(user=dealer.user, status=1).first()
# if payment_status == "paid":
# billing_info, created = BillingInfo.objects.get_or_create(
# user=dealer.user,
# tax_number=dealer.vrn,
# name=dealer.arabic_name,
# street=dealer.address,
# zipcode=dealer.entity.zip_code if dealer.entity.zip_code else " ",
# city=dealer.entity.city if dealer.entity.city else " ",
# country=dealer.entity.country if dealer.entity.country else " ",
# )
# if created:
# userplan = UserPlan.objects.create(
# user=request.user,
# plan=order.plan,
# active=True,
# )
# userplan.initialize()
# order.complete_order()
# history.status = "paid"
# history.save()
# invoice = order.get_invoices().first()
# return render(
# request, "payment_success.html", {"order": order, "invoice": invoice}
# )
# elif payment_status == "failed":
# history.status = "failed"
# history.save()
# return render(request, "payment_failed.html", {"message": message})
@login_required
def sse_stream(request):
def event_stream():
last_id = request.GET.get("last_id", 0)
while True:
# Check for new notifications
if request.user.is_authenticated:
notifications = models.Notification.objects.filter(
user=request.user, id__gt=last_id, is_read=False
).order_by("created")
for notification in notifications:
notification_data = {
"id": notification.id,
"message": notification.message,
"created": notification.created.isoformat(),
}
yield (
f"id: {notification.id}\n"
f"event: notification\n"
f"data: {json.dumps(notification_data)}\n\n"
)
last_id = notification.id
sleep(3)
response = StreamingHttpResponse(event_stream(), content_type="text/event-stream")
response["Cache-Control"] = "no-cache"
return response
@login_required
def fetch_notifications(request):
notifications = models.Notification.objects.filter(
user=request.user, is_read=False
).order_by("-created")[:10] # Get 10 most recent
return JsonResponse({"notifications": list(notifications.values())})
@login_required
def mark_notification_as_read(request, notification_id):
notification = get_object_or_404(
models.Notification, id=notification_id, user=request.user
)
notification.is_read = True
notification.save()
return JsonResponse({"status": "success"})
@login_required
def mark_all_notifications_as_read(request):
models.Notification.objects.filter(user=request.user, is_read=False).update(
is_read=True
)
messages.success(request, _("All notifications marked as read."))
return redirect("notifications_history")
@login_required
def notifications_history(request):
models.Notification.objects.filter(user=request.user, is_read=False).update(
is_read=True
)
return JsonResponse({"status": "success"})
@login_required
@permission_required("inventory.add_activity", raise_exception=True)
def add_activity(request, dealer_slug, content_type, slug):
# Get user information for logging
user_username = (
request.user.username if request.user.is_authenticated else "anonymous"
)
# Log the attempt to retrieve the model dynamically
logger.debug(
f"User {user_username} attempting to retrieve model "
f"for content_type '{content_type}' for dealer '{dealer_slug}'."
)
try:
model = apps.get_model(f"inventory.{content_type}")
except LookupError:
# --- Log for LookupError (Model not found) ---
logger.warning(
f"User {user_username} requested an invalid model content_type: '{content_type}'. "
f"Raising Http404."
)
raise Http404("Model not found")
obj = get_object_or_404(model, slug=slug)
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
if request.method == "POST":
form = forms.ActivityForm(request.POST)
if form.is_valid():
activity = form.save(commit=False)
activity.dealer = dealer
activity.content_object = obj
activity.created_by = request.user
activity.notes = form.cleaned_data["notes"]
activity.activity_type = form.cleaned_data["activity_type"]
activity.save()
# --- Log for successful activity creation ---
logger.info(
f"User {user_username} successfully added activity (Type: {activity.activity_type}) "
f"for {content_type} ID: {obj.slug} ('{obj.name if hasattr(obj, 'name') else obj}') "
f"on dealer '{dealer_slug}'. Notes: '{activity.notes[:50]}...'."
)
messages.success(request, _("Activity added successfully"))
else:
# --- Log for invalid form data ---
logger.warning(
f"User {user_username} submitted invalid activity form data "
f"for {content_type} ID: {obj.slug} (Dealer: {dealer_slug}). Errors: {form.errors.as_json()}"
)
messages.error(request, _("Activity form is not valid"))
return redirect(f"{content_type}_detail", dealer_slug=dealer_slug, slug=slug)
@login_required
@permission_required("inventory.add_tasks", raise_exception=True)
def add_task(request, dealer_slug, content_type, slug):
# Get user information for logging
user_username = (
request.user.username if request.user.is_authenticated else "anonymous"
)
# Log the attempt to retrieve the model dynamically
logger.debug(
f"User {user_username} attempting to retrieve model "
f"for content_type '{content_type}' for dealer '{dealer_slug}'."
)
try:
model = apps.get_model(f"inventory.{content_type}")
except LookupError:
# --- Single-line log for LookupError (Model not found) ---
logger.warning(
f"User {user_username} requested an invalid model content_type: '{content_type}'. "
f"Raising Http404 for dealer '{dealer_slug}'."
)
raise Http404("Model not found")
obj = get_object_or_404(model, slug=slug)
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
if request.method == "POST":
form = forms.StaffTaskForm(request.POST)
if form.is_valid():
task = form.save(commit=False)
task.dealer = dealer
task.content_object = obj
task.assigned_to = request.user
task.created_by = request.user
task.due_date = form.cleaned_data["due_date"]
task.save()
models.Activity.objects.create(
dealer=dealer,
content_object=obj,
activity_type="task",
created_by=request.user,
notes="Task Added",
)
# --- Log for successful task creation ---
logger.info(
f"User {user_username} successfully added task "
f"(Assigned to: {task.assigned_to.email}) for {content_type} ID: {obj.slug} "
f"on dealer '{dealer_slug}'. Due: {task.due_date}, Notes: '{task.description}'."
)
messages.success(request, _("Task added successfully"))
else:
# --- Log for invalid form data ---
logger.warning(
f"User {user_username} submitted invalid task form data "
f"for {content_type} ID: {obj.slug} (Dealer: {dealer_slug}). Errors: {form.errors.as_json()}"
)
messages.error(request, _("Task form is not valid"))
return redirect(f"{content_type}_detail", dealer_slug=dealer_slug, slug=slug)
@login_required
@permission_required("inventory.change_tasks", raise_exception=True)
def update_task(request, dealer_slug, pk):
task = get_object_or_404(models.Tasks, pk=pk)
if request.method == "POST":
task.completed = False if task.completed else True
task.save()
# tasks = models.Tasks.objects.filter(content_type__model=content_type, object_id=obj.id)
return render(request, "partials/task.html", {"task": task})
@login_required
@permission_required("inventory.change_schedule", raise_exception=True)
def update_schedule(request, dealer_slug, pk):
task = get_object_or_404(models.Schedule, pk=pk)
if request.method == "POST":
task.completed = False if task.completed else True
task.save()
print("task")
return render(request, "partials/task.html", {"task": task})
@login_required
@permission_required("inventory.add_notes", raise_exception=True)
def add_note(request, dealer_slug, content_type, slug):
# Get user information for logging
user_username = (
request.user.username if request.user.is_authenticated else "anonymous"
)
# Log the attempt to retrieve the model dynamically
logger.debug(
f"User {user_username} attempting to retrieve model "
f"for content_type '{content_type}' for dealer '{dealer_slug}'."
)
try:
model = apps.get_model(f"inventory.{content_type}")
except LookupError:
# --- Single-line log for LookupError (Model not found) ---
logger.warning(
f"User {user_username} requested an invalid model content_type: '{content_type}'. "
f"Raising Http404 for dealer '{dealer_slug}'."
)
raise Http404("Model not found")
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
obj = get_object_or_404(model, slug=slug)
if request.method == "POST":
form = forms.NoteForm(request.POST)
if form.is_valid():
note = form.save(commit=False)
note.dealer = dealer
note.content_object = obj
note.created_by = request.user
note.save()
models.Activity.objects.create(
dealer=dealer,
content_object=obj,
activity_type="note",
created_by=request.user,
notes="Note Added",
)
# --- Single-line log for successful note creation ---
logger.info(
f"User {user_username} successfully added a note "
f"for {content_type} ID: {obj.slug} (Dealer: {dealer_slug}). "
f"Note: '{note.note[:50]}...'."
)
messages.success(request, _("Note added successfully"))
else:
# --- Single-line log for invalid form data ---
logger.warning(
f"User {user_username} submitted invalid note form data "
f"for {content_type} ID: {obj.slug} (Dealer: {dealer_slug}). Errors: {form.errors.as_text()}"
)
messages.error(request, _("Note form is not valid"))
return redirect(f"{content_type}_detail", dealer_slug=dealer_slug, slug=slug)
@login_required
@require_http_methods(["POST"])
@permission_required("inventory.change_notes", raise_exception=True)
def update_note(request, dealer_slug, pk):
print(pk)
note = get_object_or_404(models.Notes, pk=pk)
# lead = get_object_or_404(models.Lead, pk=note.content_object.id)
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
if request.method == "POST":
note.note = request.POST.get("note")
note.save()
messages.success(request, _("Note updated successfully"))
return redirect(request.META.get("HTTP_REFERER", "/"))
# Admin Management
@login_required
@permission_required("inventory.change_dealer", raise_exception=True)
def management_view(request, dealer_slug):
get_object_or_404(models.Dealer, slug=dealer_slug)
return render(request, "admin_management/management.html")
@login_required
@permission_required("inventory.change_dealer", raise_exception=True)
def user_management(request, dealer_slug):
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
context = {
"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)
@login_required
@permission_required("inventory.change_dealer", raise_exception=True)
def AuditLogDashboardView(request, dealer_slug):
"""
Displays audit logs (User Actions, Login Events, Request Events) with pagination.
Log type is determined by the 'q' query parameter (e.g., ?q=userActions).
Pagination page number is passed as a query parameter (e.g., ?page=2).
"""
get_object_or_404(models.Dealer, slug=dealer_slug)
q = request.GET.get("q") # Get the log type from the 'q' query parameter
current_pagination_page = request.GET.get("page", 1)
context = {}
template_name = None
logs_per_page = 30 # Define logs per page once
# --- Determine Data Source and Template based on 'q' parameter ---
if (
q == "userRequests"
): # This block handles cases where 'q' is 'requestEvents', None, or any other invalid value.
# It defaults to Request Logs if 'q' is not 'userActions' or 'loginEvents'.
template_name = "admin_management/request_logs.html"
context["title"] = "Request Logs Dashboard"
request_events = RequestEvent.objects.all().order_by("-datetime")
paginator = Paginator(request_events, logs_per_page)
try:
page_obj = paginator.page(current_pagination_page)
except PageNotAnInteger:
page_obj = paginator.page(1)
except EmptyPage:
page_obj = paginator.page(paginator.num_pages)
elif q == "loginEvents":
template_name = "admin_management/auth_logs.html"
context["title"] = "Login Events Dashboard"
auth_events = LoginEvent.objects.all().order_by("-datetime")
paginator = Paginator(auth_events, logs_per_page)
try:
page_obj = paginator.page(current_pagination_page)
except PageNotAnInteger:
page_obj = paginator.page(1)
except EmptyPage:
page_obj = paginator.page(paginator.num_pages)
else:
template_name = "admin_management/model_logs.html"
context["title"] = "User Actions Dashboard"
# OPTIMIZATION: Get the QuerySet but don't evaluate it yet
model_events_queryset = CRUDEvent.objects.all().order_by("-datetime")
# 1. Paginate the raw QuerySet FIRST
paginator = Paginator(model_events_queryset, logs_per_page)
try:
# Get the page object, which contains only the raw QuerySet objects for the current page
page_obj_raw = paginator.page(current_pagination_page)
except PageNotAnInteger:
page_obj_raw = paginator.page(1)
except EmptyPage:
page_obj_raw = paginator.page(paginator.num_pages)
# 2. Now, process 'field_changes' ONLY for the events on the current page
processed_model_events_for_page = []
for (
event
) in page_obj_raw.object_list: # Loop only through the current page's items
event_data = {
"datetime": event.datetime,
"user": event.user,
"event_type_display": event.get_event_type_display(),
"model_name": event.content_type.model,
"object_id": event.object_id,
"object_repr": event.object_repr,
"field_changes": [],
}
if event.changed_fields:
try:
changes = json.loads(event.changed_fields)
if isinstance(changes, dict):
for field_name, values in changes.items():
old_value = (
values[0]
if isinstance(values, list) and len(values) > 0
else None
)
new_value = (
values[1]
if isinstance(values, list) and len(values) > 1
else None
)
event_data["field_changes"].append(
{
"field": field_name,
"old": old_value,
"new": new_value,
}
)
elif changes is None:
event_data["field_changes"].append(
{
"field": "Info",
"old": "",
"new": "No specific field changes recorded (JSON was null)",
}
)
else: # Handle valid JSON but not a dictionary (e.g., "[]", 123)
event_data["field_changes"].append(
{
"field": "Error",
"old": "",
"new": f"Unexpected JSON format: {type(changes).__name__}",
}
)
except json.JSONDecodeError:
# Handle invalid JSON; you might log this error
event_data["field_changes"].append(
{
"field": "Error",
"old": "",
"new": "Invalid JSON in changed_fields",
}
)
processed_model_events_for_page.append(event_data)
# 3. Replace the object_list of the original page_obj with the processed data
# This keeps all pagination properties (has_next, number, etc.) intact.
page_obj_raw.object_list = processed_model_events_for_page
page_obj = page_obj_raw # This will be passed to the context
# Pass the final page object to the context
context["page_obj"] = page_obj
return render(request, template_name, context)
@login_required
@permission_required("inventory.change_dealer", raise_exception=True)
def activate_account(request, dealer_slug, content_type, slug):
get_object_or_404(models.Dealer, slug=dealer_slug)
# Get user information for logging
user_username = (
request.user.username if request.user.is_authenticated else "anonymous"
)
# Log the attempt to retrieve the model dynamically
logger.debug(
f"User {user_username}attempting to retrieve model "
f"for content_type '{content_type}' for dealer '{dealer_slug}' in activate_account view."
)
try:
model = apps.get_model(f"inventory.{content_type}")
except LookupError:
# --- Single-line log for LookupError (Model not found) ---
logger.warning(
f"User {user_username} requested an invalid model content_type: '{content_type}'. "
f"Raising Http404 in activate_account view for dealer '{dealer_slug}'."
)
raise Http404("Model not found")
obj = get_object_or_404(model, slug=slug)
if request.method == "POST":
obj.activate_account()
# --- Single-line log for successful account activation ---
logger.info(
f"User {user_username} successfully activated account "
f"for {content_type} ID: {obj.slug} ('{obj.name if hasattr(obj, 'name') else obj}') "
f"on dealer '{dealer_slug}'."
)
messages.success(request, _("Account activated successfully"))
return redirect("user_management", dealer_slug=dealer_slug)
return render(
request, "admin_management/confirm_activate_account.html", {"obj": obj}
)
@login_required
@permission_required("inventory.change_dealer", raise_exception=True)
def permenant_delete_account(request, dealer_slug, content_type, slug):
get_object_or_404(models.Dealer, slug=dealer_slug)
# Get user information for logging
user_username = (
request.user.username if request.user.is_authenticated else "anonymous"
)
logger.debug(
f"User {user_username} attempting to retrieve model "
f"for content_type '{content_type}' for permanent account deletion."
)
try:
model = apps.get_model(f"inventory.{content_type}")
except LookupError:
# Log if model not found
logger.warning(
f"User {user_username} requested an invalid model content_type: '{content_type}'. "
f"Raising Http404 for permanent account deletion (Dealer: {dealer_slug})."
)
raise Http404("Model not found")
obj = get_object_or_404(model, slug=slug)
if request.method == "POST":
logger.debug(
f"User {user_username} attempting to permanently delete account "
f"for {content_type} ID: {obj.slug} ('{obj.name if hasattr(obj, 'name') else obj}') " # Attempt to get name
f"on dealer '{dealer_slug}'."
)
try:
obj.permenant_delete()
# Log successful permanent deletion
logger.info(
f"User {user_username} successfully permanently deleted account "
f"for {content_type} ID: {obj.slug} ('{obj.name if hasattr(obj, 'name') else obj}') "
f"on dealer '{dealer_slug}'."
)
messages.success(request, _("Account Deleted successfully"))
except RestrictedError:
# Log restricted deletion
logger.warning(
f"User {user_username} attempted to permanently delete account "
f"for {content_type} ID: {obj.slug} ('{obj.name if hasattr(obj, 'name') else obj}') "
f"on dealer '{dealer_slug}', but deletion was restricted due to related objects."
)
messages.error(
request,
_("You cannot delete this account,it is related to another account"),
)
except Exception as e:
# Log generic error during permanent deletion
logger.error(
f"User {user_username} encountered an unexpected error "
f"while permanently deleting account for {content_type} ID: {obj.slug} "
f"('{obj.name if hasattr(obj, 'name') else obj}') on dealer '{dealer_slug}'. Error: {e}",
exc_info=True,
)
messages.error(request, _(f"Error deleting account: {e}"))
return redirect("user_management", dealer_slug=dealer_slug)
return render(
request, "admin_management/permenant_delete_account.html", {"obj": obj}
)
#####################################################################
@login_required
@permission_required("django_ledger.add_purchaseordermodel", raise_exception=True)
def PurchaseOrderCreateView(request, dealer_slug, entity_slug):
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
entity = dealer.entity
# Get user information for logging
user_username = (
request.user.username if request.user.is_authenticated else "anonymous"
)
if request.method == "POST":
# Log the attempt to create a purchase order
logger.debug(
f"User {user_username} attempting to create a Purchase Order "
f"for Entity ID: {entity.pk} ('{entity.name}'). Title: '{request.POST.get('po_title')}'."
)
try:
po = entity.create_purchase_order(po_title=request.POST.get("po_title"))
po.entity = entity
po.save()
# --- Single-line log for successful purchase order creation ---
logger.info(
f"User {user_username} successfully created Purchase Order ID: {po.pk} "
f"('{po.po_title}') for Entity ID: {entity.pk} ('{entity.name}')."
)
except ValidationError as e:
# --- Single-line log for ValidationError ---
logger.warning(
f"User {user_username} encountered a validation error "
f"while creating a Purchase Order for Entity ID: {entity.pk} ('{entity.name}'). "
f"Error: {e}"
)
messages.error(request, str(e))
return redirect(
"purchase_order_create",
dealer_slug=dealer.slug,
entity_slug=entity.slug,
)
messages.success(request, _("Purchase order created successfully"))
return redirect(
"purchase_order_detail",
dealer_slug=dealer.slug,
entity_slug=entity.slug,
pk=po.pk,
)
form = PurchaseOrderModelCreateForm(
entity_slug=entity.slug,
user_model=entity.admin,
)
return render(request, "purchase_orders/po_form.html", {"form": form})
@login_required
def InventoryItemCreateView(request, dealer_slug):
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
for_po = request.GET.get("for_po")
entity = dealer.entity
coa = entity.get_default_coa()
inventory_accounts = entity.get_coa_accounts().filter(role="asset_ca_inv")
cogs_accounts = entity.get_coa_accounts().filter(role="cogs_regular")
if request.method == "POST":
response = HttpResponse()
response["HX-Refresh"] = "true"
name = request.POST.get("name")
account = request.POST.get("account")
account = inventory_accounts.get(pk=account)
inventory_name = None
if name:
inventory_name = name
else:
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")
exterior = models.ExteriorColors.objects.get(
pk=request.POST.get("exterior")
)
interior = models.InteriorColors.objects.get(
pk=request.POST.get("interior")
)
make_name = models.CarMake.objects.get(pk=make)
model_name = models.CarModel.objects.get(pk=model)
serie_name = models.CarSerie.objects.get(pk=serie)
trim_name = models.CarTrim.objects.get(pk=trim)
inventory_name = f"{make_name.name} || {model_name.name} || {serie_name.name} || {trim_name.name} || {year} || {exterior.name} || {interior.name}"
if (
inventory := entity.get_items_inventory()
.filter(name=inventory_name)
.first()
):
messages.error(request, _("Inventory item already exists"))
return response
uom = entity.get_uom_all().get(name="Unit")
entity.create_item_inventory(
name=inventory_name,
uom_model=uom,
item_type=ItemModel.ITEM_TYPE_MATERIAL,
inventory_account=account,
coa_model=coa,
)
messages.success(request, _("Inventory item created successfully"))
return response
if for_po:
form = forms.CSVUploadForm()
form.fields["year"].widget.attrs["hx-get"] = reverse(
"inventory_items_filter", kwargs={"dealer_slug": dealer.slug}
)
form.fields["vendor"].queryset = dealer.vendors.filter(active=True)
context = {
"make_data": models.CarMake.objects.filter(is_sa_import=True),
"inventory_accounts": inventory_accounts,
"cogs_accounts": cogs_accounts,
"form": form,
}
return render(request, "purchase_orders/car_inventory_item_form.html", context)
return render(
request,
"purchase_orders/inventory_item_form.html",
{
"make_data": models.CarMake.objects.filter(is_sa_import=True),
"inventory_accounts": inventory_accounts,
"cogs_accounts": cogs_accounts,
},
)
@login_required
@permission_required("django_ledger.view_purchaseordermodel", raise_exception=True)
def inventory_items_filter(request, dealer_slug):
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
year = request.GET.get("year", None)
make = request.GET.get("make")
model = request.GET.get("model")
serie = request.GET.get("serie")
model_data = models.CarModel.objects.none()
serie_data = models.CarSerie.objects.none()
trim_data = models.CarTrim.objects.none()
if make:
make = models.CarMake.objects.get(pk=make)
model_data = make.carmodel_set.all()
elif model:
model = models.CarModel.objects.get(pk=model)
serie_data = model.carserie_set.all()
if year:
serie_data = serie_data.filter(year_begin__lte=year, year_end__gte=year)
print(serie_data)
elif serie:
serie = models.CarSerie.objects.get(pk=serie)
trim_data = serie.cartrim_set.all()
context = {
"model_data": model_data,
"serie_data": serie_data,
"trim_data": trim_data,
}
return render(request, "purchase_orders/car_inventory_item_form.html", context)
class PurchaseOrderDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
slug_url_kwarg = "po_pk"
slug_field = "uuid"
context_object_name = "po_model"
extra_context = {"header_subtitle_icon": "uil:bill", "hide_menu": True}
template_name = "purchase_orders/po_detail.html"
permission_required = ["django_ledger.view_purchaseordermodel"]
def get_queryset(self):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
self.queryset = dealer.entity.get_purchase_orders().select_related(
"entity", "ce_model"
)
return super().get_queryset()
def get_context_data(self, *, object_list=None, **kwargs):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
context = super().get_context_data(**kwargs)
context["entity_slug"] = dealer.entity.slug
po_model: PurchaseOrderModel = self.object
title = f"Purchase Order {po_model.po_number}"
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'])
po_model: PurchaseOrderModel = self.object
po_items_qs, item_data = po_model.get_itemtxs_data(
queryset=po_model.itemtransactionmodel_set.all().select_related(
"item_model", "bill_model"
)
)
context["po_items"] = po_items_qs
context["po_total_amount"] = sum(
i["po_total_amount"]
for i in po_items_qs.values("po_total_amount", "po_item_status")
if i["po_item_status"] != "cancelled"
)
return context
class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
model = PurchaseOrderModel
context_object_name = "purchase_orders"
paginate_by = 20
template_name = "purchase_orders/po_list.html"
permission_required = ["django_ledger.view_purchaseordermodel"]
def get_queryset(self):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
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))
return qs
return qs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
dealer = get_user_type(self.request)
vendors=models.Vendor.objects.filter(dealer=dealer)
context = super().get_context_data(**kwargs)
context["entity_slug"] = dealer.entity.slug
context["vendors"] = vendors
context["empty_state_value"] = _("purchase order")
return context
class PurchaseOrderUpdateView(PurchaseOrderModelUpdateViewBase):
pass
class BasePurchaseOrderActionActionView(BasePurchaseOrderActionActionViewBase):
permission_required = "django_ledger.change_purchaseordermodel"
class PurchaseOrderModelDeleteView(PurchaseOrderModelDeleteViewBase):
template_name = "purchase_orders/po_delete.html"
permission_required = "django_ledger.delete_purchaseordermodel"
def get_success_url(self):
messages.add_message(
self.request,
message="PO deleted successfully.",
level=messages.SUCCESS,
)
return reverse(
"purchase_order_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
)
class PurchaseOrderMarkAsDraftView(BasePurchaseOrderActionActionView):
action_name = "mark_as_draft"
class PurchaseOrderMarkAsReviewView(BasePurchaseOrderActionActionView):
action_name = "mark_as_review"
class PurchaseOrderMarkAsApprovedView(BasePurchaseOrderActionActionView):
action_name = "mark_as_approved"
class PurchaseOrderMarkAsFulfilledView(BasePurchaseOrderActionActionView):
action_name = "mark_as_fulfilled"
class PurchaseOrderMarkAsCanceledView(BasePurchaseOrderActionActionView):
action_name = "mark_as_canceled"
class PurchaseOrderMarkAsVoidView(BasePurchaseOrderActionActionView):
action_name = "mark_as_void"
##############################bil
class BaseBillActionView(BaseBillActionViewBase):
pass
class BillModelActionMarkAsDraftView(BaseBillActionView):
action_name = "mark_as_draft"
class BillModelActionMarkAsInReviewView(BaseBillActionView):
action_name = "mark_as_review"
class BillModelActionMarkAsApprovedView(BaseBillActionView):
action_name = "mark_as_approved"
class BillModelActionMarkAsPaidView(BaseBillActionView):
action_name = "mark_as_paid"
class BillModelActionDeleteView(BaseBillActionView):
action_name = "mark_as_delete"
class BillModelActionVoidView(BaseBillActionView):
action_name = "mark_as_void"
class BillModelActionCanceledView(BaseBillActionView):
action_name = "mark_as_canceled"
class BillModelActionLockLedgerView(BaseBillActionView):
action_name = "lock_ledger"
class BillModelActionUnlockLedgerView(BaseBillActionView):
action_name = "unlock_ledger"
class BillModelActionForceMigrateView(BaseBillActionView):
action_name = "migrate_state"
###############################################################
###############################################################
@login_required
def view_items_inventory(request, dealer_slug, entity_slug, po_pk):
get_object_or_404(models.Dealer, slug=dealer_slug)
po = PurchaseOrderModel.objects.get(pk=po_pk)
items = po.items.all()
return render(
request, "purchase_orders/po_upload_cars.html", {"po": po, "items": items}
)
@login_required
@permission_required("inventory.add_poitemsuploaded", raise_exception=True)
def upload_cars(request, dealer_slug, pk=None):
item = None
po_item = None
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
response = redirect("upload_cars", dealer_slug=dealer_slug)
# Get user information for logging
user_username = (
request.user.username if request.user.is_authenticated else "anonymous"
)
if pk:
# Log retrieval of PO item for upload
logger.debug(
f"User {user_username} retrieved ItemTransactionModel PK: {pk} for car upload."
)
item = get_object_or_404(ItemTransactionModel, pk=pk)
po_item = models.PoItemsUploaded.objects.get(dealer=dealer, item=item)
response = redirect("upload_cars", dealer_slug=dealer_slug, pk=pk)
if po_item.status == "uploaded":
messages.add_message(request, messages.SUCCESS, "Item uploaded Sucessfully.")
return redirect(
"view_items_inventory",
dealer_slug=dealer_slug,
entity_slug=dealer.entity.slug,
po_pk=item.po_model.pk,
)
if request.method == "POST":
csv_file = request.FILES.get("csv_file")
# Log initial attempt to process data from item or POST
logger.debug(
f"User {user_username} starting car data processing for upload. "
f"Source: {'Existing Item PK: ' + str(pk) if pk else 'Form submission'}."
)
try:
if item:
data = [x.strip() for x in item.item_model.name.split("||")]
make = models.CarMake.objects.filter(is_sa_import=True).get(
name=data[0]
)
model = make.carmodel_set.get(name=data[1])
trim = models.CarTrim.objects.filter(
name=data[3], id_car_serie__id_car_model=model.id_car_model
).first()
serie = trim.id_car_serie
year = data[4]
exterior = models.ExteriorColors.objects.get(name=data[5])
interior = models.InteriorColors.objects.get(name=data[6])
receiving_date = timezone.now()
vendor_model = item.bill_model.vendor
vendor = models.Vendor.objects.get(vendor_model=vendor_model)
logger.debug(
f"User {user_username} extracted car details from existing Item PK: {pk}."
)
else:
make = models.CarMake.objects.get(pk=request.POST.get("make"))
model = models.CarModel.objects.get(pk=request.POST.get("model"))
serie = models.CarSerie.objects.get(pk=request.POST.get("serie"))
trim = models.CarTrim.objects.get(pk=request.POST.get("trim"))
exterior = models.ExteriorColors.objects.get(
pk=request.POST.get("exterior")
)
interior = models.InteriorColors.objects.get(
pk=request.POST.get("interior")
)
year = request.POST.get("year")
receiving_date = datetime.strptime(
request.POST.get("receiving_date"), "%Y-%m-%d"
)
vendor = models.Vendor.objects.get(pk=request.POST.get("vendor"))
logger.debug(
f"User {user_username} extracted car details from form submission."
)
except Exception as e:
# --- Log for errors during data extraction/retrieval ---
logger.error(
f"User {user_username} encountered an error while preparing car data "
f"for upload (Item PK: {pk if pk else 'N/A'}, Dealer: {dealer_slug}). Error: {e}",
exc_info=True,
)
messages.error(request, f"Error processing CSV: {str(e)}")
return response
if not csv_file.name.endswith(".csv"):
logger.warning(
f"User {user_username} attempted to upload a non-CSV file "
f"('{csv_file.name}') for car upload. Dealer: {dealer_slug}."
)
messages.error(request, "Please upload a CSV file")
return response
try:
# Read the file content
file_content = csv_file.read().decode("utf-8")
csv_data = io.StringIO(file_content)
reader = csv.DictReader(csv_data)
data = [x for x in reader]
if len(data) < item.quantity:
messages.error(
request,
f"CSV file has {len(data)} rows, but the quantity of the item is {item.quantity}.",
)
return response
for row in data:
# Log VIN decoding and initial validation for each row
logger.debug(
f"Processing VIN: {row.get('vin', 'N/A')} from CSV row by user {user_username}."
)
if result := decodevin(row["vin"]):
if models.Car.objects.filter(vin=row["vin"]).exists():
messages.error(request, f"vin {row['vin']} already exists")
return response
manufacturer_name, model_name, year_model = result.values()
car_make = get_make(manufacturer_name)
car_model = get_model(model_name, car_make)
if (
not all([car_make]) or (make.pk != car_make.pk)
# not all([car_make, car_model])
# or (make.pk != car_make.pk)
# or (model.pk != car_model.pk)
):
logger.warning(
f"User {user_username} uploaded CSV with VIN '{row['vin']}' "
f"having data mismatch (Make/Model from VIN vs. expected). "
f"VIN Make: '{manufacturer_name}', Expected Make: '{make.name}'. "
f"VIN Model: '{model_name}', Expected Model: '{model.name}'. "
f"Returning error."
)
messages.error(
request,
f"invalid data at vin {row['vin']}, Please upload a valid CSV file",
)
return response
cars_created = 0
for row in data:
car = models.Car.objects.create(
dealer=dealer,
vin=row["vin"],
id_car_make=make,
id_car_model=model,
id_car_serie=serie,
id_car_trim=trim,
year=int(year_model),
vendor=vendor,
receiving_date=receiving_date,
cost_price=po_item.item.unit_cost,
)
# if po_item: #TODO:update
# 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(
f"User {user_username} created Car ID: {car.pk} (VIN: {car.vin}). "
f"Count: {cars_created}."
)
if po_item:
po_item.status = "uploaded"
po_item.save()
logger.info(
f"User {user_username} updated PoItemsUploaded status to 'uploaded' for Item PK: {item.pk}."
)
return redirect("home")
# --- Log for successful CSV import and car creation ---
logger.info(
f"User {user_username} successfully imported {cars_created} cars "
f"for Item PK: {pk if pk else 'N/A'} (Dealer: {dealer_slug})."
)
messages.success(request, f"Successfully imported {cars_created} cars")
return redirect(
"view_items_inventory",
dealer_slug=dealer_slug,
slug_entity=dealer.entity.slug,
po_pk=item.po_model.pk,
)
except Exception as e:
# --- Log for general errors during CSV processing or car creation ---
logger.error(
f"User {user_username} encountered an unexpected error "
f"during CSV file processing or car creation for Item PK: {pk if pk else 'N/A'} "
f"(Dealer: {dealer_slug}). Error: {e}",
exc_info=True, # Crucial for full traceback
)
messages.error(request, f"Error processing CSV: {str(e)}")
return response
form = forms.CSVUploadForm()
form.fields["vendor"].queryset = dealer.vendors.filter(active=True).all()
return render(
request,
"csv_upload.html",
{
"make_data": models.CarMake.objects.filter(is_sa_import=True),
"form": form,
"item": item,
},
)
###############################################################
###############################################################
@login_required
@permission_required("inventory.add_poitemsuploaded", raise_exception=True)
@require_POST
def bulk_update_car_price(request):
if request.method == "POST":
cars = request.POST.getlist("car")
price = request.POST.get("price")
if not price or int(price) <= 0:
messages.error(request, "Please enter a valid price")
elif not cars:
messages.error(request, "No cars selected for price update")
else:
for car_pk in cars:
car = models.Car.objects.get(pk=car_pk)
car.cost_price = Decimal(price)
car.save()
messages.success(request, "Price updated successfully")
response = HttpResponse()
response["HX-Redirect"] = reverse("car_list")
return response
class InventoryListView(InventoryListViewBase):
template_name = "inventory/list.html"
permission_required = ["django_ledger.view_purchaseordermodel"]
@login_required
def purchase_report_view(request, dealer_slug):
start_date_str = request.GET.get('start_date')
end_date_str = request.GET.get('end_date')
pos = request.entity.get_purchase_orders()
if start_date_str:
try:
start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date()
pos = pos.filter(created__date__gte=start_date)
except (ValueError, TypeError):
pass
if end_date_str:
try:
end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date()
pos = pos.filter(created__date__lte=end_date)
except (ValueError, TypeError):
pass
data = []
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()]
po_amount = sum(item["total"] for item in items)
po_quantity = sum(item["q"] for item in items)
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])
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
})
current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
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,
"start_date": start_date_str,
"end_date": end_date_str,
}
return render(request, 'ledger/reports/purchase_report.html', context)
def purchase_report_csv_export(request, dealer_slug):
response = HttpResponse(content_type='text/csv')
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}"'
writer = csv.writer(response)
header = [
'PO Number',
'Created Date',
'Status',
'Fulfilled Date',
'PO Amount',
'PO Quantity',
'Vendors'
]
writer.writerow(header)
pos = request.entity.get_purchase_orders()
start_date_str = request.GET.get('start_date')
end_date_str = request.GET.get('end_date')
if start_date_str:
try:
start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date()
pos = pos.filter(created__date__gte=start_date)
except (ValueError, TypeError):
pass
if end_date_str:
try:
end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date()
pos = pos.filter(created__date__lte=end_date)
except (ValueError, TypeError):
pass
for po in pos:
po_amount = 0
po_quantity = 0
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"]
po_quantity += item["q"]
bills = po.get_po_bill_queryset()
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
])
return response
@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 if vat else 0
cars_sold = models.Car.objects.filter(dealer=dealer, status='sold')
# Get filter parameters from the request
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')
start_date_str = request.GET.get('start_date')
end_date_str = request.GET.get('end_date')
# Apply filters to the queryset
if selected_make:
cars_sold = cars_sold.filter(id_car_make__name=selected_make)
if selected_model:
cars_sold = cars_sold.filter(id_car_model__name=selected_model)
if selected_serie:
cars_sold = cars_sold.filter(id_car_serie__name=selected_serie)
if selected_year:
cars_sold = cars_sold.filter(year=selected_year)
if selected_stock_type:
cars_sold = cars_sold.filter(stock_type=selected_stock_type)
# Corrected: Apply date filters using the 'sold_date' field
if start_date_str:
try:
start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date()
cars_sold = cars_sold.filter(sold_date__gte=start_date)
except (ValueError, TypeError):
pass
if end_date_str:
try:
end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date()
cars_sold = cars_sold.filter(sold_date__lte=end_date)
except (ValueError, TypeError):
pass
# 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_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_discount = cars_sold.aggregate(total=Sum('discount_amount'))['total'] or 0
current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
# Get distinct makes for the initial dropdown, other dropdowns will be populated via AJAX
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().order_by('id_car_make__name')
context = {
'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,
'selected_make': selected_make,
'selected_model': selected_model,
'selected_serie': selected_serie,
'selected_year': selected_year,
'selected_stock_type': selected_stock_type,
'start_date': start_date_str,
'end_date': end_date_str,
}
return render(request, 'ledger/reports/car_sale_report.html', context)
### 2. Updated `get_filtered_choices`
@login_required
def get_filtered_choices(request, dealer_slug):
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
# Get all filter parameters from the request
selected_make = request.GET.get('make')
selected_model = request.GET.get('model')
selected_serie = request.GET.get('serie')
start_date_str = request.GET.get('start_date')
end_date_str = request.GET.get('end_date')
# Start with the base queryset
queryset = models.Car.objects.filter(dealer=dealer, status='sold')
# Apply filters based on what is selected
if selected_make:
queryset = queryset.filter(id_car_make__name=selected_make)
if selected_model:
queryset = queryset.filter(id_car_model__name=selected_model)
if selected_serie:
queryset = queryset.filter(id_car_serie__name=selected_serie)
# Corrected: Apply date filters to the AJAX queryset
if start_date_str:
try:
start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date()
queryset = queryset.filter(sold_date__gte=start_date)
except (ValueError, TypeError):
pass
if end_date_str:
try:
end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date()
queryset = queryset.filter(sold_date__lte=end_date)
except (ValueError, TypeError):
pass
data = {
'models': list(queryset.values_list('id_car_model__name', flat=True).distinct().order_by('id_car_model__name')),
'series': list(queryset.values_list('id_car_serie__name', flat=True).distinct().order_by('id_car_serie__name')),
'years': list(queryset.values_list('year', flat=True).distinct().order_by('-year')),
'stock_types': list(queryset.values_list('stock_type', flat=True).distinct().order_by('stock_type'))
}
return JsonResponse(data)
### 3. Updated `car_sale_report_csv_export`
@login_required
def car_sale_report_csv_export(request, dealer_slug):
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}"'
writer = csv.writer(response)
# Define the CSV header based on your HTML table headers
header = [
'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')
# Apply filters from the request, just like in your HTML view
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')
start_date_str = request.GET.get('start_date')
end_date_str = request.GET.get('end_date')
if selected_make:
cars_sold = cars_sold.filter(id_car_make__name=selected_make)
if selected_model:
cars_sold = cars_sold.filter(id_car_model__name=selected_model)
if selected_serie:
cars_sold = cars_sold.filter(id_car_serie__name=selected_serie)
if selected_year:
cars_sold = cars_sold.filter(year=selected_year)
if selected_stock_type:
cars_sold = cars_sold.filter(stock_type=selected_stock_type)
# Corrected: Apply date filters for CSV export
if start_date_str:
try:
start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date()
cars_sold = cars_sold.filter(sold_date__gte=start_date)
except (ValueError, TypeError):
pass
if end_date_str:
try:
end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date()
cars_sold = cars_sold.filter(sold_date__lte=end_date)
except (ValueError, TypeError):
pass
# Write the data for the filtered cars
for car in cars_sold:
additional_services = car.get_additional_services()
services_total_price = additional_services['total']
services_vat_amount = additional_services['services_vat']
invoice_number = None
sold_date = None
if car.invoice:
invoice_number = car.invoice.invoice_number
sold_date = car.invoice.date_paid
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_amount,
car.final_price,
car.vat_amount,
services_total_price,
services_vat_amount,
car.final_price_plus_services_plus_vat,
invoice_number,
])
return response
@login_required
# @permission_required('inventory.view_staff')
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':
form = forms.CustomSetPasswordForm(staff.user, request.POST)
if form.is_valid():
print(form.cleaned_data['new_password1'])
print(form.cleaned_data['new_password2'])
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)
else:
messages.error(request, _('Invalid password. Please try again.'))
form = forms.CustomSetPasswordForm(staff.user)
return render(request, 'users/user_password_reset.html', {'form': form})
class RecallListView(ListView):
model = models.Recall
template_name = 'recalls/recall_list.html'
context_object_name = 'recalls'
paginate_by = 20
def get_queryset(self):
queryset = super().get_queryset().annotate(
dealer_count=Count('notifications', distinct=True),
car_count=Count('notifications__cars_affected', distinct=True)
)
return queryset.select_related('make', 'model', 'serie', 'trim')
class RecallDetailView(DetailView):
model = models.Recall
template_name = 'recalls/recall_detail.html'
context_object_name = 'recall'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['notifications'] = self.object.notifications.select_related('dealer')
return context
def RecallFilterView(request):
context = {'make_data': models.CarMake.objects.all()}
if request.method == "POST":
make = request.POST.get('make')
model = request.POST.get('model')
serie = request.POST.get('serie')
trim = request.POST.get('trim')
year = request.POST.get('year')
url = reverse('recall_create')
url += f"?make={make}&model={model}&serie={serie}&trim={trim}&year={year}"
cars = models.Car.objects.filter(id_car_make=make,id_car_model=model,id_car_serie=serie,id_car_trim=trim,year=year)
context['url'] = url
context['cars'] = cars
return render(request,'recalls/recall_filter.html',context)
class RecallCreateView(FormView):
template_name = 'recalls/recall_create.html'
form_class = forms.RecallCreateForm
success_url = reverse_lazy('recall_success')
def get_form(self, form_class=None):
form = super().get_form(form_class)
make = self.request.GET.get('make')
model = self.request.GET.get('model')
serie = self.request.GET.get('serie')
trim = self.request.GET.get('trim')
year = self.request.GET.get('year')
if make:
qs = models.CarMake.objects.filter(pk=make)
form.fields['make'].queryset = qs
form.initial['make'] = qs.first()
if model:
qs = models.CarModel.objects.filter(pk=model)
form.fields['model'].queryset = qs
form.initial['model'] = qs.first()
if serie:
qs = models.CarSerie.objects.filter(pk=serie)
form.fields['serie'].queryset = qs
form.initial['serie'] = qs.first()
if trim:
qs = models.CarTrim.objects.filter(pk=trim)
form.fields['trim'].queryset = qs
form.initial['trim'] = qs.first()
if year:
form.fields['year_from'].initial = year
form.fields['year_to'].initial = year
return form
def get_initial(self):
initial = super().get_initial()
if self.request.method == 'GET':
initial.update(self.request.GET.dict())
return initial
def form_valid(self, form):
recall = form.save(commit=False)
recall.created_by = self.request.user
recall.save()
# Get affected cars based on recall criteria
cars = models.Car.objects.all()
if recall.make:
cars = cars.filter(id_car_make=recall.make)
if recall.model:
cars = cars.filter(id_car_model=recall.model)
if recall.serie:
cars = cars.filter(id_car_serie=recall.serie)
if recall.trim:
cars = cars.filter(id_car_trim=recall.trim)
if recall.year_from:
cars = cars.filter(year__gte=recall.year_from)
if recall.year_to:
cars = cars.filter(year__lte=recall.year_to)
# Group cars by dealer and send notifications
dealers = models.Dealer.objects.filter(cars__in=cars).distinct()
for dealer in dealers:
dealer_cars = cars.filter(dealer=dealer)
notification = models.RecallNotification.objects.create(
recall=recall,
dealer=dealer
)
notification.cars_affected.set(dealer_cars)
# Send email
self.send_notification_email(dealer, recall, dealer_cars)
messages.success(self.request, _("Recall created and notifications sent successfully"))
return super().form_valid(form)
def send_notification_email(self, dealer, recall, cars):
subject = f"Recall Notification: {recall.title}"
message = render_to_string('recalls/email/recall_notification.txt', {
'dealer': dealer,
'recall': recall,
'cars': cars,
})
send_email(
subject,
message,
'noreply@yourdomain.com',
[dealer.user.email],
)
class RecallSuccessView(TemplateView):
template_name = 'recalls/recall_success.html'
@login_required
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')
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')
@login_required
@permission_required('inventory.add_ticket')
def create_ticket(request,dealer_slug):
if not request.is_dealer:
return redirect('home')
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
if request.method == 'POST':
form = forms.TicketForm(request.POST)
if form.is_valid():
instance = form.save(commit=False)
instance.dealer = dealer
instance.save()
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})
@login_required
@permission_required('inventory.view_ticket')
def ticket_list(request,dealer_slug):
dealer= get_object_or_404(models.Dealer, slug=dealer_slug)
tickets = models.Ticket.objects.filter(dealer=dealer).order_by('-created_at')
query=request.GET.get('q')
if query:
tickets=tickets.filter(Q(id__icontains=query)| Q(subject__icontains=query))
return render(request, 'support/ticket_list.html', {'tickets': tickets})
@login_required
@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})
@login_required
@permission_required('inventory.change_ticket')
def ticket_mark_resolved(request, ticket_id):
ticket = models.Ticket.objects.get(id=ticket_id)
ticket.status = 'resolved'
ticket.save()
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,
ticket.dealer.user.email,
subject,
message
)
return render(request, 'support/ticket_detail.html', {'ticket': ticket})
@login_required
@permission_required('inventory.change_ticket')
def ticket_update(request, ticket_id):
ticket = models.Ticket.objects.get(id=ticket_id)
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)
else:
form = forms.TicketResolutionForm(instance=ticket)
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'
class ChartOfAccountModelCreateView(ChartOfAccountModelCreateViewBase):
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'
class ChartOfAccountModelUpdateView(ChartOfAccountModelUpdateViewBase):
template_name = 'chart_of_accounts/coa_update.html'
permission_required = 'django_ledger.change_chartofaccountmodel'
class CharOfAccountModelActionView(CharOfAccountModelActionViewBase):
permission_required = 'django_ledger.change_chartofaccountmodel'
class CarDealershipSignUpView(CreateView):
model = models.UserRegistration
form_class = forms.CarDealershipRegistrationForm
template_name = 'account/signup-wizard.html'
success_url = reverse_lazy('registration_success')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = _('Car Dealership Registration')
return context
def form_valid(self, form):
response = super().form_valid(form)
messages.success(self.request, _('Your request has been submitted. We will contact you soon.'))
return response