9847 lines
388 KiB
Python
9847 lines
388 KiB
Python
# Standard
|
|
import os
|
|
import io
|
|
import csv
|
|
import cv2
|
|
import json
|
|
import logging
|
|
import tempfile
|
|
import numpy as np
|
|
from time import sleep
|
|
# from rich import print
|
|
from random import randint
|
|
from decimal import Decimal
|
|
from io import TextIOWrapper
|
|
from django.apps import apps
|
|
from datetime import datetime
|
|
from calendar import month_name
|
|
from pyzbar.pyzbar import decode
|
|
from urllib.parse import urlparse, urlunparse
|
|
|
|
#####################################################################
|
|
from inventory.mixins import DealerSlugMixin
|
|
from inventory.models import Status as LeadStatus
|
|
from django.db import IntegrityError
|
|
from django.views.generic import FormView
|
|
from django.views.decorators.http import require_http_methods
|
|
from django.db.models.deletion import RestrictedError
|
|
from django.http.response import StreamingHttpResponse
|
|
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
|
|
|
# Django
|
|
from django.db.models import Q
|
|
from django.conf import settings
|
|
from django.db.models import Func
|
|
from django.contrib import messages
|
|
from django.http import (
|
|
Http404,
|
|
HttpResponseBadRequest,
|
|
HttpResponseNotFound,
|
|
HttpResponseRedirect,
|
|
JsonResponse,
|
|
HttpResponseForbidden,
|
|
)
|
|
from django.forms import HiddenInput, ValidationError
|
|
from django.shortcuts import HttpResponse
|
|
|
|
from django.db.models import Sum, F, Count
|
|
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
|
from django.contrib.auth.models import User
|
|
from django.contrib.auth.models import Group
|
|
from django.db.models import Value
|
|
from django.urls import reverse, reverse_lazy
|
|
from django.utils import timezone, translation
|
|
from django.db.models.functions import Coalesce
|
|
from django.contrib.auth.models import Permission
|
|
from django.utils.decorators import method_decorator
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
from django.views.generic import DetailView, RedirectView
|
|
from django.contrib.messages.views import SuccessMessageMixin
|
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
|
from django.contrib.auth.decorators import permission_required
|
|
from django.shortcuts import render, get_object_or_404, redirect
|
|
from plans.models import Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo
|
|
from django.views.generic import (
|
|
View,
|
|
ListView,
|
|
CreateView,
|
|
UpdateView,
|
|
DeleteView,
|
|
TemplateView,
|
|
ArchiveIndexView,
|
|
)
|
|
|
|
from django.db.models import Case, Value, IntegerField, When
|
|
|
|
# Django Ledger
|
|
from django_ledger.io import roles
|
|
from django_ledger.utils import accruable_net_summary
|
|
from django_ledger.views import (
|
|
JournalEntryModelTXSDetailView as JournalEntryModelTXSDetailViewBase,
|
|
LedgerModelModelActionView as LedgerModelModelActionViewBase,
|
|
LedgerModelDeleteView as LedgerModelDeleteViewBase,
|
|
LedgerModelCreateView as LedgerModelCreateViewBase,
|
|
)
|
|
from django_ledger.forms.account import AccountModelCreateForm, AccountModelUpdateForm
|
|
from django_ledger.views.inventory import InventoryListView as InventoryListViewBase
|
|
from django_ledger.views.entity import (
|
|
EntityModelDetailBaseView,
|
|
EntityModelDetailHandlerView,
|
|
)
|
|
from django_ledger.forms.ledger import LedgerModelCreateForm
|
|
from django_ledger.forms.item import (
|
|
ExpenseItemCreateForm,
|
|
ExpenseItemUpdateForm,
|
|
)
|
|
from django_ledger.forms.bank_account import (
|
|
BankAccountCreateForm,
|
|
BankAccountUpdateForm,
|
|
)
|
|
from django_ledger.views.bill import (
|
|
# BillModelCreateView,
|
|
BillModelDetailView,
|
|
BillModelUpdateView,
|
|
BaseBillActionView as BaseBillActionViewBase,
|
|
BillModelModelBaseView,
|
|
)
|
|
from django_ledger.forms.bill import (
|
|
ApprovedBillModelUpdateForm,
|
|
InReviewBillModelUpdateForm,
|
|
get_bill_itemtxs_formset_class,
|
|
BillModelCreateForm,
|
|
)
|
|
from django_ledger.forms.invoice import (
|
|
DraftInvoiceModelUpdateForm,
|
|
ApprovedInvoiceModelUpdateForm,
|
|
PaidInvoiceModelUpdateForm,
|
|
)
|
|
from django_ledger.forms.item import (
|
|
InventoryItemCreateForm,
|
|
)
|
|
from django_ledger.forms.purchase_order import (
|
|
PurchaseOrderModelCreateForm,
|
|
BasePurchaseOrderModelUpdateForm,
|
|
DraftPurchaseOrderModelUpdateForm,
|
|
ReviewPurchaseOrderModelUpdateForm,
|
|
ApprovedPurchaseOrderModelUpdateForm,
|
|
get_po_itemtxs_formset_class,
|
|
)
|
|
from django_ledger.views.purchase_order import (
|
|
PurchaseOrderModelDetailView as PurchaseOrderModelDetailViewBase,
|
|
PurchaseOrderModelUpdateView as PurchaseOrderModelUpdateViewBase,
|
|
BasePurchaseOrderActionActionView as BasePurchaseOrderActionActionViewBase,
|
|
PurchaseOrderModelDeleteView as PurchaseOrderModelDeleteViewBase,
|
|
)
|
|
from django_ledger.models import (
|
|
ItemTransactionModel,
|
|
EntityModel,
|
|
InvoiceModel,
|
|
BankAccountModel,
|
|
AccountModel,
|
|
JournalEntryModel,
|
|
TransactionModel,
|
|
EstimateModel,
|
|
CustomerModel,
|
|
ItemModel,
|
|
BillModel,
|
|
LedgerModel,
|
|
PurchaseOrderModel,
|
|
)
|
|
from django_ledger.views.financial_statement import (
|
|
FiscalYearBalanceSheetView,
|
|
BaseIncomeStatementRedirectView,
|
|
FiscalYearIncomeStatementView,
|
|
BaseCashFlowStatementRedirectView,
|
|
FiscalYearCashFlowStatementView,
|
|
)
|
|
|
|
from django_ledger.io.io_core import get_localdate
|
|
from django_ledger.views.mixins import (
|
|
QuarterlyReportMixIn,
|
|
MonthlyReportMixIn,
|
|
DateReportMixIn,
|
|
DjangoLedgerSecurityMixIn,
|
|
EntityUnitMixIn,
|
|
)
|
|
|
|
# Other
|
|
from plans.models import Plan
|
|
|
|
from . import models, forms, tables
|
|
from django_tables2 import SingleTableView
|
|
from django_tables2.export.views import ExportMixin
|
|
from appointment.models import Appointment, AppointmentRequest, Service, StaffMember
|
|
from .services import (
|
|
decodevin,
|
|
get_make,
|
|
get_model,
|
|
)
|
|
from .utils import (
|
|
CarFinanceCalculator,
|
|
create_user_dealer,
|
|
get_car_finance_data,
|
|
get_item_transactions,
|
|
handle_payment,
|
|
reserve_car,
|
|
# send_email,
|
|
get_user_type,
|
|
set_bill_payment,
|
|
set_invoice_payment,
|
|
CarTransfer,
|
|
)
|
|
from .tasks import create_accounts_for_make, send_email
|
|
|
|
# djago easy audit log
|
|
from easyaudit.models import RequestEvent, CRUDEvent, LoginEvent
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
|
|
class Hash(Func):
|
|
"""
|
|
Represents a function used to compute a hash value.
|
|
|
|
This class serves as a placeholder to specify a particular hash
|
|
computation function. It extends the `Func` base class and is used
|
|
primarily to work with hash-related operations within the underlying
|
|
implementation. The specific hash algorithm can be modified or accessed
|
|
through the `function` attribute.
|
|
|
|
:ivar function: Specifies the hash computation function.
|
|
:type function: str
|
|
"""
|
|
|
|
function = "get_hash"
|
|
|
|
|
|
def switch_language(request):
|
|
"""
|
|
Switches the current language context for the user based on a request parameter, modifies the URL path
|
|
accordingly, and updates session and cookies with the new language preference.
|
|
|
|
:param request: The HTTP request object containing information about the user request, including
|
|
the desired language to switch to and the referring URL.
|
|
- "GET" dictionary is accessed to retrieve the desired language parameter.
|
|
- "META" dictionary is used to extract the referring URL via "HTTP_REFERER".
|
|
|
|
:return: A redirect response object pointing to the modified URL with the updated language
|
|
preference, if the requested language is valid. Otherwise, redirects to the default URL.
|
|
"""
|
|
language = request.GET.get("language", "en")
|
|
referer = request.META.get("HTTP_REFERER", "/")
|
|
parsed_url = urlparse(referer)
|
|
path_parts = parsed_url.path.split("/")
|
|
|
|
if path_parts[1] in dict(settings.LANGUAGES):
|
|
path_parts.pop(1)
|
|
|
|
new_path = "/".join(path_parts)
|
|
new_url = urlunparse(
|
|
(
|
|
parsed_url.scheme,
|
|
parsed_url.netloc,
|
|
new_path,
|
|
parsed_url.params,
|
|
parsed_url.query,
|
|
parsed_url.fragment,
|
|
)
|
|
)
|
|
|
|
if language in dict(settings.LANGUAGES):
|
|
logger.debug(f"Switching language to: {language}")
|
|
response = redirect(new_url)
|
|
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
|
|
translation.activate(language)
|
|
request.session[settings.LANGUAGE_COOKIE_NAME] = language
|
|
logger.debug(
|
|
f"Language switched to: {language}, Session: {request.session[settings.LANGUAGE_COOKIE_NAME]}"
|
|
)
|
|
return response
|
|
else:
|
|
logger.warning(f"Invalid language code: {language}")
|
|
return redirect("/")
|
|
|
|
def testview(request):
|
|
return HttpResponse("test")
|
|
|
|
def dealer_signup(request):
|
|
"""
|
|
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"
|
|
|
|
|
|
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.CONTACTED
|
|
).count()
|
|
canceled_leads = models.Lead.objects.filter(
|
|
dealer=dealer, status=models.Status.UNQUALIFIED
|
|
).count()
|
|
available_cars = models.Car.objects.filter(
|
|
dealer=dealer, status=models.CarStatusChoices.AVAILABLE
|
|
).count()
|
|
sold_cars = models.Car.objects.filter(
|
|
dealer=dealer, status=models.CarStatusChoices.SOLD
|
|
).count()
|
|
reserved_cars = models.Car.objects.filter(
|
|
dealer=dealer, status=models.CarStatusChoices.RESERVED
|
|
).count()
|
|
hold_cars = models.Car.objects.filter(
|
|
dealer=dealer, status=models.CarStatusChoices.HOLD
|
|
).count()
|
|
damaged_cars = models.Car.objects.filter(
|
|
dealer=dealer, status=models.CarStatusChoices.DAMAGED
|
|
).count()
|
|
transfer_cars = models.Car.objects.filter(
|
|
dealer=dealer, status=models.CarStatusChoices.TRANSFER
|
|
).count()
|
|
reserved_percentage = reserved_cars / total_cars * 100
|
|
sold_percentage = sold_cars / total_cars * 100
|
|
qs = (
|
|
models.Car.objects.values("id_car_make__name")
|
|
.annotate(count=Count("id"))
|
|
.order_by("id_car_make__name")
|
|
)
|
|
car_by_make = list(qs)
|
|
total_activity = models.UserActivityLog.objects.filter(user=dealer.user).count()
|
|
staff = models.Staff.objects.filter(dealer=dealer).count()
|
|
total_leads = models.Lead.objects.filter(dealer=dealer).count()
|
|
invoices = entity.get_invoices().count()
|
|
customers = entity.get_customers().count()
|
|
purchase_orders = entity.get_purchase_orders().count()
|
|
estimates = entity.get_estimates().count()
|
|
|
|
context["dealer"] = dealer
|
|
context["total_activity"] = total_activity
|
|
context["total_cars"] = total_cars
|
|
context["total_reservations"] = total_reservations
|
|
context["total_cost_price"] = total_cost_price
|
|
context["total_selling_price"] = total_selling_price
|
|
context["total_profit"] = total_profit
|
|
context["new_leads"] = new_leads
|
|
context["pending_leads"] = pending_leads
|
|
context["canceled_leads"] = canceled_leads
|
|
context["reserved_percentage"] = reserved_percentage
|
|
context["sold_percentage"] = sold_percentage
|
|
context["available_cars"] = available_cars
|
|
context["sold_cars"] = sold_cars
|
|
context["reserved_cars"] = reserved_cars
|
|
context["hold_cars"] = hold_cars
|
|
context["damaged_cars"] = damaged_cars
|
|
context["transfer_cars"] = transfer_cars
|
|
context["car"] = json.dumps(car_by_make)
|
|
context["customers"] = customers
|
|
context["staff"] = staff
|
|
context["total_leads"] = total_leads
|
|
context["invoices"] = invoices
|
|
context["estimates"] = estimates
|
|
context["purchase_orders"] = purchase_orders
|
|
|
|
return context
|
|
|
|
|
|
class SalesDashboard(LoginRequiredMixin, TemplateView):
|
|
"""
|
|
SalesDashboard class provides a view for the sales dashboard.
|
|
|
|
This class is responsible for generating the context data required to
|
|
display various statistics and information on the sales dashboard. It
|
|
inherits from `LoginRequiredMixin` and `TemplateView`, ensuring only
|
|
authenticated users can access the view. The dashboard includes data
|
|
such as the total number of cars, reservations, sold percentages,
|
|
reserved percentages, and cars categorized by various statuses. The
|
|
purpose of this dashboard is to provide dealership staff an overview
|
|
of their inventory and related sales metrics.
|
|
|
|
:ivar template_name: The name of the HTML template used for rendering
|
|
the sales dashboard.
|
|
:type template_name: str
|
|
"""
|
|
|
|
template_name = "dashboards/sales.html"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
dealer = get_user_type(self.request)
|
|
staff = getattr(self.request.user, "staff", None)
|
|
total_cars = models.Car.objects.filter(dealer=dealer).count()
|
|
total_reservations = models.CarReservation.objects.filter(
|
|
reserved_by=self.request.user, reserved_until__gte=timezone.now()
|
|
).count()
|
|
|
|
available_cars = models.Car.objects.filter(
|
|
dealer=dealer, status=models.CarStatusChoices.AVAILABLE
|
|
).count()
|
|
sold_cars = models.Car.objects.filter(
|
|
dealer=dealer, status=models.CarStatusChoices.SOLD
|
|
).count()
|
|
reserved_cars = models.Car.objects.filter(
|
|
dealer=dealer, status=models.CarStatusChoices.RESERVED
|
|
).count()
|
|
hold_cars = models.Car.objects.filter(
|
|
dealer=dealer, status=models.CarStatusChoices.HOLD
|
|
).count()
|
|
damaged_cars = models.Car.objects.filter(
|
|
dealer=dealer, status=models.CarStatusChoices.DAMAGED
|
|
).count()
|
|
transfer_cars = models.Car.objects.filter(
|
|
dealer=dealer, status=models.CarStatusChoices.TRANSFER
|
|
).count()
|
|
reserved_percentage = reserved_cars / total_cars * 100
|
|
sold_percentage = sold_cars / total_cars * 100
|
|
qs = (
|
|
models.Car.objects.values("id_car_make__name")
|
|
.annotate(count=Count("id"))
|
|
.order_by("id_car_make__name")
|
|
)
|
|
car_by_make = list(qs)
|
|
|
|
context["dealer"] = dealer
|
|
context["staff"] = staff
|
|
context["total_cars"] = total_cars
|
|
context["total_reservations"] = total_reservations
|
|
context["reserved_percentage"] = reserved_percentage
|
|
context["sold_percentage"] = sold_percentage
|
|
context["available_cars"] = available_cars
|
|
context["sold_cars"] = sold_cars
|
|
context["reserved_cars"] = reserved_cars
|
|
context["hold_cars"] = hold_cars
|
|
context["damaged_cars"] = damaged_cars
|
|
context["transfer_cars"] = transfer_cars
|
|
context["car"] = json.dumps(car_by_make)
|
|
|
|
return context
|
|
|
|
|
|
def terms_and_privacy(request):
|
|
return render(request, "terms_and_privacy.html")
|
|
|
|
|
|
def WelcomeView(request):
|
|
"""
|
|
Handles the rendering and context data for the Welcome view.
|
|
|
|
This class serves as a Django TemplateView for the "welcome.html" template. It
|
|
is primarily responsible for providing the necessary context data, including
|
|
user-specific information and the list of plans, to the template for rendering
|
|
the welcome page.
|
|
|
|
:ivar template_name: Path to the template used by the view.
|
|
:type template_name: str
|
|
"""
|
|
if request.user.is_authenticated:
|
|
return redirect("home", dealer_slug=request.dealer.slug)
|
|
plan_list = Plan.objects.all()
|
|
context = {"plan_list": plan_list}
|
|
return render(request, "welcome.html", context)
|
|
|
|
|
|
class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
|
"""
|
|
Manages the creation of a new car entry in the inventory system.
|
|
|
|
This class is responsible for handling the creation of a new car, including form
|
|
display and data submission. It ensures the user has appropriate permissions and
|
|
customizes the available vendors for the user depending on their dealer entity.
|
|
|
|
:ivar model: Specifies the Car model that this view interacts with.
|
|
:type model: models.Car
|
|
:ivar form_class: Defines the form class to be used for creating or editing `Car` instances.
|
|
:type form_class: forms.CarForm
|
|
:ivar template_name: Name of the template to render the car creation form.
|
|
:type template_name: str
|
|
:ivar permission_required: Permissions required to add a car.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = models.Car
|
|
form_class = forms.CarForm
|
|
template_name = "inventory/car_form.html"
|
|
permission_required = ["inventory.add_car"]
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
dealer = get_user_type(self.request)
|
|
form.fields["vendor"].queryset = dealer.vendors.filter(active=True)
|
|
return form
|
|
|
|
def get_success_url(self):
|
|
"""Determine the redirect URL based on user choice."""
|
|
if self.request.POST.get("add_another"):
|
|
return reverse(
|
|
"car_add", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
|
|
)
|
|
return reverse(
|
|
"inventory_stats", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
|
|
)
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
form.instance.dealer = dealer
|
|
form.save()
|
|
messages.success(self.request, _("Car saved successfully"))
|
|
return super().form_valid(form)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["vendor_exists"] = self.request.dealer.vendors.exists()
|
|
return context
|
|
|
|
|
|
def car_history(request, slug):
|
|
"""
|
|
Fetch and display the history of activities related to a specific car.
|
|
|
|
This view retrieves a car object based on its primary key (pk) and gathers
|
|
all related activity records where the content type corresponds to the car
|
|
model. The retrieved data is then rendered into a specified HTML template
|
|
for presentation.
|
|
|
|
:param request: The HTTP request object that contains metadata about the
|
|
request made by the client.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the car object to retrieve.
|
|
:type pk: int
|
|
:return: An HTTP response with the rendered car history HTML template,
|
|
including the car and its associated activities as context data.
|
|
:rtype: HttpResponse
|
|
"""
|
|
car = get_object_or_404(models.Car, slug=slug)
|
|
activities = models.Activity.objects.filter(
|
|
content_type__model="car", object_id=car.id
|
|
)
|
|
return render(
|
|
request, "inventory/car_history.html", {"car": car, "activities": activities}
|
|
)
|
|
|
|
|
|
class AjaxHandlerView(LoginRequiredMixin, View):
|
|
"""
|
|
Handles AJAX requests for various car-related operations.
|
|
|
|
This class enables dynamic handling of AJAX requests related to vehicle
|
|
data, such as retrieving models, series, trims, specifications, and other
|
|
related details. It checks the requested action, delegates it to the
|
|
corresponding handler method, and returns an appropriate response.
|
|
|
|
:ivar request: Django request object containing HTTP request details.
|
|
"""
|
|
|
|
def get(self, request, dealer_slug, *args, **kwargs):
|
|
action = request.GET.get("action")
|
|
handlers = {
|
|
"decode_vin": self.decode_vin,
|
|
"get_models": self.get_models,
|
|
"get_series": self.get_series,
|
|
"get_trims": self.get_trims,
|
|
"get_specifications": self.get_specifications,
|
|
"get_equipments": self.get_equipments,
|
|
"get_options": self.get_options,
|
|
}
|
|
handler = handlers.get(action)
|
|
if handler:
|
|
return handler(request)
|
|
else:
|
|
return JsonResponse({"error": "Invalid action"}, status=400)
|
|
|
|
def decode_vin(self, request):
|
|
vin_no = request.GET.get("vin_no")
|
|
car_existed = models.Car.objects.filter(vin=vin_no).exists()
|
|
|
|
if car_existed:
|
|
return JsonResponse({"error": _("VIN number exists")}, status=400)
|
|
|
|
if not vin_no or len(vin_no.strip()) != 17:
|
|
return JsonResponse(
|
|
{"success": False, "error": _("Invalid VIN number provided")},
|
|
status=400,
|
|
)
|
|
|
|
vin_no = vin_no.strip()
|
|
vin_data = {}
|
|
decoding_method = ""
|
|
if result := decodevin(vin_no):
|
|
manufacturer_name, model_name, year_model = result.values()
|
|
car_make = get_make(manufacturer_name)
|
|
car_model = get_model(model_name, car_make)
|
|
|
|
logger.info(
|
|
f"VIN decoded using {decoding_method}: Make={manufacturer_name}, Model={model_name}, Year={year_model}"
|
|
)
|
|
|
|
if not car_make:
|
|
return JsonResponse(
|
|
{
|
|
"success": False,
|
|
"error": _("Manufacturer not found in the database"),
|
|
},
|
|
status=404,
|
|
)
|
|
vin_data["make_id"] = car_make.id_car_make
|
|
vin_data["name"] = car_make.name
|
|
vin_data["arabic_name"] = car_make.arabic_name
|
|
|
|
if not car_model:
|
|
vin_data["model_id"] = ""
|
|
else:
|
|
vin_data["model_id"] = car_model.id_car_model
|
|
vin_data["year"] = year_model
|
|
return JsonResponse({"success": True, "data": vin_data})
|
|
|
|
# manufacturer_name = model_name = year_model = None
|
|
return JsonResponse(
|
|
{"success": False, "error": _("VIN not found in all sources")},
|
|
status=404,
|
|
)
|
|
|
|
def get_models(self, request):
|
|
make_id = request.GET.get("make_id")
|
|
car_models = (
|
|
models.CarModel.objects.filter(id_car_make=make_id)
|
|
.values("id_car_model", "name", "arabic_name")
|
|
.order_by("name")
|
|
)
|
|
return JsonResponse(list(car_models), safe=False)
|
|
|
|
def get_series(self, request):
|
|
model_id = request.GET.get("model_id")
|
|
year = request.GET.get("year")
|
|
|
|
model_id = int(model_id)
|
|
year = int(year)
|
|
|
|
query = Q(id_car_model=model_id) & (
|
|
Q(year_begin__lte=year, year_end__gte=year)
|
|
| Q(year_end__isnull=True)
|
|
| Q(year_begin__isnull=True)
|
|
)
|
|
try:
|
|
series = models.CarSerie.objects.filter(query).values(
|
|
"id_car_serie", "name", "arabic_name", "generation_name"
|
|
)
|
|
except Exception:
|
|
return JsonResponse({"error": _("Server error occurred")}, status=500)
|
|
return JsonResponse(list(series), safe=False)
|
|
|
|
def get_trims(self, request):
|
|
serie_id = request.GET.get("serie_id")
|
|
# model_id = request.GET.get('model_id')
|
|
trims = models.CarTrim.objects.filter(id_car_serie=serie_id).values(
|
|
"id_car_trim", "name", "arabic_name"
|
|
)
|
|
return JsonResponse(list(trims), safe=False)
|
|
|
|
def get_specifications(self, request):
|
|
trim_id = request.GET.get("trim_id")
|
|
car_spec_values = models.CarSpecificationValue.objects.filter(
|
|
id_car_trim=trim_id
|
|
)
|
|
lang = translation.get_language()
|
|
specs_by_parent = {}
|
|
for value in car_spec_values:
|
|
specification = value.id_car_specification
|
|
parent = specification.id_parent
|
|
parent_id = parent.id_car_specification if parent else 0
|
|
if lang == "ar":
|
|
parent_name = parent.arabic_name if parent else "Root"
|
|
else:
|
|
parent_name = parent.name if parent else "Root"
|
|
if parent_id not in specs_by_parent:
|
|
specs_by_parent[parent_id] = {
|
|
"parent_name": parent_name,
|
|
"specifications": [],
|
|
}
|
|
spec_data = {
|
|
"specification_id": specification.id_car_specification,
|
|
"s_name": specification.arabic_name
|
|
if lang == "ar"
|
|
else specification.name,
|
|
"s_value": value.value,
|
|
"s_unit": value.unit if value.unit else "",
|
|
"trim_name": value.id_car_trim.name,
|
|
}
|
|
specs_by_parent[parent_id]["specifications"].append(spec_data)
|
|
serialized_specs = [
|
|
{"parent_name": v["parent_name"], "specifications": v["specifications"]}
|
|
for v in specs_by_parent.values()
|
|
]
|
|
return JsonResponse(serialized_specs, safe=False)
|
|
|
|
def get_equipments(self, request):
|
|
trim_id = request.GET.get("trim_id")
|
|
equipments = (
|
|
models.CarEquipment.objects.filter(id_car_trim=trim_id)
|
|
.values("id_car_equipment", "name")
|
|
.order_by("name")
|
|
)
|
|
return JsonResponse(list(equipments), safe=False)
|
|
|
|
def get_options(self, request):
|
|
equipment_id = request.GET.get("equipment_id")
|
|
car_option_values = models.CarOptionValue.objects.filter(
|
|
id_car_equipment=equipment_id
|
|
)
|
|
|
|
options_by_parent = {}
|
|
for value in car_option_values:
|
|
option = value.id_car_option
|
|
parent = option.id_parent
|
|
parent_id = parent.id_car_option if parent else 0
|
|
parent_name = parent.name if parent else "Root"
|
|
if parent_id not in options_by_parent:
|
|
options_by_parent[parent_id] = {
|
|
"parent_name": parent_name,
|
|
"options": [],
|
|
}
|
|
option_data = {
|
|
"option_id": option.id_car_option,
|
|
"option_name": option.name,
|
|
"is_base": value.is_base,
|
|
"equipment_name": value.id_car_equipment.name,
|
|
}
|
|
options_by_parent[parent_id]["options"].append(option_data)
|
|
serialized_options = [
|
|
{"parent_name": v["parent_name"], "options": v["options"]}
|
|
for v in options_by_parent.values()
|
|
]
|
|
return JsonResponse(serialized_options, safe=False)
|
|
|
|
|
|
@method_decorator(csrf_exempt, name="dispatch")
|
|
class SearchCodeView(LoginRequiredMixin, View):
|
|
template_name = "inventory/scan_vin.html"
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
return render(request, self.template_name)
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
image_file = request.FILES.get("image")
|
|
|
|
if not image_file:
|
|
return JsonResponse({"success": False, "error": _("No image provided")})
|
|
|
|
try:
|
|
np_arr = np.frombuffer(image_file.read(), np.uint8)
|
|
image = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
|
|
if image is None:
|
|
raise ValueError("Invalid image format")
|
|
|
|
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
|
decoded_objects = decode(gray)
|
|
|
|
if not decoded_objects:
|
|
return JsonResponse(
|
|
{"success": False, "error": _("No QR/Barcode detected")}
|
|
)
|
|
|
|
code = decoded_objects[0].data.decode("utf-8").strip()
|
|
car = get_object_or_404(models.Car, vin=code)
|
|
return JsonResponse(
|
|
{
|
|
"success": True,
|
|
"code": code,
|
|
"redirect_url": reverse("car_detail", args=[car.slug]),
|
|
}
|
|
)
|
|
|
|
except Exception as e:
|
|
return JsonResponse({"success": False, "error": str(e)})
|
|
|
|
|
|
class CarInventory(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Represents the car inventory listing view for a dealership.
|
|
|
|
This class is a Django ListView-based implementation to display a list of cars
|
|
filtered by specific criteria such as make, model, and trim, while supporting user
|
|
authentication and permissions. It includes pagination and ordering features and
|
|
integrates search filtering functionality.
|
|
|
|
:ivar model: The model associated with the list view.
|
|
:type model: models.Car
|
|
:ivar home_label: The label of the home section of the application, used for UI
|
|
or breadcrumbs. Defined as a translatable string.
|
|
:type home_label: str
|
|
:ivar template_name: The template path used for rendering the car inventory
|
|
list view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable containing the
|
|
car objects in the template.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of car objects displayed per page.
|
|
:type paginate_by: int
|
|
:ivar ordering: The default ordering of car objects in the list view.
|
|
:type ordering: list
|
|
:ivar permission_required: The permission(s) required to access this view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = models.Car
|
|
home_label = _("inventory")
|
|
template_name = "inventory/car_inventory.html"
|
|
context_object_name = "cars"
|
|
paginate_by = 20
|
|
ordering = ["receiving_date"]
|
|
permission_required = ["inventory.view_car"]
|
|
|
|
def get_queryset(self, *args, **kwargs):
|
|
query = self.request.GET.get("q")
|
|
make = models.CarMake.objects.get(slug=self.kwargs["make_id"])
|
|
model = models.CarModel.objects.get(slug=self.kwargs["model_id"])
|
|
trim = models.CarTrim.objects.get(slug=self.kwargs["trim_id"])
|
|
|
|
dealer = get_user_type(self.request)
|
|
cars = models.Car.objects.filter(
|
|
dealer=dealer,
|
|
id_car_make=make,
|
|
id_car_model=model,
|
|
id_car_trim=trim,
|
|
).order_by("receiving_date")
|
|
|
|
return apply_search_filters(cars, query)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["query"] = self.request.GET.get("q", "")
|
|
context["make_id"] = self.kwargs["make_id"]
|
|
context["model_id"] = self.kwargs["model_id"]
|
|
context["trim_id"] = self.kwargs["trim_id"]
|
|
return context
|
|
|
|
|
|
class CarColorCreate(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
|
"""
|
|
View for creating a new car color.
|
|
|
|
This view is used to add new colors to a specific car by associating
|
|
the color with a car instance. It ensures that the user is logged in
|
|
and has the necessary permissions to add car colors. The form used in
|
|
this view is populated dynamically with respect to the related car
|
|
object that is retrieved based on the provided car primary key.
|
|
|
|
:ivar model: Reference to the model representing car colors.
|
|
:type model: models.CarColors
|
|
:ivar form_class: The form class used for car color creation.
|
|
:type form_class: forms.CarColorsForm
|
|
:ivar template_name: Path to the template used while rendering the view.
|
|
:type template_name: str
|
|
:ivar permission_required: List of permissions required by the view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = models.CarColors
|
|
form_class = forms.CarColorsForm
|
|
template_name = "inventory/add_colors.html"
|
|
permission_required = ["inventory.view_car"]
|
|
|
|
def form_valid(self, form):
|
|
car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
|
form.instance.car = car
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"car_detail",
|
|
kwargs={
|
|
"dealer_slug": self.request.dealer.slug,
|
|
"slug": self.kwargs["slug"],
|
|
},
|
|
)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
|
return context
|
|
|
|
|
|
class CarColorsUpdateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
|
):
|
|
model = models.CarColors
|
|
form_class = forms.CarColorsForm
|
|
template_name = "inventory/add_colors.html"
|
|
success_message = _("Car Colors details updated successfully")
|
|
permission_required = ["inventory.change_car"]
|
|
|
|
def get_object(self, queryset=None):
|
|
"""
|
|
Retrieves the CarColors instance associated with the Car slug from the URL.
|
|
This ensures we are updating the colors for the correct car.
|
|
"""
|
|
|
|
# Get the car_slug from the URL keywords arguments
|
|
slug = self.kwargs.get("slug")
|
|
|
|
# If no car_slug is provided, it's an invalid request
|
|
if not slug:
|
|
# You might want to raise Http404 or a more specific error here
|
|
raise ValueError("Car slug is required to identify the colors to update.")
|
|
|
|
return get_object_or_404(models.CarColors, car__slug=slug)
|
|
|
|
def get_success_url(self):
|
|
"""
|
|
Redirects to the car's detail page using its slug after a successful update.
|
|
"""
|
|
# self.object refers to the CarColors instance that was just updated.
|
|
# self.object.car then refers to the associated Car instance.
|
|
return reverse(
|
|
"car_detail",
|
|
kwargs={
|
|
"dealer_slug": self.request.dealer.slug,
|
|
"slug": self.object.car.slug,
|
|
},
|
|
)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""
|
|
Adds the related Car object to the template context.
|
|
"""
|
|
context = super().get_context_data(**kwargs)
|
|
# self.object is already available here from get_object()
|
|
context["car"] = self.object.car
|
|
context["page_title"] = _("Update Colors for %(car_name)s") % {
|
|
"car_name": context["car"]
|
|
}
|
|
return context
|
|
|
|
|
|
class CarListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Represents a view for listing and managing a collection of cars.
|
|
|
|
This class is used to display a paginated list of cars according to specific
|
|
filters and permissions. It ensures that only users with the required
|
|
permissions can access the view and handles dynamic filtering of cars based
|
|
on user input. The view also populates additional context data,
|
|
such as car statistics and related make, model, and year information,
|
|
to support detailed customization in templates.
|
|
|
|
:ivar model: Specifies the model associated with this view.
|
|
:type model: models.Car
|
|
:ivar template_name: The name of the template used for this view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the variable used in the template to
|
|
represent the objects being displayed.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of items to display per page in the paginated list.
|
|
:type paginate_by: int
|
|
:ivar permission_required: The permission required to access this view.
|
|
:type permission_required: str
|
|
"""
|
|
|
|
model = models.Car
|
|
template_name = "inventory/car_list_view.html"
|
|
context_object_name = "cars"
|
|
paginate_by = 10
|
|
permission_required = "inventory.view_car"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
dealer = get_user_type(self.request)
|
|
cars = models.Car.objects.filter(dealer=dealer).order_by("receiving_date")
|
|
|
|
context["stats"] = {
|
|
"all": cars.count(),
|
|
"available": cars.filter(status="available").count(),
|
|
"reserved": cars.filter(status="reserved").count(),
|
|
"sold": cars.filter(status="sold").count(),
|
|
"transfer": cars.filter(status="transfer").count(),
|
|
}
|
|
context["make"] = models.CarMake.objects.filter(is_sa_import=True,car__in=cars).distinct()
|
|
context["model"] = models.CarModel.objects.none()
|
|
context["year"] = models.Car.objects.none()
|
|
make = self.request.GET.get("make")
|
|
model = self.request.GET.get("model")
|
|
|
|
if make:
|
|
make_ = models.CarMake.objects.get(id_car_make=int(make))
|
|
context["model"] = make_.carmodel_set.filter(car__in=cars).distinct()
|
|
if make and model:
|
|
make_ = models.CarMake.objects.get(id_car_make=int(make))
|
|
model_ = models.CarModel.objects.get(id_car_model=int(model))
|
|
context["year"] = (
|
|
models.Car.objects.filter(id_car_make=make_, id_car_model=model_)
|
|
.values_list("year")
|
|
.distinct()
|
|
)
|
|
return context
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
qs = super().get_queryset()
|
|
qs = qs.filter(dealer=dealer)
|
|
status = self.request.GET.get("status")
|
|
search = self.request.GET.get("search")
|
|
make = self.request.GET.get("make", None)
|
|
model = self.request.GET.get("model", None)
|
|
year = self.request.GET.get("year", None)
|
|
car_status = self.request.GET.get("car_status", None)
|
|
|
|
if status:
|
|
qs = qs.filter(status=status)
|
|
if search:
|
|
query = (
|
|
Q(vin__icontains=search)
|
|
| Q(id_car_make__name__icontains=search)
|
|
| Q(id_car_model__name__icontains=search)
|
|
| Q(id_car_trim__name__icontains=search)
|
|
| Q(vin=search)
|
|
)
|
|
qs = qs.filter(query)
|
|
if any([make, model, year, car_status]):
|
|
query = Q()
|
|
if make:
|
|
query &= Q(id_car_make=int(make))
|
|
if model:
|
|
query &= Q(id_car_model=model)
|
|
if year:
|
|
query &= Q(year=year)
|
|
if car_status:
|
|
query &= Q(status=car_status)
|
|
qs = qs.filter(query)
|
|
return qs
|
|
|
|
|
|
@login_required
|
|
def inventory_stats_view(request, dealer_slug):
|
|
"""
|
|
Handle the inventory stats view for a dealer, displaying detailed information
|
|
about the cars, including counts grouped by make, model, and trim.
|
|
|
|
The function fetches all cars associated with the authenticated dealer, calculates
|
|
the inventory statistics (e.g., total cars, reserved cars, and cars categorized
|
|
by make, model, and trim levels), and prepares the data to be rendered in a
|
|
template.
|
|
|
|
:param request: The HTTP request object from the client.
|
|
:type request: HttpRequest
|
|
:return: An HTTP response containing structured inventory data rendered in the
|
|
"inventory/inventory_stats.html" template.
|
|
:rtype: HttpResponse
|
|
"""
|
|
|
|
# Base queryset for cars belonging to the dealer
|
|
cars = models.Car.objects.filter(dealer=request.dealer)
|
|
|
|
# Count for total, reserved, showroom, and unreserved cars
|
|
total_cars = cars.count()
|
|
reserved_cars = models.CarReservation.objects.count()
|
|
# showroom_cars = cars.filter(location='showroom').count()
|
|
# unreserved_cars = total_cars - reserved_cars
|
|
|
|
# Annotate total cars by make, model, and trim
|
|
cars = cars.select_related("id_car_make", "id_car_model", "id_car_trim").annotate(
|
|
make_total=Count("id_car_make"),
|
|
model_total=Count("id_car_model"),
|
|
trim_total=Count("id_car_trim"),
|
|
)
|
|
|
|
inventory = {}
|
|
for car in cars:
|
|
make = car.id_car_make
|
|
if make.id_car_make not in inventory:
|
|
inventory[make.id_car_make] = {
|
|
"make_id": make.id_car_make,
|
|
"slug": make.slug,
|
|
"make_name": make.get_local_name(),
|
|
"total_cars": 0,
|
|
"models": {},
|
|
}
|
|
inventory[make.id_car_make]["total_cars"] += 1
|
|
|
|
model = car.id_car_model
|
|
if model and model.id_car_model not in inventory[make.id_car_make]["models"]:
|
|
inventory[make.id_car_make]["models"][model.id_car_model] = {
|
|
"model_id": model.id_car_model,
|
|
"slug": model.slug,
|
|
"model_name": model.get_local_name(),
|
|
"total_cars": 0,
|
|
"trims": {},
|
|
}
|
|
try:
|
|
inventory[make.id_car_make]["models"][model.id_car_model]["total_cars"] += 1
|
|
|
|
trim = car.id_car_trim
|
|
if (
|
|
trim
|
|
and trim.id_car_trim
|
|
not in inventory[make.id_car_make]["models"][model.id_car_model][
|
|
"trims"
|
|
]
|
|
):
|
|
inventory[make.id_car_make]["models"][model.id_car_model]["trims"][
|
|
trim.id_car_trim
|
|
] = {
|
|
"trim_id": trim.id_car_trim,
|
|
"slug": trim.slug,
|
|
"trim_name": trim.name,
|
|
"total_cars": 0,
|
|
}
|
|
inventory[make.id_car_make]["models"][model.id_car_model]["trims"][
|
|
trim.id_car_trim
|
|
]["total_cars"] += 1
|
|
except Exception as e:
|
|
print(e)
|
|
result = {
|
|
"total_cars": total_cars,
|
|
"reserved_cars": reserved_cars,
|
|
"makes": [
|
|
{
|
|
"make_id": make_data["make_id"],
|
|
"slug": make_data["slug"],
|
|
"make_name": make_data["make_name"],
|
|
"total_cars": make_data["total_cars"],
|
|
"models": [
|
|
{
|
|
"model_id": model_data["model_id"],
|
|
"slug": model_data["slug"],
|
|
"model_name": model_data["model_name"],
|
|
"total_cars": model_data["total_cars"],
|
|
"trims": list(model_data["trims"].values()),
|
|
}
|
|
for model_data in make_data["models"].values()
|
|
],
|
|
}
|
|
for make_data in inventory.values()
|
|
],
|
|
}
|
|
return render(request, "inventory/inventory_stats.html", {"inventory": result})
|
|
|
|
|
|
# @login_required
|
|
# def inventory_stats_view(request):
|
|
# """
|
|
# Handle the inventory stats view for a dealer, displaying detailed information
|
|
# about the cars, including counts grouped by make, model, and trim.
|
|
|
|
# The function fetches all cars associated with the authenticated dealer, calculates
|
|
# the inventory statistics (e.g., total cars, reserved cars, and cars categorized
|
|
# by make, model, and trim levels), and prepares the data to be rendered in a
|
|
# template.
|
|
|
|
# :param request: The HTTP request object from the client.
|
|
# :type request: HttpRequest
|
|
# :return: An HTTP response containing structured inventory data rendered in the
|
|
# "inventory/inventory_stats.html" template.
|
|
# :rtype: HttpResponse
|
|
# """
|
|
# dealer = get_user_type(request)
|
|
|
|
# # Base queryset for cars belonging to the dealer
|
|
# # Ordering here is important for consistent pagination results
|
|
# cars = models.Car.objects.filter(dealer=dealer).order_by(
|
|
# 'id_car_make__name', 'id_car_model__name', 'id_car_trim__name'
|
|
# ) # Added ordering for consistent pagination
|
|
|
|
# # Count for total, reserved, showroom, and unreserved cars
|
|
# total_cars = cars.count()
|
|
# reserved_cars = models.CarReservation.objects.filter(car__dealer=dealer).count() # Filter reservations by dealer's cars
|
|
|
|
# # We need to process the cars into the inventory structure FIRST,
|
|
# # then paginate the list of makes.
|
|
# inventory_data = {}
|
|
# for car in cars:
|
|
# make = car.id_car_make
|
|
# if make.id_car_make not in inventory_data:
|
|
# inventory_data[make.id_car_make] = {
|
|
# "make_id": make.id_car_make,
|
|
# "slug": make.slug,
|
|
# "make_name": make.get_local_name(),
|
|
# "total_cars": 0,
|
|
# "models": {},
|
|
# }
|
|
# inventory_data[make.id_car_make]["total_cars"] += 1
|
|
|
|
# model = car.id_car_model
|
|
# # Ensure model exists before trying to access its attributes
|
|
# if model:
|
|
# if model.id_car_model not in inventory_data[make.id_car_make]["models"]:
|
|
# inventory_data[make.id_car_make]["models"][model.id_car_model] = {
|
|
# "model_id": model.id_car_model,
|
|
# "slug": model.slug,
|
|
# "model_name": model.get_local_name(),
|
|
# "total_cars": 0,
|
|
# "trims": {},
|
|
# }
|
|
# inventory_data[make.id_car_make]["models"][model.id_car_model]["total_cars"] += 1
|
|
|
|
# trim = car.id_car_trim
|
|
# if trim: # Ensure trim exists
|
|
# if trim.id_car_trim not in inventory_data[make.id_car_make]["models"][model.id_car_model]["trims"]:
|
|
# inventory_data[make.id_car_make]["models"][model.id_car_model]["trims"][
|
|
# trim.id_car_trim
|
|
# ] = {
|
|
# "trim_id": trim.id_car_trim,
|
|
# "slug": trim.slug,
|
|
# "trim_name": trim.name,
|
|
# "total_cars": 0,
|
|
# }
|
|
# inventory_data[make.id_car_make]["models"][model.id_car_model]["trims"][
|
|
# trim.id_car_trim
|
|
# ]["total_cars"] += 1
|
|
|
|
# # Convert the inventory dictionary into a list of makes for pagination
|
|
# # Sort the makes by name for consistent pagination
|
|
# all_makes = sorted(inventory_data.values(), key=lambda x: x['make_name'])
|
|
|
|
# # --- Pagination Logic ---
|
|
# items_per_page = 10 # You can adjust this number
|
|
# paginator = Paginator(all_makes, items_per_page)
|
|
|
|
# page_number = request.GET.get('page')
|
|
# try:
|
|
# page_obj = paginator.get_page(page_number)
|
|
# except PageNotAnInteger:
|
|
# page_obj = paginator.get_page(1)
|
|
# except EmptyPage:
|
|
# page_obj = paginator.get_page(paginator.num_pages)
|
|
|
|
# # The 'makes' list for the current page
|
|
# # Ensure models and trims are also converted to lists within each make on the current page
|
|
# paginated_makes_list = []
|
|
# for make_data in page_obj.object_list:
|
|
# make_data_copy = make_data.copy() # Avoid modifying the original dict during iteration
|
|
# make_data_copy['models'] = []
|
|
# for model_id, model_data in make_data['models'].items():
|
|
# model_data_copy = model_data.copy()
|
|
# model_data_copy['trims'] = list(model_data['trims'].values())
|
|
# make_data_copy['models'].append(model_data_copy)
|
|
# paginated_makes_list.append(make_data_copy)
|
|
|
|
|
|
# result = {
|
|
# "total_cars": total_cars,
|
|
# "reserved_cars": reserved_cars,
|
|
# "makes": paginated_makes_list, # This is now the paginated list of makes
|
|
# }
|
|
|
|
# # print(result["makes"]) # For debugging
|
|
# context = {
|
|
# "inventory": result,
|
|
# "page_obj": page_obj, # Pass the page_obj to the template
|
|
# "is_paginated": page_obj.has_other_pages(), # To use in template for conditional rendering
|
|
# }
|
|
# return render(request, "inventory/inventory_stats.html", context)
|
|
|
|
|
|
class CarDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Provides a detailed view of a single car instance.
|
|
|
|
This class-based view is designed to display detailed information about a
|
|
single car instance from the inventory. It utilizes Django's built-in
|
|
DetailView along with mixins to enforce authentication and permission
|
|
requirements. The view ensures that only authenticated users with the proper
|
|
permissions can access car details.
|
|
|
|
:ivar model: The model associated with this view, representing the Car model.
|
|
:type model: Model
|
|
:ivar template_name: The path to the template used to render the detailed
|
|
car view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable that contains
|
|
the car object.
|
|
:type context_object_name: str
|
|
:ivar permission_required: A list of permissions required to access the
|
|
view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = models.Car
|
|
template_name = "inventory/car_detail.html"
|
|
context_object_name = "car"
|
|
permission_required = ["inventory.view_car"]
|
|
|
|
|
|
class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
|
"""
|
|
Handles the creation of car finance records within the inventory system.
|
|
|
|
This view provides functionality to create car finance records tied to a
|
|
specific car in the inventory. It enforces that the user is logged in and
|
|
has the required permissions to add a car finance record. It also customizes
|
|
form behavior and context data to associate the finance record with the
|
|
corresponding car and populate additional services based on the user's type.
|
|
|
|
:ivar model: The database model associated with the view.
|
|
:type model: models.CarFinance
|
|
:ivar form_class: The form class used to create car finance records.
|
|
:type form_class: forms.CarFinanceForm
|
|
:ivar template_name: The template used to render the car finance creation page.
|
|
:type template_name: str
|
|
:ivar permission_required: The list of permissions required to access this view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = models.CarFinance
|
|
form_class = forms.CarFinanceForm
|
|
template_name = "inventory/car_finance_form.html"
|
|
permission_required = ["inventory.add_carfinance"]
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def form_valid(self, form):
|
|
form.instance.car = self.car
|
|
messages.success(self.request, _("Car finance details saved successfully"))
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse(
|
|
"car_detail",
|
|
kwargs={"dealer_slug": self.request.dealer.slug, "slug": self.car.slug},
|
|
)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["car"] = self.car
|
|
return context
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
dealer = get_user_type(self.request)
|
|
form.fields[
|
|
"additional_finances"
|
|
].queryset = models.AdditionalServices.objects.filter(dealer=dealer)
|
|
return form
|
|
|
|
|
|
class CarFinanceUpdateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
|
):
|
|
"""
|
|
Manages the update of car finance details.
|
|
|
|
This class-based view provides functionality for updating car finance
|
|
information in the system. It enforces login and specific permissions for
|
|
access, and it leverages mixins to provide success messages upon completing
|
|
updates. It is designed to interact with car finance data, update form
|
|
handling, and enforce related business logic.
|
|
|
|
:ivar model: The model associated with the view.
|
|
:type model: models.CarFinance
|
|
:ivar form_class: The form class used by the view.
|
|
:type form_class: forms.CarFinanceForm
|
|
:ivar template_name: The template used for rendering the form.
|
|
:type template_name: str
|
|
:ivar success_message: The success message displayed after an update.
|
|
:type success_message: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = models.CarFinance
|
|
form_class = forms.CarFinanceForm
|
|
template_name = "inventory/car_finance_form.html"
|
|
success_message = _("Car finance details updated successfully")
|
|
permission_required = ["inventory.change_carfinance"]
|
|
|
|
def get_success_url(self):
|
|
return reverse(
|
|
"car_detail",
|
|
kwargs={
|
|
"dealer_slug": self.request.dealer.slug,
|
|
"slug": self.object.car.slug,
|
|
},
|
|
)
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs["instance"] = self.get_object()
|
|
return kwargs
|
|
|
|
def get_initial(self):
|
|
initial = super().get_initial()
|
|
instance = self.get_object()
|
|
dealer = get_user_type(self.request)
|
|
selected_items = instance.additional_services.filter(dealer=dealer)
|
|
initial["additional_finances"] = selected_items
|
|
return initial
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
dealer = get_user_type(self.request)
|
|
form.fields[
|
|
"additional_finances"
|
|
].queryset = models.AdditionalServices.objects.filter(dealer=dealer)
|
|
return form
|
|
|
|
|
|
class CarUpdateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
|
):
|
|
"""
|
|
Represents a Django view dedicated to updating Car model instances.
|
|
|
|
This view facilitates secure and authorized updates to car-related information
|
|
through a web interface. It ensures permission checks, requires user login for
|
|
access, and communicates feedback upon successful updates.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: models.Model
|
|
:ivar form_class: The form class used for updating the model instance.
|
|
:type form_class: django.forms.ModelForm
|
|
:ivar template_name: The path to the template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar success_message: Message displayed upon successful update.
|
|
:type success_message: str
|
|
:ivar permission_required: List of permissions required to access this view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
model = models.Car
|
|
form_class = forms.CarUpdateForm
|
|
template_name = "inventory/car_edit.html"
|
|
success_message = _("Car updated successfully")
|
|
permission_required = ["inventory.change_car"]
|
|
|
|
def get_success_url(self):
|
|
return reverse("car_detail", kwargs={"slug": self.object.slug})
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
dealer = get_user_type(self.request)
|
|
print(dealer.get_vendors())
|
|
form.fields["vendor"].queryset = dealer.vendors.all()
|
|
return form
|
|
|
|
|
|
class CarDeleteView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, DeleteView
|
|
):
|
|
"""
|
|
Handles the deletion of Car objects in the inventory application.
|
|
|
|
This view ensures that only authorized users can delete a car. It requires users to be logged in
|
|
and have the appropriate permissions. Upon successful deletion, a success message is displayed,
|
|
and the user is redirected to the inventory statistics page.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: models.Car
|
|
:ivar template_name: The path to the HTML template used for confirming a car deletion.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to after successful deletion.
|
|
:type success_url: str
|
|
:ivar permission_required: A list of permission strings required to access this view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
model = models.Car
|
|
template_name = "inventory/car_confirm_delete.html"
|
|
|
|
permission_required = ["inventory.delete_car"]
|
|
|
|
def delete(self, request, *args, **kwargs):
|
|
messages.success(request, _("Car deleted successfully"))
|
|
return super().delete(request, *args, **kwargs)
|
|
def get_success_url(self):
|
|
"""
|
|
Returns the URL to redirect to after a successful car deletion.
|
|
It dynamically includes the dealer_slug from the URL.
|
|
"""
|
|
|
|
dealer_slug = self.kwargs.get('dealer_slug')
|
|
if dealer_slug:
|
|
return reverse_lazy("car_list", kwargs={'dealer_slug': dealer_slug})
|
|
else:
|
|
|
|
messages.error(self.request, _("Could not determine dealer for redirection."))
|
|
return reverse_lazy("home")
|
|
|
|
|
|
class CarLocationCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
|
"""
|
|
Handles the creation of new car locations.
|
|
|
|
This view allows authenticated and authorized users to add new car locations
|
|
to the system. The form allows users to specify details regarding the car's
|
|
location. Permissions are required to ensure only authorized users are
|
|
permitted to perform this action. Upon successful form submission, the
|
|
user is redirected to the car's detail page and a success message is displayed.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: models.CarLocation
|
|
:ivar form_class: The form class used to create or update the model.
|
|
:type form_class: forms.CarLocationForm
|
|
:ivar template_name: Path to the template used by the view.
|
|
:type template_name: str
|
|
:ivar permission_required: List of permissions required to create a car location.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = models.CarLocation
|
|
form_class = forms.CarLocationForm
|
|
template_name = "inventory/car_location_form.html"
|
|
permission_required = ["inventory.add_carlocation"]
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy("car_detail", kwargs={"slug": self.object.car.slug})
|
|
|
|
def form_valid(self, form):
|
|
form.instance.car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
|
dealer = get_user_type(self.request)
|
|
form.instance.owner = dealer
|
|
form.save()
|
|
messages.success(self.request, _("Location saved successfully"))
|
|
return super().form_valid(form)
|
|
|
|
|
|
class CarLocationUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
|
"""
|
|
Provides functionality to update the location of a car.
|
|
|
|
This view is a subclass of `LoginRequiredMixin` and `PermissionRequiredMixin`, ensuring
|
|
that only authenticated and authorized users with the appropriate permissions can access it.
|
|
It provides a form to update car location details and handles the necessary updates to the
|
|
location record, associating it with the appropriate car and owner. Upon successful update,
|
|
a success message is displayed to the user, and the user is redirected to the car detail page.
|
|
|
|
:ivar model: The model associated with this view. Represents car location.
|
|
:type model: models.CarLocation
|
|
:ivar form_class: The form class used for updating car location.
|
|
:type form_class: forms.CarLocationForm
|
|
:ivar template_name: The path to the template used to render the view.
|
|
:type template_name: str
|
|
:ivar permission_required: Permissions required to access this view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = models.CarLocation
|
|
form_class = forms.CarLocationForm
|
|
template_name = "inventory/car_location_form.html"
|
|
permission_required = ["inventory.update_carlocation"]
|
|
|
|
# def get_initial(self):
|
|
# initial = super().get_initial()
|
|
# initial["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
|
# return initial
|
|
|
|
def form_valid(self, form):
|
|
form.instance.car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
|
dealer = get_user_type(self.request)
|
|
form.instance.owner = dealer
|
|
form.save()
|
|
messages.success(self.request, _("Location updated successfully"))
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy("car_detail", kwargs={"slug": self.object.car.slug})
|
|
|
|
|
|
class CarTransferCreateView(LoginRequiredMixin, CreateView):
|
|
"""
|
|
A view for creating car transfers.
|
|
|
|
This class-based view allows authenticated users to create car transfers. It handles the
|
|
selection of cars and dealers involved in the transfer process. The view inherits from
|
|
`LoginRequiredMixin` to ensure only logged-in users can access it, and `CreateView` to
|
|
simplify the implementation of the creation logic.
|
|
|
|
:ivar model: The model class associated with the view.
|
|
:type model: models.CarTransfer
|
|
:ivar form_class: The form class used to render and validate the input data.
|
|
:type form_class: forms.CarTransferForm
|
|
:ivar template_name: The path to the template used to render the view.
|
|
:type template_name: str
|
|
"""
|
|
|
|
model = models.CarTransfer
|
|
form_class = forms.CarTransferForm
|
|
template_name = "inventory/car_transfer_form.html"
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
form.fields["to_dealer"].queryset = models.Dealer.objects.exclude(
|
|
pk=get_user_type(self.request).pk
|
|
).all()
|
|
form.fields["car"].queryset = models.Car.objects.filter(
|
|
slug=self.kwargs["slug"]
|
|
)
|
|
return form
|
|
|
|
def get_initial(self):
|
|
initial = super().get_initial()
|
|
initial["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
|
return initial
|
|
|
|
def form_valid(self, form):
|
|
form.instance.from_dealer = get_user_type(self.request)
|
|
form.instance.car.status = "transfer"
|
|
form.instance.car.save()
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy("car_detail", kwargs={"slug": self.object.car.slug})
|
|
|
|
|
|
class CarTransferDetailView(LoginRequiredMixin, SuccessMessageMixin, DetailView):
|
|
"""
|
|
Provides a detailed view of a specific car transfer record.
|
|
|
|
This class-based view is used to display details of a car transfer for
|
|
authenticated users. It ensures that only authorized users can access this
|
|
information and renders the data along with additional context such as
|
|
specific actions tied to the transfer. The view is associated with a Django
|
|
model for car transfers and uses a specific template to display information.
|
|
|
|
:ivar model: The model associated with this view, which represents car
|
|
transfer records.
|
|
:type model: Type[models.CarTransfer]
|
|
:ivar template_name: The path to the template used for rendering the car
|
|
transfer details.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context object used in the template
|
|
to reference the car transfer record.
|
|
:type context_object_name: str
|
|
"""
|
|
|
|
model = models.CarTransfer
|
|
template_name = "inventory/transfer_details.html"
|
|
context_object_name = "transfer"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["action"] = self.request.GET.get("action")
|
|
return context
|
|
|
|
|
|
@login_required
|
|
def car_transfer_approve(request, slug, transfer_pk):
|
|
"""
|
|
Approves or cancels a car transfer request based on the action parameter. This view
|
|
handles the workflow of updating the transfer status and notifying the involved parties
|
|
accordingly. If the transfer is canceled, it reverts the car status to "available" and
|
|
deactivates the transfer record. If approved, it notifies the recipient dealer and allows
|
|
the request to proceed for further actions.
|
|
|
|
:param request: The HTTP request object containing metadata and the action parameter.
|
|
:param car_pk: Primary key of the car involved in the transfer.
|
|
:param transfer_pk: Primary key of the transfer request to approve or cancel.
|
|
:return: An HTTP response redirecting to the car detail page of the specified car.
|
|
"""
|
|
car = get_object_or_404(models.Car, slug=slug)
|
|
transfer = get_object_or_404(models.CarTransfer, pk=transfer_pk)
|
|
action = request.GET.get("action")
|
|
if action == "cancel":
|
|
transfer.status = "cancel"
|
|
transfer.active = False
|
|
transfer.save()
|
|
transfer.car.status = "available"
|
|
transfer.car.save()
|
|
messages.success(request, _("Car transfer canceled successfully"))
|
|
models.Notification.objects.create(
|
|
user=transfer.from_dealer.user,
|
|
message=f"Car transfer request from {transfer.to_dealer} is canceled.",
|
|
)
|
|
return redirect("car_detail", slug=car.slug)
|
|
transfer.status = "approved"
|
|
transfer.save()
|
|
url = request.build_absolute_uri(
|
|
reverse(
|
|
"transfer_preview", kwargs={"slug": car.slug, "transfer_pk": transfer.pk}
|
|
)
|
|
)
|
|
models.Notification.objects.create(
|
|
user=transfer.to_dealer.user,
|
|
message=f"Car transfer request from {transfer.from_dealer} is waiting for your acceptance. <a href='{url}'> Accept</a>",
|
|
)
|
|
messages.success(request, _("Car transfer approved successfully"))
|
|
return redirect("car_detail", slug=car.slug)
|
|
|
|
|
|
@login_required
|
|
def car_transfer_accept_reject(request, slug, transfer_pk):
|
|
"""
|
|
Handles the acceptance or rejection of a car transfer request. Based on the
|
|
`status` parameter obtained from the query string, the function updates the
|
|
transfer status to either 'accept' or 'reject'. If the transfer is accepted, it
|
|
initiates the car transfer process. Appropriate notifications are sent, and
|
|
activity records are created for both acceptance and rejection actions.
|
|
|
|
:param request: The HTTP request object which contains metadata about
|
|
the request made by the user, including session and user information.
|
|
:param car_pk: The primary key of the car to be transferred.
|
|
:param transfer_pk: The primary key of the car transfer request to be processed.
|
|
:return: An HTTP redirect response to the 'inventory_stats' view.
|
|
"""
|
|
car = get_object_or_404(models.Car, slug=slug)
|
|
transfer = get_object_or_404(models.CarTransfer, pk=transfer_pk)
|
|
status = request.GET.get("status")
|
|
if status == "rejected":
|
|
transfer.status = "reject"
|
|
transfer.active = False
|
|
messages.success(request, _("Car transfer rejected successfully"))
|
|
models.Notification.objects.create(
|
|
user=transfer.from_dealer.user,
|
|
message=f"Car transfer request from {transfer.to_dealer} is rejected.",
|
|
)
|
|
transfer.save()
|
|
elif status == "accepted":
|
|
transfer.status = "accept"
|
|
transfer.save()
|
|
transfer_process = CarTransfer(car, transfer)
|
|
success = transfer_process.transfer_car()
|
|
if success:
|
|
messages.success(request, _("Car Transfer Completed successfully."))
|
|
models.Activity.objects.create(
|
|
content_object=car,
|
|
notes=f"Transfered from {transfer.from_dealer} to {transfer.to_dealer}",
|
|
created_by=request.user,
|
|
)
|
|
models.Notification.objects.create(
|
|
user=transfer.from_dealer.user,
|
|
message=f"Car transfer request from {transfer.to_dealer} is completed.",
|
|
)
|
|
return redirect("inventory_stats")
|
|
|
|
|
|
@login_required
|
|
def CarTransferPreviewView(request, slug, transfer_pk):
|
|
"""
|
|
Handles the preview of car transfer details and ensures that a user has appropriate
|
|
permissions to view the transfer based on their associated dealer.
|
|
|
|
This view checks if the car transfer's destination dealer matches the current user's
|
|
associated dealer type. If not, the user is redirected to the car detail page. Otherwise,
|
|
it renders the transfer preview page with the relevant transfer details.
|
|
|
|
:param request: The HTTP request object
|
|
:type request: django.http.HttpRequest
|
|
:param car_pk: The primary key of the car related to the transfer
|
|
:type car_pk: int
|
|
:param transfer_pk: The primary key of the car transfer to preview
|
|
:type transfer_pk: int
|
|
:return: An HTTP response rendering the transfer preview page or redirecting
|
|
to the car detail page
|
|
:rtype: django.http.HttpResponse
|
|
"""
|
|
transfer = get_object_or_404(models.CarTransfer, pk=transfer_pk)
|
|
if transfer.to_dealer != get_user_type(request):
|
|
return redirect("car_detail", slug=slug)
|
|
return render(request, "inventory/transfer_preview.html", {"transfer": transfer})
|
|
|
|
|
|
class CustomCardCreateView(LoginRequiredMixin, CreateView):
|
|
"""
|
|
Represents a view for creating a custom card associated with a specific car.
|
|
|
|
This view ensures that the user is authenticated before allowing access and
|
|
associates the created custom card with a specific car. It handles form validation,
|
|
context data injection, and determines the success URL upon successful form submission.
|
|
|
|
:ivar model: The model associated with the view, which is `CustomCard`.
|
|
:type model: models.CustomCard
|
|
:ivar form_class: The form class used to create a new instance of `CustomCard`.
|
|
:type form_class: forms.CustomCardForm
|
|
:ivar template_name: The name of the template used to render the view.
|
|
:type template_name: str
|
|
"""
|
|
|
|
model = models.CustomCard
|
|
form_class = forms.CustomCardForm
|
|
template_name = "inventory/add_custom_card.html"
|
|
|
|
def form_valid(self, form):
|
|
car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
|
form.instance.car = car
|
|
return super().form_valid(form)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
|
return context
|
|
|
|
def get_success_url(self):
|
|
messages.success(self.request, _("Custom Card added successfully"))
|
|
return reverse_lazy(
|
|
"car_detail",
|
|
kwargs={
|
|
"dealer_slug": self.request.dealer.slug,
|
|
"slug": self.kwargs["slug"],
|
|
},
|
|
)
|
|
|
|
|
|
class CarRegistrationCreateView(LoginRequiredMixin, CreateView):
|
|
"""
|
|
Handles the creation of new car registration records.
|
|
|
|
This view is responsible for rendering the car registration creation form,
|
|
validating input, and saving the data to the database. It ensures that the
|
|
current user is authenticated and associates the registration with a specific
|
|
car, identified by its primary key.
|
|
|
|
Inherits from:
|
|
- LoginRequiredMixin: Ensures the user is logged in to access the view.
|
|
- CreateView: Provides built-in functionality for creating and saving model
|
|
instances.
|
|
|
|
:ivar model: The model linked to this view, representing car registrations.
|
|
:type model: models.CarRegistration
|
|
:ivar form_class: The form class used for creating car registration instances.
|
|
:type form_class: forms.CarRegistrationForm
|
|
:ivar template_name: The path to the HTML template used for rendering the car
|
|
registration form.
|
|
:type template_name: str
|
|
"""
|
|
|
|
model = models.CarRegistration
|
|
form_class = forms.CarRegistrationForm
|
|
template_name = "inventory/car_registration_form.html"
|
|
|
|
def form_valid(self, form):
|
|
car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
|
form.instance.car = car
|
|
return super().form_valid(form)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
|
return context
|
|
|
|
def get_success_url(self):
|
|
messages.success(self.request, _("Registration added successfully"))
|
|
return reverse_lazy(
|
|
"car_detail",
|
|
kwargs={
|
|
"dealer_slug": self.request.dealer.slug,
|
|
"slug": self.kwargs["slug"],
|
|
},
|
|
)
|
|
|
|
|
|
@login_required()
|
|
def reserve_car_view(request, dealer_slug, slug):
|
|
"""
|
|
Handles car reservation requests. This view requires the user to be logged in
|
|
and processes only POST requests. When invoked, it checks if the specified car
|
|
is already reserved. If not, it proceeds to reserve the car for the user and
|
|
sends an appropriate response. If the car is already reserved or if the request
|
|
method is invalid, it provides corresponding error messages or responses.
|
|
|
|
:param request: The HTTP request object.
|
|
:type request: HttpRequest
|
|
:param car_id: The unique identifier of the car to be reserved.
|
|
:type car_id: int
|
|
:return: A response indicating the result of the reservation process.
|
|
:rtype: HttpResponse or JsonResponse
|
|
"""
|
|
if request.method == "POST":
|
|
car = get_object_or_404(models.Car, slug=slug)
|
|
if car.is_reserved():
|
|
messages.error(request, _("This car is already reserved"))
|
|
return redirect("car_detail", slug=car.slug)
|
|
response = reserve_car(car, request)
|
|
return response
|
|
return JsonResponse(
|
|
{"success": False, "message": "Invalid request method"}, status=400
|
|
)
|
|
|
|
|
|
@login_required
|
|
def manage_reservation(request, dealer_slug, reservation_id):
|
|
"""
|
|
Handles the management of a car reservation, providing options to renew or
|
|
cancel an existing reservation associated with the logged-in user.
|
|
|
|
Renewing a reservation extends the reservation period by an additional
|
|
24 hours. Canceling a reservation deletes it and updates the car's status
|
|
to AVAILABLE. All actions require a valid reservation and are performed
|
|
based on the current user's authentication and request type.
|
|
|
|
:param request: Django HttpRequest object representing the client's request.
|
|
:type request: HttpRequest
|
|
:param reservation_id: The unique identifier of the car reservation to manage.
|
|
:type reservation_id: int
|
|
:return: On POST requests, returns an HTTP redirect or JSON response
|
|
based on the action performed. On other request methods,
|
|
returns a JSON response with an error message.
|
|
:rtype: JsonResponse or HttpResponseRedirect
|
|
"""
|
|
reservation = get_object_or_404(
|
|
models.CarReservation, pk=reservation_id, reserved_by=request.user
|
|
)
|
|
|
|
if request.method == "POST":
|
|
action = request.POST.get("action")
|
|
if action == "renew":
|
|
reservation.reserved_until = timezone.now() + timezone.timedelta(hours=24)
|
|
reservation.save()
|
|
messages.success(request, _("Reservation renewed successfully"))
|
|
return redirect(
|
|
"car_detail", dealer_slug=request.dealer.slug, slug=reservation.car.slug
|
|
)
|
|
|
|
elif action == "cancel":
|
|
car = reservation.car
|
|
reservation.delete()
|
|
car.status = models.CarStatusChoices.AVAILABLE
|
|
car.save()
|
|
messages.success(request, _("Reservation canceled successfully"))
|
|
return redirect(
|
|
"car_detail", dealer_slug=request.dealer.slug, slug=reservation.car.slug
|
|
)
|
|
|
|
else:
|
|
return JsonResponse(
|
|
{"success": False, "message": _("Invalid action")}, status=400
|
|
)
|
|
|
|
return JsonResponse(
|
|
{"success": False, "message": _("Invalid request method")}, status=400
|
|
)
|
|
|
|
|
|
class DealerDetailView(LoginRequiredMixin, 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,is_sa_import=True)
|
|
staff_count = dealer.staff_count
|
|
cars_count = models.Car.objects.filter(dealer=dealer).count()
|
|
|
|
context["car_makes"] = car_makes
|
|
context["staff_count"] = staff_count
|
|
context["cars_count"] = cars_count
|
|
context["allowed_users"] = dealer.user_quota
|
|
context["allowed_cars"] = dealer.car_quota
|
|
context["quota_display"] = (
|
|
f"{staff_count}/{dealer.user_quota}" if dealer.user_quota else "0"
|
|
)
|
|
|
|
return context
|
|
|
|
|
|
class DealerUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
|
"""
|
|
Handles the update functionality for the Dealer model.
|
|
|
|
This class-based view allows authenticated users to update an existing
|
|
Dealer instance using a form. Upon successful update, a success message
|
|
is displayed, and the user is redirected to the detail view of the updated
|
|
Dealer.
|
|
|
|
:ivar model: The model class associated with this view.
|
|
:type model: models.Dealer
|
|
:ivar form_class: The form class used for this view.
|
|
:type form_class: forms.DealerForm
|
|
:ivar template_name: The template used to render the form for this view.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to after a successful form submission.
|
|
:type success_url: str
|
|
:ivar success_message: The message displayed upon a successful update.
|
|
:type success_message: str
|
|
"""
|
|
|
|
model = models.Dealer
|
|
form_class = forms.DealerForm
|
|
template_name = "dealers/dealer_form.html"
|
|
success_url = reverse_lazy("dealer_detail")
|
|
success_message = _("Dealer updated successfully")
|
|
|
|
def get_success_url(self):
|
|
return reverse("dealer_detail", kwargs={"slug": self.object.slug})
|
|
|
|
|
|
class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
View for displaying a list of customers.
|
|
|
|
This class-based view is used to display a paginated and searchable list of
|
|
customers. It ensures that the user has the required permissions and is
|
|
logged in before they can access the view. The view fetches a list of
|
|
customers related to the current user's entity and applies any search
|
|
filters based on the user's query. The data is rendered using a specified
|
|
template.
|
|
|
|
:ivar model: The model associated with the view, representing customer data.
|
|
:type model: type[CustomerModel]
|
|
:ivar home_label: The label used for navigation purposes in the UI.
|
|
:type home_label: str
|
|
:ivar context_object_name: The name of the context variable in the template.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: Number of items displayed per page for pagination.
|
|
:type paginate_by: int
|
|
:ivar template_name: The path of the template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar ordering: The default ordering applied to the queryset of customers.
|
|
:type ordering: list
|
|
:ivar permission_required: A list of permissions required to access the view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = models.Customer
|
|
home_label = _("customers")
|
|
context_object_name = "customers"
|
|
paginate_by = 30
|
|
template_name = "customers/customer_list.html"
|
|
ordering = ["-created"]
|
|
permission_required = ["django_ledger.view_customermodel"]
|
|
|
|
def get_queryset(self):
|
|
query = self.request.GET.get("q")
|
|
dealer = get_user_type(self.request)
|
|
customers = dealer.customers.filter(active=True)
|
|
return apply_search_filters(customers, query)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["query"] = self.request.GET.get("q", "")
|
|
return context
|
|
|
|
|
|
class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
CustomerDetailView handles retrieving and presenting detailed information about
|
|
a specific customer. It ensures that the user is authenticated and has the
|
|
necessary permissions before accessing the customer's details. This view
|
|
provides context data including estimates and invoices related to the customer.
|
|
|
|
:ivar model: The model associated with the view.
|
|
:type model: CustomerModel
|
|
:ivar template_name: The path to the template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the variable in the template context
|
|
for the object being viewed.
|
|
:type context_object_name: str
|
|
:ivar permission_required: The list of permissions required to access this view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
model = models.Customer
|
|
template_name = "customers/view_customer.html"
|
|
context_object_name = "customer"
|
|
permission_required = ["django_ledger.view_customermodel"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
dealer = get_user_type(self.request)
|
|
entity = dealer.entity
|
|
context = super().get_context_data(**kwargs)
|
|
context["customer_notes"] = models.Notes.objects.filter(
|
|
object_id=self.object.pk
|
|
)
|
|
estimates = entity.get_estimates().filter(customer=self.object.customer_model)
|
|
invoices = entity.get_invoices().filter(customer=self.object.customer_model)
|
|
|
|
total = estimates.count() + invoices.count()
|
|
|
|
context["estimates"] = estimates
|
|
context["invoices"] = invoices
|
|
context["total"] = total
|
|
return context
|
|
|
|
|
|
@login_required
|
|
def add_note_to_customer(request,dealer_slug, slug):
|
|
"""
|
|
This function allows authenticated users to add a note to a specific customer. The
|
|
note creation is handled by a form, which is validated after submission. If the form
|
|
is valid, the note is saved and associated with the specified customer. On successful
|
|
submission, the user is redirected to the customer detail page. If the request method
|
|
is not POST, an empty form is rendered.
|
|
|
|
:param request: The HTTP request object containing metadata and the method type
|
|
(e.g., GET, POST). Should be an HttpRequest instance.
|
|
:param customer_id: The unique identifier (UUID) of the customer to whom the note
|
|
is to be added.
|
|
:return: An HTTP response. In the case of a successful POST request, the function
|
|
returns a redirect response to the customer detail page. For a GET or invalid
|
|
POST request, it renders the note form template with context including
|
|
the form and customer.
|
|
"""
|
|
# get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
customer = get_object_or_404(models.Customer, slug=slug)
|
|
if request.method == "POST":
|
|
form = forms.NoteForm(request.POST)
|
|
if form.is_valid():
|
|
note = form.save(commit=False)
|
|
note.content_object = customer
|
|
|
|
note.created_by = request.user
|
|
note.save()
|
|
return redirect("customer_detail", dealer_slug=dealer_slug,slug=customer.slug)
|
|
|
|
form = forms.NoteForm()
|
|
return render(
|
|
request, "customers/note_form.html", {"form": form, "customer": customer}
|
|
)
|
|
|
|
|
|
@login_required
|
|
def add_activity_to_customer(request, pk):
|
|
"""
|
|
Adds an activity to a specific customer.
|
|
|
|
This function allows adding a new activity to a customer identified by their
|
|
primary key (`pk`). It retrieves the customer object, processes the form for
|
|
activity creation, and saves it. If the request method is POST, it validates
|
|
the form and associates the activity with the respective customer. Upon
|
|
successful save, it redirects to the customer detail view. If the request
|
|
method is GET, it renders a form for activity submission.
|
|
|
|
:param request: The HTTP request object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the customer to which the activity will be added.
|
|
:type pk: int
|
|
:return: An HTTP response rendered with the activity form in the context of
|
|
the customer, or a redirect response to the customer detail view upon
|
|
successful activity creation.
|
|
:rtype: HttpResponse
|
|
"""
|
|
customer = get_object_or_404(CustomerModel, pk=pk)
|
|
if request.method == "POST":
|
|
form = forms.ActivityForm(request.POST)
|
|
if form.is_valid():
|
|
activity = form.save(commit=False)
|
|
activity.content_object = customer
|
|
activity.created_by = request.user
|
|
activity.save()
|
|
return redirect("customer_detail", pk=pk)
|
|
else:
|
|
form = forms.ActivityForm()
|
|
return render(
|
|
request, "crm/add_activity.html", {"form": form, "customer": customer}
|
|
)
|
|
|
|
|
|
class CustomerCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
|
"""
|
|
# Handles the creation of a new customer within the system. This view ensures that proper permissions
|
|
# and request methods are utilized. It provides feedback to the user about the success or failure of
|
|
# the customer creation process. When the form is submitted and valid, it checks for duplicate
|
|
# customers based on the email provided before proceeding with the customer creation.
|
|
|
|
# :param request: The HTTP request object containing metadata about the request initiated by the user.
|
|
# :type request: HttpRequest
|
|
# :return: The rendered form page or a redirect to the customer list page upon successful creation.
|
|
# :rtype: HttpResponse
|
|
# :raises PermissionDenied: If the user does not have the required permissions to access the view.
|
|
#"""
|
|
|
|
model = models.Customer
|
|
form_class = forms.CustomerForm
|
|
permission_required = ["django_ledger.add_customermodel"]
|
|
template_name = "customers/customer_form.html"
|
|
success_url = reverse_lazy("customer_list")
|
|
success_message = "Customer created successfully"
|
|
|
|
def form_valid(self, form):
|
|
if customer := models.Customer.objects.filter(
|
|
email=form.instance.email
|
|
).first():
|
|
if not customer.active:
|
|
messages.error(
|
|
self.request,
|
|
_(
|
|
"Customer Account with this email is Deactivated,Please Contact Admin"
|
|
),
|
|
)
|
|
else:
|
|
messages.error(
|
|
self.request, _("Customer with this email already exists")
|
|
)
|
|
return redirect("customer_create")
|
|
dealer = get_user_type(self.request)
|
|
form.instance.dealer = dealer
|
|
try:
|
|
user = form.instance.create_user_model()
|
|
except IntegrityError as e:
|
|
if "UNIQUE constraint" in str(e):
|
|
messages.error(self.request, _("Email already exists"))
|
|
else:
|
|
messages.error(self.request, str(e))
|
|
return redirect("customer_create")
|
|
customer = form.instance.create_customer_model()
|
|
|
|
form.instance.user = user
|
|
form.instance.customer_model = customer
|
|
|
|
return super().form_valid(form)
|
|
def get_success_url(self):
|
|
return reverse_lazy("customer_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]})
|
|
|
|
|
|
class CustomerUpdateView(LoginRequiredMixin, PermissionRequiredMixin, 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)
|
|
def get_success_url(self):
|
|
return reverse_lazy("customer_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]})
|
|
|
|
@login_required
|
|
def delete_customer(request, dealer_slug ,slug):
|
|
"""
|
|
Deletes a customer from the system and deactivates the corresponding user account.
|
|
|
|
This function retrieves a customer object based on the primary key (pk),
|
|
sets their active status to False, and deactivates the linked user account
|
|
(using the email associated with the customer). After saving these changes,
|
|
it displays a success message to the user and redirects to the customer list page.
|
|
|
|
:param request: A HttpRequest object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: Primary key of the customer to be deleted.
|
|
:type pk: int
|
|
:return: A redirect response to the customer list page.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
customer = get_object_or_404(models.Customer, slug=slug)
|
|
customer.deactivate_account()
|
|
messages.success(request, _("Customer deactivated successfully"))
|
|
return redirect("customer_list", dealer_slug=dealer_slug)
|
|
|
|
|
|
class VendorListView(LoginRequiredMixin, ListView):
|
|
"""
|
|
Represents a view for displaying a paginated list of vendors, accessible only
|
|
to authenticated users.
|
|
|
|
This class inherits from ``LoginRequiredMixin`` and ``ListView`` and is
|
|
designed to display a paginated list of vendors associated with a user. It
|
|
utilizes filters and search capabilities based on user input and is rendered
|
|
using a specified template. The list is ordered by the creation date of the
|
|
vendors in descending order.
|
|
|
|
:ivar model: The model that this view interacts with.
|
|
:type model: VendorModel
|
|
:ivar context_object_name: The name of the context variable used in the
|
|
template for the list of vendors.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of vendors to display per page in the paginated
|
|
view.
|
|
:type paginate_by: int
|
|
:ivar template_name: The path to the template used to render the list of
|
|
vendors.
|
|
:type template_name: str
|
|
:ivar ordering: The default ordering applied to the queryset. Vendors are
|
|
ordered by their creation date in descending order.
|
|
:type ordering: list
|
|
"""
|
|
|
|
model = models.Vendor
|
|
context_object_name = "vendors"
|
|
paginate_by = 30
|
|
template_name = "vendors/vendors_list.html"
|
|
|
|
def get_queryset(self):
|
|
query = self.request.GET.get("q")
|
|
dealer = get_user_type(self.request)
|
|
vendors = super().get_queryset().filter(dealer=dealer, active=True)
|
|
if query:
|
|
return apply_search_filters(vendors, query)
|
|
return vendors
|
|
|
|
|
|
@login_required
|
|
def vendorDetailView(request, dealer_slug,slug):
|
|
"""
|
|
Fetches and renders the detail view for a specific vendor.
|
|
|
|
This function retrieves a vendor object based on the primary key (pk)
|
|
provided in the URL, ensures the user is logged in to access the
|
|
view, and renders the vendor detail template with the vendor's context.
|
|
|
|
:param request: The HTTP request object.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the vendor to retrieve.
|
|
:type pk: int
|
|
:return: An HttpResponse object containing the rendered vendor detail page.
|
|
:rtype: HttpResponse
|
|
"""
|
|
vendor = get_object_or_404(models.Vendor, slug=slug)
|
|
return render(
|
|
request, template_name="vendors/view_vendor.html", context={"vendor": vendor}
|
|
)
|
|
|
|
|
|
class VendorCreateView(
|
|
LoginRequiredMixin,
|
|
SuccessMessageMixin,
|
|
CreateView,
|
|
):
|
|
"""
|
|
Handles the creation of a new vendor.
|
|
|
|
This view allows authenticated users to create a new vendor entry. It uses the
|
|
Vendor model and its corresponding form for creation. A success message is displayed
|
|
upon successful creation, and the user is redirected to the list of vendors.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: models.Vendor
|
|
:ivar form_class: The form class used for creating a vendor.
|
|
:type form_class: forms.VendorForm
|
|
:ivar template_name: The name of the template used to render the view.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to after successful vendor creation.
|
|
:type success_url: str
|
|
:ivar success_message: The message displayed upon successful creation.
|
|
:type success_message: str
|
|
"""
|
|
|
|
model = models.Vendor
|
|
form_class = forms.VendorForm
|
|
template_name = "vendors/vendor_form.html"
|
|
success_message = _("Vendor created successfully")
|
|
|
|
def form_valid(self, form):
|
|
if vendor := models.Vendor.objects.filter(email=form.instance.email).first():
|
|
if not vendor.active:
|
|
messages.error(
|
|
self.request,
|
|
_(
|
|
"Vendor Account with this email is Deactivated,Please Contact Admin"
|
|
),
|
|
)
|
|
else:
|
|
messages.error(self.request, _("Vendor with this email already exists"))
|
|
return redirect("vendor_create")
|
|
dealer = get_user_type(self.request)
|
|
form.instance.dealer = dealer
|
|
form.instance.save()
|
|
|
|
return super().form_valid(form)
|
|
def get_success_url(self):
|
|
return reverse_lazy("vendor_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]})
|
|
|
|
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_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)
|
|
def get_success_url(self):
|
|
return reverse_lazy("vendor_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]})
|
|
|
|
@login_required
|
|
def delete_vendor(request, dealer_slug,slug):
|
|
"""
|
|
Deletes an existing vendor record from the database.
|
|
|
|
This function allows users with valid authentication to delete a specified
|
|
vendor object by its primary key. Upon successful deletion, a success message
|
|
is displayed, and the user is redirected to the vendor list page.
|
|
|
|
:param request: HttpRequest object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: Primary key of the vendor object to be deleted.
|
|
:type pk: int
|
|
:return: HttpResponseRedirect object for redirecting to the vendor list page.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
vendor = get_object_or_404(models.Vendor, slug=slug)
|
|
vendor.active = False
|
|
vendor.vendor_model.active = False
|
|
vendor.save()
|
|
messages.success(request, _("Vendor deleted successfully"))
|
|
return redirect("vendor_list", dealer_slug=dealer_slug)
|
|
|
|
|
|
# group
|
|
class GroupListView(LoginRequiredMixin, 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_message = _("Group created successfully")
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
instance = form.save(commit=False)
|
|
group_name = f"{dealer.slug}_{instance.name}"
|
|
group,created = Group.objects.get_or_create(name=group_name)
|
|
if created:
|
|
group_manager, created = models.CustomGroup.objects.get_or_create(
|
|
name=group_name, dealer=dealer, group=group
|
|
)
|
|
group_manager.set_default_permissions()
|
|
dealer.user.groups.add(group)
|
|
else:
|
|
instance.dealer = dealer
|
|
instance.group = group
|
|
instance.save()
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy("group_list", args=[self.request.dealer.slug])
|
|
|
|
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_message = _("Group updated successfully")
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
instance = form.save(commit=False)
|
|
instance.set_default_permissions()
|
|
instance.group.name = f"{dealer.slug}_{instance.name}"
|
|
instance.save()
|
|
return super().form_valid(form)
|
|
def get_success_url(self):
|
|
return reverse_lazy("group_list", args=[self.request.dealer.slug])
|
|
|
|
@login_required
|
|
def GroupDeleteview(request, dealer_slug,pk):
|
|
"""
|
|
Handles the deletion of a specific group instance. This view ensures that only
|
|
authenticated users can perform the deletion. Upon successful deletion, a
|
|
success message is displayed, and the user is redirected to the group list page.
|
|
|
|
:param request: The HTTP request object that contains metadata about the
|
|
request context and user information. Must be an authenticated user.
|
|
:param pk: The primary key of the group instance to be deleted.
|
|
It specifies which group to retrieve and delete.
|
|
:return: The HTTP response that redirects the user to the group list page
|
|
after the group is successfully deleted.
|
|
"""
|
|
get_object_or_404(models.Dealer,slug=dealer_slug)
|
|
group = get_object_or_404(models.CustomGroup, pk=pk)
|
|
group.delete()
|
|
messages.success(request, _("Group deleted successfully"))
|
|
return redirect("group_list",dealer_slug=dealer_slug)
|
|
|
|
|
|
@login_required
|
|
def GroupPermissionView(request, dealer_slug,pk):
|
|
"""
|
|
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
|
|
"""
|
|
get_object_or_404(models.Dealer,slug=dealer_slug)
|
|
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", dealer_slug=dealer_slug,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, dealer_slug,slug):
|
|
"""
|
|
Handles the assignment of user groups to a specific staff member. This view
|
|
allows updating the groups a staff member belongs to via a form submission.
|
|
It processes both GET and POST requests, ensuring appropriate group
|
|
assignments are managed and feedback is provided to the user via messages.
|
|
|
|
:param request: HttpRequest object representing the HTTP request.
|
|
:type request: HttpRequest
|
|
:param pk: Primary key of the staff member whose groups are being updated.
|
|
:type pk: int
|
|
|
|
:return: Renders the user group form for GET requests or redirects to the
|
|
user detail page after successful submission for POST requests.
|
|
:rtype: HttpResponse or HttpResponseRedirect
|
|
"""
|
|
get_object_or_404(models.Dealer,slug=dealer_slug)
|
|
staff = get_object_or_404(models.Staff, slug=slug)
|
|
if request.method == "POST":
|
|
form = forms.UserGroupForm(request.POST)
|
|
groups = request.POST.getlist("name")
|
|
|
|
staff.clear_groups()
|
|
for i in groups:
|
|
cg = models.CustomGroup.objects.get(id=int(i))
|
|
staff.add_group(cg.group)
|
|
|
|
messages.success(request, _("Group added successfully"))
|
|
return redirect("user_detail",dealer_slug=dealer_slug, slug=staff.slug)
|
|
|
|
form = forms.UserGroupForm(initial={"name": staff.groups})
|
|
form.fields["name"].queryset = models.CustomGroup.objects.filter(
|
|
dealer=staff.dealer
|
|
)
|
|
return render(request, "users/user_group_form.html", {"staff": staff, "form": form})
|
|
|
|
|
|
class UserListView(LoginRequiredMixin, 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_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
staff = models.Staff.objects.filter(dealer=dealer, active=True).all()
|
|
return apply_search_filters(staff, query)
|
|
|
|
|
|
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_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
# 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"]
|
|
if User.objects.filter(email=email).exists():
|
|
messages.error(
|
|
self.request,
|
|
_(
|
|
"A user with this email already exists. Please use a different email."
|
|
),
|
|
)
|
|
return redirect("user_create", dealer_slug=dealer.slug)
|
|
password = "Tenhal@123"
|
|
|
|
user = User.objects.create_user(
|
|
username=email, email=email, password=password
|
|
)
|
|
user.is_staff = True
|
|
user.save()
|
|
|
|
staff_member = StaffMember.objects.create(user=user)
|
|
for service in form.cleaned_data["service_offered"]:
|
|
staff_member.services_offered.add(service)
|
|
staff = form.save(commit=False)
|
|
staff.staff_member = staff_member
|
|
staff.dealer = dealer
|
|
staff.add_as_manager()
|
|
group = models.CustomGroup.objects.filter(dealer=dealer,name__iexact=staff.staff_type).first()
|
|
staff.save()
|
|
if group:
|
|
staff.add_group(group.group)
|
|
return super().form_valid(form)
|
|
def get_success_url(self):
|
|
return reverse_lazy("user_list", args=[self.request.dealer.slug])
|
|
|
|
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)
|
|
def get_success_url(self):
|
|
return reverse_lazy("user_list", args=[self.request.dealer.slug])
|
|
|
|
@login_required
|
|
def UserDeleteview(request, dealer_slug,slug):
|
|
"""
|
|
Deletes a user and its associated staff member from the database and redirects
|
|
to the user list page. Displays a success message upon successful deletion
|
|
of the user.
|
|
|
|
:param request: The HTTP request object representing the incoming request.
|
|
:param pk: The primary key (ID) of the staff member to be deleted.
|
|
:return: An HTTP redirect to the user list page.
|
|
|
|
"""
|
|
get_object_or_404(models.Dealer,slug=dealer_slug)
|
|
staff = get_object_or_404(models.Staff, slug=slug)
|
|
staff.deactivate_account()
|
|
messages.success(request, _("User deleted successfully"))
|
|
return redirect("user_list",dealer_slug=dealer_slug)
|
|
|
|
|
|
class OrganizationListView(LoginRequiredMixin, ListView):
|
|
"""
|
|
Represents a view to display a paginated list of organizations for a dealer.
|
|
|
|
This class inherits from `LoginRequiredMixin` to ensure that only
|
|
authenticated users can access the list, and from `ListView` to provide
|
|
a generic implementation to render lists of database objects. It is designed
|
|
specifically to show organizations related to a dealer entity and includes
|
|
search functionality based on a query parameter.
|
|
|
|
:ivar model: Specifies the model to fetch data from.
|
|
:type model: type[CustomerModel]
|
|
:ivar template_name: The template used to render the organization list page.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable for the organization list.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of organizations displayed per page.
|
|
:type paginate_by: int
|
|
"""
|
|
|
|
model = models.Organization
|
|
template_name = "organizations/organization_list.html"
|
|
context_object_name = "organizations"
|
|
paginate_by = 20
|
|
|
|
def get_queryset(self):
|
|
query = self.request.GET.get("q")
|
|
dealer = get_user_type(self.request)
|
|
organization = dealer.organizations.filter(active=True)
|
|
|
|
return apply_search_filters(organization, query)
|
|
|
|
|
|
class OrganizationDetailView(LoginRequiredMixin, DetailView):
|
|
"""
|
|
Handles displaying detailed information about an organization.
|
|
|
|
This view is a detailed representation of an individual organization, which
|
|
provides the necessary data for templates to render the information. It is
|
|
intended to be used for displaying details of a `CustomerModel` instance.
|
|
Requires the user to be logged in to access this view.
|
|
|
|
:ivar model: Specifies the model that this view will interact with.
|
|
:type model: type[CustomerModel]
|
|
:ivar template_name: The name of the template to be used for rendering the
|
|
organization's detail information.
|
|
:type template_name: str
|
|
:ivar context_object_name: The context variable name to be used in the
|
|
template for accessing the organization's data.
|
|
:type context_object_name: str
|
|
"""
|
|
|
|
model = models.Organization
|
|
template_name = "organizations/organization_detail.html"
|
|
context_object_name = "organization"
|
|
|
|
class OrganizationCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
|
"""
|
|
# Handles the creation of a new organization via a web form. This view allows the
|
|
# authenticated user to submit data for creating an organization. If a POST request
|
|
# is received, it validates the data, checks for duplicate organizations, and
|
|
# creates a customer linked to the organization, including its associated
|
|
# information such as address, phone number, and logo. Upon success, the user
|
|
# is redirected to the organization list, and a success message is displayed.
|
|
|
|
# :param request: The HTTP request object containing data for creating an organization.
|
|
# :type request: HttpRequest
|
|
# :return: An HTTP response object rendering the organization create form page or
|
|
# redirecting the user after a successful creation.
|
|
# :rtype: HttpResponse
|
|
#"""
|
|
|
|
model = models.Organization
|
|
form_class = forms.OrganizationForm
|
|
permission_required = ["django_ledger.add_customermodel"]
|
|
template_name = "organizations/organization_form.html"
|
|
success_url = reverse_lazy("organization_list")
|
|
success_message = "Organization created successfully"
|
|
|
|
def form_valid(self, form):
|
|
if organization := models.Organization.objects.filter(
|
|
email=form.instance.email
|
|
).first():
|
|
if not organization.active:
|
|
messages.error(
|
|
self.request,
|
|
_(
|
|
"Organization Account with this email is Deactivated,Please Contact Admin"
|
|
),
|
|
)
|
|
else:
|
|
messages.error(
|
|
self.request, _("Organization with this email already exists")
|
|
)
|
|
return redirect("organization_create")
|
|
dealer = get_user_type(self.request)
|
|
form.instance.dealer = dealer
|
|
user = form.instance.create_user_model()
|
|
customer = form.instance.create_customer_model()
|
|
form.instance.user = user
|
|
form.instance.customer_model = customer
|
|
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy("organization_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]})
|
|
|
|
|
|
class OrganizationUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
|
"""
|
|
# Handles the update of an organization instance. This view fetches the organization
|
|
# based on the provided primary key (pk) and renders a form for editing the
|
|
# organization attributes. When a POST request is made, this view validates and
|
|
# processes the form data, updates the organization instance, and saves the changes.
|
|
# If the request method is not POST, it initializes the form with existing organization
|
|
# data for rendering.
|
|
|
|
# :param request: The HTTP request object. Must be authenticated via login.
|
|
# :type request: HttpRequest
|
|
# :param pk: The primary key of the organization to be updated.
|
|
# :type pk: int
|
|
# :return: An HTTP response object. Either renders the organization form or redirects
|
|
# to the organization list upon successful update.
|
|
# :rtype: HttpResponse
|
|
#"""
|
|
|
|
model = models.Organization
|
|
form_class = forms.OrganizationForm
|
|
permission_required = ["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)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy("organization_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]})
|
|
|
|
@login_required
|
|
def OrganizationDeleteView(request,dealer_slug, slug):
|
|
"""
|
|
Handles the deletion of an organization based on the provided primary key (pk). Looks up
|
|
the organization and its corresponding user by email, attempts to delete both, and provides
|
|
appropriate success or error feedback to the user. In case of failure, an error message is shown,
|
|
while successful deletion redirects to the organization list.
|
|
|
|
:param request: The HTTP request object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the organization to be deleted.
|
|
:type pk: int
|
|
:return: An HTTP response redirecting to the organization list view.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
organization = get_object_or_404(models.Organization, slug=slug)
|
|
organization.deactivate_account()
|
|
messages.success(request, _("Organization Deactivated successfully"))
|
|
return redirect(reverse_lazy("organization_list", kwargs={"dealer_slug": dealer_slug}))
|
|
|
|
|
|
class RepresentativeListView(LoginRequiredMixin, ListView):
|
|
"""
|
|
Represents a view for displaying a paginated list of representatives.
|
|
|
|
This view handles the functionality of displaying and paginating a list
|
|
of representatives for the logged-in user. It utilizes search filters to
|
|
allow querying representatives based on the search term provided in the
|
|
request. The view restricts access to logged-in users only.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: models.Representative
|
|
:ivar template_name: Name of the template used to render the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: Name of the context variable used to access
|
|
representatives in the template.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of representatives displayed per page.
|
|
:type paginate_by: int
|
|
"""
|
|
|
|
model = models.Representative
|
|
template_name = "representatives/representative_list.html"
|
|
context_object_name = "representatives"
|
|
paginate_by = 10
|
|
|
|
def get_queryset(self):
|
|
query = self.request.GET.get("q")
|
|
dealer = get_user_type(self.request)
|
|
representative = models.Representative.objects.filter(dealer=dealer)
|
|
return apply_search_filters(representative, query)
|
|
|
|
|
|
class RepresentativeDetailView(LoginRequiredMixin, DetailView):
|
|
"""
|
|
Represents a detailed view for Representative instances.
|
|
|
|
This class-based view is used to provide detailed representation for
|
|
``Representative`` model instances. It ensures that only authenticated
|
|
users can access the view by utilizing ``LoginRequiredMixin``. The
|
|
template used to render the view and the context name for the object
|
|
are also specified for use in a Django template.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: Type of the model (models.Representative)
|
|
:ivar template_name: The path to the template used to render this view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable containing
|
|
the object.
|
|
:type context_object_name: str
|
|
"""
|
|
|
|
model = models.Representative
|
|
template_name = "representatives/representative_detail.html"
|
|
context_object_name = "representative"
|
|
|
|
|
|
class RepresentativeCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
|
|
"""
|
|
Handles the creation of a Representative object.
|
|
|
|
This class is a view that provides the interface and functionality to create
|
|
a new representative in the application. It is designed to ensure that only
|
|
authenticated users with a valid dealer association can create representatives.
|
|
A success message is displayed upon successful creation of a representative.
|
|
|
|
:ivar model: The model that this view will work with, which is Representative.
|
|
:type model: django.db.models.Model
|
|
:ivar form_class: The form class used for creating a representative.
|
|
:type form_class: django.forms.ModelForm
|
|
:ivar template_name: The name of the template used to render the create view.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to upon successful form submission.
|
|
:type success_url: str
|
|
:ivar success_message: The success message displayed after creating a representative.
|
|
:type success_message: str
|
|
"""
|
|
|
|
model = models.Representative
|
|
form_class = forms.RepresentativeForm
|
|
template_name = "representatives/representative_form.html"
|
|
success_url = reverse_lazy("representative_list")
|
|
success_message = _("Representative created successfully")
|
|
|
|
def form_valid(self, form):
|
|
if form.is_valid():
|
|
form.instance.dealer = self.request.user.dealer
|
|
form.save()
|
|
return super().form_valid(form)
|
|
else:
|
|
return form.errors
|
|
|
|
|
|
class RepresentativeUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
|
"""
|
|
Provides functionality for updating a representative's details.
|
|
|
|
This class-based view allows authenticated users, equipped with
|
|
the required permissions, to update information for a specific
|
|
representative. It uses a predefined form for input, renders the
|
|
appropriate update template, and provides success messaging upon
|
|
successful completion. It also redirects to a predefined success
|
|
URL once the update operation is complete.
|
|
|
|
:ivar model: The model representing a representative that is being
|
|
updated.
|
|
:type model: Type[models.Representative]
|
|
:ivar form_class: The form class used for providing input fields to
|
|
update representative details.
|
|
:type form_class: Type[forms.RepresentativeForm]
|
|
:ivar template_name: The template used to render the representative
|
|
update page.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to which the user is redirected following
|
|
a successful update.
|
|
:type success_url: str
|
|
:ivar success_message: The message displayed upon a successful update
|
|
operation.
|
|
:type success_message: str
|
|
"""
|
|
|
|
model = models.Representative
|
|
form_class = forms.RepresentativeForm
|
|
template_name = "representatives/representative_form.html"
|
|
success_url = reverse_lazy("representative_list")
|
|
success_message = _("Representative updated successfully")
|
|
|
|
|
|
class RepresentativeDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
|
|
"""
|
|
Handles the deletion of a representative.
|
|
|
|
This view provides functionality to delete a representative from the system.
|
|
It ensures that only authenticated users can perform the deletion and displays
|
|
a success message upon successful deletion. The deletion is confirmed via a
|
|
template, and upon success, the user is redirected to the representative list page.
|
|
|
|
:ivar model: The model representing the representative.
|
|
:type model: models.Representative
|
|
:ivar template_name: The template used to confirm the deletion of a representative.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to after successful deletion.
|
|
:type success_url: str
|
|
:ivar success_message: The success message displayed after a representative is deleted.
|
|
:type success_message: str
|
|
"""
|
|
|
|
model = models.Representative
|
|
template_name = "representatives/representative_confirm_delete.html"
|
|
success_url = reverse_lazy("representative_list")
|
|
success_message = _("Representative deleted successfully")
|
|
|
|
|
|
# BANK ACCOUNT
|
|
class BankAccountListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Provides a view for listing bank accounts associated with a specific entity
|
|
and applying search filters if provided. Ensures user authentication and
|
|
required permissions are implemented.
|
|
|
|
This view is used to display the list of bank accounts in a paginated format.
|
|
It filters the list of bank accounts based on the associated entity of the
|
|
dealer (user type) and applies search filters when needed. It requires the
|
|
user to be logged in and have the specified permissions to view the resource.
|
|
|
|
:ivar model: The model to fetch data for the bank account listing.
|
|
:type model: type[BankAccountModel]
|
|
:ivar template_name: The template used to render the bank account list.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable containing the
|
|
list of bank accounts.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of records displayed per page in pagination.
|
|
:type paginate_by: int
|
|
:ivar permission_required: The required permissions to access the view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
model = BankAccountModel
|
|
template_name = "ledger/bank_accounts/bank_account_list.html"
|
|
context_object_name = "bank_accounts"
|
|
paginate_by = 30
|
|
permission_required = ["inventory.view_carfinance"]
|
|
|
|
def get_queryset(self):
|
|
query = self.request.GET.get("q")
|
|
dealer = get_user_type(self.request)
|
|
bank_accounts = BankAccountModel.objects.filter(entity_model=dealer.entity)
|
|
return apply_search_filters(bank_accounts, query)
|
|
|
|
|
|
class BankAccountCreateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView
|
|
):
|
|
"""
|
|
Represents a view for creating a new bank account.
|
|
|
|
This class is a Django CreateView that handles the creation of a new bank account
|
|
within the system. It integrates functionalities for login requirements, permission
|
|
enforcement, and success messages upon successful creation. The view is initialized
|
|
with a specific model, form class, and template to render the form. A success message
|
|
and redirection URL are specified for after the account creation. Additionally, it
|
|
checks permissions required to access this view.
|
|
|
|
:ivar model: The model to be used for the creation of a bank account.
|
|
:type model: BankAccountModel
|
|
:ivar form_class: The form class to be used for validating and handling bank account
|
|
creation.
|
|
:type form_class: BankAccountCreateForm
|
|
:ivar template_name: The template to render the form for bank account creation.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to after successful bank account creation.
|
|
:type success_url: str
|
|
:ivar success_message: The success message to display upon successful creation.
|
|
:type success_message: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = BankAccountModel
|
|
form_class = BankAccountCreateForm
|
|
template_name = "ledger/bank_accounts/bank_account_form.html"
|
|
success_message = _("Bank account created successfully")
|
|
permission_required = ["inventory.view_carfinance"]
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
form.instance.entity_model = dealer.entity
|
|
return super().form_valid(form)
|
|
|
|
def get_form_kwargs(self):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
entity = dealer.entity
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs["entity_slug"] = entity.slug
|
|
kwargs["user_model"] = entity.admin
|
|
return kwargs
|
|
def get_success_url(self):
|
|
return reverse_lazy("bank_account_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]})
|
|
|
|
class BankAccountDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Manages the detailed view of a bank account.
|
|
|
|
Provides a detailed view for a specific bank account by leveraging Django's
|
|
DetailView. This class ensures that only authenticated users with the relevant
|
|
permissions can access the view. It is tailored for displaying necessary
|
|
details about the bank account.
|
|
|
|
:ivar model: The model associated with the view.
|
|
:type model: type[BankAccountModel]
|
|
:ivar template_name: Path to the HTML template used to render the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable used to represent
|
|
the specific bank account instance in the template.
|
|
:type context_object_name: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
model = BankAccountModel
|
|
template_name = "ledger/bank_accounts/bank_account_detail.html"
|
|
context_object_name = "bank_account"
|
|
permission_required = ["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_message = _("Bank account updated successfully")
|
|
permission_required = ["inventory.view_carfinance"]
|
|
|
|
def get_form_kwargs(self):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
entity = dealer.entity
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs["entity_slug"] = entity.slug # Get entity_slug from URL
|
|
kwargs["user_model"] = entity.admin # Get user_model from the request
|
|
return kwargs
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"bank_account_detail",
|
|
kwargs={"dealer_slug": self.kwargs["dealer_slug"], "pk": self.object.pk},
|
|
)
|
|
|
|
|
|
@login_required
|
|
def bank_account_delete(request, dealer_slug, pk):
|
|
"""
|
|
Delete a bank account entry from the database.
|
|
|
|
This view handles the deletion of a bank account record specified by its
|
|
primary key (pk). It renders a deletion confirmation page and processes the
|
|
deletion if the request method is POST. Upon successful deletion, the user is
|
|
redirected to the list of bank accounts and a success message is displayed.
|
|
|
|
:param request: The HTTP request object representing the client's request.
|
|
It contains data such as request type (GET or POST) and session
|
|
information.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the bank account model instance to be deleted.
|
|
:type pk: int
|
|
:return: Returns an HttpResponse object. This can be an HTTP redirect to the
|
|
bank account list page upon successful deletion, or an HTML response
|
|
rendering the confirmation template if accessed via GET.
|
|
:rtype: HttpResponse
|
|
"""
|
|
get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
bank_account = get_object_or_404(BankAccountModel, pk=pk)
|
|
|
|
bank_account.delete()
|
|
messages.success(request, _("Bank account deleted successfully"))
|
|
return redirect("bank_account_list", dealer_slug=dealer_slug)
|
|
|
|
|
|
# Accounts
|
|
|
|
|
|
class AccountListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
View for displaying a list of accounts.
|
|
|
|
AccountListView is responsible for displaying a paginated list of accounts
|
|
that users can view based on their permissions. This view is restricted
|
|
to users who are logged in and have the required permissions. It also
|
|
provides functionality to filter and search the available accounts.
|
|
|
|
:ivar model: The Django model used for this view. Determines which
|
|
database records are displayed in the account list.
|
|
:type model: type[Model]
|
|
:ivar template_name: Path to the template file used for rendering the
|
|
account list view.
|
|
:type template_name: str
|
|
:ivar context_object_name: Name of the variable containing the accounts
|
|
list passed to the template.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: Number of accounts to display per page.
|
|
:type paginate_by: int
|
|
:ivar permission_required: Permissions required to access this view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
model = AccountModel
|
|
template_name = "ledger/coa_accounts/account_list.html"
|
|
context_object_name = "accounts"
|
|
|
|
permission_required = ["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_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
|
|
|
|
def get_success_url(self):
|
|
return reverse(
|
|
"account_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
|
|
)
|
|
|
|
|
|
class AccountDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Represents the detailed view for an account with additional context data related to account
|
|
transactions and permissions.
|
|
|
|
This class provides a detailed view for an account in the system. It includes functionality
|
|
for generating contextual data, managing permissions, and customizing rendering templates.
|
|
The view calculates total debits, credits, and provides transaction details for the account.
|
|
|
|
:ivar model: The Django model class representing the account data.
|
|
:type model: Type[AccountModel]
|
|
:ivar template_name: The path to the template used to render this view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The context variable name representing the account object.
|
|
:type context_object_name: str
|
|
:ivar slug_field: The field in the model used to retrieve the account instance based on a slug.
|
|
:type slug_field: str
|
|
:ivar DEFAULT_TXS_DAYS: Default number of days to filter transactions.
|
|
:type DEFAULT_TXS_DAYS: int
|
|
:ivar permission_required: Permissions required to access this view.
|
|
:type permission_required: list[str]
|
|
:ivar extra_context: Additional context data passed to the template.
|
|
:type extra_context: dict
|
|
"""
|
|
|
|
model = AccountModel
|
|
template_name = "ledger/coa_accounts/account_detail.html"
|
|
context_object_name = "account"
|
|
slug_field = "uuid"
|
|
DEFAULT_TXS_DAYS = 30
|
|
permission_required = ["inventory.view_carfinance"]
|
|
extra_context = {
|
|
"DEFAULT_TXS_DAYS": DEFAULT_TXS_DAYS,
|
|
"header_subtitle_icon": "ic:round-account-tree",
|
|
}
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
account_model: AccountModel = context["object"]
|
|
context["header_title"] = f"Account {account_model.code} - {account_model.name}"
|
|
context["page_title"] = f"Account {account_model.code} - {account_model.name}"
|
|
context["total_debits"] = sum(
|
|
x.amount for x in account_model.transactionmodel_set.filter(tx_type="debit")
|
|
)
|
|
context["total_credits"] = sum(
|
|
x.amount
|
|
for x in account_model.transactionmodel_set.filter(tx_type="credit")
|
|
)
|
|
|
|
account_model.transactionmodel_set.all().posted().order_by(
|
|
"journal_entry__timestamp"
|
|
).select_related(
|
|
"journal_entry",
|
|
"journal_entry__entity_unit",
|
|
"journal_entry__ledger__billmodel",
|
|
"journal_entry__ledger__invoicemodel",
|
|
)
|
|
return context
|
|
|
|
|
|
class AccountUpdateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
|
):
|
|
"""
|
|
Represents a view for updating an existing account.
|
|
|
|
This class provides functionality to update an account's details using a form.
|
|
The user must be logged in and have the necessary permissions to access this
|
|
view. Upon successful update of the account, a success message is displayed
|
|
and the user is redirected to the account list page.
|
|
|
|
:ivar model: The model associated with this view which represents the account.
|
|
:type model: AccountModel
|
|
:ivar form_class: The form class used for updating account details.
|
|
:type form_class: AccountModelUpdateForm
|
|
:ivar template_name: The path to the template used for rendering the update view.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to upon success.
|
|
:type success_url: str
|
|
:ivar success_message: The success message displayed after updating an account
|
|
successfully.
|
|
:type success_message: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list of str
|
|
"""
|
|
|
|
model = AccountModel
|
|
form_class = AccountModelUpdateForm
|
|
template_name = "ledger/coa_accounts/account_form.html"
|
|
success_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
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"account_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
|
|
)
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.view_carfinance")
|
|
def account_delete(request, dealer_slug, pk):
|
|
"""
|
|
Handles the deletion of an account object identified by its primary key (pk). Ensures
|
|
that the user has the necessary permissions to perform the deletion. Successfully
|
|
deletes the account and redirects to the account list view with a success message.
|
|
|
|
:param request: The HTTP request object representing the current user and request data.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the account to be deleted.
|
|
:type pk: int
|
|
:return: An HTTP redirect response to the account list page.
|
|
:rtype: HttpResponse
|
|
"""
|
|
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
account = get_object_or_404(AccountModel, pk=pk)
|
|
|
|
account.delete()
|
|
messages.success(request, _("Account deleted successfully"))
|
|
return redirect("account_list", dealer_slug=dealer_slug)
|
|
|
|
|
|
# Sales list
|
|
@login_required
|
|
@permission_required("inventory.view_lead", raise_exception=True)
|
|
def sales_list_view(request, dealer_slug):
|
|
"""
|
|
Handles the retrieval and presentation of a paginated list of item transactions for
|
|
sales, specific to the logged-in user's entity. Requires the user to have appropriate
|
|
permissions to view the list.
|
|
|
|
:param request: The HTTP request object containing metadata about the request,
|
|
such as HTTP method, user credentials, and sent data.
|
|
:type request: HttpRequest
|
|
|
|
:return: An HTTP response with the rendered sales list page containing the paginated
|
|
item transactions specific to the user's entity.
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
entity = dealer.entity
|
|
|
|
sale_orders = models.SaleOrder.objects.filter(
|
|
dealer=dealer,
|
|
)
|
|
paginator = Paginator(sale_orders, 30)
|
|
page_number = request.GET.get("page")
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
context = {"txs": page_obj, "page_obj": page_obj}
|
|
return render(request, "sales/sales_list.html", context)
|
|
|
|
|
|
class SaleOrderDetailView(LoginRequiredMixin, DetailView):
|
|
model = models.SaleOrder
|
|
template_name = "sales/saleorder_detail.html"
|
|
context_object_name = "sale_order"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
sale_order = self.get_object()
|
|
|
|
# Add additional context data
|
|
context["status_choices"] = dict(models.SaleOrder.STATUS_CHOICES)
|
|
context["page_title"] = _("Sales Order Details")
|
|
|
|
# Calculate any additional properties you want to display
|
|
context["is_delivered"] = sale_order.status == "DELIVERED"
|
|
context["is_cancelled"] = sale_order.status == "CANCELLED"
|
|
context["is_pending_approval"] = sale_order.status == "PENDING_APPROVAL"
|
|
|
|
return 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, dealer_slug, slug=None):
|
|
"""
|
|
Creates a new estimate based on the provided data and saves it. This function processes
|
|
a POST request and expects a JSON payload containing details of the estimate such as
|
|
title, customer, terms, items, and quantities. It validates the input data, ensures
|
|
availability of stocks, and updates or creates the corresponding estimate in the database.
|
|
|
|
If `pk` is provided, it links the created estimate with an existing opportunity. It handles
|
|
the reservation of cars and updates the stock information accordingly.
|
|
|
|
:param request: The HttpRequest object containing user-specific data and state.
|
|
:type request: HttpRequest
|
|
:param pk: An optional primary key of the existing opportunity to associate with
|
|
the created estimate.
|
|
:type pk: int, optional
|
|
:return: A JsonResponse object with status and either the created quotation URL
|
|
or an error message. If the request method is not POST, it renders the
|
|
estimate creation form.
|
|
:rtype: JsonResponse or HttpResponse
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
entity = dealer.entity
|
|
|
|
if request.method == "POST":
|
|
# try:
|
|
data = json.loads(request.body)
|
|
title = data.get("title")
|
|
customer_id = data.get("customer")
|
|
# terms = data.get("terms")
|
|
# customer = entity.get_customers().filter(pk=customer_id).first()
|
|
customer = models.Customer.objects.filter(pk=int(customer_id)).first()
|
|
|
|
items = data.get("item", [])
|
|
quantities = data.get("quantity", [])
|
|
|
|
if not all([items, quantities]):
|
|
return JsonResponse(
|
|
{"status": "error", "message": _("Items and Quantities are required")},
|
|
status=400,
|
|
)
|
|
|
|
if isinstance(quantities, list):
|
|
if "0" in quantities:
|
|
return JsonResponse(
|
|
{
|
|
"status": "error",
|
|
"message": _("Quantity must be greater than zero"),
|
|
}
|
|
)
|
|
else:
|
|
if int(quantities) <= 0:
|
|
return JsonResponse(
|
|
{
|
|
"status": "error",
|
|
"message": _("Quantity must be greater than zero"),
|
|
}
|
|
)
|
|
if isinstance(items, list):
|
|
for item, quantity in zip(items, quantities):
|
|
if (
|
|
int(quantity)
|
|
> models.Car.objects.filter(hash=item, status="available").count()
|
|
):
|
|
return JsonResponse(
|
|
{
|
|
"status": "error",
|
|
"message": _(
|
|
"Quantity must be less than or equal to the number of cars in stock"
|
|
),
|
|
},
|
|
)
|
|
else:
|
|
if (
|
|
int(quantities)
|
|
> models.Car.objects.filter(hash=items, status="available").count()
|
|
):
|
|
return JsonResponse(
|
|
{
|
|
"status": "error",
|
|
"message": _(
|
|
"Quantity must be less than or equal to the number of cars in stock"
|
|
),
|
|
},
|
|
)
|
|
estimate = entity.create_estimate(
|
|
estimate_title=title,
|
|
customer_model=customer.customer_model,
|
|
contract_terms="fixed",
|
|
)
|
|
if isinstance(items, list):
|
|
item_quantity_map = {}
|
|
for item, quantity in zip(items, quantities):
|
|
if item in item_quantity_map:
|
|
item_quantity_map[item] += int(quantity)
|
|
else:
|
|
item_quantity_map[item] = int(quantity)
|
|
item_list = list(item_quantity_map.keys())
|
|
quantity_list = list(item_quantity_map.values())
|
|
|
|
items_list = [
|
|
{"item_id": item_list[i], "quantity": quantity_list[i]}
|
|
for i in range(len(item_list))
|
|
]
|
|
items_txs = []
|
|
for item in items_list:
|
|
# car_instance = ItemModel.objects.filter(
|
|
# additional_info__car_info__hash=item.get("item_id")
|
|
# ).all()
|
|
# for i in car_instance[: int(quantities[0])]:
|
|
# items_txs.append(
|
|
# {
|
|
# "item_number": i.item_number,
|
|
# "quantity": 1,
|
|
# "unit_cost": i.additional_info.get("car_finance").get(
|
|
# "selling_price"
|
|
# ),
|
|
# "unit_revenue": i.additional_info.get("car_finance").get(
|
|
# "selling_price"
|
|
# ),
|
|
# "total_amount": (
|
|
# i.additional_info.get("car_finance").get("total_vat")
|
|
# ),
|
|
# }
|
|
# )
|
|
car_instance = models.Car.objects.filter(
|
|
hash=item.get("item_id"), finances__is_sold=False
|
|
).all()
|
|
|
|
for i in car_instance[: int(quantities[0])]:
|
|
items_txs.append(
|
|
{
|
|
"item_number": i.item_model.item_number,
|
|
"quantity": 1,
|
|
"unit_cost": round(float(i.finances.selling_price)),
|
|
"unit_revenue": round(float(i.finances.selling_price)),
|
|
"total_amount": round(float(i.finances.total_vat)),
|
|
}
|
|
)
|
|
|
|
estimate_itemtxs = {
|
|
item.get("item_number"): {
|
|
"unit_cost": item.get("unit_cost"),
|
|
"unit_revenue": item.get("unit_revenue"),
|
|
"quantity": item.get("quantity"),
|
|
"total_amount": item.get("total_amount"),
|
|
}
|
|
for item in items_txs
|
|
}
|
|
else:
|
|
item = entity.get_items_all().filter(pk=items).first()
|
|
instance = models.Car.objects.get(vin=item.name)
|
|
estimate_itemtxs = {
|
|
item.item_number: {
|
|
"unit_cost": instance.finances.cost_price,
|
|
"unit_revenue": instance.finances.selling_price,
|
|
"quantity": Decimal(quantities),
|
|
"total_amount": instance.finances.total_vat * int(quantities),
|
|
}
|
|
}
|
|
|
|
try:
|
|
estimate.migrate_itemtxs(
|
|
itemtxs=estimate_itemtxs,
|
|
commit=True,
|
|
operation=EstimateModel.ITEMIZE_APPEND,
|
|
)
|
|
except Exception as e:
|
|
estimate.delete()
|
|
return JsonResponse(
|
|
{
|
|
"status": "error",
|
|
"message": e,
|
|
}
|
|
)
|
|
if isinstance(items, list):
|
|
for item in estimate_itemtxs.keys():
|
|
item_instance = ItemModel.objects.filter(item_number=item).first()
|
|
instance = models.Car.objects.get(vin=item_instance.name)
|
|
reserve_car(instance, request)
|
|
|
|
else:
|
|
item_instance = ItemModel.objects.filter(
|
|
additioinal_info__car_info__hash=items
|
|
).first()
|
|
instance = models.Car.objects.get(hash=item)
|
|
reserve_car(instance, request)
|
|
|
|
opportunity_id = data.get("opportunity_id")
|
|
if opportunity_id != "None":
|
|
opportunity = models.Opportunity.objects.get(slug=opportunity_id)
|
|
opportunity.estimate = estimate
|
|
opportunity.save()
|
|
|
|
url = reverse(
|
|
"estimate_detail", kwargs={"dealer_slug": dealer.slug, "pk": estimate.pk}
|
|
)
|
|
return JsonResponse(
|
|
{
|
|
"status": "success",
|
|
"message": _("Quotation created successfully"),
|
|
"url": f"{url}",
|
|
}
|
|
)
|
|
|
|
form = forms.EstimateModelCreateForm()
|
|
form.fields["customer"].queryset = models.Customer.objects.filter(
|
|
dealer=dealer, active=True
|
|
)
|
|
|
|
if slug:
|
|
opportunity = models.Opportunity.objects.get(slug=slug)
|
|
customer = opportunity.customer
|
|
form.fields["customer"].queryset = models.Customer.objects.filter(
|
|
pk=customer.pk
|
|
)
|
|
form.initial["customer"] = customer
|
|
|
|
car_list = (
|
|
models.Car.objects.filter(
|
|
dealer=dealer,
|
|
colors__isnull=False,
|
|
finances__isnull=False,
|
|
finances__selling_price__gt=0,
|
|
status="available",
|
|
)
|
|
.annotate(
|
|
color=F("colors__exterior__rgb"),
|
|
color_name=F("colors__exterior__arabic_name"),
|
|
)
|
|
.values_list(
|
|
"id_car_make__arabic_name",
|
|
"id_car_model__arabic_name",
|
|
"id_car_serie__arabic_name",
|
|
"id_car_trim__arabic_name",
|
|
"color",
|
|
"color_name",
|
|
"hash",
|
|
)
|
|
.annotate(hash_count=Count("hash"))
|
|
.distinct()
|
|
)
|
|
context = {
|
|
"form": form,
|
|
"items": [
|
|
{
|
|
"make": x[0],
|
|
"model": x[1],
|
|
"serie": x[2],
|
|
"trim": x[3],
|
|
"color": x[4],
|
|
"color_name": x[5],
|
|
"hash": x[6],
|
|
"hash_count": x[7],
|
|
}
|
|
for x in car_list
|
|
],
|
|
"opportunity_id": slug if slug else None,
|
|
"customer_count": entity.get_customers().count(),
|
|
}
|
|
|
|
return render(request, "sales/estimates/estimate_form.html", context)
|
|
|
|
|
|
class EstimateDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Represents the detailed view for an EstimateModel instance.
|
|
|
|
This class provides functionality to display detailed information about
|
|
a specific EstimateModel instance. It ensures the user is authenticated
|
|
and has the required permissions to access the estimate details. The class
|
|
also integrates additional financial and invoice-related data into the
|
|
context for more comprehensive display and functionality.
|
|
|
|
:ivar model: Specifies the model associated with the view.
|
|
:type model: ModelBase
|
|
:ivar template_name: Path to the template used for rendering the detailed view.
|
|
:type template_name: str
|
|
:ivar context_object_name: Name used to refer to the EstimateModel instance in the context.
|
|
:type context_object_name: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = EstimateModel
|
|
template_name = "sales/estimates/estimate_detail.html"
|
|
context_object_name = "estimate"
|
|
permission_required = ["django_ledger.view_estimatemodel"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
estimate = kwargs.get("object")
|
|
if estimate.get_itemtxs_data():
|
|
calculator = CarFinanceCalculator(estimate)
|
|
finance_data = calculator.get_finance_data()
|
|
invoice_obj = InvoiceModel.objects.all().filter(ce_model=estimate).first()
|
|
kwargs["data"] = finance_data
|
|
kwargs["invoice"] = invoice_obj
|
|
return super().get_context_data(**kwargs)
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.add_saleorder", raise_exception=True)
|
|
def create_sale_order(request, dealer_slug, pk):
|
|
"""
|
|
Creates a sale order for a given estimate and updates associated item and car data.
|
|
|
|
This view is responsible for handling the submission of a sale order form linked to
|
|
a specific estimate. It ensures that the estimate is approved if not already, updates
|
|
the status of the related car items as sold, and redirects to the estimate's detailed
|
|
view upon successful creation of the sale order. If the request method is not POST, it
|
|
renders the form for the user to input sale order details, along with other contextual
|
|
information like estimate data and car finance details.
|
|
|
|
:param request: HTTP request object.
|
|
:type request: HttpRequest
|
|
:param pk: Primary key of the estimate to create a sale order for.
|
|
:type pk: int
|
|
:return: An HTTP response rendering the sale order form if the method is GET or invalid
|
|
POST data, or redirects to the estimate detail view upon successful creation.
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
estimate = get_object_or_404(EstimateModel, pk=pk)
|
|
items = estimate.get_itemtxs_data()[0].all()
|
|
if request.method == "POST":
|
|
form = forms.SaleOrderForm(request.POST)
|
|
if form.is_valid():
|
|
instance = form.save(commit=False)
|
|
instance.dealer = dealer
|
|
instance.estimate = estimate
|
|
instance.customer = estimate.customer.customer_set.first()
|
|
instance.created_by = request.user
|
|
instance.last_modified_by = request.user
|
|
instance.save()
|
|
|
|
if not estimate.is_approved():
|
|
estimate.mark_as_approved()
|
|
estimate.save()
|
|
for item in estimate.get_itemtxs_data()[0].all():
|
|
try:
|
|
item.item_model.additional_info["car_info"]["status"] = "sold"
|
|
item.item_model.save()
|
|
except KeyError:
|
|
pass
|
|
|
|
item.item_model.car.mark_as_sold()
|
|
messages.success(request, "Sale Order created successfully")
|
|
|
|
else:
|
|
print(form.errors)
|
|
messages.error(request, "Invalid form data")
|
|
return redirect("estimate_detail", dealer_slug=dealer_slug, pk=estimate.pk)
|
|
|
|
form = forms.SaleOrderForm()
|
|
customer = estimate.customer.customer_set.first()
|
|
form.fields["estimate"].queryset = EstimateModel.objects.filter(pk=pk)
|
|
form.initial["estimate"] = estimate
|
|
form.fields["customer"].queryset = models.Customer.objects.filter(pk=customer.pk)
|
|
form.initial["customer"] = customer
|
|
if hasattr(estimate, "opportunity"):
|
|
form.initial["opportunity"] = estimate.opportunity
|
|
else:
|
|
form.fields["opportunity"].widget = HiddenInput()
|
|
|
|
calculator = CarFinanceCalculator(estimate)
|
|
finance_data = calculator.get_finance_data()
|
|
return render(
|
|
request,
|
|
"sales/estimates/sale_order_form1.html",
|
|
{"form": form, "estimate": estimate, "items": items, "data": finance_data},
|
|
)
|
|
|
|
|
|
class SaleOrderDetail(DetailView):
|
|
model = models.SaleOrder
|
|
template_name = "sales/orders/order_details.html"
|
|
context_object_name = "saleorder"
|
|
|
|
def get_object(self, queryset=None):
|
|
order_pk = self.kwargs.get("order_pk")
|
|
return models.SaleOrder.objects.get(
|
|
pk=order_pk,
|
|
)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
saleorder = kwargs.get("object")
|
|
estimate = saleorder.estimate
|
|
if estimate.get_itemtxs_data():
|
|
calculator = CarFinanceCalculator(estimate)
|
|
finance_data = calculator.get_finance_data()
|
|
kwargs["data"] = finance_data
|
|
return super().get_context_data(**kwargs)
|
|
|
|
|
|
@login_required
|
|
def preview_sale_order(request, dealer_slug, pk):
|
|
"""
|
|
Handles rendering of the sale order preview page for a specific estimate.
|
|
|
|
This view retrieves an `EstimateModel` object based on the provided primary
|
|
key (`pk`), fetches related car finance data, and renders the preview of the
|
|
sale order associated with the given estimate.
|
|
|
|
:param request: The HTTP request object
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the `EstimateModel` to retrieve
|
|
:type pk: int
|
|
:return: HTTP response containing the rendered sale order preview page
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
estimate = get_object_or_404(EstimateModel, pk=pk)
|
|
data = get_car_finance_data(estimate)
|
|
return render(
|
|
request,
|
|
"sales/estimates/sale_order_preview.html",
|
|
{"order": estimate.sale_orders.first(), "data": data, "estimate": estimate},
|
|
)
|
|
|
|
|
|
class PaymentRequest(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Represents a detailed view of a payment request for an estimate.
|
|
|
|
This class is a Django DetailView that leverages mixins for login
|
|
and permission requirements. It displays details of an estimate
|
|
and fetches related car data based on the estimate's items.
|
|
It ensures only authorized users can access the payment request details.
|
|
|
|
:ivar model: The Django model associated with this view. It is used
|
|
to fetch and display detailed information for an estimate.
|
|
:type model: EstimateModel
|
|
:ivar template_name: The template utilized to render the detailed
|
|
payment request page for estimates.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context object to be
|
|
accessible in the template for the detailed view.
|
|
:type context_object_name: str
|
|
:ivar permission_required: Permissions required for accessing
|
|
this view. The user must have the specified permissions.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = EstimateModel
|
|
template_name = "sales/estimates/payment_request_detail.html"
|
|
context_object_name = "estimate"
|
|
permission_required = ["django_ledger.view_invoicemodel"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["cars"] = [
|
|
models.Car.objects.get(vin=car.item_model.name)
|
|
for car in context["estimate"].get_itemtxs_data()[0].all()
|
|
]
|
|
|
|
return context
|
|
|
|
|
|
class EstimatePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Represents a view for previewing an estimate with user-specific permissions and
|
|
context data processing. This class provides functionality to render a detailed
|
|
view of an estimate while ensuring user authentication and permission verification.
|
|
|
|
This view is primarily used in a sales module to preview the detailed financial
|
|
breakdown of an estimate, including tax, discount, additional services, and total
|
|
amount.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: Type[models.Model]
|
|
:ivar context_object_name: The name of the context variable containing the object.
|
|
:type context_object_name: str
|
|
:ivar template_name: The path to the template used for rendering this view.
|
|
:type template_name: str
|
|
:ivar permission_required: List of permissions required to access this view.
|
|
:type permission_required: List[str]
|
|
"""
|
|
|
|
model = EstimateModel
|
|
context_object_name = "estimate"
|
|
template_name = "sales/estimates/estimate_preview.html"
|
|
permission_required = ["django_ledger.view_estimatemodel"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
estimate = kwargs.get("object")
|
|
if estimate.get_itemtxs_data():
|
|
# data = get_financial_values(estimate)
|
|
calculator = CarFinanceCalculator(estimate)
|
|
kwargs["data"] = calculator.get_finance_data()
|
|
|
|
return super().get_context_data(**kwargs)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.change_estimatemodel", raise_exception=True)
|
|
def estimate_mark_as(request, dealer_slug, pk):
|
|
"""
|
|
Marks an estimate with a specified status based on the requested action and
|
|
permissions. The marking possibilities include review, approval, rejection,
|
|
completion, and cancellation. The function validates whether the estimate
|
|
can transition to the desired status before updating it. It also handles
|
|
notifications and updates related entities if required, such as car status
|
|
changes upon cancellation.
|
|
|
|
:param request: The HTTP request object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the estimate to be marked.
|
|
:type pk: int
|
|
:return: A redirect response to the estimate detail view.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
estimate = get_object_or_404(EstimateModel, pk=pk)
|
|
mark = request.GET.get("mark")
|
|
if mark:
|
|
if mark == "review":
|
|
if not estimate.can_review():
|
|
messages.error(request, _("Quotation is not ready for review"))
|
|
return redirect(
|
|
"estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk
|
|
)
|
|
estimate.mark_as_review()
|
|
|
|
elif mark == "approved":
|
|
if not estimate.can_approve():
|
|
messages.error(request, _("Quotation is not ready for approval"))
|
|
return redirect(
|
|
"estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk
|
|
)
|
|
estimate.mark_as_approved()
|
|
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", dealer_slug=dealer.slug, pk=estimate.pk
|
|
)
|
|
estimate.mark_as_canceled()
|
|
messages.success(request, _("Quotation canceled successfully"))
|
|
elif mark == "completed":
|
|
if not estimate.can_complete():
|
|
messages.error(request, _("Quotation is not ready for completion"))
|
|
return redirect(
|
|
"estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk
|
|
)
|
|
elif mark == "canceled":
|
|
if not estimate.can_cancel():
|
|
messages.error(request, _("Quotation is not ready for cancellation"))
|
|
return redirect(
|
|
"estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk
|
|
)
|
|
estimate.mark_as_canceled()
|
|
try:
|
|
car = models.Car.objects.get(
|
|
vin=estimate.get_itemtxs_data()[0].first().item_model.name
|
|
)
|
|
car.status = "available"
|
|
car.save()
|
|
except Exception:
|
|
pass
|
|
messages.success(request, _("Quotation canceled successfully"))
|
|
estimate.save()
|
|
messages.success(request, _("Quotation marked as ") + mark.upper())
|
|
|
|
return redirect("estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk)
|
|
|
|
|
|
# Invoice
|
|
class InvoiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Handles the display and management of a list of invoices.
|
|
|
|
This class-based view provides functionality for displaying a paginated list of invoices
|
|
while ensuring that only authenticated and authorized users can access the view. It allows
|
|
for applying search filters to the displayed invoices, based on user inputs. The view is
|
|
designed to work as part of the Django framework, and utilizes models, templates, and
|
|
permissions specific to the application.
|
|
|
|
:ivar model: The model representing invoices.
|
|
:type model: type
|
|
:ivar template_name: Path to the template used to render the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: Name used to reference the list of invoices in the template.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of invoices to display per page.
|
|
:type paginate_by: int
|
|
:ivar permission_required: List of permissions required for accessing this view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = InvoiceModel
|
|
template_name = "sales/invoices/invoice_list.html"
|
|
context_object_name = "invoices"
|
|
paginate_by = 20
|
|
permission_required = ["django_ledger.view_invoicemodel"]
|
|
|
|
def get_queryset(self):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
query = self.request.GET.get("q")
|
|
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_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
kwargs["entity_slug"] = dealer.entity
|
|
kwargs["user_model"] = dealer.entity.admin
|
|
return kwargs
|
|
|
|
|
|
class ApprovedInvoiceModelUpdateFormView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
|
):
|
|
"""
|
|
Handles the view for updating approved invoice models.
|
|
|
|
This class-based view is used to update the details of an approved invoice. It
|
|
inherits from ``LoginRequiredMixin``, ``PermissionRequiredMixin``, and
|
|
``UpdateView`` to ensure secured access to the functionality and integrates
|
|
with Django's permission system. Users must have the required permission to
|
|
access this view. It utilizes a custom update form and is configured with
|
|
specific success URLs.
|
|
|
|
:ivar model: The model associated with this view, which is ``InvoiceModel``.
|
|
:type model: type
|
|
:ivar form_class: The form class used for handling updates, which is
|
|
``ApprovedInvoiceModelUpdateForm``.
|
|
:type form_class: type
|
|
:ivar template_name: The path to the template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar success_url: URL to redirect upon successful operation. This uses
|
|
``reverse_lazy`` to point to the invoice list by default.
|
|
:type success_url: django.urls.reverse_lazy
|
|
:ivar permission_required: The permission required to access this view. It is
|
|
set to ``django_ledger.view_invoicemodel`` by default.
|
|
:type permission_required: list of str
|
|
"""
|
|
|
|
model = InvoiceModel
|
|
form_class = ApprovedInvoiceModelUpdateForm
|
|
template_name = "sales/invoices/approved_invoice_update.html"
|
|
success_url = reverse_lazy("invoice_list")
|
|
permission_required = ["django_ledger.view_invoicemodel"]
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
kwargs["entity_slug"] = dealer.entity
|
|
kwargs["user_model"] = dealer.entity.admin
|
|
return kwargs
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"invoice_detail",
|
|
kwargs={"dealer_slug": self.kwargs["dealer_slug"], "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_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
kwargs["entity_slug"] = dealer.entity
|
|
kwargs["user_model"] = dealer.entity.admin
|
|
return kwargs
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"invoice_detail",
|
|
kwargs={"dealer_slug": self.kwargs["dealer_slug"], "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, dealer_slug, pk):
|
|
"""
|
|
Marks an invoice as approved if it meets the required conditions.
|
|
|
|
This view is responsible for marking an invoice as approved based on the provided
|
|
`mark` parameter. If the `mark` parameter is specified as "accept" and the invoice
|
|
is eligible for approval, it gets approved and saved. Otherwise, an error message
|
|
is displayed. The function requires the user to be logged in and to have the
|
|
appropriate permission to change the InvoiceModel.
|
|
|
|
:param request: The HTTP request object containing metadata about the request.
|
|
:type request: django.http.HttpRequest
|
|
:param pk: The primary key of the invoice to be processed.
|
|
:type pk: int
|
|
:return: An HTTP redirect response to the invoice detail page after processing.
|
|
:rtype: django.http.HttpResponse
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
invoice = get_object_or_404(InvoiceModel, pk=pk)
|
|
mark = request.GET.get("mark")
|
|
if mark and mark == "accept":
|
|
if not invoice.can_approve():
|
|
messages.error(request, "invoice is not ready for approval")
|
|
return redirect("invoice_detail", dealer_slug=dealer_slug, pk=invoice.pk)
|
|
invoice.mark_as_approved(
|
|
entity_slug=dealer.entity.slug, user_model=dealer.entity.admin
|
|
)
|
|
invoice.save()
|
|
return redirect("invoice_detail", dealer_slug=dealer_slug, pk=invoice.pk)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.add_invoicemodel", raise_exception=True)
|
|
def invoice_create(request, dealer_slug, pk):
|
|
"""
|
|
Handles the creation of a new invoice associated with a given estimate. It validates
|
|
the submitted data through a form, processes the invoice, updates related models, and
|
|
finalizes the estimate. If successful, redirects to the detailed view of the created
|
|
invoice. If the submitted data is invalid or the request is not a POST request, renders
|
|
the invoice creation form.
|
|
|
|
:param request: The HTTP request object.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the estimate associated with the invoice.
|
|
:type pk: int
|
|
:return: An HTTP response. Redirects to the "invoice_detail" view upon successful invoice
|
|
creation or renders the invoice creation form template otherwise.
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
estimate = get_object_or_404(EstimateModel, pk=pk)
|
|
entity = dealer.entity
|
|
|
|
if request.method == "POST":
|
|
form = forms.InvoiceModelCreateForm(
|
|
request.POST, entity_slug=entity.slug, user_model=entity.admin
|
|
)
|
|
if form.is_valid():
|
|
invoice = form.save(commit=False)
|
|
ledger = entity.create_ledger(name=str(invoice.pk))
|
|
invoice.ledgar = ledger
|
|
ledger.invoicemodel = invoice
|
|
ledger.save()
|
|
invoice.save()
|
|
|
|
calculator = CarFinanceCalculator(estimate)
|
|
finance_data = calculator.get_finance_data()
|
|
|
|
invoice_itemtxs = {
|
|
i.get("item_number"): {
|
|
"unit_cost": i.get("total_vat"),
|
|
"quantity": i.get("quantity"),
|
|
"total_amount": i.get("total_vat"),
|
|
}
|
|
for i in finance_data.get("cars")
|
|
}
|
|
|
|
invoice_itemtxs = invoice.migrate_itemtxs(
|
|
itemtxs=invoice_itemtxs,
|
|
commit=True,
|
|
operation=InvoiceModel.ITEMIZE_APPEND,
|
|
)
|
|
sale_order = estimate.sale_orders.first()
|
|
sale_order.invoice = invoice
|
|
invoice.bind_estimate(estimate)
|
|
invoice.mark_as_review()
|
|
estimate.mark_as_completed()
|
|
sale_order.save()
|
|
estimate.save()
|
|
invoice.save()
|
|
messages.success(request, "Invoice created successfully")
|
|
return redirect("invoice_detail", dealer_slug=dealer.slug, pk=invoice.pk)
|
|
else:
|
|
print(form.errors)
|
|
form = forms.InvoiceModelCreateForm(
|
|
entity_slug=entity.slug, user_model=entity.admin
|
|
)
|
|
form.initial.update(
|
|
{
|
|
"customer": estimate.customer,
|
|
"cash_account": dealer.settings.invoice_cash_account,
|
|
"prepaid_account": dealer.settings.invoice_prepaid_account,
|
|
"unearned_account": dealer.settings.invoice_unearned_account,
|
|
}
|
|
)
|
|
|
|
context = {
|
|
"form": form,
|
|
"estimate": estimate,
|
|
}
|
|
return render(request, "sales/invoices/invoice_create.html", context)
|
|
|
|
|
|
class InvoicePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Represents a detailed view for previewing an invoice.
|
|
|
|
This class provides a mechanism to render a preview of an invoice for authorized
|
|
users. It utilizes Django's class-based views by extending `DetailView` and includes
|
|
necessary mixins for login and permission checks. The purpose of this class is to ensure
|
|
secured access to the invoice preview while generating additional context data needed
|
|
for rendering finance-related information.
|
|
|
|
:ivar model: The Django model class this view will represent.
|
|
:type model: InvoiceModel
|
|
:ivar context_object_name: Name of the context object used in the template.
|
|
:type context_object_name: str
|
|
:ivar template_name: Path to the template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = InvoiceModel
|
|
context_object_name = "invoice"
|
|
template_name = "sales/invoices/invoice_preview.html"
|
|
permission_required = ["django_ledger.view_invoicemodel"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
invoice = kwargs.get("object")
|
|
if invoice.get_itemtxs_data():
|
|
calculator = CarFinanceCalculator(invoice)
|
|
finance_data = 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, dealer_slug, pk):
|
|
"""
|
|
Handles the creation of a payment entry associated with an invoice or bill. Validates
|
|
the payment data against the model's current state and reflects the changes in
|
|
invoice or bill records. Provides appropriate error messages for invalid conditions
|
|
such as exceeding payable amounts or attempting payment for already fully paid models.
|
|
|
|
If successfully processed, the payment details are saved, and the model is updated
|
|
accordingly. This view regulates payment for dealer-associated entities while
|
|
ensuring the model consistency.
|
|
|
|
The view renders a form to submit payment details, and pre-populates the form fields
|
|
with default data for the associated model if necessary.
|
|
|
|
:param request: The HTTP request object containing user request data and session
|
|
information. This is required to handle the request and apply the appropriate
|
|
processing rules.
|
|
:param pk: The primary key of the invoice or bill being processed. It is used to
|
|
load the appropriate model instance for payment processing.
|
|
:return: An HTTP response object. Depending on the circumstances, the response may
|
|
redirect to the detail view of the processed invoice or bill, re-render the
|
|
payment form with error messages or indicate success in payment creation.
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
invoice = InvoiceModel.objects.filter(pk=pk).first()
|
|
bill = BillModel.objects.filter(pk=pk).first()
|
|
model = invoice if invoice else bill
|
|
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, dealer_slug=dealer.slug, pk=model.pk)
|
|
if model.amount_paid + amount > model.amount_due:
|
|
messages.error(request, _("Amount exceeds due amount"))
|
|
return redirect(redirect_url, dealer_slug=dealer.slug, 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, dealer_slug=dealer.slug, 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, dealer_slug):
|
|
"""
|
|
Handles the view for listing payment information associated with the journals of a specific
|
|
entity. This view is protected to ensure only authenticated and authorized users can
|
|
access it.
|
|
|
|
The function retrieves the related dealer object based on the current user session, extracts
|
|
the associated entity, and fetches all journal entries linked to the entity. This data is
|
|
then passed into the template for rendering.
|
|
|
|
:param request: The HTTP request object containing user context.
|
|
:type request: HttpRequest
|
|
|
|
:return: The rendered HTML response displaying the list of payments.
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
entity = dealer.entity
|
|
|
|
journals = JournalEntryModel.objects.filter(ledger__entity=entity).all()
|
|
|
|
paginator = Paginator(journals, 30)
|
|
page_number = request.GET.get("page")
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
return render(request, "sales/payments/payment_list.html", {"page_obj": page_obj})
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.view_journalentrymodel", raise_exception=True)
|
|
def PaymentDetailView(request, dealer_slug, pk):
|
|
"""
|
|
This function handles the detail view for a payment by fetching a journal entry
|
|
and its associated transactions. It ensures that the request is authenticated
|
|
and the user has permission to view the journal entry model.
|
|
|
|
:param request: The HTTP request object.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the journal entry for which details are to be fetched.
|
|
:type pk: int
|
|
:return: An HTTP response rendering the payment details template with the journal
|
|
entry and its associated transactions.
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
journal = JournalEntryModel.objects.filter(pk=pk).first()
|
|
transactions = (
|
|
TransactionModel.objects.filter(journal_entry=journal)
|
|
.order_by("account__code")
|
|
.all()
|
|
)
|
|
return render(
|
|
request,
|
|
"sales/payments/payment_details.html",
|
|
{"journal": journal, "transactions": transactions},
|
|
)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.change_journalentrymodel", raise_exception=True)
|
|
def payment_mark_as_paid(request, dealer_slug, pk):
|
|
"""
|
|
Marks an invoice as paid if it meets the conditions of being fully paid and eligible
|
|
for payment. Also ensures that related ledger journal entries are locked and posted
|
|
when the payment is marked successfully.
|
|
|
|
This function is protected with both `login_required` and
|
|
`permission_required` decorators, ensuring that only logged-in users with
|
|
appropriate permissions can execute it.
|
|
|
|
:param request: HttpRequest object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: Primary key of the invoice to mark as paid.
|
|
:type pk: int
|
|
:return: Redirect response to the invoice detail page.
|
|
:rtype: HttpResponseRedirect
|
|
:raises: In case of an exception during the process, an error message is
|
|
displayed to the user through Django's messaging framework.
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
invoice = get_object_or_404(InvoiceModel, pk=pk)
|
|
if request.method == "POST":
|
|
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", dealer_slug=dealer_slug, pk=invoice.pk)
|
|
|
|
|
|
# activity log
|
|
class UserActivityLogListView(LoginRequiredMixin, ListView):
|
|
"""
|
|
Represents a view to display a paginated list of user activity logs.
|
|
|
|
This class is used to display a list of user activity logs in a paginated
|
|
manner. It retrieves the logs from the `UserActivityLog` model and allows
|
|
basic filtering of logs by user email through the URL query parameters.
|
|
The view requires the user to be authenticated and utilizes the
|
|
`LoginRequiredMixin` to enforce this.
|
|
|
|
:ivar model: The model associated with the view.
|
|
:type model: models.UserActivityLog
|
|
:ivar template_name: The template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable representing
|
|
the list of logs.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of logs displayed per page.
|
|
:type paginate_by: int
|
|
"""
|
|
|
|
model = models.UserActivityLog
|
|
template_name = "dealers/activity_log.html"
|
|
context_object_name = "logs"
|
|
paginate_by = 20
|
|
|
|
def get_queryset(self):
|
|
queryset = super().get_queryset()
|
|
if "user" in self.request.GET:
|
|
queryset = queryset.filter(user__email=self.request.GET["user"])
|
|
return queryset[:100] # will update later with better pagination
|
|
|
|
|
|
def lead_view(request):
|
|
return render(request, "crm/leads/lead_view.html")
|
|
|
|
|
|
# CRM RELATED VIEWS
|
|
class LeadListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Represents a view to display a paginated list of leads, ensuring user permissions
|
|
and type-specific filtering.
|
|
|
|
This class is used for rendering a list of leads associated with a logged-in user
|
|
based on their type (dealer or staff member). It combines login and permission
|
|
controls with pagination to ensure proper access and management of leads
|
|
in a CRM context.
|
|
|
|
:ivar model: The model to be used for retrieving leads.
|
|
:type model: type[Lead]
|
|
:ivar template_name: Path to the template for rendering the lead list.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable to contain the leads.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: Number of items to display per page.
|
|
:type paginate_by: int
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
model = models.Lead
|
|
template_name = "crm/leads/lead_list.html"
|
|
context_object_name = "leads"
|
|
paginate_by = 30
|
|
permission_required = ["inventory.view_lead"]
|
|
|
|
def get_queryset(self):
|
|
query = self.request.GET.get("q")
|
|
dealer = get_user_type(self.request)
|
|
qs = models.Lead.objects.filter(dealer=dealer).exclude(status="converted")
|
|
if query:
|
|
qs = apply_search_filters(qs, query)
|
|
if self.request.is_dealer:
|
|
return qs
|
|
staffmember = getattr(self.request.user, "staffmember", None)
|
|
if staff := getattr(staffmember, "staff", None):
|
|
return qs.filter(staff=staff)
|
|
return models.Lead.objects.none()
|
|
|
|
|
|
class LeadDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
View that provides detailed information about a specific lead.
|
|
|
|
This class-based view is designed for displaying detailed information associated with a
|
|
particular lead, including related notes, emails, activities, status history, and a lead
|
|
transfer form. It combines multiple mixins to enforce login and permission requirements
|
|
for accessing the data. This view is tailored to the CRM module for managing leads.
|
|
|
|
:ivar model: The model associated with this view, which is the Lead model.
|
|
:type model: models.Lead
|
|
:ivar template_name: Path to the template used to render detailed lead information.
|
|
:type template_name: str
|
|
:ivar permission_required: List of permissions required to access this view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = models.Lead
|
|
template_name = "crm/leads/lead_detail.html"
|
|
permission_required = ["inventory.view_lead"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["notes"] = models.Notes.objects.filter(
|
|
content_type__model="lead", object_id=self.object.id
|
|
)
|
|
email_qs = models.Email.objects.filter(
|
|
content_type__model="lead", object_id=self.object.id
|
|
)
|
|
context["emails"] = {
|
|
"sent": email_qs.filter(status="SENT"),
|
|
"draft": email_qs.filter(status="DRAFT"),
|
|
}
|
|
context["activities"] = models.Activity.objects.filter(
|
|
content_type__model="lead", object_id=self.object.id
|
|
)
|
|
context["tasks"] = models.Tasks.objects.filter(
|
|
content_type__model="lead", object_id=self.object.id
|
|
)
|
|
context["status_history"] = models.LeadStatusHistory.objects.filter(
|
|
lead=self.object
|
|
)
|
|
context["transfer_form"] = forms.LeadTransferForm()
|
|
context["transfer_form"].fields[
|
|
"transfer_to"
|
|
].queryset = models.Staff.objects.filter(
|
|
dealer=self.object.dealer, staff_type="sales"
|
|
)
|
|
context["activity_form"] = forms.ActivityForm()
|
|
context["staff_task_form"] = forms.StaffTaskForm()
|
|
context["note_form"] = forms.NoteForm()
|
|
return context
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.add_lead", raise_exception=True)
|
|
def lead_create(request,dealer_slug):
|
|
"""
|
|
Handles the creation of a new lead in the inventory system.
|
|
|
|
This function manages the rendering and processing of a form for creating a new
|
|
lead. It filters options for car models in the form based on the selected car
|
|
make. For POST requests, it validates and processes the submitted form data
|
|
and creates the lead if valid. It also creates a corresponding customer in the
|
|
ledger system if one does not exist for the provided email.
|
|
|
|
:param request: The HTTP request object containing request data.
|
|
:type request: HttpRequest
|
|
|
|
:return: An HTTP response object rendering the lead creation form or redirecting to the lead list page upon success.
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer,slug=dealer_slug)
|
|
|
|
if request.method == "POST":
|
|
form = forms.LeadForm(request.POST)
|
|
|
|
# Filter car models based on the selected make (for POST requests)
|
|
if "id_car_make" in request.POST:
|
|
form.fields["id_car_model"].queryset = models.CarModel.objects.filter(
|
|
id_car_make=int(request.POST["id_car_make"])
|
|
)
|
|
try:
|
|
if form.is_valid():
|
|
instance = form.save(commit=False)
|
|
instance.dealer = dealer
|
|
instance.staff = form.cleaned_data.get("staff")
|
|
|
|
if instance.lead_type == "customer":
|
|
customer = models.Customer.objects.filter(
|
|
email=instance.email
|
|
).first()
|
|
if not customer:
|
|
customer = models.Customer(
|
|
dealer=dealer,
|
|
address=instance.address,
|
|
phone_number=instance.phone_number,
|
|
email=instance.email,
|
|
first_name=instance.first_name,
|
|
last_name=instance.last_name,
|
|
active=False,
|
|
)
|
|
customer.create_user_model(for_lead=True)
|
|
customer.create_customer_model(for_lead=True)
|
|
customer.save()
|
|
instance.customer = customer
|
|
|
|
if instance.lead_type == "organization":
|
|
organization = models.Organization.objects.filter(
|
|
email=instance.email
|
|
).first()
|
|
if not organization:
|
|
organization = models.Organization(
|
|
dealer=dealer,
|
|
address=instance.address,
|
|
phone_number=instance.phone_number,
|
|
email=instance.email,
|
|
name=instance.first_name + " " + instance.last_name,
|
|
active=False,
|
|
)
|
|
organization.create_user_model(for_lead=True)
|
|
organization.create_customer_model(for_lead=True)
|
|
organization.save()
|
|
instance.organization = organization
|
|
instance.next_action = LeadStatus.NEW
|
|
instance.save()
|
|
messages.success(request, _("Lead created successfully"))
|
|
return redirect("lead_list", dealer_slug=dealer.slug)
|
|
else:
|
|
messages.error(
|
|
request, f"Lead was not created ... : {str(form.errors)}"
|
|
)
|
|
# if email:= request.POST.get("email"):
|
|
# if models.Customer.objects.filter(email=email).exists():
|
|
# form.errors['email'] = form.error_class([
|
|
# _("Email already exists")
|
|
# ])
|
|
return render(request, "crm/leads/lead_form.html", {"form": form})
|
|
|
|
except Exception as e:
|
|
messages.error(request, f"Lead was not created ... : {str(e)}")
|
|
|
|
form = forms.LeadForm()
|
|
form.fields["staff"].queryset = form.fields["staff"].queryset.filter(active=True)
|
|
form.filter_qs(dealer=dealer)
|
|
|
|
if make := request.GET.get("id_car_make", None):
|
|
form.fields["id_car_model"].queryset = models.CarModel.objects.filter(
|
|
id_car_make=int(make)
|
|
)
|
|
else:
|
|
dealer_make_list = models.DealersMake.objects.filter(dealer=dealer).values_list(
|
|
"car_make", flat=True
|
|
)
|
|
qs = form.fields["id_car_make"].queryset.filter(
|
|
is_sa_import=True, pk__in=dealer_make_list
|
|
)
|
|
form.fields["staff"].queryset = form.fields["staff"].queryset.filter(
|
|
dealer=dealer, staff_type="sales"
|
|
)
|
|
|
|
if hasattr(request.user.staffmember, "staff"):
|
|
form.initial["staff"] = request.user.staffmember.staff
|
|
form.fields["staff"].widget.attrs["disabled"] = True
|
|
form.fields["id_car_make"].queryset = qs
|
|
form.fields["id_car_make"].choices = [
|
|
(obj.id_car_make, obj.get_local_name()) for obj in qs
|
|
]
|
|
if first_make := qs.first():
|
|
form.fields["id_car_model"].queryset = first_make.carmodel_set.all()
|
|
|
|
return render(request, "crm/leads/lead_form.html", {"form": form})
|
|
|
|
|
|
def lead_tracking(request,dealer_slug):
|
|
dealer = get_object_or_404(models.Dealer,slug=dealer_slug)
|
|
new = models.Lead.objects.filter(dealer=dealer, status="new")
|
|
follow_up = models.Lead.objects.filter(
|
|
dealer=dealer, next_action__in=["call", "meeting"]
|
|
)
|
|
won = models.Lead.objects.filter(dealer=dealer, status="won")
|
|
lose = models.Lead.objects.filter(dealer=dealer, status="lost")
|
|
negotiation = models.Lead.objects.filter(dealer=dealer, status="negotiation")
|
|
context = {
|
|
"new": new,
|
|
"follow_up": follow_up,
|
|
"won": won,
|
|
"lose": lose,
|
|
"negotiation": negotiation,
|
|
}
|
|
return render(request, "crm/leads/lead_tracking.html", context)
|
|
|
|
|
|
# @require_POST
|
|
def update_lead_actions(request,dealer_slug):
|
|
try:
|
|
lead_id = request.POST.get("lead_id")
|
|
current_action = request.POST.get("current_action")
|
|
next_action = request.POST.get("next_action")
|
|
next_action_date = request.POST.get("next_action_date", None)
|
|
|
|
if not all([lead_id, current_action, next_action]):
|
|
return JsonResponse(
|
|
{"success": False, "message": "All fields are required"}, status=400
|
|
)
|
|
|
|
# Get the lead
|
|
lead = models.Lead.objects.get(id=lead_id)
|
|
|
|
# Update lead fields
|
|
|
|
lead.status = current_action
|
|
lead.next_action = next_action
|
|
|
|
# Parse the datetime string
|
|
try:
|
|
if next_action_date:
|
|
lead.next_action_date = next_action_date
|
|
next_action_datetime = datetime.strptime(
|
|
next_action_date, "%Y-%m-%dT%H:%M"
|
|
)
|
|
lead.next_action_date = timezone.make_aware(next_action_datetime)
|
|
except ValueError:
|
|
return JsonResponse(
|
|
{"success": False, "message": "Invalid date format"}, status=400
|
|
)
|
|
|
|
# Save the lead
|
|
lead.save()
|
|
|
|
return JsonResponse(
|
|
{"success": True, "message": "Actions updated successfully"}
|
|
)
|
|
|
|
except models.Lead.DoesNotExist:
|
|
return JsonResponse({"success": False, "message": "Lead not found"}, status=404)
|
|
except Exception as e:
|
|
return JsonResponse({"success": False, "message": str(e)}, status=500)
|
|
|
|
|
|
class LeadUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
|
"""
|
|
Handles the update view for Lead objects.
|
|
|
|
This class is used to manage the process of updating Lead objects within the
|
|
application. It provides integration with Django's authentication and
|
|
permissions systems, ensuring that only authorized users can access this view.
|
|
Additionally, it customizes the behavior of the form to dynamically filter
|
|
available options for the 'id_car_model' field based on the related
|
|
'id_car_make' field.
|
|
|
|
:ivar model: Model class representing a Lead object.
|
|
:type model: models.Lead
|
|
:ivar form_class: Form class used for the Lead update form.
|
|
:type form_class: forms.LeadForm
|
|
:ivar template_name: Path to the template used for rendering the Lead update
|
|
form.
|
|
:type template_name: str
|
|
:ivar success_url: URL to redirect to upon successful Lead update.
|
|
:type success_url: str
|
|
:ivar permission_required: List of permissions required for accessing this view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = models.Lead
|
|
form_class = forms.LeadForm
|
|
template_name = "crm/leads/lead_form.html"
|
|
success_url = reverse_lazy("lead_list")
|
|
permission_required = ["inventory.change_lead"]
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
form.fields[
|
|
"id_car_model"
|
|
].queryset = form.instance.id_car_make.carmodel_set.all()
|
|
return form
|
|
def get_success_url(self):
|
|
return reverse_lazy("lead_list",kwargs={"dealer_slug":self.kwargs.get("dealer_slug")})
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.delete_lead", raise_exception=True)
|
|
def LeadDeleteView(request,dealer_slug, slug):
|
|
"""
|
|
Handles the deletion of a Lead along with its associated customer and potentially
|
|
a related user account in the system. Ensures proper permissions and login
|
|
are required before the operation is performed. Provides a success message
|
|
after the deletion is complete.
|
|
|
|
:param request: The HTTP request object specific to the current user.
|
|
:param pk: The primary key identifier of the Lead to be deleted.
|
|
:return: An HTTP redirect response to the lead list page.
|
|
"""
|
|
lead = get_object_or_404(models.Lead, slug=slug)
|
|
try:
|
|
User.objects.get(email=lead.customer.email).delete()
|
|
lead.customer.delete()
|
|
except Exception as e:
|
|
print(e)
|
|
lead.delete()
|
|
messages.success(request, _("Lead deleted successfully"))
|
|
return redirect("lead_list",dealer_slug=dealer_slug)
|
|
|
|
|
|
@login_required
|
|
def add_note_to_lead(request,dealer_slug, slug):
|
|
"""
|
|
Adds a note to a specific lead. This view is accessible only to authenticated
|
|
users. The function handles the POST request to create a new note associated
|
|
with a lead. Upon successful submission, the note is linked to the provided lead,
|
|
and the user is redirected to the lead's detail page. If the request method is
|
|
not POST, it initializes an empty form to add a note.
|
|
|
|
:param request: The HTTP request object
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the lead to which the note will be added
|
|
:type pk: int
|
|
:return: HTTP response object. Redirects to the lead detail page on successful
|
|
note creation or renders the note form template for GET or invalid POST requests.
|
|
:rtype: HttpResponse
|
|
"""
|
|
lead = get_object_or_404(models.Lead, slug=slug)
|
|
if request.method == "POST":
|
|
form = forms.NoteForm(request.POST)
|
|
if form.is_valid():
|
|
note = form.save(commit=False)
|
|
note.content_object = lead
|
|
note.created_by = request.user
|
|
note.save()
|
|
messages.success(request, _("Note added successfully"))
|
|
return redirect("lead_detail",dealer_slug=dealer_slug, slug=lead.slug)
|
|
else:
|
|
form = forms.NoteForm()
|
|
return render(request, "crm/note_form.html", {"form": form, "lead": lead})
|
|
|
|
|
|
@login_required
|
|
def add_note_to_opportunity(request,dealer_slug, slug):
|
|
"""
|
|
Add a note to a specific opportunity identified by its primary key.
|
|
|
|
This function handles the addition of a note to an existing opportunity
|
|
by processing a POST request that includes the note content. If the note
|
|
content is missing, an error message will be displayed. If the operation
|
|
is successful, the note is saved, and a success message is returned.
|
|
|
|
:param request: The HTTP request object representing the client's request.
|
|
:param pk: The primary key of the Opportunity to which the note is to be added.
|
|
:type pk: int
|
|
:return: A redirect response to the detailed view of the opportunity.
|
|
"""
|
|
opportunity = get_object_or_404(models.Opportunity, slug=slug)
|
|
dealer = get_object_or_404(models.Dealer,slug=dealer_slug)
|
|
if request.method == "POST":
|
|
notes = request.POST.get("notes")
|
|
if not notes:
|
|
messages.error(request, _("Notes field is required"))
|
|
else:
|
|
models.Notes.objects.create(
|
|
dealer=dealer,
|
|
content_object=opportunity,
|
|
created_by=request.user,
|
|
note=notes,
|
|
)
|
|
messages.success(request, _("Note added successfully"))
|
|
return redirect("opportunity_detail", dealer_slug=dealer_slug,slug=opportunity.slug)
|
|
|
|
|
|
@login_required
|
|
def delete_note(request,dealer_slug, pk):
|
|
"""
|
|
Deletes a specific note created by the currently logged-in user and redirects
|
|
to the lead detail page. If the note does not exist or the user is not the creator,
|
|
a 404 error will be raised. A success message is displayed upon successful deletion
|
|
of the note.
|
|
|
|
:param request: The HTTP request object associated with the current request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the note to be deleted.
|
|
:type pk: int
|
|
:return: An HTTP redirection to the lead detail page of the corresponding note's lead.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
note = get_object_or_404(models.Notes, pk=pk, created_by=request.user)
|
|
lead_pk = note.content_object.pk
|
|
lead = models.Lead.objects.get(pk=lead_pk)
|
|
note.delete()
|
|
messages.success(request, _("Note deleted successfully."))
|
|
return redirect("lead_detail", dealer_slug=dealer_slug,slug=lead.slug)
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.change_lead", raise_exception=True)
|
|
def lead_convert(request,dealer_slug, slug):
|
|
"""
|
|
Converts a lead into a customer and creates a corresponding opportunity.
|
|
|
|
The function ensures that leads are not converted to customers more than once.
|
|
If the lead has already been converted, an error is displayed. Otherwise,
|
|
the lead is converted into a customer and a new opportunity is created.
|
|
Upon successful conversion, the user is redirected to the lead list view
|
|
and a success message is displayed.
|
|
|
|
:param request: The HTTP request object representing the user's request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the lead to be converted.
|
|
:type pk: int
|
|
:return: An HTTP response redirecting to the lead list view.
|
|
:rtype: HttpResponse
|
|
"""
|
|
lead = get_object_or_404(models.Lead, slug=slug)
|
|
dealer = get_object_or_404(models.Dealer,slug=dealer_slug)
|
|
if hasattr(lead, "opportunity"):
|
|
messages.error(request, _("Lead is already converted to customer"))
|
|
else:
|
|
customer = lead.convert_to_customer()
|
|
models.Opportunity.objects.create(
|
|
dealer=dealer,
|
|
customer=customer,
|
|
lead=lead,
|
|
probability=50,
|
|
stage=models.Stage.NEGOTIATION,
|
|
staff=lead.staff,
|
|
)
|
|
messages.success(request, _("Lead converted to customer successfully"))
|
|
return redirect("lead_list",dealer_slug=dealer_slug)
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.add_lead", raise_exception=True)
|
|
def schedule_lead(request, dealer_slug,slug):
|
|
"""
|
|
Handles the scheduling of a lead for an appointment.
|
|
|
|
This function ensures that only staff members with the appropriate permissions
|
|
can schedule leads. If the user is not a staff member or does not have the
|
|
required inventory permissions, they are redirected with an appropriate error
|
|
message. The function creates an appointment request and an associated appointment
|
|
record upon successful submission of the scheduling form.
|
|
|
|
:param request: The HTTP request object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the lead to be scheduled.
|
|
:type pk: int
|
|
:return: A rendered template or a redirection to another view based on the request
|
|
method and validity of the form submission.
|
|
:rtype: HttpResponse
|
|
"""
|
|
|
|
if not request.is_staff:
|
|
messages.error(request, _("You do not have permission to schedule lead"))
|
|
return redirect("lead_list", dealer_slug=dealer_slug)
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
lead = get_object_or_404(models.Lead, slug=slug, dealer=dealer)
|
|
if request.method == "POST":
|
|
form = forms.ScheduleForm(request.POST)
|
|
if form.is_valid():
|
|
instance = form.save(commit=False)
|
|
instance.lead = lead
|
|
instance.scheduled_by = request.user
|
|
|
|
instance.customer = lead.get_customer_model()
|
|
|
|
service = Service.objects.get(name=instance.scheduled_type)
|
|
|
|
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", dealer_slug=lead.dealer.slug, slug=lead.slug)
|
|
|
|
client = get_object_or_404(User, email=lead.email)
|
|
# Create Appointment
|
|
Appointment.objects.create(
|
|
client=client,
|
|
appointment_request=appointment_request,
|
|
phone=lead.phone_number,
|
|
address=lead.address,
|
|
)
|
|
|
|
instance.save()
|
|
messages.success(request, _("Appointment Created Successfully"))
|
|
try:
|
|
if lead.opportunity:
|
|
return redirect("opportunity_detail", dealer_slug=lead.dealer.slug, slug=lead.opportunity.slug)
|
|
except models.Lead.opportunity.RelatedObjectDoesNotExist:
|
|
return redirect("lead_list", dealer_slug=lead.dealer.slug)
|
|
else:
|
|
messages.error(request, f"Invalid form data: {str(form.errors)}")
|
|
return redirect("schedule_lead", dealer_slug=dealer_slug,slug=lead.slug)
|
|
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,dealer_slug, slug):
|
|
"""
|
|
Handles the transfer of a lead to a different staff member. This view is accessible
|
|
only to authenticated users with the 'inventory.change_lead' permission. If the
|
|
request method is POST and the form data is valid, the lead's staff is updated
|
|
accordingly, saved, and a success message is displayed. Otherwise, an error message
|
|
is shown. In both cases, the user is redirected to the lead listing page.
|
|
|
|
:param request: The HTTP request object.
|
|
:param pk: The primary key of the lead to be transferred.
|
|
:return: An HTTP redirect response to the lead list view.
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
lead = get_object_or_404(models.Lead, slug=slug)
|
|
if request.method == "POST":
|
|
form = forms.LeadTransferForm(request.POST)
|
|
if form.is_valid():
|
|
lead.staff = form.cleaned_data["transfer_to"]
|
|
lead.save()
|
|
models.Notification.objects.create(
|
|
user=lead.staff.user,
|
|
message=f"You have been assigned a new lead: {lead.full_name}.",
|
|
)
|
|
messages.success(request, _("Lead transferred successfully"))
|
|
else:
|
|
messages.error(request, f"Invalid form data: {str(form.errors)}")
|
|
return redirect("lead_list", dealer_slug=dealer.slug)
|
|
|
|
|
|
@login_required
|
|
def send_lead_email(request,dealer_slug, slug, email_pk=None):
|
|
"""
|
|
Handles sending emails related to a lead. Supports creating drafts, sending emails, and rendering
|
|
an email composition page. Changes on the lead or email objects, such as marking a lead as contacted
|
|
or an email as sent, are recorded in associated activity logs.
|
|
|
|
:param request: The HTTP request object. This contains user information, method type, and data such as
|
|
GET or POST payload used to draft or send the email appropriately.
|
|
Type: HttpRequest
|
|
:param pk: The primary key of the lead to which the email action is associated. It's used to retrieve
|
|
the lead object from the database.
|
|
Type: int
|
|
:param email_pk: Optional parameter representing the primary key of an email template. When provided,
|
|
the respective email content is pre-populated into the email composition form.
|
|
Defaults to None.
|
|
Type: Optional[int]
|
|
:return: When successfully sending an email, redirects the user to the lead list page. On draft mode
|
|
or email composition rendering, a response object is returned to render the respective page.
|
|
Type: HttpResponse
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
lead = get_object_or_404(models.Lead, slug=slug)
|
|
status = request.GET.get("status")
|
|
if status == "draft":
|
|
models.Email.objects.create(
|
|
content_object=lead,
|
|
created_by=request.user,
|
|
from_email="manager@tenhal.com",
|
|
to_email=request.GET.get("to"),
|
|
subject=request.GET.get("subject"),
|
|
message=request.GET.get("message"),
|
|
status=models.EmailStatus.DRAFT,
|
|
)
|
|
models.Activity.objects.create(
|
|
dealer=dealer,
|
|
content_object=lead,
|
|
notes="Email Draft",
|
|
created_by=request.user,
|
|
activity_type=models.ActionChoices.EMAIL,
|
|
)
|
|
messages.success(request, _("Email Draft successfully"))
|
|
try:
|
|
if lead.opportunity:
|
|
response = HttpResponse(
|
|
redirect("opportunity_detail", dealer_slug=dealer_slug,slug=lead.opportunity.slug)
|
|
)
|
|
response["HX-Redirect"] = reverse(
|
|
"opportunity_detail", args=[lead.opportunity.slug]
|
|
)
|
|
else:
|
|
response = HttpResponse(redirect("lead_detail", dealer_slug=dealer_slug,slug=lead.slug))
|
|
response["HX-Redirect"] = reverse("lead_detail", dealer_slug=dealer_slug,slug=lead.slug)
|
|
return response
|
|
except models.Lead.opportunity.RelatedObjectDoesNotExist:
|
|
return redirect("lead_list",dealer_slug=dealer.slug)
|
|
|
|
if request.method == "POST":
|
|
email_pk = request.POST.get("email_pk")
|
|
if email_pk not in [None, "None", ""]:
|
|
email = get_object_or_404(models.Email, pk=int(email_pk))
|
|
email.status = models.EmailStatus.SENT
|
|
email.save()
|
|
else:
|
|
models.Email.objects.create(
|
|
content_object=lead,
|
|
created_by=request.user,
|
|
from_email="manager@tenhal.com",
|
|
to_email=request.POST.get("to"),
|
|
subject=request.POST.get("subject"),
|
|
message=request.POST.get("message"),
|
|
status=models.EmailStatus.SENT,
|
|
)
|
|
send_email(
|
|
"manager@tenhal.com",
|
|
request.POST.get("to"),
|
|
request.POST.get("subject"),
|
|
request.POST.get("message"),
|
|
)
|
|
models.Activity.objects.create(
|
|
dealer=dealer,
|
|
content_object=lead,
|
|
notes="Email sent",
|
|
created_by=request.user,
|
|
activity_type=models.ActionChoices.EMAIL,
|
|
)
|
|
messages.success(request, _("Email sent successfully"))
|
|
try:
|
|
if lead.opportunity:
|
|
return redirect("opportunity_detail", dealer_slug=dealer_slug,slug=lead.opportunity.slug)
|
|
except models.Lead.opportunity.RelatedObjectDoesNotExist:
|
|
return redirect("lead_list",dealer_slug=dealer_slug)
|
|
msg = f"""
|
|
السلام عليكم
|
|
Dear {lead.full_name},
|
|
|
|
أود أن أشارككم تقدير المشروع الذي ناقشناه. يرجى العثور على الوثيقة التفصيلية للمقترح المرفقة.
|
|
|
|
I hope this email finds you well. I wanted to share with you the estimate for the project we discussed. Please find the detailed estimate document attached.
|
|
|
|
يرجى مراجعة المقترح وإعلامي إذا كانت لديك أي أسئلة أو مخاوف. إذا كانت كل شيء يبدو جيدًا، يمكننا المضي قدمًا في المشروع.
|
|
|
|
Please review the estimate and let me know if you have any questions or concerns. If everything looks good, we can proceed with the project.
|
|
|
|
شكراً لاهتمامكم بهذا الأمر.
|
|
Thank you for your attention to this matter.
|
|
|
|
تحياتي,
|
|
Best regards,
|
|
[Your Name]
|
|
[Your Position]
|
|
[Your Company]
|
|
[Your Contact Information]
|
|
"""
|
|
subject = ""
|
|
if email_pk:
|
|
email = get_object_or_404(models.Email, pk=email_pk)
|
|
msg = email.message
|
|
subject = email.subject
|
|
return render(
|
|
request,
|
|
"crm/leads/lead_send.html",
|
|
{"lead": lead, "message": msg, "subject": subject, "email_pk": email_pk},
|
|
)
|
|
|
|
|
|
@login_required
|
|
def add_activity_to_lead(request, pk):
|
|
"""
|
|
Handles the process of adding a new activity to a specific lead. This includes
|
|
rendering a form for user input, validating the form submission, and saving the
|
|
new activity if the input is valid. If the method is GET, it will simply
|
|
render the form. If the method is POST, it checks the form validity, creates
|
|
the activity, associates it with the lead, and saves it to the database.
|
|
|
|
:param request: The HTTP request object containing metadata about the request made
|
|
:param pk: The primary key of the lead to which the activity is to be added
|
|
:return: An HTTP response that either renders the form or redirects to the lead detail page
|
|
"""
|
|
lead = get_object_or_404(models.Lead, pk=pk)
|
|
dealer = get_user_type(request)
|
|
if request.method == "POST":
|
|
form = forms.ActivityForm(request.POST)
|
|
if form.is_valid():
|
|
activity = form.save(commit=False)
|
|
print(activity)
|
|
activity.content_object = lead
|
|
activity.dealer = dealer
|
|
activity.activity_type = form.cleaned_data["activity_type"]
|
|
activity.notes = form.cleaned_data["notes"]
|
|
activity.created_by = request.user
|
|
activity.save()
|
|
return redirect("lead_detail", pk=pk)
|
|
else:
|
|
form = forms.ActivityForm()
|
|
return render(request, "crm/add_activity.html", {"form": form, "lead": lead})
|
|
|
|
|
|
class OpportunityCreateView(CreateView, SuccessMessageMixin, LoginRequiredMixin):
|
|
"""
|
|
Handles the creation of Opportunity instances through a form while enforcing
|
|
specific user access control and initial data population. This view ensures
|
|
an authenticated user can create opportunities tied to their dealer type and
|
|
allows pre-filling data when linked to an existing Lead.
|
|
|
|
Allows authenticated users to create a new Opportunity instance, associating it
|
|
with the requesting user's dealer type and prefilling some fields based on
|
|
existing Lead data, if applicable.
|
|
|
|
:ivar model: The database model associated with this view, which is used to
|
|
represent Opportunity data.
|
|
:type model: models.Opportunity
|
|
:ivar form_class: The form class used for creating new Opportunity instances.
|
|
:type form_class: forms.OpportunityForm
|
|
:ivar template_name: The template used to render the Opportunity creation form.
|
|
:type template_name: str
|
|
"""
|
|
|
|
model = models.Opportunity
|
|
form_class = forms.OpportunityForm
|
|
template_name = "crm/opportunities/opportunity_form.html"
|
|
success_message = "Opportunity created successfully."
|
|
|
|
def get_initial(self):
|
|
initial = super().get_initial()
|
|
dealer = get_object_or_404(models.Dealer,slug=self.kwargs.get("dealer_slug"))
|
|
if self.kwargs.get("slug", None):
|
|
lead = models.Lead.objects.get(slug=self.kwargs.get("slug"), dealer=dealer)
|
|
initial["lead"] = lead
|
|
initial["stage"] = models.Stage.QUALIFICATION
|
|
return initial
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_object_or_404(models.Dealer,slug=self.kwargs.get("dealer_slug"))
|
|
instance = form.save(commit=False)
|
|
instance.dealer = dealer
|
|
instance.staff = instance.lead.staff
|
|
instance.save()
|
|
instance.lead.convert_to_customer()
|
|
instance.lead.save()
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy("opportunity_detail", kwargs={"dealer_slug":self.kwargs.get("dealer_slug"),"slug": self.object.slug})
|
|
|
|
|
|
class OpportunityUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
|
"""
|
|
Handles the update functionality for Opportunity objects.
|
|
|
|
This class-based view is responsible for handling the update of existing
|
|
Opportunity instances. It uses a Django form that is specified by the
|
|
`form_class` attribute and renders a template to display and process the
|
|
update form. Access to this view is restricted to authenticated users, as
|
|
it inherits from `LoginRequiredMixin`.
|
|
|
|
It defines the model to be updated and the form template to be used. Upon
|
|
successful update, it redirects the user to the detail page of the updated
|
|
opportunity instance.
|
|
|
|
:ivar model: The model associated with this view. Represents the Opportunity model.
|
|
:type model: django.db.models.Model
|
|
:ivar form_class: The form class used to manage the Opportunity update process.
|
|
:type form_class: django.forms.ModelForm
|
|
:ivar template_name: The path to the template used to render the opportunity
|
|
update form.
|
|
:type template_name: str
|
|
"""
|
|
|
|
model = models.Opportunity
|
|
form_class = forms.OpportunityForm
|
|
template_name = "crm/opportunities/opportunity_form.html"
|
|
success_message = "Opportunity updated successfully."
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy("opportunity_detail", kwargs={"dealer_slug":self.kwargs.get("dealer_slug"),"slug": self.object.slug})
|
|
|
|
|
|
class OpportunityDetailView(LoginRequiredMixin, DetailView):
|
|
"""
|
|
Handles the detailed view of an Opportunity object.
|
|
|
|
Displays detailed information about a specific opportunity, including its
|
|
status, stage, notes, activities, and related emails. This view utilizes
|
|
a form to manage and update the status and stage of the opportunity
|
|
through HTTPX requests. Suitable for use in CRM applications.
|
|
|
|
:ivar model: The model for which this view is being implemented.
|
|
:type model: models.Opportunity
|
|
:ivar template_name: The template used to render this view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The context variable name for the model object in the template.
|
|
:type context_object_name: str
|
|
"""
|
|
|
|
model = models.Opportunity
|
|
template_name = "crm/opportunities/opportunity_detail.html"
|
|
context_object_name = "opportunity"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
form = forms.OpportunityStatusForm()
|
|
url = reverse("opportunity_update_status", kwargs={"dealer_slug": self.kwargs["dealer_slug"], "slug": self.object.slug})
|
|
form.fields["status"].widget.attrs["hx-get"] = url
|
|
form.fields["stage"].widget.attrs["hx-get"] = url
|
|
form.fields["stage"].initial = self.object.stage
|
|
context["status_form"] = form
|
|
|
|
context["lead_notes"] = models.Notes.objects.filter(
|
|
content_type__model="lead", object_id=self.object.id
|
|
).order_by("-created")
|
|
context["lead_notes"] = models.Notes.objects.filter(
|
|
content_type__model="lead", object_id=self.object.lead.id
|
|
).order_by("-created")
|
|
context["notes"] = models.Notes.objects.filter(
|
|
content_type__model="opportunity", object_id=self.object.id
|
|
).order_by("-created")
|
|
context["lead_activities"] = models.Activity.objects.filter(
|
|
content_type__model="lead", object_id=self.object.lead.id
|
|
)
|
|
context["activities"] = models.Activity.objects.filter(
|
|
content_type__model="opportunity", object_id=self.object.id
|
|
)
|
|
lead_email_qs = models.Email.objects.filter(
|
|
content_type__model="lead", object_id=self.object.lead.id
|
|
)
|
|
email_qs = models.Email.objects.filter(
|
|
content_type__model="opportunity", object_id=self.object.id
|
|
)
|
|
context["emails"] = {
|
|
"sent": email_qs.filter(status="SENT"),
|
|
"draft": email_qs.filter(status="DRAFT"),
|
|
}
|
|
context["lead_emails"] = {
|
|
"sent": lead_email_qs.filter(status="SENT"),
|
|
"draft": lead_email_qs.filter(status="DRAFT"),
|
|
}
|
|
context["staff_task_form"] = forms.StaffTaskForm()
|
|
context["lead_tasks"] = models.Tasks.objects.filter(
|
|
content_type__model="lead", object_id=self.object.lead.id
|
|
)
|
|
context["tasks"] = models.Tasks.objects.filter(
|
|
content_type__model="opportunity", object_id=self.object.id
|
|
)
|
|
context["upcoming_events"] = {
|
|
"schedules": self.object.lead.get_all_schedules().filter(
|
|
scheduled_at__gt=timezone.now()
|
|
),
|
|
}
|
|
return context
|
|
|
|
|
|
class OpportunityListView(LoginRequiredMixin, ListView):
|
|
model = models.Opportunity
|
|
template_name = "crm/opportunities/opportunity_list.html"
|
|
context_object_name = "opportunities"
|
|
paginate_by = 30
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
queryset = models.Opportunity.objects.filter(dealer=dealer)
|
|
|
|
# Search filter
|
|
search = self.request.GET.get("search")
|
|
if search:
|
|
queryset = queryset.filter(
|
|
Q(customer__customer_name__icontains=search)
|
|
| Q(customer__email__icontains=search)
|
|
)
|
|
|
|
# Stage filter
|
|
stage = self.request.GET.get("stage")
|
|
if stage:
|
|
queryset = queryset.filter(stage=stage)
|
|
|
|
# Sorting
|
|
sort = self.request.GET.get("sort", "newest")
|
|
if sort == "newest":
|
|
queryset = queryset.order_by("-created")
|
|
elif sort == "highest":
|
|
queryset = queryset.order_by("-expected_revenue")
|
|
elif sort == "closing":
|
|
queryset = queryset.order_by("closing_date")
|
|
|
|
return queryset
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["stage_choices"] = models.Stage.choices
|
|
return context
|
|
|
|
def get_template_names(self):
|
|
return self.template_name
|
|
|
|
|
|
@login_required
|
|
def delete_opportunity(request,dealer_slug, pk):
|
|
"""
|
|
Deletes an opportunity object from the database and redirects to the opportunity
|
|
list view. If the opportunity with the specified primary key is not found, a 404
|
|
error will be raised. Displays a success message after deletion.
|
|
|
|
:param request: The HTTP request object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the opportunity object to be deleted.
|
|
:type pk: int
|
|
:return: An HTTP response redirecting to the opportunity list view.
|
|
:rtype: HttpResponse
|
|
"""
|
|
get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
opportunity = get_object_or_404(models.Opportunity, pk=pk)
|
|
opportunity.delete()
|
|
messages.success(request, _("Opportunity deleted successfully"))
|
|
return redirect("opportunity_list",dealer_slug=dealer_slug)
|
|
|
|
|
|
@login_required
|
|
def opportunity_update_status(request,dealer_slug, slug):
|
|
"""
|
|
Update the status and/or stage of a specific Opportunity instance. This is a
|
|
view function, which is generally tied to a URL endpoint in a Django application.
|
|
The function is accessible only to authenticated users due to the
|
|
`@login_required` decorator.
|
|
|
|
The function retrieves the `Opportunity` instance based on the primary key
|
|
in the URL, updates the status or stage of the instance based on query
|
|
parameters in the request, saves the changes, and then redirects to the
|
|
detail view of the `Opportunity`. Additionally, a success message is displayed,
|
|
and a custom header (`HX-Refresh`) is added to the response.
|
|
|
|
:param request: The HTTP request object containing details of the user's
|
|
request, such as query parameters for status and stage. This is a
|
|
mandatory parameter provided by Django's view function framework.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the `Opportunity` instance to be updated.
|
|
:type pk: int
|
|
|
|
:return: An HTTP response object that redirects to the updated Opportunity's
|
|
detail page, with a success message and the "HX-Refresh" header to trigger
|
|
frontend behavior.
|
|
:rtype: HttpResponse
|
|
"""
|
|
opportunity = get_object_or_404(models.Opportunity, slug=slug)
|
|
status = request.GET.get("status")
|
|
stage = request.GET.get("stage")
|
|
if status:
|
|
opportunity.status = status
|
|
if stage:
|
|
opportunity.stage = stage
|
|
opportunity.save()
|
|
messages.success(request, _("Opportunity status updated successfully"))
|
|
response = HttpResponse(redirect("opportunity_detail",dealer_slug=dealer_slug, slug=opportunity.slug))
|
|
response["HX-Refresh"] = "true"
|
|
return response
|
|
|
|
|
|
class NotificationListView(LoginRequiredMixin, ListView):
|
|
"""
|
|
Handles the display and management of notification history for a user.
|
|
|
|
This class provides functionality to display a paginated list of user notifications
|
|
sorted by their creation time in descending order. It ensures that the user is logged
|
|
in before accessing the notifications and filters the displayed notifications specific
|
|
to the logged-in user.
|
|
|
|
:ivar model: Specifies the model to retrieve data from.
|
|
:type model: models.Notification
|
|
:ivar template_name: Specifies the template to render the notifications page.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the variable accessible in the template to
|
|
refer to the list of notifications.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: Defines the number of notifications to display per page.
|
|
:type paginate_by: int
|
|
:ivar ordering: Defines the default ordering of notifications.
|
|
:type ordering: str
|
|
"""
|
|
|
|
model = models.Notification
|
|
template_name = "crm/notifications_history.html"
|
|
context_object_name = "notifications"
|
|
paginate_by = 20
|
|
ordering = "-created"
|
|
|
|
def get_queryset(self):
|
|
return models.Notification.objects.filter(user=self.request.user)
|
|
|
|
|
|
# @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_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)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy("item_service_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]})
|
|
|
|
class ItemServiceUpdateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
|
):
|
|
"""
|
|
Handles update functionality for additional services in the system.
|
|
|
|
This view enables authenticated and authorized users to update instances of
|
|
the `AdditionalServices` model. It requires the user to have the necessary
|
|
permissions and provides success feedback upon successful update. Additionally,
|
|
it calculates and updates the price of the service based on the tax rate if
|
|
the service is taxable.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: models.AdditionalServices
|
|
:ivar form_class: The form class used for handling AdditionalService updates.
|
|
:type form_class: forms.AdditionalServiceForm
|
|
:ivar template_name: The template used for rendering the service update page.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect after a successful update.
|
|
:type success_url: str
|
|
:ivar success_message: The success message displayed after a successful update.
|
|
:type success_message: str
|
|
:ivar context_object_name: The context variable name for the object to be displayed.
|
|
:type context_object_name: str
|
|
:ivar permission_required: Permissions required for accessing this view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
model = models.AdditionalServices
|
|
form_class = forms.AdditionalServiceForm
|
|
template_name = "items/service/service_create.html"
|
|
success_message = _("Service updated successfully")
|
|
context_object_name = "service"
|
|
permission_required = ["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)
|
|
def get_success_url(self):
|
|
return reverse_lazy("item_service_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]})
|
|
class ItemServiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Handles the listing of additional services for a dealer.
|
|
|
|
This class is responsible for displaying a paginated list of additional
|
|
services specific to a dealer. It enforces login, permission checking,
|
|
and customizes the queryset to return only services associated with the
|
|
currently logged-in dealer.
|
|
|
|
:ivar model: The model representing the additional services data.
|
|
:type model: Model
|
|
:ivar template_name: Path to the template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable for the
|
|
object list.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: Number of items displayed per page.
|
|
:type paginate_by: int
|
|
:ivar permission_required: List of permissions required to access the
|
|
view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = models.AdditionalServices
|
|
template_name = "items/service/service_list.html"
|
|
context_object_name = "services"
|
|
paginate_by = 30
|
|
permission_required = ["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)
|
|
def get_success_url(self):
|
|
return reverse_lazy("item_expense_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]})
|
|
|
|
class ItemExpenseUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
|
"""
|
|
Manages the update view for item expenses.
|
|
|
|
This class enables authenticated users with the necessary permissions to update
|
|
information related to item expenses. It integrates form handling and permission
|
|
requirements, ensuring entity-specific contextual data is appended to the form
|
|
during updates.
|
|
|
|
:ivar model: The model associated with the update operation.
|
|
:type model: ItemModel
|
|
:ivar form_class: The form used to validate and process updates.
|
|
:type form_class: ExpenseItemUpdateForm
|
|
:ivar template_name: Path to the template used for rendering the update view.
|
|
:type template_name: str
|
|
:ivar success_url: URL where the user is redirected after a successful update.
|
|
:type success_url: str
|
|
:ivar permission_required: List of permissions required to access this view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
model = ItemModel
|
|
form_class = ExpenseItemUpdateForm
|
|
template_name = "items/expenses/expense_update.html"
|
|
success_url = reverse_lazy("item_expense_list")
|
|
permission_required = ["django_ledger.change_itemmodel"]
|
|
|
|
def get_form_kwargs(self):
|
|
dealer = get_user_type(self.request)
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs["entity_slug"] = dealer.entity.slug
|
|
kwargs["user_model"] = dealer.entity.admin
|
|
return kwargs
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
form.instance.entity = dealer.entity
|
|
return super().form_valid(form)
|
|
def get_success_url(self):
|
|
return reverse_lazy("item_expense_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]})
|
|
|
|
class ItemExpenseListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Handles the display of a list of item expenses.
|
|
|
|
This class-based view is responsible for displaying a paginated list of item
|
|
expenses related to a specific entity. It uses the Django generic `ListView`
|
|
class and integrates with permissions and login requirements. It retrieves
|
|
data through the `get_queryset` method by invoking a specific function on
|
|
the user's associated entity object.
|
|
|
|
:ivar model: The model associated with the view.
|
|
:type model: type[ItemModel]
|
|
:ivar template_name: The template used to render the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The context variable name used in the template.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of items displayed per page.
|
|
:type paginate_by: int
|
|
:ivar permission_required: The list of required permissions to access the view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
model = ItemModel
|
|
template_name = "items/expenses/expenses_list.html"
|
|
context_object_name = "expenses"
|
|
paginate_by = 30
|
|
permission_required = ["django_ledger.view_itemmodel"]
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
return dealer.entity.get_items_expenses()
|
|
|
|
|
|
class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Provides a view for listing bills.
|
|
|
|
This class-based view is used to display a list of bills. It ensures that
|
|
the user is logged in and has the appropriate permissions to view the list
|
|
of bills. The view retrieves the bills associated with the dealer's entity
|
|
and displays them using the specified template.
|
|
|
|
:ivar model: The model associated with the view.
|
|
:type model: django.db.models.Model
|
|
:ivar template_name: The template used to render the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: Name of the context variable containing the list of bills.
|
|
:type context_object_name: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
model = BillModel
|
|
template_name = "ledger/bills/bill_list.html"
|
|
context_object_name = "bills"
|
|
permission_required = ["django_ledger.view_billmodel"]
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
qs = dealer.entity.get_bills()
|
|
return qs
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["entity"] = get_user_type(self.request).entity
|
|
return context
|
|
|
|
|
|
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 = "bill/bill_detail.html"
|
|
context_object_name = "bill"
|
|
permission_required = ["django_ledger.view_billmodel"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
dealer = get_user_type(self.request)
|
|
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
|
|
kwargs["entity"] = dealer.entity
|
|
|
|
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)
|
|
|
|
|
|
# class BillModelCreateViewView(Create):
|
|
# template_name = 'bill/bill_create.html'
|
|
|
|
# def get_context_data(self, **kwargs):
|
|
# context = super().get_context_data(**kwargs)
|
|
# context["entity"] = get_user_type(self.request).entity
|
|
# return context
|
|
|
|
|
|
# def post(self, request, *args, **kwargs):
|
|
# """
|
|
# Completely override the post method to ensure our form handling is called
|
|
# """
|
|
# print("Custom post method called") # Debugging
|
|
# form = self.get_form()
|
|
# if form.is_valid():
|
|
# return self.custom_form_valid(form)
|
|
# else:
|
|
# return self.form_invalid(form)
|
|
|
|
# def custom_form_valid(self, form):
|
|
# """
|
|
# Our own form_valid implementation that will definitely be called
|
|
# """
|
|
# print("Custom form_valid called") # Debugging
|
|
|
|
# # Handle PO case
|
|
# if self.for_purchase_order:
|
|
# print("Handling PO case") # Debugging
|
|
# return self.handle_po_case(form)
|
|
|
|
# # Handle Estimate case
|
|
# if self.for_estimate:
|
|
# print("Handling Estimate case") # Debugging
|
|
# return self.handle_estimate_case(form)
|
|
|
|
# # Default case
|
|
# print("Handling default case") # Debugging
|
|
# return self.handle_default_case(form)
|
|
|
|
# def handle_po_case(self, form):
|
|
# """Special handling for purchase orders"""
|
|
# po_pk = self.kwargs['po_pk']
|
|
# item_uuids = self.request.GET.get('item_uuids', '').split(',')
|
|
|
|
# if not item_uuids or not all(item_uuids):
|
|
# return HttpResponseBadRequest()
|
|
|
|
# # Create bill
|
|
# bill_model = form.save(commit=False)
|
|
# ledger_model, bill_model = bill_model.configure(
|
|
# entity_slug=self.AUTHORIZED_ENTITY_MODEL,
|
|
# commit_ledger=True
|
|
# )
|
|
|
|
# # Get PO
|
|
# po_qs = PurchaseOrderModel.objects.for_entity(
|
|
# entity_slug=self.kwargs['entity_slug'],
|
|
# user_model=self.request.user
|
|
# )
|
|
# po_model = get_object_or_404(po_qs, uuid__exact=po_pk)
|
|
|
|
# # Validate
|
|
# try:
|
|
# bill_model.can_bind_po(po_model, raise_exception=True)
|
|
# except ValidationError as e:
|
|
# messages.error(self.request, e.message)
|
|
# return self.render_to_response(self.get_context_data(form=form))
|
|
|
|
# # Update models
|
|
# po_model_items_qs = po_model.itemtransactionmodel_set.filter(uuid__in=item_uuids)
|
|
# if po_model.is_contract_bound():
|
|
# bill_model.ce_model_id = po_model.ce_model_id
|
|
|
|
# bill_model.update_amount_due()
|
|
# bill_model.get_state(commit=True)
|
|
# bill_model.clean()
|
|
# bill_model.save()
|
|
# po_model_items_qs.update(bill_model=bill_model)
|
|
|
|
# # Redirect to our custom URL
|
|
# return HttpResponseRedirect(
|
|
# reverse('purchase_order_update',
|
|
# kwargs={
|
|
# 'entity_slug': self.kwargs['entity_slug'],
|
|
# 'po_pk': po_pk
|
|
# })
|
|
# )
|
|
|
|
# def handle_estimate_case(self, form):
|
|
# """Special handling for estimates"""
|
|
# bill_model = form.save(commit=False)
|
|
# ledger_model, bill_model = bill_model.configure(
|
|
# entity_slug=self.AUTHORIZED_ENTITY_MODEL,
|
|
# commit_ledger=True
|
|
# )
|
|
|
|
# ce_pk = self.kwargs['ce_pk']
|
|
# estimate_model_qs = EstimateModel.objects.for_entity(
|
|
# entity_slug=self.kwargs['entity_slug'],
|
|
# user_model=self.request.user
|
|
# )
|
|
# estimate_model = get_object_or_404(estimate_model_qs, uuid__exact=ce_pk)
|
|
# bill_model.bind_estimate(estimate_model=estimate_model, commit=True)
|
|
|
|
# # Redirect to our custom URL
|
|
# return HttpResponseRedirect(
|
|
# reverse('estimate_detail',
|
|
# kwargs={
|
|
# 'pk': ce_pk
|
|
# })
|
|
# )
|
|
|
|
# def handle_default_case(self, form):
|
|
# """Default form handling"""
|
|
# bill_model = form.save(commit=False)
|
|
# ledger_model, bill_model = bill_model.configure(
|
|
# entity_slug=self.AUTHORIZED_ENTITY_MODEL,
|
|
# commit_ledger=True
|
|
# )
|
|
# bill_model.save()
|
|
|
|
|
|
# # Redirect to our custom URL
|
|
# return HttpResponseRedirect(
|
|
# reverse('bill-detail',
|
|
# kwargs={
|
|
# 'entity_slug': self.kwargs['entity_slug'],
|
|
# 'bill_pk': bill_model.uuid
|
|
# })
|
|
# )
|
|
class BillModelCreateView(CreateView):
|
|
template_name = "bill/bill_create.html"
|
|
PAGE_TITLE = _("Create Bill")
|
|
extra_context = {
|
|
"page_title": PAGE_TITLE,
|
|
"header_title": PAGE_TITLE,
|
|
"header_subtitle_icon": "uil:bill",
|
|
}
|
|
for_purchase_order = False
|
|
for_estimate = False
|
|
|
|
def get(self, request, dealer_slug, **kwargs):
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
if not request.user.is_authenticated:
|
|
return HttpResponseForbidden()
|
|
|
|
if self.for_estimate and "ce_pk" in self.kwargs:
|
|
estimate_qs = EstimateModel.objects.for_entity(
|
|
entity_slug=self.kwargs["entity_slug"], user_model=self.request.user
|
|
)
|
|
estimate_model: EstimateModel = get_object_or_404(
|
|
estimate_qs, uuid__exact=self.kwargs["ce_pk"]
|
|
)
|
|
if not estimate_model.can_bind():
|
|
return HttpResponseNotFound("404 Not Found")
|
|
return super(BillModelCreateView, self).get(request, dealer_slug, **kwargs)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(BillModelCreateView, self).get_context_data(**kwargs)
|
|
|
|
if self.for_purchase_order:
|
|
po_pk = self.kwargs["po_pk"]
|
|
po_item_uuids_qry_param = self.request.GET.get("item_uuids")
|
|
if po_item_uuids_qry_param:
|
|
try:
|
|
po_item_uuids = po_item_uuids_qry_param.split(",")
|
|
except:
|
|
return HttpResponseBadRequest()
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
po_qs = PurchaseOrderModel.objects.for_entity(
|
|
entity_slug=self.kwargs["entity_slug"], user_model=self.request.user
|
|
).prefetch_related("itemtransactionmodel_set")
|
|
po_model: PurchaseOrderModel = get_object_or_404(po_qs, uuid__exact=po_pk)
|
|
po_itemtxs_qs = po_model.itemtransactionmodel_set.filter(
|
|
bill_model__isnull=True, uuid__in=po_item_uuids
|
|
)
|
|
context["po_model"] = po_model
|
|
context["po_itemtxs_qs"] = po_itemtxs_qs
|
|
form_action = (
|
|
reverse(
|
|
"bill-create-po",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"entity_slug": self.kwargs["entity_slug"],
|
|
"po_pk": po_model.uuid,
|
|
},
|
|
)
|
|
+ f"?item_uuids={po_item_uuids_qry_param}"
|
|
)
|
|
elif self.for_estimate:
|
|
estimate_qs = EstimateModel.objects.for_entity(
|
|
entity_slug=self.kwargs["entity_slug"], user_model=self.request.user
|
|
)
|
|
estimate_uuid = self.kwargs["ce_pk"]
|
|
estimate_model: EstimateModel = get_object_or_404(
|
|
estimate_qs, uuid__exact=estimate_uuid
|
|
)
|
|
form_action = reverse(
|
|
"bill-create-estimate",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"entity_slug": self.kwargs["entity_slug"],
|
|
"ce_pk": estimate_model.uuid,
|
|
},
|
|
)
|
|
else:
|
|
form_action = reverse(
|
|
"bill-create",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"entity_slug": self.kwargs["entity_slug"],
|
|
},
|
|
)
|
|
context["form_action_url"] = form_action
|
|
return context
|
|
|
|
def get_initial(self):
|
|
return {"date_draft": get_localdate()}
|
|
|
|
def get_form(self, form_class=None):
|
|
dealer = get_user_type(self.request)
|
|
return BillModelCreateForm(entity_model=dealer.entity, **self.get_form_kwargs())
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
bill_model: BillModel = form.save(commit=False)
|
|
ledger_model, bill_model = bill_model.configure(
|
|
entity_slug=dealer.entity.slug,
|
|
user_model=self.request.user,
|
|
commit_ledger=True,
|
|
)
|
|
|
|
if self.for_estimate:
|
|
ce_pk = self.kwargs["ce_pk"]
|
|
estimate_model_qs = EstimateModel.objects.for_entity(
|
|
entity_slug=self.kwargs["entity_slug"], user_model=self.request.user
|
|
)
|
|
|
|
estimate_model = get_object_or_404(estimate_model_qs, uuid__exact=ce_pk)
|
|
bill_model.bind_estimate(estimate_model=estimate_model, commit=False)
|
|
|
|
elif self.for_purchase_order:
|
|
po_pk = self.kwargs["po_pk"]
|
|
item_uuids = self.request.GET.get("item_uuids")
|
|
if not item_uuids:
|
|
return HttpResponseBadRequest()
|
|
item_uuids = item_uuids.split(",")
|
|
po_qs = PurchaseOrderModel.objects.for_entity(
|
|
entity_slug=self.kwargs["entity_slug"], user_model=self.request.user
|
|
)
|
|
po_model: PurchaseOrderModel = get_object_or_404(po_qs, uuid__exact=po_pk)
|
|
|
|
try:
|
|
bill_model.can_bind_po(po_model, raise_exception=True)
|
|
except ValidationError as e:
|
|
messages.add_message(
|
|
self.request,
|
|
message=e.message,
|
|
level=messages.ERROR,
|
|
extra_tags="is-danger",
|
|
)
|
|
return self.render_to_response(self.get_context_data(form=form))
|
|
|
|
po_model_items_qs = po_model.itemtransactionmodel_set.filter(
|
|
uuid__in=item_uuids
|
|
)
|
|
|
|
if po_model.is_contract_bound():
|
|
bill_model.ce_model_id = po_model.ce_model_id
|
|
|
|
bill_model.update_amount_due()
|
|
bill_model.get_state(commit=True)
|
|
bill_model.clean()
|
|
bill_model.save()
|
|
po_model_items_qs.update(bill_model=bill_model)
|
|
return HttpResponseRedirect(self.get_success_url())
|
|
|
|
return super(BillModelCreateView, self).form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
entity_slug = self.kwargs["entity_slug"]
|
|
if self.for_purchase_order:
|
|
po_pk = self.kwargs["po_pk"]
|
|
return reverse(
|
|
"purchase_order_update",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"entity_slug": entity_slug,
|
|
"po_pk": po_pk,
|
|
},
|
|
)
|
|
elif self.for_estimate:
|
|
return reverse(
|
|
"customer-estimate-detail",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"entity_slug": entity_slug,
|
|
"ce_pk": self.kwargs["ce_pk"],
|
|
},
|
|
)
|
|
bill_model: BillModel = self.object
|
|
return reverse(
|
|
"bill-detail",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"entity_slug": entity_slug,
|
|
"bill_pk": bill_model.uuid,
|
|
},
|
|
)
|
|
|
|
|
|
class BillModelDetailViewView(BillModelDetailView):
|
|
template_name = "bill/bill_detail.html"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(BillModelDetailViewView, self).get_context_data(**kwargs)
|
|
context["dealer"] = self.request.dealer
|
|
return context
|
|
|
|
|
|
class BillModelUpdateViewView(BillModelUpdateView):
|
|
template_name = "bill/bill_update.html"
|
|
|
|
def post(self, request, dealer_slug, entity_slug, bill_pk, *args, **kwargs):
|
|
if self.action_update_items:
|
|
if not request.user.is_authenticated:
|
|
return HttpResponseForbidden()
|
|
|
|
queryset = self.get_queryset()
|
|
entity_model: EntityModel = self.get_authorized_entity_instance()
|
|
bill_model: BillModel = self.get_object(queryset=queryset)
|
|
bill_pk = bill_model.uuid
|
|
|
|
self.object = bill_model
|
|
|
|
bill_itemtxs_formset_class = get_bill_itemtxs_formset_class(bill_model)
|
|
itemtxs_formset = bill_itemtxs_formset_class(
|
|
request.POST, bill_model=bill_model, entity_model=entity_model
|
|
)
|
|
|
|
if itemtxs_formset.has_changed():
|
|
if itemtxs_formset.is_valid():
|
|
itemtxs_list = itemtxs_formset.save(commit=False)
|
|
|
|
for itemtxs in itemtxs_list:
|
|
itemtxs.bill_model_id = bill_model.uuid
|
|
itemtxs.clean()
|
|
|
|
itemtxs_formset.save()
|
|
itemtxs_qs = bill_model.update_amount_due()
|
|
bill_model.get_state(commit=True)
|
|
bill_model.clean()
|
|
bill_model.save(
|
|
update_fields=[
|
|
"amount_due",
|
|
"amount_receivable",
|
|
"amount_unearned",
|
|
"amount_earned",
|
|
"updated",
|
|
]
|
|
)
|
|
|
|
bill_model.migrate_state(
|
|
entity_slug=self.kwargs["entity_slug"],
|
|
user_model=self.request.user,
|
|
itemtxs_qs=itemtxs_qs,
|
|
raise_exception=False,
|
|
)
|
|
|
|
messages.add_message(
|
|
request,
|
|
message=f"Items for Invoice {bill_model.bill_number} saved.",
|
|
level=messages.SUCCESS,
|
|
)
|
|
|
|
# if valid get saved formset from DB
|
|
return HttpResponseRedirect(
|
|
redirect_to=reverse(
|
|
"bill-update",
|
|
kwargs={
|
|
"dealer_slug": dealer_slug,
|
|
"entity_slug": entity_model.slug,
|
|
"bill_pk": bill_pk,
|
|
},
|
|
)
|
|
)
|
|
context = self.get_context_data(itemtxs_formset=itemtxs_formset)
|
|
return self.render_to_response(context=context)
|
|
return super(BillModelUpdateViewView, self).post(
|
|
request, dealer_slug, entity_slug, bill_pk, **kwargs
|
|
)
|
|
|
|
def get_success_url(self):
|
|
return reverse(
|
|
"bill-update",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"entity_slug": self.kwargs["entity_slug"],
|
|
"bill_pk": self.kwargs["bill_pk"],
|
|
},
|
|
)
|
|
|
|
|
|
# @login_required
|
|
# @permission_required("django_ledger.add_billmodel", raise_exception=True)
|
|
# def bill_create(request):
|
|
# """
|
|
# Handles creation of a bill in the system, including the validation of input data,
|
|
# creation of transactions associated with the bill, and rendering of the appropriate
|
|
# response or form. Ensures the user creating the bill has the necessary permissions and
|
|
# correct input parameters for successful bill creation and itemization.
|
|
|
|
# :param request: Django HttpRequest object containing metadata and data of the HTTP request.
|
|
# :type request: HttpRequest
|
|
# :return: JsonResponse with success/error information if the request is processed,
|
|
# or HttpResponse rendering the form for bill creation.
|
|
# :rtype: JsonResponse or HttpResponse
|
|
# """
|
|
# dealer = get_user_type(request)
|
|
# entity = dealer.entity
|
|
|
|
# if request.method == "POST":
|
|
# data = json.loads(request.body)
|
|
# vendor_id = data.get("vendor")
|
|
# terms = data.get("terms")
|
|
# vendor = entity.get_vendors().filter(pk=vendor_id).first()
|
|
|
|
# items = data.get("item", [])
|
|
# quantities = data.get("quantity", [])
|
|
|
|
# if not all([items, quantities]):
|
|
# return JsonResponse(
|
|
# {"status": "error", "message": _("Items and Quantities are required")},
|
|
# status=400,
|
|
# )
|
|
# if isinstance(quantities, list):
|
|
# if "0" in quantities:
|
|
# return JsonResponse(
|
|
# {
|
|
# "status": "error",
|
|
# "message": _("Quantity must be greater than zero"),
|
|
# }
|
|
# )
|
|
# else:
|
|
# if int(quantities) <= 0:
|
|
# return JsonResponse(
|
|
# {
|
|
# "status": "error",
|
|
# "message": _("Quantity must be greater than zero"),
|
|
# }
|
|
# )
|
|
|
|
# bill = entity.create_bill(vendor_model=vendor, terms=terms)
|
|
# if isinstance(items, list):
|
|
# item_quantity_map = {}
|
|
# for item, quantity in zip(items, quantities):
|
|
# if item in item_quantity_map:
|
|
# item_quantity_map[item] += int(quantity)
|
|
# else:
|
|
# item_quantity_map[item] = int(quantity)
|
|
# item_list = list(item_quantity_map.keys())
|
|
# quantity_list = list(item_quantity_map.values())
|
|
|
|
# items_list = [
|
|
# {"item_id": item_list[i], "quantity": quantity_list[i]}
|
|
# for i in range(len(item_list))
|
|
# ]
|
|
# items_txs = []
|
|
# for item in items_list:
|
|
# item_instance = ItemModel.objects.get(pk=item.get("item_id"))
|
|
# car = models.Car.objects.get(vin=item_instance.name)
|
|
# quantity = Decimal(item.get("quantity"))
|
|
# items_txs.append(
|
|
# {
|
|
# "item_number": item_instance.item_number,
|
|
# "quantity": quantity,
|
|
# "unit_cost": car.finances.cost_price,
|
|
# "total_amount": car.finances.cost_price * quantity,
|
|
# }
|
|
# )
|
|
|
|
# bill_itemtxs = {
|
|
# item.get("item_number"): {
|
|
# "unit_cost": item.get("unit_cost"),
|
|
# "quantity": item.get("quantity"),
|
|
# "total_amount": item.get("total_amount"),
|
|
# }
|
|
# for item in items_txs
|
|
# }
|
|
# else:
|
|
# item = entity.get_items_all().filter(pk=items).first()
|
|
# instance = models.Car.objects.get(vin=item.name)
|
|
# bill_itemtxs = {
|
|
# item.item_number: {
|
|
# "unit_cost": instance.finances.cost_price,
|
|
# "quantity": Decimal(quantities),
|
|
# "total_amount": instance.finances.cost_price * Decimal(quantities),
|
|
# }
|
|
# }
|
|
|
|
# bill_itemtxs = bill.migrate_itemtxs(
|
|
# itemtxs=bill_itemtxs,
|
|
# commit=True,
|
|
# operation=BillModel.ITEMIZE_APPEND,
|
|
# )
|
|
|
|
# url = reverse("bill_detail", kwargs={"pk": bill.pk})
|
|
# return JsonResponse(
|
|
# {
|
|
# "status": "success",
|
|
# "message": _("Bill created successfully"),
|
|
# "url": f"{url}",
|
|
# }
|
|
# )
|
|
|
|
# form = forms.BillModelCreateForm(entity_model=entity)
|
|
# form.initial.update(
|
|
# {
|
|
# "cash_account": dealer.settings.bill_cash_account,
|
|
# "prepaid_account": dealer.settings.bill_prepaid_account,
|
|
# "unearned_account": dealer.settings.bill_unearned_account,
|
|
# }
|
|
# )
|
|
# car_list = models.Car.objects.filter(dealer=dealer)
|
|
# context = {
|
|
# "form": form,
|
|
# "items": [
|
|
# {
|
|
# "car": x,
|
|
# "product": entity.get_items_products().filter(name=x.vin).first(),
|
|
# }
|
|
# for x in car_list
|
|
# ],
|
|
# }
|
|
|
|
# return render(request, "ledger/bills/bill_form.html", context)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.delete_billmodel", raise_exception=True)
|
|
def BillDeleteView(request, pk):
|
|
"""
|
|
Deletes a specific BillModel instance based on the primary key (pk).
|
|
This view requires the user to be logged in and have the appropriate
|
|
permission to delete a bill. After the delete operation is successful,
|
|
the user is redirected to the bill list view.
|
|
|
|
:param request: The HTTP request object.
|
|
:type request: HttpRequest
|
|
:param pk: Primary key of the BillModel instance to be deleted.
|
|
:type pk: int
|
|
:return: Redirect to the bill list view after successful deletion.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
bill = get_object_or_404(BillModel, pk=pk)
|
|
bill.delete()
|
|
return redirect("bill_list")
|
|
|
|
|
|
# orders
|
|
class OrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Represents a view for listing sale orders.
|
|
|
|
This class provides the functionality to display a list of sale orders
|
|
within a web template. It includes permission-based access control, and
|
|
retrieves sale orders corresponding to the entity of the logged-in dealer.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: models.SaleOrder
|
|
:ivar template_name: The name of the template to be rendered.
|
|
:type template_name: str
|
|
:ivar context_object_name: The context variable name to be used for the object list.
|
|
:type context_object_name: str
|
|
:ivar permission_required: A list of permissions required to access this view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
model = models.SaleOrder
|
|
template_name = "sales/orders/order_list.html"
|
|
context_object_name = "orders"
|
|
paginate_by = 30
|
|
permission_required = ["inventory.view_saleorder"]
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
qs = super().get_queryset()
|
|
return qs.filter(estimate__entity=dealer.entity)
|
|
|
|
|
|
# email
|
|
@login_required
|
|
@permission_required("django_ledger.view_estimatemodel", raise_exception=True)
|
|
def send_email_view(request, dealer_slug, pk):
|
|
"""
|
|
View function to send an email for an estimate. This function allows authenticated and
|
|
authorized users to send an email containing the estimate details to the customer.
|
|
The email includes a link to preview the estimate and a message template in multiple
|
|
languages. If the estimate does not have any associated items, the user will receive
|
|
an error message. Upon successfully sending the email, the estimate is marked as reviewed.
|
|
|
|
:param request: The HttpRequest object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the estimate to be sent via email.
|
|
:type pk: int
|
|
:return: An HttpResponseRedirect to the estimate detail view.
|
|
:rtype: HttpResponseRedirect
|
|
:raises Http404: If the estimate with the given primary key does not exist.
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
estimate = get_object_or_404(EstimateModel, pk=pk)
|
|
|
|
if not estimate.get_itemtxs_data()[0]:
|
|
messages.error(request, _("Quotation has no items"))
|
|
return redirect("estimate_detail", dealer_slug=dealer_slug, pk=estimate.pk)
|
|
|
|
link = request.build_absolute_uri(
|
|
reverse_lazy(
|
|
"estimate_preview", kwargs={"dealer_slug": dealer_slug, "pk": estimate.pk}
|
|
)
|
|
)
|
|
|
|
msg = f"""
|
|
السلام عليكم
|
|
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", dealer_slug=dealer.slug, pk=estimate.pk)
|
|
|
|
|
|
# errors
|
|
def custom_page_not_found_view(request, exception=None):
|
|
"""
|
|
Custom handler for 404 errors that renders a custom HTML page
|
|
upon encountering a Page Not Found error.
|
|
|
|
:param request: The HttpRequest object associated with the request.
|
|
:type request: HttpRequest
|
|
:param exception: The exception that triggered the 404 error, if any.
|
|
:type exception: Exception, optional
|
|
:return: An HttpResponse object rendering the "errors/404.html" template.
|
|
:rtype: HttpResponse
|
|
"""
|
|
return render(request, "errors/404.html", {})
|
|
|
|
|
|
def custom_error_view(request, exception=None):
|
|
"""
|
|
Handles rendering the custom error page for HTTP 500 errors.
|
|
|
|
This function is called when an unhandled exception occurs in the application. It renders
|
|
a predefined template for server errors, providing a consistent error page layout
|
|
to the users.
|
|
|
|
:param request: The HTTP request instance which triggered the error.
|
|
:type request: HttpRequest
|
|
:param exception: The exception that caused the error to trigger. Defaults to None.
|
|
:type exception: Exception, optional
|
|
:return: An HttpResponse object representing the rendered error page.
|
|
:rtype: HttpResponse
|
|
"""
|
|
return render(request, "errors/500.html", {})
|
|
|
|
|
|
def custom_permission_denied_view(request, exception=None):
|
|
"""
|
|
Handles the custom view for 403 Forbidden permission denied errors. This
|
|
function renders a custom template for the error page when users are denied
|
|
permission to access a particular resource or view.
|
|
|
|
:param request: Django HttpRequest object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param exception: Optional exception that caused the 403 error.
|
|
Defaults to None.
|
|
:type exception: Exception | None
|
|
:return: HttpResponse object rendering the 403.html error page.
|
|
:rtype: HttpResponse
|
|
"""
|
|
return render(request, "errors/403.html", {})
|
|
|
|
|
|
def custom_bad_request_view(request, exception=None):
|
|
"""
|
|
Handles custom bad request responses by rendering a specified HTML
|
|
template for 400 status errors.
|
|
|
|
:return: Rendered HTTP response rendering the error page.
|
|
:rtype: HttpResponse
|
|
"""
|
|
return render(request, "errors/400.html", {})
|
|
|
|
|
|
# BALANCE SHEET
|
|
class BaseBalanceSheetRedirectView(RedirectView):
|
|
"""
|
|
Handles redirection for balance sheet views of entities.
|
|
|
|
This class is specifically designed to redirect users to the
|
|
appropriate year's balance sheet for an entity. It determines
|
|
the URL based on the current year and the provided entity slug
|
|
in the request. Additionally, it provides a URL for login redirection
|
|
if required.
|
|
|
|
:ivar url: The original URL or base URL to redirect users.
|
|
:type url: str
|
|
"""
|
|
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
year = get_localdate().year
|
|
return reverse(
|
|
"entity-bs-year",
|
|
kwargs={"entity_slug": self.kwargs["entity_slug"], "year": year},
|
|
)
|
|
|
|
def get_login_url(self):
|
|
return reverse("account_login")
|
|
|
|
|
|
class FiscalYearBalanceSheetViewBase(
|
|
FiscalYearBalanceSheetView, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Defines a base view for the fiscal year balance sheet.
|
|
|
|
This class serves as a base view for displaying the balance sheet of a fiscal
|
|
year. It provides functionality for rendering the associated template and
|
|
managing user login requirements. It integrates with Django's security system
|
|
to ensure appropriate access control.
|
|
|
|
:ivar template_name: The template used for rendering the balance sheet view.
|
|
:type template_name: str
|
|
"""
|
|
|
|
template_name = "ledger/reports/balance_sheet.html"
|
|
# AUTHORIZE_SUPERUSER = False
|
|
# permission_required = []
|
|
|
|
# def get_authorized_entity_queryset(self):
|
|
# dealer = get_user_type(self.request)
|
|
# return EntityModel.objects.filter(admin=dealer)
|
|
|
|
def get_login_url(self):
|
|
return reverse("account_login")
|
|
|
|
|
|
class QuarterlyBalanceSheetView(
|
|
FiscalYearBalanceSheetViewBase, QuarterlyReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a quarterly balance sheet view.
|
|
|
|
The purpose of this class is to provide a representation and handling
|
|
for the balance sheet data specific to fiscal quarters. It extends from
|
|
`FiscalYearBalanceSheetViewBase` to inherit year-level balance sheet
|
|
operations and incorporates functionality from `QuarterlyReportMixIn`
|
|
to manage quarterly-specific logic. Additionally, the `DjangoLedgerSecurityMixIn`
|
|
is used to handle security features related to Django ledger operations.
|
|
|
|
:ivar fiscal_year: The fiscal year associated with the quarterly balance sheet.
|
|
:type fiscal_year: int
|
|
:ivar quarter: The specific quarter (e.g., Q1, Q2, Q3, Q4) for the balance sheet.
|
|
:type quarter: int
|
|
:ivar balance_data: The data structure representing the detailed financial
|
|
information for the balance sheet of the quarter.
|
|
:type balance_data: dict
|
|
"""
|
|
|
|
|
|
class MonthlyBalanceSheetView(
|
|
FiscalYearBalanceSheetViewBase, MonthlyReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents the view for the monthly balance sheet.
|
|
|
|
This class is responsible for handling the presentation and processing of
|
|
monthly balance sheet data. It incorporates necessary mix-ins to provide
|
|
functionality specific to fiscal year balance sheet viewing, monthly reporting,
|
|
and security validation within the Django ledger context.
|
|
|
|
:ivar fiscal_year: Stores the fiscal year information associated with the
|
|
balance sheet.
|
|
:type fiscal_year: int
|
|
:ivar monthly_reports: Holds a collection of monthly report instances that
|
|
pertain to the balance sheet.
|
|
:type monthly_reports: List[MonthlyReport]
|
|
:ivar user_permissions: Manages user-specific permissions for accessing and
|
|
modifying the balance sheet data.
|
|
:type user_permissions: Dict[str, Any]
|
|
"""
|
|
|
|
|
|
class DateBalanceSheetView(
|
|
FiscalYearBalanceSheetViewBase, DateReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a balance sheet view for a specific date.
|
|
|
|
DateBalanceSheetView extends functionality for displaying balance sheets
|
|
specific to certain dates. This class combines features from various mixins
|
|
to enforce security, handle date-based reporting, and provide fiscal year
|
|
context. It is used in scenarios where financial reports for a particular
|
|
date are required to ensure accurate and filtered viewing of accounting
|
|
data.
|
|
|
|
:ivar fiscal_year: Represents the fiscal year context for the balance sheet.
|
|
:type fiscal_year: FiscalYear
|
|
:ivar report_date: The specific date for which the balance sheet report
|
|
is generated.
|
|
:type report_date: datetime.date
|
|
:ivar is_authenticated: Indicates whether the user accessing the
|
|
balance sheet has been authenticated.
|
|
:type is_authenticated: bool
|
|
:ivar account_data: Contains processed account data used in the balance sheet
|
|
for computations or display purposes.
|
|
:type account_data: dict
|
|
"""
|
|
|
|
|
|
# Income Statement -----------
|
|
|
|
|
|
class BaseIncomeStatementRedirectViewBase(
|
|
BaseIncomeStatementRedirectView, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
The BaseIncomeStatementRedirectViewBase class provides functionality for handling
|
|
redirects in the context of income statements. This class combines features from
|
|
both BaseIncomeStatementRedirectView and DjangoLedgerSecurityMixIn to ensure
|
|
secure handling of data and user authentication.
|
|
|
|
This class operates by calculating the current year, determining the user's dealer
|
|
type, and generating a URL to redirect to an income statement view associated with a
|
|
specific entity for the calculated year. Additionally, it defines a method to generate
|
|
the login URL for unauthenticated users.
|
|
|
|
:ivar request: The HTTP request object containing user session and other request-related data.
|
|
:type request: HttpRequest
|
|
"""
|
|
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
year = get_localdate().year
|
|
dealer = get_user_type(self.request)
|
|
return reverse(
|
|
"entity-ic-year", kwargs={"entity_slug": dealer.entity.slug, "year": year}
|
|
)
|
|
|
|
def get_login_url(self):
|
|
return reverse("account_login")
|
|
|
|
|
|
class FiscalYearIncomeStatementViewBase(
|
|
FiscalYearIncomeStatementView, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a base view for fiscal year income statement.
|
|
|
|
This class serves as a base view for generating and rendering the fiscal
|
|
year income statement within the application. It combines multiple
|
|
functionalities, including template rendering and permission handling,
|
|
to provide a secure and user-friendly interface.
|
|
|
|
:ivar template_name: Path to the HTML template used for rendering the
|
|
income statement view.
|
|
:type template_name: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
template_name = "ledger/reports/income_statement.html"
|
|
permission_required = ["inventory.view_carfinance"]
|
|
|
|
def get_login_url(self):
|
|
return reverse("account_login")
|
|
|
|
|
|
class QuarterlyIncomeStatementView(
|
|
FiscalYearIncomeStatementViewBase, QuarterlyReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a detailed view for a quarterly income statement.
|
|
|
|
This class is responsible for providing a structured view of a company's
|
|
quarterly income statement. It combines the functionality of multiple
|
|
base classes to deliver a comprehensive representation of quarterly report
|
|
data while ensuring integration with Django Ledger security protocols.
|
|
|
|
The class serves as a specialized view tied to specific fiscal data,
|
|
enabling efficient management and rendering of quarterly financial reports.
|
|
|
|
:ivar fiscal_year: The fiscal year associated with the quarterly income
|
|
statement.
|
|
:type fiscal_year: int
|
|
:ivar quarter: The quarter number (e.g., 1 for Q1, 2 for Q2, etc.) of the
|
|
income statement.
|
|
:type quarter: int
|
|
:ivar revenue: The total revenue recorded in the quarterly income statement.
|
|
:type revenue: float
|
|
:ivar net_income: The net income calculated for the quarter.
|
|
:type net_income: float
|
|
:ivar expenses: The total expenses incurred during the quarter.
|
|
:type expenses: float
|
|
"""
|
|
|
|
|
|
class MonthlyIncomeStatementView(
|
|
FiscalYearIncomeStatementViewBase, MonthlyReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents the view for a monthly income statement in the financial
|
|
application.
|
|
|
|
This class is designed to support the presentation and display of
|
|
financial income statement data on a monthly basis. It incorporates
|
|
functionality from multiple mixins to include specific fiscal year
|
|
operations, monthly report behaviors, and security aspects tailored
|
|
to the Django Ledger system.
|
|
|
|
:ivar fiscal_year: The fiscal year associated with the income
|
|
statement.
|
|
:type fiscal_year: int
|
|
:ivar month: The month for which the income statement is viewed.
|
|
:type month: int
|
|
:ivar data: The financial data contained in the income statement.
|
|
:type data: dict
|
|
:ivar authorized_user: The user authorized to access this income
|
|
statement view.
|
|
:type authorized_user: str
|
|
"""
|
|
|
|
|
|
class DateModelIncomeStatementView(
|
|
FiscalYearIncomeStatementViewBase, DateReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a detailed view of an income statement for a fiscal year with additional
|
|
capabilities for handling dates and security integrations.
|
|
|
|
This class is a specialized combination of views and mixins designed to manage income
|
|
statements with added functionality for date-based operations and Django-based ledger
|
|
security. It inherits behaviors for fiscal year reports, date handling, and security
|
|
enforcements, providing a cohesive interface for managing income statement data.
|
|
|
|
:ivar fiscal_year_data: Contains detailed income statement data for a specific fiscal year.
|
|
:type fiscal_year_data: dict
|
|
:ivar report_date: Represents the date of the associated report.
|
|
:type report_date: datetime.date
|
|
:ivar user_permissions: Tracks security permissions associated with the income statement for
|
|
authenticated users.
|
|
:type user_permissions: dict
|
|
"""
|
|
|
|
|
|
# Cash Flow -----------
|
|
|
|
|
|
class BaseCashFlowStatementRedirectViewBase(
|
|
BaseCashFlowStatementRedirectView, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Base class for handling cash flow statement redirection views.
|
|
|
|
This class is designed to manage the redirection of users to the
|
|
appropriate cash flow statement view for a particular year and entity.
|
|
It incorporates security features and functionalities specific to dealers
|
|
or entities by extending relevant mixins.
|
|
|
|
:ivar request: The HTTP request object associated with the view.
|
|
:type request: HttpRequest
|
|
"""
|
|
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
year = get_localdate().year
|
|
dealer = get_user_type(self.request)
|
|
return reverse(
|
|
"entity-cf-year", kwargs={"entity_slug": dealer.entity.slug, "year": year}
|
|
)
|
|
|
|
def get_login_url(self):
|
|
return reverse("account_login")
|
|
|
|
|
|
class FiscalYearCashFlowStatementViewBase(
|
|
FiscalYearCashFlowStatementView, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a base view for fiscal year cash flow statements.
|
|
|
|
This class is utilized to handle the rendering and permissions control
|
|
for fiscal year cash flow statement views. It inherits functionality
|
|
from FiscalYearCashFlowStatementView and DjangoLedgerSecurityMixIn
|
|
to integrate with the Django framework's security and reporting
|
|
mechanisms.
|
|
|
|
:ivar template_name: Template path to render the view.
|
|
:type template_name: str
|
|
:ivar permission_required: List of permissions required for accessing
|
|
the view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
template_name = "ledger/reports/cash_flow_statement.html"
|
|
permission_required = ["inventory.view_carfinance"]
|
|
|
|
def get_login_url(self):
|
|
return reverse("account_login")
|
|
|
|
|
|
class QuarterlyCashFlowStatementView(
|
|
FiscalYearCashFlowStatementViewBase, QuarterlyReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a view model for quarterly cash flow statements.
|
|
|
|
This class is designed to encapsulate and handle data related to
|
|
quarterly cash flow statements, extending the functionality from
|
|
base classes and mix-ins for specific behaviors. It combines features
|
|
from fiscal year cash flow statements, quarterly reporting utilities,
|
|
and Django ledger security mechanisms to provide comprehensive support
|
|
for managing and displaying financial data for quarterly cash flows.
|
|
|
|
:ivar fiscal_year: The fiscal year associated with the cash flow
|
|
statement.
|
|
:type fiscal_year: int
|
|
:ivar quarter: The quarter associated with the cash flow statement,
|
|
typically denoted as an integer from 1 to 4.
|
|
:type quarter: int
|
|
:ivar cash_flow_data: A structure containing detailed cash flow
|
|
information for the given quarter and fiscal year.
|
|
:type cash_flow_data: dict
|
|
:ivar is_audited: Boolean flag indicating whether the quarterly cash
|
|
flow statement has been audited.
|
|
:type is_audited: bool
|
|
"""
|
|
|
|
|
|
class MonthlyCashFlowStatementView(
|
|
FiscalYearCashFlowStatementViewBase, MonthlyReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a view for monthly cash flow statements.
|
|
|
|
This class combines the functionality of fiscal year cash flow statement views,
|
|
monthly reporting capabilities, and security features related to Django Ledger.
|
|
It is utilized to generate or retrieve cash flow statements on a monthly basis,
|
|
ensuring seamless integration with other components that extend or implement
|
|
financial reporting logic.
|
|
|
|
:ivar attribute1: Description of attribute1.
|
|
:type attribute1: type
|
|
:ivar attribute2: Description of attribute2.
|
|
:type attribute2: type
|
|
"""
|
|
|
|
|
|
class DateCashFlowStatementView(
|
|
FiscalYearCashFlowStatementViewBase, DateReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Representation of a detailed view for a cash flow statement associated with a specific fiscal year,
|
|
including reporting capabilities and security features. This class extends functionality from
|
|
multiple mix-in classes to provide a comprehensive cash flow statement representation with date-specific
|
|
reporting.
|
|
|
|
:ivar field1: Description of field1.
|
|
:type field1: type
|
|
:ivar field2: Description of field2.
|
|
:type field2: type
|
|
"""
|
|
|
|
|
|
# Dashboard
|
|
|
|
|
|
class EntityModelDetailHandlerViewBase(
|
|
EntityModelDetailHandlerView, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Handles detailed views for Entity Models along with redirection logic
|
|
customized for different users and units.
|
|
|
|
This class extends functionality from `EntityModelDetailHandlerView` and
|
|
integrates additional security measures using `DjangoLedgerSecurityMixIn`.
|
|
It provides a mechanism to construct redirection URLs based on the specifics
|
|
of the current request, such as the user's dealer type, the unit slug, and the
|
|
current localized date. Typically used for redirecting to appropriate dashboards
|
|
with monthly data filtered by either entity or unit.
|
|
|
|
:ivar request: The HTTP request object associated with the current view.
|
|
:type request: HttpRequest
|
|
"""
|
|
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
loc_date = get_localdate()
|
|
dealer = get_user_type(self.request)
|
|
unit_slug = self.get_unit_slug()
|
|
if unit_slug:
|
|
return reverse(
|
|
"unit-dashboard-month",
|
|
kwargs={
|
|
"entity_slug": dealer.entity.slug,
|
|
"unit_slug": unit_slug,
|
|
"year": loc_date.year,
|
|
"month": loc_date.month,
|
|
},
|
|
)
|
|
return reverse(
|
|
"entity-dashboard-month",
|
|
kwargs={
|
|
"entity_slug": dealer.entity.slug,
|
|
"year": loc_date.year,
|
|
"month": loc_date.month,
|
|
},
|
|
)
|
|
|
|
|
|
class EntityModelDetailBaseViewBase(
|
|
EntityModelDetailBaseView, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a base view that extends functionality for displaying detailed
|
|
dashboard information about an entity model. Handles context population for
|
|
dynamic elements like charts and titles specific to the entity or an optional
|
|
unit.
|
|
|
|
This class is designed as part of a Django application, utilizing mixins for
|
|
enhanced security and inheriting a basic entity model detail view. It is
|
|
customized to render a specific dashboard template and manage context data
|
|
for charts and other display elements relevant to the entity.
|
|
|
|
:ivar template_name: Path to the HTML template used for rendering the
|
|
dashboard.
|
|
:type template_name: str
|
|
"""
|
|
|
|
template_name = "ledger/reports/dashboard.html"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
dealer = get_user_type(self.request)
|
|
entity_model: EntityModel = dealer.entity
|
|
context["page_title"] = entity_model.name
|
|
context["header_title"] = entity_model.name
|
|
context["header_subtitle"] = _("Dashboard")
|
|
context["header_subtitle_icon"] = "mdi:monitor-dashboard"
|
|
|
|
unit_slug = context.get("unit_slug", self.get_unit_slug())
|
|
KWARGS = dict(entity_slug=self.kwargs["entity_slug"])
|
|
|
|
if unit_slug:
|
|
KWARGS["unit_slug"] = unit_slug
|
|
|
|
url_pointer = "entity" if not unit_slug else "unit"
|
|
context["pnl_chart_id"] = f"djl-entity-pnl-chart-{randint(10000, 99999)}"
|
|
context["pnl_chart_endpoint"] = reverse(
|
|
f"django_ledger:{url_pointer}-json-pnl", kwargs=KWARGS
|
|
)
|
|
context["payables_chart_id"] = (
|
|
f"djl-entity-payables-chart-{randint(10000, 99999)}"
|
|
)
|
|
context["payables_chart_endpoint"] = reverse(
|
|
f"django_ledger:{url_pointer}-json-net-payables", kwargs=KWARGS
|
|
)
|
|
context["receivables_chart_id"] = (
|
|
f"djl-entity-receivables-chart-{randint(10000, 99999)}"
|
|
)
|
|
context["receivables_chart_endpoint"] = reverse(
|
|
f"django_ledger:{url_pointer}-json-net-receivables", kwargs=KWARGS
|
|
)
|
|
|
|
return context
|
|
|
|
|
|
class FiscalYearEntityModelDashboardView(
|
|
EntityModelDetailBaseViewBase, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a dashboard view for fiscal year entity models.
|
|
|
|
This class serves as a view to display details related to fiscal year
|
|
entity models while enforcing permissions and login requirements.
|
|
It integrates security features and provides functionalities specific
|
|
to managing fiscal year entity models within the dashboard.
|
|
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
permission_required = ["inventory.view_carfinance"]
|
|
|
|
def get_login_url(self):
|
|
return reverse("account_login")
|
|
|
|
|
|
class QuarterlyEntityDashboardView(
|
|
FiscalYearEntityModelDashboardView, QuarterlyReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a dashboard view for quarterly entities.
|
|
|
|
This class integrates the functionalities of `FiscalYearEntityModelDashboardView`,
|
|
`QuarterlyReportMixIn`, and `DjangoLedgerSecurityMixIn` to provide a dashboard
|
|
view that displays and organizes quarterly financial metrics and reports for a
|
|
specific fiscal year entity. It facilitates secure access to financial data and
|
|
provides interfaces to analyze quarterly reports.
|
|
|
|
:ivar quarters: A list of quarters included in the dashboard view.
|
|
:type quarters: list
|
|
:ivar entity: The entity for which the dashboard view is generated.
|
|
:type entity: object
|
|
:ivar fiscal_year: The fiscal year associated with the dashboard.
|
|
:type fiscal_year: int
|
|
:ivar reporting_data: A collection of data specifically prepared for
|
|
quarterly reporting.
|
|
:type reporting_data: dict
|
|
"""
|
|
|
|
|
|
class MonthlyEntityDashboardView(
|
|
FiscalYearEntityModelDashboardView, MonthlyReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a dashboard view for a specific entity's monthly report.
|
|
|
|
This class combines functionalities from the base dashboard view for
|
|
fiscal year entities, as well as specific monthly report and security
|
|
features, to provide a comprehensive dashboard handler. It is designed
|
|
to work within the Django Ledger system, facilitating secured and
|
|
contextual data display tailored for monthly reporting of financial
|
|
entities.
|
|
|
|
:ivar fiscal_year: Represents the fiscal year associated with the
|
|
dashboard view.
|
|
:type fiscal_year: int
|
|
:ivar entity_id: The unique identifier for the financial entity
|
|
being reported on in the dashboard.
|
|
:type entity_id: int
|
|
:ivar report_month: The specific month for which the report is
|
|
generated and displayed.
|
|
:type report_month: str
|
|
:ivar user_permissions: Permissions associated with the currently
|
|
authenticated user to enforce security on
|
|
sensitive actions or data.
|
|
:type user_permissions: list
|
|
"""
|
|
|
|
|
|
class DateEntityDashboardView(
|
|
FiscalYearEntityModelDashboardView, DateReportMixIn, DjangoLedgerSecurityMixIn
|
|
):
|
|
"""
|
|
Represents a dashboard view for date-based entity data visualization.
|
|
|
|
This class integrates date-based information into the dashboard view,
|
|
extending the functionality of `FiscalYearEntityModelDashboardView`.
|
|
It utilizes mixins to incorporate report generation and security features
|
|
specific to the Django Ledger framework.
|
|
|
|
:ivar fiscal_year: The fiscal year associated with the dashboard data.
|
|
:type fiscal_year: FiscalYear
|
|
:ivar date_report_data: Data pertaining to the date-specific reports displayed.
|
|
:type date_report_data: dict
|
|
:ivar user_permissions: The security permissions granted to the user for viewing
|
|
or interacting with the dashboard.
|
|
:type user_permissions: set
|
|
"""
|
|
|
|
|
|
class PayableNetAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View):
|
|
"""
|
|
Handles the retrieval of net payable data for authenticated users.
|
|
|
|
The PayableNetAPIView is designed to process GET requests for retrieving
|
|
a summary of unpaid bills, specifically for authenticated users associated
|
|
with an entity. This view ensures that only authorized users, based on their
|
|
authentication state and type, can access this sensitive financial data. It
|
|
leverages the DjangoLedgerSecurityMixIn and EntityUnitMixIn for security
|
|
enhancements and entity-specific processing.
|
|
|
|
:ivar http_method_names: HTTP methods supported by this view.
|
|
:type http_method_names: list[str]
|
|
"""
|
|
|
|
http_method_names = ["get"]
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
if request.user.is_authenticated:
|
|
dealer = get_user_type(request)
|
|
bill_qs = BillModel.objects.for_entity(
|
|
entity_slug=dealer.entity.slug,
|
|
user_model=dealer.entity.admin,
|
|
).unpaid()
|
|
|
|
net_summary = accruable_net_summary(bill_qs)
|
|
net_payables = {"net_payable_data": net_summary}
|
|
|
|
return JsonResponse({"results": net_payables})
|
|
|
|
return JsonResponse({"message": _("Unauthorized")}, status=401)
|
|
|
|
|
|
class ReceivableNetAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View):
|
|
"""
|
|
Handles the retrieval of net receivables summary for authenticated users.
|
|
|
|
This view is part of a ledger system and is designed to provide a net summary
|
|
of unpaid invoices for an authenticated user. If the user is not authenticated,
|
|
the view will return an unauthorized response. The view supports only GET
|
|
requests.
|
|
|
|
Class Attributes:
|
|
:ivar http_method_names: Restricts the allowed HTTP methods for this view.
|
|
:type http_method_names: list[str]
|
|
"""
|
|
|
|
http_method_names = ["get"]
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
if request.user.is_authenticated:
|
|
dealer = get_user_type(request)
|
|
invoice_qs = InvoiceModel.objects.for_entity(
|
|
entity_slug=dealer.entity.slug,
|
|
user_model=dealer.entity.admin,
|
|
).unpaid()
|
|
|
|
net_summary = accruable_net_summary(invoice_qs)
|
|
|
|
net_receivable = {"net_receivable_data": net_summary}
|
|
|
|
return JsonResponse({"results": net_receivable})
|
|
|
|
return JsonResponse({"message": _("Unauthorized")}, status=401)
|
|
|
|
|
|
class PnLAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View):
|
|
"""
|
|
APIView for handling Profit and Loss (PnL) data retrieval.
|
|
|
|
This class provides an endpoint to handle the retrieval of Profit and Loss data
|
|
for an entity. It uses authentication to validate the user's access and retrieves
|
|
PnL data based on the user's entity and optional query parameters such as dates.
|
|
The retrieved data is organized and returned in a JSON format.
|
|
|
|
:ivar http_method_names: Restricts HTTP methods that can be used with this view.
|
|
:type http_method_names: list[str]
|
|
"""
|
|
|
|
http_method_names = ["get"]
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
if request.user.is_authenticated:
|
|
dealer = get_user_type(request)
|
|
entity = EntityModel.objects.for_user(user_model=dealer.entity.admin).get(
|
|
slug__exact=dealer.entity.slug
|
|
)
|
|
|
|
unit_slug = self.get_unit_slug()
|
|
|
|
io_digest = entity.digest(
|
|
user_model=self.request.user,
|
|
unit_slug=unit_slug,
|
|
equity_only=True,
|
|
signs=False,
|
|
by_period=True,
|
|
process_groups=True,
|
|
from_date=self.request.GET.get("fromDate"),
|
|
to_date=self.request.GET.get("toDate"),
|
|
# todo: For PnL to display proper period values must not use closing entries.
|
|
use_closing_entries=False,
|
|
)
|
|
|
|
io_data = io_digest.get_io_data()
|
|
group_balance_by_period = io_data["group_balance_by_period"]
|
|
group_balance_by_period = dict(
|
|
sorted((k, v) for k, v in group_balance_by_period.items())
|
|
)
|
|
|
|
entity_data = {
|
|
f"{month_name[k[1]]} {k[0]}": {d: float(f) for d, f in v.items()}
|
|
for k, v in group_balance_by_period.items()
|
|
}
|
|
|
|
entity_pnl = {
|
|
"entity_slug": entity.slug,
|
|
"entity_name": entity.name,
|
|
"pnl_data": entity_data,
|
|
}
|
|
|
|
return JsonResponse({"results": entity_pnl})
|
|
|
|
return JsonResponse({"message": _("Unauthorized")}, status=401)
|
|
|
|
|
|
class EmployeeCalendarView(LoginRequiredMixin, ListView):
|
|
"""
|
|
Provides a view for displaying the employee's calendar in a list format.
|
|
|
|
Displays a list of appointments for logged-in users. This view ensures that
|
|
only appointments relevant to the logged-in user as a dealer or staff member
|
|
are displayed. Supports search functionality to filter displayed appointments
|
|
based on provided query parameters.
|
|
|
|
:ivar template_name: Path to the HTML template used to render the view.
|
|
:type template_name: str
|
|
:ivar model: Model object to interact with appointments data.
|
|
:type model: Appointment
|
|
:ivar context_object_name: Name of the context variable that contains the
|
|
list of appointments in the template.
|
|
:type context_object_name: str
|
|
"""
|
|
|
|
template_name = "crm/employee_calendar.html"
|
|
model = Appointment
|
|
context_object_name = "appointments"
|
|
|
|
def get_queryset(self):
|
|
query = self.request.GET.get("q")
|
|
staff = getattr(self.request, "staff", None)
|
|
if staff:
|
|
appointments = Appointment.objects.filter(
|
|
appointment_request__staff_member=staff,
|
|
ppointment_request__date__gt=timezone.now(),
|
|
)
|
|
appointments = Appointment.objects.filter(
|
|
appointment_request__date__gt=timezone.now()
|
|
)
|
|
return apply_search_filters(appointments, query)
|
|
|
|
|
|
def apply_search_filters(queryset, query):
|
|
"""
|
|
Apply search filters to a queryset based on a query string.
|
|
|
|
This function filters the provided queryset by applying a Q object for all
|
|
CharField, TextField, or EmailField fields in the model. It checks if the
|
|
query exists within these fields using case-insensitive containment (icontains).
|
|
If the query string is empty or None, the original queryset is returned
|
|
without any modifications.
|
|
|
|
:param queryset: The initial queryset to apply the search filters on.
|
|
:param query: The search string to match against the model fields. If None
|
|
or an empty string, no filtering is applied.
|
|
:return: A filtered queryset that satisfies the search condition.
|
|
"""
|
|
if not query:
|
|
return queryset
|
|
|
|
search_filters = Q()
|
|
model = queryset.model
|
|
|
|
for field in model._meta.get_fields():
|
|
if hasattr(field, "attname") and field.get_internal_type() in [
|
|
"CharField",
|
|
"TextField",
|
|
"EmailField",
|
|
]:
|
|
search_filters |= Q(**{f"{field.name}__icontains": query})
|
|
return queryset.filter(search_filters).distinct()
|
|
|
|
|
|
class CarListViewTable(LoginRequiredMixin, ExportMixin, SingleTableView):
|
|
"""
|
|
Displays a table view of cars for a user with proper permissions.
|
|
|
|
This class-based view leverages functionality for login-required access, data
|
|
exporting, and integration with a single table representation. It is designed
|
|
to fetch and display a list of car objects related to a specific dealer in
|
|
a tabular format. The view also renders a predefined template to display
|
|
the data appropriately.
|
|
|
|
:ivar model: Specifies the model for the table view (Car).
|
|
:type model: models.Car
|
|
:ivar table_class: Represents the table class to be used for rendering
|
|
the data in tabular format.
|
|
:type table_class: tables.CarTable
|
|
:ivar template_name: Defines the template to be used for rendering the view.
|
|
:type template_name: str
|
|
"""
|
|
|
|
model = models.Car
|
|
table_class = tables.CarTable
|
|
template_name = "inventory/car_list_table.html"
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
return models.Car.objects.select_related(
|
|
"finances", "colors__exterior", "colors__interior"
|
|
).filter(dealer=dealer)
|
|
|
|
|
|
@login_required
|
|
def DealerSettingsView(request, slug):
|
|
"""
|
|
Handles dealer settings view where dealers can update their financial and
|
|
payment account settings. This view ensures validation and reassigns form
|
|
fields dynamically based on the dealer's account roles.
|
|
|
|
:param request: The HTTP request object received from the client. This parameter
|
|
contains data including HTTP headers, session, and POST data if applicable.
|
|
:type request: HttpRequest
|
|
:param pk: Primary key representing the dealer for whom the settings are being
|
|
retrieved or modified. This identifier is used to fetch or update dealer-
|
|
specific settings from the database.
|
|
:type pk: int
|
|
:return: An HTTP response rendering the dealer settings form or redirecting
|
|
to the dealer detail view after successful form submission.
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer_setting = get_object_or_404(models.DealerSettings, dealer__slug=slug)
|
|
dealer = get_user_type(request)
|
|
if request.method == "POST":
|
|
form = forms.DealerSettingsForm(request.POST, instance=dealer_setting)
|
|
if form.is_valid():
|
|
instance = form.save(commit=False)
|
|
instance.dealer = dealer
|
|
instance.save()
|
|
messages.success(request, _("Settings updated"))
|
|
return redirect("dealer_detail", slug=dealer.slug)
|
|
else:
|
|
print(form.errors)
|
|
form = forms.DealerSettingsForm(instance=dealer_setting, initial={"dealer": dealer})
|
|
form.fields[
|
|
"invoice_cash_account"
|
|
].queryset = dealer.entity.get_all_accounts().filter(role=roles.ASSET_CA_CASH)
|
|
form.fields[
|
|
"invoice_prepaid_account"
|
|
].queryset = dealer.entity.get_all_accounts().filter(
|
|
role=roles.ASSET_CA_RECEIVABLES
|
|
)
|
|
form.fields[
|
|
"invoice_unearned_account"
|
|
].queryset = dealer.entity.get_all_accounts().filter(
|
|
role=roles.LIABILITY_CL_DEFERRED_REVENUE
|
|
)
|
|
|
|
form.fields["bill_cash_account"].queryset = dealer.entity.get_all_accounts().filter(
|
|
role=roles.ASSET_CA_CASH
|
|
)
|
|
form.fields[
|
|
"bill_prepaid_account"
|
|
].queryset = dealer.entity.get_all_accounts().filter(role=roles.ASSET_CA_PREPAID)
|
|
form.fields[
|
|
"bill_unearned_account"
|
|
].queryset = dealer.entity.get_all_accounts().filter(
|
|
role=roles.LIABILITY_CL_ACC_PAYABLE
|
|
)
|
|
return render(request, "account/user_settings.html", {"form": form})
|
|
|
|
|
|
@login_required
|
|
def schedule_cancel(request,dealer_slug, pk):
|
|
"""
|
|
Cancel a schedule by updating its status to "Canceled". The function is protected
|
|
by a login requirement, ensuring only authenticated users can execute it. It
|
|
retrieves the schedule object by its primary key, updates its status, saves the
|
|
changes, and returns an HTTP response with status code 200.
|
|
|
|
:param request: The HTTP request object representing the user's request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the schedule to be canceled.
|
|
:type pk: int
|
|
:return: An HTTP response object with a 200 status code upon successful execution.
|
|
:rtype: HttpResponse
|
|
"""
|
|
get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
schedule = get_object_or_404(models.Schedule, pk=pk)
|
|
schedule.status = "Canceled"
|
|
schedule.save()
|
|
response = HttpResponse()
|
|
response.status_code = 200
|
|
return response
|
|
|
|
|
|
@login_required
|
|
def assign_car_makes(request,dealer_slug):
|
|
"""
|
|
Assigns car makes to a dealer.
|
|
|
|
This function handles both the display and processing of a form that allows
|
|
a dealer to assign or modify their associated car makes. If the request
|
|
method is POST, it validates and saves the submitted form data. If the
|
|
method is not POST, it displays a form prefilled with the existing car makes
|
|
associated with the dealer.
|
|
|
|
:param request: The HTTP request object containing information about the
|
|
current request.
|
|
:type request: HttpRequest
|
|
:return: A rendered HTML response for GET requests, or a redirect to the
|
|
dealer detail page after successful form submission.
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
if request.method == "POST":
|
|
form = forms.DealersMakeForm(request.POST, dealer=dealer)
|
|
if form.is_valid():
|
|
makes = form.cleaned_data["car_makes"]
|
|
create_accounts_for_make(dealer, makes)
|
|
form.save()
|
|
return redirect("dealer_detail", slug=dealer.slug)
|
|
else:
|
|
print(form.errors)
|
|
else:
|
|
# Pre-fill the form with existing selections
|
|
existing_car_makes = models.DealersMake.objects.filter(
|
|
dealer=dealer
|
|
).values_list("car_make", flat=True)
|
|
form = forms.DealersMakeForm(
|
|
initial={"car_makes": existing_car_makes}, dealer=dealer
|
|
)
|
|
for choice in form.fields["car_makes"].choices:
|
|
print(choice[0].instance)
|
|
break
|
|
return render(request, "dealers/assign_car_makes.html", {"form": form})
|
|
|
|
|
|
class LedgerModelListView(LoginRequiredMixin, ListView, ArchiveIndexView):
|
|
"""
|
|
Provides a view for listing ledger entries in the system.
|
|
|
|
This class-based view combines the functionality of LoginRequiredMixin,
|
|
ListView, and ArchiveIndexView to display a filtered and ordered
|
|
list of LedgerModel entries based on the user's type and entity. It
|
|
also allows toggling between different subsets of data, such as
|
|
all entries, current entries, or visible entries.
|
|
|
|
:ivar model: The model that this view displays.
|
|
:type model: type[LedgerModel]
|
|
:ivar context_object_name: The name of the context variable containing the list of LedgerModel instances.
|
|
:type context_object_name: str
|
|
:ivar template_name: The template used to render this view.
|
|
:type template_name: str
|
|
:ivar date_field: The date field used for ordering and archive functionality.
|
|
:type date_field: str
|
|
:ivar ordering: Default ordering for the queryset.
|
|
:type ordering: str
|
|
:ivar show_all: Determines whether all ledger entries should be shown.
|
|
:type show_all: bool
|
|
:ivar show_current: Determines whether only current ledger entries should be shown.
|
|
:type show_current: bool
|
|
:ivar show_visible: Determines whether only visible ledger entries should be shown.
|
|
:type show_visible: bool
|
|
:ivar allow_empty: Allows rendering of the page even if the queryset is empty.
|
|
:type allow_empty: bool
|
|
"""
|
|
|
|
model = LedgerModel
|
|
context_object_name = "ledgers"
|
|
template_name = "ledger/ledger/ledger_list.html"
|
|
date_field = "created"
|
|
ordering = "-created"
|
|
show_all = False
|
|
show_current = False
|
|
show_visible = False
|
|
allow_empty = True
|
|
paginate_by = 30
|
|
|
|
def get_queryset(self):
|
|
qs = super().get_queryset()
|
|
dealer = get_user_type(self.request)
|
|
qs = qs.filter(entity=dealer.entity)
|
|
qs = qs.select_related("billmodel", "invoicemodel")
|
|
qs = qs.order_by("-created")
|
|
if self.show_all:
|
|
return qs
|
|
if self.show_current:
|
|
qs = qs.current()
|
|
if self.show_visible:
|
|
qs = qs.visible()
|
|
return qs
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
dealer = get_user_type(self.request)
|
|
context["entity_slug"] = dealer.entity.slug
|
|
return context
|
|
|
|
|
|
# class LedgerModelDetailView(InvoiceModelDetailViewBase):
|
|
class LedgerModelDetailView(LoginRequiredMixin, DetailView):
|
|
"""
|
|
This class provides a detailed view of a specific ledger entry.
|
|
|
|
The LedgerModelDetailView is implemented as a Django DetailView, enabling it to
|
|
retrieve and display detailed information about a single ledger instance.
|
|
It ensures that only authenticated users have access to this view by using the
|
|
LoginRequiredMixin.
|
|
|
|
:ivar model: The model associated with this view, representing ledger entries.
|
|
:type model: type[LedgerModel]
|
|
:ivar context_object_name: The name of the context variable available in the
|
|
template for the detailed ledger.
|
|
:type context_object_name: str
|
|
:ivar template_name: The path to the template used to render the detailed ledger view.
|
|
:type template_name: str
|
|
"""
|
|
|
|
model = LedgerModel
|
|
context_object_name = "ledger"
|
|
template_name = "ledger/ledger/ledger_detail.html"
|
|
|
|
|
|
class LedgerModelCreateView(LedgerModelCreateViewBase):
|
|
"""
|
|
Handles the creation of LedgerModel entities.
|
|
|
|
This class provides the logic to manage the creation process of
|
|
LedgerModel instances. It determines the appropriate form for creating
|
|
a LedgerModel, validates the submitted form, and redirects the user
|
|
upon success. The view is specific to a particular user type (dealer).
|
|
|
|
:ivar template_name: Specifies the path to the template used for the
|
|
form rendering.
|
|
:type template_name: str
|
|
"""
|
|
|
|
template_name = "ledger/ledger/ledger_form.html"
|
|
|
|
def get_form(self, form_class=None):
|
|
dealer = get_user_type(self.request)
|
|
return LedgerModelCreateForm(
|
|
entity_slug=dealer.entity.slug,
|
|
user_model=dealer.entity.admin,
|
|
**self.get_form_kwargs(),
|
|
)
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
instance = form.save(commit=False)
|
|
instance.entity = dealer.entity
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse("ledger_list")
|
|
|
|
|
|
class LedgerModelModelActionView(LedgerModelModelActionViewBase):
|
|
"""
|
|
Represents a view for handling actions related to the Ledger model.
|
|
|
|
This class extends the LedgerModelModelActionViewBase and is responsible for
|
|
providing functionality to determine the redirection URL after performing
|
|
specific actions on a Ledger model instance.
|
|
|
|
:ivar template_name: Name of the template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar model: The model associated with this view.
|
|
:type model: type[Model]
|
|
"""
|
|
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
return reverse("ledger_list", args=[self.kwargs["dealer_slug"]])
|
|
|
|
|
|
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", args=[self.kwargs["dealer_slug"]])
|
|
|
|
|
|
# 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"
|
|
ordering = ["-timestamp"]
|
|
|
|
def get_queryset(self):
|
|
qs = super().get_queryset()
|
|
ledger = LedgerModel.objects.filter(pk=self.kwargs["pk"]).first()
|
|
qs = qs.filter(ledger=ledger)
|
|
return qs
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["ledger"] = LedgerModel.objects.filter(pk=self.kwargs["pk"]).first()
|
|
return context
|
|
|
|
|
|
class JournalEntryCreateView(LoginRequiredMixin, 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={"dealer_slug":self.kwargs["dealer_slug"],"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, dealer_slug,pk):
|
|
"""
|
|
Handles the retrieval and display of journal entry transactions for a specific journal
|
|
entry instance. It retrieves the journal entry and its associated transactions, ordering
|
|
the transactions by account code. The data is then rendered using the specified template.
|
|
|
|
:param request: The HTTP request object.
|
|
:type request: django.http.HttpRequest
|
|
:param pk: The primary key of the journal entry to be retrieved.
|
|
:type pk: int
|
|
:return: An HTTP response with the rendered template, including the journal entry and
|
|
its transactions.
|
|
:rtype: django.http.HttpResponse
|
|
"""
|
|
get_object_or_404(models.Dealer,slug=dealer_slug)
|
|
journal = JournalEntryModel.objects.filter(pk=pk).first()
|
|
qs = TransactionModel.objects.filter(journal_entry=journal).all()
|
|
transactions = qs.annotate(
|
|
debit_credit_sort_order=Case(
|
|
When(tx_type="debit", then=Value(0)),
|
|
When(tx_type="credit", then=Value(1)),
|
|
output_field=IntegerField(),
|
|
)
|
|
).order_by("debit_credit_sort_order")
|
|
return render(
|
|
request,
|
|
"ledger/journal_entry/journal_entry_transactions.html",
|
|
{"journal_entry": journal, "transactions": transactions},
|
|
)
|
|
|
|
|
|
class JournalEntryModelTXSDetailView(JournalEntryModelTXSDetailViewBase):
|
|
"""
|
|
Represents a detailed view of journal entry transactions in the ledger.
|
|
|
|
This class is used to render the transactions associated with a specific
|
|
journal entry. It extends the base view functionality and is tailored for a
|
|
transaction details page.
|
|
|
|
:ivar template_name: The path to the template used to render the journal
|
|
entry transactions view.
|
|
:type template_name: str
|
|
"""
|
|
|
|
template_name = "ledger/journal_entry/journal_entry_txs.html"
|
|
|
|
|
|
def ledger_lock_all_journals(request,dealer_slug, entity_slug, pk):
|
|
"""
|
|
Locks all journals associated with a specific ledger. If the ledger is already locked,
|
|
it will notify the user through an error message. Otherwise, it initiates the locking of
|
|
related journal entries, locks the ledger itself, and saves the changes to the database.
|
|
After the operation, it redirects the user to the journal entry list associated with the
|
|
ledger.
|
|
|
|
:param request: HttpRequest object representing the current request.
|
|
:type request: HttpRequest
|
|
:param entity_slug: The slug identifier of the entity.
|
|
:type entity_slug: str
|
|
:param pk: The primary key of the ledger to be locked.
|
|
:type pk: int
|
|
:return: HttpResponse redirecting to the journal entry list page of the locked ledger.
|
|
:rtype: HttpResponse
|
|
"""
|
|
get_object_or_404(models.Dealer,slug=dealer_slug)
|
|
ledger = LedgerModel.objects.filter(pk=pk).first()
|
|
if ledger.is_locked():
|
|
messages.error(request, _("Ledger is already locked"))
|
|
return redirect("journalentry_list",dealer_slug=dealer_slug, pk=ledger.pk)
|
|
ledger.lock_journal_entries()
|
|
ledger.lock()
|
|
ledger.save()
|
|
return redirect("journalentry_list", dealer_slug=dealer_slug,pk=ledger.pk)
|
|
|
|
|
|
def ledger_unlock_all_journals(request, dealer_slug, entity_slug, pk):
|
|
"""
|
|
Unlocks all journal entries associated with a specific ledger. This function first checks if the
|
|
ledger is locked. If it is already unlocked, it shows an error message and redirects the user
|
|
to the journal entry list page. If the ledger is locked, it unlocks the ledger, saves changes,
|
|
and iterates through all the locked journal entries within the ledger to unlock them as well.
|
|
Afterward, it redirects the user to the journal entry list page.
|
|
|
|
:param request: The HTTP request object containing user session and request metadata.
|
|
:type request: HttpRequest
|
|
:param entity_slug: A unique string slug representing the specific entity or organization context.
|
|
:type entity_slug: str
|
|
:param pk: The primary key of the ledger record to be unlocked.
|
|
:type pk: int
|
|
:return: A redirection to the journal entry list page for the specified ledger.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
get_object_or_404(models.Dealer,slug=dealer_slug)
|
|
ledger = LedgerModel.objects.filter(pk=pk).first()
|
|
if not ledger.is_locked():
|
|
messages.error(request, _("Ledger is already Unlocked"))
|
|
return redirect("journalentry_list",dealer_slug=dealer_slug, pk=ledger.pk)
|
|
|
|
ledger.unlock()
|
|
ledger.save()
|
|
qs = ledger.journal_entries.locked()
|
|
for je in qs:
|
|
je.unlock()
|
|
je.save()
|
|
return redirect("journalentry_list", dealer_slug=dealer_slug, pk=ledger.pk)
|
|
|
|
|
|
def ledger_post_all_journals(request, dealer_slug, entity_slug, pk):
|
|
"""
|
|
Posts all journal entries associated with a ledger. This function updates the ledger's
|
|
state to reflect that its journal entries have been posted. If the ledger is already
|
|
posted, an error message is displayed and the user is redirected.
|
|
|
|
:param request: The HTTP request object used for processing the action.
|
|
:type request: HttpRequest
|
|
:param entity_slug: A string representing the specific entity slug for the ledger context.
|
|
:type entity_slug: str
|
|
:param pk: The primary key of the ledger to be posted.
|
|
:type pk: int
|
|
:return: A redirect to the journal entry list view for the specified ledger.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
get_object_or_404(models.Dealer,slug=dealer_slug)
|
|
ledger = LedgerModel.objects.filter(pk=pk).first()
|
|
if ledger.is_posted():
|
|
messages.error(request, _("Ledger is already posted"))
|
|
return redirect("journalentry_list",dealer_slug=dealer_slug, pk=ledger.pk)
|
|
ledger.post_journal_entries()
|
|
ledger.post()
|
|
ledger.save()
|
|
return redirect("journalentry_list",dealer_slug=dealer_slug, pk=ledger.pk)
|
|
|
|
|
|
def ledger_unpost_all_journals(request,dealer_slug, entity_slug, pk):
|
|
"""
|
|
Unposts all journal entries for a specified ledger and marks the ledger as unposted.
|
|
|
|
This function identifies a ledger by its primary key (pk) and checks if it is
|
|
already posted. If the ledger is not posted, an error message is displayed.
|
|
If it is posted, the function iterates through its posted journal entries,
|
|
marks them as unposted, and saves the changes. Finally, it marks the ledger itself
|
|
as unposted and saves it.
|
|
|
|
:param request: The HTTP request object from the client.
|
|
:type request: HttpRequest
|
|
:param entity_slug: A slug identifying the entity associated with the ledger.
|
|
:type entity_slug: str
|
|
:param pk: The primary key of the ledger whose journal entries are being unposted.
|
|
:type pk: int
|
|
:return: An HTTP redirect response object directing the client to the journal entry list
|
|
page for the specified ledger.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
get_object_or_404(models.Dealer,slug=dealer_slug)
|
|
ledger = LedgerModel.objects.filter(pk=pk).first()
|
|
if not ledger.is_posted():
|
|
messages.error(request, _("Ledger is already Unposted"))
|
|
return redirect("journalentry_list",dealer_slug=dealer_slug, pk=ledger.pk)
|
|
qs = ledger.journal_entries.posted()
|
|
for je in qs:
|
|
je.mark_as_unposted()
|
|
je.save()
|
|
|
|
ledger.unpost()
|
|
ledger.save()
|
|
return redirect("journalentry_list",dealer_slug=dealer_slug, pk=ledger.pk)
|
|
|
|
|
|
def pricing_page(request,dealer_slug):
|
|
get_object_or_404(models.Dealer,slug=dealer_slug)
|
|
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_slug):
|
|
dealer = get_object_or_404(models.Dealer,slug=dealer_slug)
|
|
selected_plan_id = request.POST.get("selected_plan")
|
|
pp = PlanPricing.objects.get(pk=selected_plan_id)
|
|
|
|
order = 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_slug):
|
|
message = request.GET.get("message")
|
|
dealer = get_object_or_404(models.Dealer,slug=dealer_slug)
|
|
payment_id = request.GET.get("id")
|
|
history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first()
|
|
payment_status = request.GET.get("status")
|
|
order = Order.objects.filter(
|
|
user=dealer.user, status=AbstractOrder.STATUS.NEW
|
|
).first()
|
|
|
|
if payment_status == "paid":
|
|
billing_info, created = BillingInfo.objects.get_or_create(
|
|
user=dealer.user,
|
|
tax_number=dealer.vrn,
|
|
name=dealer.arabic_name,
|
|
street=dealer.address,
|
|
zipcode=dealer.entity.zip_code if dealer.entity.zip_code else " ",
|
|
city=dealer.entity.city if dealer.entity.city else " ",
|
|
country=dealer.entity.country if dealer.entity.country else " ",
|
|
)
|
|
if created:
|
|
userplan = UserPlan.objects.create(
|
|
user=request.user,
|
|
plan=order.plan,
|
|
active=True,
|
|
)
|
|
userplan.initialize()
|
|
|
|
order.complete_order()
|
|
history.status = "paid"
|
|
history.save()
|
|
invoice = order.get_invoices().first()
|
|
return render(
|
|
request, "payment_success.html", {"order": order, "invoice": invoice}
|
|
)
|
|
|
|
elif payment_status == "failed":
|
|
history.status = "failed"
|
|
history.save()
|
|
|
|
return render(request, "payment_failed.html", {"message": message})
|
|
|
|
|
|
def sse_stream(request):
|
|
print("hi")
|
|
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.is_read = True
|
|
notification.save()
|
|
return JsonResponse({"status": "success"})
|
|
|
|
|
|
@login_required
|
|
def mark_all_notifications_as_read(request):
|
|
models.Notification.objects.filter(user=request.user, is_read=False).update(
|
|
is_read=True
|
|
)
|
|
return redirect(request.META.get("HTTP_REFERER"))
|
|
|
|
|
|
@login_required
|
|
def notifications_history(request):
|
|
models.Notification.objects.filter(user=request.user, is_read=False).update(
|
|
read=True
|
|
)
|
|
return JsonResponse({"status": "success"})
|
|
|
|
|
|
# def activity_create(request,pk):
|
|
# lead = get_object_or_404(models.Lead, pk=pk)
|
|
# form = forms.ActivityHistoryForm()
|
|
# dealer = get_user_type(request)
|
|
# if request.method == "POST":
|
|
# form = forms.ActivityHistoryForm(request.POST)
|
|
# if form.is_valid():
|
|
# models.Activity.objects.create(
|
|
# dealer=dealer,
|
|
# activity_type=form.cleaned_data['activity_type'],
|
|
# notes=form.cleaned_data['description'],
|
|
# created_by=request.user,
|
|
# content_object=lead
|
|
# )
|
|
# return render(request, 'activity_history.html')
|
|
|
|
|
|
def add_activity(request,dealer_slug, content_type, slug):
|
|
try:
|
|
model = apps.get_model(f"inventory.{content_type}")
|
|
except LookupError:
|
|
raise Http404("Model not found")
|
|
|
|
obj = get_object_or_404(model, slug=slug)
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
if request.method == "POST":
|
|
form = forms.ActivityForm(request.POST)
|
|
if form.is_valid():
|
|
activity = form.save(commit=False)
|
|
activity.dealer = dealer
|
|
activity.content_object = obj
|
|
activity.created_by = request.user
|
|
activity.notes = form.cleaned_data["notes"]
|
|
activity.activity_type = form.cleaned_data["activity_type"]
|
|
|
|
activity.save()
|
|
messages.success(request, _("Activity added successfully"))
|
|
else:
|
|
messages.error(request, _("Activity form is not valid"))
|
|
return redirect(f"{content_type}_detail",dealer_slug=dealer_slug, slug=slug)
|
|
|
|
|
|
def add_task(request,dealer_slug, content_type, slug):
|
|
try:
|
|
model = apps.get_model(f"inventory.{content_type}")
|
|
except LookupError:
|
|
raise Http404("Model not found")
|
|
|
|
obj = get_object_or_404(model, slug=slug)
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
if request.method == "POST":
|
|
form = forms.StaffTaskForm(request.POST)
|
|
if form.is_valid():
|
|
task = form.save(commit=False)
|
|
task.dealer = dealer
|
|
task.content_object = obj
|
|
task.assigned_to = request.user
|
|
task.created_by = request.user
|
|
task.due_date = form.cleaned_data["due_date"]
|
|
task.save()
|
|
messages.success(request, _("Task added successfully"))
|
|
else:
|
|
messages.error(request, _("Task form is not valid"))
|
|
return redirect(f"{content_type}_detail",dealer_slug=dealer_slug, slug=slug)
|
|
|
|
|
|
def update_task(request,dealer_slug, pk):
|
|
task = get_object_or_404(models.Tasks, pk=pk)
|
|
|
|
if request.method == "POST":
|
|
task.completed = False if task.completed else True
|
|
task.save()
|
|
|
|
# tasks = models.Tasks.objects.filter(content_type__model=content_type, object_id=obj.id)
|
|
|
|
return render(request, "partials/task.html", {"task": task})
|
|
|
|
|
|
def add_note(request,dealer_slug, content_type, slug):
|
|
try:
|
|
model = apps.get_model(f"inventory.{content_type}")
|
|
except LookupError:
|
|
raise Http404("Model not found")
|
|
|
|
dealer = get_object_or_404(models.Dealer,slug=dealer_slug)
|
|
obj = get_object_or_404(model, slug=slug)
|
|
if request.method == "POST":
|
|
form = forms.NoteForm(request.POST)
|
|
if form.is_valid():
|
|
note = form.save(commit=False)
|
|
note.dealer = dealer
|
|
note.content_object = obj
|
|
note.created_by = request.user
|
|
|
|
note.save()
|
|
messages.success(request, _("Note added successfully"))
|
|
else:
|
|
messages.error(request, _("Note form is not valid"))
|
|
return redirect(f"{content_type}_detail",dealer_slug=dealer_slug, slug=slug)
|
|
|
|
|
|
def update_note(request,dealer_slug, pk):
|
|
note = get_object_or_404(models.Notes, pk=pk)
|
|
lead = get_object_or_404(models.Lead, pk=note.content_object.id)
|
|
dealer = get_object_or_404(models.Dealer,slug=dealer_slug)
|
|
if request.method == "POST":
|
|
note.note = request.POST.get("note")
|
|
note.save()
|
|
messages.success(request, _("Note updated successfully"))
|
|
return redirect("lead_detail",dealer_slug=dealer_slug, slug=lead.slug)
|
|
else:
|
|
messages.error(request, _("Note form is not valid"))
|
|
notes = models.Notes.objects.filter(
|
|
content_type__model="lead", object_id=lead.id, dealer=dealer
|
|
)
|
|
return render(request, "crm/leads/lead_detail.html", {"lead": lead, "notes": notes})
|
|
|
|
|
|
# Admin Management
|
|
|
|
|
|
def management_view(request,dealer_slug):
|
|
get_object_or_404(models.Dealer,slug=dealer_slug)
|
|
return render(request, "admin_management/management.html")
|
|
|
|
|
|
def user_management(request,dealer_slug):
|
|
get_object_or_404(models.Dealer,slug=dealer_slug)
|
|
context = {
|
|
"customers": models.Customer.objects.filter(active=False),
|
|
"organizations": models.Organization.objects.filter(active=False),
|
|
"vendors": models.Vendor.objects.filter(active=False),
|
|
"staff": models.Staff.objects.filter(active=False),
|
|
}
|
|
return render(request, "admin_management/user_management.html", context)
|
|
|
|
|
|
def AuditLogDashboardView(request,dealer_slug):
|
|
"""
|
|
Displays audit logs (User Actions, Login Events, Request Events) with pagination.
|
|
Log type is determined by the 'q' query parameter (e.g., ?q=userActions).
|
|
Pagination page number is passed as a query parameter (e.g., ?page=2).
|
|
"""
|
|
get_object_or_404(models.Dealer,slug=dealer_slug)
|
|
q = request.GET.get("q") # Get the log type from the 'q' query parameter
|
|
current_pagination_page = request.GET.get("page", 1)
|
|
context = {}
|
|
template_name = None
|
|
logs_per_page = 30 # Define logs per page once
|
|
|
|
# --- Determine Data Source and Template based on 'q' parameter ---
|
|
if (
|
|
q == "userRequests"
|
|
): # This block handles cases where 'q' is 'requestEvents', None, or any other invalid value.
|
|
# It defaults to Request Logs if 'q' is not 'userActions' or 'loginEvents'.
|
|
template_name = "admin_management/request_logs.html"
|
|
context["title"] = "Request Logs Dashboard"
|
|
request_events = RequestEvent.objects.all().order_by("-datetime")
|
|
paginator = Paginator(request_events, logs_per_page)
|
|
try:
|
|
page_obj = paginator.page(current_pagination_page)
|
|
except PageNotAnInteger:
|
|
page_obj = paginator.page(1)
|
|
except EmptyPage:
|
|
page_obj = paginator.page(paginator.num_pages)
|
|
|
|
elif q == "loginEvents":
|
|
template_name = "admin_management/auth_logs.html"
|
|
context["title"] = "Login Events Dashboard"
|
|
auth_events = LoginEvent.objects.all().order_by("-datetime")
|
|
paginator = Paginator(auth_events, logs_per_page)
|
|
try:
|
|
page_obj = paginator.page(current_pagination_page)
|
|
except PageNotAnInteger:
|
|
page_obj = paginator.page(1)
|
|
except EmptyPage:
|
|
page_obj = paginator.page(paginator.num_pages)
|
|
|
|
else:
|
|
template_name = "admin_management/model_logs.html"
|
|
context["title"] = "User Actions Dashboard"
|
|
|
|
# OPTIMIZATION: Get the QuerySet but don't evaluate it yet
|
|
model_events_queryset = CRUDEvent.objects.all().order_by("-datetime")
|
|
|
|
# 1. Paginate the raw QuerySet FIRST
|
|
paginator = Paginator(model_events_queryset, logs_per_page)
|
|
|
|
try:
|
|
# Get the page object, which contains only the raw QuerySet objects for the current page
|
|
page_obj_raw = paginator.page(current_pagination_page)
|
|
except PageNotAnInteger:
|
|
page_obj_raw = paginator.page(1)
|
|
except EmptyPage:
|
|
page_obj_raw = paginator.page(paginator.num_pages)
|
|
|
|
# 2. Now, process 'field_changes' ONLY for the events on the current page
|
|
processed_model_events_for_page = []
|
|
for (
|
|
event
|
|
) in page_obj_raw.object_list: # Loop only through the current page's items
|
|
event_data = {
|
|
"datetime": event.datetime,
|
|
"user": event.user,
|
|
"event_type_display": event.get_event_type_display(),
|
|
"model_name": event.content_type.model,
|
|
"object_id": event.object_id,
|
|
"object_repr": event.object_repr,
|
|
"field_changes": [],
|
|
}
|
|
|
|
if event.changed_fields:
|
|
try:
|
|
changes = json.loads(event.changed_fields)
|
|
if isinstance(changes, dict):
|
|
for field_name, values in changes.items():
|
|
old_value = (
|
|
values[0]
|
|
if isinstance(values, list) and len(values) > 0
|
|
else None
|
|
)
|
|
new_value = (
|
|
values[1]
|
|
if isinstance(values, list) and len(values) > 1
|
|
else None
|
|
)
|
|
event_data["field_changes"].append(
|
|
{
|
|
"field": field_name,
|
|
"old": old_value,
|
|
"new": new_value,
|
|
}
|
|
)
|
|
elif changes is None:
|
|
event_data["field_changes"].append(
|
|
{
|
|
"field": "Info",
|
|
"old": "",
|
|
"new": "No specific field changes recorded (JSON was null)",
|
|
}
|
|
)
|
|
else: # Handle valid JSON but not a dictionary (e.g., "[]", 123)
|
|
event_data["field_changes"].append(
|
|
{
|
|
"field": "Error",
|
|
"old": "",
|
|
"new": f"Unexpected JSON format: {type(changes).__name__}",
|
|
}
|
|
)
|
|
except json.JSONDecodeError:
|
|
# Handle invalid JSON; you might log this error
|
|
event_data["field_changes"].append(
|
|
{
|
|
"field": "Error",
|
|
"old": "",
|
|
"new": "Invalid JSON in changed_fields",
|
|
}
|
|
)
|
|
processed_model_events_for_page.append(event_data)
|
|
|
|
# 3. Replace the object_list of the original page_obj with the processed data
|
|
# This keeps all pagination properties (has_next, number, etc.) intact.
|
|
page_obj_raw.object_list = processed_model_events_for_page
|
|
page_obj = page_obj_raw # This will be passed to the context
|
|
|
|
# Pass the final page object to the context
|
|
context["page_obj"] = page_obj
|
|
|
|
return render(request, template_name, context)
|
|
|
|
|
|
def activate_account(request,dealer_slug, content_type, slug):
|
|
get_object_or_404(models.Dealer,slug=dealer_slug)
|
|
try:
|
|
model = apps.get_model(f"inventory.{content_type}")
|
|
except LookupError:
|
|
raise Http404("Model not found")
|
|
|
|
obj = get_object_or_404(model, slug=slug)
|
|
if request.method == "POST":
|
|
obj.activate_account()
|
|
messages.success(request, _("Account activated successfully"))
|
|
return redirect("user_management", dealer_slug=dealer_slug)
|
|
return render(
|
|
request, "admin_management/confirm_activate_account.html", {"obj": obj}
|
|
)
|
|
|
|
|
|
def permenant_delete_account(request,dealer_slug, content_type, slug):
|
|
get_object_or_404(models.Dealer,slug=dealer_slug)
|
|
try:
|
|
model = apps.get_model(f"inventory.{content_type}")
|
|
except LookupError:
|
|
raise Http404("Model not found")
|
|
|
|
obj = get_object_or_404(model, slug=slug)
|
|
if request.method == "POST":
|
|
try:
|
|
obj.permenant_delete()
|
|
messages.success(request, _("Account Deleted successfully"))
|
|
except RestrictedError:
|
|
messages.error(
|
|
request,
|
|
_("You cannot delete this account,it is related to another account"),
|
|
)
|
|
except Exception as e:
|
|
messages.error(request, _(f"Error deleting account: {e}"))
|
|
return redirect("user_management", dealer_slug=dealer_slug)
|
|
return render(
|
|
request, "admin_management/permenant_delete_account.html", {"obj": obj}
|
|
)
|
|
|
|
|
|
#####################################################################
|
|
|
|
|
|
def PurchaseOrderCreateView(request, dealer_slug):
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
entity = dealer.entity
|
|
if request.method == "POST":
|
|
po = entity.create_purchase_order(po_title=request.POST.get("po_title"))
|
|
po.entity = entity
|
|
po.save()
|
|
messages.success(request, _("Purchase order created successfully"))
|
|
return redirect("purchase_order_detail", dealer_slug=dealer.slug, pk=po.pk)
|
|
|
|
form = PurchaseOrderModelCreateForm(
|
|
entity_slug=entity.slug, user_model=entity.admin
|
|
)
|
|
return render(request, "purchase_orders/po_form.html", {"form": form})
|
|
|
|
|
|
def InventoryItemCreateView(request, dealer_slug):
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
for_po = request.GET.get("for_po")
|
|
entity = dealer.entity
|
|
coa = entity.get_default_coa()
|
|
|
|
inventory_accounts = entity.get_coa_accounts().filter(role="asset_ca_inv")
|
|
cogs_accounts = entity.get_coa_accounts().filter(role="cogs_regular")
|
|
|
|
if request.method == "POST":
|
|
response = HttpResponse()
|
|
response["HX-Refresh"] = "true"
|
|
|
|
name = request.POST.get("name")
|
|
account = request.POST.get("account")
|
|
account = inventory_accounts.get(pk=account)
|
|
inventory_name = None
|
|
if name:
|
|
inventory_name = name
|
|
else:
|
|
make = request.POST.get("make")
|
|
model = request.POST.get("model")
|
|
serie = request.POST.get("serie")
|
|
trim = request.POST.get("trim")
|
|
year = request.POST.get("year")
|
|
exterior = models.ExteriorColors.objects.get(
|
|
pk=request.POST.get("exterior")
|
|
)
|
|
interior = models.InteriorColors.objects.get(
|
|
pk=request.POST.get("interior")
|
|
)
|
|
|
|
make_name = models.CarMake.objects.get(pk=make)
|
|
model_name = models.CarModel.objects.get(pk=model)
|
|
serie_name = models.CarSerie.objects.get(pk=serie)
|
|
trim_name = models.CarTrim.objects.get(pk=trim)
|
|
|
|
inventory_name = f"{make_name.name} || {model_name.name} || {serie_name.name} || {trim_name.name} || {year} || {exterior.name} || {interior.name}"
|
|
if (
|
|
inventory := entity.get_items_inventory()
|
|
.filter(name=inventory_name)
|
|
.first()
|
|
):
|
|
messages.error(request, _("Inventory item already exists"))
|
|
return response
|
|
# return redirect(
|
|
# f"{reverse('inventory_item_create')}?for_po={for_po}",
|
|
# dealer_slug=dealer.slug,
|
|
# )
|
|
uom = entity.get_uom_all().get(name="Unit")
|
|
entity.create_item_inventory(
|
|
name=inventory_name,
|
|
uom_model=uom,
|
|
item_type=ItemModel.ITEM_TYPE_MATERIAL,
|
|
inventory_account=account,
|
|
coa_model=coa,
|
|
)
|
|
messages.success(request, _("Inventory item created successfully"))
|
|
return response
|
|
# return redirect("purchase_order_list", dealer_slug=dealer.slug)
|
|
if for_po:
|
|
form = forms.CSVUploadForm()
|
|
form.fields["vendor"].queryset = dealer.vendors.filter(active=True)
|
|
context = {
|
|
"make_data": models.CarMake.objects.filter(is_sa_import=True),
|
|
"inventory_accounts": inventory_accounts,
|
|
"cogs_accounts": cogs_accounts,
|
|
"form": form,
|
|
}
|
|
return render(request, "purchase_orders/car_inventory_item_form.html", context)
|
|
return render(
|
|
request,
|
|
"purchase_orders/inventory_item_form.html",
|
|
{
|
|
"make_data": models.CarMake.objects.filter(is_sa_import=True),
|
|
"inventory_accounts": inventory_accounts,
|
|
"cogs_accounts": cogs_accounts,
|
|
},
|
|
)
|
|
|
|
|
|
def inventory_items_filter(request, dealer_slug):
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
make = request.GET.get("make")
|
|
model = request.GET.get("model")
|
|
serie = request.GET.get("serie")
|
|
|
|
model_data = models.CarModel.objects.none()
|
|
serie_data = models.CarSerie.objects.none()
|
|
trim_data = models.CarTrim.objects.none()
|
|
if make:
|
|
make = models.CarMake.objects.get(pk=make)
|
|
model_data = make.carmodel_set.all()
|
|
elif model:
|
|
model = models.CarModel.objects.get(pk=model)
|
|
serie_data = model.carserie_set.all()
|
|
elif serie:
|
|
serie = models.CarSerie.objects.get(pk=serie)
|
|
trim_data = serie.cartrim_set.all()
|
|
context = {
|
|
"model_data": model_data,
|
|
"serie_data": serie_data,
|
|
"trim_data": trim_data,
|
|
# 'inventory_items': dealer.entity.get_items_inventory(),
|
|
# 'entity_slug': dealer.entity.slug,
|
|
}
|
|
return render(request, "purchase_orders/car_inventory_item_form.html", context)
|
|
|
|
|
|
class PurchaseOrderDetailView(PurchaseOrderModelDetailViewBase):
|
|
template_name = "purchase_orders/po_detail.html"
|
|
context_object_name = "po_model"
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
self.queryset = PurchaseOrderModel.objects.for_entity(
|
|
entity_slug=dealer.entity.slug, user_model=dealer.entity.admin
|
|
).select_related("entity", "ce_model")
|
|
return super().get_queryset()
|
|
|
|
def get_context_data(self, **kwargs):
|
|
dealer = get_user_type(self.request)
|
|
context = super().get_context_data(**kwargs)
|
|
context["entity_slug"] = dealer.entity.slug
|
|
return context
|
|
|
|
|
|
class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
model = PurchaseOrderModel
|
|
context_object_name = "purchase_orders"
|
|
paginate_by = 20
|
|
template_name = "purchase_orders/po_list.html"
|
|
permission_required = ["inventory.view_carfinance"]
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
entity = dealer.entity
|
|
return self.model.objects.filter(entity=entity)
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
entity = dealer.entity
|
|
queryset = self.model.objects.filter(entity=entity)
|
|
|
|
query = self.request.GET.get('q') # This is generic: looks for 'q' from GET
|
|
|
|
if query:
|
|
# Start with an empty Q object for the search filters
|
|
search_filters = Q()
|
|
|
|
# 1. Try to parse the query as a date
|
|
parsed_date = None
|
|
date_formats = [
|
|
'%Y-%m-%d', # 2023-10-26
|
|
'%m/%d/%Y', # 10/26/2023
|
|
'%d-%m-%Y', # 26-10-2023
|
|
'%B %d, %Y', # October 26, 2023
|
|
'%b %d, %Y', # Oct 26, 2023
|
|
'%Y/%m/%d', # 2023/10/26
|
|
'%Y-%m', # 2023-10 (for year-month search)
|
|
'%Y',
|
|
'%b %d',
|
|
'%B %d' # 2023 (for year search)
|
|
]
|
|
|
|
for fmt in date_formats:
|
|
try:
|
|
# For '%Y-%m' and '%Y', we only care about year/month, not exact day
|
|
if fmt == '%Y-%m':
|
|
parsed_date = datetime.strptime(query, fmt)
|
|
search_filters |= Q(created__year=parsed_date.year, created__month=parsed_date.month)
|
|
break
|
|
elif fmt == '%Y':
|
|
parsed_date = datetime.strptime(query, fmt)
|
|
search_filters |= Q(created__year=parsed_date.year)
|
|
break
|
|
else:
|
|
parsed_date = datetime.strptime(query, fmt).date()
|
|
search_filters |= Q(created__date=parsed_date) # Matches exact date part of datetime field
|
|
break # Found a match, no need to try other formats
|
|
except ValueError:
|
|
continue # Try next format
|
|
|
|
# 2. Add text-based search filters (always apply these)
|
|
# Combine them with OR operator
|
|
text_filters = (
|
|
Q(po_number__icontains=query) |
|
|
Q(po_title__icontains=query) |
|
|
Q(po_status__icontains=query) |
|
|
Q(created__icontains=query)
|
|
|
|
)
|
|
|
|
# If a date was successfully parsed, combine with text filters
|
|
if parsed_date:
|
|
# Use a combined Q object. This means it will search for
|
|
# (date_match OR po_number_match OR po_title_match)
|
|
queryset = queryset.filter(search_filters | text_filters).distinct()
|
|
else:
|
|
# If no date was parsed, only apply text filters
|
|
queryset = queryset.filter(text_filters).distinct()
|
|
|
|
return queryset
|
|
|
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
dealer = get_user_type(self.request)
|
|
context = super().get_context_data(**kwargs)
|
|
context["entity_slug"] = dealer.entity.slug
|
|
return context
|
|
|
|
|
|
class PurchaseOrderUpdateView(PurchaseOrderModelUpdateViewBase):
|
|
template_name = "purchase_orders/po_update.html"
|
|
context_object_name = "po_model"
|
|
|
|
def get_context_data(self, itemtxs_formset=None, **kwargs):
|
|
dealer = self.request.dealer
|
|
context = super().get_context_data(**kwargs)
|
|
context["entity_slug"] = dealer.entity.slug
|
|
po_model: PurchaseOrderModel = self.object
|
|
if not itemtxs_formset:
|
|
itemtxs_qs = self.get_po_itemtxs_qs(po_model)
|
|
itemtxs_qs, itemtxs_agg = po_model.get_itemtxs_data(queryset=itemtxs_qs)
|
|
po_itemtxs_formset_class = get_po_itemtxs_formset_class(po_model)
|
|
itemtxs_formset = po_itemtxs_formset_class(
|
|
entity_slug=dealer.entity.slug,
|
|
user_model=dealer.entity.admin,
|
|
po_model=po_model,
|
|
queryset=itemtxs_qs,
|
|
)
|
|
else:
|
|
itemtxs_qs, itemtxs_agg = po_model.get_itemtxs_data()
|
|
|
|
context["itemtxs_qs"] = itemtxs_qs
|
|
context["itemtxs_formset"] = itemtxs_formset
|
|
return context
|
|
|
|
def get_success_url(self):
|
|
return reverse(
|
|
"purchase_order_update",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"entity_slug": self.kwargs["entity_slug"],
|
|
"po_pk": self.kwargs["po_pk"],
|
|
},
|
|
)
|
|
|
|
def get(self, request, dealer_slug, entity_slug, po_pk, *args, **kwargs):
|
|
if self.action_update_items:
|
|
return HttpResponseRedirect(
|
|
redirect_to=reverse(
|
|
"purchase_order_update",
|
|
kwargs={
|
|
"dealer_slug": dealer_slug,
|
|
"entity_slug": entity_slug,
|
|
"po_pk": po_pk,
|
|
},
|
|
)
|
|
)
|
|
return super(PurchaseOrderUpdateView, self).get(
|
|
request, dealer_slug, entity_slug, po_pk, *args, **kwargs
|
|
)
|
|
|
|
def post(self, request, dealer_slug, entity_slug, *args, **kwargs):
|
|
if self.action_update_items:
|
|
if not request.user.is_authenticated:
|
|
return HttpResponseForbidden()
|
|
queryset = self.get_queryset()
|
|
po_model: PurchaseOrderModel = self.get_object(queryset=queryset)
|
|
self.object = po_model
|
|
po_itemtxs_formset_class = get_po_itemtxs_formset_class(po_model)
|
|
itemtxs_formset = po_itemtxs_formset_class(
|
|
request.POST,
|
|
user_model=request.dealer.entity.admin,
|
|
po_model=po_model,
|
|
entity_slug=entity_slug,
|
|
)
|
|
|
|
if itemtxs_formset.has_changed():
|
|
if itemtxs_formset.is_valid():
|
|
itemtxs_list = itemtxs_formset.save(commit=False)
|
|
create_bill_uuids = [
|
|
str(i["uuid"].uuid)
|
|
for i in itemtxs_formset.cleaned_data
|
|
if i and i["create_bill"] is True
|
|
]
|
|
|
|
if create_bill_uuids:
|
|
item_uuids = ",".join(create_bill_uuids)
|
|
redirect_url = reverse(
|
|
"bill-create-po",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"entity_slug": self.kwargs["entity_slug"],
|
|
"po_pk": po_model.uuid,
|
|
},
|
|
)
|
|
redirect_url += f"?item_uuids={item_uuids}"
|
|
return HttpResponseRedirect(redirect_url)
|
|
|
|
for itemtxs in itemtxs_list:
|
|
if not itemtxs.po_model_id:
|
|
itemtxs.po_model_id = po_model.uuid
|
|
itemtxs.clean()
|
|
|
|
itemtxs_list = itemtxs_formset.save()
|
|
po_model.update_state()
|
|
po_model.clean()
|
|
po_model.save(
|
|
update_fields=["po_amount", "po_amount_received", "updated"]
|
|
)
|
|
# if valid get saved formset from DB
|
|
messages.add_message(
|
|
request, messages.SUCCESS, "PO items updated successfully."
|
|
)
|
|
return self.render_to_response(context=self.get_context_data())
|
|
# if not valid, return formset with errors...
|
|
return self.render_to_response(
|
|
context=self.get_context_data(itemtxs_formset=itemtxs_formset)
|
|
)
|
|
return super(PurchaseOrderUpdateView, self).post(
|
|
request, dealer_slug, entity_slug, *args, **kwargs
|
|
)
|
|
|
|
def get_form(self, form_class=None):
|
|
po_model: PurchaseOrderModel = self.object
|
|
if po_model.is_draft():
|
|
return DraftPurchaseOrderModelUpdateForm(
|
|
entity_slug=self.kwargs["entity_slug"],
|
|
user_model=self.request.user,
|
|
**self.get_form_kwargs(),
|
|
)
|
|
elif po_model.is_review():
|
|
return ReviewPurchaseOrderModelUpdateForm(
|
|
entity_slug=self.kwargs["entity_slug"],
|
|
user_model=self.request.user,
|
|
**self.get_form_kwargs(),
|
|
)
|
|
elif po_model.is_approved():
|
|
return ApprovedPurchaseOrderModelUpdateForm(
|
|
entity_slug=self.kwargs["entity_slug"],
|
|
user_model=self.request.user,
|
|
**self.get_form_kwargs(),
|
|
)
|
|
return BasePurchaseOrderModelUpdateForm(
|
|
entity_slug=self.kwargs["entity_slug"],
|
|
user_model=self.request.user,
|
|
**self.get_form_kwargs(),
|
|
)
|
|
|
|
|
|
class BasePurchaseOrderActionActionView(BasePurchaseOrderActionActionViewBase):
|
|
def get_redirect_url(self, dealer_slug, entity_slug, po_pk, *args, **kwargs):
|
|
return reverse(
|
|
"purchase_order_update",
|
|
kwargs={
|
|
"dealer_slug": dealer_slug,
|
|
"entity_slug": entity_slug,
|
|
"po_pk": po_pk,
|
|
},
|
|
)
|
|
|
|
def get(self, request, dealer_slug, entity_slug, po_pk, *args, **kwargs):
|
|
# kwargs["user_model"] = self.request.user
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
kwargs["user_model"] = dealer.entity.admin
|
|
if not self.action_name:
|
|
raise ImproperlyConfigured("View attribute action_name is required.")
|
|
response = super(BasePurchaseOrderActionActionView, self).get(
|
|
request, dealer_slug, entity_slug, po_pk, *args, **kwargs
|
|
)
|
|
po_model: PurchaseOrderModel = self.get_object()
|
|
|
|
try:
|
|
getattr(po_model, self.action_name)(commit=self.commit, **kwargs)
|
|
messages.add_message(
|
|
request,
|
|
message="PO updated successfully.",
|
|
level=messages.SUCCESS,
|
|
)
|
|
except ValidationError as e:
|
|
print(e)
|
|
return response
|
|
|
|
|
|
class PurchaseOrderModelDeleteView(PurchaseOrderModelDeleteViewBase):
|
|
template_name = "purchase_orders/po_delete.html"
|
|
|
|
def get_success_url(self):
|
|
messages.add_message(
|
|
self.request,
|
|
message="PO deleted successfully.",
|
|
level=messages.SUCCESS,
|
|
)
|
|
return reverse(
|
|
"purchase_order_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
|
|
)
|
|
|
|
|
|
class PurchaseOrderMarkAsDraftView(BasePurchaseOrderActionActionView):
|
|
action_name = "mark_as_draft"
|
|
|
|
|
|
class PurchaseOrderMarkAsReviewView(BasePurchaseOrderActionActionView):
|
|
action_name = "mark_as_review"
|
|
|
|
|
|
class PurchaseOrderMarkAsApprovedView(BasePurchaseOrderActionActionView):
|
|
action_name = "mark_as_approved"
|
|
|
|
|
|
class PurchaseOrderMarkAsFulfilledView(BasePurchaseOrderActionActionView):
|
|
action_name = "mark_as_fulfilled"
|
|
|
|
class PurchaseOrderMarkAsCanceledView(BasePurchaseOrderActionActionView):
|
|
action_name = "mark_as_canceled"
|
|
|
|
|
|
class PurchaseOrderMarkAsVoidView(BasePurchaseOrderActionActionView):
|
|
action_name = "mark_as_void"
|
|
|
|
|
|
##############################bil
|
|
class BaseBillActionView(BaseBillActionViewBase):
|
|
def get_redirect_url(self, dealer_slug, entity_slug, bill_pk, *args, **kwargs):
|
|
return reverse(
|
|
"bill-update",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"entity_slug": entity_slug,
|
|
"bill_pk": bill_pk,
|
|
},
|
|
)
|
|
|
|
|
|
class BillModelActionMarkAsDraftView(BaseBillActionView):
|
|
action_name = "mark_as_draft"
|
|
|
|
|
|
class BillModelActionMarkAsInReviewView(BaseBillActionView):
|
|
action_name = "mark_as_review"
|
|
|
|
|
|
class BillModelActionMarkAsApprovedView(BaseBillActionView):
|
|
action_name = "mark_as_approved"
|
|
|
|
|
|
class BillModelActionMarkAsPaidView(BaseBillActionView):
|
|
action_name = "mark_as_paid"
|
|
|
|
|
|
class BillModelActionDeleteView(BaseBillActionView):
|
|
action_name = "mark_as_delete"
|
|
|
|
|
|
class BillModelActionVoidView(BaseBillActionView):
|
|
action_name = "mark_as_void"
|
|
|
|
|
|
class BillModelActionCanceledView(BaseBillActionView):
|
|
action_name = "mark_as_canceled"
|
|
|
|
|
|
class BillModelActionLockLedgerView(BaseBillActionView):
|
|
action_name = "lock_ledger"
|
|
|
|
|
|
class BillModelActionUnlockLedgerView(BaseBillActionView):
|
|
action_name = "unlock_ledger"
|
|
|
|
|
|
class BillModelActionForceMigrateView(BaseBillActionView):
|
|
action_name = "migrate_state"
|
|
|
|
|
|
###############################################################
|
|
###############################################################
|
|
|
|
|
|
def view_items_inventory(request, dealer_slug, entity_slug, po_pk):
|
|
get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
po = PurchaseOrderModel.objects.get(pk=po_pk)
|
|
items = po.items.all()
|
|
|
|
return render(request,"purchase_orders/po_upload_cars.html",{"po": po, "items": items})
|
|
|
|
|
|
def upload_cars(request, dealer_slug, pk=None):
|
|
item = None
|
|
po_item = None
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
response = redirect("upload_cars", dealer_slug=dealer_slug)
|
|
if pk:
|
|
item = get_object_or_404(ItemTransactionModel, pk=pk)
|
|
po_item = models.PoItemsUploaded.objects.get(dealer=dealer, item=item)
|
|
response = redirect("upload_cars", dealer_slug=dealer_slug, pk=pk)
|
|
if po_item.status == "uploaded":
|
|
messages.add_message(request, messages.ERROR, "Item already uploaded.")
|
|
return redirect(
|
|
"view_items_inventory",
|
|
dealer_slug=dealer_slug,
|
|
entity_slug=dealer.entity.slug,
|
|
po_pk=item.po_model.pk,
|
|
)
|
|
|
|
if request.method == "POST":
|
|
csv_file = request.FILES.get("csv_file")
|
|
|
|
try:
|
|
if item:
|
|
data = [x.strip() for x in item.item_model.name.split("||")]
|
|
make = models.CarMake.objects.filter(is_sa_import=True).get(
|
|
name=data[0]
|
|
)
|
|
model = make.carmodel_set.get(name=data[1])
|
|
trim = models.CarTrim.objects.filter(
|
|
name=data[3], id_car_serie__id_car_model=model.id_car_model
|
|
).first()
|
|
serie = trim.id_car_serie
|
|
year = data[4]
|
|
exterior = models.ExteriorColors.objects.get(name=data[5])
|
|
interior = models.InteriorColors.objects.get(name=data[6])
|
|
receiving_date = timezone.now()
|
|
vendor_model = item.bill_model.vendor
|
|
vendor = models.Vendor.objects.get(vendor_model=vendor_model)
|
|
else:
|
|
make = models.CarMake.objects.get(pk=request.POST.get("make"))
|
|
model = models.CarModel.objects.get(pk=request.POST.get("model"))
|
|
serie = models.CarSerie.objects.get(pk=request.POST.get("serie"))
|
|
trim = models.CarTrim.objects.get(pk=request.POST.get("trim"))
|
|
exterior = models.ExteriorColors.objects.get(
|
|
pk=request.POST.get("exterior")
|
|
)
|
|
interior = models.InteriorColors.objects.get(
|
|
pk=request.POST.get("interior")
|
|
)
|
|
year = request.POST.get("year")
|
|
receiving_date = datetime.strptime(
|
|
request.POST.get("receiving_date"), "%Y-%m-%d"
|
|
)
|
|
vendor = models.Vendor.objects.get(pk=request.POST.get("vendor"))
|
|
|
|
except Exception as e:
|
|
messages.error(request, f"Error processing CSV: {str(e)}")
|
|
return response
|
|
|
|
if not csv_file.name.endswith(".csv"):
|
|
messages.error(request, "Please upload a CSV file")
|
|
return response
|
|
try:
|
|
# Read the file content
|
|
file_content = csv_file.read().decode("utf-8")
|
|
csv_data = io.StringIO(file_content)
|
|
reader = csv.DictReader(csv_data)
|
|
data = [x for x in reader]
|
|
for row in data:
|
|
if result := decodevin(row["vin"]):
|
|
if models.Car.objects.filter(vin=row["vin"]).exists():
|
|
messages.error(request, f"vin {row['vin']} already exists")
|
|
return response
|
|
manufacturer_name, model_name, year_model = result.values()
|
|
car_make = get_make(manufacturer_name)
|
|
car_model = get_model(model_name, car_make)
|
|
if (
|
|
not all([car_make, car_model])
|
|
or (make.pk != car_make.pk)
|
|
or (model.pk != car_model.pk)
|
|
):
|
|
messages.error(
|
|
request,
|
|
f"invalid data at vin {row['vin']}, Please upload a valid CSV file",
|
|
)
|
|
return response
|
|
|
|
cars_created = 0
|
|
for row in data:
|
|
car = models.Car.objects.create(
|
|
dealer=dealer,
|
|
vin=row["vin"],
|
|
id_car_make=make,
|
|
id_car_model=model,
|
|
id_car_serie=serie,
|
|
id_car_trim=trim,
|
|
year=int(year_model),
|
|
vendor=vendor,
|
|
receiving_date=receiving_date,
|
|
)
|
|
car.add_colors(exterior=exterior, interior=interior)
|
|
cars_created += 1
|
|
if po_item:
|
|
po_item.status = "uploaded"
|
|
po_item.save()
|
|
|
|
messages.success(request, f"Successfully imported {cars_created} cars")
|
|
return redirect(
|
|
"view_items_inventory",
|
|
dealer_slug=dealer_slug,
|
|
slug_entity=dealer.entity.slug,
|
|
po_pk=item.po_model.pk,)
|
|
|
|
except Exception as e:
|
|
messages.error(request, f"Error processing CSV: {str(e)}")
|
|
return response
|
|
|
|
form = forms.CSVUploadForm()
|
|
form.fields["vendor"].queryset = dealer.vendors.all()
|
|
|
|
return render(
|
|
request,
|
|
"csv_upload.html",
|
|
{"make_data": models.CarMake.objects.filter(is_sa_import=True), "form": form, "item": item},
|
|
)
|
|
|
|
|
|
###############################################################
|
|
###############################################################
|
|
|
|
|
|
@require_http_methods(["POST"])
|
|
def bulk_update_car_price(request):
|
|
if request.method == "POST":
|
|
cars = request.POST.getlist("car")
|
|
price = request.POST.get("price")
|
|
|
|
if not price or int(price) <= 0:
|
|
messages.error(request, "Please enter a valid price")
|
|
elif not cars:
|
|
messages.error(request, "No cars selected for price update")
|
|
else:
|
|
for car_pk in cars:
|
|
car = models.Car.objects.get(pk=car_pk)
|
|
if not hasattr(car, "finances"):
|
|
models.CarFinance.objects.create(
|
|
car=car, cost_price=Decimal(price), selling_price=0
|
|
)
|
|
else:
|
|
car.finances.cost_price = Decimal(price)
|
|
car.finances.selling_price = 0
|
|
car.finances.save()
|
|
messages.success(request, "Price updated successfully")
|
|
|
|
response = HttpResponse()
|
|
response["HX-Redirect"] = reverse("car_list")
|
|
return response
|
|
|
|
|
|
class InventoryListView(InventoryListViewBase):
|
|
template_name = "inventory/list.html"
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
|
|
if self.queryset is None:
|
|
self.queryset = ItemTransactionModel.objects.inventory_pipeline_aggregate(
|
|
entity_slug=dealer.entity.slug,
|
|
)
|
|
return super().get_queryset()
|