7709 lines
310 KiB
Python
7709 lines
310 KiB
Python
# Standard
|
|
import cv2
|
|
import json
|
|
import logging
|
|
from datetime import datetime
|
|
from time import sleep
|
|
import numpy as np
|
|
# from rich import print
|
|
from random import randint
|
|
from decimal import Decimal
|
|
from datetime import timedelta
|
|
from calendar import month_name
|
|
from pyzbar.pyzbar import decode
|
|
from urllib.parse import urlparse, urlunparse
|
|
#####################################################################
|
|
from inventory.models import Status as LeadStatus
|
|
|
|
from background_task.models import Task
|
|
from django.db.models.deletion import RestrictedError
|
|
from django.http.response import StreamingHttpResponse
|
|
# Django
|
|
from django.db.models import Q
|
|
from django.conf import settings
|
|
from django.db import transaction
|
|
from django.db.models import Func
|
|
from django.contrib import messages
|
|
from django.http import JsonResponse, HttpResponseForbidden
|
|
from django.forms import HiddenInput, ValidationError
|
|
from django.shortcuts import HttpResponse
|
|
from django.db.models import Sum, F, Count
|
|
from django.core.paginator import Paginator
|
|
from django.contrib.auth.models import User
|
|
from django.contrib.auth.models import Group
|
|
from django.db.models import 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.contrib.auth.decorators import permission_required
|
|
from django.shortcuts import render, get_object_or_404, redirect
|
|
from plans.models import Order,PlanPricing,AbstractOrder,UserPlan,BillingInfo
|
|
from django.views.generic import (
|
|
View,
|
|
ListView,
|
|
DetailView,
|
|
CreateView,
|
|
UpdateView,
|
|
DeleteView,
|
|
TemplateView,
|
|
ArchiveIndexView,
|
|
)
|
|
|
|
# Django Ledger
|
|
from django_ledger.io import roles
|
|
from django_ledger.utils import accruable_net_summary
|
|
from django_ledger.views.invoice import (
|
|
InvoiceModelDetailView as InvoiceModelDetailViewBase,
|
|
)
|
|
from django_ledger.views import (
|
|
LedgerModelListView as LedgerModelListViewBase,
|
|
JournalEntryModelTXSDetailView as JournalEntryModelTXSDetailViewBase,
|
|
LedgerModelModelActionView as LedgerModelModelActionViewBase,
|
|
LedgerModelDeleteView as LedgerModelDeleteViewBase,
|
|
LedgerModelCreateView as LedgerModelCreateViewBase
|
|
)
|
|
from django_ledger.views.invoice import (
|
|
InvoiceModelDetailView as InvoiceModelDetailViewBase,
|
|
)
|
|
from django_ledger.views import (
|
|
LedgerModelListView as LedgerModelListViewBase,
|
|
JournalEntryModelTXSDetailView as JournalEntryModelTXSDetailViewBase,
|
|
LedgerModelModelActionView as LedgerModelModelActionViewBase,
|
|
LedgerModelDeleteView as LedgerModelDeleteViewBase,
|
|
LedgerModelCreateView as LedgerModelCreateViewBase
|
|
)
|
|
from django_ledger.forms.account import AccountModelCreateForm, AccountModelUpdateForm
|
|
from django_ledger.views.entity import (
|
|
EntityModelDetailBaseView,
|
|
EntityModelDetailHandlerView,
|
|
)
|
|
from django_ledger.forms.ledger import LedgerModelCreateForm
|
|
from django_ledger.views.entity import (
|
|
EntityModelDetailBaseView,
|
|
EntityModelDetailHandlerView,
|
|
)
|
|
from django_ledger.forms.ledger import LedgerModelCreateForm
|
|
from django_ledger.forms.item import (
|
|
ExpenseItemCreateForm,
|
|
ExpenseItemUpdateForm,
|
|
)
|
|
from django_ledger.forms.bank_account import (
|
|
BankAccountCreateForm,
|
|
BankAccountUpdateForm,
|
|
)
|
|
from django_ledger.forms.bill import (
|
|
ApprovedBillModelUpdateForm,
|
|
InReviewBillModelUpdateForm,
|
|
)
|
|
from django_ledger.forms.invoice import (
|
|
DraftInvoiceModelUpdateForm,
|
|
ApprovedInvoiceModelUpdateForm,
|
|
PaidInvoiceModelUpdateForm,
|
|
)
|
|
from django_ledger.models import (
|
|
ItemTransactionModel,
|
|
EntityModel,
|
|
InvoiceModel,
|
|
BankAccountModel,
|
|
AccountModel,
|
|
JournalEntryModel,
|
|
TransactionModel,
|
|
EstimateModel,
|
|
CustomerModel,
|
|
ItemModel,
|
|
BillModel,
|
|
LedgerModel,
|
|
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 inventory.filters import AccountModelFilter
|
|
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 django.db.models.functions import Lower
|
|
from .models import SaleOrder
|
|
from .services import (
|
|
decodevin,
|
|
get_make,
|
|
get_model,
|
|
)
|
|
from .utils import (
|
|
CarFinanceCalculator,
|
|
create_user_dealer,
|
|
get_car_finance_data,
|
|
get_financial_values,
|
|
get_item_transactions,
|
|
handle_payment,
|
|
reserve_car,
|
|
# send_email,
|
|
get_user_type,
|
|
set_bill_payment,
|
|
set_invoice_payment,
|
|
CarTransfer,
|
|
)
|
|
from .tasks import create_accounts_for_make, send_email
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
|
|
|
|
class Hash(Func):
|
|
"""
|
|
Represents a function used to compute a hash value.
|
|
|
|
This class serves as a placeholder to specify a particular hash
|
|
computation function. It extends the `Func` base class and is used
|
|
primarily to work with hash-related operations within the underlying
|
|
implementation. The specific hash algorithm can be modified or accessed
|
|
through the `function` attribute.
|
|
|
|
:ivar function: Specifies the hash computation function.
|
|
:type function: str
|
|
"""
|
|
function = "get_hash"
|
|
|
|
|
|
def switch_language(request):
|
|
"""
|
|
Switches the current language context for the user based on a request parameter, modifies the URL path
|
|
accordingly, and updates session and cookies with the new language preference.
|
|
|
|
:param request: The HTTP request object containing information about the user request, including
|
|
the desired language to switch to and the referring URL.
|
|
- "GET" dictionary is accessed to retrieve the desired language parameter.
|
|
- "META" dictionary is used to extract the referring URL via "HTTP_REFERER".
|
|
|
|
:return: A redirect response object pointing to the modified URL with the updated language
|
|
preference, if the requested language is valid. Otherwise, redirects to the default URL.
|
|
"""
|
|
language = request.GET.get("language", "en")
|
|
referer = request.META.get("HTTP_REFERER", "/")
|
|
parsed_url = urlparse(referer)
|
|
path_parts = parsed_url.path.split("/")
|
|
|
|
if path_parts[1] in dict(settings.LANGUAGES):
|
|
path_parts.pop(1)
|
|
|
|
new_path = "/".join(path_parts)
|
|
new_url = urlunparse(
|
|
(
|
|
parsed_url.scheme,
|
|
parsed_url.netloc,
|
|
new_path,
|
|
parsed_url.params,
|
|
parsed_url.query,
|
|
parsed_url.fragment,
|
|
)
|
|
)
|
|
|
|
if language in dict(settings.LANGUAGES):
|
|
logger.debug(f"Switching language to: {language}")
|
|
response = redirect(new_url)
|
|
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
|
|
translation.activate(language)
|
|
request.session[settings.LANGUAGE_COOKIE_NAME] = language
|
|
logger.debug(
|
|
f"Language switched to: {language}, Session: {request.session[settings.LANGUAGE_COOKIE_NAME]}"
|
|
)
|
|
return response
|
|
else:
|
|
logger.warning(f"Invalid language code: {language}")
|
|
return redirect("/")
|
|
|
|
|
|
def dealer_signup(request, *args, **kwargs):
|
|
"""
|
|
Handles the dealer signup wizard process, including forms validation, user and group
|
|
creation, permissions assignment, and dealer data storage. This view supports GET
|
|
requests for rendering the signup wizard page, and POST requests for processing
|
|
submitted data. The function also differentiates between requests sent with the
|
|
"Hx-Request" header for partial form validations in the wizard.
|
|
|
|
:param request: The HTTP request object representing the client request. It contains
|
|
metadata about the request and the POST data for creating the dealer.
|
|
:type request: django.http.HttpRequest
|
|
:param args: Optional positional arguments passed to the view during the call.
|
|
:type args: tuple
|
|
:param kwargs: Optional keyword arguments passed to the view during the call.
|
|
:type kwargs: dict
|
|
:return: A rendered signup wizard page or a JSON response indicating operation success
|
|
or failure.
|
|
:rtype: Union[django.http.HttpResponse, django.http.JsonResponse]
|
|
"""
|
|
form1 = forms.WizardForm1()
|
|
form2 = forms.WizardForm2()
|
|
form3 = forms.WizardForm3()
|
|
|
|
if request.method == "POST":
|
|
if "Hx-Request" in request.headers:
|
|
form1 = forms.WizardForm1(request.POST)
|
|
return render(
|
|
request,
|
|
"account/signup-wizard.html",
|
|
{"form1": form1, "form2": form2, "form3": form3},
|
|
)
|
|
|
|
data = json.loads(request.body)
|
|
wf1 = data.get("wizardValidationForm1")
|
|
wf2 = data.get("wizardValidationForm2")
|
|
wf3 = data.get("wizardValidationForm3")
|
|
email = wf1.get("email")
|
|
password = wf1.get("password")
|
|
password_confirm = wf1.get("confirm_password")
|
|
name = wf2.get("name")
|
|
arabic_name = wf2.get("arabic_name")
|
|
phone = wf2.get("phone_number")
|
|
crn = wf3.get("crn")
|
|
vrn = wf3.get("vrn")
|
|
address = wf3.get("address")
|
|
|
|
if password != password_confirm:
|
|
return JsonResponse({"error": _("Passwords do not match")}, status=400)
|
|
try:
|
|
create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, address)
|
|
return JsonResponse(
|
|
{"message": _("User created successfully")}, status=200
|
|
)
|
|
except Exception as e:
|
|
return JsonResponse({"error": str(e)}, status=400)
|
|
return render(
|
|
request,
|
|
"account/signup-wizard.html",
|
|
{"form1": form1, "form2": form2, "form3": form3},
|
|
)
|
|
|
|
|
|
|
|
class HomeView(LoginRequiredMixin, TemplateView):
|
|
"""
|
|
HomeView class responsible for rendering the home page.
|
|
|
|
This class ensures that only authenticated users can access the home page.
|
|
Unauthenticated users are redirected to the welcome page. It is built on top of
|
|
Django's TemplateView and includes additional functionality by inheriting from
|
|
LoginRequiredMixin. The purpose of this class is to control the accessibility
|
|
of the main index page of the application and manage context data for frontend
|
|
rendering.
|
|
|
|
:ivar template_name: The path to the template used for rendering the view's
|
|
output.
|
|
:type template_name: str
|
|
"""
|
|
template_name = "index.html"
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
# Redirect unauthenticated users to the welcome page
|
|
if not request.user.is_authenticated:
|
|
return redirect("welcome")
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
|
|
class TestView(TemplateView):
|
|
"""
|
|
Represents a view for displaying a list of cars.
|
|
|
|
This class is a Django TemplateView-based view that renders a specific template
|
|
to display a list of cars. It uses a pre-defined template file and can be
|
|
extended or customized to provide additional functionality for rendering
|
|
content in a Django application.
|
|
|
|
:ivar template_name: Specifies the path to the HTML template file used for
|
|
rendering the cars list view.
|
|
:type template_name: str
|
|
"""
|
|
template_name = "inventory/cars_list_api.html"
|
|
|
|
|
|
class ManagerDashboard(LoginRequiredMixin, TemplateView):
|
|
"""
|
|
ManagerDashboard class is a view handling the dashboard for a manager.
|
|
|
|
Provides functionality to manage and view various statistics and data specific
|
|
to the dealer associated with the authenticated manager. It uses a specific
|
|
template and ensures authentication before granting access. The class
|
|
aggregates data about cars, leads, financial statistics, and other related
|
|
business information for display in the manager's dashboard.
|
|
|
|
:ivar template_name: Path to the template used for rendering the manager's dashboard.
|
|
:type template_name: str
|
|
"""
|
|
template_name = "dashboards/manager.html"
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
if not request.user.is_authenticated:
|
|
return redirect("welcome")
|
|
if not getattr(request.user, 'dealer', False):
|
|
return HttpResponseForbidden("You are not authorized to view this dashboard.")
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
dealer = get_user_type(self.request)
|
|
entity = dealer.entity
|
|
total_cars = models.Car.objects.filter(dealer=dealer).count()
|
|
total_reservations = models.CarReservation.objects.filter(
|
|
reserved_until__gte=timezone.now()
|
|
).count()
|
|
stats = models.CarFinance.objects.aggregate(
|
|
total_cost_price=Sum("cost_price"),
|
|
total_selling_price=Sum("selling_price"),
|
|
)
|
|
total_cost_price = stats["total_cost_price"] or 0
|
|
total_selling_price = stats["total_selling_price"] or 0
|
|
total_profit = total_selling_price - total_cost_price
|
|
|
|
new_leads = models.Lead.objects.filter(
|
|
dealer=dealer, status=models.Status.NEW
|
|
).count()
|
|
pending_leads = models.Lead.objects.filter(
|
|
dealer=dealer, status=models.Status.PENDING
|
|
).count()
|
|
canceled_leads = models.Lead.objects.filter(
|
|
dealer=dealer, status=models.Status.CANCELED
|
|
).count()
|
|
available_cars = models.Car.objects.filter(
|
|
dealer=dealer, status=models.CarStatusChoices.AVAILABLE
|
|
).count()
|
|
sold_cars = models.Car.objects.filter(
|
|
dealer=dealer, status=models.CarStatusChoices.SOLD
|
|
).count()
|
|
reserved_cars = models.Car.objects.filter(
|
|
dealer=dealer, status=models.CarStatusChoices.RESERVED
|
|
).count()
|
|
hold_cars = models.Car.objects.filter(
|
|
dealer=dealer, status=models.CarStatusChoices.HOLD
|
|
).count()
|
|
damaged_cars = models.Car.objects.filter(
|
|
dealer=dealer, status=models.CarStatusChoices.DAMAGED
|
|
).count()
|
|
transfer_cars = models.Car.objects.filter(
|
|
dealer=dealer, status=models.CarStatusChoices.TRANSFER
|
|
).count()
|
|
reserved_percentage = reserved_cars / total_cars * 100
|
|
sold_percentage = sold_cars / total_cars * 100
|
|
qs = (
|
|
models.Car.objects.values("id_car_make__name")
|
|
.annotate(count=Count("id"))
|
|
.order_by("id_car_make__name")
|
|
)
|
|
car_by_make = list(qs)
|
|
total_activity = models.UserActivityLog.objects.filter(user=dealer.user).count()
|
|
staff = models.Staff.objects.filter(dealer=dealer).count()
|
|
total_leads = models.Lead.objects.filter(dealer=dealer).count()
|
|
invoices = entity.get_invoices().count()
|
|
customers = entity.get_customers().count()
|
|
purchase_orders = entity.get_purchase_orders().count()
|
|
estimates = entity.get_estimates().count()
|
|
|
|
context["dealer"] = dealer
|
|
context["total_cars"] = total_cars
|
|
context["total_reservations"] = total_reservations
|
|
context["total_cost_price"] = total_cost_price
|
|
context["total_selling_price"] = total_selling_price
|
|
context["total_profit"] = total_profit
|
|
context["new_leads"] = new_leads
|
|
context["pending_leads"] = pending_leads
|
|
context["canceled_leads"] = canceled_leads
|
|
context["reserved_percentage"] = reserved_percentage
|
|
context["sold_percentage"] = sold_percentage
|
|
context["available_cars"] = available_cars
|
|
context["sold_cars"] = sold_cars
|
|
context["reserved_cars"] = reserved_cars
|
|
context["hold_cars"] = hold_cars
|
|
context["damaged_cars"] = damaged_cars
|
|
context["transfer_cars"] = transfer_cars
|
|
context["car"] = json.dumps(car_by_make)
|
|
context["customers"] = customers
|
|
context["staff"] = staff
|
|
context["total_leads"] = total_leads
|
|
context["invoices"] = invoices
|
|
context["estimates"] = estimates
|
|
context["purchase_orders"] = purchase_orders
|
|
|
|
return context
|
|
|
|
|
|
class SalesDashboard(LoginRequiredMixin, TemplateView):
|
|
"""
|
|
SalesDashboard class provides a view for the sales dashboard.
|
|
|
|
This class is responsible for generating the context data required to
|
|
display various statistics and information on the sales dashboard. It
|
|
inherits from `LoginRequiredMixin` and `TemplateView`, ensuring only
|
|
authenticated users can access the view. The dashboard includes data
|
|
such as the total number of cars, reservations, sold percentages,
|
|
reserved percentages, and cars categorized by various statuses. The
|
|
purpose of this dashboard is to provide dealership staff an overview
|
|
of their inventory and related sales metrics.
|
|
|
|
:ivar template_name: The name of the HTML template used for rendering
|
|
the sales dashboard.
|
|
:type template_name: str
|
|
"""
|
|
template_name = "dashboards/sales.html"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
dealer = get_user_type(self.request)
|
|
staff = getattr(self.request.user, "staff", None)
|
|
total_cars = models.Car.objects.filter(dealer=dealer).count()
|
|
total_reservations = models.CarReservation.objects.filter(
|
|
reserved_by=self.request.user, reserved_until__gte=timezone.now()
|
|
).count()
|
|
|
|
# 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["reserved_percentage"] = reserved_percentage
|
|
context["sold_percentage"] = sold_percentage
|
|
context["available_cars"] = available_cars
|
|
context["sold_cars"] = sold_cars
|
|
context["reserved_cars"] = reserved_cars
|
|
context["hold_cars"] = hold_cars
|
|
context["damaged_cars"] = damaged_cars
|
|
context["transfer_cars"] = transfer_cars
|
|
context["car"] = json.dumps(car_by_make)
|
|
|
|
return context
|
|
|
|
|
|
class WelcomeView(TemplateView):
|
|
"""
|
|
Handles the rendering and context data for the Welcome view.
|
|
|
|
This class serves as a Django TemplateView for the "welcome.html" template. It
|
|
is primarily responsible for providing the necessary context data, including
|
|
user-specific information and the list of plans, to the template for rendering
|
|
the welcome page.
|
|
|
|
:ivar template_name: Path to the template used by the view.
|
|
:type template_name: str
|
|
"""
|
|
template_name = "welcome.html"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
dealer = get_user_type(self.request)
|
|
plan_list = Plan.objects.all()
|
|
context["plan_list"] = plan_list
|
|
return context
|
|
|
|
|
|
class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
|
"""
|
|
Manages the creation of a new car entry in the inventory system.
|
|
|
|
This class is responsible for handling the creation of a new car, including form
|
|
display and data submission. It ensures the user has appropriate permissions and
|
|
customizes the available vendors for the user depending on their dealer entity.
|
|
|
|
:ivar model: Specifies the Car model that this view interacts with.
|
|
:type model: models.Car
|
|
:ivar form_class: Defines the form class to be used for creating or editing `Car` instances.
|
|
:type form_class: forms.CarForm
|
|
:ivar template_name: Name of the template to render the car creation form.
|
|
:type template_name: str
|
|
:ivar permission_required: Permissions required to add a car.
|
|
:type permission_required: list
|
|
"""
|
|
model = models.Car
|
|
form_class = forms.CarForm
|
|
template_name = "inventory/car_form.html"
|
|
permission_required = ["inventory.add_car"]
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
dealer = get_user_type(self.request)
|
|
form.fields["vendor"].queryset = dealer.vendors.all()
|
|
return form
|
|
|
|
def get_success_url(self):
|
|
"""Determine the redirect URL based on user choice."""
|
|
if self.request.POST.get("add_another"):
|
|
return reverse("car_add")
|
|
return reverse("inventory_stats")
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
form.instance.dealer = dealer
|
|
form.save()
|
|
messages.success(self.request, _("Car saved successfully"))
|
|
return super().form_valid(form)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
dealer = get_user_type(self.request)
|
|
context["vendor_exists"] = dealer.vendors.exists()
|
|
|
|
return context
|
|
|
|
def car_history(request, pk):
|
|
"""
|
|
Fetch and display the history of activities related to a specific car.
|
|
|
|
This view retrieves a car object based on its primary key (pk) and gathers
|
|
all related activity records where the content type corresponds to the car
|
|
model. The retrieved data is then rendered into a specified HTML template
|
|
for presentation.
|
|
|
|
:param request: The HTTP request object that contains metadata about the
|
|
request made by the client.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the car object to retrieve.
|
|
:type pk: int
|
|
:return: An HTTP response with the rendered car history HTML template,
|
|
including the car and its associated activities as context data.
|
|
:rtype: HttpResponse
|
|
"""
|
|
car = get_object_or_404(models.Car, 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):
|
|
"""
|
|
Handles AJAX requests for various car-related operations.
|
|
|
|
This class enables dynamic handling of AJAX requests related to vehicle
|
|
data, such as retrieving models, series, trims, specifications, and other
|
|
related details. It checks the requested action, delegates it to the
|
|
corresponding handler method, and returns an appropriate response.
|
|
|
|
:ivar request: Django request object containing HTTP request details.
|
|
"""
|
|
def get(self, request, *args, **kwargs):
|
|
action = request.GET.get("action")
|
|
handlers = {
|
|
"decode_vin": self.decode_vin,
|
|
"get_models": self.get_models,
|
|
"get_series": self.get_series,
|
|
"get_trims": self.get_trims,
|
|
"get_specifications": self.get_specifications,
|
|
"get_equipments": self.get_equipments,
|
|
"get_options": self.get_options,
|
|
}
|
|
handler = handlers.get(action)
|
|
if handler:
|
|
return handler(request)
|
|
else:
|
|
return JsonResponse({"error": "Invalid action"}, status=400)
|
|
|
|
def decode_vin(self, request):
|
|
vin_no = request.GET.get("vin_no")
|
|
car_existed = models.Car.objects.filter(vin=vin_no).exists()
|
|
|
|
if car_existed:
|
|
return JsonResponse({"error": _("VIN number exists")}, status=400)
|
|
|
|
if not vin_no or len(vin_no.strip()) != 17:
|
|
return JsonResponse(
|
|
{"success": False, "error": _("Invalid VIN number provided")}, status=400
|
|
)
|
|
|
|
vin_no = vin_no.strip()
|
|
vin_data = {}
|
|
decoding_method = ""
|
|
# 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)
|
|
|
|
|
|
import cv2
|
|
import numpy as np
|
|
from pyzbar.pyzbar import decode
|
|
from django.views import View
|
|
from django.shortcuts import render, get_object_or_404, redirect
|
|
from django.http import JsonResponse
|
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
from django.utils.decorators import method_decorator
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
from django.urls import reverse
|
|
from . import models # Adjust to your project structure
|
|
|
|
@method_decorator(csrf_exempt, name="dispatch")
|
|
class SearchCodeView(LoginRequiredMixin, View):
|
|
template_name = "inventory/scan_vin.html"
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
return render(request, self.template_name)
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
image_file = request.FILES.get("image")
|
|
|
|
if not image_file:
|
|
return JsonResponse({"success": False, "error": _("No image provided")})
|
|
|
|
try:
|
|
np_arr = np.frombuffer(image_file.read(), np.uint8)
|
|
image = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
|
|
if image is None:
|
|
raise ValueError("Invalid image format")
|
|
|
|
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
|
decoded_objects = decode(gray)
|
|
|
|
if not decoded_objects:
|
|
return JsonResponse({"success": False, "error": _("No QR/Barcode detected")})
|
|
|
|
code = decoded_objects[0].data.decode("utf-8").strip()
|
|
car = get_object_or_404(models.Car, vin=code)
|
|
return JsonResponse({
|
|
"success": True,
|
|
"code": code,
|
|
"redirect_url": reverse("car_detail", args=[car.pk])
|
|
})
|
|
|
|
except Exception as e:
|
|
return JsonResponse({"success": False, "error": str(e)})
|
|
|
|
|
|
class CarInventory(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Represents the car inventory listing view for a dealership.
|
|
|
|
This class is a Django ListView-based implementation to display a list of cars
|
|
filtered by specific criteria such as make, model, and trim, while supporting user
|
|
authentication and permissions. It includes pagination and ordering features and
|
|
integrates search filtering functionality.
|
|
|
|
:ivar model: The model associated with the list view.
|
|
:type model: models.Car
|
|
:ivar home_label: The label of the home section of the application, used for UI
|
|
or breadcrumbs. Defined as a translatable string.
|
|
:type home_label: str
|
|
:ivar template_name: The template path used for rendering the car inventory
|
|
list view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable containing the
|
|
car objects in the template.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of car objects displayed per page.
|
|
:type paginate_by: int
|
|
:ivar ordering: The default ordering of car objects in the list view.
|
|
:type ordering: list
|
|
:ivar permission_required: The permission(s) required to access this view.
|
|
:type permission_required: list
|
|
"""
|
|
model = models.Car
|
|
home_label = _("inventory")
|
|
template_name = "inventory/car_inventory.html"
|
|
context_object_name = "cars"
|
|
paginate_by = 10
|
|
ordering = ["receiving_date"]
|
|
permission_required = ["inventory.view_car"]
|
|
|
|
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, PermissionRequiredMixin, CreateView):
|
|
"""
|
|
View for creating a new car color.
|
|
|
|
This view is used to add new colors to a specific car by associating
|
|
the color with a car instance. It ensures that the user is logged in
|
|
and has the necessary permissions to add car colors. The form used in
|
|
this view is populated dynamically with respect to the related car
|
|
object that is retrieved based on the provided car primary key.
|
|
|
|
:ivar model: Reference to the model representing car colors.
|
|
:type model: models.CarColors
|
|
:ivar form_class: The form class used for car color creation.
|
|
:type form_class: forms.CarColorsForm
|
|
:ivar template_name: Path to the template used while rendering the view.
|
|
:type template_name: str
|
|
:ivar permission_required: List of permissions required by the view.
|
|
:type permission_required: list
|
|
"""
|
|
model = models.CarColors
|
|
form_class = forms.CarColorsForm
|
|
template_name = "inventory/add_colors.html"
|
|
permission_required = ["inventory.view_car"]
|
|
|
|
def form_valid(self, form):
|
|
car = get_object_or_404(models.Car, 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, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Represents a view for listing and managing a collection of cars.
|
|
|
|
This class is used to display a paginated list of cars according to specific
|
|
filters and permissions. It ensures that only users with the required
|
|
permissions can access the view and handles dynamic filtering of cars based
|
|
on user input. The view also populates additional context data,
|
|
such as car statistics and related make, model, and year information,
|
|
to support detailed customization in templates.
|
|
|
|
:ivar model: Specifies the model associated with this view.
|
|
:type model: models.Car
|
|
:ivar template_name: The name of the template used for this view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the variable used in the template to
|
|
represent the objects being displayed.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of items to display per page in the paginated list.
|
|
:type paginate_by: int
|
|
:ivar permission_required: The permission required to access this view.
|
|
:type permission_required: str
|
|
"""
|
|
model = models.Car
|
|
template_name = "inventory/car_list_view.html"
|
|
context_object_name = "cars"
|
|
paginate_by = 20
|
|
permission_required = "inventory.view_car"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
dealer = get_user_type(self.request)
|
|
cars = models.Car.objects.filter(dealer=dealer).order_by("receiving_date")
|
|
|
|
context["stats"] = {
|
|
"all": cars.count(),
|
|
"available": cars.filter(status="available").count(),
|
|
"reserved": cars.filter(status="reserved").count(),
|
|
"sold": cars.filter(status="sold").count(),
|
|
"transfer": cars.filter(status="transfer").count(),
|
|
}
|
|
context["make"] = models.CarMake.objects.filter(car__in=cars).distinct()
|
|
context["model"] = models.CarModel.objects.none()
|
|
context["year"] = models.Car.objects.none()
|
|
make = self.request.GET.get("make")
|
|
model = self.request.GET.get("model")
|
|
|
|
if make:
|
|
make_ = models.CarMake.objects.get(id_car_make=int(make))
|
|
context["model"] = make_.carmodel_set.filter(car__in=cars).distinct()
|
|
if make and model:
|
|
make_ = models.CarMake.objects.get(id_car_make=int(make))
|
|
model_ = models.CarModel.objects.get(id_car_model=int(model))
|
|
context["year"] = (
|
|
models.Car.objects.filter(id_car_make=make_, id_car_model=model_)
|
|
.values_list("year")
|
|
.distinct()
|
|
)
|
|
return context
|
|
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
qs = super().get_queryset()
|
|
qs = qs.filter(dealer=dealer)
|
|
status = self.request.GET.get("status")
|
|
search = self.request.GET.get("search")
|
|
make = self.request.GET.get("make", None)
|
|
model = self.request.GET.get("model", None)
|
|
year = self.request.GET.get("year", None)
|
|
car_status = self.request.GET.get("car_status", None)
|
|
|
|
if status:
|
|
qs = qs.filter(status=status)
|
|
if search:
|
|
query = (
|
|
Q(vin__icontains=search)
|
|
| Q(id_car_make__name__icontains=search)
|
|
| Q(id_car_model__name__icontains=search)
|
|
| Q(id_car_trim__name__icontains=search)
|
|
| Q(vin=search)
|
|
)
|
|
qs = qs.filter(query)
|
|
if any([make, model, year, car_status]):
|
|
query = Q()
|
|
if make:
|
|
query &= Q(id_car_make=int(make))
|
|
if model:
|
|
query &= Q(id_car_model=model)
|
|
if year:
|
|
query &= Q(year=year)
|
|
if car_status:
|
|
query &= Q(status=car_status)
|
|
qs = qs.filter(query)
|
|
return qs
|
|
|
|
|
|
|
|
@login_required
|
|
def inventory_stats_view(request):
|
|
"""
|
|
Handle the inventory stats view for a dealer, displaying detailed information
|
|
about the cars, including counts grouped by make, model, and trim.
|
|
|
|
The function fetches all cars associated with the authenticated dealer, calculates
|
|
the inventory statistics (e.g., total cars, reserved cars, and cars categorized
|
|
by make, model, and trim levels), and prepares the data to be rendered in a
|
|
template.
|
|
|
|
:param request: The HTTP request object from the client.
|
|
:type request: HttpRequest
|
|
:return: An HTTP response containing structured inventory data rendered in the
|
|
"inventory/inventory_stats.html" template.
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer = get_user_type(request)
|
|
|
|
# Base queryset for cars belonging to the dealer
|
|
cars = models.Car.objects.filter(dealer=dealer)
|
|
|
|
# Count for total, reserved, showroom, and unreserved cars
|
|
total_cars = cars.count()
|
|
reserved_cars = models.CarReservation.objects.count()
|
|
# showroom_cars = cars.filter(location='showroom').count()
|
|
# unreserved_cars = total_cars - reserved_cars
|
|
|
|
# Annotate total cars by make, model, and trim
|
|
cars = cars.select_related("id_car_make", "id_car_model", "id_car_trim").annotate(
|
|
make_total=Count("id_car_make"),
|
|
model_total=Count("id_car_model"),
|
|
trim_total=Count("id_car_trim"),
|
|
)
|
|
|
|
inventory = {}
|
|
for car in cars:
|
|
make = car.id_car_make
|
|
if make.id_car_make not in inventory:
|
|
inventory[make.id_car_make] = {
|
|
"make_id": make.id_car_make,
|
|
"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, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Provides a detailed view of a single car instance.
|
|
|
|
This class-based view is designed to display detailed information about a
|
|
single car instance from the inventory. It utilizes Django's built-in
|
|
DetailView along with mixins to enforce authentication and permission
|
|
requirements. The view ensures that only authenticated users with the proper
|
|
permissions can access car details.
|
|
|
|
:ivar model: The model associated with this view, representing the Car model.
|
|
:type model: Model
|
|
:ivar template_name: The path to the template used to render the detailed
|
|
car view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable that contains
|
|
the car object.
|
|
:type context_object_name: str
|
|
:ivar permission_required: A list of permissions required to access the
|
|
view.
|
|
:type permission_required: list
|
|
"""
|
|
model = models.Car
|
|
template_name = "inventory/car_detail.html"
|
|
context_object_name = "car"
|
|
permission_required = ["inventory.view_car"]
|
|
|
|
|
|
class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
|
"""
|
|
Handles the creation of car finance records within the inventory system.
|
|
|
|
This view provides functionality to create car finance records tied to a
|
|
specific car in the inventory. It enforces that the user is logged in and
|
|
has the required permissions to add a car finance record. It also customizes
|
|
form behavior and context data to associate the finance record with the
|
|
corresponding car and populate additional services based on the user's type.
|
|
|
|
:ivar model: The database model associated with the view.
|
|
:type model: models.CarFinance
|
|
:ivar form_class: The form class used to create car finance records.
|
|
:type form_class: forms.CarFinanceForm
|
|
:ivar template_name: The template used to render the car finance creation page.
|
|
:type template_name: str
|
|
:ivar permission_required: The list of permissions required to access this view.
|
|
:type permission_required: list
|
|
"""
|
|
model = models.CarFinance
|
|
form_class = forms.CarFinanceForm
|
|
template_name = "inventory/car_finance_form.html"
|
|
permission_required = ["inventory.add_carfinance"]
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.car = get_object_or_404(models.Car, 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, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
|
):
|
|
"""
|
|
Manages the update of car finance details.
|
|
|
|
This class-based view provides functionality for updating car finance
|
|
information in the system. It enforces login and specific permissions for
|
|
access, and it leverages mixins to provide success messages upon completing
|
|
updates. It is designed to interact with car finance data, update form
|
|
handling, and enforce related business logic.
|
|
|
|
:ivar model: The model associated with the view.
|
|
:type model: models.CarFinance
|
|
:ivar form_class: The form class used by the view.
|
|
:type form_class: forms.CarFinanceForm
|
|
:ivar template_name: The template used for rendering the form.
|
|
:type template_name: str
|
|
:ivar success_message: The success message displayed after an update.
|
|
:type success_message: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list
|
|
"""
|
|
model = models.CarFinance
|
|
form_class = forms.CarFinanceForm
|
|
template_name = "inventory/car_finance_form.html"
|
|
success_message = _("Car finance details updated successfully")
|
|
permission_required = ["inventory.change_carfinance"]
|
|
|
|
def get_success_url(self):
|
|
return reverse("car_detail", kwargs={"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, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
|
):
|
|
"""
|
|
Represents a Django view dedicated to updating Car model instances.
|
|
|
|
This view facilitates secure and authorized updates to car-related information
|
|
through a web interface. It ensures permission checks, requires user login for
|
|
access, and communicates feedback upon successful updates.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: models.Model
|
|
:ivar form_class: The form class used for updating the model instance.
|
|
:type form_class: django.forms.ModelForm
|
|
:ivar template_name: The path to the template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar success_message: Message displayed upon successful update.
|
|
:type success_message: str
|
|
:ivar permission_required: List of permissions required to access this view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
model = models.Car
|
|
form_class = forms.CarUpdateForm
|
|
template_name = "inventory/car_edit.html"
|
|
success_message = _("Car updated successfully")
|
|
permission_required = ["inventory.change_car"]
|
|
|
|
def get_success_url(self):
|
|
return reverse("car_detail", kwargs={"pk": self.object.pk})
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
dealer = get_user_type(self.request)
|
|
print(dealer.get_vendors())
|
|
form.fields["vendor"].queryset = dealer.vendors.all()
|
|
return form
|
|
|
|
class CarDeleteView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, DeleteView
|
|
):
|
|
"""
|
|
Handles the deletion of Car objects in the inventory application.
|
|
|
|
This view ensures that only authorized users can delete a car. It requires users to be logged in
|
|
and have the appropriate permissions. Upon successful deletion, a success message is displayed,
|
|
and the user is redirected to the inventory statistics page.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: models.Car
|
|
:ivar template_name: The path to the HTML template used for confirming a car deletion.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to after successful deletion.
|
|
:type success_url: str
|
|
:ivar permission_required: A list of permission strings required to access this view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
model = models.Car
|
|
template_name = "inventory/car_confirm_delete.html"
|
|
success_url = reverse_lazy("inventory_stats")
|
|
permission_required = ["inventory.delete_car"]
|
|
|
|
def delete(self, request, *args, **kwargs):
|
|
messages.success(request, _("Car deleted successfully"))
|
|
return super().delete(request, *args, **kwargs)
|
|
|
|
|
|
class CarLocationCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
|
"""
|
|
Handles the creation of new car locations.
|
|
|
|
This view allows authenticated and authorized users to add new car locations
|
|
to the system. The form allows users to specify details regarding the car's
|
|
location. Permissions are required to ensure only authorized users are
|
|
permitted to perform this action. Upon successful form submission, the
|
|
user is redirected to the car's detail page and a success message is displayed.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: models.CarLocation
|
|
:ivar form_class: The form class used to create or update the model.
|
|
:type form_class: forms.CarLocationForm
|
|
:ivar template_name: Path to the template used by the view.
|
|
:type template_name: str
|
|
:ivar permission_required: List of permissions required to create a car location.
|
|
:type permission_required: list
|
|
"""
|
|
model = models.CarLocation
|
|
form_class = forms.CarLocationForm
|
|
template_name = "inventory/car_location_form.html"
|
|
permission_required = ["inventory.add_carlocation"]
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy("car_detail", kwargs={"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, _("Location saved successfully"))
|
|
return super().form_valid(form)
|
|
|
|
|
|
class CarLocationUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
|
"""
|
|
Provides functionality to update the location of a car.
|
|
|
|
This view is a subclass of `LoginRequiredMixin` and `PermissionRequiredMixin`, ensuring
|
|
that only authenticated and authorized users with the appropriate permissions can access it.
|
|
It provides a form to update car location details and handles the necessary updates to the
|
|
location record, associating it with the appropriate car and owner. Upon successful update,
|
|
a success message is displayed to the user, and the user is redirected to the car detail page.
|
|
|
|
:ivar model: The model associated with this view. Represents car location.
|
|
:type model: models.CarLocation
|
|
:ivar form_class: The form class used for updating car location.
|
|
:type form_class: forms.CarLocationForm
|
|
:ivar template_name: The path to the template used to render the view.
|
|
:type template_name: str
|
|
:ivar permission_required: Permissions required to access this view.
|
|
:type permission_required: list
|
|
"""
|
|
model = models.CarLocation
|
|
form_class = forms.CarLocationForm
|
|
template_name = "inventory/car_location_form.html"
|
|
permission_required = ["inventory.update_carlocation"]
|
|
|
|
# def get_initial(self):
|
|
# initial = super().get_initial()
|
|
# initial["car"] = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
|
|
# return initial
|
|
|
|
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, _("Location updated successfully"))
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy("car_detail", kwargs={"pk": self.object.car.pk})
|
|
|
|
|
|
class CarTransferCreateView(LoginRequiredMixin, CreateView):
|
|
"""
|
|
A view for creating car transfers.
|
|
|
|
This class-based view allows authenticated users to create car transfers. It handles the
|
|
selection of cars and dealers involved in the transfer process. The view inherits from
|
|
`LoginRequiredMixin` to ensure only logged-in users can access it, and `CreateView` to
|
|
simplify the implementation of the creation logic.
|
|
|
|
:ivar model: The model class associated with the view.
|
|
:type model: models.CarTransfer
|
|
:ivar form_class: The form class used to render and validate the input data.
|
|
:type form_class: forms.CarTransferForm
|
|
:ivar template_name: The path to the template used to render the view.
|
|
:type template_name: str
|
|
"""
|
|
model = models.CarTransfer
|
|
form_class = forms.CarTransferForm
|
|
template_name = "inventory/car_transfer_form.html"
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
form.fields["to_dealer"].queryset = models.Dealer.objects.exclude(
|
|
pk=get_user_type(self.request).pk
|
|
).all()
|
|
form.fields["car"].queryset = models.Car.objects.filter(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):
|
|
"""
|
|
Provides a detailed view of a specific car transfer record.
|
|
|
|
This class-based view is used to display details of a car transfer for
|
|
authenticated users. It ensures that only authorized users can access this
|
|
information and renders the data along with additional context such as
|
|
specific actions tied to the transfer. The view is associated with a Django
|
|
model for car transfers and uses a specific template to display information.
|
|
|
|
:ivar model: The model associated with this view, which represents car
|
|
transfer records.
|
|
:type model: Type[models.CarTransfer]
|
|
:ivar template_name: The path to the template used for rendering the car
|
|
transfer details.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context object used in the template
|
|
to reference the car transfer record.
|
|
:type context_object_name: str
|
|
"""
|
|
model = models.CarTransfer
|
|
template_name = "inventory/transfer_details.html"
|
|
context_object_name = "transfer"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["action"] = self.request.GET.get("action")
|
|
return context
|
|
|
|
|
|
|
|
@login_required
|
|
def car_transfer_approve(request, car_pk, transfer_pk):
|
|
"""
|
|
Approves or cancels a car transfer request based on the action parameter. This view
|
|
handles the workflow of updating the transfer status and notifying the involved parties
|
|
accordingly. If the transfer is canceled, it reverts the car status to "available" and
|
|
deactivates the transfer record. If approved, it notifies the recipient dealer and allows
|
|
the request to proceed for further actions.
|
|
|
|
:param request: The HTTP request object containing metadata and the action parameter.
|
|
:param car_pk: Primary key of the car involved in the transfer.
|
|
:param transfer_pk: Primary key of the transfer request to approve or cancel.
|
|
:return: An HTTP response redirecting to the car detail page of the specified car.
|
|
"""
|
|
car = get_object_or_404(models.Car, 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)
|
|
|
|
|
|
@login_required
|
|
def car_transfer_accept_reject(request, car_pk, transfer_pk):
|
|
"""
|
|
Handles the acceptance or rejection of a car transfer request. Based on the
|
|
`status` parameter obtained from the query string, the function updates the
|
|
transfer status to either 'accept' or 'reject'. If the transfer is accepted, it
|
|
initiates the car transfer process. Appropriate notifications are sent, and
|
|
activity records are created for both acceptance and rejection actions.
|
|
|
|
:param request: The HTTP request object which contains metadata about
|
|
the request made by the user, including session and user information.
|
|
:param car_pk: The primary key of the car to be transferred.
|
|
:param transfer_pk: The primary key of the car transfer request to be processed.
|
|
:return: An HTTP redirect response to the 'inventory_stats' view.
|
|
"""
|
|
car = get_object_or_404(models.Car, 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")
|
|
|
|
|
|
@login_required
|
|
def CarTransferPreviewView(request, car_pk, transfer_pk):
|
|
"""
|
|
Handles the preview of car transfer details and ensures that a user has appropriate
|
|
permissions to view the transfer based on their associated dealer.
|
|
|
|
This view checks if the car transfer's destination dealer matches the current user's
|
|
associated dealer type. If not, the user is redirected to the car detail page. Otherwise,
|
|
it renders the transfer preview page with the relevant transfer details.
|
|
|
|
:param request: The HTTP request object
|
|
:type request: django.http.HttpRequest
|
|
:param car_pk: The primary key of the car related to the transfer
|
|
:type car_pk: int
|
|
:param transfer_pk: The primary key of the car transfer to preview
|
|
:type transfer_pk: int
|
|
:return: An HTTP response rendering the transfer preview page or redirecting
|
|
to the car detail page
|
|
:rtype: django.http.HttpResponse
|
|
"""
|
|
transfer = get_object_or_404(models.CarTransfer, pk=transfer_pk)
|
|
if transfer.to_dealer != get_user_type(request):
|
|
return redirect("car_detail", pk=car_pk)
|
|
return render(request, "inventory/transfer_preview.html", {"transfer": transfer})
|
|
|
|
|
|
class CustomCardCreateView(LoginRequiredMixin, CreateView):
|
|
"""
|
|
Represents a view for creating a custom card associated with a specific car.
|
|
|
|
This view ensures that the user is authenticated before allowing access and
|
|
associates the created custom card with a specific car. It handles form validation,
|
|
context data injection, and determines the success URL upon successful form submission.
|
|
|
|
:ivar model: The model associated with the view, which is `CustomCard`.
|
|
:type model: models.CustomCard
|
|
:ivar form_class: The form class used to create a new instance of `CustomCard`.
|
|
:type form_class: forms.CustomCardForm
|
|
:ivar template_name: The name of the template used to render the view.
|
|
:type template_name: str
|
|
"""
|
|
model = models.CustomCard
|
|
form_class = forms.CustomCardForm
|
|
template_name = "inventory/add_custom_card.html"
|
|
|
|
def form_valid(self, form):
|
|
car = get_object_or_404(models.Car, 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):
|
|
"""
|
|
Handles the creation of new car registration records.
|
|
|
|
This view is responsible for rendering the car registration creation form,
|
|
validating input, and saving the data to the database. It ensures that the
|
|
current user is authenticated and associates the registration with a specific
|
|
car, identified by its primary key.
|
|
|
|
Inherits from:
|
|
- LoginRequiredMixin: Ensures the user is logged in to access the view.
|
|
- CreateView: Provides built-in functionality for creating and saving model
|
|
instances.
|
|
|
|
:ivar model: The model linked to this view, representing car registrations.
|
|
:type model: models.CarRegistration
|
|
:ivar form_class: The form class used for creating car registration instances.
|
|
:type form_class: forms.CarRegistrationForm
|
|
:ivar template_name: The path to the HTML template used for rendering the car
|
|
registration form.
|
|
:type template_name: str
|
|
"""
|
|
model = models.CarRegistration
|
|
form_class = forms.CarRegistrationForm
|
|
template_name = "inventory/car_registration_form.html"
|
|
|
|
def form_valid(self, form):
|
|
car = get_object_or_404(models.Car, 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):
|
|
"""
|
|
Handles car reservation requests. This view requires the user to be logged in
|
|
and processes only POST requests. When invoked, it checks if the specified car
|
|
is already reserved. If not, it proceeds to reserve the car for the user and
|
|
sends an appropriate response. If the car is already reserved or if the request
|
|
method is invalid, it provides corresponding error messages or responses.
|
|
|
|
:param request: The HTTP request object.
|
|
:type request: HttpRequest
|
|
:param car_id: The unique identifier of the car to be reserved.
|
|
:type car_id: int
|
|
:return: A response indicating the result of the reservation process.
|
|
:rtype: HttpResponse or JsonResponse
|
|
"""
|
|
if request.method == "POST":
|
|
car = get_object_or_404(models.Car, 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):
|
|
"""
|
|
Handles the management of a car reservation, providing options to renew or
|
|
cancel an existing reservation associated with the logged-in user.
|
|
|
|
Renewing a reservation extends the reservation period by an additional
|
|
24 hours. Canceling a reservation deletes it and updates the car's status
|
|
to AVAILABLE. All actions require a valid reservation and are performed
|
|
based on the current user's authentication and request type.
|
|
|
|
:param request: Django HttpRequest object representing the client's request.
|
|
:type request: HttpRequest
|
|
:param reservation_id: The unique identifier of the car reservation to manage.
|
|
:type reservation_id: int
|
|
:return: On POST requests, returns an HTTP redirect or JSON response
|
|
based on the action performed. On other request methods,
|
|
returns a JSON response with an error message.
|
|
:rtype: JsonResponse or HttpResponseRedirect
|
|
"""
|
|
reservation = get_object_or_404(
|
|
models.CarReservation, pk=reservation_id, reserved_by=request.user
|
|
)
|
|
|
|
if request.method == "POST":
|
|
action = request.POST.get("action")
|
|
if action == "renew":
|
|
reservation.reserved_until = timezone.now() + timezone.timedelta(hours=24)
|
|
reservation.save()
|
|
messages.success(request, _("Reservation renewed successfully"))
|
|
return redirect("car_detail", 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):
|
|
"""
|
|
Represents a detailed view for a Dealer model.
|
|
|
|
This class extends Django's `DetailView` to provide a detailed view of a dealer.
|
|
It includes additional context data such as the count of staff members, cars
|
|
associated with the dealer, available car makes, and dynamically fetched quotas.
|
|
The class also ensures that users must be logged in to access the detailed view.
|
|
|
|
:ivar model: The model associated with this view (Dealer model).
|
|
:type model: django.db.models.Model
|
|
:ivar template_name: Path to the template used to render the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name used to refer to the object in the template context.
|
|
:type context_object_name: str
|
|
"""
|
|
model = models.Dealer
|
|
template_name = "dealers/dealer_detail.html"
|
|
context_object_name = "dealer"
|
|
|
|
def get_queryset(self):
|
|
return models.Dealer.objects.annotate(
|
|
staff_count=Coalesce(
|
|
Count("staff"), Value(0)
|
|
) # Get the number of staff members
|
|
)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
dealer = self.object
|
|
car_makes = models.CarMake.objects.filter(car_dealers__dealer=dealer)
|
|
staff_count = dealer.staff_count
|
|
cars_count = models.Car.objects.filter(dealer=dealer).count()
|
|
|
|
context["car_makes"] = car_makes
|
|
context["staff_count"] = staff_count
|
|
context["cars_count"] = cars_count
|
|
context["allowed_users"] = dealer.user_quota
|
|
context["allowed_cars"] = dealer.car_quota
|
|
context["quota_display"] = (
|
|
f"{staff_count}/{dealer.user_quota}" if dealer.user_quota else "0"
|
|
)
|
|
|
|
return context
|
|
|
|
|
|
class DealerUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
|
"""
|
|
Handles the update functionality for the Dealer model.
|
|
|
|
This class-based view allows authenticated users to update an existing
|
|
Dealer instance using a form. Upon successful update, a success message
|
|
is displayed, and the user is redirected to the detail view of the updated
|
|
Dealer.
|
|
|
|
:ivar model: The model class associated with this view.
|
|
:type model: models.Dealer
|
|
:ivar form_class: The form class used for this view.
|
|
:type form_class: forms.DealerForm
|
|
:ivar template_name: The template used to render the form for this view.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to after a successful form submission.
|
|
:type success_url: str
|
|
:ivar success_message: The message displayed upon a successful update.
|
|
:type success_message: str
|
|
"""
|
|
model = models.Dealer
|
|
form_class = forms.DealerForm
|
|
template_name = "dealers/dealer_form.html"
|
|
success_url = reverse_lazy("dealer_detail")
|
|
success_message = _("Dealer updated successfully")
|
|
|
|
def get_success_url(self):
|
|
return reverse("dealer_detail", kwargs={"pk": self.object.pk})
|
|
|
|
|
|
class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
View for displaying a list of customers.
|
|
|
|
This class-based view is used to display a paginated and searchable list of
|
|
customers. It ensures that the user has the required permissions and is
|
|
logged in before they can access the view. The view fetches a list of
|
|
customers related to the current user's entity and applies any search
|
|
filters based on the user's query. The data is rendered using a specified
|
|
template.
|
|
|
|
:ivar model: The model associated with the view, representing customer data.
|
|
:type model: type[CustomerModel]
|
|
:ivar home_label: The label used for navigation purposes in the UI.
|
|
:type home_label: str
|
|
:ivar context_object_name: The name of the context variable in the template.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: Number of items displayed per page for pagination.
|
|
:type paginate_by: int
|
|
:ivar template_name: The path of the template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar ordering: The default ordering applied to the queryset of customers.
|
|
:type ordering: list
|
|
:ivar permission_required: A list of permissions required to access the view.
|
|
:type permission_required: list
|
|
"""
|
|
model = models.Customer
|
|
home_label = _("customers")
|
|
context_object_name = "customers"
|
|
paginate_by = 10
|
|
template_name = "customers/customer_list.html"
|
|
ordering = ["-created"]
|
|
permission_required = ["django_ledger.view_customermodel"]
|
|
|
|
def get_queryset(self):
|
|
query = self.request.GET.get("q")
|
|
dealer = get_user_type(self.request)
|
|
customers = dealer.customers.filter(active=True)
|
|
return apply_search_filters(customers, query)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["query"] = self.request.GET.get("q", "")
|
|
return context
|
|
|
|
|
|
class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
CustomerDetailView handles retrieving and presenting detailed information about
|
|
a specific customer. It ensures that the user is authenticated and has the
|
|
necessary permissions before accessing the customer's details. This view
|
|
provides context data including estimates and invoices related to the customer.
|
|
|
|
:ivar model: The model associated with the view.
|
|
:type model: CustomerModel
|
|
:ivar template_name: The path to the template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the variable in the template context
|
|
for the object being viewed.
|
|
:type context_object_name: str
|
|
:ivar permission_required: The list of permissions required to access this view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
model = models.Customer
|
|
template_name = "customers/view_customer.html"
|
|
context_object_name = "customer"
|
|
permission_required = ["django_ledger.view_customermodel"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
dealer = get_user_type(self.request)
|
|
entity = dealer.entity
|
|
context = super().get_context_data(**kwargs)
|
|
context["customer_notes"] = models.Notes.objects.filter(
|
|
object_id=self.object.pk
|
|
)
|
|
estimates = entity.get_estimates().filter(customer=self.object.customer_model)
|
|
invoices = entity.get_invoices().filter(customer=self.object.customer_model)
|
|
|
|
total = estimates.count() + invoices.count()
|
|
|
|
context["estimates"] = estimates
|
|
context["invoices"] = invoices
|
|
context["total"] = total
|
|
return context
|
|
|
|
|
|
@login_required
|
|
def add_note_to_customer(request, pk):
|
|
"""
|
|
This function allows authenticated users to add a note to a specific customer. The
|
|
note creation is handled by a form, which is validated after submission. If the form
|
|
is valid, the note is saved and associated with the specified customer. On successful
|
|
submission, the user is redirected to the customer detail page. If the request method
|
|
is not POST, an empty form is rendered.
|
|
|
|
:param request: The HTTP request object containing metadata and the method type
|
|
(e.g., GET, POST). Should be an HttpRequest instance.
|
|
:param customer_id: The unique identifier (UUID) of the customer to whom the note
|
|
is to be added.
|
|
:return: An HTTP response. In the case of a successful POST request, the function
|
|
returns a redirect response to the customer detail page. For a GET or invalid
|
|
POST request, it renders the note form template with context including
|
|
the form and customer.
|
|
"""
|
|
customer = get_object_or_404(models.Customer, pk=pk)
|
|
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}
|
|
)
|
|
|
|
|
|
@login_required
|
|
def add_activity_to_customer(request, pk):
|
|
"""
|
|
Adds an activity to a specific customer.
|
|
|
|
This function allows adding a new activity to a customer identified by their
|
|
primary key (`pk`). It retrieves the customer object, processes the form for
|
|
activity creation, and saves it. If the request method is POST, it validates
|
|
the form and associates the activity with the respective customer. Upon
|
|
successful save, it redirects to the customer detail view. If the request
|
|
method is GET, it renders a form for activity submission.
|
|
|
|
:param request: The HTTP request object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the customer to which the activity will be added.
|
|
:type pk: int
|
|
:return: An HTTP response rendered with the activity form in the context of
|
|
the customer, or a redirect response to the customer detail view upon
|
|
successful activity creation.
|
|
:rtype: HttpResponse
|
|
"""
|
|
customer = get_object_or_404(CustomerModel, pk=pk)
|
|
if request.method == "POST":
|
|
form = forms.ActivityForm(request.POST)
|
|
if form.is_valid():
|
|
activity = form.save(commit=False)
|
|
activity.content_object = customer
|
|
activity.created_by = request.user
|
|
activity.save()
|
|
return redirect("customer_detail", pk=pk)
|
|
else:
|
|
form = forms.ActivityForm()
|
|
return render(
|
|
request, "crm/add_activity.html", {"form": form, "customer": customer}
|
|
)
|
|
|
|
|
|
class CustomerCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
|
"""
|
|
# Handles the creation of a new customer within the system. This view ensures that proper permissions
|
|
# and request methods are utilized. It provides feedback to the user about the success or failure of
|
|
# the customer creation process. When the form is submitted and valid, it checks for duplicate
|
|
# customers based on the email provided before proceeding with the customer creation.
|
|
|
|
# :param request: The HTTP request object containing metadata about the request initiated by the user.
|
|
# :type request: HttpRequest
|
|
# :return: The rendered form page or a redirect to the customer list page upon successful creation.
|
|
# :rtype: HttpResponse
|
|
# :raises PermissionDenied: If the user does not have the required permissions to access the view.
|
|
# """
|
|
model = models.Customer
|
|
form_class = forms.CustomerForm
|
|
permission_required = ["django_ledger.add_customermodel"]
|
|
template_name = "customers/customer_form.html"
|
|
success_url = reverse_lazy("customer_list")
|
|
success_message = "Customer created successfully"
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
form.instance.dealer = dealer
|
|
user = form.instance.create_user_model()
|
|
customer = form.instance.create_customer_model()
|
|
|
|
form.instance.user = user
|
|
form.instance.customer_model = customer
|
|
|
|
return super().form_valid(form)
|
|
|
|
class CustomerUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
|
"""
|
|
# Updates the details of an existing customer in the database. This view is
|
|
# accessible only to logged-in users with the appropriate permissions. It
|
|
# handles both GET (form rendering with pre-filled customer data) and POST
|
|
# (submitting updates) requests. Data validation and customer updates are
|
|
# conducted based on the received form data.
|
|
|
|
# :param request: The HTTP request object used to determine the request method,
|
|
# access user session details, and provide request data such as POST content.
|
|
# Expected to contain the updated customer data if request method is POST.
|
|
# :type request: HttpRequest
|
|
|
|
# :param pk: The primary key of the CustomerModel object that is to be updated.
|
|
# :type pk: int
|
|
|
|
# :return: A rendered HTML template displaying the customer form pre-filled
|
|
# with existing data if a GET request is received. On successful form
|
|
# submission (POST request), redirects to the customer list page
|
|
# and displays a success message. In case of invalid data or errors,
|
|
# returns the rendered form template with the validation errors.
|
|
# :rtype: HttpResponse
|
|
# """
|
|
model = models.Customer
|
|
form_class = forms.CustomerForm
|
|
permission_required = ["django_ledger.change_customermodel"]
|
|
template_name = "customers/customer_form.html"
|
|
success_url = reverse_lazy("customer_list")
|
|
success_message = "Customer updated successfully"
|
|
|
|
def form_valid(self, form):
|
|
form.instance.update_user_model()
|
|
form.instance.update_customer_model()
|
|
return super().form_valid(form)
|
|
|
|
|
|
@login_required
|
|
def delete_customer(request, pk):
|
|
"""
|
|
Deletes a customer from the system and deactivates the corresponding user account.
|
|
|
|
This function retrieves a customer object based on the primary key (pk),
|
|
sets their active status to False, and deactivates the linked user account
|
|
(using the email associated with the customer). After saving these changes,
|
|
it displays a success message to the user and redirects to the customer list page.
|
|
|
|
:param request: A HttpRequest object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: Primary key of the customer to be deleted.
|
|
:type pk: int
|
|
:return: A redirect response to the customer list page.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
customer = get_object_or_404(models.Customer, pk=pk)
|
|
customer.deactivate_account()
|
|
messages.success(request, _("Customer deactivated successfully"))
|
|
return redirect("customer_list")
|
|
|
|
|
|
class VendorListView(LoginRequiredMixin, ListView):
|
|
"""
|
|
Represents a view for displaying a paginated list of vendors, accessible only
|
|
to authenticated users.
|
|
|
|
This class inherits from ``LoginRequiredMixin`` and ``ListView`` and is
|
|
designed to display a paginated list of vendors associated with a user. It
|
|
utilizes filters and search capabilities based on user input and is rendered
|
|
using a specified template. The list is ordered by the creation date of the
|
|
vendors in descending order.
|
|
|
|
:ivar model: The model that this view interacts with.
|
|
:type model: VendorModel
|
|
:ivar context_object_name: The name of the context variable used in the
|
|
template for the list of vendors.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of vendors to display per page in the paginated
|
|
view.
|
|
:type paginate_by: int
|
|
:ivar template_name: The path to the template used to render the list of
|
|
vendors.
|
|
:type template_name: str
|
|
:ivar ordering: The default ordering applied to the queryset. Vendors are
|
|
ordered by their creation date in descending order.
|
|
:type ordering: list
|
|
"""
|
|
model = models.Vendor
|
|
context_object_name = "vendors"
|
|
paginate_by = 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)
|
|
|
|
|
|
@login_required
|
|
def vendorDetailView(request, pk):
|
|
"""
|
|
Fetches and renders the detail view for a specific vendor.
|
|
|
|
This function retrieves a vendor object based on the primary key (pk)
|
|
provided in the URL, ensures the user is logged in to access the
|
|
view, and renders the vendor detail template with the vendor's context.
|
|
|
|
:param request: The HTTP request object.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the vendor to retrieve.
|
|
:type pk: int
|
|
:return: An HttpResponse object containing the rendered vendor detail page.
|
|
:rtype: HttpResponse
|
|
"""
|
|
vendor = get_object_or_404(models.Vendor, pk=pk)
|
|
return render(
|
|
request, template_name="vendors/view_vendor.html", context={"vendor": vendor}
|
|
)
|
|
|
|
|
|
class VendorCreateView(
|
|
LoginRequiredMixin,
|
|
SuccessMessageMixin,
|
|
CreateView,
|
|
):
|
|
"""
|
|
Handles the creation of a new vendor.
|
|
|
|
This view allows authenticated users to create a new vendor entry. It uses the
|
|
Vendor model and its corresponding form for creation. A success message is displayed
|
|
upon successful creation, and the user is redirected to the list of vendors.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: models.Vendor
|
|
:ivar form_class: The form class used for creating a vendor.
|
|
:type form_class: forms.VendorForm
|
|
:ivar template_name: The name of the template used to render the view.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to after successful vendor creation.
|
|
:type success_url: str
|
|
:ivar success_message: The message displayed upon successful creation.
|
|
:type success_message: str
|
|
"""
|
|
model = models.Vendor
|
|
form_class = forms.VendorForm
|
|
template_name = "vendors/vendor_form.html"
|
|
success_url = reverse_lazy("vendor_list")
|
|
success_message = _("Vendor created successfully")
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
form.instance.dealer = dealer
|
|
form.instance.save()
|
|
return super().form_valid(form)
|
|
|
|
|
|
class VendorUpdateView(
|
|
LoginRequiredMixin,
|
|
SuccessMessageMixin,
|
|
UpdateView,
|
|
):
|
|
"""
|
|
View for updating vendor information.
|
|
|
|
This class-based view is used to handle the update of vendor information.
|
|
It ensures that only authenticated users can access this page, provides
|
|
form handling functionality, and includes a success message upon completion.
|
|
The form fields are dynamically populated and validated before vendor
|
|
information is updated in the database.
|
|
|
|
:ivar model: The model that this view is based on.
|
|
:type model: models.Vendor
|
|
:ivar form_class: The form class used to validate and process vendor data.
|
|
:type form_class: forms.VendorForm
|
|
:ivar template_name: The path to the HTML template used to render this view.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to after successful data submission.
|
|
:type success_url: str
|
|
:ivar success_message: The message to display upon successful data update.
|
|
:type success_message: str
|
|
"""
|
|
model = models.Vendor
|
|
form_class = forms.VendorForm
|
|
template_name = "vendors/vendor_form.html"
|
|
success_url = reverse_lazy("vendor_list")
|
|
success_message = _("Vendor updated successfully")
|
|
|
|
# def get_initial(self):
|
|
# initial = super().get_initial()
|
|
# initial = self.object.additional_info
|
|
# return initial
|
|
|
|
def form_valid(self, form):
|
|
# instance = form.save(commit=False)
|
|
print(self.request.POST)
|
|
# instance.vendor_name = self.request.POST["name"]
|
|
# instance.vendor_number = self.request.POST["crn"]
|
|
# instance.address_1 = self.request.POST["address"]
|
|
# instance.phone = self.request.POST["phone_number"]
|
|
# instance.email = self.request.POST["email"]
|
|
# instance.tax_id_number = self.request.POST["vrn"]
|
|
# additionals = form.cleaned_data
|
|
# additionals["phone_number"] = str(additionals["phone_number"])
|
|
# instance.additional_info = additionals
|
|
# instance.save()
|
|
return super().form_valid(form)
|
|
|
|
|
|
@login_required
|
|
def delete_vendor(request, pk):
|
|
"""
|
|
Deletes an existing vendor record from the database.
|
|
|
|
This function allows users with valid authentication to delete a specified
|
|
vendor object by its primary key. Upon successful deletion, a success message
|
|
is displayed, and the user is redirected to the vendor list page.
|
|
|
|
:param request: HttpRequest object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: Primary key of the vendor object to be deleted.
|
|
:type pk: int
|
|
:return: HttpResponseRedirect object for redirecting to the vendor list page.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
vendor = get_object_or_404(models.Vendor, pk=pk)
|
|
vendor.active = False
|
|
vendor.vendor_model.active = False
|
|
vendor.save()
|
|
messages.success(request, _("Vendor deleted successfully"))
|
|
return redirect("vendor_list")
|
|
|
|
|
|
# group
|
|
class GroupListView(LoginRequiredMixin, ListView):
|
|
"""
|
|
Represents a view for listing groups for a logged-in user.
|
|
|
|
This view is designed for authenticated users, inheriting functionalities
|
|
from `LoginRequiredMixin` to enforce authentication checks and `ListView`
|
|
to handle the display of a list of groups. It queries the groups related
|
|
to the user type (dealer) and displays the results in a paginated format
|
|
using a specified template.
|
|
|
|
:ivar model: The model used for retrieving group data.
|
|
:type model: type
|
|
:ivar context_object_name: The name of the context variable used to contain
|
|
the queryset of groups.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of groups listed on each page.
|
|
:type paginate_by: int
|
|
:ivar template_name: The path to the template used for rendering the group list.
|
|
:type template_name: str
|
|
"""
|
|
model = models.CustomGroup
|
|
context_object_name = "groups"
|
|
paginate_by = 10
|
|
template_name = "groups/group_list.html"
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
return dealer.groups.all()
|
|
|
|
|
|
class GroupDetailView(LoginRequiredMixin, DetailView):
|
|
"""
|
|
Represents the detail view for a specific group.
|
|
|
|
Handles the display of detailed information about a specific CustomGroup
|
|
instance. Requires the user to be logged in to access this view. The class
|
|
is designed to fetch a specific group instance and render its details using
|
|
the specified template.
|
|
|
|
:ivar model: The model that represents the group details being viewed.
|
|
:type model: models.CustomGroup
|
|
:ivar template_name: The name of the template used to render the group detail
|
|
view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The context variable name under which the group
|
|
instance will be available in the template.
|
|
:type context_object_name: str
|
|
"""
|
|
model = models.CustomGroup
|
|
template_name = "groups/group_detail.html"
|
|
context_object_name = "group"
|
|
|
|
|
|
class GroupCreateView(
|
|
LoginRequiredMixin,
|
|
SuccessMessageMixin,
|
|
CreateView,
|
|
):
|
|
"""
|
|
Represents a view for creating a new group in the application.
|
|
|
|
This class provides a form-based interface for authenticated users to create
|
|
new group entities. It utilizes mixins to enforce login requirements and display
|
|
success messages upon successful group creation.
|
|
|
|
:ivar model: The model associated with the view.
|
|
:type model: models.CustomGroup
|
|
:ivar form_class: The form class used to create a new group.
|
|
:type form_class: forms.GroupForm
|
|
:ivar template_name: The template used to render the form for creating a group.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to be redirected to when the group creation is successful.
|
|
:type success_url: str
|
|
:ivar success_message: A message displayed upon successful creation of a group.
|
|
:type success_message: str
|
|
"""
|
|
model = models.CustomGroup
|
|
form_class = forms.GroupForm
|
|
template_name = "groups/group_form.html"
|
|
success_url = reverse_lazy("group_list")
|
|
success_message = _("Group created successfully")
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
instance = form.save(commit=False)
|
|
group = Group.objects.create(name=f"{dealer.pk}_{instance.name}")
|
|
instance.dealer = dealer
|
|
instance.group = group
|
|
instance.save()
|
|
return super().form_valid(form)
|
|
|
|
|
|
class GroupUpdateView(
|
|
LoginRequiredMixin,
|
|
SuccessMessageMixin,
|
|
UpdateView,
|
|
):
|
|
"""
|
|
Handles the update of group objects with permission control and custom behavior.
|
|
|
|
This view allows users with login credentials to update existing group
|
|
objects. It ensures that changes made to groups adhere to default
|
|
permissions and adds necessary naming conventions based on user type.
|
|
Upon successful update, it redirects the user to the group list view and
|
|
displays a success message.
|
|
|
|
:ivar model: Specifies the model to be used for the update operation.
|
|
:type model: models.CustomGroup
|
|
:ivar form_class: Specifies the form class to be used for validation
|
|
and data manipulation.
|
|
:type form_class: forms.GroupForm
|
|
:ivar template_name: File path to the template that renders the form view.
|
|
:type template_name: str
|
|
:ivar success_url: URL to redirect upon successful form submission.
|
|
:type success_url: str
|
|
:ivar success_message: Message displayed upon successful update of a group.
|
|
:type success_message: str
|
|
"""
|
|
model = models.CustomGroup
|
|
form_class = forms.GroupForm
|
|
template_name = "groups/group_form.html"
|
|
success_url = reverse_lazy("group_list")
|
|
success_message = _("Group updated successfully")
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
instance = form.save(commit=False)
|
|
instance.set_defualt_permissions()
|
|
instance.group.name = f"{dealer.pk}_{instance.name}"
|
|
instance.save()
|
|
return super().form_valid(form)
|
|
|
|
|
|
@login_required
|
|
def GroupDeleteview(request, pk):
|
|
"""
|
|
Handles the deletion of a specific group instance. This view ensures that only
|
|
authenticated users can perform the deletion. Upon successful deletion, a
|
|
success message is displayed, and the user is redirected to the group list page.
|
|
|
|
:param request: The HTTP request object that contains metadata about the
|
|
request context and user information. Must be an authenticated user.
|
|
:param pk: The primary key of the group instance to be deleted.
|
|
It specifies which group to retrieve and delete.
|
|
:return: The HTTP response that redirects the user to the group list page
|
|
after the group is successfully deleted.
|
|
"""
|
|
group = get_object_or_404(models.CustomGroup, pk=pk)
|
|
group.delete()
|
|
messages.success(request, _("Group deleted successfully"))
|
|
return redirect("group_list")
|
|
|
|
|
|
@login_required
|
|
def GroupPermissionView(request, pk):
|
|
"""
|
|
Handles the view for adding or modifying permissions of a specific group. This view
|
|
fetches the group based on the primary key passed as a parameter, and either displays
|
|
a form for editing permissions or processes the submitted permissions.
|
|
|
|
If the request method is POST, the permissions of the group are cleared and updated
|
|
based on the submitted data. A success message is displayed upon completion, and
|
|
the user is redirected to the group's detail page.
|
|
|
|
In case of a GET request, the view renders the form pre-filled with the group's
|
|
current permissions.
|
|
|
|
:param request: The HTTP request object.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the group whose permissions are being modified.
|
|
:type pk: int
|
|
:return: The HTTP response depending on the request type. For GET requests, renders
|
|
the permission form for the specified group. For POST requests, clears and updates
|
|
the group's permissions and redirects to the group's detail page.
|
|
:rtype: HttpResponse
|
|
"""
|
|
group = get_object_or_404(models.CustomGroup, pk=pk)
|
|
if request.method == "POST":
|
|
form = forms.PermissionForm(request.POST)
|
|
group.clear_permissions()
|
|
permissions = request.POST.getlist("name")
|
|
for i in permissions:
|
|
group.add_permission(Permission.objects.get(id=int(i)))
|
|
messages.success(request, _("Permission added successfully"))
|
|
return redirect("group_detail", pk=group.pk)
|
|
form = forms.PermissionForm(initial={"name": group.permissions})
|
|
return render(
|
|
request, "groups/group_permission_form.html", {"group": group, "form": form}
|
|
)
|
|
|
|
|
|
# Users
|
|
@login_required
|
|
def UserGroupView(request, pk):
|
|
"""
|
|
Handles the assignment of user groups to a specific staff member. This view
|
|
allows updating the groups a staff member belongs to via a form submission.
|
|
It processes both GET and POST requests, ensuring appropriate group
|
|
assignments are managed and feedback is provided to the user via messages.
|
|
|
|
:param request: HttpRequest object representing the HTTP request.
|
|
:type request: HttpRequest
|
|
:param pk: Primary key of the staff member whose groups are being updated.
|
|
:type pk: int
|
|
|
|
:return: Renders the user group form for GET requests or redirects to the
|
|
user detail page after successful submission for POST requests.
|
|
:rtype: HttpResponse or HttpResponseRedirect
|
|
"""
|
|
staff = get_object_or_404(models.Staff, 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):
|
|
"""
|
|
Represents a view for listing users with pagination and filters.
|
|
|
|
This class is designed to display a list view of staff users for a specific
|
|
dealer. It supports pagination and search filtering functionality. This view
|
|
requires the user to be logged in, as it inherits from `LoginRequiredMixin`.
|
|
|
|
:ivar model: The model that the view will work with.
|
|
:type model: Type[models.Staff]
|
|
:ivar context_object_name: The name of the context variable that will contain
|
|
the list of users.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of items to display per page.
|
|
:type paginate_by: int
|
|
:ivar template_name: The path to the template used for rendering the view's
|
|
page.
|
|
:type template_name: str
|
|
"""
|
|
model = models.Staff
|
|
context_object_name = "users"
|
|
paginate_by = 10
|
|
template_name = "users/user_list.html"
|
|
|
|
def get_queryset(self):
|
|
query = self.request.GET.get("q")
|
|
dealer = get_user_type(self.request)
|
|
staff = models.Staff.objects.filter(dealer=dealer).all()
|
|
return apply_search_filters(staff, query)
|
|
|
|
|
|
class UserDetailView(LoginRequiredMixin, DetailView):
|
|
"""
|
|
Represents a detailed view for displaying user-specific information.
|
|
|
|
This class-based view is used to display detailed information for
|
|
a specific user. It ensures that only logged-in users can access
|
|
this view, leveraging the ``LoginRequiredMixin`` for authentication.
|
|
The data for the view comes from the ``models.Staff`` model and
|
|
is rendered using the specified HTML template.
|
|
|
|
:ivar model: The data model associated with this view.
|
|
:type model: models.Staff
|
|
:ivar template_name: Path to the HTML template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: Name of the context variable available in the template.
|
|
:type context_object_name: str
|
|
"""
|
|
model = models.Staff
|
|
template_name = "users/user_detail.html"
|
|
context_object_name = "user_"
|
|
|
|
|
|
class UserCreateView(
|
|
LoginRequiredMixin,
|
|
SuccessMessageMixin,
|
|
CreateView,
|
|
):
|
|
"""
|
|
Manages the creation of a new staff user and their associated details.
|
|
|
|
This view is responsible for handling the creation of a new staff user in the
|
|
system. It ensures quota limits are adhered to, validates form input, saves user
|
|
details, and associates them with services and permissions.
|
|
|
|
:ivar model: The database model associated with this view.
|
|
:type model: django.db.models.Model
|
|
|
|
:ivar form_class: The form class used to validate and create a staff user.
|
|
:type form_class: type
|
|
|
|
:ivar template_name: The template used to render the user creation form.
|
|
:type template_name: str
|
|
|
|
:ivar success_url: The URL to redirect to after successfully creating a user.
|
|
:type success_url: str
|
|
|
|
:ivar success_message: The success message displayed upon user creation.
|
|
:type success_message: str
|
|
"""
|
|
model = models.Staff
|
|
form_class = forms.StaffForm
|
|
template_name = "users/user_form.html"
|
|
success_url = reverse_lazy("user_list")
|
|
success_message = _("User created successfully")
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
# quota_dict = get_user_quota(dealer.user)
|
|
# allowed_users = quota_dict.get("Users")
|
|
|
|
# if allowed_users is None:
|
|
# messages.error(self.request, _("The user quota for staff members is not defined. Please contact support"))
|
|
# return self.form_invalid(form)
|
|
|
|
if dealer.is_staff_exceed_quota_limit:
|
|
messages.error(self.request, _("You have reached the maximum number of staff users allowed for your plan"))
|
|
return self.form_invalid(form)
|
|
|
|
email = form.cleaned_data["email"]
|
|
password = "Tenhal@123"
|
|
|
|
user = User.objects.create_user(
|
|
username=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
|
|
staff.add_as_manager()
|
|
group = Group.objects.filter(customgroup__name__iexact=staff.staff_type).first()
|
|
staff.save()
|
|
if group:
|
|
staff.add_group(group)
|
|
return super().form_valid(form)
|
|
|
|
|
|
class UserUpdateView(
|
|
LoginRequiredMixin,
|
|
SuccessMessageMixin,
|
|
UpdateView,
|
|
):
|
|
"""
|
|
UserUpdateView updates information for a user with specific details.
|
|
|
|
This view handles updating user details such as email, name, phone number, and services
|
|
offered. It leverages Django's `UpdateView` to manage the update operation. The view
|
|
disables editing of email addresses, initializes specific fields with data associated
|
|
with the user, and processes input to manage related services. Validation of the form and
|
|
saving changes are customized to align with specific business logic.
|
|
|
|
:ivar model: The model class associated with the view, used for retrieving the object to update.
|
|
:type model: models.Staff
|
|
:ivar form_class: The form class used to render and process the form for updating the user.
|
|
:type form_class: forms.StaffForm
|
|
:ivar template_name: The template used for rendering the form in the UI.
|
|
:type template_name: str
|
|
:ivar success_url: URL to redirect to after a successful form submission.
|
|
:type success_url: str
|
|
:ivar success_message: Message displayed to the user after a successful update.
|
|
:type success_message: str
|
|
"""
|
|
model = models.Staff
|
|
form_class = forms.StaffForm
|
|
template_name = "users/user_form.html"
|
|
success_url = reverse_lazy("user_list")
|
|
success_message = _("User updated successfully")
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs["instance"] = self.get_object() # Pass the Staff instance to the form
|
|
return kwargs
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
form.fields["email"].disabled = True
|
|
return form
|
|
|
|
def get_initial(self):
|
|
initial = super().get_initial()
|
|
initial["email"] = self.object.staff_member.user.email
|
|
initial["service_offered"] = self.object.staff_member.services_offered.all()
|
|
return initial
|
|
|
|
def form_valid(self, form):
|
|
services = form.cleaned_data["service_offered"]
|
|
if not services:
|
|
self.object.staff_member.services_offered.clear()
|
|
else:
|
|
for service in services:
|
|
self.object.staff_member.services_offered.add(service)
|
|
|
|
staff = form.save(commit=False)
|
|
staff.name = form.cleaned_data["name"]
|
|
staff.arabic_name = form.cleaned_data["arabic_name"]
|
|
staff.phone_number = form.cleaned_data["phone_number"]
|
|
staff.staff_type = form.cleaned_data["staff_type"]
|
|
|
|
staff.add_as_manager()
|
|
staff.save()
|
|
return super().form_valid(form)
|
|
|
|
|
|
@login_required
|
|
def UserDeleteview(request, pk):
|
|
"""
|
|
Deletes a user and its associated staff member from the database and redirects
|
|
to the user list page. Displays a success message upon successful deletion
|
|
of the user.
|
|
|
|
:param request: The HTTP request object representing the incoming request.
|
|
:param pk: The primary key (ID) of the staff member to be deleted.
|
|
:return: An HTTP redirect to the user list page.
|
|
|
|
"""
|
|
staff = get_object_or_404(models.Staff, pk=pk)
|
|
staff.staff_member.delete()
|
|
staff.delete()
|
|
messages.success(request, _("User deleted successfully"))
|
|
return redirect("user_list")
|
|
|
|
|
|
class OrganizationListView(LoginRequiredMixin, ListView):
|
|
"""
|
|
Represents a view to display a paginated list of organizations for a dealer.
|
|
|
|
This class inherits from `LoginRequiredMixin` to ensure that only
|
|
authenticated users can access the list, and from `ListView` to provide
|
|
a generic implementation to render lists of database objects. It is designed
|
|
specifically to show organizations related to a dealer entity and includes
|
|
search functionality based on a query parameter.
|
|
|
|
:ivar model: Specifies the model to fetch data from.
|
|
:type model: type[CustomerModel]
|
|
:ivar template_name: The template used to render the organization list page.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable for the organization list.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of organizations displayed per page.
|
|
:type paginate_by: int
|
|
"""
|
|
model = models.Organization
|
|
template_name = "organizations/organization_list.html"
|
|
context_object_name = "organizations"
|
|
paginate_by = 10
|
|
|
|
def get_queryset(self):
|
|
query = self.request.GET.get("q")
|
|
dealer = get_user_type(self.request)
|
|
organization = dealer.organizations.filter(active=True)
|
|
|
|
return apply_search_filters(organization, query)
|
|
|
|
|
|
class OrganizationDetailView(LoginRequiredMixin, DetailView):
|
|
"""
|
|
Handles displaying detailed information about an organization.
|
|
|
|
This view is a detailed representation of an individual organization, which
|
|
provides the necessary data for templates to render the information. It is
|
|
intended to be used for displaying details of a `CustomerModel` instance.
|
|
Requires the user to be logged in to access this view.
|
|
|
|
:ivar model: Specifies the model that this view will interact with.
|
|
:type model: type[CustomerModel]
|
|
:ivar template_name: The name of the template to be used for rendering the
|
|
organization's detail information.
|
|
:type template_name: str
|
|
:ivar context_object_name: The context variable name to be used in the
|
|
template for accessing the organization's data.
|
|
:type context_object_name: str
|
|
"""
|
|
model = models.Organization
|
|
template_name = "organizations/organization_detail.html"
|
|
context_object_name = "organization"
|
|
|
|
|
|
class OrganizationCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
|
"""
|
|
# Handles the creation of a new organization via a web form. This view allows the
|
|
# authenticated user to submit data for creating an organization. If a POST request
|
|
# is received, it validates the data, checks for duplicate organizations, and
|
|
# creates a customer linked to the organization, including its associated
|
|
# information such as address, phone number, and logo. Upon success, the user
|
|
# is redirected to the organization list, and a success message is displayed.
|
|
|
|
# :param request: The HTTP request object containing data for creating an organization.
|
|
# :type request: HttpRequest
|
|
# :return: An HTTP response object rendering the organization create form page or
|
|
# redirecting the user after a successful creation.
|
|
# :rtype: HttpResponse
|
|
# """
|
|
model = models.Organization
|
|
form_class = forms.OrganizationForm
|
|
permission_required = ["django_ledger.add_customermodel"]
|
|
template_name = "organizations/organization_form.html"
|
|
success_url = reverse_lazy("organization_list")
|
|
success_message = "Organization created successfully"
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
form.instance.dealer = dealer
|
|
user = form.instance.create_user_model()
|
|
customer = form.instance.create_customer_model()
|
|
form.instance.user = user
|
|
form.instance.customer_model = customer
|
|
|
|
return super().form_valid(form)
|
|
|
|
class OrganizationUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
|
"""
|
|
# Handles the update of an organization instance. This view fetches the organization
|
|
# based on the provided primary key (pk) and renders a form for editing the
|
|
# organization attributes. When a POST request is made, this view validates and
|
|
# processes the form data, updates the organization instance, and saves the changes.
|
|
# If the request method is not POST, it initializes the form with existing organization
|
|
# data for rendering.
|
|
|
|
# :param request: The HTTP request object. Must be authenticated via login.
|
|
# :type request: HttpRequest
|
|
# :param pk: The primary key of the organization to be updated.
|
|
# :type pk: int
|
|
# :return: An HTTP response object. Either renders the organization form or redirects
|
|
# to the organization list upon successful update.
|
|
# :rtype: HttpResponse
|
|
# """
|
|
model = models.Organization
|
|
form_class = forms.OrganizationForm
|
|
permission_required = ["django_ledger.change_customermodel"]
|
|
template_name = "organizations/organization_form.html"
|
|
success_url = reverse_lazy("organization_list")
|
|
success_message = "Organization updated successfully"
|
|
|
|
def form_valid(self, form):
|
|
form.instance.update_user_model()
|
|
form.instance.update_customer_model()
|
|
return super().form_valid(form)
|
|
|
|
@login_required
|
|
def OrganizationDeleteView(request, pk):
|
|
"""
|
|
Handles the deletion of an organization based on the provided primary key (pk). Looks up
|
|
the organization and its corresponding user by email, attempts to delete both, and provides
|
|
appropriate success or error feedback to the user. In case of failure, an error message is shown,
|
|
while successful deletion redirects to the organization list.
|
|
|
|
:param request: The HTTP request object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the organization to be deleted.
|
|
:type pk: int
|
|
:return: An HTTP response redirecting to the organization list view.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
organization = get_object_or_404(models.Organization, pk=pk)
|
|
organization.deactivate_account()
|
|
messages.success(request, _("Organization Deactivated successfully"))
|
|
return redirect("organization_list")
|
|
|
|
|
|
class RepresentativeListView(LoginRequiredMixin, ListView):
|
|
"""
|
|
Represents a view for displaying a paginated list of representatives.
|
|
|
|
This view handles the functionality of displaying and paginating a list
|
|
of representatives for the logged-in user. It utilizes search filters to
|
|
allow querying representatives based on the search term provided in the
|
|
request. The view restricts access to logged-in users only.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: models.Representative
|
|
:ivar template_name: Name of the template used to render the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: Name of the context variable used to access
|
|
representatives in the template.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of representatives displayed per page.
|
|
:type paginate_by: int
|
|
"""
|
|
model = models.Representative
|
|
template_name = "representatives/representative_list.html"
|
|
context_object_name = "representatives"
|
|
paginate_by = 10
|
|
|
|
def get_queryset(self):
|
|
query = self.request.GET.get("q")
|
|
dealer = get_user_type(self.request)
|
|
representative = models.Representative.objects.filter(dealer=dealer)
|
|
return apply_search_filters(representative, query)
|
|
|
|
|
|
class RepresentativeDetailView(LoginRequiredMixin, DetailView):
|
|
"""
|
|
Represents a detailed view for Representative instances.
|
|
|
|
This class-based view is used to provide detailed representation for
|
|
``Representative`` model instances. It ensures that only authenticated
|
|
users can access the view by utilizing ``LoginRequiredMixin``. The
|
|
template used to render the view and the context name for the object
|
|
are also specified for use in a Django template.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: Type of the model (models.Representative)
|
|
:ivar template_name: The path to the template used to render this view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable containing
|
|
the object.
|
|
:type context_object_name: str
|
|
"""
|
|
model = models.Representative
|
|
template_name = "representatives/representative_detail.html"
|
|
context_object_name = "representative"
|
|
|
|
|
|
class RepresentativeCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
|
|
"""
|
|
Handles the creation of a Representative object.
|
|
|
|
This class is a view that provides the interface and functionality to create
|
|
a new representative in the application. It is designed to ensure that only
|
|
authenticated users with a valid dealer association can create representatives.
|
|
A success message is displayed upon successful creation of a representative.
|
|
|
|
:ivar model: The model that this view will work with, which is Representative.
|
|
:type model: django.db.models.Model
|
|
:ivar form_class: The form class used for creating a representative.
|
|
:type form_class: django.forms.ModelForm
|
|
:ivar template_name: The name of the template used to render the create view.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to upon successful form submission.
|
|
:type success_url: str
|
|
:ivar success_message: The success message displayed after creating a representative.
|
|
:type success_message: str
|
|
"""
|
|
model = models.Representative
|
|
form_class = forms.RepresentativeForm
|
|
template_name = "representatives/representative_form.html"
|
|
success_url = reverse_lazy("representative_list")
|
|
success_message = _("Representative created successfully")
|
|
|
|
def form_valid(self, form):
|
|
if form.is_valid():
|
|
form.instance.dealer = self.request.user.dealer
|
|
form.save()
|
|
return super().form_valid(form)
|
|
else:
|
|
return form.errors
|
|
|
|
|
|
class RepresentativeUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
|
"""
|
|
Provides functionality for updating a representative's details.
|
|
|
|
This class-based view allows authenticated users, equipped with
|
|
the required permissions, to update information for a specific
|
|
representative. It uses a predefined form for input, renders the
|
|
appropriate update template, and provides success messaging upon
|
|
successful completion. It also redirects to a predefined success
|
|
URL once the update operation is complete.
|
|
|
|
:ivar model: The model representing a representative that is being
|
|
updated.
|
|
:type model: Type[models.Representative]
|
|
:ivar form_class: The form class used for providing input fields to
|
|
update representative details.
|
|
:type form_class: Type[forms.RepresentativeForm]
|
|
:ivar template_name: The template used to render the representative
|
|
update page.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to which the user is redirected following
|
|
a successful update.
|
|
:type success_url: str
|
|
:ivar success_message: The message displayed upon a successful update
|
|
operation.
|
|
:type success_message: str
|
|
"""
|
|
model = models.Representative
|
|
form_class = forms.RepresentativeForm
|
|
template_name = "representatives/representative_form.html"
|
|
success_url = reverse_lazy("representative_list")
|
|
success_message = _("Representative updated successfully")
|
|
|
|
|
|
class RepresentativeDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
|
|
"""
|
|
Handles the deletion of a representative.
|
|
|
|
This view provides functionality to delete a representative from the system.
|
|
It ensures that only authenticated users can perform the deletion and displays
|
|
a success message upon successful deletion. The deletion is confirmed via a
|
|
template, and upon success, the user is redirected to the representative list page.
|
|
|
|
:ivar model: The model representing the representative.
|
|
:type model: models.Representative
|
|
:ivar template_name: The template used to confirm the deletion of a representative.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to after successful deletion.
|
|
:type success_url: str
|
|
:ivar success_message: The success message displayed after a representative is deleted.
|
|
:type success_message: str
|
|
"""
|
|
model = models.Representative
|
|
template_name = "representatives/representative_confirm_delete.html"
|
|
success_url = reverse_lazy("representative_list")
|
|
success_message = _("Representative deleted successfully")
|
|
|
|
|
|
# BANK ACCOUNT
|
|
class BankAccountListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Provides a view for listing bank accounts associated with a specific entity
|
|
and applying search filters if provided. Ensures user authentication and
|
|
required permissions are implemented.
|
|
|
|
This view is used to display the list of bank accounts in a paginated format.
|
|
It filters the list of bank accounts based on the associated entity of the
|
|
dealer (user type) and applies search filters when needed. It requires the
|
|
user to be logged in and have the specified permissions to view the resource.
|
|
|
|
:ivar model: The model to fetch data for the bank account listing.
|
|
:type model: type[BankAccountModel]
|
|
:ivar template_name: The template used to render the bank account list.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable containing the
|
|
list of bank accounts.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of records displayed per page in pagination.
|
|
:type paginate_by: int
|
|
:ivar permission_required: The required permissions to access the view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
model = BankAccountModel
|
|
template_name = "ledger/bank_accounts/bank_account_list.html"
|
|
context_object_name = "bank_accounts"
|
|
paginate_by = 10
|
|
permission_required = ["inventory.view_carfinance"]
|
|
|
|
def get_queryset(self):
|
|
query = self.request.GET.get("q")
|
|
dealer = get_user_type(self.request)
|
|
bank_accounts = BankAccountModel.objects.filter(entity_model=dealer.entity)
|
|
return apply_search_filters(bank_accounts, query)
|
|
|
|
|
|
class BankAccountCreateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView
|
|
):
|
|
"""
|
|
Represents a view for creating a new bank account.
|
|
|
|
This class is a Django CreateView that handles the creation of a new bank account
|
|
within the system. It integrates functionalities for login requirements, permission
|
|
enforcement, and success messages upon successful creation. The view is initialized
|
|
with a specific model, form class, and template to render the form. A success message
|
|
and redirection URL are specified for after the account creation. Additionally, it
|
|
checks permissions required to access this view.
|
|
|
|
:ivar model: The model to be used for the creation of a bank account.
|
|
:type model: BankAccountModel
|
|
:ivar form_class: The form class to be used for validating and handling bank account
|
|
creation.
|
|
:type form_class: BankAccountCreateForm
|
|
:ivar template_name: The template to render the form for bank account creation.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to after successful bank account creation.
|
|
:type success_url: str
|
|
:ivar success_message: The success message to display upon successful creation.
|
|
:type success_message: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list
|
|
"""
|
|
model = BankAccountModel
|
|
form_class = BankAccountCreateForm
|
|
template_name = "ledger/bank_accounts/bank_account_form.html"
|
|
success_url = reverse_lazy("bank_account_list")
|
|
success_message = _("Bank account created successfully")
|
|
permission_required = ['inventory.view_carfinance']
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
form.instance.entity_model = dealer.entity
|
|
return super().form_valid(form)
|
|
|
|
def get_form_kwargs(self):
|
|
dealer = get_user_type(self.request)
|
|
entity = dealer.entity
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs["entity_slug"] = entity.slug
|
|
kwargs["user_model"] = entity.admin
|
|
return kwargs
|
|
|
|
|
|
class BankAccountDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Manages the detailed view of a bank account.
|
|
|
|
Provides a detailed view for a specific bank account by leveraging Django's
|
|
DetailView. This class ensures that only authenticated users with the relevant
|
|
permissions can access the view. It is tailored for displaying necessary
|
|
details about the bank account.
|
|
|
|
:ivar model: The model associated with the view.
|
|
:type model: type[BankAccountModel]
|
|
:ivar template_name: Path to the HTML template used to render the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable used to represent
|
|
the specific bank account instance in the template.
|
|
:type context_object_name: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
model = BankAccountModel
|
|
template_name = "ledger/bank_accounts/bank_account_detail.html"
|
|
context_object_name = "bank_account"
|
|
permission_required = ["inventory.view_carfinance"]
|
|
|
|
|
|
class BankAccountUpdateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
|
):
|
|
"""
|
|
Represents a view for updating bank account details in the system.
|
|
|
|
This class is responsible for providing functionality to update existing
|
|
bank account information within the system. It ensures that only logged-in
|
|
users with the appropriate permissions can access and update the data. The
|
|
view also provides user feedback regarding successful updates using a
|
|
success message and redirects to the bank account list upon completion.
|
|
|
|
:ivar model: Defines the model associated with the view.
|
|
:type model: BankAccountModel
|
|
:ivar form_class: Specifies the form class used for updating bank account information.
|
|
:type form_class: BankAccountUpdateForm
|
|
:ivar template_name: Path to the template used for rendering the update form.
|
|
:type template_name: str
|
|
:ivar success_url: URL to redirect to upon successful update of a bank account.
|
|
:type success_url: str
|
|
:ivar success_message: Message displayed to the user upon successful update.
|
|
:type success_message: str
|
|
:ivar permission_required: List of permissions required to access the update view.
|
|
:type permission_required: list
|
|
"""
|
|
model = BankAccountModel
|
|
form_class = BankAccountUpdateForm
|
|
template_name = "ledger/bank_accounts/bank_account_form.html"
|
|
success_url = reverse_lazy("bank_account_list")
|
|
success_message = _("Bank account updated successfully")
|
|
permission_required = ['inventory.view_carfinance']
|
|
|
|
def get_form_kwargs(self):
|
|
dealer = get_user_type(self.request)
|
|
entity = dealer.entity
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs["entity_slug"] = entity.slug # Get entity_slug from URL
|
|
kwargs["user_model"] = entity.admin # Get user_model from the request
|
|
return kwargs
|
|
|
|
|
|
@login_required
|
|
def bank_account_delete(request, pk):
|
|
"""
|
|
Delete a bank account entry from the database.
|
|
|
|
This view handles the deletion of a bank account record specified by its
|
|
primary key (pk). It renders a deletion confirmation page and processes the
|
|
deletion if the request method is POST. Upon successful deletion, the user is
|
|
redirected to the list of bank accounts and a success message is displayed.
|
|
|
|
:param request: The HTTP request object representing the client's request.
|
|
It contains data such as request type (GET or POST) and session
|
|
information.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the bank account model instance to be deleted.
|
|
:type pk: int
|
|
:return: Returns an HttpResponse object. This can be an HTTP redirect to the
|
|
bank account list page upon successful deletion, or an HTML response
|
|
rendering the confirmation template if accessed via GET.
|
|
:rtype: HttpResponse
|
|
"""
|
|
bank_account = get_object_or_404(BankAccountModel, pk=pk)
|
|
if request.method == "POST":
|
|
bank_account.delete()
|
|
messages.success(request, _("Bank account deleted successfully"))
|
|
return redirect("bank_account_list")
|
|
return render(
|
|
request,
|
|
"ledger/bank_accounts/bank_account_delete.html",
|
|
{"bank_account": bank_account},
|
|
)
|
|
|
|
|
|
# Accounts
|
|
|
|
|
|
class AccountListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
View for displaying a list of accounts.
|
|
|
|
AccountListView is responsible for displaying a paginated list of accounts
|
|
that users can view based on their permissions. This view is restricted
|
|
to users who are logged in and have the required permissions. It also
|
|
provides functionality to filter and search the available accounts.
|
|
|
|
:ivar model: The Django model used for this view. Determines which
|
|
database records are displayed in the account list.
|
|
:type model: type[Model]
|
|
:ivar template_name: Path to the template file used for rendering the
|
|
account list view.
|
|
:type template_name: str
|
|
:ivar context_object_name: Name of the variable containing the accounts
|
|
list passed to the template.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: Number of accounts to display per page.
|
|
:type paginate_by: int
|
|
:ivar permission_required: Permissions required to access this view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
model = AccountModel
|
|
template_name = "ledger/coa_accounts/account_list.html"
|
|
context_object_name = "accounts"
|
|
paginate_by = 10
|
|
permission_required = ["inventory.view_carfinance"]
|
|
|
|
def get_queryset(self):
|
|
query = self.request.GET.get("q")
|
|
dealer = get_user_type(self.request)
|
|
accounts = dealer.entity.get_all_accounts()
|
|
return apply_search_filters(accounts, query)
|
|
|
|
|
|
class AccountCreateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView
|
|
):
|
|
"""
|
|
View for creating an account in the ledger system.
|
|
|
|
This class provides functionality for rendering a form to create a new account,
|
|
validating the form, setting default account properties based on the current
|
|
user's entity, and saving the new account. It is designed to ensure that only
|
|
authorized users with the required permissions can create accounts. The view
|
|
also provides feedback to the user upon successful account creation.
|
|
|
|
:ivar model: Defines the model associated with this view. In this case, the model
|
|
represents accounts in the ledger system.
|
|
:type model: type
|
|
|
|
:ivar form_class: Defines the form class used for creating new accounts. This
|
|
ensures data validation and captures input for new account creation.
|
|
:type form_class: type
|
|
|
|
:ivar template_name: Specifies the template used to render the account creation form.
|
|
:type template_name: str
|
|
|
|
:ivar success_url: URL to which the user is redirected upon successful account creation.
|
|
:type success_url: str
|
|
|
|
:ivar success_message: Feedback message displayed to the user upon successfully
|
|
creating an account.
|
|
:type success_message: str
|
|
|
|
:ivar permission_required: List of permissions required to access this view. Enforces
|
|
the permission checking to prevent unauthorized access.
|
|
:type permission_required: list
|
|
"""
|
|
model = AccountModel
|
|
form_class = AccountModelCreateForm
|
|
template_name = "ledger/coa_accounts/account_form.html"
|
|
success_url = reverse_lazy("account_list")
|
|
success_message = _("Account created successfully")
|
|
permission_required = ['inventory.view_carfinance']
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
form.instance.entity_model = dealer.entity
|
|
form.instance.coa_model = dealer.entity.get_default_coa()
|
|
form.instance.depth = 0
|
|
form.instance.path = form.instance.code
|
|
return super().form_valid(form)
|
|
|
|
def get_form_kwargs(self):
|
|
dealer = get_user_type(self.request)
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs["coa_model"] = dealer.entity.get_default_coa()
|
|
return kwargs
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
entity = get_user_type(self.request).entity
|
|
form.initial["coa_model"] = entity.get_default_coa()
|
|
return form
|
|
|
|
|
|
class AccountDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Represents the detailed view for an account with additional context data related to account
|
|
transactions and permissions.
|
|
|
|
This class provides a detailed view for an account in the system. It includes functionality
|
|
for generating contextual data, managing permissions, and customizing rendering templates.
|
|
The view calculates total debits, credits, and provides transaction details for the account.
|
|
|
|
:ivar model: The Django model class representing the account data.
|
|
:type model: Type[AccountModel]
|
|
:ivar template_name: The path to the template used to render this view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The context variable name representing the account object.
|
|
:type context_object_name: str
|
|
:ivar slug_field: The field in the model used to retrieve the account instance based on a slug.
|
|
:type slug_field: str
|
|
:ivar DEFAULT_TXS_DAYS: Default number of days to filter transactions.
|
|
:type DEFAULT_TXS_DAYS: int
|
|
:ivar permission_required: Permissions required to access this view.
|
|
:type permission_required: list[str]
|
|
:ivar extra_context: Additional context data passed to the template.
|
|
:type extra_context: dict
|
|
"""
|
|
model = AccountModel
|
|
template_name = "ledger/coa_accounts/account_detail.html"
|
|
context_object_name = "account"
|
|
slug_field = "uuid"
|
|
DEFAULT_TXS_DAYS = 30
|
|
permission_required = ["inventory.view_carfinance"]
|
|
extra_context = {
|
|
"DEFAULT_TXS_DAYS": DEFAULT_TXS_DAYS,
|
|
"header_subtitle_icon": "ic:round-account-tree",
|
|
}
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
account_model: AccountModel = context["object"]
|
|
context["header_title"] = f"Account {account_model.code} - {account_model.name}"
|
|
context["page_title"] = f"Account {account_model.code} - {account_model.name}"
|
|
context["total_debits"] = sum(
|
|
x.amount for x in account_model.transactionmodel_set.filter(tx_type="debit")
|
|
)
|
|
context["total_credits"] = sum(
|
|
x.amount
|
|
for x in account_model.transactionmodel_set.filter(tx_type="credit")
|
|
)
|
|
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, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
|
):
|
|
"""
|
|
Represents a view for updating an existing account.
|
|
|
|
This class provides functionality to update an account's details using a form.
|
|
The user must be logged in and have the necessary permissions to access this
|
|
view. Upon successful update of the account, a success message is displayed
|
|
and the user is redirected to the account list page.
|
|
|
|
:ivar model: The model associated with this view which represents the account.
|
|
:type model: AccountModel
|
|
:ivar form_class: The form class used for updating account details.
|
|
:type form_class: AccountModelUpdateForm
|
|
:ivar template_name: The path to the template used for rendering the update view.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to upon success.
|
|
:type success_url: str
|
|
:ivar success_message: The success message displayed after updating an account
|
|
successfully.
|
|
:type success_message: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list of str
|
|
"""
|
|
model = AccountModel
|
|
form_class = AccountModelUpdateForm
|
|
template_name = "ledger/coa_accounts/account_form.html"
|
|
success_url = reverse_lazy("account_list")
|
|
success_message = _("Account updated successfully")
|
|
permission_required = ['inventory.view_carfinance']
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
form.fields["_ref_node_id"].widget = HiddenInput()
|
|
form.fields["_position"].widget = HiddenInput()
|
|
return form
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.view_carfinance")
|
|
def account_delete(request, pk):
|
|
"""
|
|
Handles the deletion of an account object identified by its primary key (pk). Ensures
|
|
that the user has the necessary permissions to perform the deletion. Successfully
|
|
deletes the account and redirects to the account list view with a success message.
|
|
|
|
:param request: The HTTP request object representing the current user and request data.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the account to be deleted.
|
|
:type pk: int
|
|
:return: An HTTP redirect response to the account list page.
|
|
:rtype: HttpResponse
|
|
"""
|
|
account = get_object_or_404(AccountModel, pk=pk)
|
|
|
|
account.delete()
|
|
messages.success(request, _("Account deleted successfully"))
|
|
return redirect("account_list")
|
|
|
|
|
|
# Sales list
|
|
@login_required
|
|
@permission_required("inventory.view_lead", raise_exception=True)
|
|
def sales_list_view(request):
|
|
"""
|
|
Handles the retrieval and presentation of a paginated list of item transactions for
|
|
sales, specific to the logged-in user's entity. Requires the user to have appropriate
|
|
permissions to view the list.
|
|
|
|
:param request: The HTTP request object containing metadata about the request,
|
|
such as HTTP method, user credentials, and sent data.
|
|
:type request: HttpRequest
|
|
|
|
:return: An HTTP response with the rendered sales list page containing the paginated
|
|
item transactions specific to the user's entity.
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer = get_user_type(request)
|
|
entity = dealer.entity
|
|
|
|
transactions = ItemTransactionModel.objects.for_entity(
|
|
entity_slug=entity.slug, user_model=dealer.user
|
|
).order_by('created')
|
|
paginator = Paginator(transactions, 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, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Handles the display of a paginated list of estimates for a specific entity.
|
|
|
|
This class-based view displays estimates related to an entity associated
|
|
with the logged-in user. It renders a paginated list of estimates on a
|
|
template and allows filtering of estimates based on their status. Access
|
|
to this view is restricted to users with the required permissions.
|
|
|
|
:ivar model: The database model associated with the view.
|
|
:type model: Model
|
|
:ivar template_name: The path to the template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable representing
|
|
the list of estimates.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of estimates displayed per page.
|
|
:type paginate_by: int
|
|
:ivar permission_required: List of permissions required to view this page.
|
|
:type permission_required: list
|
|
"""
|
|
model = EstimateModel
|
|
template_name = "sales/estimates/estimate_list.html"
|
|
context_object_name = "estimates"
|
|
paginate_by = 20
|
|
permission_required = ["django_ledger.view_estimatemodel"]
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
entity = dealer.entity
|
|
status = self.request.GET.get("status")
|
|
queryset = entity.get_estimates()
|
|
if status:
|
|
queryset = queryset.filter(status=status)
|
|
return queryset
|
|
|
|
|
|
# @csrf_exempt
|
|
@login_required
|
|
@permission_required("django_ledger.add_estimatemodel", raise_exception=True)
|
|
def create_estimate(request, pk=None):
|
|
"""
|
|
Creates a new estimate based on the provided data and saves it. This function processes
|
|
a POST request and expects a JSON payload containing details of the estimate such as
|
|
title, customer, terms, items, and quantities. It validates the input data, ensures
|
|
availability of stocks, and updates or creates the corresponding estimate in the database.
|
|
|
|
If `pk` is provided, it links the created estimate with an existing opportunity. It handles
|
|
the reservation of cars and updates the stock information accordingly.
|
|
|
|
:param request: The HttpRequest object containing user-specific data and state.
|
|
:type request: HttpRequest
|
|
:param pk: An optional primary key of the existing opportunity to associate with
|
|
the created estimate.
|
|
:type pk: int, optional
|
|
:return: A JsonResponse object with status and either the created quotation URL
|
|
or an error message. If the request method is not POST, it renders the
|
|
estimate creation form.
|
|
:rtype: JsonResponse or HttpResponse
|
|
"""
|
|
dealer = get_user_type(request)
|
|
entity = dealer.entity
|
|
|
|
if request.method == "POST":
|
|
# try:
|
|
data = json.loads(request.body)
|
|
title = data.get("title")
|
|
customer_id = data.get("customer")
|
|
# terms = data.get("terms")
|
|
# customer = entity.get_customers().filter(pk=customer_id).first()
|
|
customer = models.Customer.objects.filter(pk=customer_id).first()
|
|
|
|
items = data.get("item", [])
|
|
quantities = data.get("quantity", [])
|
|
|
|
if not all([items, quantities]):
|
|
return JsonResponse(
|
|
{"status": "error", "message": _("Items and Quantities are required")},
|
|
status=400,
|
|
)
|
|
|
|
if isinstance(quantities, list):
|
|
if "0" in quantities:
|
|
return JsonResponse(
|
|
{"status": "error", "message": _("Quantity must be greater than zero")}
|
|
)
|
|
else:
|
|
if int(quantities) <= 0:
|
|
return JsonResponse(
|
|
{"status": "error", "message": _("Quantity must be greater than zero")}
|
|
)
|
|
if isinstance(items, list):
|
|
for item, quantity in zip(items, quantities):
|
|
if (
|
|
int(quantity)
|
|
> models.Car.objects.filter(hash=item, status="available").count()
|
|
):
|
|
return JsonResponse(
|
|
{"status": "error", "message": _("Quantity must be less than or equal to the number of cars in stock")},
|
|
)
|
|
else:
|
|
if (
|
|
int(quantities)
|
|
> models.Car.objects.filter(hash=items, status="available").count()
|
|
):
|
|
return JsonResponse(
|
|
{"status": "error", "message": _("Quantity must be less than or equal to the number of cars in stock")},
|
|
)
|
|
estimate = entity.create_estimate(
|
|
estimate_title=title, customer_model=customer.customer_model, contract_terms="fixed"
|
|
)
|
|
if isinstance(items, list):
|
|
item_quantity_map = {}
|
|
for item, quantity in zip(items, quantities):
|
|
if item in item_quantity_map:
|
|
item_quantity_map[item] += int(quantity)
|
|
else:
|
|
item_quantity_map[item] = int(quantity)
|
|
item_list = list(item_quantity_map.keys())
|
|
quantity_list = list(item_quantity_map.values())
|
|
|
|
items_list = [
|
|
{"item_id": item_list[i], "quantity": quantity_list[i]}
|
|
for i in range(len(item_list))
|
|
]
|
|
items_txs = []
|
|
for item in items_list:
|
|
car_instance = ItemModel.objects.filter(
|
|
additional_info__car_info__hash=item.get("item_id")
|
|
).all()
|
|
|
|
for i in car_instance[: int(quantities[0])]:
|
|
items_txs.append(
|
|
{
|
|
"item_number": i.item_number,
|
|
"quantity": 1,
|
|
"unit_cost": i.additional_info.get("car_finance").get(
|
|
"selling_price"
|
|
),
|
|
"unit_revenue": i.additional_info.get("car_finance").get(
|
|
"selling_price"
|
|
),
|
|
"total_amount": (
|
|
i.additional_info.get("car_finance").get("total_vat")
|
|
),
|
|
}
|
|
)
|
|
|
|
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 = forms.EstimateModelCreateForm()
|
|
form.fields["customer"].queryset = dealer.customers.all()
|
|
|
|
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__arabic_name')).values_list(
|
|
'id_car_make__arabic_name', 'id_car_model__arabic_name','id_car_serie__arabic_name','id_car_trim__arabic_name','color','color_name','hash').annotate(hash_count=Count('hash')).distinct()
|
|
context = {
|
|
"form": form,
|
|
"items": [
|
|
{
|
|
"make": x[0],
|
|
"model": x[1],
|
|
"serie": x[2],
|
|
"trim": x[3],
|
|
"color": x[4],
|
|
"color_name": x[5],
|
|
"hash": x[6],
|
|
"hash_count": x[7],
|
|
}
|
|
for x in car_list
|
|
],
|
|
"opportunity_id": pk if pk else None,
|
|
"customer_count": entity.get_customers().count(),
|
|
}
|
|
|
|
return render(request, "sales/estimates/estimate_form.html", context)
|
|
|
|
|
|
class EstimateDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Represents the detailed view for an EstimateModel instance.
|
|
|
|
This class provides functionality to display detailed information about
|
|
a specific EstimateModel instance. It ensures the user is authenticated
|
|
and has the required permissions to access the estimate details. The class
|
|
also integrates additional financial and invoice-related data into the
|
|
context for more comprehensive display and functionality.
|
|
|
|
:ivar model: Specifies the model associated with the view.
|
|
:type model: ModelBase
|
|
:ivar template_name: Path to the template used for rendering the detailed view.
|
|
:type template_name: str
|
|
:ivar context_object_name: Name used to refer to the EstimateModel instance in the context.
|
|
:type context_object_name: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list
|
|
"""
|
|
model = EstimateModel
|
|
template_name = "sales/estimates/estimate_detail.html"
|
|
context_object_name = "estimate"
|
|
permission_required = ["django_ledger.view_estimatemodel"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
estimate = kwargs.get("object")
|
|
if estimate.get_itemtxs_data():
|
|
calculator = CarFinanceCalculator(estimate)
|
|
finance_data = calculator.get_finance_data()
|
|
invoice_obj = InvoiceModel.objects.all().filter(ce_model=estimate).first()
|
|
kwargs["data"] = finance_data
|
|
kwargs["invoice"] = invoice_obj
|
|
return super().get_context_data(**kwargs)
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.add_saleorder", raise_exception=True)
|
|
def create_sale_order(request, pk):
|
|
"""
|
|
Creates a sale order for a given estimate and updates associated item and car data.
|
|
|
|
This view is responsible for handling the submission of a sale order form linked to
|
|
a specific estimate. It ensures that the estimate is approved if not already, updates
|
|
the status of the related car items as sold, and redirects to the estimate's detailed
|
|
view upon successful creation of the sale order. If the request method is not POST, it
|
|
renders the form for the user to input sale order details, along with other contextual
|
|
information like estimate data and car finance details.
|
|
|
|
:param request: HTTP request object.
|
|
:type request: HttpRequest
|
|
:param pk: Primary key of the estimate to create a sale order for.
|
|
:type pk: int
|
|
:return: An HTTP response rendering the sale order form if the method is GET or invalid
|
|
POST data, or redirects to the estimate detail view upon successful creation.
|
|
:rtype: HttpResponse
|
|
"""
|
|
estimate = get_object_or_404(EstimateModel, pk=pk)
|
|
items = estimate.get_itemtxs_data()[0].all()
|
|
|
|
if request.method == "POST":
|
|
form = forms.SaleOrderForm(request.POST)
|
|
if form.is_valid():
|
|
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},
|
|
)
|
|
|
|
|
|
@login_required
|
|
def preview_sale_order(request, pk):
|
|
"""
|
|
Handles rendering of the sale order preview page for a specific estimate.
|
|
|
|
This view retrieves an `EstimateModel` object based on the provided primary
|
|
key (`pk`), fetches related car finance data, and renders the preview of the
|
|
sale order associated with the given estimate.
|
|
|
|
:param request: The HTTP request object
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the `EstimateModel` to retrieve
|
|
:type pk: int
|
|
:return: HTTP response containing the rendered sale order preview page
|
|
:rtype: HttpResponse
|
|
"""
|
|
estimate = get_object_or_404(EstimateModel, pk=pk)
|
|
data = get_car_finance_data(estimate)
|
|
return render(
|
|
request,
|
|
"sales/estimates/sale_order_preview.html",
|
|
{"order": estimate.sale_orders.first(), "data": data, "estimate": estimate},
|
|
)
|
|
|
|
|
|
class PaymentRequest(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Represents a detailed view of a payment request for an estimate.
|
|
|
|
This class is a Django DetailView that leverages mixins for login
|
|
and permission requirements. It displays details of an estimate
|
|
and fetches related car data based on the estimate's items.
|
|
It ensures only authorized users can access the payment request details.
|
|
|
|
:ivar model: The Django model associated with this view. It is used
|
|
to fetch and display detailed information for an estimate.
|
|
:type model: EstimateModel
|
|
:ivar template_name: The template utilized to render the detailed
|
|
payment request page for estimates.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context object to be
|
|
accessible in the template for the detailed view.
|
|
:type context_object_name: str
|
|
:ivar permission_required: Permissions required for accessing
|
|
this view. The user must have the specified permissions.
|
|
:type permission_required: list
|
|
"""
|
|
model = EstimateModel
|
|
template_name = "sales/estimates/payment_request_detail.html"
|
|
context_object_name = "estimate"
|
|
permission_required = ["django_ledger.view_invoicemodel"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["cars"] = [
|
|
models.Car.objects.get(vin=car.item_model.name)
|
|
for car in context["estimate"].get_itemtxs_data()[0].all()
|
|
]
|
|
|
|
return context
|
|
|
|
|
|
class EstimatePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Represents a view for previewing an estimate with user-specific permissions and
|
|
context data processing. This class provides functionality to render a detailed
|
|
view of an estimate while ensuring user authentication and permission verification.
|
|
|
|
This view is primarily used in a sales module to preview the detailed financial
|
|
breakdown of an estimate, including tax, discount, additional services, and total
|
|
amount.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: Type[models.Model]
|
|
:ivar context_object_name: The name of the context variable containing the object.
|
|
:type context_object_name: str
|
|
:ivar template_name: The path to the template used for rendering this view.
|
|
:type template_name: str
|
|
:ivar permission_required: List of permissions required to access this view.
|
|
:type permission_required: List[str]
|
|
"""
|
|
model = EstimateModel
|
|
context_object_name = "estimate"
|
|
template_name = "sales/estimates/estimate_preview.html"
|
|
permission_required = ["django_ledger.view_estimatemodel"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
dealer = get_user_type(self.request)
|
|
estimate = kwargs.get("object")
|
|
if estimate.get_itemtxs_data():
|
|
# data = get_financial_values(estimate)
|
|
calculator = CarFinanceCalculator(estimate)
|
|
kwargs["data"] = calculator.get_finance_data()
|
|
kwargs["dealer"] = dealer
|
|
return super().get_context_data(**kwargs)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.change_estimatemodel", raise_exception=True)
|
|
def estimate_mark_as(request, pk):
|
|
"""
|
|
Marks an estimate with a specified status based on the requested action and
|
|
permissions. The marking possibilities include review, approval, rejection,
|
|
completion, and cancellation. The function validates whether the estimate
|
|
can transition to the desired status before updating it. It also handles
|
|
notifications and updates related entities if required, such as car status
|
|
changes upon cancellation.
|
|
|
|
:param request: The HTTP request object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the estimate to be marked.
|
|
:type pk: int
|
|
:return: A redirect response to the estimate detail view.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
estimate = get_object_or_404(EstimateModel, pk=pk)
|
|
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, _("Quotation is not ready for review"))
|
|
return redirect("estimate_detail", pk=estimate.pk)
|
|
estimate.mark_as_review()
|
|
|
|
elif mark == "approved":
|
|
if not estimate.can_approve():
|
|
messages.error(request, _("Quotation is not ready for approval"))
|
|
return redirect("estimate_detail", pk=estimate.pk)
|
|
estimate.mark_as_approved()
|
|
messages.success(request, _("Quotation approved successfully"))
|
|
elif mark == "rejected":
|
|
if not estimate.can_cancel():
|
|
messages.error(request, _("Quotation is not ready for rejection"))
|
|
return redirect("estimate_detail", pk=estimate.pk)
|
|
estimate.mark_as_canceled()
|
|
messages.success(request, _("Quotation canceled successfully"))
|
|
elif mark == "completed":
|
|
if not estimate.can_complete():
|
|
messages.error(request, _("Quotation is not ready for completion"))
|
|
return redirect("estimate_detail", pk=estimate.pk)
|
|
elif mark == "canceled":
|
|
if not estimate.can_cancel():
|
|
messages.error(request, _("Quotation is not ready for cancellation"))
|
|
return redirect("estimate_detail", pk=estimate.pk)
|
|
estimate.mark_as_canceled()
|
|
try:
|
|
car = models.Car.objects.get(
|
|
vin=estimate.get_itemtxs_data()[0].first().item_model.name
|
|
)
|
|
car.status = "available"
|
|
car.save()
|
|
except Exception as e:
|
|
pass
|
|
messages.success(request, _("Quotation canceled successfully"))
|
|
estimate.save()
|
|
messages.success(request, _("Quotation marked as ") + mark.upper())
|
|
|
|
return redirect("estimate_detail", pk=estimate.pk)
|
|
|
|
|
|
# Invoice
|
|
class InvoiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Handles the display and management of a list of invoices.
|
|
|
|
This class-based view provides functionality for displaying a paginated list of invoices
|
|
while ensuring that only authenticated and authorized users can access the view. It allows
|
|
for applying search filters to the displayed invoices, based on user inputs. The view is
|
|
designed to work as part of the Django framework, and utilizes models, templates, and
|
|
permissions specific to the application.
|
|
|
|
:ivar model: The model representing invoices.
|
|
:type model: type
|
|
:ivar template_name: Path to the template used to render the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: Name used to reference the list of invoices in the template.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of invoices to display per page.
|
|
:type paginate_by: int
|
|
:ivar permission_required: List of permissions required for accessing this view.
|
|
:type permission_required: list
|
|
"""
|
|
model = InvoiceModel
|
|
template_name = "sales/invoices/invoice_list.html"
|
|
context_object_name = "invoices"
|
|
paginate_by = 20
|
|
permission_required = ["django_ledger.view_invoicemodel"]
|
|
|
|
def get_queryset(self):
|
|
query = self.request.GET.get("q")
|
|
dealer = get_user_type(self.request)
|
|
invoices = dealer.entity.get_invoices()
|
|
return apply_search_filters(invoices, query)
|
|
|
|
|
|
class InvoiceDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Handles the detailed view for an invoice.
|
|
|
|
This class is responsible for displaying detailed information about a specific
|
|
invoice. It uses Django's DetailView to render the details, requires the user
|
|
to be logged in, and enforces specific permissions for viewing invoices. The
|
|
class also processes and includes additional invoice-specific data into the
|
|
context provided to the template.
|
|
|
|
:ivar model: Specifies the model to be used for the detail view.
|
|
:type model: Type[InvoiceModel]
|
|
:ivar template_name: Path to the template used for rendering the invoice details.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable representing the object.
|
|
:type context_object_name: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list
|
|
"""
|
|
model = InvoiceModel
|
|
template_name = "sales/invoices/invoice_detail.html"
|
|
context_object_name = "invoice"
|
|
permission_required = ["django_ledger.view_invoicemodel"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
invoice = kwargs.get("object")
|
|
|
|
if invoice.get_itemtxs_data():
|
|
calculator = CarFinanceCalculator(invoice)
|
|
finance_data = calculator.get_finance_data()
|
|
kwargs["data"] = finance_data
|
|
kwargs["payments"] = JournalEntryModel.objects.filter(
|
|
ledger=invoice.ledger
|
|
).all()
|
|
|
|
return super().get_context_data(**kwargs)
|
|
|
|
|
|
class DraftInvoiceModelUpdateFormView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
|
):
|
|
"""
|
|
Representation of a form view for updating draft invoices.
|
|
|
|
This class inherits from Django's login and permission mixins as well as
|
|
UpdateView, providing the functionality required to update an existing
|
|
instance of `InvoiceModel` using the associated form class. It enforces
|
|
that the user logged in has the required permissions to view invoices
|
|
and redirects to the invoice list upon successful update. This form
|
|
view specifically customizes the form initialization logic to include
|
|
additional data based on the requesting user's type and associated
|
|
entity.
|
|
|
|
:ivar model: The Django model that this view will operate upon.
|
|
:type model: Type[InvoiceModel]
|
|
:ivar form_class: The form class to be used for updating `InvoiceModel`
|
|
instances.
|
|
:type form_class: Type[DraftInvoiceModelUpdateForm]
|
|
:ivar template_name: The path to the template used to render this view.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to upon successful form submission.
|
|
:type success_url: str
|
|
:ivar permission_required: The list of permissions required to access
|
|
this view.
|
|
:type permission_required: List[str]
|
|
"""
|
|
model = InvoiceModel
|
|
form_class = DraftInvoiceModelUpdateForm
|
|
template_name = "sales/invoices/draft_invoice_update.html"
|
|
success_url = reverse_lazy("invoice_list")
|
|
permission_required = ["django_ledger.view_invoicemodel"]
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
dealer = get_user_type(self.request)
|
|
kwargs["entity_slug"] = dealer.entity
|
|
kwargs["user_model"] = dealer.entity.admin
|
|
return kwargs
|
|
|
|
|
|
class ApprovedInvoiceModelUpdateFormView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
|
):
|
|
"""
|
|
Handles the view for updating approved invoice models.
|
|
|
|
This class-based view is used to update the details of an approved invoice. It
|
|
inherits from ``LoginRequiredMixin``, ``PermissionRequiredMixin``, and
|
|
``UpdateView`` to ensure secured access to the functionality and integrates
|
|
with Django's permission system. Users must have the required permission to
|
|
access this view. It utilizes a custom update form and is configured with
|
|
specific success URLs.
|
|
|
|
:ivar model: The model associated with this view, which is ``InvoiceModel``.
|
|
:type model: type
|
|
:ivar form_class: The form class used for handling updates, which is
|
|
``ApprovedInvoiceModelUpdateForm``.
|
|
:type form_class: type
|
|
:ivar template_name: The path to the template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar success_url: URL to redirect upon successful operation. This uses
|
|
``reverse_lazy`` to point to the invoice list by default.
|
|
:type success_url: django.urls.reverse_lazy
|
|
:ivar permission_required: The permission required to access this view. It is
|
|
set to ``django_ledger.view_invoicemodel`` by default.
|
|
:type permission_required: list of str
|
|
"""
|
|
model = InvoiceModel
|
|
form_class = ApprovedInvoiceModelUpdateForm
|
|
template_name = "sales/invoices/approved_invoice_update.html"
|
|
success_url = reverse_lazy("invoice_list")
|
|
permission_required = ["django_ledger.view_invoicemodel"]
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
dealer = get_user_type(self.request)
|
|
kwargs["entity_slug"] = dealer.entity
|
|
kwargs["user_model"] = dealer.entity.admin
|
|
return kwargs
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy("invoice_detail", kwargs={"pk": self.object.pk})
|
|
|
|
|
|
class PaidInvoiceModelUpdateFormView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
|
):
|
|
"""
|
|
Handles the update process for paid invoices.
|
|
|
|
This view allows updating invoice details for paid invoices within the system.
|
|
The user must be logged in and have the necessary permissions for accessing
|
|
and modifying invoice-related data. Additionally, the view ensures that any
|
|
required validation is performed and only executes updates when the desired
|
|
business logic conditions are met. It inherits from `UpdateView` and includes
|
|
custom behavior for handling form validation, success URLs, and associated
|
|
permissions.
|
|
|
|
:ivar model: The model class associated with the view. Represents the invoice
|
|
data being managed.
|
|
:type model: type
|
|
:ivar form_class: The form class used to render and validate the update form.
|
|
:type form_class: type
|
|
:ivar template_name: Path to the template used to render the update view.
|
|
:type template_name: str
|
|
:ivar success_url: Default URL to redirect to after a successful update. This
|
|
can be overridden in specific cases.
|
|
:type success_url: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list of str
|
|
"""
|
|
model = InvoiceModel
|
|
form_class = PaidInvoiceModelUpdateForm
|
|
template_name = "sales/invoices/paid_invoice_update.html"
|
|
success_url = reverse_lazy("invoice_list")
|
|
permission_required = ["django_ledger.view_invoicemodel"]
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
dealer = get_user_type(self.request)
|
|
kwargs["entity_slug"] = dealer.entity
|
|
kwargs["user_model"] = dealer.entity.admin
|
|
return kwargs
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy("invoice_detail", kwargs={"pk": self.object.pk})
|
|
|
|
def form_valid(self, form):
|
|
invoice = form.save()
|
|
|
|
if invoice.get_amount_open() > 0:
|
|
messages.error(self.request, "Invoice is not fully paid")
|
|
return redirect("invoice_detail", pk=invoice.pk)
|
|
else:
|
|
invoice.post_ledger()
|
|
invoice.save()
|
|
return super().form_valid(form)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.change_invoicemodel", raise_exception=True)
|
|
def invoice_mark_as(request, pk):
|
|
"""
|
|
Marks an invoice as approved if it meets the required conditions.
|
|
|
|
This view is responsible for marking an invoice as approved based on the provided
|
|
`mark` parameter. If the `mark` parameter is specified as "accept" and the invoice
|
|
is eligible for approval, it gets approved and saved. Otherwise, an error message
|
|
is displayed. The function requires the user to be logged in and to have the
|
|
appropriate permission to change the InvoiceModel.
|
|
|
|
:param request: The HTTP request object containing metadata about the request.
|
|
:type request: django.http.HttpRequest
|
|
:param pk: The primary key of the invoice to be processed.
|
|
:type pk: int
|
|
:return: An HTTP redirect response to the invoice detail page after processing.
|
|
:rtype: django.http.HttpResponse
|
|
"""
|
|
invoice = get_object_or_404(InvoiceModel, pk=pk)
|
|
dealer = get_user_type(request)
|
|
mark = request.GET.get("mark")
|
|
if mark and mark == "accept":
|
|
if not invoice.can_approve():
|
|
messages.error(request, "invoice is not ready for approval")
|
|
return redirect("invoice_detail", pk=invoice.pk)
|
|
invoice.mark_as_approved(
|
|
entity_slug=dealer.entity.slug, user_model=dealer.entity.admin
|
|
)
|
|
invoice.save()
|
|
return redirect("invoice_detail", pk=invoice.pk)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.add_invoicemodel", raise_exception=True)
|
|
def invoice_create(request, pk):
|
|
"""
|
|
Handles the creation of a new invoice associated with a given estimate. It validates
|
|
the submitted data through a form, processes the invoice, updates related models, and
|
|
finalizes the estimate. If successful, redirects to the detailed view of the created
|
|
invoice. If the submitted data is invalid or the request is not a POST request, renders
|
|
the invoice creation form.
|
|
|
|
:param request: The HTTP request object.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the estimate associated with the invoice.
|
|
:type pk: int
|
|
:return: An HTTP response. Redirects to the "invoice_detail" view upon successful invoice
|
|
creation or renders the invoice creation form template otherwise.
|
|
:rtype: HttpResponse
|
|
"""
|
|
estimate = get_object_or_404(EstimateModel, pk=pk)
|
|
dealer = get_user_type(request)
|
|
entity = dealer.entity
|
|
|
|
if request.method == "POST":
|
|
form = forms.InvoiceModelCreateForm(
|
|
request.POST, entity_slug=entity.slug, user_model=entity.admin
|
|
)
|
|
if form.is_valid():
|
|
invoice = form.save(commit=False)
|
|
ledger = entity.create_ledger(name=str(invoice.pk))
|
|
invoice.ledgar = ledger
|
|
ledger.invoicemodel = invoice
|
|
ledger.save()
|
|
invoice.save()
|
|
|
|
calculator = CarFinanceCalculator(estimate)
|
|
finance_data = calculator.get_finance_data()
|
|
|
|
invoice_itemtxs = {
|
|
i.get("item_number"): {
|
|
"unit_cost": i.get("total_vat"),
|
|
"quantity": i.get("quantity"),
|
|
"total_amount": i.get("total_vat"),
|
|
}
|
|
for i in finance_data.get("cars")
|
|
}
|
|
|
|
invoice_itemtxs = invoice.migrate_itemtxs(
|
|
itemtxs=invoice_itemtxs,
|
|
commit=True,
|
|
operation=InvoiceModel.ITEMIZE_APPEND,
|
|
)
|
|
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)
|
|
else:
|
|
print(form.errors)
|
|
form = forms.InvoiceModelCreateForm(
|
|
entity_slug=entity.slug, user_model=entity.admin
|
|
)
|
|
|
|
form.initial.update(
|
|
{
|
|
"customer": estimate.customer,
|
|
"cash_account": dealer.settings.invoice_cash_account,
|
|
"prepaid_account": dealer.settings.invoice_prepaid_account,
|
|
"unearned_account": dealer.settings.invoice_unearned_account,
|
|
}
|
|
)
|
|
|
|
context = {
|
|
"form": form,
|
|
"estimate": estimate,
|
|
}
|
|
return render(request, "sales/invoices/invoice_create.html", context)
|
|
|
|
|
|
class InvoicePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Represents a detailed view for previewing an invoice.
|
|
|
|
This class provides a mechanism to render a preview of an invoice for authorized
|
|
users. It utilizes Django's class-based views by extending `DetailView` and includes
|
|
necessary mixins for login and permission checks. The purpose of this class is to ensure
|
|
secured access to the invoice preview while generating additional context data needed
|
|
for rendering finance-related information.
|
|
|
|
:ivar model: The Django model class this view will represent.
|
|
:type model: InvoiceModel
|
|
:ivar context_object_name: Name of the context object used in the template.
|
|
:type context_object_name: str
|
|
:ivar template_name: Path to the template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list
|
|
"""
|
|
model = InvoiceModel
|
|
context_object_name = "invoice"
|
|
template_name = "sales/invoices/invoice_preview.html"
|
|
permission_required = ["django_ledger.view_invoicemodel"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
dealer = get_user_type(self.request)
|
|
invoice = kwargs.get("object")
|
|
if invoice.get_itemtxs_data():
|
|
calculator = CarFinanceCalculator(invoice)
|
|
finance_data = calculator.get_finance_data()
|
|
kwargs["data"] = finance_data
|
|
kwargs["dealer"] = dealer
|
|
return super().get_context_data(**kwargs)
|
|
|
|
|
|
# payments
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.add_journalentrymodel", raise_exception=True)
|
|
def PaymentCreateView(request, pk):
|
|
"""
|
|
Handles the creation of a payment entry associated with an invoice or bill. Validates
|
|
the payment data against the model's current state and reflects the changes in
|
|
invoice or bill records. Provides appropriate error messages for invalid conditions
|
|
such as exceeding payable amounts or attempting payment for already fully paid models.
|
|
|
|
If successfully processed, the payment details are saved, and the model is updated
|
|
accordingly. This view regulates payment for dealer-associated entities while
|
|
ensuring the model consistency.
|
|
|
|
The view renders a form to submit payment details, and pre-populates the form fields
|
|
with default data for the associated model if necessary.
|
|
|
|
:param request: The HTTP request object containing user request data and session
|
|
information. This is required to handle the request and apply the appropriate
|
|
processing rules.
|
|
:param pk: The primary key of the invoice or bill being processed. It is used to
|
|
load the appropriate model instance for payment processing.
|
|
:return: An HTTP response object. Depending on the circumstances, the response may
|
|
redirect to the detail view of the processed invoice or bill, re-render the
|
|
payment form with error messages or indicate success in payment creation.
|
|
"""
|
|
invoice = InvoiceModel.objects.filter(pk=pk).first()
|
|
bill = BillModel.objects.filter(pk=pk).first()
|
|
model = invoice if invoice else bill
|
|
dealer = get_user_type(request)
|
|
entity = dealer.entity
|
|
form = forms.PaymentForm()
|
|
if request.method == "POST":
|
|
form = forms.PaymentForm(request.POST)
|
|
if form.is_valid():
|
|
amount = form.cleaned_data.get("amount")
|
|
invoice = form.cleaned_data.get("invoice")
|
|
bill = form.cleaned_data.get("bill")
|
|
payment_method = form.cleaned_data.get("payment_method")
|
|
redirect_url = "invoice_detail" if invoice else "bill_detail"
|
|
model = invoice if invoice else bill
|
|
|
|
if not model.is_approved():
|
|
model.mark_as_approved(user_model=entity.admin)
|
|
if model.amount_paid == model.amount_due:
|
|
messages.error(request,_("fully paid"))
|
|
return redirect(redirect_url, pk=model.pk)
|
|
if model.amount_paid + amount > model.amount_due:
|
|
messages.error(request,_("Amount exceeds due amount"))
|
|
return redirect(redirect_url, pk=model.pk)
|
|
|
|
try:
|
|
if invoice:
|
|
set_invoice_payment(dealer, entity, invoice, amount, payment_method)
|
|
elif bill:
|
|
set_bill_payment(dealer, entity, bill, amount, payment_method)
|
|
messages.success(request, _("Payment created successfully"))
|
|
return redirect(redirect_url, pk=model.pk)
|
|
except Exception as e:
|
|
messages.error(request, f"Error creating payment: {str(e)}")
|
|
else:
|
|
messages.error(request, f"Invalid form data: {str(form.errors)}")
|
|
form = forms.PaymentForm()
|
|
if model:
|
|
form.initial["amount"] = model.amount_due - model.amount_paid
|
|
if isinstance(model, InvoiceModel):
|
|
form.initial["invoice"] = model
|
|
form.fields["bill"].widget = HiddenInput()
|
|
elif isinstance(model, BillModel):
|
|
form.initial["bill"] = model
|
|
form.fields["invoice"].widget = HiddenInput()
|
|
return render(
|
|
request, "sales/payments/payment_form.html", {"model": model, "form": form}
|
|
)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.view_journalentrymodel", raise_exception=True)
|
|
def PaymentListView(request):
|
|
"""
|
|
Handles the view for listing payment information associated with the journals of a specific
|
|
entity. This view is protected to ensure only authenticated and authorized users can
|
|
access it.
|
|
|
|
The function retrieves the related dealer object based on the current user session, extracts
|
|
the associated entity, and fetches all journal entries linked to the entity. This data is
|
|
then passed into the template for rendering.
|
|
|
|
:param request: The HTTP request object containing user context.
|
|
:type request: HttpRequest
|
|
|
|
:return: The rendered HTML response displaying the list of payments.
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer = get_user_type(request)
|
|
entity = dealer.entity
|
|
journals = JournalEntryModel.objects.filter(ledger__entity=entity).all()
|
|
return render(request, "sales/payments/payment_list.html", {"journals": journals})
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.view_journalentrymodel", raise_exception=True)
|
|
def PaymentDetailView(request, pk):
|
|
"""
|
|
This function handles the detail view for a payment by fetching a journal entry
|
|
and its associated transactions. It ensures that the request is authenticated
|
|
and the user has permission to view the journal entry model.
|
|
|
|
:param request: The HTTP request object.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the journal entry for which details are to be fetched.
|
|
:type pk: int
|
|
:return: An HTTP response rendering the payment details template with the journal
|
|
entry and its associated transactions.
|
|
:rtype: HttpResponse
|
|
"""
|
|
journal = JournalEntryModel.objects.filter(pk=pk).first()
|
|
transactions = (
|
|
TransactionModel.objects.filter(journal_entry=journal)
|
|
.order_by("account__code")
|
|
.all()
|
|
)
|
|
return render(
|
|
request,
|
|
"sales/payments/payment_details.html",
|
|
{"journal": journal, "transactions": transactions},
|
|
)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.change_journalentrymodel", raise_exception=True)
|
|
def payment_mark_as_paid(request, pk):
|
|
"""
|
|
Marks an invoice as paid if it meets the conditions of being fully paid and eligible
|
|
for payment. Also ensures that related ledger journal entries are locked and posted
|
|
when the payment is marked successfully.
|
|
|
|
This function is protected with both `login_required` and
|
|
`permission_required` decorators, ensuring that only logged-in users with
|
|
appropriate permissions can execute it.
|
|
|
|
:param request: HttpRequest object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: Primary key of the invoice to mark as paid.
|
|
:type pk: int
|
|
:return: Redirect response to the invoice detail page.
|
|
:rtype: HttpResponseRedirect
|
|
:raises: In case of an exception during the process, an error message is
|
|
displayed to the user through Django's messaging framework.
|
|
"""
|
|
invoice = get_object_or_404(InvoiceModel, pk=pk)
|
|
if request.method == "POST":
|
|
try:
|
|
if invoice.amount_due == invoice.amount_paid:
|
|
if not invoice.is_paid() and invoice.can_pay():
|
|
invoice.mark_as_paid(
|
|
entity_slug=invoice.ledger.entity.slug,
|
|
user_model=invoice.ledger.entity.admin,
|
|
)
|
|
invoice.save()
|
|
|
|
invoice.ledger.lock_journal_entries()
|
|
invoice.ledger.post_journal_entries()
|
|
|
|
# invoice.ledger.post()
|
|
invoice.ledger.save()
|
|
messages.success(request, _("Payment created successfully"))
|
|
else:
|
|
messages.error(
|
|
request,
|
|
_("Invoice is not fully paid, Payment cannot be marked as paid"),
|
|
)
|
|
except Exception as e:
|
|
messages.error(request, f"Error: {str(e)}")
|
|
return redirect("invoice_detail", pk=invoice.pk)
|
|
|
|
|
|
# activity log
|
|
class UserActivityLogListView(LoginRequiredMixin, ListView):
|
|
"""
|
|
Represents a view to display a paginated list of user activity logs.
|
|
|
|
This class is used to display a list of user activity logs in a paginated
|
|
manner. It retrieves the logs from the `UserActivityLog` model and allows
|
|
basic filtering of logs by user email through the URL query parameters.
|
|
The view requires the user to be authenticated and utilizes the
|
|
`LoginRequiredMixin` to enforce this.
|
|
|
|
:ivar model: The model associated with the view.
|
|
:type model: models.UserActivityLog
|
|
:ivar template_name: The template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable representing
|
|
the list of logs.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of logs displayed per page.
|
|
:type paginate_by: int
|
|
"""
|
|
model = models.UserActivityLog
|
|
template_name = "dealers/activity_log.html"
|
|
context_object_name = "logs"
|
|
paginate_by = 20
|
|
|
|
def get_queryset(self):
|
|
queryset = super().get_queryset()
|
|
if "user" in self.request.GET:
|
|
queryset = queryset.filter(user__email=self.request.GET["user"])
|
|
return queryset[:100] # will update later with better pagination
|
|
|
|
def lead_view(request):
|
|
return render(request, "crm/leads/lead_view.html")
|
|
|
|
# CRM RELATED VIEWS
|
|
class LeadListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Represents a view to display a paginated list of leads, ensuring user permissions
|
|
and type-specific filtering.
|
|
|
|
This class is used for rendering a list of leads associated with a logged-in user
|
|
based on their type (dealer or staff member). It combines login and permission
|
|
controls with pagination to ensure proper access and management of leads
|
|
in a CRM context.
|
|
|
|
:ivar model: The model to be used for retrieving leads.
|
|
:type model: type[Lead]
|
|
:ivar template_name: Path to the template for rendering the lead list.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable to contain the leads.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: Number of items to display per page.
|
|
:type paginate_by: int
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
model = models.Lead
|
|
template_name = "crm/leads/lead_list.html"
|
|
context_object_name = "leads"
|
|
paginate_by = 10
|
|
permission_required = ["inventory.view_lead"]
|
|
|
|
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(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
View that provides detailed information about a specific lead.
|
|
|
|
This class-based view is designed for displaying detailed information associated with a
|
|
particular lead, including related notes, emails, activities, status history, and a lead
|
|
transfer form. It combines multiple mixins to enforce login and permission requirements
|
|
for accessing the data. This view is tailored to the CRM module for managing leads.
|
|
|
|
:ivar model: The model associated with this view, which is the Lead model.
|
|
:type model: models.Lead
|
|
:ivar template_name: Path to the template used to render detailed lead information.
|
|
:type template_name: str
|
|
:ivar permission_required: List of permissions required to access this view.
|
|
:type permission_required: list
|
|
"""
|
|
model = models.Lead
|
|
template_name = "crm/leads/lead_detail.html"
|
|
permission_required = ["inventory.view_lead"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["notes"] = models.Notes.objects.filter(
|
|
content_type__model="lead", object_id=self.object.id
|
|
)
|
|
email_qs = models.Email.objects.filter(
|
|
content_type__model="lead", object_id=self.object.id
|
|
)
|
|
context["emails"] = {
|
|
"sent": email_qs.filter(status="SENT"),
|
|
"draft": email_qs.filter(status="DRAFT"),
|
|
}
|
|
context["activities"] = models.Activity.objects.filter(
|
|
content_type__model="lead", object_id=self.object.id
|
|
)
|
|
context["status_history"] = models.LeadStatusHistory.objects.filter(
|
|
lead=self.object
|
|
)
|
|
context["transfer_form"] = forms.LeadTransferForm()
|
|
return context
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.add_lead", raise_exception=True)
|
|
def lead_create(request):
|
|
"""
|
|
Handles the creation of a new lead in the inventory system.
|
|
|
|
This function manages the rendering and processing of a form for creating a new
|
|
lead. It filters options for car models in the form based on the selected car
|
|
make. For POST requests, it validates and processes the submitted form data
|
|
and creates the lead if valid. It also creates a corresponding customer in the
|
|
ledger system if one does not exist for the provided email.
|
|
|
|
:param request: The HTTP request object containing request data.
|
|
:type request: HttpRequest
|
|
|
|
:return: An HTTP response object rendering the lead creation form or redirecting to the lead list page upon success.
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer = get_user_type(request)
|
|
|
|
if request.method == "POST":
|
|
form = forms.LeadForm(request.POST)
|
|
|
|
# Filter car models based on the selected make (for POST requests)
|
|
if "id_car_make" in request.POST:
|
|
form.fields["id_car_model"].queryset = models.CarModel.objects.filter(
|
|
id_car_make=int(request.POST["id_car_make"])
|
|
)
|
|
try:
|
|
if form.is_valid():
|
|
instance = form.save(commit=False)
|
|
instance.dealer = dealer
|
|
instance.staff = form.cleaned_data.get("staff")
|
|
|
|
if instance.lead_type == "customer":
|
|
customer = models.Customer.objects.filter(email=instance.email)
|
|
if not customer:
|
|
customer = models.Customer(
|
|
dealer=dealer,
|
|
address=instance.address,
|
|
phone_number=instance.phone_number,
|
|
email=instance.email,
|
|
first_name=instance.first_name,
|
|
last_name=instance.last_name,
|
|
)
|
|
customer.create_user_model()
|
|
customer.create_customer_model()
|
|
customer.save()
|
|
instance.customer = customer
|
|
|
|
|
|
if instance.lead_type == "organization":
|
|
organization = models.Organization.objects.filter(email=instance.email)
|
|
if not organization:
|
|
organization = models.Organization(
|
|
dealer=dealer,
|
|
address=instance.address,
|
|
phone_number=instance.phone_number,
|
|
email=instance.email,
|
|
name=instance.first_name + " " + instance.last_name,
|
|
)
|
|
organization.create_user_model()
|
|
organization.create_customer_model()
|
|
organization.save()
|
|
instance.organization = organization
|
|
instance.next_action = LeadStatus.FOLLOW_UP
|
|
instance.save()
|
|
messages.success(request, _("Lead created successfully"))
|
|
return redirect("lead_list")
|
|
else:
|
|
messages.error(request, f"Lead was not created ... : {str(form.errors)}")
|
|
except Exception as e:
|
|
messages.error(request, f"Lead was not created ... : {str(e)}")
|
|
|
|
form = forms.LeadForm()
|
|
form.filter_qs(dealer=dealer)
|
|
|
|
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)
|
|
)
|
|
return render(request, "crm/leads/lead_form.html", {"form": form})
|
|
|
|
def lead_tracking(request):
|
|
dealer = get_user_type(request)
|
|
new = models.Lead.objects.filter(dealer=dealer)
|
|
follow_up = models.Lead.objects.filter(dealer=dealer, next_action__in=["call", "meeting"])
|
|
won = models.Lead.objects.filter(dealer=dealer, status="won")
|
|
lose = models.Lead.objects.filter(dealer=dealer, status="lose")
|
|
negotiation = models.Lead.objects.filter(dealer=dealer, status="negotiation")
|
|
context = {"new": new,"follow_up": follow_up,"won": won,"lose": lose,"negotiation": negotiation}
|
|
return render(request, "crm/leads/lead_tracking.html", context)
|
|
|
|
# @require_POST
|
|
def update_lead_actions(request):
|
|
try:
|
|
lead_id = request.POST.get('lead_id')
|
|
current_action = request.POST.get('current_action')
|
|
next_action = request.POST.get('next_action')
|
|
next_action_date = request.POST.get('next_action_date')
|
|
action_notes = request.POST.get('action_notes', '')
|
|
|
|
# Validate required fields
|
|
if not all([lead_id, current_action, next_action, next_action_date]):
|
|
return JsonResponse({'success': False, 'message': 'All fields are required'}, status=400)
|
|
|
|
# Get the lead
|
|
lead = models.Lead.objects.get(id=lead_id)
|
|
|
|
# Update lead fields
|
|
lead.action = current_action
|
|
lead.next_action = next_action
|
|
lead.next_action_date = next_action_date
|
|
|
|
# Parse the datetime string
|
|
try:
|
|
next_action_datetime = datetime.strptime(next_action_date, '%Y-%m-%dT%H:%M')
|
|
lead.next_action_date = timezone.make_aware(next_action_datetime)
|
|
except ValueError:
|
|
return JsonResponse({'success': False, 'message': 'Invalid date format'}, status=400)
|
|
|
|
# Save the lead
|
|
lead.save()
|
|
|
|
return JsonResponse({'success': True, 'message': 'Actions updated successfully'})
|
|
|
|
except models.Lead.DoesNotExist:
|
|
return JsonResponse({'success': False, 'message': 'Lead not found'}, status=404)
|
|
except Exception as e:
|
|
return JsonResponse({'success': False, 'message': str(e)}, status=500)
|
|
|
|
class LeadUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
|
"""
|
|
Handles the update view for Lead objects.
|
|
|
|
This class is used to manage the process of updating Lead objects within the
|
|
application. It provides integration with Django's authentication and
|
|
permissions systems, ensuring that only authorized users can access this view.
|
|
Additionally, it customizes the behavior of the form to dynamically filter
|
|
available options for the 'id_car_model' field based on the related
|
|
'id_car_make' field.
|
|
|
|
:ivar model: Model class representing a Lead object.
|
|
:type model: models.Lead
|
|
:ivar form_class: Form class used for the Lead update form.
|
|
:type form_class: forms.LeadForm
|
|
:ivar template_name: Path to the template used for rendering the Lead update
|
|
form.
|
|
:type template_name: str
|
|
:ivar success_url: URL to redirect to upon successful Lead update.
|
|
:type success_url: str
|
|
:ivar permission_required: List of permissions required for accessing this view.
|
|
:type permission_required: list
|
|
"""
|
|
model = models.Lead
|
|
form_class = forms.LeadForm
|
|
template_name = "crm/leads/lead_form.html"
|
|
success_url = reverse_lazy("lead_list")
|
|
permission_required = ["inventory.change_lead"]
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
form.fields[
|
|
"id_car_model"
|
|
].queryset = form.instance.id_car_make.carmodel_set.all()
|
|
return form
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.delete_lead", raise_exception=True)
|
|
def LeadDeleteView(request, pk):
|
|
"""
|
|
Handles the deletion of a Lead along with its associated customer and potentially
|
|
a related user account in the system. Ensures proper permissions and login
|
|
are required before the operation is performed. Provides a success message
|
|
after the deletion is complete.
|
|
|
|
:param request: The HTTP request object specific to the current user.
|
|
:param pk: The primary key identifier of the Lead to be deleted.
|
|
:return: An HTTP redirect response to the lead list page.
|
|
"""
|
|
lead = get_object_or_404(models.Lead, 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):
|
|
"""
|
|
Adds a note to a specific lead. This view is accessible only to authenticated
|
|
users. The function handles the POST request to create a new note associated
|
|
with a lead. Upon successful submission, the note is linked to the provided lead,
|
|
and the user is redirected to the lead's detail page. If the request method is
|
|
not POST, it initializes an empty form to add a note.
|
|
|
|
:param request: The HTTP request object
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the lead to which the note will be added
|
|
:type pk: int
|
|
:return: HTTP response object. Redirects to the lead detail page on successful
|
|
note creation or renders the note form template for GET or invalid POST requests.
|
|
:rtype: HttpResponse
|
|
"""
|
|
lead = get_object_or_404(models.Lead, 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):
|
|
"""
|
|
Add a note to a specific opportunity identified by its primary key.
|
|
|
|
This function handles the addition of a note to an existing opportunity
|
|
by processing a POST request that includes the note content. If the note
|
|
content is missing, an error message will be displayed. If the operation
|
|
is successful, the note is saved, and a success message is returned.
|
|
|
|
:param request: The HTTP request object representing the client's request.
|
|
:param pk: The primary key of the Opportunity to which the note is to be added.
|
|
:type pk: int
|
|
:return: A redirect response to the detailed view of the opportunity.
|
|
"""
|
|
opportunity = get_object_or_404(models.Opportunity, 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):
|
|
"""
|
|
Handles the update of a specific note attached to a lead object, ensuring that the user
|
|
making the request has permissions over the note. If the request method is `POST`, it
|
|
validates and updates the note using the submitted data. If validation is successful,
|
|
it redirects to the lead's detail page. Otherwise, a form is rendered for editing.
|
|
|
|
:param request: The HTTP request object containing metadata about the request
|
|
and the user making the request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the note to be updated, identifying the specific
|
|
note object.
|
|
:type pk: int
|
|
:return: An HTTP response either rendering the note update form along with the
|
|
current note data, or a redirect to the lead detail page upon successful
|
|
update.
|
|
:rtype: HttpResponse
|
|
"""
|
|
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):
|
|
"""
|
|
Deletes a specific note created by the currently logged-in user and redirects
|
|
to the lead detail page. If the note does not exist or the user is not the creator,
|
|
a 404 error will be raised. A success message is displayed upon successful deletion
|
|
of the note.
|
|
|
|
:param request: The HTTP request object associated with the current request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the note to be deleted.
|
|
:type pk: int
|
|
:return: An HTTP redirection to the lead detail page of the corresponding note's lead.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
note = get_object_or_404(models.Notes, pk=pk, created_by=request.user)
|
|
lead_pk = note.content_object.pk
|
|
note.delete()
|
|
messages.success(request, _("Note deleted successfully."))
|
|
return redirect("lead_detail", pk=lead_pk)
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.change_lead", raise_exception=True)
|
|
def lead_convert(request, pk):
|
|
"""
|
|
Converts a lead into a customer and creates a corresponding opportunity.
|
|
|
|
The function ensures that leads are not converted to customers more than once.
|
|
If the lead has already been converted, an error is displayed. Otherwise,
|
|
the lead is converted into a customer and a new opportunity is created.
|
|
Upon successful conversion, the user is redirected to the lead list view
|
|
and a success message is displayed.
|
|
|
|
:param request: The HTTP request object representing the user's request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the lead to be converted.
|
|
:type pk: int
|
|
:return: An HTTP response redirecting to the lead list view.
|
|
:rtype: HttpResponse
|
|
"""
|
|
lead = get_object_or_404(models.Lead, 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()
|
|
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
|
|
@permission_required("inventory.add_lead", raise_exception=True)
|
|
def schedule_lead(request, pk):
|
|
"""
|
|
Handles the scheduling of a lead for an appointment.
|
|
|
|
This function ensures that only staff members with the appropriate permissions
|
|
can schedule leads. If the user is not a staff member or does not have the
|
|
required inventory permissions, they are redirected with an appropriate error
|
|
message. The function creates an appointment request and an associated appointment
|
|
record upon successful submission of the scheduling form.
|
|
|
|
:param request: The HTTP request object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the lead to be scheduled.
|
|
:type pk: int
|
|
:return: A rendered template or a redirection to another view based on the request
|
|
method and validity of the form submission.
|
|
:rtype: HttpResponse
|
|
"""
|
|
|
|
if not request.is_staff:
|
|
messages.error(request, _("You do not have permission to schedule lead"))
|
|
return redirect("lead_list")
|
|
dealer = get_user_type(request)
|
|
lead = get_object_or_404(models.Lead, 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.get_customer_model()
|
|
|
|
# Create AppointmentRequest
|
|
# service,_ = Service.objects.get_or_create(name=instance.scheduled_type,duration=datetime.timedelta(minutes=10),price=0)
|
|
service = Service.objects.get(name=instance.scheduled_type)
|
|
# service = Service.objects.filter(name=instance.scheduled_type).first()
|
|
# if not service:
|
|
# messages.error(request, "Service not found!")
|
|
# return redirect("lead_list")
|
|
|
|
try:
|
|
appointment_request = AppointmentRequest.objects.create(
|
|
date=instance.scheduled_at.date(),
|
|
start_time=instance.scheduled_at.time(),
|
|
end_time=(instance.scheduled_at + instance.duration).time(),
|
|
service=service,
|
|
staff_member=request.user.staffmember,
|
|
)
|
|
except ValidationError as e:
|
|
messages.error(request, str(e))
|
|
return redirect("schedule_lead", 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
|
|
@permission_required("inventory.change_lead", raise_exception=True)
|
|
def lead_transfer(request, pk):
|
|
"""
|
|
Handles the transfer of a lead to a different staff member. This view is accessible
|
|
only to authenticated users with the 'inventory.change_lead' permission. If the
|
|
request method is POST and the form data is valid, the lead's staff is updated
|
|
accordingly, saved, and a success message is displayed. Otherwise, an error message
|
|
is shown. In both cases, the user is redirected to the lead listing page.
|
|
|
|
:param request: The HTTP request object.
|
|
:param pk: The primary key of the lead to be transferred.
|
|
:return: An HTTP redirect response to the lead list view.
|
|
"""
|
|
lead = get_object_or_404(models.Lead, 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):
|
|
"""
|
|
Handles sending emails related to a lead. Supports creating drafts, sending emails, and rendering
|
|
an email composition page. Changes on the lead or email objects, such as marking a lead as contacted
|
|
or an email as sent, are recorded in associated activity logs.
|
|
|
|
:param request: The HTTP request object. This contains user information, method type, and data such as
|
|
GET or POST payload used to draft or send the email appropriately.
|
|
Type: HttpRequest
|
|
:param pk: The primary key of the lead to which the email action is associated. It's used to retrieve
|
|
the lead object from the database.
|
|
Type: int
|
|
:param email_pk: Optional parameter representing the primary key of an email template. When provided,
|
|
the respective email content is pre-populated into the email composition form.
|
|
Defaults to None.
|
|
Type: Optional[int]
|
|
:return: When successfully sending an email, redirects the user to the lead list page. On draft mode
|
|
or email composition rendering, a response object is returned to render the respective page.
|
|
Type: HttpResponse
|
|
"""
|
|
lead = get_object_or_404(models.Lead, 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()
|
|
|
|
|
|
if request.method == "POST":
|
|
email_pk = request.POST.get("email_pk")
|
|
if email_pk not in [None, "None", ""]:
|
|
email = get_object_or_404(models.Email, pk=int(email_pk))
|
|
email.status = models.EmailStatus.SENT
|
|
email.save()
|
|
else:
|
|
models.Email.objects.create(
|
|
content_object=lead,
|
|
created_by=request.user,
|
|
from_email="manager@tenhal.com",
|
|
to_email=request.POST.get("to"),
|
|
subject=request.POST.get("subject"),
|
|
message=request.POST.get("message"),
|
|
status=models.EmailStatus.SENT,
|
|
)
|
|
send_email(
|
|
"manager@tenhal.com",
|
|
request.POST.get("to"),
|
|
request.POST.get("subject"),
|
|
request.POST.get("message"),
|
|
)
|
|
dealer = get_user_type(request)
|
|
models.Activity.objects.create(dealer=dealer,content_object=lead, notes="Email sent",created_by=request.user,activity_type=models.ActionChoices.EMAIL)
|
|
messages.success(request, _("Email sent successfully"))
|
|
return redirect("lead_list")
|
|
msg = f"""
|
|
السلام عليكم
|
|
Dear {lead.full_name},
|
|
|
|
أود أن أشارككم تقدير المشروع الذي ناقشناه. يرجى العثور على الوثيقة التفصيلية للمقترح المرفقة.
|
|
|
|
I hope this email finds you well. I wanted to share with you the estimate for the project we discussed. Please find the detailed estimate document attached.
|
|
|
|
يرجى مراجعة المقترح وإعلامي إذا كانت لديك أي أسئلة أو مخاوف. إذا كانت كل شيء يبدو جيدًا، يمكننا المضي قدمًا في المشروع.
|
|
|
|
Please review the estimate and let me know if you have any questions or concerns. If everything looks good, we can proceed with the project.
|
|
|
|
شكراً لاهتمامكم بهذا الأمر.
|
|
Thank you for your attention to this matter.
|
|
|
|
تحياتي,
|
|
Best regards,
|
|
[Your Name]
|
|
[Your Position]
|
|
[Your Company]
|
|
[Your Contact Information]
|
|
"""
|
|
subject = ""
|
|
if email_pk:
|
|
email = get_object_or_404(models.Email, pk=email_pk)
|
|
msg = email.message
|
|
subject = email.subject
|
|
return render(
|
|
request,
|
|
"crm/leads/lead_send.html",
|
|
{"lead": lead, "message": msg, "subject": subject, "email_pk": email_pk},
|
|
)
|
|
|
|
|
|
@login_required
|
|
def add_activity_to_lead(request, pk):
|
|
"""
|
|
Handles the process of adding a new activity to a specific lead. This includes
|
|
rendering a form for user input, validating the form submission, and saving the
|
|
new activity if the input is valid. If the method is GET, it will simply
|
|
render the form. If the method is POST, it checks the form validity, creates
|
|
the activity, associates it with the lead, and saves it to the database.
|
|
|
|
:param request: The HTTP request object containing metadata about the request made
|
|
:param pk: The primary key of the lead to which the activity is to be added
|
|
:return: An HTTP response that either renders the form or redirects to the lead detail page
|
|
"""
|
|
lead = get_object_or_404(models.Lead, pk=pk)
|
|
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):
|
|
"""
|
|
Handles the creation of Opportunity instances through a form while enforcing
|
|
specific user access control and initial data population. This view ensures
|
|
an authenticated user can create opportunities tied to their dealer type and
|
|
allows pre-filling data when linked to an existing Lead.
|
|
|
|
Allows authenticated users to create a new Opportunity instance, associating it
|
|
with the requesting user's dealer type and prefilling some fields based on
|
|
existing Lead data, if applicable.
|
|
|
|
:ivar model: The database model associated with this view, which is used to
|
|
represent Opportunity data.
|
|
:type model: models.Opportunity
|
|
:ivar form_class: The form class used for creating new Opportunity instances.
|
|
:type form_class: forms.OpportunityForm
|
|
:ivar template_name: The template used to render the Opportunity creation form.
|
|
:type template_name: str
|
|
"""
|
|
model = models.Opportunity
|
|
form_class = forms.OpportunityForm
|
|
template_name = "crm/opportunities/opportunity_form.html"
|
|
|
|
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(LoginRequiredMixin, UpdateView):
|
|
"""
|
|
Handles the update functionality for Opportunity objects.
|
|
|
|
This class-based view is responsible for handling the update of existing
|
|
Opportunity instances. It uses a Django form that is specified by the
|
|
`form_class` attribute and renders a template to display and process the
|
|
update form. Access to this view is restricted to authenticated users, as
|
|
it inherits from `LoginRequiredMixin`.
|
|
|
|
It defines the model to be updated and the form template to be used. Upon
|
|
successful update, it redirects the user to the detail page of the updated
|
|
opportunity instance.
|
|
|
|
:ivar model: The model associated with this view. Represents the Opportunity model.
|
|
:type model: django.db.models.Model
|
|
:ivar form_class: The form class used to manage the Opportunity update process.
|
|
:type form_class: django.forms.ModelForm
|
|
:ivar template_name: The path to the template used to render the opportunity
|
|
update form.
|
|
:type template_name: str
|
|
"""
|
|
model = models.Opportunity
|
|
form_class = forms.OpportunityForm
|
|
template_name = "crm/opportunities/opportunity_form.html"
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy("opportunity_detail", kwargs={"pk": self.object.pk})
|
|
|
|
|
|
class OpportunityDetailView(LoginRequiredMixin, DetailView):
|
|
"""
|
|
Handles the detailed view of an Opportunity object.
|
|
|
|
Displays detailed information about a specific opportunity, including its
|
|
status, stage, notes, activities, and related emails. This view utilizes
|
|
a form to manage and update the status and stage of the opportunity
|
|
through HTTPX requests. Suitable for use in CRM applications.
|
|
|
|
:ivar model: The model for which this view is being implemented.
|
|
:type model: models.Opportunity
|
|
:ivar template_name: The template used to render this view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The context variable name for the model object in the template.
|
|
:type context_object_name: str
|
|
"""
|
|
model = models.Opportunity
|
|
template_name = "crm/opportunities/opportunity_detail.html"
|
|
context_object_name = "opportunity"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
form = forms.OpportunityStatusForm()
|
|
url = reverse("opportunity_update_status", args=[self.object.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
|
|
).order_by("-created")
|
|
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(LoginRequiredMixin, ListView):
|
|
"""
|
|
View for displaying a paginated list of opportunities.
|
|
|
|
This class-based view inherits from `LoginRequiredMixin` and `ListView` to
|
|
provide a view rendering a list of `Opportunity` objects associated with
|
|
the current dealer. It ensures the user is authenticated before providing
|
|
access to the opportunity list and adds filtering based on the dealer
|
|
associated with the request.
|
|
|
|
:ivar model: The model used to retrieve opportunities.
|
|
:type model: models.Opportunity
|
|
:ivar template_name: The template used to render the opportunities list.
|
|
:type template_name: str
|
|
:ivar context_object_name: The context variable name for the list of
|
|
opportunities in the template.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of opportunities displayed per page.
|
|
:type paginate_by: int
|
|
"""
|
|
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):
|
|
"""
|
|
Deletes an opportunity object from the database and redirects to the opportunity
|
|
list view. If the opportunity with the specified primary key is not found, a 404
|
|
error will be raised. Displays a success message after deletion.
|
|
|
|
:param request: The HTTP request object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the opportunity object to be deleted.
|
|
:type pk: int
|
|
:return: An HTTP response redirecting to the opportunity list view.
|
|
:rtype: HttpResponse
|
|
"""
|
|
opportunity = get_object_or_404(models.Opportunity, pk=pk)
|
|
opportunity.delete()
|
|
messages.success(request, _("Opportunity deleted successfully"))
|
|
return redirect("opportunity_list")
|
|
|
|
|
|
@login_required
|
|
def opportunity_update_status(request, pk):
|
|
"""
|
|
Update the status and/or stage of a specific Opportunity instance. This is a
|
|
view function, which is generally tied to a URL endpoint in a Django application.
|
|
The function is accessible only to authenticated users due to the
|
|
`@login_required` decorator.
|
|
|
|
The function retrieves the `Opportunity` instance based on the primary key
|
|
in the URL, updates the status or stage of the instance based on query
|
|
parameters in the request, saves the changes, and then redirects to the
|
|
detail view of the `Opportunity`. Additionally, a success message is displayed,
|
|
and a custom header (`HX-Refresh`) is added to the response.
|
|
|
|
:param request: The HTTP request object containing details of the user's
|
|
request, such as query parameters for status and stage. This is a
|
|
mandatory parameter provided by Django's view function framework.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the `Opportunity` instance to be updated.
|
|
:type pk: int
|
|
|
|
:return: An HTTP response object that redirects to the updated Opportunity's
|
|
detail page, with a success message and the "HX-Refresh" header to trigger
|
|
frontend behavior.
|
|
:rtype: HttpResponse
|
|
"""
|
|
opportunity = get_object_or_404(models.Opportunity, 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):
|
|
"""
|
|
Handles the display and management of notification history for a user.
|
|
|
|
This class provides functionality to display a paginated list of user notifications
|
|
sorted by their creation time in descending order. It ensures that the user is logged
|
|
in before accessing the notifications and filters the displayed notifications specific
|
|
to the logged-in user.
|
|
|
|
:ivar model: Specifies the model to retrieve data from.
|
|
:type model: models.Notification
|
|
:ivar template_name: Specifies the template to render the notifications page.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the variable accessible in the template to
|
|
refer to the list of notifications.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: Defines the number of notifications to display per page.
|
|
:type paginate_by: int
|
|
:ivar ordering: Defines the default ordering of notifications.
|
|
:type ordering: str
|
|
"""
|
|
model = models.Notification
|
|
template_name = "crm/notifications_history.html"
|
|
context_object_name = "notifications"
|
|
paginate_by = 20
|
|
ordering = "-created"
|
|
|
|
def get_queryset(self):
|
|
return models.Notification.objects.filter(user=self.request.user)
|
|
|
|
|
|
# @login_required
|
|
# def mark_notification_as_read(request, pk):
|
|
# """
|
|
# Marks a user notification as read.
|
|
|
|
# This view allows an authenticated user to mark a specific notification,
|
|
# identified by its primary key, as read. Upon successfully marking the
|
|
# notification, a success message is displayed, and the user is redirected
|
|
# to their notification history page.
|
|
|
|
# :param request: The HTTP request object.
|
|
# :type request: HttpRequest
|
|
# :param pk: Primary key of the notification to be marked as read.
|
|
# :type pk: int
|
|
# :return: An HTTP response redirecting to the notification history page.
|
|
# :rtype: HttpResponse
|
|
# """
|
|
# notification = get_object_or_404(models.Notification, pk=pk, user=request.user)
|
|
# notification.is_read = True
|
|
# notification.save()
|
|
# messages.success(request, _("Notification marked as read"))
|
|
# return redirect("notifications_history")
|
|
|
|
|
|
# @login_required
|
|
# def fetch_notifications(request):
|
|
# """
|
|
# Fetches unread notifications for the currently logged-in user and renders them
|
|
# to the `notifications.html` template. The notifications are filtered to include
|
|
# only those belonging to the logged-in user and are sorted by creation date in
|
|
# descending order.
|
|
|
|
# :param request: The HTTP request object representing the current user request.
|
|
# Must include details of the logged-in user.
|
|
# :return: An HttpResponse object that renders the `notifications.html` template
|
|
# with the fetched notifications.
|
|
|
|
# """
|
|
# notifications = models.Notification.objects.filter(
|
|
# user=request.user, is_read=False
|
|
# ).order_by("-created")
|
|
|
|
# return render(request, "notifications.html", {"notifications_": notifications})
|
|
|
|
|
|
class ItemServiceCreateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView
|
|
):
|
|
"""
|
|
Manages the creation of additional services with permission checks, success messages,
|
|
and validation of VAT and dealer-specific pricing.
|
|
|
|
This class-based view facilitates the creation of additional service items within
|
|
a custom Django application framework. It includes mixins for login-required access,
|
|
permission control, and success message handling. The form is validated to apply VAT
|
|
rates to taxable services and associates the service with the dealer determined from
|
|
the logged-in user's context.
|
|
|
|
:ivar model: The model representing additional services.
|
|
:type model: models.AdditionalServices
|
|
:ivar form_class: The form class used for creating a service.
|
|
:type form_class: forms.AdditionalServiceForm
|
|
:ivar template_name: The template used to render the service creation page.
|
|
:type template_name: str
|
|
:ivar success_url: The redirect URL upon successful form submission.
|
|
:type success_url: str
|
|
:ivar success_message: The success message displayed upon successful form submission.
|
|
:type success_message: str
|
|
:ivar context_object_name: The context name used to refer to the service object in templates.
|
|
:type context_object_name: str
|
|
:ivar permission_required: The permissions required for accessing this view.
|
|
:type permission_required: list
|
|
"""
|
|
model = models.AdditionalServices
|
|
form_class = forms.AdditionalServiceForm
|
|
template_name = "items/service/service_create.html"
|
|
success_url = reverse_lazy("item_service_list")
|
|
success_message = _("Service created successfully")
|
|
context_object_name = "service"
|
|
permission_required = ["django_ledger.add_itemmodel"]
|
|
|
|
def form_valid(self, form):
|
|
vat = models.VatRate.objects.get(is_active=True)
|
|
dealer = get_user_type(self.request)
|
|
form.instance.dealer = dealer
|
|
if form.instance.taxable:
|
|
form.instance.price = (form.instance.price * vat.rate) + form.instance.price
|
|
return super().form_valid(form)
|
|
|
|
|
|
class ItemServiceUpdateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
|
):
|
|
"""
|
|
Handles update functionality for additional services in the system.
|
|
|
|
This view enables authenticated and authorized users to update instances of
|
|
the `AdditionalServices` model. It requires the user to have the necessary
|
|
permissions and provides success feedback upon successful update. Additionally,
|
|
it calculates and updates the price of the service based on the tax rate if
|
|
the service is taxable.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: models.AdditionalServices
|
|
:ivar form_class: The form class used for handling AdditionalService updates.
|
|
:type form_class: forms.AdditionalServiceForm
|
|
:ivar template_name: The template used for rendering the service update page.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect after a successful update.
|
|
:type success_url: str
|
|
:ivar success_message: The success message displayed after a successful update.
|
|
:type success_message: str
|
|
:ivar context_object_name: The context variable name for the object to be displayed.
|
|
:type context_object_name: str
|
|
:ivar permission_required: Permissions required for accessing this view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
model = models.AdditionalServices
|
|
form_class = forms.AdditionalServiceForm
|
|
template_name = "items/service/service_create.html"
|
|
success_url = reverse_lazy("item_service_list")
|
|
success_message = _("Service updated successfully")
|
|
context_object_name = "service"
|
|
permission_required = ["django_ledger.change_itemmodel"]
|
|
|
|
def form_valid(self, form):
|
|
vat = models.VatRate.objects.get(is_active=True)
|
|
dealer = get_user_type(self.request)
|
|
form.instance.dealer = dealer
|
|
if form.instance.taxable:
|
|
form.instance.price = (form.instance.price * vat.rate) + form.instance.price
|
|
return super().form_valid(form)
|
|
|
|
|
|
class ItemServiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Handles the listing of additional services for a dealer.
|
|
|
|
This class is responsible for displaying a paginated list of additional
|
|
services specific to a dealer. It enforces login, permission checking,
|
|
and customizes the queryset to return only services associated with the
|
|
currently logged-in dealer.
|
|
|
|
:ivar model: The model representing the additional services data.
|
|
:type model: Model
|
|
:ivar template_name: Path to the template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable for the
|
|
object list.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: Number of items displayed per page.
|
|
:type paginate_by: int
|
|
:ivar permission_required: List of permissions required to access the
|
|
view.
|
|
:type permission_required: list
|
|
"""
|
|
model = models.AdditionalServices
|
|
template_name = "items/service/service_list.html"
|
|
context_object_name = "services"
|
|
paginate_by = 20
|
|
permission_required = ["django_ledger.view_itemmodel"]
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
return models.AdditionalServices.objects.filter(dealer=dealer).all()
|
|
|
|
|
|
class ItemExpenseCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
|
"""
|
|
Represents a view for creating item expense entries.
|
|
|
|
This class is responsible for handling the creation of expense items in the
|
|
application. It integrates with Django's class-based views and includes
|
|
customizations specific to the current user and their associated entity.
|
|
Permission control ensures only authorized users can create expenses.
|
|
|
|
:ivar model: The database model used for this view.
|
|
:type model: ItemModel
|
|
:ivar form_class: The form class used to render and validate input.
|
|
:type form_class: ExpenseItemCreateForm
|
|
:ivar template_name: The path to the template used to render the view.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to upon successful form submission.
|
|
:type success_url: str
|
|
:ivar permission_required: A list of permissions required to access the view.
|
|
:type permission_required: list
|
|
"""
|
|
model = ItemModel
|
|
form_class = ExpenseItemCreateForm
|
|
template_name = "items/expenses/expense_create.html"
|
|
success_url = reverse_lazy("item_expense_list")
|
|
permission_required = ["django_ledger.add_itemmodel"]
|
|
|
|
def get_form_kwargs(self):
|
|
dealer = get_user_type(self.request)
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs["entity_slug"] = dealer.entity.slug
|
|
kwargs["user_model"] = dealer.entity.admin
|
|
return kwargs
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
form.instance.entity = dealer.entity
|
|
return super().form_valid(form)
|
|
|
|
|
|
class ItemExpenseUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
|
"""
|
|
Manages the update view for item expenses.
|
|
|
|
This class enables authenticated users with the necessary permissions to update
|
|
information related to item expenses. It integrates form handling and permission
|
|
requirements, ensuring entity-specific contextual data is appended to the form
|
|
during updates.
|
|
|
|
:ivar model: The model associated with the update operation.
|
|
:type model: ItemModel
|
|
:ivar form_class: The form used to validate and process updates.
|
|
:type form_class: ExpenseItemUpdateForm
|
|
:ivar template_name: Path to the template used for rendering the update view.
|
|
:type template_name: str
|
|
:ivar success_url: URL where the user is redirected after a successful update.
|
|
:type success_url: str
|
|
:ivar permission_required: List of permissions required to access this view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
model = ItemModel
|
|
form_class = ExpenseItemUpdateForm
|
|
template_name = "items/expenses/expense_update.html"
|
|
success_url = reverse_lazy("item_expense_list")
|
|
permission_required = ["django_ledger.change_itemmodel"]
|
|
|
|
def get_form_kwargs(self):
|
|
dealer = get_user_type(self.request)
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs["entity_slug"] = dealer.entity.slug
|
|
kwargs["user_model"] = dealer.entity.admin
|
|
return kwargs
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
form.instance.entity = dealer.entity
|
|
return super().form_valid(form)
|
|
|
|
|
|
class ItemExpenseListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Handles the display of a list of item expenses.
|
|
|
|
This class-based view is responsible for displaying a paginated list of item
|
|
expenses related to a specific entity. It uses the Django generic `ListView`
|
|
class and integrates with permissions and login requirements. It retrieves
|
|
data through the `get_queryset` method by invoking a specific function on
|
|
the user's associated entity object.
|
|
|
|
:ivar model: The model associated with the view.
|
|
:type model: type[ItemModel]
|
|
:ivar template_name: The template used to render the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The context variable name used in the template.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of items displayed per page.
|
|
:type paginate_by: int
|
|
:ivar permission_required: The list of required permissions to access the view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
model = ItemModel
|
|
template_name = "items/expenses/expenses_list.html"
|
|
context_object_name = "expenses"
|
|
paginate_by = 20
|
|
permission_required = ["django_ledger.view_itemmodel"]
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
return dealer.entity.get_items_expenses()
|
|
|
|
|
|
class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Provides a view for listing bills.
|
|
|
|
This class-based view is used to display a list of bills. It ensures that
|
|
the user is logged in and has the appropriate permissions to view the list
|
|
of bills. The view retrieves the bills associated with the dealer's entity
|
|
and displays them using the specified template.
|
|
|
|
:ivar model: The model associated with the view.
|
|
:type model: django.db.models.Model
|
|
:ivar template_name: The template used to render the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: Name of the context variable containing the list of bills.
|
|
:type context_object_name: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
model = BillModel
|
|
template_name = "ledger/bills/bill_list.html"
|
|
context_object_name = "bills"
|
|
permission_required = ["django_ledger.view_billmodel"]
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
qs = dealer.entity.get_bills()
|
|
return qs
|
|
|
|
|
|
class BillDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Handles the functionality for viewing detailed information about a single bill.
|
|
|
|
This class allows logged-in users with the required permissions to view specific bills
|
|
along with their attributes and calculated data provided in the context, such as a list
|
|
of detailed item transactions and a computed grand total.
|
|
|
|
:ivar model: The model associated with the view, which in this case represents bills.
|
|
:type model: BillModel
|
|
:ivar template_name: The template used for rendering the bill detail view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context object passed to the template.
|
|
:type context_object_name: str
|
|
:ivar permission_required: A list of permissions required to access this view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
model = BillModel
|
|
template_name = "ledger/bills/bill_detail.html"
|
|
context_object_name = "bill"
|
|
permission_required = ["django_ledger.view_billmodel"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
bill = kwargs.get("object")
|
|
if bill.get_itemtxs_data():
|
|
txs = bill.get_itemtxs_data()[0]
|
|
|
|
transactions = [
|
|
{
|
|
"item": x,
|
|
"total": Decimal(x.unit_cost) * Decimal(x.quantity),
|
|
}
|
|
for x in txs
|
|
]
|
|
grand_total = sum(Decimal(x.unit_cost) * Decimal(x.quantity) for x in txs)
|
|
|
|
kwargs["transactions"] = transactions
|
|
kwargs["grand_total"] = grand_total
|
|
|
|
return super().get_context_data(**kwargs)
|
|
|
|
|
|
class InReviewBillView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
|
"""
|
|
Handles the in-review update functionality for the BillModel.
|
|
|
|
This view is responsible for managing the update operations of bills that are in the
|
|
"in-review" state. It ensures all updates are performed according to the associated
|
|
permissions, and checks the user's type for processing additional context and logic.
|
|
The view also enforces that only authorized users with the required permission can
|
|
interact with it.
|
|
|
|
:ivar model: The model being used for this view, representing the BillModel.
|
|
:type model: BillModel
|
|
:ivar form_class: The form class used to handle the update of bill data.
|
|
:type form_class: InReviewBillModelUpdateForm
|
|
:ivar template_name: The template used to render the bill update form.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to upon successful bill update.
|
|
:type success_url: str
|
|
:ivar success_message: A message displayed upon successfully updating the bill.
|
|
:type success_message: str
|
|
:ivar context_object_name: The context variable name for the view's object.
|
|
:type context_object_name: str
|
|
:ivar permission_required: The list of permissions required to use this view.
|
|
:type permission_required: list
|
|
"""
|
|
model = BillModel
|
|
form_class = InReviewBillModelUpdateForm
|
|
template_name = "ledger/bills/bill_update_form.html"
|
|
success_url = reverse_lazy("bill_list")
|
|
success_message = _("Bill updated successfully")
|
|
context_object_name = "bill"
|
|
permission_required = ["django_ledger.change_billmodel"]
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
dealer = get_user_type(self.request)
|
|
kwargs["entity_model"] = dealer.entity
|
|
kwargs["user_model"] = dealer.entity.admin
|
|
return kwargs
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy("bill_detail", kwargs={"pk": self.kwargs["pk"]})
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
form.instance.entity = dealer.entity
|
|
self.object.mark_as_review()
|
|
return super().form_valid(form)
|
|
|
|
|
|
class ApprovedBillModelView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
|
"""
|
|
Represents a view to update and approve a bill.
|
|
|
|
This class provides the functionality to handle the update and
|
|
approval process of a bill within the system. It ensures only
|
|
authorized users can access and manage the approval. The view
|
|
uses specific permissions and form classes to manage this
|
|
process. Additionally, it provides feedback upon successful
|
|
completion of the operation, such as updating or approving the
|
|
bill.
|
|
|
|
:ivar model: The Django model associated with this view.
|
|
:type model: BillModel
|
|
:ivar form_class: The form class to use for handling bill updates.
|
|
:type form_class: ApprovedBillModelUpdateForm
|
|
:ivar template_name: The path to the HTML template for this view.
|
|
:type template_name: str
|
|
:ivar success_url: The redirect destination upon successful form submission.
|
|
:type success_url: str
|
|
:ivar success_message: The success message to display upon successful form submission.
|
|
:type success_message: str
|
|
:ivar context_object_name: The name of the context object.
|
|
:type context_object_name: str
|
|
:ivar permission_required: A list of permissions required to access this view.
|
|
:type permission_required: list
|
|
"""
|
|
model = BillModel
|
|
form_class = ApprovedBillModelUpdateForm
|
|
template_name = "ledger/bills/bill_update_form.html"
|
|
success_url = reverse_lazy("bill_list")
|
|
success_message = _("Bill updated successfully")
|
|
context_object_name = "bill"
|
|
permission_required = ["django_ledger.change_billmodel"]
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
dealer = get_user_type(self.request)
|
|
kwargs["entity_model"] = dealer.entity
|
|
kwargs["user_model"] = dealer.entity.admin
|
|
return kwargs
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy("bill_detail", kwargs={"pk": self.kwargs["pk"]})
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
form.instance.entity = dealer.entity
|
|
if not self.object.is_approved():
|
|
self.object.mark_as_approved(user_model=dealer.entity.admin)
|
|
return super().form_valid(form)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.change_billmodel", raise_exception=True)
|
|
def bill_mark_as_approved(request, pk):
|
|
"""
|
|
Marks a bill as approved for the given bill ID (primary key) if it is not
|
|
already approved. This action can only be completed by an authorized user
|
|
with the corresponding permissions. Once the bill is approved, it is saved
|
|
and a success message is displayed. If the bill is already approved, an
|
|
error message is shown instead.
|
|
|
|
:param request: HttpRequest object representing the current request.
|
|
:param pk: Primary key of the bill to be marked as approved.
|
|
:return: HttpResponseRedirect to the bill detail page after the operation is
|
|
completed or an error/success message is set.
|
|
"""
|
|
bill = get_object_or_404(BillModel, pk=pk)
|
|
if request.method == "POST":
|
|
dealer = get_user_type(request)
|
|
if bill.is_approved():
|
|
messages.error(request, _("Bill is already approved"))
|
|
return redirect("bill_detail", pk=bill.pk)
|
|
bill.mark_as_approved(user_model=dealer.entity.admin)
|
|
bill.save()
|
|
messages.success(request, _("Bill marked as approved successfully"))
|
|
return redirect("bill_detail", pk=bill.pk)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.change_billmodel", raise_exception=True)
|
|
def bill_mark_as_paid(request, pk):
|
|
"""
|
|
Marks a bill as paid if certain conditions are met and updates the ledger accordingly.
|
|
|
|
This function is used to mark a specific bill as paid. It verifies whether the bill
|
|
is already paid or if the amount paid matches the amount due. If the conditions are
|
|
met, it updates the bill's status, locks the journal entries in the associated ledger,
|
|
posts them, and saves the changes. Appropriate success or error messages are displayed
|
|
to the user, and the user is redirected to the bill details page.
|
|
|
|
:param request: The HTTP request object containing details about the request made by the user.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the bill to be marked as paid.
|
|
:type pk: int
|
|
:return: A redirect response to the bill details page.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
bill = get_object_or_404(BillModel, pk=pk)
|
|
if request.method == "POST":
|
|
dealer = get_user_type(request)
|
|
if bill.is_paid():
|
|
messages.error(request, _("Bill is already paid"))
|
|
return redirect("bill_detail", pk=bill.pk)
|
|
if bill.amount_due == bill.amount_paid:
|
|
bill.mark_as_paid(user_model=dealer.entity.admin)
|
|
bill.save()
|
|
bill.ledger.lock_journal_entries()
|
|
bill.ledger.post_journal_entries()
|
|
bill.ledger.post()
|
|
bill.ledger.save()
|
|
messages.success(request, _("Bill marked as paid successfully"))
|
|
else:
|
|
messages.error(request, _("Amount paid is not equal to amount due"))
|
|
return redirect("bill_detail", pk=bill.pk)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.add_billmodel", raise_exception=True)
|
|
def bill_create(request):
|
|
"""
|
|
Handles creation of a bill in the system, including the validation of input data,
|
|
creation of transactions associated with the bill, and rendering of the appropriate
|
|
response or form. Ensures the user creating the bill has the necessary permissions and
|
|
correct input parameters for successful bill creation and itemization.
|
|
|
|
:param request: Django HttpRequest object containing metadata and data of the HTTP request.
|
|
:type request: HttpRequest
|
|
:return: JsonResponse with success/error information if the request is processed,
|
|
or HttpResponse rendering the form for bill creation.
|
|
:rtype: JsonResponse or HttpResponse
|
|
"""
|
|
dealer = get_user_type(request)
|
|
entity = dealer.entity
|
|
|
|
if request.method == "POST":
|
|
data = json.loads(request.body)
|
|
vendor_id = data.get("vendor")
|
|
terms = data.get("terms")
|
|
vendor = entity.get_vendors().filter(pk=vendor_id).first()
|
|
|
|
items = data.get("item", [])
|
|
quantities = data.get("quantity", [])
|
|
|
|
if not all([items, quantities]):
|
|
return JsonResponse(
|
|
{"status": "error", "message": _("Items and Quantities are required")},
|
|
status=400,
|
|
)
|
|
if isinstance(quantities, list):
|
|
if "0" in quantities:
|
|
return JsonResponse(
|
|
{"status": "error", "message": _("Quantity must be greater than zero")}
|
|
)
|
|
else:
|
|
if int(quantities) <= 0:
|
|
return JsonResponse(
|
|
{"status": "error", "message": _("Quantity must be greater than zero")}
|
|
)
|
|
|
|
bill = entity.create_bill(vendor_model=vendor, terms=terms)
|
|
if isinstance(items, list):
|
|
item_quantity_map = {}
|
|
for item, quantity in zip(items, quantities):
|
|
if item in item_quantity_map:
|
|
item_quantity_map[item] += int(quantity)
|
|
else:
|
|
item_quantity_map[item] = int(quantity)
|
|
item_list = list(item_quantity_map.keys())
|
|
quantity_list = list(item_quantity_map.values())
|
|
|
|
items_list = [
|
|
{"item_id": item_list[i], "quantity": quantity_list[i]}
|
|
for i in range(len(item_list))
|
|
]
|
|
items_txs = []
|
|
for item in items_list:
|
|
item_instance = ItemModel.objects.get(pk=item.get("item_id"))
|
|
car = models.Car.objects.get(vin=item_instance.name)
|
|
quantity = Decimal(item.get("quantity"))
|
|
items_txs.append(
|
|
{
|
|
"item_number": item_instance.item_number,
|
|
"quantity": quantity,
|
|
"unit_cost": car.finances.cost_price,
|
|
"total_amount": car.finances.cost_price * quantity,
|
|
}
|
|
)
|
|
|
|
bill_itemtxs = {
|
|
item.get("item_number"): {
|
|
"unit_cost": item.get("unit_cost"),
|
|
"quantity": item.get("quantity"),
|
|
"total_amount": item.get("total_amount"),
|
|
}
|
|
for item in items_txs
|
|
}
|
|
else:
|
|
item = entity.get_items_all().filter(pk=items).first()
|
|
instance = models.Car.objects.get(vin=item.name)
|
|
bill_itemtxs = {
|
|
item.item_number: {
|
|
"unit_cost": instance.finances.cost_price,
|
|
"quantity": Decimal(quantities),
|
|
"total_amount": instance.finances.cost_price * Decimal(quantities),
|
|
}
|
|
}
|
|
|
|
bill_itemtxs = bill.migrate_itemtxs(
|
|
itemtxs=bill_itemtxs,
|
|
commit=True,
|
|
operation=BillModel.ITEMIZE_APPEND,
|
|
)
|
|
|
|
url = reverse("bill_detail", kwargs={"pk": bill.pk})
|
|
return JsonResponse(
|
|
{
|
|
"status": "success",
|
|
"message": _("Bill created successfully"),
|
|
"url": f"{url}",
|
|
}
|
|
)
|
|
|
|
form = forms.BillModelCreateForm(entity_model=entity)
|
|
form.initial.update(
|
|
{
|
|
"cash_account": dealer.settings.bill_cash_account,
|
|
"prepaid_account": dealer.settings.bill_prepaid_account,
|
|
"unearned_account": dealer.settings.bill_unearned_account,
|
|
}
|
|
)
|
|
car_list = models.Car.objects.filter(dealer=dealer)
|
|
context = {
|
|
"form": form,
|
|
"items": [
|
|
{
|
|
"car": x,
|
|
"product": entity.get_items_products().filter(name=x.vin).first(),
|
|
}
|
|
for x in car_list
|
|
],
|
|
}
|
|
|
|
return render(request, "ledger/bills/bill_form.html", context)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.delete_billmodel", raise_exception=True)
|
|
def BillDeleteView(request, pk):
|
|
"""
|
|
Deletes a specific BillModel instance based on the primary key (pk).
|
|
This view requires the user to be logged in and have the appropriate
|
|
permission to delete a bill. After the delete operation is successful,
|
|
the user is redirected to the bill list view.
|
|
|
|
:param request: The HTTP request object.
|
|
:type request: HttpRequest
|
|
:param pk: Primary key of the BillModel instance to be deleted.
|
|
:type pk: int
|
|
:return: Redirect to the bill list view after successful deletion.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
bill = get_object_or_404(BillModel, pk=pk)
|
|
bill.delete()
|
|
return redirect("bill_list")
|
|
|
|
|
|
# orders
|
|
class OrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Represents a view for listing sale orders.
|
|
|
|
This class provides the functionality to display a list of sale orders
|
|
within a web template. It includes permission-based access control, and
|
|
retrieves sale orders corresponding to the entity of the logged-in dealer.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: models.SaleOrder
|
|
:ivar template_name: The name of the template to be rendered.
|
|
:type template_name: str
|
|
:ivar context_object_name: The context variable name to be used for the object list.
|
|
:type context_object_name: str
|
|
:ivar permission_required: A list of permissions required to access this view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
model = models.SaleOrder
|
|
template_name = "sales/orders/order_list.html"
|
|
context_object_name = "orders"
|
|
permission_required = ["inventory.view_saleorder"]
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
qs = super().get_queryset()
|
|
return qs.filter(estimate__entity=dealer.entity)
|
|
|
|
|
|
# email
|
|
@login_required
|
|
@permission_required("django_ledger.view_estimatemodel", raise_exception=True)
|
|
def send_email_view(request, pk):
|
|
"""
|
|
View function to send an email for an estimate. This function allows authenticated and
|
|
authorized users to send an email containing the estimate details to the customer.
|
|
The email includes a link to preview the estimate and a message template in multiple
|
|
languages. If the estimate does not have any associated items, the user will receive
|
|
an error message. Upon successfully sending the email, the estimate is marked as reviewed.
|
|
|
|
:param request: The HttpRequest object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the estimate to be sent via email.
|
|
:type pk: int
|
|
:return: An HttpResponseRedirect to the estimate detail view.
|
|
:rtype: HttpResponseRedirect
|
|
:raises Http404: If the estimate with the given primary key does not exist.
|
|
"""
|
|
dealer = get_user_type(request)
|
|
estimate = get_object_or_404(EstimateModel, pk=pk)
|
|
|
|
if not estimate.get_itemtxs_data()[0]:
|
|
messages.error(request, _("Quotation has no items"))
|
|
return redirect("estimate_detail", pk=estimate.pk)
|
|
|
|
link = request.build_absolute_uri(
|
|
reverse_lazy("estimate_preview", kwargs={"pk": estimate.pk})
|
|
)
|
|
|
|
msg = f"""
|
|
السلام عليكم
|
|
Dear {estimate.customer.customer_name},
|
|
|
|
أود أن أشارككم عرض السعر.
|
|
|
|
I wanted to share with you the quotation.
|
|
|
|
يرجى مراجعة عرض السعر وإعلامي إذا كانت لديك أي استفسارات أو ملاحظات. إذا كان كل شيء على ما يرام، يمكننا المتابعة في الإجراءات.
|
|
|
|
Please review the quotation and let me know if you have any questions or concerns. If everything looks good, we can proceed with the process.
|
|
|
|
رابط عرض السعر:
|
|
{link}
|
|
|
|
|
|
تحياتي,
|
|
Best regards,
|
|
{dealer.get_local_name}
|
|
{dealer.phone_number}
|
|
هيكل | Haikal
|
|
"""
|
|
# subject = _("Quotation")
|
|
|
|
send_email(
|
|
str(settings.DEFAULT_FROM_EMAIL),
|
|
estimate.customer.email,
|
|
"عرض سعر - Quotation",
|
|
msg,
|
|
)
|
|
|
|
estimate.mark_as_review()
|
|
messages.success(request, _("Email sent successfully"))
|
|
|
|
return redirect("estimate_detail", pk=estimate.pk)
|
|
|
|
|
|
# errors
|
|
def custom_page_not_found_view(request, exception=None):
|
|
"""
|
|
Custom handler for 404 errors that renders a custom HTML page
|
|
upon encountering a Page Not Found error.
|
|
|
|
:param request: The HttpRequest object associated with the request.
|
|
:type request: HttpRequest
|
|
:param exception: The exception that triggered the 404 error, if any.
|
|
:type exception: Exception, optional
|
|
:return: An HttpResponse object rendering the "errors/404.html" template.
|
|
:rtype: HttpResponse
|
|
"""
|
|
return render(request, "errors/404.html", {})
|
|
|
|
|
|
def custom_error_view(request, exception=None):
|
|
"""
|
|
Handles rendering the custom error page for HTTP 500 errors.
|
|
|
|
This function is called when an unhandled exception occurs in the application. It renders
|
|
a predefined template for server errors, providing a consistent error page layout
|
|
to the users.
|
|
|
|
:param request: The HTTP request instance which triggered the error.
|
|
:type request: HttpRequest
|
|
:param exception: The exception that caused the error to trigger. Defaults to None.
|
|
:type exception: Exception, optional
|
|
:return: An HttpResponse object representing the rendered error page.
|
|
:rtype: HttpResponse
|
|
"""
|
|
return render(request, "errors/500.html", {})
|
|
|
|
|
|
def custom_permission_denied_view(request, exception=None):
|
|
"""
|
|
Handles the custom view for 403 Forbidden permission denied errors. This
|
|
function renders a custom template for the error page when users are denied
|
|
permission to access a particular resource or view.
|
|
|
|
:param request: Django HttpRequest object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param exception: Optional exception that caused the 403 error.
|
|
Defaults to None.
|
|
:type exception: Exception | None
|
|
:return: HttpResponse object rendering the 403.html error page.
|
|
:rtype: HttpResponse
|
|
"""
|
|
return render(request, "errors/403.html", {})
|
|
|
|
|
|
def custom_bad_request_view(request, exception=None):
|
|
"""
|
|
Handles custom bad request responses by rendering a specified HTML
|
|
template for 400 status errors.
|
|
|
|
:return: Rendered HTTP response rendering the error page.
|
|
:rtype: HttpResponse
|
|
"""
|
|
return render(request, "errors/400.html", {})
|
|
|
|
|
|
# BALANCE SHEET
|
|
class BaseBalanceSheetRedirectView(RedirectView):
|
|
"""
|
|
Handles redirection for balance sheet views of entities.
|
|
|
|
This class is specifically designed to redirect users to the
|
|
appropriate year's balance sheet for an entity. It determines
|
|
the URL based on the current year and the provided entity slug
|
|
in the request. Additionally, it provides a URL for login redirection
|
|
if required.
|
|
|
|
:ivar url: The original URL or base URL to redirect users.
|
|
:type url: str
|
|
"""
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
year = get_localdate().year
|
|
return reverse(
|
|
"entity-bs-year",
|
|
kwargs={"entity_slug": self.kwargs["entity_slug"], "year": year},
|
|
)
|
|
|
|
def get_login_url(self):
|
|
return reverse("account_login")
|
|
|
|
|
|
class FiscalYearBalanceSheetViewBase(
|
|
FiscalYearBalanceSheetView, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Defines a base view for the fiscal year balance sheet.
|
|
|
|
This class serves as a base view for displaying the balance sheet of a fiscal
|
|
year. It provides functionality for rendering the associated template and
|
|
managing user login requirements. It integrates with Django's security system
|
|
to ensure appropriate access control.
|
|
|
|
:ivar template_name: The template used for rendering the balance sheet view.
|
|
:type template_name: str
|
|
"""
|
|
template_name = "ledger/reports/balance_sheet.html"
|
|
# AUTHORIZE_SUPERUSER = False
|
|
# permission_required = []
|
|
|
|
# def get_authorized_entity_queryset(self):
|
|
# dealer = get_user_type(self.request)
|
|
# return EntityModel.objects.filter(admin=dealer)
|
|
|
|
def get_login_url(self):
|
|
return reverse("account_login")
|
|
|
|
|
|
class QuarterlyBalanceSheetView(
|
|
FiscalYearBalanceSheetViewBase, QuarterlyReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a quarterly balance sheet view.
|
|
|
|
The purpose of this class is to provide a representation and handling
|
|
for the balance sheet data specific to fiscal quarters. It extends from
|
|
`FiscalYearBalanceSheetViewBase` to inherit year-level balance sheet
|
|
operations and incorporates functionality from `QuarterlyReportMixIn`
|
|
to manage quarterly-specific logic. Additionally, the `DjangoLedgerSecurityMixIn`
|
|
is used to handle security features related to Django ledger operations.
|
|
|
|
:ivar fiscal_year: The fiscal year associated with the quarterly balance sheet.
|
|
:type fiscal_year: int
|
|
:ivar quarter: The specific quarter (e.g., Q1, Q2, Q3, Q4) for the balance sheet.
|
|
:type quarter: int
|
|
:ivar balance_data: The data structure representing the detailed financial
|
|
information for the balance sheet of the quarter.
|
|
:type balance_data: dict
|
|
"""
|
|
|
|
|
|
class MonthlyBalanceSheetView(
|
|
FiscalYearBalanceSheetViewBase, MonthlyReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents the view for the monthly balance sheet.
|
|
|
|
This class is responsible for handling the presentation and processing of
|
|
monthly balance sheet data. It incorporates necessary mix-ins to provide
|
|
functionality specific to fiscal year balance sheet viewing, monthly reporting,
|
|
and security validation within the Django ledger context.
|
|
|
|
:ivar fiscal_year: Stores the fiscal year information associated with the
|
|
balance sheet.
|
|
:type fiscal_year: int
|
|
:ivar monthly_reports: Holds a collection of monthly report instances that
|
|
pertain to the balance sheet.
|
|
:type monthly_reports: List[MonthlyReport]
|
|
:ivar user_permissions: Manages user-specific permissions for accessing and
|
|
modifying the balance sheet data.
|
|
:type user_permissions: Dict[str, Any]
|
|
"""
|
|
|
|
|
|
class DateBalanceSheetView(
|
|
FiscalYearBalanceSheetViewBase, DateReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a balance sheet view for a specific date.
|
|
|
|
DateBalanceSheetView extends functionality for displaying balance sheets
|
|
specific to certain dates. This class combines features from various mixins
|
|
to enforce security, handle date-based reporting, and provide fiscal year
|
|
context. It is used in scenarios where financial reports for a particular
|
|
date are required to ensure accurate and filtered viewing of accounting
|
|
data.
|
|
|
|
:ivar fiscal_year: Represents the fiscal year context for the balance sheet.
|
|
:type fiscal_year: FiscalYear
|
|
:ivar report_date: The specific date for which the balance sheet report
|
|
is generated.
|
|
:type report_date: datetime.date
|
|
:ivar is_authenticated: Indicates whether the user accessing the
|
|
balance sheet has been authenticated.
|
|
:type is_authenticated: bool
|
|
:ivar account_data: Contains processed account data used in the balance sheet
|
|
for computations or display purposes.
|
|
:type account_data: dict
|
|
"""
|
|
|
|
|
|
# Income Statement -----------
|
|
|
|
|
|
class BaseIncomeStatementRedirectViewBase(
|
|
BaseIncomeStatementRedirectView, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
The BaseIncomeStatementRedirectViewBase class provides functionality for handling
|
|
redirects in the context of income statements. This class combines features from
|
|
both BaseIncomeStatementRedirectView and DjangoLedgerSecurityMixIn to ensure
|
|
secure handling of data and user authentication.
|
|
|
|
This class operates by calculating the current year, determining the user's dealer
|
|
type, and generating a URL to redirect to an income statement view associated with a
|
|
specific entity for the calculated year. Additionally, it defines a method to generate
|
|
the login URL for unauthenticated users.
|
|
|
|
:ivar request: The HTTP request object containing user session and other request-related data.
|
|
:type request: HttpRequest
|
|
"""
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
year = get_localdate().year
|
|
dealer = get_user_type(self.request)
|
|
return reverse(
|
|
"entity-ic-year", kwargs={"entity_slug": dealer.entity.slug, "year": year}
|
|
)
|
|
|
|
def get_login_url(self):
|
|
return reverse("account_login")
|
|
|
|
|
|
class FiscalYearIncomeStatementViewBase(
|
|
FiscalYearIncomeStatementView, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a base view for fiscal year income statement.
|
|
|
|
This class serves as a base view for generating and rendering the fiscal
|
|
year income statement within the application. It combines multiple
|
|
functionalities, including template rendering and permission handling,
|
|
to provide a secure and user-friendly interface.
|
|
|
|
:ivar template_name: Path to the HTML template used for rendering the
|
|
income statement view.
|
|
:type template_name: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
template_name = "ledger/reports/income_statement.html"
|
|
permission_required = ["inventory.view_carfinance"]
|
|
|
|
def get_login_url(self):
|
|
return reverse("account_login")
|
|
|
|
|
|
class QuarterlyIncomeStatementView(
|
|
FiscalYearIncomeStatementViewBase, QuarterlyReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a detailed view for a quarterly income statement.
|
|
|
|
This class is responsible for providing a structured view of a company's
|
|
quarterly income statement. It combines the functionality of multiple
|
|
base classes to deliver a comprehensive representation of quarterly report
|
|
data while ensuring integration with Django Ledger security protocols.
|
|
|
|
The class serves as a specialized view tied to specific fiscal data,
|
|
enabling efficient management and rendering of quarterly financial reports.
|
|
|
|
:ivar fiscal_year: The fiscal year associated with the quarterly income
|
|
statement.
|
|
:type fiscal_year: int
|
|
:ivar quarter: The quarter number (e.g., 1 for Q1, 2 for Q2, etc.) of the
|
|
income statement.
|
|
:type quarter: int
|
|
:ivar revenue: The total revenue recorded in the quarterly income statement.
|
|
:type revenue: float
|
|
:ivar net_income: The net income calculated for the quarter.
|
|
:type net_income: float
|
|
:ivar expenses: The total expenses incurred during the quarter.
|
|
:type expenses: float
|
|
"""
|
|
|
|
|
|
class MonthlyIncomeStatementView(
|
|
FiscalYearIncomeStatementViewBase, MonthlyReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents the view for a monthly income statement in the financial
|
|
application.
|
|
|
|
This class is designed to support the presentation and display of
|
|
financial income statement data on a monthly basis. It incorporates
|
|
functionality from multiple mixins to include specific fiscal year
|
|
operations, monthly report behaviors, and security aspects tailored
|
|
to the Django Ledger system.
|
|
|
|
:ivar fiscal_year: The fiscal year associated with the income
|
|
statement.
|
|
:type fiscal_year: int
|
|
:ivar month: The month for which the income statement is viewed.
|
|
:type month: int
|
|
:ivar data: The financial data contained in the income statement.
|
|
:type data: dict
|
|
:ivar authorized_user: The user authorized to access this income
|
|
statement view.
|
|
:type authorized_user: str
|
|
"""
|
|
|
|
|
|
class DateModelIncomeStatementView(
|
|
FiscalYearIncomeStatementViewBase, DateReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a detailed view of an income statement for a fiscal year with additional
|
|
capabilities for handling dates and security integrations.
|
|
|
|
This class is a specialized combination of views and mixins designed to manage income
|
|
statements with added functionality for date-based operations and Django-based ledger
|
|
security. It inherits behaviors for fiscal year reports, date handling, and security
|
|
enforcements, providing a cohesive interface for managing income statement data.
|
|
|
|
:ivar fiscal_year_data: Contains detailed income statement data for a specific fiscal year.
|
|
:type fiscal_year_data: dict
|
|
:ivar report_date: Represents the date of the associated report.
|
|
:type report_date: datetime.date
|
|
:ivar user_permissions: Tracks security permissions associated with the income statement for
|
|
authenticated users.
|
|
:type user_permissions: dict
|
|
"""
|
|
|
|
|
|
# Cash Flow -----------
|
|
|
|
|
|
class BaseCashFlowStatementRedirectViewBase(
|
|
BaseCashFlowStatementRedirectView, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Base class for handling cash flow statement redirection views.
|
|
|
|
This class is designed to manage the redirection of users to the
|
|
appropriate cash flow statement view for a particular year and entity.
|
|
It incorporates security features and functionalities specific to dealers
|
|
or entities by extending relevant mixins.
|
|
|
|
:ivar request: The HTTP request object associated with the view.
|
|
:type request: HttpRequest
|
|
"""
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
year = get_localdate().year
|
|
dealer = get_user_type(self.request)
|
|
return reverse(
|
|
"entity-cf-year", kwargs={"entity_slug": dealer.entity.slug, "year": year}
|
|
)
|
|
|
|
def get_login_url(self):
|
|
return reverse("account_login")
|
|
|
|
|
|
class FiscalYearCashFlowStatementViewBase(
|
|
FiscalYearCashFlowStatementView, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a base view for fiscal year cash flow statements.
|
|
|
|
This class is utilized to handle the rendering and permissions control
|
|
for fiscal year cash flow statement views. It inherits functionality
|
|
from FiscalYearCashFlowStatementView and DjangoLedgerSecurityMixIn
|
|
to integrate with the Django framework's security and reporting
|
|
mechanisms.
|
|
|
|
:ivar template_name: Template path to render the view.
|
|
:type template_name: str
|
|
:ivar permission_required: List of permissions required for accessing
|
|
the view.
|
|
:type permission_required: list
|
|
"""
|
|
template_name = "ledger/reports/cash_flow_statement.html"
|
|
permission_required = ["inventory.view_carfinance"]
|
|
|
|
def get_login_url(self):
|
|
return reverse("account_login")
|
|
|
|
|
|
class QuarterlyCashFlowStatementView(
|
|
FiscalYearCashFlowStatementViewBase, QuarterlyReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a view model for quarterly cash flow statements.
|
|
|
|
This class is designed to encapsulate and handle data related to
|
|
quarterly cash flow statements, extending the functionality from
|
|
base classes and mix-ins for specific behaviors. It combines features
|
|
from fiscal year cash flow statements, quarterly reporting utilities,
|
|
and Django ledger security mechanisms to provide comprehensive support
|
|
for managing and displaying financial data for quarterly cash flows.
|
|
|
|
:ivar fiscal_year: The fiscal year associated with the cash flow
|
|
statement.
|
|
:type fiscal_year: int
|
|
:ivar quarter: The quarter associated with the cash flow statement,
|
|
typically denoted as an integer from 1 to 4.
|
|
:type quarter: int
|
|
:ivar cash_flow_data: A structure containing detailed cash flow
|
|
information for the given quarter and fiscal year.
|
|
:type cash_flow_data: dict
|
|
:ivar is_audited: Boolean flag indicating whether the quarterly cash
|
|
flow statement has been audited.
|
|
:type is_audited: bool
|
|
"""
|
|
|
|
|
|
class MonthlyCashFlowStatementView(
|
|
FiscalYearCashFlowStatementViewBase, MonthlyReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a view for monthly cash flow statements.
|
|
|
|
This class combines the functionality of fiscal year cash flow statement views,
|
|
monthly reporting capabilities, and security features related to Django Ledger.
|
|
It is utilized to generate or retrieve cash flow statements on a monthly basis,
|
|
ensuring seamless integration with other components that extend or implement
|
|
financial reporting logic.
|
|
|
|
:ivar attribute1: Description of attribute1.
|
|
:type attribute1: type
|
|
:ivar attribute2: Description of attribute2.
|
|
:type attribute2: type
|
|
"""
|
|
|
|
|
|
class DateCashFlowStatementView(
|
|
FiscalYearCashFlowStatementViewBase, DateReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Representation of a detailed view for a cash flow statement associated with a specific fiscal year,
|
|
including reporting capabilities and security features. This class extends functionality from
|
|
multiple mix-in classes to provide a comprehensive cash flow statement representation with date-specific
|
|
reporting.
|
|
|
|
:ivar field1: Description of field1.
|
|
:type field1: type
|
|
:ivar field2: Description of field2.
|
|
:type field2: type
|
|
"""
|
|
|
|
|
|
# Dashboard
|
|
|
|
|
|
class EntityModelDetailHandlerViewBase(
|
|
EntityModelDetailHandlerView, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Handles detailed views for Entity Models along with redirection logic
|
|
customized for different users and units.
|
|
|
|
This class extends functionality from `EntityModelDetailHandlerView` and
|
|
integrates additional security measures using `DjangoLedgerSecurityMixIn`.
|
|
It provides a mechanism to construct redirection URLs based on the specifics
|
|
of the current request, such as the user's dealer type, the unit slug, and the
|
|
current localized date. Typically used for redirecting to appropriate dashboards
|
|
with monthly data filtered by either entity or unit.
|
|
|
|
:ivar request: The HTTP request object associated with the current view.
|
|
:type request: HttpRequest
|
|
"""
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
loc_date = get_localdate()
|
|
dealer = get_user_type(self.request)
|
|
unit_slug = self.get_unit_slug()
|
|
if unit_slug:
|
|
return reverse(
|
|
"unit-dashboard-month",
|
|
kwargs={
|
|
"entity_slug": dealer.entity.slug,
|
|
"unit_slug": unit_slug,
|
|
"year": loc_date.year,
|
|
"month": loc_date.month,
|
|
},
|
|
)
|
|
return reverse(
|
|
"entity-dashboard-month",
|
|
kwargs={
|
|
"entity_slug": dealer.entity.slug,
|
|
"year": loc_date.year,
|
|
"month": loc_date.month,
|
|
},
|
|
)
|
|
|
|
|
|
class EntityModelDetailBaseViewBase(
|
|
EntityModelDetailBaseView, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a base view that extends functionality for displaying detailed
|
|
dashboard information about an entity model. Handles context population for
|
|
dynamic elements like charts and titles specific to the entity or an optional
|
|
unit.
|
|
|
|
This class is designed as part of a Django application, utilizing mixins for
|
|
enhanced security and inheriting a basic entity model detail view. It is
|
|
customized to render a specific dashboard template and manage context data
|
|
for charts and other display elements relevant to the entity.
|
|
|
|
:ivar template_name: Path to the HTML template used for rendering the
|
|
dashboard.
|
|
:type template_name: str
|
|
"""
|
|
template_name = "ledger/reports/dashboard.html"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
dealer = get_user_type(self.request)
|
|
entity_model: EntityModel = dealer.entity
|
|
context["page_title"] = entity_model.name
|
|
context["header_title"] = entity_model.name
|
|
context["header_subtitle"] = _("Dashboard")
|
|
context["header_subtitle_icon"] = "mdi:monitor-dashboard"
|
|
|
|
unit_slug = context.get("unit_slug", self.get_unit_slug())
|
|
KWARGS = dict(entity_slug=self.kwargs["entity_slug"])
|
|
|
|
if unit_slug:
|
|
KWARGS["unit_slug"] = unit_slug
|
|
|
|
url_pointer = "entity" if not unit_slug else "unit"
|
|
context["pnl_chart_id"] = f"djl-entity-pnl-chart-{randint(10000, 99999)}"
|
|
context["pnl_chart_endpoint"] = reverse(
|
|
f"django_ledger:{url_pointer}-json-pnl", kwargs=KWARGS
|
|
)
|
|
context["payables_chart_id"] = (
|
|
f"djl-entity-payables-chart-{randint(10000, 99999)}"
|
|
)
|
|
context["payables_chart_endpoint"] = reverse(
|
|
f"django_ledger:{url_pointer}-json-net-payables", kwargs=KWARGS
|
|
)
|
|
context["receivables_chart_id"] = (
|
|
f"djl-entity-receivables-chart-{randint(10000, 99999)}"
|
|
)
|
|
context["receivables_chart_endpoint"] = reverse(
|
|
f"django_ledger:{url_pointer}-json-net-receivables", kwargs=KWARGS
|
|
)
|
|
|
|
return context
|
|
|
|
|
|
class FiscalYearEntityModelDashboardView(
|
|
EntityModelDetailBaseViewBase, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a dashboard view for fiscal year entity models.
|
|
|
|
This class serves as a view to display details related to fiscal year
|
|
entity models while enforcing permissions and login requirements.
|
|
It integrates security features and provides functionalities specific
|
|
to managing fiscal year entity models within the dashboard.
|
|
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
permission_required = ["inventory.view_carfinance"]
|
|
|
|
def get_login_url(self):
|
|
return reverse("account_login")
|
|
|
|
|
|
class QuarterlyEntityDashboardView(
|
|
FiscalYearEntityModelDashboardView, QuarterlyReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a dashboard view for quarterly entities.
|
|
|
|
This class integrates the functionalities of `FiscalYearEntityModelDashboardView`,
|
|
`QuarterlyReportMixIn`, and `DjangoLedgerSecurityMixIn` to provide a dashboard
|
|
view that displays and organizes quarterly financial metrics and reports for a
|
|
specific fiscal year entity. It facilitates secure access to financial data and
|
|
provides interfaces to analyze quarterly reports.
|
|
|
|
:ivar quarters: A list of quarters included in the dashboard view.
|
|
:type quarters: list
|
|
:ivar entity: The entity for which the dashboard view is generated.
|
|
:type entity: object
|
|
:ivar fiscal_year: The fiscal year associated with the dashboard.
|
|
:type fiscal_year: int
|
|
:ivar reporting_data: A collection of data specifically prepared for
|
|
quarterly reporting.
|
|
:type reporting_data: dict
|
|
"""
|
|
|
|
|
|
class MonthlyEntityDashboardView(
|
|
FiscalYearEntityModelDashboardView, MonthlyReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a dashboard view for a specific entity's monthly report.
|
|
|
|
This class combines functionalities from the base dashboard view for
|
|
fiscal year entities, as well as specific monthly report and security
|
|
features, to provide a comprehensive dashboard handler. It is designed
|
|
to work within the Django Ledger system, facilitating secured and
|
|
contextual data display tailored for monthly reporting of financial
|
|
entities.
|
|
|
|
:ivar fiscal_year: Represents the fiscal year associated with the
|
|
dashboard view.
|
|
:type fiscal_year: int
|
|
:ivar entity_id: The unique identifier for the financial entity
|
|
being reported on in the dashboard.
|
|
:type entity_id: int
|
|
:ivar report_month: The specific month for which the report is
|
|
generated and displayed.
|
|
:type report_month: str
|
|
:ivar user_permissions: Permissions associated with the currently
|
|
authenticated user to enforce security on
|
|
sensitive actions or data.
|
|
:type user_permissions: list
|
|
"""
|
|
|
|
|
|
class DateEntityDashboardView(
|
|
FiscalYearEntityModelDashboardView, DateReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a dashboard view for date-based entity data visualization.
|
|
|
|
This class integrates date-based information into the dashboard view,
|
|
extending the functionality of `FiscalYearEntityModelDashboardView`.
|
|
It utilizes mixins to incorporate report generation and security features
|
|
specific to the Django Ledger framework.
|
|
|
|
:ivar fiscal_year: The fiscal year associated with the dashboard data.
|
|
:type fiscal_year: FiscalYear
|
|
:ivar date_report_data: Data pertaining to the date-specific reports displayed.
|
|
:type date_report_data: dict
|
|
:ivar user_permissions: The security permissions granted to the user for viewing
|
|
or interacting with the dashboard.
|
|
:type user_permissions: set
|
|
"""
|
|
|
|
|
|
class PayableNetAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View):
|
|
"""
|
|
Handles the retrieval of net payable data for authenticated users.
|
|
|
|
The PayableNetAPIView is designed to process GET requests for retrieving
|
|
a summary of unpaid bills, specifically for authenticated users associated
|
|
with an entity. This view ensures that only authorized users, based on their
|
|
authentication state and type, can access this sensitive financial data. It
|
|
leverages the DjangoLedgerSecurityMixIn and EntityUnitMixIn for security
|
|
enhancements and entity-specific processing.
|
|
|
|
:ivar http_method_names: HTTP methods supported by this view.
|
|
:type http_method_names: list[str]
|
|
"""
|
|
http_method_names = ["get"]
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
if request.user.is_authenticated:
|
|
dealer = get_user_type(request)
|
|
bill_qs = BillModel.objects.for_entity(
|
|
entity_slug=dealer.entity.slug,
|
|
user_model=dealer.entity.admin,
|
|
).unpaid()
|
|
|
|
net_summary = accruable_net_summary(bill_qs)
|
|
net_payables = {"net_payable_data": net_summary}
|
|
|
|
return JsonResponse({"results": net_payables})
|
|
|
|
return JsonResponse({
|
|
'message': _('Unauthorized')
|
|
}, status=401)
|
|
|
|
|
|
class ReceivableNetAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View):
|
|
"""
|
|
Handles the retrieval of net receivables summary for authenticated users.
|
|
|
|
This view is part of a ledger system and is designed to provide a net summary
|
|
of unpaid invoices for an authenticated user. If the user is not authenticated,
|
|
the view will return an unauthorized response. The view supports only GET
|
|
requests.
|
|
|
|
Class Attributes:
|
|
:ivar http_method_names: Restricts the allowed HTTP methods for this view.
|
|
:type http_method_names: list[str]
|
|
"""
|
|
http_method_names = ["get"]
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
if request.user.is_authenticated:
|
|
dealer = get_user_type(request)
|
|
invoice_qs = InvoiceModel.objects.for_entity(
|
|
entity_slug=dealer.entity.slug,
|
|
user_model=dealer.entity.admin,
|
|
).unpaid()
|
|
|
|
net_summary = accruable_net_summary(invoice_qs)
|
|
|
|
net_receivable = {"net_receivable_data": net_summary}
|
|
|
|
return JsonResponse({"results": net_receivable})
|
|
|
|
return JsonResponse({
|
|
'message': _('Unauthorized')
|
|
}, status=401)
|
|
|
|
|
|
class PnLAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View):
|
|
"""
|
|
APIView for handling Profit and Loss (PnL) data retrieval.
|
|
|
|
This class provides an endpoint to handle the retrieval of Profit and Loss data
|
|
for an entity. It uses authentication to validate the user's access and retrieves
|
|
PnL data based on the user's entity and optional query parameters such as dates.
|
|
The retrieved data is organized and returned in a JSON format.
|
|
|
|
:ivar http_method_names: Restricts HTTP methods that can be used with this view.
|
|
:type http_method_names: list[str]
|
|
"""
|
|
http_method_names = ["get"]
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
if request.user.is_authenticated:
|
|
dealer = get_user_type(request)
|
|
entity = EntityModel.objects.for_user(user_model=dealer.entity.admin).get(
|
|
slug__exact=dealer.entity.slug
|
|
)
|
|
|
|
unit_slug = self.get_unit_slug()
|
|
|
|
io_digest = entity.digest(
|
|
user_model=self.request.user,
|
|
unit_slug=unit_slug,
|
|
equity_only=True,
|
|
signs=False,
|
|
by_period=True,
|
|
process_groups=True,
|
|
from_date=self.request.GET.get("fromDate"),
|
|
to_date=self.request.GET.get("toDate"),
|
|
# todo: For PnL to display proper period values must not use closing entries.
|
|
use_closing_entries=False,
|
|
)
|
|
|
|
io_data = io_digest.get_io_data()
|
|
group_balance_by_period = io_data["group_balance_by_period"]
|
|
group_balance_by_period = dict(
|
|
sorted((k, v) for k, v in group_balance_by_period.items())
|
|
)
|
|
|
|
entity_data = {
|
|
f"{month_name[k[1]]} {k[0]}": {d: float(f) for d, f in v.items()}
|
|
for k, v in group_balance_by_period.items()
|
|
}
|
|
|
|
entity_pnl = {
|
|
"entity_slug": entity.slug,
|
|
"entity_name": entity.name,
|
|
"pnl_data": entity_data,
|
|
}
|
|
|
|
return JsonResponse({"results": entity_pnl})
|
|
|
|
return JsonResponse({
|
|
'message': _('Unauthorized')
|
|
}, status=401)
|
|
|
|
|
|
class EmployeeCalendarView(LoginRequiredMixin, ListView):
|
|
"""
|
|
Provides a view for displaying the employee's calendar in a list format.
|
|
|
|
Displays a list of appointments for logged-in users. This view ensures that
|
|
only appointments relevant to the logged-in user as a dealer or staff member
|
|
are displayed. Supports search functionality to filter displayed appointments
|
|
based on provided query parameters.
|
|
|
|
:ivar template_name: Path to the HTML template used to render the view.
|
|
:type template_name: str
|
|
:ivar model: Model object to interact with appointments data.
|
|
:type model: Appointment
|
|
:ivar context_object_name: Name of the context variable that contains the
|
|
list of appointments in the template.
|
|
:type context_object_name: str
|
|
"""
|
|
template_name = "crm/employee_calendar.html"
|
|
model = Appointment
|
|
context_object_name = "appointments"
|
|
|
|
def get_queryset(self):
|
|
query = self.request.GET.get("q")
|
|
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):
|
|
"""
|
|
Apply search filters to a queryset based on a query string.
|
|
|
|
This function filters the provided queryset by applying a Q object for all
|
|
CharField, TextField, or EmailField fields in the model. It checks if the
|
|
query exists within these fields using case-insensitive containment (icontains).
|
|
If the query string is empty or None, the original queryset is returned
|
|
without any modifications.
|
|
|
|
:param queryset: The initial queryset to apply the search filters on.
|
|
:param query: The search string to match against the model fields. If None
|
|
or an empty string, no filtering is applied.
|
|
:return: A filtered queryset that satisfies the search condition.
|
|
"""
|
|
if not query:
|
|
return queryset
|
|
|
|
search_filters = Q()
|
|
model = queryset.model
|
|
|
|
for field in model._meta.get_fields():
|
|
if hasattr(field, "attname") and field.get_internal_type() in [
|
|
"CharField",
|
|
"TextField",
|
|
"EmailField",
|
|
]:
|
|
search_filters |= Q(**{f"{field.name}__icontains": query})
|
|
return queryset.filter(search_filters).distinct()
|
|
|
|
|
|
class CarListViewTable(LoginRequiredMixin, ExportMixin, SingleTableView):
|
|
"""
|
|
Displays a table view of cars for a user with proper permissions.
|
|
|
|
This class-based view leverages functionality for login-required access, data
|
|
exporting, and integration with a single table representation. It is designed
|
|
to fetch and display a list of car objects related to a specific dealer in
|
|
a tabular format. The view also renders a predefined template to display
|
|
the data appropriately.
|
|
|
|
:ivar model: Specifies the model for the table view (Car).
|
|
:type model: models.Car
|
|
:ivar table_class: Represents the table class to be used for rendering
|
|
the data in tabular format.
|
|
:type table_class: tables.CarTable
|
|
:ivar template_name: Defines the template to be used for rendering the view.
|
|
:type template_name: str
|
|
"""
|
|
model = models.Car
|
|
table_class = tables.CarTable
|
|
template_name = "inventory/car_list_table.html"
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
return models.Car.objects.select_related(
|
|
"finances", "colors__exterior", "colors__interior"
|
|
).filter(dealer=dealer)
|
|
|
|
|
|
@login_required
|
|
def DealerSettingsView(request, pk):
|
|
"""
|
|
Handles dealer settings view where dealers can update their financial and
|
|
payment account settings. This view ensures validation and reassigns form
|
|
fields dynamically based on the dealer's account roles.
|
|
|
|
:param request: The HTTP request object received from the client. This parameter
|
|
contains data including HTTP headers, session, and POST data if applicable.
|
|
:type request: HttpRequest
|
|
:param pk: Primary key representing the dealer for whom the settings are being
|
|
retrieved or modified. This identifier is used to fetch or update dealer-
|
|
specific settings from the database.
|
|
:type pk: int
|
|
:return: An HTTP response rendering the dealer settings form or redirecting
|
|
to the dealer detail view after successful form submission.
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer_setting = get_object_or_404(models.DealerSettings, dealer__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})
|
|
|
|
|
|
@login_required
|
|
def schedule_cancel(request, pk):
|
|
"""
|
|
Cancel a schedule by updating its status to "Canceled". The function is protected
|
|
by a login requirement, ensuring only authenticated users can execute it. It
|
|
retrieves the schedule object by its primary key, updates its status, saves the
|
|
changes, and returns an HTTP response with status code 200.
|
|
|
|
:param request: The HTTP request object representing the user's request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the schedule to be canceled.
|
|
:type pk: int
|
|
:return: An HTTP response object with a 200 status code upon successful execution.
|
|
:rtype: HttpResponse
|
|
"""
|
|
schedule = get_object_or_404(models.Schedule, pk=pk)
|
|
schedule.status = "Canceled"
|
|
schedule.save()
|
|
response = HttpResponse()
|
|
response.status_code = 200
|
|
return response
|
|
|
|
|
|
@login_required
|
|
def assign_car_makes(request):
|
|
"""
|
|
Assigns car makes to a dealer.
|
|
|
|
This function handles both the display and processing of a form that allows
|
|
a dealer to assign or modify their associated car makes. If the request
|
|
method is POST, it validates and saves the submitted form data. If the
|
|
method is not POST, it displays a form prefilled with the existing car makes
|
|
associated with the dealer.
|
|
|
|
:param request: The HTTP request object containing information about the
|
|
current request.
|
|
:type request: HttpRequest
|
|
:return: A rendered HTML response for GET requests, or a redirect to the
|
|
dealer detail page after successful form submission.
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer = get_user_type(request)
|
|
if request.method == "POST":
|
|
form = forms.DealersMakeForm(request.POST, dealer=dealer)
|
|
if form.is_valid():
|
|
makes = form.cleaned_data["car_makes"]
|
|
create_accounts_for_make(dealer, makes)
|
|
form.save()
|
|
return redirect("dealer_detail", pk=dealer.pk)
|
|
else:
|
|
print(form.errors)
|
|
else:
|
|
# Pre-fill the form with existing selections
|
|
existing_car_makes = models.DealersMake.objects.filter(
|
|
dealer=dealer
|
|
).values_list("car_make", flat=True)
|
|
form = forms.DealersMakeForm(
|
|
initial={"car_makes": existing_car_makes}, dealer=dealer
|
|
)
|
|
for choice in form.fields["car_makes"].choices:
|
|
print(choice[0].instance)
|
|
break
|
|
return render(request, "dealers/assign_car_makes.html", {"form": form})
|
|
|
|
|
|
class LedgerModelListView(LoginRequiredMixin, ListView, ArchiveIndexView):
|
|
"""
|
|
Provides a view for listing ledger entries in the system.
|
|
|
|
This class-based view combines the functionality of LoginRequiredMixin,
|
|
ListView, and ArchiveIndexView to display a filtered and ordered
|
|
list of LedgerModel entries based on the user's type and entity. It
|
|
also allows toggling between different subsets of data, such as
|
|
all entries, current entries, or visible entries.
|
|
|
|
:ivar model: The model that this view displays.
|
|
:type model: type[LedgerModel]
|
|
:ivar context_object_name: The name of the context variable containing the list of LedgerModel instances.
|
|
:type context_object_name: str
|
|
:ivar template_name: The template used to render this view.
|
|
:type template_name: str
|
|
:ivar date_field: The date field used for ordering and archive functionality.
|
|
:type date_field: str
|
|
:ivar ordering: Default ordering for the queryset.
|
|
:type ordering: str
|
|
:ivar show_all: Determines whether all ledger entries should be shown.
|
|
:type show_all: bool
|
|
:ivar show_current: Determines whether only current ledger entries should be shown.
|
|
:type show_current: bool
|
|
:ivar show_visible: Determines whether only visible ledger entries should be shown.
|
|
:type show_visible: bool
|
|
:ivar allow_empty: Allows rendering of the page even if the queryset is empty.
|
|
:type allow_empty: bool
|
|
"""
|
|
model = LedgerModel
|
|
context_object_name = "ledgers"
|
|
template_name = "ledger/ledger/ledger_list.html"
|
|
date_field = "created"
|
|
ordering = "-created"
|
|
show_all = False
|
|
show_current = False
|
|
show_visible = False
|
|
allow_empty = True
|
|
|
|
def get_queryset(self):
|
|
qs = super().get_queryset()
|
|
dealer = get_user_type(self.request)
|
|
qs = qs.filter(entity=dealer.entity)
|
|
qs = qs.select_related("billmodel", "invoicemodel")
|
|
qs = qs.order_by("-created")
|
|
if self.show_all:
|
|
return qs
|
|
if self.show_current:
|
|
qs = qs.current()
|
|
if self.show_visible:
|
|
qs = qs.visible()
|
|
return qs
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
dealer = get_user_type(self.request)
|
|
context["entity_slug"] = dealer.entity.slug
|
|
return context
|
|
|
|
|
|
# class LedgerModelDetailView(InvoiceModelDetailViewBase):
|
|
class LedgerModelDetailView(LoginRequiredMixin, DetailView):
|
|
"""
|
|
This class provides a detailed view of a specific ledger entry.
|
|
|
|
The LedgerModelDetailView is implemented as a Django DetailView, enabling it to
|
|
retrieve and display detailed information about a single ledger instance.
|
|
It ensures that only authenticated users have access to this view by using the
|
|
LoginRequiredMixin.
|
|
|
|
:ivar model: The model associated with this view, representing ledger entries.
|
|
:type model: type[LedgerModel]
|
|
:ivar context_object_name: The name of the context variable available in the
|
|
template for the detailed ledger.
|
|
:type context_object_name: str
|
|
:ivar template_name: The path to the template used to render the detailed ledger view.
|
|
:type template_name: str
|
|
"""
|
|
model = LedgerModel
|
|
context_object_name = "ledger"
|
|
template_name = "ledger/ledger/ledger_detail.html"
|
|
|
|
class LedgerModelCreateView(LedgerModelCreateViewBase):
|
|
"""
|
|
Handles the creation of LedgerModel entities.
|
|
|
|
This class provides the logic to manage the creation process of
|
|
LedgerModel instances. It determines the appropriate form for creating
|
|
a LedgerModel, validates the submitted form, and redirects the user
|
|
upon success. The view is specific to a particular user type (dealer).
|
|
|
|
:ivar template_name: Specifies the path to the template used for the
|
|
form rendering.
|
|
:type template_name: str
|
|
"""
|
|
template_name = "ledger/ledger/ledger_form.html"
|
|
|
|
def get_form(self, form_class=None):
|
|
dealer = get_user_type(self.request)
|
|
return LedgerModelCreateForm(
|
|
entity_slug=dealer.entity.slug,
|
|
user_model=dealer.entity.admin,
|
|
**self.get_form_kwargs()
|
|
)
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
instance = form.save(commit=False)
|
|
instance.entity = dealer.entity
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse('ledger_list')
|
|
|
|
|
|
class LedgerModelModelActionView(LedgerModelModelActionViewBase):
|
|
"""
|
|
Represents a view for handling actions related to the Ledger model.
|
|
|
|
This class extends the LedgerModelModelActionViewBase and is responsible for
|
|
providing functionality to determine the redirection URL after performing
|
|
specific actions on a Ledger model instance.
|
|
|
|
:ivar template_name: Name of the template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar model: The model associated with this view.
|
|
:type model: type[Model]
|
|
"""
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
return reverse("ledger_list")
|
|
|
|
|
|
class LedgerModelDeleteView(LedgerModelDeleteViewBase, SuccessMessageMixin):
|
|
"""
|
|
Handles the deletion of a Ledger model instance.
|
|
|
|
Provides functionality for rendering a confirmation template and deleting a
|
|
ledger instance from the system. Extends functionality for managing success
|
|
messages and redirections upon successful deletion.
|
|
|
|
:ivar template_name: Path to the template used for rendering the delete
|
|
confirmation view.
|
|
:type template_name: str
|
|
:ivar success_message: Success message displayed upon successful deletion
|
|
of the ledger instance.
|
|
:type success_message: str
|
|
"""
|
|
template_name = "ledger/ledger/ledger_delete.html"
|
|
success_message = "Ledger deleted"
|
|
|
|
def get_success_url(self):
|
|
return reverse("ledger_list")
|
|
|
|
|
|
# class LedgerModelCreateView(LoginRequiredMixin,SuccessMessageMixin, CreateView):
|
|
# model = LedgerModel
|
|
# template_name = "ledger/ledger/ledger_form.html"
|
|
# form_class = forms.LedgerModelCreateForm
|
|
# success_message = "Ledger created"
|
|
|
|
# def get_form(self, form_class=None):
|
|
# dealer = get_user_type(self.request)
|
|
# form = forms.LedgerModelCreateForm(entity_slug=dealer.entity.slug,user_model=dealer.entity.admin,**self.get_form_kwargs())
|
|
# return form
|
|
# def get_success_url(self):
|
|
# return reverse('ledger_list')
|
|
|
|
# def form_valid(self, form):
|
|
# instance = form.save(commit=False)
|
|
# dealer = get_user_type(self.request)
|
|
# instance.entity = dealer.entity
|
|
# instance.save()
|
|
# return super().form_valid(form)
|
|
|
|
|
|
class JournalEntryListView(LoginRequiredMixin, ListView):
|
|
"""
|
|
Represents a view to list all journal entries for a specific ledger.
|
|
|
|
This class inherits from Django's `ListView` and `LoginRequiredMixin` to provide
|
|
a secure and paginated list of journal entries associated with a specific ledger.
|
|
It ensures the user is authenticated before access to the journal entries is
|
|
granted. The view customizes the queryset and context data to include only
|
|
entries tied to the given ledger specified by its primary key.
|
|
|
|
:ivar model: The model associated with the view.
|
|
:type model: JournalEntryModel
|
|
:ivar context_object_name: The name of the context variable which will hold the
|
|
list of journal entries in the template.
|
|
:type context_object_name: str
|
|
:ivar template_name: The path to the HTML template that renders the list view.
|
|
:type template_name: str
|
|
"""
|
|
model = JournalEntryModel
|
|
context_object_name = "journal_entries"
|
|
template_name = "ledger/journal_entry/journal_entry_list.html"
|
|
|
|
def get_queryset(self):
|
|
qs = super().get_queryset()
|
|
ledger = LedgerModel.objects.filter(pk=self.kwargs["pk"]).first()
|
|
qs = qs.filter(ledger=ledger)
|
|
return qs
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["ledger"] = LedgerModel.objects.filter(pk=self.kwargs["pk"]).first()
|
|
return context
|
|
|
|
|
|
class JournalEntryCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
|
|
"""
|
|
View for creating a new journal entry.
|
|
|
|
This class-based view allows authenticated users to create new journal entries
|
|
associated with a specific ledger. It includes functionality for handling
|
|
context data, form initialization, and redirection upon successful submission
|
|
of the form.
|
|
|
|
:ivar model: The model used for creating the journal entry.
|
|
:type model: JournalEntryModel
|
|
:ivar template_name: The template used to render the form.
|
|
:type template_name: str
|
|
:ivar form_class: The form class used to create the journal entry.
|
|
:type form_class: forms.JournalEntryModelCreateForm
|
|
:ivar ledger_model: The ledger model associated with this journal entry.
|
|
:type ledger_model: LedgerModel or None
|
|
:ivar success_message: The message displayed upon successfully creating a journal entry.
|
|
:type success_message: str
|
|
"""
|
|
model = JournalEntryModel
|
|
template_name = "ledger/journal_entry/journal_entry_form.html"
|
|
form_class = forms.JournalEntryModelCreateForm
|
|
ledger_model = None
|
|
success_message = _("Journal Entry created")
|
|
|
|
def get_form(self, form_class=None):
|
|
dealer = get_user_type(self.request)
|
|
ledger = LedgerModel.objects.filter(pk=self.kwargs["pk"]).first()
|
|
form = forms.JournalEntryModelCreateForm(
|
|
entity_model=dealer.entity, ledger_model=ledger, **self.get_form_kwargs()
|
|
)
|
|
form.fields.pop("entity_unit")
|
|
return form
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["ledger"] = LedgerModel.objects.filter(pk=self.kwargs["pk"]).first()
|
|
return context
|
|
|
|
def get_success_url(self):
|
|
ledger = LedgerModel.objects.filter(pk=self.kwargs["pk"]).first()
|
|
return reverse("journalentry_list", kwargs={"pk": ledger.pk})
|
|
|
|
|
|
def JournalEntryDeleteView(request, pk):
|
|
"""
|
|
Handles the deletion of a specific journal entry. This view facilitates
|
|
the deletion of a journal entry identified by its primary key (pk). If the
|
|
deletion is successful, the user is redirected to the list of journal entries
|
|
for the associated ledger. If the deletion cannot proceed, an error message
|
|
is displayed, and the user is redirected back to the journal entry list.
|
|
|
|
:param request: The HTTP request object.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key (pk) of the journal entry to be deleted.
|
|
:type pk: int
|
|
:return: A rendered HTML response for GET requests or a redirect upon
|
|
successful/failed deletion.
|
|
:rtype: HttpResponse
|
|
"""
|
|
journal_entry = get_object_or_404(JournalEntryModel, pk=pk)
|
|
if request.method == "POST":
|
|
ledger = journal_entry.ledger
|
|
if not journal_entry.can_delete():
|
|
messages.error(request, _("Journal Entry cannot be deleted"))
|
|
return redirect("journalentry_list", pk=ledger.pk)
|
|
journal_entry.delete()
|
|
messages.success(request, "Journal Entry deleted")
|
|
return redirect("journalentry_list", pk=ledger.pk)
|
|
return render(
|
|
request,
|
|
"ledger/journal_entry/journal_entry_delete.html",
|
|
{"journal_entry": journal_entry},
|
|
)
|
|
|
|
|
|
def JournalEntryTransactionsView(request, pk):
|
|
"""
|
|
Handles the retrieval and display of journal entry transactions for a specific journal
|
|
entry instance. It retrieves the journal entry and its associated transactions, ordering
|
|
the transactions by account code. The data is then rendered using the specified template.
|
|
|
|
:param request: The HTTP request object.
|
|
:type request: django.http.HttpRequest
|
|
:param pk: The primary key of the journal entry to be retrieved.
|
|
:type pk: int
|
|
:return: An HTTP response with the rendered template, including the journal entry and
|
|
its transactions.
|
|
:rtype: django.http.HttpResponse
|
|
"""
|
|
journal = JournalEntryModel.objects.filter(pk=pk).first()
|
|
transactions = (
|
|
TransactionModel.objects.filter(journal_entry=journal)
|
|
.order_by("account__code")
|
|
.all()
|
|
)
|
|
return render(
|
|
request,
|
|
"ledger/journal_entry/journal_entry_transactions.html",
|
|
{"journal_entry": journal, "transactions": transactions},
|
|
)
|
|
|
|
|
|
class JournalEntryModelTXSDetailView(JournalEntryModelTXSDetailViewBase):
|
|
"""
|
|
Represents a detailed view of journal entry transactions in the ledger.
|
|
|
|
This class is used to render the transactions associated with a specific
|
|
journal entry. It extends the base view functionality and is tailored for a
|
|
transaction details page.
|
|
|
|
:ivar template_name: The path to the template used to render the journal
|
|
entry transactions view.
|
|
:type template_name: str
|
|
"""
|
|
template_name = "ledger/journal_entry/journal_entry_txs.html"
|
|
|
|
|
|
def ledger_lock_all_journals(request, entity_slug, pk):
|
|
"""
|
|
Locks all journals associated with a specific ledger. If the ledger is already locked,
|
|
it will notify the user through an error message. Otherwise, it initiates the locking of
|
|
related journal entries, locks the ledger itself, and saves the changes to the database.
|
|
After the operation, it redirects the user to the journal entry list associated with the
|
|
ledger.
|
|
|
|
:param request: HttpRequest object representing the current request.
|
|
:type request: HttpRequest
|
|
:param entity_slug: The slug identifier of the entity.
|
|
:type entity_slug: str
|
|
:param pk: The primary key of the ledger to be locked.
|
|
:type pk: int
|
|
:return: HttpResponse redirecting to the journal entry list page of the locked ledger.
|
|
:rtype: HttpResponse
|
|
"""
|
|
ledger = LedgerModel.objects.filter(pk=pk).first()
|
|
if ledger.is_locked():
|
|
messages.error(request, _("Ledger is already locked"))
|
|
return redirect("journalentry_list", pk=ledger.pk)
|
|
ledger.lock_journal_entries()
|
|
ledger.lock()
|
|
ledger.save()
|
|
return redirect("journalentry_list", pk=ledger.pk)
|
|
|
|
|
|
def ledger_unlock_all_journals(request, entity_slug, pk):
|
|
"""
|
|
Unlocks all journal entries associated with a specific ledger. This function first checks if the
|
|
ledger is locked. If it is already unlocked, it shows an error message and redirects the user
|
|
to the journal entry list page. If the ledger is locked, it unlocks the ledger, saves changes,
|
|
and iterates through all the locked journal entries within the ledger to unlock them as well.
|
|
Afterward, it redirects the user to the journal entry list page.
|
|
|
|
:param request: The HTTP request object containing user session and request metadata.
|
|
:type request: HttpRequest
|
|
:param entity_slug: A unique string slug representing the specific entity or organization context.
|
|
:type entity_slug: str
|
|
:param pk: The primary key of the ledger record to be unlocked.
|
|
:type pk: int
|
|
:return: A redirection to the journal entry list page for the specified ledger.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
ledger = LedgerModel.objects.filter(pk=pk).first()
|
|
if not ledger.is_locked():
|
|
messages.error(request, _("Ledger is already Unlocked"))
|
|
return redirect("journalentry_list", pk=ledger.pk)
|
|
|
|
ledger.unlock()
|
|
ledger.save()
|
|
qs = ledger.journal_entries.locked()
|
|
for je in qs:
|
|
je.unlock()
|
|
je.save()
|
|
return redirect("journalentry_list", pk=ledger.pk)
|
|
|
|
|
|
def ledger_post_all_journals(request, entity_slug, pk):
|
|
"""
|
|
Posts all journal entries associated with a ledger. This function updates the ledger's
|
|
state to reflect that its journal entries have been posted. If the ledger is already
|
|
posted, an error message is displayed and the user is redirected.
|
|
|
|
:param request: The HTTP request object used for processing the action.
|
|
:type request: HttpRequest
|
|
:param entity_slug: A string representing the specific entity slug for the ledger context.
|
|
:type entity_slug: str
|
|
:param pk: The primary key of the ledger to be posted.
|
|
:type pk: int
|
|
:return: A redirect to the journal entry list view for the specified ledger.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
ledger = LedgerModel.objects.filter(pk=pk).first()
|
|
if ledger.is_posted():
|
|
messages.error(request, _("Ledger is already posted"))
|
|
return redirect("journalentry_list", pk=ledger.pk)
|
|
ledger.post_journal_entries()
|
|
ledger.post()
|
|
ledger.save()
|
|
return redirect("journalentry_list", pk=ledger.pk)
|
|
|
|
|
|
def ledger_unpost_all_journals(request, entity_slug, pk):
|
|
"""
|
|
Unposts all journal entries for a specified ledger and marks the ledger as unposted.
|
|
|
|
This function identifies a ledger by its primary key (pk) and checks if it is
|
|
already posted. If the ledger is not posted, an error message is displayed.
|
|
If it is posted, the function iterates through its posted journal entries,
|
|
marks them as unposted, and saves the changes. Finally, it marks the ledger itself
|
|
as unposted and saves it.
|
|
|
|
:param request: The HTTP request object from the client.
|
|
:type request: HttpRequest
|
|
:param entity_slug: A slug identifying the entity associated with the ledger.
|
|
:type entity_slug: str
|
|
:param pk: The primary key of the ledger whose journal entries are being unposted.
|
|
:type pk: int
|
|
:return: An HTTP redirect response object directing the client to the journal entry list
|
|
page for the specified ledger.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
ledger = LedgerModel.objects.filter(pk=pk).first()
|
|
if not ledger.is_posted():
|
|
messages.error(request, _("Ledger is already Unposted"))
|
|
return redirect("journalentry_list", pk=ledger.pk)
|
|
qs = ledger.journal_entries.posted()
|
|
for je in qs:
|
|
je.mark_as_unposted()
|
|
je.save()
|
|
|
|
ledger.unpost()
|
|
ledger.save()
|
|
return redirect("journalentry_list", pk=ledger.pk)
|
|
|
|
|
|
def pricing_page(request):
|
|
plan_list = PlanPricing.objects.all()
|
|
form = forms.PaymentPlanForm()
|
|
return render(request, "pricing_page.html", {"plan_list": plan_list, "form":form})
|
|
|
|
# @require_POST
|
|
def submit_plan(request):
|
|
dealer = get_user_type(request)
|
|
selected_plan_id = request.POST.get("selected_plan")
|
|
pp = PlanPricing.objects.get(pk=selected_plan_id)
|
|
|
|
order = Order.objects.create(
|
|
user=dealer.user,
|
|
plan=pp.plan,
|
|
pricing=pp.pricing,
|
|
amount=pp.price,
|
|
currency=settings.DEFAULT_CURRENCY,
|
|
tax=15,
|
|
status=AbstractOrder.STATUS.NEW
|
|
)
|
|
transaction_url = handle_payment(request,order)
|
|
return redirect(transaction_url)
|
|
|
|
def payment_callback(request):
|
|
dealer = get_user_type(request)
|
|
payment_id = request.GET.get("id")
|
|
history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first()
|
|
payment_status = request.GET.get("status")
|
|
order = Order.objects.filter(user=dealer.user,status=AbstractOrder.STATUS.NEW).first()
|
|
|
|
if payment_status == "paid":
|
|
billing_info,created = BillingInfo.objects.get_or_create(
|
|
user=dealer.user,
|
|
tax_number=dealer.vrn,
|
|
name=dealer.arabic_name,
|
|
street=dealer.address,
|
|
zipcode=dealer.entity.zip_code if dealer.entity.zip_code else " ",
|
|
city=dealer.entity.city if dealer.entity.city else " ",
|
|
country=dealer.entity.country if dealer.entity.country else " ",
|
|
)
|
|
if created:
|
|
userplan =UserPlan.objects.create(
|
|
user=request.user,
|
|
plan=order.plan,
|
|
active=True,
|
|
)
|
|
userplan.initialize()
|
|
|
|
order.complete_order()
|
|
history.status = "paid"
|
|
history.save()
|
|
invoice = order.get_invoices().first()
|
|
return render(request, "payment_success.html",{"order":order,"invoice":invoice})
|
|
|
|
elif payment_status == "failed":
|
|
history.status = "failed"
|
|
history.save()
|
|
message = request.GET.get('message')
|
|
return render(request, "payment_failed.html", {"message": message})
|
|
|
|
|
|
# Background Tasks
|
|
def task_list(request):
|
|
# Get all tasks ordered by creation time
|
|
tasks = Task.objects.all()
|
|
|
|
# Add pagination
|
|
paginator = Paginator(tasks, 10) # Show 10 tasks per page
|
|
page_number = request.GET.get('page')
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
return render(request, 'tasks/task_list.html', {'page_obj': page_obj})
|
|
|
|
def sse_stream(request):
|
|
def event_stream():
|
|
last_id = request.GET.get('last_id', 0)
|
|
while True:
|
|
# Check for new notifications
|
|
notifications = models.Notification.objects.filter(
|
|
user=request.user,
|
|
id__gt=last_id,
|
|
is_read=False
|
|
).order_by('created')
|
|
for notification in notifications:
|
|
notification_data = {
|
|
'id': notification.id,
|
|
'message': notification.message,
|
|
'created': notification.created.isoformat(),
|
|
}
|
|
|
|
yield (
|
|
f"id: {notification.id}\n"
|
|
f"event: notification\n"
|
|
f"data: {json.dumps(notification_data)}\n\n"
|
|
)
|
|
last_id = notification.id
|
|
|
|
sleep(2)
|
|
response = StreamingHttpResponse(
|
|
event_stream(),
|
|
content_type='text/event-stream'
|
|
)
|
|
response['Cache-Control'] = 'no-cache'
|
|
return response
|
|
|
|
|
|
@login_required
|
|
def fetch_notifications(request):
|
|
notifications = models.Notification.objects.filter(
|
|
user=request.user,
|
|
is_read=False
|
|
).order_by('-created')[:10] # Get 10 most recent
|
|
|
|
return JsonResponse({'notifications': list(notifications.values())})
|
|
|
|
@login_required
|
|
def mark_notification_as_read(request, notification_id):
|
|
notification = get_object_or_404(models.Notification, id=notification_id, user=request.user)
|
|
notification.read = True
|
|
notification.save()
|
|
return JsonResponse({'status': 'success'})
|
|
|
|
@login_required
|
|
def mark_all_notifications_as_read(request):
|
|
models.Notification.objects.filter(user=request.user, is_read=False).update(read=True)
|
|
return JsonResponse({'status': 'success'})
|
|
|
|
@login_required
|
|
def notifications_history(request):
|
|
models.Notification.objects.filter(user=request.user, is_read=False).update(read=True)
|
|
return JsonResponse({'status': 'success'})
|