haikal/inventory/views.py
Marwan Alwali 9c55bdbe92 update
2025-03-04 00:03:29 +03:00

3929 lines
143 KiB
Python

# Standard
import cv2
import json
import logging
import numpy as np
from rich import print
from random import randint
from decimal import Decimal
from calendar import month_name
from pyzbar.pyzbar import decode
from urllib.parse import urlparse, urlunparse
#####################################################################
from django.db.models.deletion import RestrictedError
# Django
from django.db.models import Q
from django.conf import settings
from django.db import transaction
from django.db.models import Func
from django.contrib import messages
from django.http import JsonResponse
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 Count, F, 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.core.files.storage import default_storage
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.shortcuts import render, get_object_or_404, redirect
from django.views.generic import (
View,
ListView,
DetailView,
CreateView,
UpdateView,
DeleteView,
TemplateView,
)
#####################################################################
# Django Ledger
from django_ledger.io import roles
from django_ledger.utils import accruable_net_summary
from django_ledger.forms.account import AccountModelCreateForm, AccountModelUpdateForm
from django_ledger.views.entity import EntityModelDetailBaseView,EntityModelDetailHandlerView
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,
VendorModel,
)
from django_ledger.views.financial_statement import (
FiscalYearBalanceSheetView,
BaseIncomeStatementRedirectView,
FiscalYearIncomeStatementView,
BaseCashFlowStatementRedirectView,
FiscalYearCashFlowStatementView,
)
from django_ledger.io.io_core import get_localdate
from django_ledger.models import EntityModel
from django_ledger.views.mixins import (
QuarterlyReportMixIn,
MonthlyReportMixIn,
DateReportMixIn,
DjangoLedgerSecurityMixIn,
EntityUnitMixIn,
)
#####################################################################
# Other
from plans.models import Plan
from . import models, forms, tables
from plans.quota import get_user_quota
from django_tables2 import SingleTableView
from django_tables2.export.views import ExportMixin
from appointment.models import Appointment,AppointmentRequest,Service,StaffMember
from .services import (
decodevin,
get_make,
get_model,
)
from .utils import (
CarFinanceCalculator,
get_car_finance_data,
get_financial_values,
get_item_transactions,
reserve_car,
send_email,
get_user_type,
set_bill_payment,
set_invoice_payment,
CarTransfer,
)
#####################################################################
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
class Hash(Func):
function = 'get_hash'
def switch_language(request):
language = request.GET.get("language", "en")
referer = request.META.get("HTTP_REFERER", "/")
parsed_url = urlparse(referer)
path_parts = parsed_url.path.split("/")
if path_parts[1] in dict(settings.LANGUAGES):
path_parts.pop(1)
new_path = "/".join(path_parts)
new_url = urlunparse(
(
parsed_url.scheme,
parsed_url.netloc,
new_path,
parsed_url.params,
parsed_url.query,
parsed_url.fragment,
)
)
if language in dict(settings.LANGUAGES):
logger.debug(f"Switching language to: {language}")
response = redirect(new_url)
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
translation.activate(language)
request.session[settings.LANGUAGE_COOKIE_NAME] = language
logger.debug(
f"Language switched to: {language}, Session: {request.session[settings.LANGUAGE_COOKIE_NAME]}"
)
return response
else:
logger.warning(f"Invalid language code: {language}")
return redirect("/")
def dealer_signup(request, *args, **kwargs):
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:
with transaction.atomic():
user = User.objects.create(username=email, email=email)
user.set_password(password)
user.save()
StaffMember.objects.create(user=user)
models.Dealer.objects.create(
user=user,
name=name,
arabic_name=arabic_name,
crn=crn,
vrn=vrn,
phone_number=phone,
address=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 OTPView(View, LoginRequiredMixin):
# template_name = "account/otp_verification.html"
#
# def get(self, request, *args, **kwargs):
# # device = default_device(request.user)
# # device.generate_challenge()
# return render(request, self.template_name)
#
# def post(self, request, *args, **kwargs):
# otp_code = request.POST.get("otp_code")
#
# if self.verify_otp(otp_code, request.user):
# messages.success(request, _("OTP verified successfully!"))
# return redirect("home")
#
# messages.error(request, _("Invalid OTP. Please try again."))
# return render(request, self.template_name)
# def verify_otp(self, otp_code, user):
# device = default_device(user)
# if device and device.verify_token(otp_code):
# return True
# return False
class HomeView(TemplateView):
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)
# def get_context_data(self, **kwargs):
# context = super().get_context_data(**kwargs)
# dealer = get_user_type(self.request)
#
# try:
# # Fetch car-related statistics
# total_cars = models.Car.objects.filter(dealer=dealer).count()
# total_reservations = models.CarReservation.objects.filter(
# reserved_until__gte=timezone.now()
# ).count()
# cars_in_house = models.CarLocation.objects.filter(
# owner=dealer,
# ).count()
# cars_outside = total_cars - cars_in_house
#
# # Fetch financial statistics
# stats = models.CarFinance.objects.aggregate(
# total_cost_price=Sum("cost_price"),
# total_selling_price=Sum("selling_price"),
# )
# total_cost_price = stats.get("total_cost_price", 0) or 0
# total_selling_price = stats.get("total_selling_price", 0) or 0
# total_profit = total_selling_price - total_cost_price
#
# # Prepare context data
# context.update({
# "dealer": dealer,
# "total_cars": total_cars,
# "cars_in_house": cars_in_house,
# "cars_outside": cars_outside,
# "total_reservations": total_reservations,
# "total_cost_price": total_cost_price,
# "total_selling_price": total_selling_price,
# "total_profit": total_profit,
# })
#
# except Exception as e:
# # Log the error (you can use Django's logging framework)
# print(f"Error fetching data: {e}")
# # Provide default values in case of an error
# context.update({
# "dealer": dealer,
# "total_cars": 0,
# "cars_in_house": 0,
# "cars_outside": 0,
# "total_reservations": 0,
# "total_cost_price": 0,
# "total_selling_price": 0,
# "total_profit": 0,
# })
# return context
class TestView(TemplateView):
template_name = "inventory/cars_list_api.html"
class ManagerDashboard(LoginRequiredMixin, TemplateView):
template_name = "dashboards/manager.html"
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return redirect("welcome")
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_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):
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()
# new_leads = models.Lead.objects.filter(dealer=dealer, status=models.Status.NEW).count()
pending_leads = models.Lead.objects.filter(dealer=dealer, dealer__staff__assigned=staff, 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)
context["dealer"] = dealer
context["staff"] = staff
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
return context
class WelcomeView(TemplateView):
template_name = "welcome.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
dealer = get_user_type(self.request)
plan_list = Plan.objects.all()
# pricing = PlanPricing.objects.filter(plan=plan).
context["plan_list"] = plan_list
return context
class CarCreateView(LoginRequiredMixin, CreateView):
model = models.Car
form_class = forms.CarForm
template_name = "inventory/car_form.html"
# success_url = reverse_lazy('inventory_stats')
def get_form(self, form_class=None):
form = super().get_form(form_class)
dealer = get_user_type(self.request)
form.fields["vendor"].queryset = dealer.entity.get_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 car_history(request,pk):
car = get_object_or_404(models.Car, pk=pk)
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):
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 = ""
# manufacturer_name = model_name = year_model = None
if not (result := decodevin(vin_no)):
return JsonResponse(
{"success": False, "error": "VIN not found in all sources."}, status=404
)
manufacturer_name, model_name, year_model = result.values()
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})
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 as e:
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(View):
template_name = "inventory/scan_vin.html"
def get(self, request, *args, **kwargs):
"""Render the form page."""
return render(request, self.template_name)
def post(self, request, *args, **kwargs):
image_file = request.FILES.get("image")
if image_file:
print("image received!")
image = cv2.imdecode(
np.frombuffer(image_file.read(), np.uint8), cv2.IMREAD_COLOR
)
decoded_objects = decode(image)
if decoded_objects:
print("image decoded!")
print(decoded_objects[0])
code = decoded_objects[0].data.decode("utf-8")
print("code received!")
print(code)
car = get_object_or_404(models.Car, vin=code)
name = car.id_car_make.get_local_name
print(name)
return redirect("car_detail", pk=car.pk)
else:
print("back to else statement")
return JsonResponse({"success": False, "error": "No code detected"})
else:
return JsonResponse({"success": False, "error": "No image provided"})
class CarInventory(LoginRequiredMixin, ListView):
model = models.Car
home_label = _("inventory")
template_name = "inventory/car_inventory.html"
context_object_name = "cars"
paginate_by = 10
ordering = ["receiving_date"]
def get_queryset(self, *args, **kwargs):
query = self.request.GET.get("q")
make_id = self.kwargs["make_id"]
model_id = self.kwargs["model_id"]
trim_id = self.kwargs["trim_id"]
dealer = get_user_type(self.request)
cars = models.Car.objects.filter(
dealer=dealer,
id_car_make=make_id,
id_car_model=model_id,
id_car_trim=trim_id,
).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, CreateView):
model = models.CarColors
form_class = forms.CarColorsForm
template_name = "inventory/add_colors.html"
def form_valid(self, form):
car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
form.instance.car = car
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("car_detail", kwargs={"pk": self.kwargs["car_pk"]})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["car"] = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
return context
class CarListView(LoginRequiredMixin, ListView):
model = models.Car
template_name = "inventory/car_list_view.html"
context_object_name = "cars"
paginate_by = 20
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):
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,
"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,
"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,
"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"],
"make_name": make_data["make_name"],
"total_cars": make_data["total_cars"],
"models": [
{
"model_id": model_data["model_id"],
"model_name": model_data["model_name"],
"total_cars": model_data["total_cars"],
"trims": list(model_data["trims"].values()),
}
for model_data in make_data["models"].values()
],
}
for make_data in inventory.values()
],
}
return render(request, "inventory/inventory_stats.html", {"inventory": result})
class CarDetailView(LoginRequiredMixin, DetailView):
model = models.Car
template_name = "inventory/car_detail.html"
context_object_name = "car"
class CarFinanceCreateView(LoginRequiredMixin, CreateView):
model = models.CarFinance
form_class = forms.CarFinanceForm
template_name = "inventory/car_finance_form.html"
def dispatch(self, request, *args, **kwargs):
self.car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
form.instance.car = self.car
messages.success(self.request, _("Car finance details saved successfully."))
return super().form_valid(form)
def get_success_url(self):
return reverse("car_detail", kwargs={"pk": self.car.pk})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["car"] = self.car
return context
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, SuccessMessageMixin, UpdateView):
model = models.CarFinance
form_class = forms.CarFinanceForm
template_name = "inventory/car_finance_form.html"
success_message = _("Car finance details updated successfully.")
def get_success_url(self):
return reverse("car_detail", kwargs={"pk": self.object.car.pk})
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, SuccessMessageMixin, UpdateView):
model = models.Car
form_class = forms.CarUpdateForm
template_name = "inventory/car_edit.html"
success_message = _("Car updated successfully.")
def get_success_url(self):
return reverse("car_detail", kwargs={"pk": self.object.pk})
class CarDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
model = models.Car
template_name = "inventory/car_confirm_delete.html"
success_url = reverse_lazy("inventory_stats")
def delete(self, request, *args, **kwargs):
messages.success(request, _("Car deleted successfully."))
return super().delete(request, *args, **kwargs)
class CarLocationCreateView(CreateView):
model = models.CarLocation
form_class = forms.CarLocationForm
template_name = "inventory/car_location_form.html"
def get_success_url(self):
return reverse_lazy("car_detail", kwargs={"pk": self.object.car.pk})
def form_valid(self, form):
form.instance.car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
dealer = get_user_type(self.request)
form.instance.owner = dealer
form.save()
messages.success(self.request, "Car saved successfully.")
return super().form_valid(form)
class CarLocationUpdateView(UpdateView):
model = models.CarLocation
form_class = forms.CarLocationForm
template_name = "inventory/car_location_form.html"
def get_success_url(self):
return reverse_lazy("car_detail", kwargs={"pk": self.object.car.pk})
class CarTransferCreateView(CreateView):
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(pk=self.kwargs["pk"])
return form
def get_initial(self):
initial = super().get_initial()
initial["car"] = get_object_or_404(models.Car, pk=self.kwargs["pk"])
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={"pk": self.object.car.pk})
class CarTransferDetailView(LoginRequiredMixin, SuccessMessageMixin, DetailView):
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
def car_transfer_approve(request, car_pk, transfer_pk):
car = get_object_or_404(models.Car, pk=car_pk)
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", pk=car.pk)
transfer.status = "approved"
transfer.save()
url = request.build_absolute_uri(
reverse(
"transfer_preview", kwargs={"car_pk": car.pk, "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", pk=car.pk)
def car_transfer_accept_reject(request, car_pk, transfer_pk):
car = get_object_or_404(models.Car, pk=car_pk)
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")
def CarTransferPreviewView(request, car_pk, transfer_pk):
transfer = get_object_or_404(models.CarTransfer, pk=transfer_pk)
if transfer.to_dealer != get_user_type(request):
return redirect("car_detail", pk=car_pk)
return render(request, "inventory/transfer_preview.html", {"transfer": transfer})
class CustomCardCreateView(LoginRequiredMixin, CreateView):
model = models.CustomCard
form_class = forms.CustomCardForm
template_name = "inventory/add_custom_card.html"
def form_valid(self, form):
car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
form.instance.car = car
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["car"] = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
return context
def get_success_url(self):
messages.success(self.request, _("Custom Card added successfully."))
return reverse_lazy("car_detail", kwargs={"pk": self.kwargs["car_pk"]})
class CarRegistrationCreateView(LoginRequiredMixin, CreateView):
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, pk=self.kwargs["car_pk"])
form.instance.car = car
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["car"] = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
return context
def get_success_url(self):
messages.success(self.request, _("Registration added successfully."))
return reverse_lazy("car_detail", kwargs={"pk": self.kwargs["car_pk"]})
@login_required()
def reserve_car_view(request, car_id):
if request.method == "POST":
car = get_object_or_404(models.Car, pk=car_id)
if car.is_reserved():
messages.error(request, _("This car is already reserved."))
return redirect("car_detail", pk=car.pk)
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):
reservation = get_object_or_404(
models.CarReservation, pk=reservation_id, reserved_by=request.user
)
if request.method == "POST":
action = request.POST.get("action")
if action == "renew":
reservation.reserved_until = timezone.now() + timezone.timedelta(hours=24)
reservation.save()
messages.success(request, _("Reservation renewed successfully."))
return redirect("car_detail", pk=reservation.car.pk)
elif action == "cancel":
car = reservation.car
reservation.delete()
car.status = models.CarStatusChoices.AVAILABLE
car.save()
messages.success(request, _("Reservation canceled successfully."))
return redirect("car_detail", pk=reservation.car.pk)
else:
return JsonResponse(
{"success": False, "message": _("Invalid action.")}, status=400
)
return JsonResponse(
{"success": False, "message": _("Invalid request method.")}, status=400
)
class DealerDetailView(LoginRequiredMixin, DetailView):
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)
# Fetch current staff count from the annotated queryset
staff_count = dealer.staff_count
cars_count = models.Car.objects.filter(dealer=dealer).count()
# Get the quota value dynamically
quota_dict = get_user_quota(dealer.user)
allowed_users = quota_dict.get("Users", None)
allowed_cars = quota_dict.get("Cars", None)
context["car_makes"] = car_makes
context["staff_count"] = staff_count
context["cars_count"] = cars_count
context["allowed_users"] = allowed_users
context["allowed_cars"] = allowed_cars
context["quota_display"] = f"{staff_count}/{allowed_users}" if allowed_users is not None else "N/A"
return context
class DealerUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
model = models.Dealer
form_class = forms.DealerForm
template_name = "dealers/dealer_form.html"
success_url = reverse_lazy("dealer_detail")
success_message = _("Dealer updated successfully.")
def get_success_url(self):
return reverse("dealer_detail", kwargs={"pk": self.object.pk})
class CustomerListView(LoginRequiredMixin, ListView):
model = CustomerModel
home_label = _("customers")
context_object_name = "customers"
paginate_by = 10
template_name = "customers/customer_list.html"
ordering = ["-created"]
def get_queryset(self):
query = self.request.GET.get("q")
dealer = get_user_type(self.request)
customers = dealer.entity.get_customers().filter(additional_info__type="customer")
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, DetailView):
model = CustomerModel
template_name = "customers/view_customer.html"
context_object_name = "customer"
def get_context_data(self, **kwargs):
dealer = get_user_type(self.request)
entity = dealer.entity
context = super().get_context_data(**kwargs)
estimates = entity.get_estimates().filter(customer=self.object)
invoices = entity.get_invoices().filter(customer=self.object)
# txs = entity. transactions(customer=self.object)
total = estimates.count() + invoices.count()
context["estimates"] = estimates
context["invoices"] = invoices
context["total"] = total
return context
def add_note_to_customer(request, customer_id):
customer = get_object_or_404(CustomerModel, pk=customer_id)
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", pk=customer.pk)
else:
form = forms.NoteForm()
return render(request, "customers/note_form.html", {"form": form, "customer": customer})
def add_activity_to_customer(request, pk):
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}
)
def CustomerCreateView(request):
form = forms.CustomerForm()
if request.method == "POST":
form = forms.CustomerForm(request.POST)
dealer = get_user_type(request)
if form.is_valid():
if dealer.entity.get_customers().filter(email=form.cleaned_data["email"]).exists():
messages.error(request, _("Customer with this email already exists."))
else:
# Create customer name
customer_name = (
f"{form.cleaned_data['first_name']} "
f"{form.cleaned_data['last_name']}"
)
customer_dict = { x: request.POST[x] for x in request.POST if x != "csrfmiddlewaretoken"}
# Create customer instance
try:
customer = dealer.entity.create_customer(
commit=False,
customer_model_kwargs={
"customer_name": customer_name,
"address_1": form.cleaned_data["address"],
"phone": form.cleaned_data["phone_number"],
"email": form.cleaned_data["email"],
}
)
# customer.additional_info = {}
customer.additional_info.update({"customer_info": customer_dict})
customer.additional_info.update({"type": "customer"})
customer.save()
messages.success(request, _("Customer created successfully."))
return redirect("customer_list")
except Exception as e:
messages.error(request, _(f"An error occurred: {str(e)}"))
else:
# Form is invalid, show errors
messages.error(request, _("Please correct the errors below."))
return render(request, "customers/customer_form.html", {"form": form})
def CustomerUpdateView(request, pk):
customer = get_object_or_404(CustomerModel, pk=pk)
if request.method == "POST":
# form = forms.CustomerForm(request.POST, instance=customer)
customer_dict = {
x: request.POST[x] for x in request.POST if x != "csrfmiddlewaretoken"
}
dealer = get_user_type(request)
customer_name = (
customer_dict["first_name"]
+ " "
+ customer_dict["last_name"]
)
instance = dealer.entity.get_customers().get(pk=pk)
instance.customer_name = customer_name
instance.address_1 = customer_dict["address"]
instance.phone = customer_dict["phone_number"]
instance.email = customer_dict["email"]
customer_dict["pk"] = str(instance.pk)
instance.additional_info.update({"customer_info": customer_dict})
try:
user = User.objects.filter(pk=int(instance.additional_info['user_info']['id'])).first()
if user:
user.username = customer_dict["email"]
user.email = customer_dict["email"]
user.save()
except Exception as e:
raise Exception(e)
instance.save()
messages.success(request, _("Customer updated successfully."))
return redirect("customer_list")
else:
form = forms.CustomerForm(
initial=customer.additional_info["customer_info"]
if "customer_info" in customer.additional_info
else {}
)
return render(request, "customers/customer_form.html", {"form": form})
@login_required
def delete_customer(request, pk):
customer = get_object_or_404(models.CustomerModel, pk=pk)
user = User.objects.get(email=customer.email)
customer.active = False
user.is_active = False
customer.save()
user.save()
messages.success(request, _("Customer deleted successfully."))
return redirect("customer_list")
class VendorListView(LoginRequiredMixin, ListView):
model = VendorModel
context_object_name = "vendors"
paginate_by = 10
template_name = "vendors/vendors_list.html"
ordering = ["-created"]
def get_queryset(self):
query = self.request.GET.get("q")
dealer = get_user_type(self.request)
vendors = dealer.entity.get_vendors().filter(active=True)
return apply_search_filters(vendors, query)
def vendorDetailView(request, pk):
vendor = get_object_or_404(models.Vendor, pk=pk)
return render(request, template_name="vendors/view_vendor.html", context={"vendor": vendor})
class VendorCreateView(
LoginRequiredMixin,
SuccessMessageMixin,
CreateView,
):
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):
dealer = get_user_type(self.request)
form.instance.dealer = dealer
form.instance.save()
return super().form_valid(form)
class VendorUpdateView(
LoginRequiredMixin,
SuccessMessageMixin,
UpdateView,
):
model = models.Vendor
form_class = forms.VendorForm
template_name = "vendors/vendor_form.html"
success_url = reverse_lazy("vendor_list")
success_message = _("Vendor updated successfully.")
@login_required
def delete_vendor(request, pk):
vendor = get_object_or_404(models.Vendor, pk=pk)
# vendor.active = False
vendor.delete()
messages.success(request, _("Vendor deleted successfully."))
return redirect("vendor_list")
#group
class GroupListView(LoginRequiredMixin, ListView):
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):
model = models.CustomGroup
template_name = "groups/group_detail.html"
context_object_name = "group"
class GroupCreateView(
LoginRequiredMixin,
SuccessMessageMixin,
CreateView,
):
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.pk}_{instance.name}")
instance.dealer = dealer
instance.group = group
instance.save()
return super().form_valid(form)
class GroupUpdateView(
LoginRequiredMixin,
SuccessMessageMixin,
UpdateView,
):
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.group.name = f"{dealer.pk}_{instance.name}"
instance.save()
return super().form_valid(form)
def GroupDeleteview(request, pk):
group = get_object_or_404(models.CustomGroup, pk=pk)
group.delete()
messages.success(request, _("Group deleted successfully."))
return redirect("group_list")
def GroupPermissionView(request, pk):
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
def UserGroupView(request, pk):
staff = get_object_or_404(models.Staff, pk=pk)
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", pk=staff.pk)
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):
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).all()
return apply_search_filters(staff, query)
class UserDetailView(LoginRequiredMixin, DetailView):
model = models.Staff
template_name = "users/user_detail.html"
context_object_name = "user_"
class UserCreateView(
LoginRequiredMixin,
SuccessMessageMixin,
CreateView,
):
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)
current_staff_count = dealer.staff.count()
if current_staff_count >= allowed_users:
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=form.cleaned_data["name"], 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
group = Group.objects.filter(customgroup__name__iexact=staff.staff_type).first()
if group:
staff.add_group(group)
staff.save()
return super().form_valid(form)
class UserUpdateView(
LoginRequiredMixin,
SuccessMessageMixin,
UpdateView,
):
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.save()
return super().form_valid(form)
def UserDeleteview(request, pk):
staff = get_object_or_404(models.Staff, pk=pk)
staff.staff_member.delete()
staff.delete()
messages.success(request, _("User deleted successfully."))
return redirect("user_list")
# errors
def custom_page_not_found_view(request, exception):
return render(request, "errors/404.html", {})
def custom_error_view(request, exception=None):
return render(request, "errors/500.html", {})
def custom_permission_denied_view(request, exception=None):
return render(request, "errors/403.html", {})
def custom_bad_request_view(request, exception=None):
return render(request, "errors/400.html", {})
class OrganizationListView(LoginRequiredMixin, ListView):
model = CustomerModel
template_name = "organizations/organization_list.html"
context_object_name = "organizations"
paginate_by = 10
def get_queryset(self):
query = self.request.GET.get("q")
dealer = get_user_type(self.request)
organization = dealer.entity.get_customers().filter(additional_info__type="organization", active=True)
return apply_search_filters(organization, query)
class OrganizationDetailView(DetailView):
model = CustomerModel
template_name = "organizations/organization_detail.html"
context_object_name = "organization"
def OrganizationCreateView(request):
if request.method == "POST":
form = forms.OrganizationForm(request.POST)
if CustomerModel.objects.filter(email=request.POST["email"]).exists():
messages.error(request, _("An organization with this email already exists."))
return redirect("organization_create")
organization_dict = {
x: request.POST[x] for x in request.POST if x != "csrfmiddlewaretoken"
}
dealer = get_user_type(request)
name = organization_dict["first_name"] + " " + organization_dict["last_name"]
customer = dealer.entity.create_customer(
commit=False,
customer_model_kwargs={
"customer_name": name,
"address_1": organization_dict["address"],
"phone": organization_dict["phone_number"],
"email": organization_dict["email"],
}
)
image = request.FILES.get("logo")
if image:
file_name = default_storage.save("images/{}".format(image.name), image)
file_url = default_storage.url(file_name)
organization_dict["logo"] = file_url
organization_dict["pk"] = str(customer.pk)
customer.additional_info.update({"customer_info": organization_dict})
customer.additional_info.update({"type": "organization"})
customer.save()
messages.success(request, _("Organization created successfully."))
return redirect("organization_list")
else:
form = forms.OrganizationForm()
return render(request, "organizations/organization_form.html", {"form": form})
def OrganizationUpdateView(request,pk):
organization = get_object_or_404(CustomerModel, pk=pk)
if request.method == "POST":
form = forms.OrganizationForm(request.POST)
organization_dict = {
x: request.POST[x] for x in request.POST if x != "csrfmiddlewaretoken"
}
dealer = get_user_type(request)
instance = dealer.entity.get_customers().get(
pk=organization.additional_info["customer_info"]["pk"]
)
name = organization_dict["first_name"] + " " + organization_dict["last_name"]
instance.customer_name = name
instance.address_1 = organization_dict["address"]
instance.phone = organization_dict["phone_number"]
instance.email = organization_dict["email"]
image = request.FILES.get("logo")
if image:
file_name = default_storage.save("images/{}".format(image.name), image)
file_url = default_storage.url(file_name)
organization_dict["logo"] = file_url
else:
organization_dict["logo"] = organization.additional_info["customer_info"]["logo"]
organization_dict["pk"] = str(instance.pk)
instance.additional_info["customer_info"] = organization_dict
instance.additional_info["type"] = "organization"
instance.save()
messages.success(request, _("Organization created successfully."))
return redirect("organization_list")
else:
form = forms.OrganizationForm(
initial=organization.additional_info["customer_info"] or {}
)
# form.fields.pop("logo", None)
return render(request, "organizations/organization_form.html", {"form": form})
# class OrganizationDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
# model = models.Organization
# template_name = "organizations/organization_confirm_delete.html"
# success_url = reverse_lazy("organization_list")
# success_message = "Organization deleted successfully."
def OrganizationDeleteView(request, pk):
organization = get_object_or_404(CustomerModel, pk=pk)
try:
User.objects.get(email=organization.email).delete()
organization.delete()
messages.success(request, _("Organization deleted successfully."))
except Exception as e:
print("unable to delete user", e)
messages.error(request,_("Unable to delete organization"))
return redirect("organization_list")
class RepresentativeListView(LoginRequiredMixin, ListView):
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(DetailView):
model = models.Representative
template_name = "representatives/representative_detail.html"
context_object_name = "representative"
class RepresentativeCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
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):
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):
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, ListView):
model = BankAccountModel
template_name = "ledger/bank_accounts/bank_account_list.html"
context_object_name = "bank_accounts"
paginate_by = 10
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, SuccessMessageMixin, CreateView):
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."
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, DetailView):
model = BankAccountModel
template_name = "ledger/bank_accounts/bank_account_detail.html"
context_object_name = "bank_account"
class BankAccountUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
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."
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):
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, ListView):
model = AccountModel
template_name = "ledger/coa_accounts/account_list.html"
context_object_name = "accounts"
paginate_by = 10
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, SuccessMessageMixin, CreateView):
model = AccountModel
form_class = AccountModelCreateForm
template_name = "ledger/coa_accounts/account_form.html"
success_url = reverse_lazy("account_list")
success_message = "Account created successfully."
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, DetailView):
model = AccountModel
template_name = "ledger/coa_accounts/account_detail.html"
context_object_name = "account"
slug_field = "uuid"
DEFAULT_TXS_DAYS = 30
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")
)
txs_qs = (
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, SuccessMessageMixin, UpdateView):
model = AccountModel
form_class = AccountModelUpdateForm
template_name = "ledger/coa_accounts/account_form.html"
success_url = reverse_lazy("account_list")
success_message = "Account updated successfully."
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
def account_delete(request, pk):
account = get_object_or_404(AccountModel, pk=pk)
account.delete()
messages.success(request, "Account deleted successfully.")
return redirect("account_list")
# Sales list
@login_required
def sales_list_view(request):
dealer = get_user_type(request)
entity = dealer.entity
transactions = ItemTransactionModel.objects.for_entity(entity_slug=entity.slug, user_model=dealer.user)
paginator = Paginator(transactions, 10)
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, ListView):
model = EstimateModel
template_name = "sales/estimates/estimate_list.html"
context_object_name = "estimates"
paginate_by = 20
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
def create_estimate(request,pk=None):
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()
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, contract_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:
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"))
}
)
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),
}
}
estimate.migrate_itemtxs(
itemtxs=estimate_itemtxs,
commit=True,
operation=EstimateModel.ITEMIZE_APPEND,
)
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)
response = reserve_car(instance, request)
opportunity_id = data.get("opportunity_id")
if opportunity_id != "None":
opportunity = models.Opportunity.objects.get(pk=int(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(
entity_slug=entity.slug, user_model=entity.admin
)
form.fields["customer"].queryset = entity.get_customers().filter(active=True,additional_info__type="customer")
if pk:
opportunity = models.Opportunity.objects.get(pk=pk)
customer = opportunity.customer
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__name')).values_list(
'id_car_make__name', 'id_car_model__name','id_car_serie__name','id_car_trim__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": pk if pk else None,
"customer_count": entity.get_customers().count()
}
return render(request, "sales/estimates/estimate_form.html", context)
class EstimateDetailView(LoginRequiredMixin, DetailView):
model = EstimateModel
template_name = "sales/estimates/estimate_detail.html"
context_object_name = "estimate"
def get_context_data(self, **kwargs):
estimate = kwargs.get("object")
if estimate.get_itemtxs_data():
calculator = CarFinanceCalculator(estimate)
finance_data = calculator.get_finance_data()
kwargs["data"] = finance_data
kwargs["invoice"] = (
InvoiceModel.objects.all().filter(ce_model=estimate).first()
)
return super().get_context_data(**kwargs)
def create_sale_order(request, pk):
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():
form.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
models.Car.objects.get(vin=item.item_model.name).mark_as_sold(request)
messages.success(request, "Sale Order created successfully")
return redirect("estimate_detail", pk=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_form.html",
{"form": form, "estimate": estimate, "items": items, "data": finance_data},
)
def preview_sale_order(request, pk):
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, DetailView):
model = EstimateModel
template_name = "sales/estimates/payment_request_detail.html"
context_object_name = "estimate"
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(DetailView):
model = EstimateModel
context_object_name = "estimate"
template_name = "sales/estimates/estimate_preview.html"
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)
kwargs["vat_amount"] = data["vat_amount"]
kwargs["total"] = data["grand_total"]
kwargs["discount_amount"] = data["discount_amount"]
kwargs["vat"] = data["vat"]
kwargs["additional_services"] = data["additional_services"]
kwargs["dealer"] = dealer
return super().get_context_data(**kwargs)
@login_required
def estimate_mark_as(request, pk):
estimate = get_object_or_404(EstimateModel, pk=pk)
dealer = get_user_type(request)
entity = dealer.entity
mark = request.GET.get("mark")
if mark:
if mark == "review":
if not estimate.can_review():
messages.error(request, _("Estimate 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, _("Estimate is not ready for approval"))
return redirect("estimate_detail", pk=estimate.pk)
estimate.mark_as_approved()
messages.success(request, _("Estimate approved successfully."))
elif mark == "rejected":
if not estimate.can_cancel():
messages.error(request, _("Estimate is not ready for rejection"))
return redirect("estimate_detail", pk=estimate.pk)
estimate.mark_as_canceled()
messages.success(request, _("Estimate canceled successfully."))
elif mark == "completed":
if not estimate.can_complete():
messages.error(request, _("Estimate is not ready for completion"))
return redirect("estimate_detail", pk=estimate.pk)
elif mark == "canceled":
if not estimate.can_cancel():
messages.error(request, _("Estimate is not ready for cancelation"))
return redirect("estimate_detail", pk=estimate.pk)
estimate.mark_as_canceled()
messages.success(request, _("Estimate canceled successfully."))
estimate.save()
messages.success(request, "Estimate marked as " + mark.upper())
return redirect("estimate_detail", pk=estimate.pk)
# Invoice
class InvoiceListView(LoginRequiredMixin, ListView):
model = InvoiceModel
template_name = "sales/invoices/invoice_list.html"
context_object_name = "invoices"
paginate_by = 20
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, DetailView):
model = InvoiceModel
template_name = "sales/invoices/invoice_detail.html"
context_object_name = "invoice"
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, UpdateView):
model = InvoiceModel
form_class = DraftInvoiceModelUpdateForm
template_name = "sales/invoices/draft_invoice_update.html"
success_url = reverse_lazy("invoice_list")
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, UpdateView):
model = InvoiceModel
form_class = ApprovedInvoiceModelUpdateForm
template_name = "sales/invoices/approved_invoice_update.html"
success_url = reverse_lazy("invoice_list")
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, UpdateView):
model = InvoiceModel
form_class = PaidInvoiceModelUpdateForm
template_name = "sales/invoices/paid_invoice_update.html"
success_url = reverse_lazy("invoice_list")
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
def invoice_mark_as(request, pk):
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)
def invoice_create(request, pk):
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,
)
invoice.bind_estimate(estimate)
invoice.mark_as_review()
estimate.mark_as_completed()
estimate.save()
invoice.save()
messages.success(request, "Invoice created successfully!")
return redirect("invoice_detail", pk=invoice.pk)
form = forms.InvoiceModelCreateForm(
entity_slug=entity.slug, user_model=entity.admin
)
form.initial.update(
{
"customer": estimate.customer,
"cash_account": dealer.settings.invoice_cash_account,
"prepaid_account": dealer.settings.invoice_prepaid_account,
"unearned_account": dealer.settings.invoice_unearned_account,
}
)
context = {
"form": form,
"estimate": estimate,
}
return render(request, "sales/invoices/invoice_create.html", context)
class InvoicePreviewView(LoginRequiredMixin, DetailView):
model = InvoiceModel
context_object_name = "invoice"
template_name = "sales/invoices/invoice_preview.html"
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
def PaymentCreateView(request, pk):
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}
)
def PaymentListView(request):
dealer = get_user_type(request)
entity = dealer.entity
journals = JournalEntryModel.objects.filter(ledger__entity=entity).all()
return render(request, "sales/payments/payment_list.html", {"journals": journals})
def PaymentDetailView(request, pk):
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},
)
def payment_mark_as_paid(request, pk):
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(ListView):
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
# CRM RELATED VIEWS
class LeadListView(ListView):
model = models.Lead
template_name = "crm/leads/lead_list.html"
context_object_name = "leads"
paginate_by = 10
def get_queryset(self):
dealer = get_user_type(self.request)
qs = models.Lead.objects.filter(dealer=dealer)
if self.request.is_dealer:
return qs
staffmember = getattr(self.request.user, "staffmember", None)
if staffmember and getattr(staffmember, "staff", None):
return qs.filter(staff=staffmember.staff)
return models.Lead.objects.none()
class LeadDetailView(DetailView):
model = models.Lead
template_name = "crm/leads/lead_detail.html"
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["status_history"] = models.LeadStatusHistory.objects.filter(
lead=self.object
)
context["transfer_form"] = forms.LeadTransferForm()
return context
def lead_create(request):
form = forms.LeadForm()
make = request.GET.get("id_car_make", None)
if make:
form.fields['id_car_model'].queryset = models.CarModel.objects.filter(id_car_make=int(make))
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)
dealer = get_user_type(request)
instance.dealer = dealer
instance.staff = form.cleaned_data.get("staff")
# creating customer in ledger
customer = dealer.entity.get_customers().filter(email=instance.email).first()
if not customer:
customer = dealer.entity.create_customer(
commit=False,
customer_model_kwargs={
"customer_name": instance.full_name,
"address_1": instance.address,
"phone": instance.phone_number,
"email": instance.email,
"sales_tax_rate": 0.15,
}
)
customer_info = {
"first_name": instance.first_name,
"last_name": instance.last_name,
"address": instance.address,
"phone_number": str(instance.phone_number),
"email": instance.email,
"crn": form.cleaned_data["crn"],
"vrn": form.cleaned_data["vrn"],
}
customer.additional_info.update({"customer_info": customer_info })
customer.additional_info.update({"type":"lead"})
customer.save()
instance.customer = customer
# try:
# user = User.objects.get(email=customer.email)
# user.first_name = instance.first_name
# user.last_name = instance.last_name
# user.save()
# except Exception as e:
# print(e)
instance.save()
messages.success(request, "Lead created successfully!")
return redirect("lead_list")
except Exception as e:
messages.error(request, f"Lead was not created ... : {str(e)}")
return render(request, "crm/leads/lead_form.html", {"form": form})
class LeadUpdateView(UpdateView):
model = models.Lead
form_class = forms.LeadForm
template_name = "crm/leads/lead_form.html"
success_url = reverse_lazy("lead_list")
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
def LeadDeleteView(request,pk):
lead = get_object_or_404(models.Lead, pk=pk)
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, pk):
lead = get_object_or_404(models.Lead, pk=pk)
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", pk=lead.pk)
else:
form = forms.NoteForm()
return render(request, "crm/note_form.html", {"form": form, "lead": lead})
@login_required
def add_note_to_opportunity(request, pk):
opportunity = get_object_or_404(models.Opportunity, pk=pk)
if request.method == "POST":
notes = request.POST.get("notes")
if not notes:
messages.error(request, "Notes field is required.")
else:
models.Notes.objects.create(content_object=opportunity, created_by=request.user,note=notes)
messages.success(request, "Note added successfully!")
return redirect("opportunity_detail", pk=opportunity.pk)
@login_required
def update_note(request, pk):
note = get_object_or_404(models.Notes, pk=pk, created_by=request.user)
lead_pk = note.content_object.pk
if request.method == "POST":
form = forms.NoteForm(request.POST, instance=note)
if form.is_valid():
updated_note = form.save(commit=False)
updated_note.content_object = note.content_object
updated_note.created_by = request.user
updated_note.save()
messages.success(request, "Note updated successfully!")
return redirect("lead_detail", pk=lead_pk)
else:
form = forms.NoteForm(instance=note)
return render(request, "crm/note_form.html", {"form": form, "note": note})
@login_required
def delete_note(request, pk):
note = get_object_or_404(models.Notes, pk=pk, created_by=request.user)
lead_pk = note.content_object.pk
note.delete()
messages.success(request, _("Note deleted successfully."))
return redirect("lead_detail", pk=lead_pk)
@login_required
def lead_convert(request, pk):
lead = get_object_or_404(models.Lead, pk=pk)
dealer = get_user_type(request)
if hasattr(lead, "opportunity"):
messages.error(request, "Lead is already converted to customer.")
else:
customer = lead.convert_to_customer(dealer.entity,lead)
models.Opportunity.objects.create(dealer=dealer,customer=customer,lead=lead,probability=50,stage=models.Stage.PROSPECT,staff=lead.staff,status=models.Status.QUALIFIED)
messages.success(request, "Lead converted to customer successfully!")
return redirect("lead_list")
@login_required
def schedule_lead(request, pk):
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, pk=pk, 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.customer
# Create AppointmentRequest
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", pk=lead.pk)
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
def lead_transfer(request,pk):
lead = get_object_or_404(models.Lead, pk=pk)
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, pk,email_pk=None):
lead = get_object_or_404(models.Lead, pk=pk)
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", pk=lead.pk))
response['HX-Redirect'] = reverse('lead_detail', args=[lead.pk])
return response
lead.status = models.Status.CONTACTED
lead.save()
# lead.convert_to_customer(dealer.entity)
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):
lead = get_object_or_404(models.Lead, pk=pk)
if request.method == "POST":
form = forms.ActivityForm(request.POST)
if form.is_valid():
activity = form.save(commit=False)
activity.content_object = lead
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, LoginRequiredMixin):
model = models.Opportunity
form_class = forms.OpportunityForm
template_name = "crm/opportunities/opportunity_form.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
dealer = get_user_type(self.request)
return context
def get_initial(self):
initial = super().get_initial()
if self.kwargs.get('pk',None):
lead = models.Lead.objects.get(pk=self.kwargs.get('pk'))
initial['customer'] = lead.customer
return initial
def form_valid(self, form):
dealer = get_user_type(self.request)
form.instance.dealer = dealer
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("opportunity_detail", kwargs={"pk": self.object.pk})
class OpportunityUpdateView(UpdateView, LoginRequiredMixin):
model = models.Opportunity
form_class = forms.OpportunityForm
template_name = "crm/opportunities/opportunity_form.html"
def get_success_url(self):
return reverse_lazy("opportunity_detail", kwargs={"pk": self.object.pk})
class OpportunityDetailView(DetailView):
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.pk])
form.fields["status"].widget.attrs["hx-get"] = url
form.fields["stage"].widget.attrs["hx-get"] = url
form.fields["status"].initial = self.object.status
form.fields["stage"].initial = self.object.stage
context["status_form"] = form
context["notes"] = models.Notes.objects.filter(content_type__model="opportunity", object_id=self.object.id)
context["activities"] = models.Activity.objects.filter(content_type__model="opportunity", 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"),
}
return context
class OpportunityListView(ListView):
model = models.Opportunity
template_name = "crm/opportunities/opportunity_list.html"
context_object_name = "opportunities"
paginate_by = 10
def get_queryset(self):
dealer = get_user_type(self.request)
return models.Opportunity.objects.filter(dealer=dealer).all()
@login_required
def delete_opportunity(request, pk):
opportunity = get_object_or_404(models.Opportunity, pk=pk)
opportunity.delete()
messages.success(request, _("Opportunity deleted successfully."))
return redirect("opportunity_list")
def opportunity_update_status(request,pk):
opportunity = get_object_or_404(models.Opportunity, pk=pk)
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",pk=opportunity.pk))
response['HX-Refresh'] = 'true'
return response
class NotificationListView(LoginRequiredMixin, ListView):
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):
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):
notifications = models.Notification.objects.filter(
user=request.user, is_read=False
).order_by("-created")
return render(request, "notifications.html", {"notifications_": notifications})
class ItemServiceCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
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"
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, SuccessMessageMixin, UpdateView):
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"
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(ListView, LoginRequiredMixin):
model = models.AdditionalServices
template_name = "items/service/service_list.html"
context_object_name = "services"
paginate_by = 20
def get_queryset(self):
dealer = get_user_type(self.request)
return models.AdditionalServices.objects.filter(dealer=dealer).all()
class ItemExpenseCreateView(CreateView, LoginRequiredMixin):
model = ItemModel
form_class = ExpenseItemCreateForm
template_name = "items/expenses/expense_create.html"
success_url = reverse_lazy("item_expense_list")
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(UpdateView, LoginRequiredMixin):
model = ItemModel
form_class = ExpenseItemUpdateForm
template_name = "items/expenses/expense_update.html"
success_url = reverse_lazy("item_expense_list")
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(ListView, LoginRequiredMixin):
model = ItemModel
template_name = "items/expenses/expenses_list.html"
context_object_name = "expenses"
paginate_by = 20
def get_queryset(self):
dealer = get_user_type(self.request)
return dealer.entity.get_items_expenses()
class BillListView(ListView, LoginRequiredMixin):
model = BillModel
template_name = "ledger/bills/bill_list.html"
context_object_name = "bills"
def get_queryset(self):
dealer = get_user_type(self.request)
qs = dealer.entity.get_bills()
return qs
class BillDetailView(LoginRequiredMixin, DetailView):
model = BillModel
template_name = "ledger/bills/bill_detail.html"
context_object_name = "bill"
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, UpdateView):
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"
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, UpdateView):
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"
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
def bill_mark_as_approved(request, pk):
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
def bill_mark_as_paid(request, pk):
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
def bill_create(request):
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
def BillDeleteView(request, pk):
bill = get_object_or_404(BillModel, pk=pk)
bill.delete()
return redirect("bill_list")
# orders
class OrderListView(ListView):
model = models.SaleOrder
template_name = "sales/orders/order_list.html"
context_object_name = "orders"
def get_queryset(self):
dealer = get_user_type(self.request)
qs = super().get_queryset()
return qs.filter(estimate__entity=dealer.entity)
# email
@login_required
def send_email_view(request, pk):
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
"""
send_email(
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):
return render(request, "errors/404.html", {})
def custom_error_view(request, exception=None):
return render(request, "errors/500.html", {})
def custom_permission_denied_view(request, exception=None):
return render(request, "errors/403.html", {})
def custom_bad_request_view(request, exception=None):
return render(request, "errors/400.html", {})
# BALANCE SHEET
class BaseBalanceSheetRedirectView(DjangoLedgerSecurityMixIn, RedirectView):
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},
)
class FiscalYearBalanceSheetViewBase(FiscalYearBalanceSheetView,DjangoLedgerSecurityMixIn):
template_name = "ledger/reports/balance_sheet.html"
class QuarterlyBalanceSheetView(FiscalYearBalanceSheetViewBase, QuarterlyReportMixIn, DjangoLedgerSecurityMixIn):
"""
Quarter Balance Sheet View.
"""
class MonthlyBalanceSheetView(FiscalYearBalanceSheetViewBase, MonthlyReportMixIn, DjangoLedgerSecurityMixIn):
"""
Monthly Balance Sheet View.
"""
class DateBalanceSheetView(FiscalYearBalanceSheetViewBase, DateReportMixIn, DjangoLedgerSecurityMixIn):
"""
Date Balance Sheet View.
"""
# Income Statement -----------
class BaseIncomeStatementRedirectViewBase(BaseIncomeStatementRedirectView, DjangoLedgerSecurityMixIn):
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}
)
class FiscalYearIncomeStatementViewBase(FiscalYearIncomeStatementView, DjangoLedgerSecurityMixIn):
template_name = "ledger/reports/income_statement.html"
class QuarterlyIncomeStatementView(
FiscalYearIncomeStatementViewBase, QuarterlyReportMixIn, DjangoLedgerSecurityMixIn
):
"""
Quarter Income Statement View.
"""
class MonthlyIncomeStatementView(FiscalYearIncomeStatementViewBase, MonthlyReportMixIn, DjangoLedgerSecurityMixIn):
"""
Monthly Income Statement View.
"""
class DateModelIncomeStatementView(FiscalYearIncomeStatementViewBase, DateReportMixIn, DjangoLedgerSecurityMixIn):
"""
Date Income Statement View.
"""
# Cash Flow -----------
class BaseCashFlowStatementRedirectViewBase(BaseCashFlowStatementRedirectView, DjangoLedgerSecurityMixIn):
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}
)
class FiscalYearCashFlowStatementViewBase(FiscalYearCashFlowStatementView, DjangoLedgerSecurityMixIn):
template_name = "ledger/reports/cash_flow_statement.html"
class QuarterlyCashFlowStatementView(
FiscalYearCashFlowStatementViewBase, QuarterlyReportMixIn, DjangoLedgerSecurityMixIn
):
"""
Quarter Cash Flow Statement View.
"""
class MonthlyCashFlowStatementView(
FiscalYearCashFlowStatementViewBase, MonthlyReportMixIn, DjangoLedgerSecurityMixIn
):
"""
Monthly Cash Flow Statement View.
"""
class DateCashFlowStatementView(FiscalYearCashFlowStatementViewBase, DateReportMixIn, DjangoLedgerSecurityMixIn):
"""
Date Cash Flow Statement View.
"""
# Dashboard
class EntityModelDetailHandlerViewBase(EntityModelDetailHandlerView, DjangoLedgerSecurityMixIn):
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):
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):
"""
Entity Fiscal Year Dashboard View.
"""
class QuarterlyEntityDashboardView(FiscalYearEntityModelDashboardView, QuarterlyReportMixIn, DjangoLedgerSecurityMixIn):
"""
Entity Quarterly Dashboard View.
"""
class MonthlyEntityDashboardView(FiscalYearEntityModelDashboardView, MonthlyReportMixIn, DjangoLedgerSecurityMixIn):
"""
Monthly Entity Dashboard View.
"""
class DateEntityDashboardView(FiscalYearEntityModelDashboardView, DateReportMixIn, DjangoLedgerSecurityMixIn):
"""
Date-specific Entity Dashboard View.
"""
class PayableNetAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View):
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):
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):
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(ListView):
template_name = 'crm/employee_calendar.html'
model = Appointment
context_object_name = 'appointments'
def get_queryset(self):
query = self.request.GET.get('q')
dealer = get_user_type(self.request)
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):
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(ExportMixin, LoginRequiredMixin, SingleTableView):
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)
def DealerSettingsView(request,pk):
dealer_setting = get_object_or_404(models.DealerSettings, pk=pk)
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', pk=dealer.pk)
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})
def schedule_cancel(request,pk):
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):
dealer = get_user_type(request)
if request.method == "POST":
form = forms.DealersMakeForm(request.POST, dealer=dealer)
if form.is_valid():
form.save()
return redirect("dealer_detail", pk=dealer.pk)
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)
return render(request, "dealers/assign_car_makes.html", {"form": form})