haikal/inventory/views.py
2025-06-01 14:16:02 +03:00

8267 lines
324 KiB
Python

# Standard
import cv2
import json
import logging
from datetime import datetime
from time import sleep
import numpy as np
# from rich import print
from random import randint
from decimal import Decimal
from django.apps import apps
from calendar import month_name
from pyzbar.pyzbar import decode
from urllib.parse import urlparse, urlunparse
#####################################################################
from inventory.models import Status as LeadStatus
from background_task.models import Task
from django.db.models.deletion import RestrictedError
from django.http.response import StreamingHttpResponse
# 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, JsonResponse, HttpResponseForbidden
from django.forms import HiddenInput, ValidationError
from django.shortcuts import HttpResponse
from django.db.models import Sum, F, Count
from django.core.paginator import Paginator
from django.contrib.auth.models import User
from django.contrib.auth.models import 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 Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo
from django.views.generic import (
View,
ListView,
CreateView,
UpdateView,
DeleteView,
TemplateView,
ArchiveIndexView,
)
# 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.forms.bill import (
ApprovedBillModelUpdateForm,
InReviewBillModelUpdateForm,
)
from django_ledger.forms.invoice import (
DraftInvoiceModelUpdateForm,
ApprovedInvoiceModelUpdateForm,
PaidInvoiceModelUpdateForm,
)
from django_ledger.models import (
ItemTransactionModel,
EntityModel,
InvoiceModel,
BankAccountModel,
AccountModel,
JournalEntryModel,
TransactionModel,
EstimateModel,
CustomerModel,
ItemModel,
BillModel,
LedgerModel,
)
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 plans.models import Plan
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,
create_user_dealer,
get_car_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, send_email
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, *args, **kwargs):
"""
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]
"""
form1 = forms.WizardForm1()
form2 = forms.WizardForm2()
form3 = forms.WizardForm3()
if request.method == "POST":
if "Hx-Request" in request.headers:
form1 = forms.WizardForm1(request.POST)
return render(
request,
"account/signup-wizard.html",
{"form1": form1, "form2": form2, "form3": form3},
)
data = json.loads(request.body)
wf1 = data.get("wizardValidationForm1")
wf2 = data.get("wizardValidationForm2")
wf3 = data.get("wizardValidationForm3")
email = wf1.get("email")
password = wf1.get("password")
password_confirm = wf1.get("confirm_password")
name = wf2.get("name")
arabic_name = wf2.get("arabic_name")
phone = wf2.get("phone_number")
crn = wf3.get("crn")
vrn = wf3.get("vrn")
address = wf3.get("address")
if password != password_confirm:
return JsonResponse({"error": _("Passwords do not match")}, status=400)
try:
create_user_dealer(
email, password, name, arabic_name, phone, crn, vrn, address
)
return JsonResponse({"message": _("User created successfully")}, status=200)
except Exception as e:
return JsonResponse({"error": str(e)}, status=400)
return render(
request,
"account/signup-wizard.html",
{"form1": form1, "form2": form2, "form3": form3},
)
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"
def dispatch(self, request, *args, **kwargs):
# Redirect unauthenticated users to the welcome page
if not request.user.is_authenticated:
return redirect("welcome")
return super().dispatch(request, *args, **kwargs)
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"
class ManagerDashboard(LoginRequiredMixin, TemplateView):
"""
ManagerDashboard class is a view handling the dashboard for a manager.
Provides functionality to manage and view various statistics and data specific
to the dealer associated with the authenticated manager. It uses a specific
template and ensures authentication before granting access. The class
aggregates data about cars, leads, financial statistics, and other related
business information for display in the manager's dashboard.
:ivar template_name: Path to the template used for rendering the manager's dashboard.
:type template_name: str
"""
template_name = "dashboards/manager.html"
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return redirect("welcome")
if not getattr(request.user, "dealer", False):
return HttpResponseForbidden(
"You are not authorized to view this dashboard."
)
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
dealer = get_user_type(self.request)
entity = dealer.entity
total_cars = models.Car.objects.filter(dealer=dealer).count()
total_reservations = models.CarReservation.objects.filter(
reserved_until__gte=timezone.now()
).count()
stats = models.CarFinance.objects.aggregate(
total_cost_price=Sum("cost_price"),
total_selling_price=Sum("selling_price"),
)
total_cost_price = stats["total_cost_price"] or 0
total_selling_price = stats["total_selling_price"] or 0
total_profit = total_selling_price - total_cost_price
new_leads = models.Lead.objects.filter(
dealer=dealer, status=models.Status.NEW
).count()
pending_leads = models.Lead.objects.filter(
dealer=dealer, status=models.Status.PENDING
).count()
canceled_leads = models.Lead.objects.filter(
dealer=dealer, status=models.Status.CANCELED
).count()
available_cars = models.Car.objects.filter(
dealer=dealer, status=models.CarStatusChoices.AVAILABLE
).count()
sold_cars = models.Car.objects.filter(
dealer=dealer, status=models.CarStatusChoices.SOLD
).count()
reserved_cars = models.Car.objects.filter(
dealer=dealer, status=models.CarStatusChoices.RESERVED
).count()
hold_cars = models.Car.objects.filter(
dealer=dealer, status=models.CarStatusChoices.HOLD
).count()
damaged_cars = models.Car.objects.filter(
dealer=dealer, status=models.CarStatusChoices.DAMAGED
).count()
transfer_cars = models.Car.objects.filter(
dealer=dealer, status=models.CarStatusChoices.TRANSFER
).count()
reserved_percentage = reserved_cars / total_cars * 100
sold_percentage = sold_cars / total_cars * 100
qs = (
models.Car.objects.values("id_car_make__name")
.annotate(count=Count("id"))
.order_by("id_car_make__name")
)
car_by_make = list(qs)
total_activity = models.UserActivityLog.objects.filter(user=dealer.user).count()
staff = models.Staff.objects.filter(dealer=dealer).count()
total_leads = models.Lead.objects.filter(dealer=dealer).count()
invoices = entity.get_invoices().count()
customers = entity.get_customers().count()
purchase_orders = entity.get_purchase_orders().count()
estimates = entity.get_estimates().count()
context["dealer"] = dealer
context["total_activity"] = total_activity
context["total_cars"] = total_cars
context["total_reservations"] = total_reservations
context["total_cost_price"] = total_cost_price
context["total_selling_price"] = total_selling_price
context["total_profit"] = total_profit
context["new_leads"] = new_leads
context["pending_leads"] = pending_leads
context["canceled_leads"] = canceled_leads
context["reserved_percentage"] = reserved_percentage
context["sold_percentage"] = sold_percentage
context["available_cars"] = available_cars
context["sold_cars"] = sold_cars
context["reserved_cars"] = reserved_cars
context["hold_cars"] = hold_cars
context["damaged_cars"] = damaged_cars
context["transfer_cars"] = transfer_cars
context["car"] = json.dumps(car_by_make)
context["customers"] = customers
context["staff"] = staff
context["total_leads"] = total_leads
context["invoices"] = invoices
context["estimates"] = estimates
context["purchase_orders"] = purchase_orders
return context
class SalesDashboard(LoginRequiredMixin, TemplateView):
"""
SalesDashboard class provides a view for the sales dashboard.
This class is responsible for generating the context data required to
display various statistics and information on the sales dashboard. It
inherits from `LoginRequiredMixin` and `TemplateView`, ensuring only
authenticated users can access the view. The dashboard includes data
such as the total number of cars, reservations, sold percentages,
reserved percentages, and cars categorized by various statuses. The
purpose of this dashboard is to provide dealership staff an overview
of their inventory and related sales metrics.
:ivar template_name: The name of the HTML template used for rendering
the sales dashboard.
:type template_name: str
"""
template_name = "dashboards/sales.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
dealer = get_user_type(self.request)
staff = getattr(self.request.user, "staff", None)
total_cars = models.Car.objects.filter(dealer=dealer).count()
total_reservations = models.CarReservation.objects.filter(
reserved_by=self.request.user, reserved_until__gte=timezone.now()
).count()
available_cars = models.Car.objects.filter(
dealer=dealer, status=models.CarStatusChoices.AVAILABLE
).count()
sold_cars = models.Car.objects.filter(
dealer=dealer, status=models.CarStatusChoices.SOLD
).count()
reserved_cars = models.Car.objects.filter(
dealer=dealer, status=models.CarStatusChoices.RESERVED
).count()
hold_cars = models.Car.objects.filter(
dealer=dealer, status=models.CarStatusChoices.HOLD
).count()
damaged_cars = models.Car.objects.filter(
dealer=dealer, status=models.CarStatusChoices.DAMAGED
).count()
transfer_cars = models.Car.objects.filter(
dealer=dealer, status=models.CarStatusChoices.TRANSFER
).count()
reserved_percentage = reserved_cars / total_cars * 100
sold_percentage = sold_cars / total_cars * 100
qs = (
models.Car.objects.values("id_car_make__name")
.annotate(count=Count("id"))
.order_by("id_car_make__name")
)
car_by_make = list(qs)
context["dealer"] = dealer
context["staff"] = staff
context["total_cars"] = total_cars
context["total_reservations"] = total_reservations
context["reserved_percentage"] = reserved_percentage
context["sold_percentage"] = sold_percentage
context["available_cars"] = available_cars
context["sold_cars"] = sold_cars
context["reserved_cars"] = reserved_cars
context["hold_cars"] = hold_cars
context["damaged_cars"] = damaged_cars
context["transfer_cars"] = transfer_cars
context["car"] = json.dumps(car_by_make)
return context
def terms_and_privacy(request):
return render(request, 'terms_and_privacy.html')
class WelcomeView(TemplateView):
"""
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
"""
template_name = "welcome.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
plan_list = Plan.objects.all()
context["plan_list"] = plan_list
return context
class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin, 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"]
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
def get_success_url(self):
"""Determine the redirect URL based on user choice."""
if self.request.POST.get("add_another"):
return reverse("car_add")
return reverse("inventory_stats")
def form_valid(self, form):
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):
context = super().get_context_data(**kwargs)
dealer = get_user_type(self.request)
context["vendor_exists"] = dealer.vendors.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, *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)
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"
)
except Exception:
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:
np_arr = np.frombuffer(image_file.read(), np.uint8)
image = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
if image is None:
raise ValueError("Invalid image format")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
decoded_objects = decode(gray)
if not decoded_objects:
return JsonResponse(
{"success": False, "error": _("No QR/Barcode detected")}
)
code = decoded_objects[0].data.decode("utf-8").strip()
car = get_object_or_404(models.Car, vin=code)
return JsonResponse(
{
"success": True,
"code": code,
"redirect_url": reverse("car_detail", args=[car.slug]),
}
)
except Exception as e:
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, 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.view_car"]
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={"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 finance details updated successfully")
permission_required = ["inventory.change_carfinance"]
def get_success_url(self):
return reverse("car_detail", kwargs={"slug": self.object.car.slug})
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["instance"] = self.get_object()
return kwargs
def get_initial(self):
initial = super().get_initial()
instance = self.get_object()
dealer = get_user_type(self.request)
selected_items = instance.additional_services.filter(dealer=dealer)
initial["additional_finances"] = selected_items
return initial
def get_form(self, form_class=None):
form = super().get_form(form_class)
dealer = get_user_type(self.request)
form.fields[
"additional_finances"
].queryset = models.AdditionalServices.objects.filter(dealer=dealer)
return form
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 = 30
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(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")
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)
if status:
qs = qs.filter(status=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
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
cars = models.Car.objects.filter(dealer=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:
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:
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()
],
}
print(result["makes"])
return render(request, "inventory/inventory_stats.html", {"inventory": result})
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"]
class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
"""
Handles the creation of car finance records within the inventory system.
This view provides functionality to create car finance records tied to a
specific car in the inventory. It enforces that the user is logged in and
has the required permissions to add a car finance record. It also customizes
form behavior and context data to associate the finance record with the
corresponding car and populate additional services based on the user's type.
:ivar model: The database model associated with the view.
:type model: models.CarFinance
:ivar form_class: The form class used to create car finance records.
:type form_class: forms.CarFinanceForm
:ivar template_name: The template used to render the car finance creation page.
:type template_name: str
:ivar permission_required: The list of permissions required to access this view.
:type permission_required: list
"""
model = models.CarFinance
form_class = forms.CarFinanceForm
template_name = "inventory/car_finance_form.html"
permission_required = ["inventory.add_carfinance"]
def dispatch(self, request, *args, **kwargs):
self.car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
form.instance.car = self.car
messages.success(self.request, _("Car finance details saved successfully"))
return super().form_valid(form)
def get_success_url(self):
return reverse("car_detail", kwargs={"slug": self.car.slug})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["car"] = self.car
return context
def get_form(self, form_class=None):
form = super().get_form(form_class)
dealer = get_user_type(self.request)
form.fields[
"additional_finances"
].queryset = models.AdditionalServices.objects.filter(dealer=dealer)
return form
class CarFinanceUpdateView(
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
):
"""
Manages the update of car finance details.
This class-based view provides functionality for updating car finance
information in the system. It enforces login and specific permissions for
access, and it leverages mixins to provide success messages upon completing
updates. It is designed to interact with car finance data, update form
handling, and enforce related business logic.
:ivar model: The model associated with the view.
:type model: models.CarFinance
:ivar form_class: The form class used by the view.
:type form_class: forms.CarFinanceForm
:ivar template_name: The template used for rendering the form.
:type template_name: str
:ivar success_message: The success message displayed after an update.
:type success_message: str
:ivar permission_required: List of permissions required to access the view.
:type permission_required: list
"""
model = models.CarFinance
form_class = forms.CarFinanceForm
template_name = "inventory/car_finance_form.html"
success_message = _("Car finance details updated successfully")
permission_required = ["inventory.change_carfinance"]
def get_success_url(self):
return reverse("car_detail", kwargs={"slug": self.object.car.slug})
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["instance"] = self.get_object()
return kwargs
def get_initial(self):
initial = super().get_initial()
instance = self.get_object()
dealer = get_user_type(self.request)
selected_items = instance.additional_services.filter(dealer=dealer)
initial["additional_finances"] = selected_items
return initial
def get_form(self, form_class=None):
form = super().get_form(form_class)
dealer = get_user_type(self.request)
form.fields[
"additional_finances"
].queryset = models.AdditionalServices.objects.filter(dealer=dealer)
return 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={"slug": self.object.slug})
def get_form(self, form_class=None):
form = super().get_form(form_class)
dealer = get_user_type(self.request)
print(dealer.get_vendors())
form.fields["vendor"].queryset = dealer.vendors.all()
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"
success_url = reverse_lazy("inventory_stats")
permission_required = ["inventory.delete_car"]
def delete(self, request, *args, **kwargs):
messages.success(request, _("Car deleted successfully"))
return super().delete(request, *args, **kwargs)
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, 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"
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, 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"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["action"] = self.request.GET.get("action")
return context
@login_required
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
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
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, 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"
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={"slug": self.kwargs["slug"]})
class CarRegistrationCreateView(LoginRequiredMixin, 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"
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={"slug": self.kwargs["slug"]})
@login_required()
def reserve_car_view(request, 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, 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", 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", 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, 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"
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)
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["quota_display"] = (
f"{staff_count}/{dealer.user_quota}" if dealer.user_quota else "0"
)
return context
class DealerUpdateView(LoginRequiredMixin, 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")
def get_success_url(self):
return reverse("dealer_detail", kwargs={"slug": self.object.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 = ["django_ledger.view_customermodel"]
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", "")
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 = ["django_ledger.view_customermodel"]
def get_context_data(self, **kwargs):
dealer = get_user_type(self.request)
entity = dealer.entity
context = super().get_context_data(**kwargs)
context["customer_notes"] = models.Notes.objects.filter(
object_id=self.object.pk
)
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
return context
@login_required
def add_note_to_customer(request, 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.
"""
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", slug=customer.slug)
else:
form = forms.NoteForm()
return render(
request, "customers/note_form.html", {"form": form, "customer": customer}
)
@login_required
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, 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 = ["django_ledger.add_customermodel"]
template_name = "customers/customer_form.html"
success_url = reverse_lazy("customer_list")
success_message = "Customer created successfully"
def form_valid(self, form):
if customer := models.Customer.objects.filter(
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
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)
class CustomerUpdateView(LoginRequiredMixin, PermissionRequiredMixin, 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 = ["django_ledger.change_customermodel"]
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)
@login_required
def delete_customer(request, 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")
class VendorListView(LoginRequiredMixin, 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"
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
def vendorDetailView(request, 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
"""
vendor = get_object_or_404(models.Vendor, slug=slug)
return render(
request, template_name="vendors/view_vendor.html", context={"vendor": vendor}
)
class VendorCreateView(
LoginRequiredMixin,
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_url = reverse_lazy("vendor_list")
success_message = _("Vendor created successfully")
def form_valid(self, form):
if vendor := models.Vendor.objects.filter(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 = get_user_type(self.request)
form.instance.dealer = dealer
form.instance.save()
return super().form_valid(form)
class VendorUpdateView(
LoginRequiredMixin,
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_url = reverse_lazy("vendor_list")
success_message = _("Vendor updated successfully")
# 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)
@login_required
def delete_vendor(request, 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")
# group
class GroupListView(LoginRequiredMixin, 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"
def get_queryset(self):
dealer = get_user_type(self.request)
return dealer.groups.all()
class GroupDetailView(LoginRequiredMixin, 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"
class GroupCreateView(
LoginRequiredMixin,
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_url = reverse_lazy("group_list")
success_message = _("Group created successfully")
def form_valid(self, form):
dealer = get_user_type(self.request)
instance = form.save(commit=False)
group = Group.objects.create(name=f"{dealer.slug}_{instance.name}")
instance.dealer = dealer
instance.group = group
instance.save()
return super().form_valid(form)
class GroupUpdateView(
LoginRequiredMixin,
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_url = reverse_lazy("group_list")
success_message = _("Group updated successfully")
def form_valid(self, form):
dealer = get_user_type(self.request)
instance = form.save(commit=False)
instance.set_defualt_permissions()
instance.group.name = f"{dealer.slug}_{instance.name}"
instance.save()
return super().form_valid(form)
@login_required
def GroupDeleteview(request, 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.
"""
group = get_object_or_404(models.CustomGroup, pk=pk)
group.delete()
messages.success(request, _("Group deleted successfully"))
return redirect("group_list")
@login_required
def GroupPermissionView(request, pk):
"""
Handles the view for adding or modifying permissions of a specific group. This view
fetches the group based on the primary key passed as a parameter, and either displays
a form for editing permissions or processes the submitted permissions.
If the request method is POST, the permissions of the group are cleared and updated
based on the submitted data. A success message is displayed upon completion, and
the user is redirected to the group's detail page.
In case of a GET request, the view renders the form pre-filled with the group's
current permissions.
:param request: The HTTP request object.
:type request: HttpRequest
:param pk: The primary key of the group whose permissions are being modified.
:type pk: int
:return: The HTTP response depending on the request type. For GET requests, renders
the permission form for the specified group. For POST requests, clears and updates
the group's permissions and redirects to the group's detail page.
:rtype: HttpResponse
"""
group = get_object_or_404(models.CustomGroup, pk=pk)
if request.method == "POST":
form = forms.PermissionForm(request.POST)
group.clear_permissions()
permissions = request.POST.getlist("name")
for i in permissions:
group.add_permission(Permission.objects.get(id=int(i)))
messages.success(request, _("Permission added successfully"))
return redirect("group_detail", pk=group.pk)
form = forms.PermissionForm(initial={"name": group.permissions})
return render(
request, "groups/group_permission_form.html", {"group": group, "form": form}
)
# Users
@login_required
def UserGroupView(request, 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
"""
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", 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, 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"
def get_queryset(self):
query = self.request.GET.get("q")
dealer = get_user_type(self.request)
staff = models.Staff.objects.filter(dealer=dealer, active=True).all()
return apply_search_filters(staff, query)
class UserDetailView(LoginRequiredMixin, 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_"
class UserCreateView(
LoginRequiredMixin,
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")
def form_valid(self, form):
dealer = get_user_type(self.request)
# quota_dict = get_user_quota(dealer.user)
# allowed_users = quota_dict.get("Users")
# if allowed_users is None:
# messages.error(self.request, _("The user quota for staff members is not defined. Please contact support"))
# return self.form_invalid(form)
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"]
password = "Tenhal@123"
user = User.objects.create_user(
username=email, email=email, password=password
)
user.is_staff = True
user.save()
staff_member = StaffMember.objects.create(user=user)
for service in form.cleaned_data["service_offered"]:
staff_member.services_offered.add(service)
staff = form.save(commit=False)
staff.staff_member = staff_member
staff.dealer = dealer
staff.add_as_manager()
group = Group.objects.filter(customgroup__name__iexact=staff.staff_type).first()
staff.save()
if group:
staff.add_group(group)
return super().form_valid(form)
class UserUpdateView(
LoginRequiredMixin,
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")
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)
form.fields["email"].disabled = True
return form
def get_initial(self):
initial = super().get_initial()
initial["email"] = self.object.staff_member.user.email
initial["service_offered"] = self.object.staff_member.services_offered.all()
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"]
staff.staff_type = form.cleaned_data["staff_type"]
staff.add_as_manager()
staff.save()
return super().form_valid(form)
@login_required
def UserDeleteview(request, 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.
"""
staff = get_object_or_404(models.Staff, slug=slug)
staff.deactivate_account()
messages.success(request, _("User deleted successfully"))
return redirect("user_list")
class OrganizationListView(LoginRequiredMixin, 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 = 30
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)
class OrganizationDetailView(LoginRequiredMixin, 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"
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 = ["django_ledger.add_customermodel"]
template_name = "organizations/organization_form.html"
success_url = reverse_lazy("organization_list")
success_message = "Organization created successfully"
def form_valid(self, form):
if organization := models.Organization.objects.filter(
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)
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 = ["django_ledger.change_customermodel"]
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)
@login_required
def OrganizationDeleteView(request, 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("organization_list")
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.user.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 = ["inventory.view_carfinance"]
def get_queryset(self):
query = self.request.GET.get("q")
dealer = get_user_type(self.request)
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_url = reverse_lazy("bank_account_list")
success_message = _("Bank account created successfully")
permission_required = ["inventory.view_carfinance"]
def form_valid(self, form):
dealer = get_user_type(self.request)
form.instance.entity_model = dealer.entity
return super().form_valid(form)
def get_form_kwargs(self):
dealer = get_user_type(self.request)
entity = dealer.entity
kwargs = super().get_form_kwargs()
kwargs["entity_slug"] = entity.slug
kwargs["user_model"] = entity.admin
return kwargs
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 = ["inventory.view_carfinance"]
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_url = reverse_lazy("bank_account_list")
success_message = _("Bank account updated successfully")
permission_required = ["inventory.view_carfinance"]
def get_form_kwargs(self):
dealer = get_user_type(self.request)
entity = dealer.entity
kwargs = super().get_form_kwargs()
kwargs["entity_slug"] = entity.slug # Get entity_slug from URL
kwargs["user_model"] = entity.admin # Get user_model from the request
return kwargs
@login_required
def bank_account_delete(request, 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
"""
bank_account = get_object_or_404(BankAccountModel, pk=pk)
if request.method == "POST":
bank_account.delete()
messages.success(request, _("Bank account deleted successfully"))
return redirect("bank_account_list")
return render(
request,
"ledger/bank_accounts/bank_account_delete.html",
{"bank_account": bank_account},
)
# 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"
paginate_by = 30
permission_required = ["inventory.view_carfinance"]
def get_queryset(self):
query = self.request.GET.get("q")
dealer = get_user_type(self.request)
accounts = dealer.entity.get_all_accounts()
return apply_search_filters(accounts, query)
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_url = reverse_lazy("account_list")
success_message = _("Account created successfully")
permission_required = ["inventory.view_carfinance"]
def form_valid(self, form):
dealer = get_user_type(self.request)
form.instance.entity_model = dealer.entity
form.instance.coa_model = dealer.entity.get_default_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()
kwargs["coa_model"] = dealer.entity.get_default_coa()
return kwargs
def get_form(self, form_class=None):
form = super().get_form(form_class)
entity = get_user_type(self.request).entity
form.initial["coa_model"] = entity.get_default_coa()
return form
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 = ["inventory.view_carfinance"]
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",
)
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_url = reverse_lazy("account_list")
success_message = _("Account updated successfully")
permission_required = ["inventory.view_carfinance"]
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
@login_required
@permission_required("inventory.view_carfinance")
def account_delete(request, 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
"""
account = get_object_or_404(AccountModel, pk=pk)
account.delete()
messages.success(request, _("Account deleted successfully"))
return redirect("account_list")
# Sales list
@login_required
@permission_required("inventory.view_lead", raise_exception=True)
def sales_list_view(request):
"""
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_user_type(request)
entity = dealer.entity
transactions = ItemTransactionModel.objects.for_entity(
entity_slug=entity.slug, user_model=dealer.user
).order_by("created")
paginator = Paginator(transactions, 30)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
txs = get_item_transactions(page_obj)
context = {"txs": txs, "page_obj": page_obj}
return render(request, "sales/sales_list.html", context)
# 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_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)
return queryset
# @csrf_exempt
@login_required
@permission_required("django_ledger.add_estimatemodel", raise_exception=True)
def create_estimate(request, 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_user_type(request)
entity = dealer.entity
if request.method == "POST":
# try:
data = json.loads(request.body)
title = data.get("title")
customer_id = data.get("customer")
# terms = data.get("terms")
# customer = entity.get_customers().filter(pk=customer_id).first()
customer = models.Customer.objects.filter(pk=int(customer_id)).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 = ItemModel.objects.filter(
# additional_info__car_info__hash=item.get("item_id")
# ).all()
# for i in car_instance[: int(quantities[0])]:
# items_txs.append(
# {
# "item_number": i.item_number,
# "quantity": 1,
# "unit_cost": i.additional_info.get("car_finance").get(
# "selling_price"
# ),
# "unit_revenue": i.additional_info.get("car_finance").get(
# "selling_price"
# ),
# "total_amount": (
# i.additional_info.get("car_finance").get("total_vat")
# ),
# }
# )
car_instance = models.Car.objects.filter(hash=item.get("item_id"),finances__is_sold=False).all()
for i in car_instance[: int(quantities[0])]:
items_txs.append(
{
"item_number": i.item_model.item_number,
"quantity": 1,
"unit_cost": round(float(i.finances.selling_price)),
"unit_revenue": round(float(i.finances.selling_price)),
"total_amount": round(float(i.finances.total_vat)),
}
)
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.finances.cost_price,
"unit_revenue": instance.finances.selling_price,
"quantity": Decimal(quantities),
"total_amount": instance.finances.total_vat * int(quantities),
}
}
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 = ItemModel.objects.filter(item_number=item).first()
instance = models.Car.objects.get(vin=item_instance.name)
reserve_car(instance, request)
else:
item_instance = ItemModel.objects.filter(
additioinal_info__car_info__hash=items
).first()
instance = models.Car.objects.get(hash=item)
reserve_car(instance, request)
opportunity_id = data.get("opportunity_id")
if opportunity_id != "None":
opportunity = models.Opportunity.objects.get(slug=opportunity_id)
opportunity.estimate = estimate
opportunity.save()
url = reverse("estimate_detail", kwargs={"pk": estimate.pk})
return JsonResponse(
{
"status": "success",
"message": _("Quotation created successfully"),
"url": f"{url}",
}
)
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
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,
finances__isnull=False,
status="available",
)
.annotate(
color=F("colors__exterior__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",
"color",
"color_name",
"hash",
)
.annotate(hash_count=Count("hash"))
.distinct()
)
context = {
"form": form,
"items": [
{
"make": x[0],
"model": x[1],
"serie": x[2],
"trim": x[3],
"color": x[4],
"color_name": x[5],
"hash": x[6],
"hash_count": x[7],
}
for x in car_list
],
"opportunity_id": slug if slug else None,
"customer_count": entity.get_customers().count(),
}
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):
estimate = kwargs.get("object")
if estimate.get_itemtxs_data():
calculator = CarFinanceCalculator(estimate)
finance_data = calculator.get_finance_data()
invoice_obj = InvoiceModel.objects.all().filter(ce_model=estimate).first()
kwargs["data"] = finance_data
kwargs["invoice"] = invoice_obj
return super().get_context_data(**kwargs)
@login_required
@permission_required("inventory.add_saleorder", raise_exception=True)
def create_sale_order(request, 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
"""
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.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()
except KeyError:
pass
dealer = get_user_type(request)
item.item_model.car.mark_as_sold()
return redirect("estimate_detail", pk=estimate.pk)
# models.Activity.objects.create(dealer=dealer,content_object=item.item_model.car, notes="Car Sold",created_by=request.user,activity_type=models.ActionChoices.SALE_CAR)
else:
print(form.errors)
messages.success(request, "Sale Order created successfully")
return redirect("estimate_detail", pk=estimate.pk)
form = forms.SaleOrderForm()
form.fields["estimate"].queryset = EstimateModel.objects.filter(pk=pk)
form.initial["estimate"] = estimate
calculator = CarFinanceCalculator(estimate)
finance_data = calculator.get_finance_data()
return render(
request,
"sales/estimates/sale_order_form1.html",
{"form": form, "estimate": estimate, "items": items, "data": finance_data},
)
class SaleOrderDetail(DetailView):
model = models.SaleOrder
template_name = "sales/orders/order_details.html"
context_object_name = "saleorder"
def get_object(self, queryset=None):
order_pk = self.kwargs.get('order_pk')
return models.SaleOrder.objects.get(
pk=order_pk,
)
def get_context_data(self, **kwargs):
saleorder = kwargs.get("object")
estimate = saleorder.estimate
if estimate.get_itemtxs_data():
calculator = CarFinanceCalculator(estimate)
finance_data = calculator.get_finance_data()
kwargs["data"] = finance_data
return super().get_context_data(**kwargs)
@login_required
def preview_sale_order(request, 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
"""
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):
dealer = get_user_type(self.request)
estimate = kwargs.get("object")
if estimate.get_itemtxs_data():
# data = get_financial_values(estimate)
calculator = CarFinanceCalculator(estimate)
kwargs["data"] = calculator.get_finance_data()
kwargs["dealer"] = dealer
return super().get_context_data(**kwargs)
@login_required
@permission_required("django_ledger.change_estimatemodel", raise_exception=True)
def estimate_mark_as(request, 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
"""
estimate = get_object_or_404(EstimateModel, pk=pk)
mark = request.GET.get("mark")
if mark:
if mark == "review":
if not estimate.can_review():
messages.error(request, _("Quotation is not ready for review"))
return redirect("estimate_detail", 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", pk=estimate.pk)
estimate.mark_as_approved()
messages.success(request, _("Quotation approved successfully"))
elif mark == "rejected":
if not estimate.can_cancel():
messages.error(request, _("Quotation is not ready for rejection"))
return redirect("estimate_detail", 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", pk=estimate.pk)
elif mark == "canceled":
if not estimate.can_cancel():
messages.error(request, _("Quotation is not ready for cancellation"))
return redirect("estimate_detail", 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()
except Exception:
pass
messages.success(request, _("Quotation canceled successfully"))
estimate.save()
messages.success(request, _("Quotation marked as ") + mark.upper())
return redirect("estimate_detail", 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 = 30
permission_required = ["django_ledger.view_invoicemodel"]
def get_queryset(self):
query = self.request.GET.get("q")
dealer = get_user_type(self.request)
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()
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.view_invoicemodel"]
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
dealer = get_user_type(self.request)
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.view_invoicemodel"]
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
dealer = get_user_type(self.request)
kwargs["entity_slug"] = dealer.entity
kwargs["user_model"] = dealer.entity.admin
return kwargs
def get_success_url(self):
return reverse_lazy("invoice_detail", kwargs={"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.view_invoicemodel"]
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
dealer = get_user_type(self.request)
kwargs["entity_slug"] = dealer.entity
kwargs["user_model"] = dealer.entity.admin
return kwargs
def get_success_url(self):
return reverse_lazy("invoice_detail", kwargs={"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", 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, 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
"""
invoice = get_object_or_404(InvoiceModel, pk=pk)
dealer = get_user_type(request)
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", pk=invoice.pk)
invoice.mark_as_approved(
entity_slug=dealer.entity.slug, user_model=dealer.entity.admin
)
invoice.save()
return redirect("invoice_detail", pk=invoice.pk)
@login_required
@permission_required("django_ledger.add_invoicemodel", raise_exception=True)
def invoice_create(request, 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
"""
estimate = get_object_or_404(EstimateModel, pk=pk)
dealer = get_user_type(request)
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()
invoice_itemtxs = {
i.get("item_number"): {
"unit_cost": i.get("total_vat"),
"quantity": i.get("quantity"),
"total_amount": i.get("total_vat"),
}
for i in finance_data.get("cars")
}
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", 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,
}
)
print(dir(form.fields["customer"]))
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_user_type(self.request)
invoice = kwargs.get("object")
if invoice.get_itemtxs_data():
calculator = CarFinanceCalculator(invoice)
finance_data = calculator.get_finance_data()
kwargs["data"] = finance_data
kwargs["dealer"] = dealer
return super().get_context_data(**kwargs)
# payments
@login_required
@permission_required("django_ledger.add_journalentrymodel", raise_exception=True)
def PaymentCreateView(request, 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.
"""
invoice = InvoiceModel.objects.filter(pk=pk).first()
bill = BillModel.objects.filter(pk=pk).first()
model = invoice if invoice else bill
dealer = get_user_type(request)
entity = dealer.entity
form = forms.PaymentForm()
if request.method == "POST":
form = forms.PaymentForm(request.POST)
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")
redirect_url = "invoice_detail" 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 redirect(redirect_url, pk=model.pk)
if model.amount_paid + amount > model.amount_due:
messages.error(request, _("Amount exceeds due amount"))
return redirect(redirect_url, pk=model.pk)
try:
if invoice:
set_invoice_payment(dealer, entity, invoice, amount, payment_method)
elif bill:
set_bill_payment(dealer, entity, bill, amount, payment_method)
messages.success(request, _("Payment created successfully"))
return redirect(redirect_url, pk=model.pk)
except Exception as e:
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_form.html", {"model": model, "form": form}
)
@login_required
@permission_required("django_ledger.view_journalentrymodel", raise_exception=True)
def PaymentListView(request):
"""
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_user_type(request)
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("django_ledger.view_journalentrymodel", raise_exception=True)
def PaymentDetailView(request, 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
"""
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("django_ledger.change_journalentrymodel", raise_exception=True)
def payment_mark_as_paid(request, 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.
"""
invoice = get_object_or_404(InvoiceModel, pk=pk)
if request.method == "POST":
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()
messages.success(request, _("Payment created successfully"))
else:
messages.error(
request,
_("Invoice is not fully paid, Payment cannot be marked as paid"),
)
except Exception as e:
messages.error(request, f"Error: {str(e)}")
return redirect("invoice_detail", 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):
query = self.request.GET.get("q")
dealer = get_user_type(self.request)
qs = models.Lead.objects.filter(dealer=dealer).exclude(status="converted")
if query:
qs = apply_search_filters(qs, query)
if self.request.is_dealer:
return qs
staffmember = getattr(self.request.user, "staffmember", None)
if staff:= getattr(staffmember, "staff", None):
return qs.filter(staff=staff)
return models.Lead.objects.none()
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):
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["status_history"] = models.LeadStatusHistory.objects.filter(
lead=self.object
)
context["transfer_form"] = forms.LeadTransferForm()
context["activity_form"] = forms.ActivityForm()
context["staff_task_form"] = forms.StaffTaskForm()
context["note_form"] = forms.NoteForm()
return context
@login_required
@permission_required("inventory.add_lead", raise_exception=True)
def lead_create(request):
"""
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_user_type(request)
if request.method == "POST":
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(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(
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()
messages.success(request, _("Lead created successfully"))
return redirect("lead_list")
else:
messages.error(
request, f"Lead was not created ... : {str(form.errors)}"
)
# if email:= request.POST.get("email"):
# if models.Customer.objects.filter(email=email).exists():
# form.errors['email'] = form.error_class([
# _("Email already exists")
# ])
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):
form.fields["id_car_model"].queryset = models.CarModel.objects.filter(
id_car_make=int(make)
)
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)
form.fields["staff"].queryset = form.fields["staff"].queryset.filter(dealer=dealer,staff_type="sales")
print(form.fields["staff"].queryset)
if hasattr(request.user.staffmember,"staff"):
form.initial["staff"] = request.user.staffmember.staff
form.fields["staff"].widget.attrs["disabled"] = True
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():
form.fields["id_car_model"].queryset = first_make.carmodel_set.all()
return render(request, "crm/leads/lead_form.html", {"form": form})
def lead_tracking(request):
dealer = get_user_type(request)
new = models.Lead.objects.filter(dealer=dealer, status="new")
follow_up = models.Lead.objects.filter(
dealer=dealer, next_action__in=["call", "meeting"]
)
won = models.Lead.objects.filter(dealer=dealer, status="won")
lose = models.Lead.objects.filter(dealer=dealer, status="lost")
negotiation = models.Lead.objects.filter(dealer=dealer, status="negotiation")
context = {
"new": new,
"follow_up": follow_up,
"won": won,
"lose": lose,
"negotiation": negotiation,
}
return render(request, "crm/leads/lead_tracking.html", context)
# @require_POST
def update_lead_actions(request):
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)
if not all([lead_id, current_action, next_action]):
return JsonResponse(
{"success": False, "message": "All fields are required"}, status=400
)
# Get the lead
lead = models.Lead.objects.get(id=lead_id)
# Update lead fields
lead.status = current_action
lead.next_action = next_action
# 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)
except ValueError:
return JsonResponse(
{"success": False, "message": "Invalid date format"}, status=400
)
# Save the lead
lead.save()
return JsonResponse(
{"success": True, "message": "Actions updated successfully"}
)
except models.Lead.DoesNotExist:
return JsonResponse({"success": False, "message": "Lead not found"}, status=404)
except Exception as e:
return JsonResponse({"success": False, "message": str(e)}, status=500)
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):
form = super().get_form(form_class)
form.fields[
"id_car_model"
].queryset = form.instance.id_car_make.carmodel_set.all()
return form
@login_required
@permission_required("inventory.delete_lead", raise_exception=True)
def LeadDeleteView(request, 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.
"""
lead = get_object_or_404(models.Lead, slug=slug)
try:
User.objects.get(email=lead.customer.email).delete()
lead.customer.delete()
except Exception as e:
print(e)
lead.delete()
messages.success(request, _("Lead deleted successfully"))
return redirect("lead_list")
@login_required
def add_note_to_lead(request, 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", slug=lead.slug)
else:
form = forms.NoteForm()
return render(request, "crm/note_form.html", {"form": form, "lead": lead})
@login_required
def add_note_to_opportunity(request, 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_user_type(request)
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", slug=opportunity.slug)
@login_required
def delete_note(request, 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
"""
note = get_object_or_404(models.Notes, pk=pk, created_by=request.user)
lead_pk = note.content_object.pk
lead = models.Lead.objects.get(pk=lead_pk)
note.delete()
messages.success(request, _("Note deleted successfully."))
return redirect("lead_detail", slug=lead.slug)
@login_required
@permission_required("inventory.change_lead", raise_exception=True)
def lead_convert(request, 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_user_type(request)
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")
@login_required
@permission_required("inventory.add_lead", raise_exception=True)
def schedule_lead(request, 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
"""
if not request.is_staff:
messages.error(request, _("You do not have permission to schedule lead"))
return redirect("lead_list")
dealer = get_user_type(request)
lead = get_object_or_404(models.Lead, slug=slug, dealer=dealer)
if request.method == "POST":
form = forms.ScheduleForm(request.POST)
if form.is_valid():
instance = form.save(commit=False)
instance.lead = lead
instance.scheduled_by = request.user
instance.customer = lead.get_customer_model()
# Create AppointmentRequest
# service,_ = Service.objects.get_or_create(name=instance.scheduled_type,duration=datetime.timedelta(minutes=10),price=0)
service = Service.objects.get(name=instance.scheduled_type)
# service = Service.objects.filter(name=instance.scheduled_type).first()
# if not service:
# messages.error(request, "Service not found!")
# return redirect("lead_list")
try:
appointment_request = AppointmentRequest.objects.create(
date=instance.scheduled_at.date(),
start_time=instance.scheduled_at.time(),
end_time=(instance.scheduled_at + instance.duration).time(),
service=service,
staff_member=request.user.staffmember,
)
except ValidationError as e:
messages.error(request, str(e))
return redirect("schedule_lead", slug=lead.slug)
client = get_object_or_404(User, email=lead.email)
# Create Appointment
Appointment.objects.create(
client=client,
appointment_request=appointment_request,
phone=lead.phone_number,
address=lead.address,
)
instance.save()
messages.success(
request, _("Lead scheduled and appointment created successfully")
)
return redirect("lead_list")
else:
messages.error(request, f"Invalid form data: {str(form.errors)}")
return redirect("lead_list")
form = forms.ScheduleForm()
return render(request, "crm/leads/schedule_lead.html", {"lead": lead, "form": form})
@login_required
@permission_required("inventory.change_lead", raise_exception=True)
def lead_transfer(request, 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.
"""
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()
messages.success(request, _("Lead transferred successfully"))
else:
messages.error(request, f"Invalid form data: {str(form.errors)}")
return redirect("lead_list")
@login_required
def send_lead_email(request, 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
"""
lead = get_object_or_404(models.Lead, slug=slug)
status = request.GET.get("status")
dealer = get_user_type(request)
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"))
response = HttpResponse(redirect("lead_detail", slug=lead.slug))
response["HX-Redirect"] = reverse("lead_detail", args=[lead.slug])
return response
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"),
)
dealer = get_user_type(request)
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"))
return redirect("lead_list")
msg = f"""
السلام عليكم
Dear {lead.full_name},
أود أن أشارككم تقدير المشروع الذي ناقشناه. يرجى العثور على الوثيقة التفصيلية للمقترح المرفقة.
I hope this email finds you well. I wanted to share with you the estimate for the project we discussed. Please find the detailed estimate document attached.
يرجى مراجعة المقترح وإعلامي إذا كانت لديك أي أسئلة أو مخاوف. إذا كانت كل شيء يبدو جيدًا، يمكننا المضي قدمًا في المشروع.
Please review the estimate and let me know if you have any questions or concerns. If everything looks good, we can proceed with the project.
شكراً لاهتمامكم بهذا الأمر.
Thank you for your attention to this matter.
تحياتي,
Best regards,
[Your Name]
[Your Position]
[Your Company]
[Your Contact Information]
"""
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},
)
@login_required
def add_activity_to_lead(request, pk):
"""
Handles the process of adding a new activity to a specific lead. This includes
rendering a form for user input, validating the form submission, and saving the
new activity if the input is valid. If the method is GET, it will simply
render the form. If the method is POST, it checks the form validity, creates
the activity, associates it with the lead, and saves it to the database.
:param request: The HTTP request object containing metadata about the request made
:param pk: The primary key of the lead to which the activity is to be added
:return: An HTTP response that either renders the form or redirects to the lead detail page
"""
lead = get_object_or_404(models.Lead, pk=pk)
dealer = get_user_type(request)
if request.method == "POST":
form = forms.ActivityForm(request.POST)
if form.is_valid():
activity = form.save(commit=False)
print(activity)
activity.content_object = lead
activity.dealer = dealer
activity.activity_type = form.cleaned_data["activity_type"]
activity.notes = form.cleaned_data["notes"]
activity.created_by = request.user
activity.save()
return redirect("lead_detail", pk=pk)
else:
form = forms.ActivityForm()
return render(request, "crm/add_activity.html", {"form": form, "lead": lead})
class OpportunityCreateView(CreateView, SuccessMessageMixin, LoginRequiredMixin):
"""
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."
def get_initial(self):
initial = super().get_initial()
dealer = get_user_type(self.request)
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_user_type(self.request)
instance = form.save(commit=False)
if self.kwargs.get("slug"):
lead = models.Lead.objects.get(slug=self.kwargs.get("slug"), dealer=dealer)
instance.dealer = dealer
instance.staff = lead.staff
instance.lead = lead
lead.convert_to_customer()
lead.save()
instance.save()
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("opportunity_detail", kwargs={"slug": self.object.slug})
class OpportunityUpdateView(LoginRequiredMixin, 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."
def get_success_url(self):
return reverse_lazy("opportunity_detail", kwargs={"slug": self.object.slug})
class OpportunityDetailView(LoginRequiredMixin, 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"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
form = forms.OpportunityStatusForm()
url = reverse("opportunity_update_status", args=[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["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.id
)
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.id
)
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["lead_tasks"] = models.Tasks.objects.filter(
content_type__model="lead", object_id=self.object.id
)
context["tasks"] = models.Tasks.objects.filter(
content_type__model="opportunity", object_id=self.object.id
)
context["upcoming_events"] = {
"schedules": self.object.lead.get_all_schedules().filter(
scheduled_at__gt=timezone.now()
),
}
return context
class OpportunityListView(LoginRequiredMixin, ListView):
model = models.Opportunity
template_name = "crm/opportunities/opportunity_list.html"
context_object_name = "opportunities"
paginate_by = 30
def get_queryset(self):
dealer = get_user_type(self.request)
queryset = models.Opportunity.objects.filter(dealer=dealer)
# Search filter
search = self.request.GET.get("search")
if search:
queryset = queryset.filter(
Q(customer__customer_name__icontains=search)
| Q(customer__email__icontains=search)
)
# Stage filter
stage = self.request.GET.get("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("closing_date")
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["stage_choices"] = models.Stage.choices
return context
def get_template_names(self):
return self.template_name
@login_required
def delete_opportunity(request, 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
"""
opportunity = get_object_or_404(models.Opportunity, pk=pk)
opportunity.delete()
messages.success(request, _("Opportunity deleted successfully"))
return redirect("opportunity_list")
@login_required
def opportunity_update_status(request, 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", 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)
# @login_required
# def mark_notification_as_read(request, pk):
# """
# Marks a user notification as read.
# This view allows an authenticated user to mark a specific notification,
# identified by its primary key, as read. Upon successfully marking the
# notification, a success message is displayed, and the user is redirected
# to their notification history page.
# :param request: The HTTP request object.
# :type request: HttpRequest
# :param pk: Primary key of the notification to be marked as read.
# :type pk: int
# :return: An HTTP response redirecting to the notification history page.
# :rtype: HttpResponse
# """
# notification = get_object_or_404(models.Notification, pk=pk, user=request.user)
# notification.is_read = True
# notification.save()
# messages.success(request, _("Notification marked as read"))
# return redirect("notifications_history")
# @login_required
# def fetch_notifications(request):
# """
# Fetches unread notifications for the currently logged-in user and renders them
# to the `notifications.html` template. The notifications are filtered to include
# only those belonging to the logged-in user and are sorted by creation date in
# descending order.
# :param request: The HTTP request object representing the current user request.
# Must include details of the logged-in user.
# :return: An HttpResponse object that renders the `notifications.html` template
# with the fetched notifications.
# """
# notifications = models.Notification.objects.filter(
# user=request.user, is_read=False
# ).order_by("-created")
# return render(request, "notifications.html", {"notifications_": notifications})
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_url = reverse_lazy("item_service_list")
success_message = _("Service created successfully")
context_object_name = "service"
permission_required = ["django_ledger.add_itemmodel"]
def form_valid(self, form):
vat = models.VatRate.objects.get(is_active=True)
dealer = get_user_type(self.request)
form.instance.dealer = dealer
if form.instance.taxable:
form.instance.price = (form.instance.price * vat.rate) + form.instance.price
return super().form_valid(form)
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_url = reverse_lazy("item_service_list")
success_message = _("Service updated successfully")
context_object_name = "service"
permission_required = ["django_ledger.change_itemmodel"]
def form_valid(self, form):
vat = models.VatRate.objects.get(is_active=True)
dealer = get_user_type(self.request)
form.instance.dealer = dealer
if form.instance.taxable:
form.instance.price = (form.instance.price * vat.rate) + form.instance.price
return super().form_valid(form)
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 = ["django_ledger.view_itemmodel"]
def get_queryset(self):
dealer = get_user_type(self.request)
return models.AdditionalServices.objects.filter(dealer=dealer).all()
class ItemExpenseCreateView(LoginRequiredMixin, PermissionRequiredMixin, 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")
permission_required = ["django_ledger.add_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)
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)
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 = 30
permission_required = ["django_ledger.view_itemmodel"]
def get_queryset(self):
dealer = get_user_type(self.request)
return dealer.entity.get_items_expenses()
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()
return qs
class BillDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""
Handles the functionality for viewing detailed information about a single bill.
This class allows logged-in users with the required permissions to view specific bills
along with their attributes and calculated data provided in the context, such as a list
of detailed item transactions and a computed grand total.
:ivar model: The model associated with the view, which in this case represents bills.
:type model: BillModel
:ivar template_name: The template used for rendering the bill detail view.
:type template_name: str
:ivar context_object_name: The name of the context object passed to the template.
:type context_object_name: str
:ivar permission_required: A list of permissions required to access this view.
:type permission_required: list[str]
"""
model = BillModel
template_name = "ledger/bills/bill_detail.html"
context_object_name = "bill"
permission_required = ["django_ledger.view_billmodel"]
def get_context_data(self, **kwargs):
bill = kwargs.get("object")
if bill.get_itemtxs_data():
txs = bill.get_itemtxs_data()[0]
transactions = [
{
"item": x,
"total": Decimal(x.unit_cost) * Decimal(x.quantity),
}
for x in txs
]
grand_total = sum(Decimal(x.unit_cost) * Decimal(x.quantity) for x in txs)
kwargs["transactions"] = transactions
kwargs["grand_total"] = grand_total
return super().get_context_data(**kwargs)
class InReviewBillView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
"""
Handles the in-review update functionality for the BillModel.
This view is responsible for managing the update operations of bills that are in the
"in-review" state. It ensures all updates are performed according to the associated
permissions, and checks the user's type for processing additional context and logic.
The view also enforces that only authorized users with the required permission can
interact with it.
:ivar model: The model being used for this view, representing the BillModel.
:type model: BillModel
:ivar form_class: The form class used to handle the update of bill data.
:type form_class: InReviewBillModelUpdateForm
:ivar template_name: The template used to render the bill update form.
:type template_name: str
:ivar success_url: The URL to redirect to upon successful bill update.
:type success_url: str
:ivar success_message: A message displayed upon successfully updating the bill.
:type success_message: str
:ivar context_object_name: The context variable name for the view's object.
:type context_object_name: str
:ivar permission_required: The list of permissions required to use this view.
:type permission_required: list
"""
model = BillModel
form_class = InReviewBillModelUpdateForm
template_name = "ledger/bills/bill_update_form.html"
success_url = reverse_lazy("bill_list")
success_message = _("Bill updated successfully")
context_object_name = "bill"
permission_required = ["django_ledger.change_billmodel"]
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
dealer = get_user_type(self.request)
kwargs["entity_model"] = dealer.entity
kwargs["user_model"] = dealer.entity.admin
return kwargs
def get_success_url(self):
return reverse_lazy("bill_detail", kwargs={"pk": self.kwargs["pk"]})
def form_valid(self, form):
dealer = get_user_type(self.request)
form.instance.entity = dealer.entity
self.object.mark_as_review()
return super().form_valid(form)
class ApprovedBillModelView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
"""
Represents a view to update and approve a bill.
This class provides the functionality to handle the update and
approval process of a bill within the system. It ensures only
authorized users can access and manage the approval. The view
uses specific permissions and form classes to manage this
process. Additionally, it provides feedback upon successful
completion of the operation, such as updating or approving the
bill.
:ivar model: The Django model associated with this view.
:type model: BillModel
:ivar form_class: The form class to use for handling bill updates.
:type form_class: ApprovedBillModelUpdateForm
:ivar template_name: The path to the HTML template for this view.
:type template_name: str
:ivar success_url: The redirect destination upon successful form submission.
:type success_url: str
:ivar success_message: The success message to display upon successful form submission.
:type success_message: str
:ivar context_object_name: The name of the context object.
:type context_object_name: str
:ivar permission_required: A list of permissions required to access this view.
:type permission_required: list
"""
model = BillModel
form_class = ApprovedBillModelUpdateForm
template_name = "ledger/bills/bill_update_form.html"
success_url = reverse_lazy("bill_list")
success_message = _("Bill updated successfully")
context_object_name = "bill"
permission_required = ["django_ledger.change_billmodel"]
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
dealer = get_user_type(self.request)
kwargs["entity_model"] = dealer.entity
kwargs["user_model"] = dealer.entity.admin
return kwargs
def get_success_url(self):
return reverse_lazy("bill_detail", kwargs={"pk": self.kwargs["pk"]})
def form_valid(self, form):
dealer = get_user_type(self.request)
form.instance.entity = dealer.entity
if not self.object.is_approved():
self.object.mark_as_approved(user_model=dealer.entity.admin)
return super().form_valid(form)
@login_required
@permission_required("django_ledger.change_billmodel", raise_exception=True)
def bill_mark_as_approved(request, pk):
"""
Marks a bill as approved for the given bill ID (primary key) if it is not
already approved. This action can only be completed by an authorized user
with the corresponding permissions. Once the bill is approved, it is saved
and a success message is displayed. If the bill is already approved, an
error message is shown instead.
:param request: HttpRequest object representing the current request.
:param pk: Primary key of the bill to be marked as approved.
:return: HttpResponseRedirect to the bill detail page after the operation is
completed or an error/success message is set.
"""
bill = get_object_or_404(BillModel, pk=pk)
if request.method == "POST":
dealer = get_user_type(request)
if bill.is_approved():
messages.error(request, _("Bill is already approved"))
return redirect("bill_detail", pk=bill.pk)
bill.mark_as_approved(user_model=dealer.entity.admin)
bill.save()
messages.success(request, _("Bill marked as approved successfully"))
return redirect("bill_detail", pk=bill.pk)
@login_required
@permission_required("django_ledger.change_billmodel", raise_exception=True)
def bill_mark_as_paid(request, pk):
"""
Marks a bill as paid if certain conditions are met and updates the ledger accordingly.
This function is used to mark a specific bill as paid. It verifies whether the bill
is already paid or if the amount paid matches the amount due. If the conditions are
met, it updates the bill's status, locks the journal entries in the associated ledger,
posts them, and saves the changes. Appropriate success or error messages are displayed
to the user, and the user is redirected to the bill details page.
:param request: The HTTP request object containing details about the request made by the user.
:type request: HttpRequest
:param pk: The primary key of the bill to be marked as paid.
:type pk: int
:return: A redirect response to the bill details page.
:rtype: HttpResponseRedirect
"""
bill = get_object_or_404(BillModel, pk=pk)
if request.method == "POST":
dealer = get_user_type(request)
if bill.is_paid():
messages.error(request, _("Bill is already paid"))
return redirect("bill_detail", pk=bill.pk)
if bill.amount_due == bill.amount_paid:
bill.mark_as_paid(user_model=dealer.entity.admin)
bill.save()
bill.ledger.lock_journal_entries()
bill.ledger.post_journal_entries()
bill.ledger.post()
bill.ledger.save()
messages.success(request, _("Bill marked as paid successfully"))
else:
messages.error(request, _("Amount paid is not equal to amount due"))
return redirect("bill_detail", pk=bill.pk)
@login_required
@permission_required("django_ledger.add_billmodel", raise_exception=True)
def bill_create(request):
"""
Handles creation of a bill in the system, including the validation of input data,
creation of transactions associated with the bill, and rendering of the appropriate
response or form. Ensures the user creating the bill has the necessary permissions and
correct input parameters for successful bill creation and itemization.
:param request: Django HttpRequest object containing metadata and data of the HTTP request.
:type request: HttpRequest
:return: JsonResponse with success/error information if the request is processed,
or HttpResponse rendering the form for bill creation.
:rtype: JsonResponse or HttpResponse
"""
dealer = get_user_type(request)
entity = dealer.entity
if request.method == "POST":
data = json.loads(request.body)
vendor_id = data.get("vendor")
terms = data.get("terms")
vendor = entity.get_vendors().filter(pk=vendor_id).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"),
}
)
bill = entity.create_bill(vendor_model=vendor, terms=terms)
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:
item_instance = ItemModel.objects.get(pk=item.get("item_id"))
car = models.Car.objects.get(vin=item_instance.name)
quantity = Decimal(item.get("quantity"))
items_txs.append(
{
"item_number": item_instance.item_number,
"quantity": quantity,
"unit_cost": car.finances.cost_price,
"total_amount": car.finances.cost_price * quantity,
}
)
bill_itemtxs = {
item.get("item_number"): {
"unit_cost": item.get("unit_cost"),
"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)
bill_itemtxs = {
item.item_number: {
"unit_cost": instance.finances.cost_price,
"quantity": Decimal(quantities),
"total_amount": instance.finances.cost_price * Decimal(quantities),
}
}
bill_itemtxs = bill.migrate_itemtxs(
itemtxs=bill_itemtxs,
commit=True,
operation=BillModel.ITEMIZE_APPEND,
)
url = reverse("bill_detail", kwargs={"pk": bill.pk})
return JsonResponse(
{
"status": "success",
"message": _("Bill created successfully"),
"url": f"{url}",
}
)
form = forms.BillModelCreateForm(entity_model=entity)
form.initial.update(
{
"cash_account": dealer.settings.bill_cash_account,
"prepaid_account": dealer.settings.bill_prepaid_account,
"unearned_account": dealer.settings.bill_unearned_account,
}
)
car_list = models.Car.objects.filter(dealer=dealer)
context = {
"form": form,
"items": [
{
"car": x,
"product": entity.get_items_products().filter(name=x.vin).first(),
}
for x in car_list
],
}
return render(request, "ledger/bills/bill_form.html", context)
@login_required
@permission_required("django_ledger.delete_billmodel", raise_exception=True)
def BillDeleteView(request, pk):
"""
Deletes a specific BillModel instance based on the primary key (pk).
This view requires the user to be logged in and have the appropriate
permission to delete a bill. After the delete operation is successful,
the user is redirected to the bill list view.
:param request: The HTTP request object.
:type request: HttpRequest
:param pk: Primary key of the BillModel instance to be deleted.
:type pk: int
:return: Redirect to the bill list view after successful deletion.
:rtype: HttpResponseRedirect
"""
bill = get_object_or_404(BillModel, pk=pk)
bill.delete()
return redirect("bill_list")
# orders
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()
return qs.filter(estimate__entity=dealer.entity)
# email
@login_required
@permission_required("django_ledger.view_estimatemodel", raise_exception=True)
def send_email_view(request, 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_user_type(request)
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", pk=estimate.pk)
link = request.build_absolute_uri(
reverse_lazy("estimate_preview", kwargs={"pk": estimate.pk})
)
msg = f"""
السلام عليكم
Dear {estimate.customer.customer_name},
أود أن أشارككم عرض السعر.
I wanted to share with you the quotation.
يرجى مراجعة عرض السعر وإعلامي إذا كانت لديك أي استفسارات أو ملاحظات. إذا كان كل شيء على ما يرام، يمكننا المتابعة في الإجراءات.
Please review the quotation and let me know if you have any questions or concerns. If everything looks good, we can proceed with the process.
رابط عرض السعر:
{link}
تحياتي,
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", 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
return reverse(
"entity-bs-year",
kwargs={"entity_slug": self.kwargs["entity_slug"], "year": year},
)
def get_login_url(self):
return reverse("account_login")
class FiscalYearBalanceSheetViewBase(
FiscalYearBalanceSheetView, DjangoLedgerSecurityMixIn
):
"""
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"
# AUTHORIZE_SUPERUSER = False
# permission_required = []
# def get_authorized_entity_queryset(self):
# dealer = get_user_type(self.request)
# return EntityModel.objects.filter(admin=dealer)
def get_login_url(self):
return reverse("account_login")
class QuarterlyBalanceSheetView(
FiscalYearBalanceSheetViewBase, QuarterlyReportMixIn, DjangoLedgerSecurityMixIn
):
"""
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, DjangoLedgerSecurityMixIn
):
"""
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, DjangoLedgerSecurityMixIn
):
"""
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(
BaseIncomeStatementRedirectView, DjangoLedgerSecurityMixIn
):
"""
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_user_type(self.request)
return reverse(
"entity-ic-year", kwargs={"entity_slug": dealer.entity.slug, "year": year}
)
def get_login_url(self):
return reverse("account_login")
class FiscalYearIncomeStatementViewBase(
FiscalYearIncomeStatementView, DjangoLedgerSecurityMixIn
):
"""
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 = ["inventory.view_carfinance"]
def get_login_url(self):
return reverse("account_login")
class QuarterlyIncomeStatementView(
FiscalYearIncomeStatementViewBase, QuarterlyReportMixIn, DjangoLedgerSecurityMixIn
):
"""
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, DjangoLedgerSecurityMixIn
):
"""
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, DjangoLedgerSecurityMixIn
):
"""
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(
BaseCashFlowStatementRedirectView, DjangoLedgerSecurityMixIn
):
"""
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_user_type(self.request)
return reverse(
"entity-cf-year", kwargs={"entity_slug": dealer.entity.slug, "year": year}
)
def get_login_url(self):
return reverse("account_login")
class FiscalYearCashFlowStatementViewBase(
FiscalYearCashFlowStatementView, DjangoLedgerSecurityMixIn
):
"""
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 = ["inventory.view_carfinance"]
def get_login_url(self):
return reverse("account_login")
class QuarterlyCashFlowStatementView(
FiscalYearCashFlowStatementViewBase, QuarterlyReportMixIn, DjangoLedgerSecurityMixIn
):
"""
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, DjangoLedgerSecurityMixIn
):
"""
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, DjangoLedgerSecurityMixIn
):
"""
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, DjangoLedgerSecurityMixIn
):
"""
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, DjangoLedgerSecurityMixIn
):
"""
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, DjangoLedgerSecurityMixIn
):
"""
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 = ["inventory.view_carfinance"]
def get_login_url(self):
return reverse("account_login")
class QuarterlyEntityDashboardView(
FiscalYearEntityModelDashboardView, QuarterlyReportMixIn, DjangoLedgerSecurityMixIn
):
"""
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, DjangoLedgerSecurityMixIn
):
"""
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, DjangoLedgerSecurityMixIn
):
"""
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(DjangoLedgerSecurityMixIn, 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, 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
"""
schedule = get_object_or_404(models.Schedule, pk=pk)
schedule.status = "Canceled"
schedule.save()
response = HttpResponse()
response.status_code = 200
return response
@login_required
def assign_car_makes(request):
"""
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_user_type(request)
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, 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"
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()
dealer = get_user_type(self.request)
qs = qs.filter(entity=dealer.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)
dealer = get_user_type(self.request)
context["entity_slug"] = dealer.entity.slug
return context
# class LedgerModelDetailView(InvoiceModelDetailViewBase):
class LedgerModelDetailView(LoginRequiredMixin, 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"
class LedgerModelCreateView(LedgerModelCreateViewBase):
"""
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"
def get_form(self, form_class=None):
dealer = get_user_type(self.request)
return LedgerModelCreateForm(
entity_slug=dealer.entity.slug,
user_model=dealer.entity.admin,
**self.get_form_kwargs(),
)
def form_valid(self, form):
dealer = get_user_type(self.request)
instance = form.save(commit=False)
instance.entity = dealer.entity
return super().form_valid(form)
def get_success_url(self):
return reverse("ledger_list")
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")
class LedgerModelDeleteView(LedgerModelDeleteViewBase, 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"
success_message = "Ledger deleted"
def get_success_url(self):
return reverse("ledger_list")
# class LedgerModelCreateView(LoginRequiredMixin,SuccessMessageMixin, CreateView):
# model = LedgerModel
# template_name = "ledger/ledger/ledger_form.html"
# form_class = forms.LedgerModelCreateForm
# success_message = "Ledger created"
# def get_form(self, form_class=None):
# dealer = get_user_type(self.request)
# form = forms.LedgerModelCreateForm(entity_slug=dealer.entity.slug,user_model=dealer.entity.admin,**self.get_form_kwargs())
# return form
# def get_success_url(self):
# return reverse('ledger_list')
# def form_valid(self, form):
# instance = form.save(commit=False)
# dealer = get_user_type(self.request)
# instance.entity = dealer.entity
# instance.save()
# return super().form_valid(form)
class JournalEntryListView(LoginRequiredMixin, 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"
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, 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"
form_class = forms.JournalEntryModelCreateForm
ledger_model = None
success_message = _("Journal Entry created")
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={"pk": ledger.pk})
def JournalEntryDeleteView(request, 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", pk=ledger.pk)
journal_entry.delete()
messages.success(request, "Journal Entry deleted")
return redirect("journalentry_list", pk=ledger.pk)
return render(
request,
"ledger/journal_entry/journal_entry_delete.html",
{"journal_entry": journal_entry},
)
def JournalEntryTransactionsView(request, 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
"""
journal = JournalEntryModel.objects.filter(pk=pk).first()
transactions = (
TransactionModel.objects.filter(journal_entry=journal)
.order_by("account__code")
.all()
)
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"
def ledger_lock_all_journals(request, 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
"""
ledger = LedgerModel.objects.filter(pk=pk).first()
if ledger.is_locked():
messages.error(request, _("Ledger is already locked"))
return redirect("journalentry_list", pk=ledger.pk)
ledger.lock_journal_entries()
ledger.lock()
ledger.save()
return redirect("journalentry_list", pk=ledger.pk)
def ledger_unlock_all_journals(request, 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
"""
ledger = LedgerModel.objects.filter(pk=pk).first()
if not ledger.is_locked():
messages.error(request, _("Ledger is already Unlocked"))
return redirect("journalentry_list", pk=ledger.pk)
ledger.unlock()
ledger.save()
qs = ledger.journal_entries.locked()
for je in qs:
je.unlock()
je.save()
return redirect("journalentry_list", pk=ledger.pk)
def ledger_post_all_journals(request, 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
"""
ledger = LedgerModel.objects.filter(pk=pk).first()
if ledger.is_posted():
messages.error(request, _("Ledger is already posted"))
return redirect("journalentry_list", pk=ledger.pk)
ledger.post_journal_entries()
ledger.post()
ledger.save()
return redirect("journalentry_list", pk=ledger.pk)
def ledger_unpost_all_journals(request, 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
"""
ledger = LedgerModel.objects.filter(pk=pk).first()
if not ledger.is_posted():
messages.error(request, _("Ledger is already Unposted"))
return redirect("journalentry_list", 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", pk=ledger.pk)
def pricing_page(request):
plan_list = PlanPricing.objects.all()
form = forms.PaymentPlanForm()
return render(request, "pricing_page.html", {"plan_list": plan_list, "form": form})
# @require_POST
def submit_plan(request):
dealer = get_user_type(request)
selected_plan_id = request.POST.get("selected_plan")
pp = PlanPricing.objects.get(pk=selected_plan_id)
order = Order.objects.create(
user=dealer.user,
plan=pp.plan,
pricing=pp.pricing,
amount=pp.price,
currency=settings.DEFAULT_CURRENCY,
tax=15,
status=AbstractOrder.STATUS.NEW,
)
transaction_url = handle_payment(request, order)
return redirect(transaction_url)
def payment_callback(request):
message = request.GET.get("message")
dealer = get_user_type(request)
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=AbstractOrder.STATUS.NEW
).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})
# Background Tasks
def task_list(request):
# Get all tasks ordered by creation time
tasks = Task.objects.all()
# Add pagination
paginator = Paginator(tasks, 10) # Show 10 tasks per page
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
return render(request, "tasks/task_list.html", {"page_obj": page_obj})
def sse_stream(request):
def event_stream():
last_id = request.GET.get("last_id", 0)
while True:
# Check for new notifications
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(2)
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.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(
read=True
)
return JsonResponse({"status": "success"})
@login_required
def notifications_history(request):
models.Notification.objects.filter(user=request.user, is_read=False).update(
read=True
)
return JsonResponse({"status": "success"})
# def activity_create(request,pk):
# lead = get_object_or_404(models.Lead, pk=pk)
# form = forms.ActivityHistoryForm()
# dealer = get_user_type(request)
# if request.method == "POST":
# form = forms.ActivityHistoryForm(request.POST)
# if form.is_valid():
# models.Activity.objects.create(
# dealer=dealer,
# activity_type=form.cleaned_data['activity_type'],
# notes=form.cleaned_data['description'],
# created_by=request.user,
# content_object=lead
# )
# return render(request, 'activity_history.html')
def add_activity(request, content_type, slug):
try:
model = apps.get_model(f"inventory.{content_type}")
except LookupError:
raise Http404("Model not found")
obj = get_object_or_404(model, slug=slug)
dealer = get_user_type(request)
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()
messages.success(request, _("Activity added successfully"))
else:
messages.error(request, _("Activity form is not valid"))
return redirect(f"{content_type}_detail", slug=slug)
def add_task(request, content_type, slug):
try:
model = apps.get_model(f"inventory.{content_type}")
except LookupError:
raise Http404("Model not found")
obj = get_object_or_404(model, slug=slug)
dealer = get_user_type(request)
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()
messages.success(request, _("Task added successfully"))
else:
print(form.errors)
messages.error(request, _("Task form is not valid"))
return redirect(f"{content_type}_detail", slug=slug)
def update_task(request,content_type, pk):
try:
model = apps.get_model(f"inventory.{content_type}")
except LookupError:
raise Http404("Model not found")
task = get_object_or_404(models.Tasks, pk=pk)
obj = get_object_or_404(model, pk=task.content_object.id)
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})
def add_note(request, content_type, slug):
try:
model = apps.get_model(f"inventory.{content_type}")
except LookupError:
raise Http404("Model not found")
obj = get_object_or_404(model, slug=slug)
dealer = get_user_type(request)
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()
messages.success(request, _("Note added successfully"))
else:
print(form.errors)
messages.error(request, _("Note form is not valid"))
return redirect(f"{content_type}_detail", slug=slug)
def update_note(request, pk):
note = get_object_or_404(models.Notes, pk=pk)
lead = get_object_or_404(models.Lead, pk=note.content_object.id)
dealer = get_user_type(request)
if request.method == "POST":
note.note = request.POST.get("note")
note.save()
messages.success(request, _("Note updated successfully"))
return redirect("lead_detail", slug=lead.slug)
else:
messages.error(request, _("Note form is not valid"))
notes = models.Notes.objects.filter(
content_type__model="lead", object_id=lead.id, dealer=dealer
)
return render(request, "crm/leads/lead_detail.html", {"lead": lead, "notes": notes})
# Admin Management
def management_view(request):
return render(request, "admin_management/management.html")
def user_management(request):
context = {
"customers": models.Customer.objects.filter(active=False),
"organizations": models.Organization.objects.filter(active=False),
"vendors": models.Vendor.objects.filter(active=False),
"staff": models.Staff.objects.filter(active=False),
}
return render(request, "admin_management/user_management.html", context)
def activate_account(request, content_type, slug):
try:
model = apps.get_model(f"inventory.{content_type}")
except LookupError:
raise Http404("Model not found")
obj = get_object_or_404(model, slug=slug)
if request.method == "POST":
obj.activate_account()
messages.success(request, _("Account activated successfully"))
return redirect("user_management")
return render(
request, "admin_management/confirm_activate_account.html", {"obj": obj}
)
def permenant_delete_account(request, content_type, slug):
try:
model = apps.get_model(f"inventory.{content_type}")
except LookupError:
raise Http404("Model not found")
obj = get_object_or_404(model, slug=slug)
if request.method == "POST":
try:
obj.permenant_delete()
messages.success(request, _("Account Deleted successfully"))
except RestrictedError:
messages.error(
request,
_("You cannot delete this account,it is related to another account"),
)
except Exception as e:
messages.error(request, _(f"Error deleting account: {e}"))
return redirect("user_management")
return render(
request, "admin_management/permenant_delete_account.html", {"obj": obj}
)