12628 lines
505 KiB
Python
12628 lines
505 KiB
Python
# Standard
|
|
from django.core.files.base import ContentFile
|
|
import base64
|
|
import os
|
|
import re
|
|
import io
|
|
import csv
|
|
import cv2
|
|
import json
|
|
import logging
|
|
import tempfile
|
|
import numpy as np
|
|
from time import sleep
|
|
|
|
from weasyprint import HTML
|
|
|
|
# from rich import print
|
|
from random import randint
|
|
from decimal import Decimal
|
|
from io import TextIOWrapper
|
|
from django.apps import apps
|
|
from datetime import datetime, timedelta, date
|
|
from calendar import month_name
|
|
from pyzbar.pyzbar import decode
|
|
from urllib.parse import urlparse, urlunparse
|
|
|
|
#####################################################################
|
|
from inventory.mixins import AuthorizedEntityMixin, DealerSlugMixin
|
|
from inventory.models import Status as LeadStatus
|
|
from django.db import IntegrityError
|
|
from django.views.generic import FormView
|
|
from django.views.decorators.http import require_http_methods
|
|
from django.db.models.deletion import RestrictedError
|
|
from django.http.response import StreamingHttpResponse
|
|
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
|
from django.core.exceptions import PermissionDenied
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.views.decorators.http import require_POST
|
|
from django.template.loader import render_to_string
|
|
|
|
# Django
|
|
from django.db.models import Q
|
|
from django.conf import settings
|
|
from django.db.models import Func
|
|
from django.contrib import messages
|
|
from django.http import (
|
|
Http404,
|
|
HttpResponseBadRequest,
|
|
HttpResponseNotFound,
|
|
HttpResponseRedirect,
|
|
JsonResponse,
|
|
HttpResponseForbidden,
|
|
)
|
|
from django.forms import CharField, HiddenInput, ValidationError
|
|
from django.shortcuts import HttpResponse
|
|
|
|
from django.db.models import Sum, F, Count
|
|
from django.db.models.functions import ExtractMonth, Round
|
|
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
|
from django.contrib.auth.models import User, Group
|
|
from django.db.models import Value
|
|
from django.urls import reverse, reverse_lazy
|
|
from django.utils import timezone, translation
|
|
from django.db.models.functions import Coalesce
|
|
from django.contrib.auth.models import Permission
|
|
from django.utils.decorators import method_decorator
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
from django.views.generic import DetailView, RedirectView
|
|
from django.contrib.messages.views import SuccessMessageMixin
|
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
|
from django.contrib.auth.decorators import permission_required
|
|
from django.shortcuts import render, get_object_or_404, redirect
|
|
from plans.models import Plan, Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo
|
|
from django.views.generic import (
|
|
View,
|
|
ListView,
|
|
CreateView,
|
|
UpdateView,
|
|
DeleteView,
|
|
TemplateView,
|
|
ArchiveIndexView,
|
|
)
|
|
|
|
from django.db.models import Case, Value, IntegerField, When
|
|
|
|
# Django Ledger
|
|
from django_ledger.io import roles
|
|
from django_ledger.utils import accruable_net_summary
|
|
from django_ledger.views import (
|
|
JournalEntryModelTXSDetailView as JournalEntryModelTXSDetailViewBase,
|
|
LedgerModelModelActionView as LedgerModelModelActionViewBase,
|
|
LedgerModelDeleteView as LedgerModelDeleteViewBase,
|
|
LedgerModelCreateView as LedgerModelCreateViewBase,
|
|
)
|
|
from django_ledger.forms.account import AccountModelCreateForm, AccountModelUpdateForm
|
|
from django_ledger.views.entity import (
|
|
EntityModelDetailBaseView,
|
|
EntityModelDetailHandlerView,
|
|
)
|
|
from django_ledger.forms.ledger import LedgerModelCreateForm
|
|
from django_ledger.forms.item import (
|
|
ExpenseItemCreateForm,
|
|
ExpenseItemUpdateForm,
|
|
)
|
|
from django_ledger.forms.bank_account import (
|
|
BankAccountCreateForm,
|
|
BankAccountUpdateForm,
|
|
)
|
|
from django_ledger.views.chart_of_accounts import (
|
|
ChartOfAccountModelListView as ChartOfAccountModelListViewBase,
|
|
)
|
|
from django_ledger.views.bill import (
|
|
BillModelCreateView,
|
|
BillModelDetailView,
|
|
BillModelUpdateView,
|
|
BaseBillActionView as BaseBillActionViewBase,
|
|
BillModelModelBaseView,
|
|
)
|
|
from django_ledger.forms.bill import (
|
|
ApprovedBillModelUpdateForm,
|
|
InReviewBillModelUpdateForm,
|
|
get_bill_itemtxs_formset_class,
|
|
BillModelCreateForm,
|
|
)
|
|
from django_ledger.forms.invoice import (
|
|
DraftInvoiceModelUpdateForm,
|
|
ApprovedInvoiceModelUpdateForm,
|
|
PaidInvoiceModelUpdateForm,
|
|
)
|
|
from django_ledger.forms.item import (
|
|
InventoryItemCreateForm,
|
|
)
|
|
|
|
from django_ledger.forms.purchase_order import (
|
|
PurchaseOrderModelCreateForm,
|
|
BasePurchaseOrderModelUpdateForm,
|
|
DraftPurchaseOrderModelUpdateForm,
|
|
ReviewPurchaseOrderModelUpdateForm,
|
|
ApprovedPurchaseOrderModelUpdateForm,
|
|
get_po_itemtxs_formset_class,
|
|
)
|
|
from django_ledger.views.purchase_order import (
|
|
PurchaseOrderModelDetailView as PurchaseOrderModelDetailViewBase,
|
|
# PurchaseOrderModelUpdateView as PurchaseOrderModelUpdateViewBase,
|
|
# BasePurchaseOrderActionActionView as BasePurchaseOrderActionActionViewBase,
|
|
PurchaseOrderModelDeleteView as PurchaseOrderModelDeleteViewBase,
|
|
)
|
|
from .override import (
|
|
PurchaseOrderModelUpdateView as PurchaseOrderModelUpdateViewBase,
|
|
BasePurchaseOrderActionActionView as BasePurchaseOrderActionActionViewBase,
|
|
BillModelDetailView as BillModelDetailViewBase,
|
|
BillModelUpdateView as BillModelUpdateViewBase,
|
|
BaseBillActionView as BaseBillActionViewBase,
|
|
InventoryListView as InventoryListViewBase,
|
|
InvoiceModelUpdateView as InvoiceModelUpdateViewBase,
|
|
ChartOfAccountModelCreateView as ChartOfAccountModelCreateViewBase,
|
|
ChartOfAccountModelListView as ChartOfAccountModelListViewBase,
|
|
ChartOfAccountModelUpdateView as ChartOfAccountModelUpdateViewBase,
|
|
CharOfAccountModelActionView as CharOfAccountModelActionViewBase,
|
|
)
|
|
|
|
from django_ledger.models import (
|
|
ItemTransactionModel,
|
|
EntityModel,
|
|
InvoiceModel,
|
|
BankAccountModel,
|
|
AccountModel,
|
|
JournalEntryModel,
|
|
TransactionModel,
|
|
EstimateModel,
|
|
CustomerModel,
|
|
ItemModel,
|
|
BillModel,
|
|
LedgerModel,
|
|
PurchaseOrderModel,
|
|
ChartOfAccountModel,
|
|
)
|
|
from django_ledger.views.financial_statement import (
|
|
FiscalYearBalanceSheetView,
|
|
BaseIncomeStatementRedirectView,
|
|
FiscalYearIncomeStatementView,
|
|
BaseCashFlowStatementRedirectView,
|
|
FiscalYearCashFlowStatementView,
|
|
)
|
|
|
|
from django_ledger.io.io_core import get_localdate
|
|
from django_ledger.views.mixins import (
|
|
QuarterlyReportMixIn,
|
|
MonthlyReportMixIn,
|
|
DateReportMixIn,
|
|
DjangoLedgerSecurityMixIn,
|
|
EntityUnitMixIn,
|
|
)
|
|
|
|
# Other
|
|
from . import models, forms, tables
|
|
from django_tables2 import SingleTableView
|
|
from django_tables2.export.views import ExportMixin
|
|
|
|
# from appointment.models import Appointment, AppointmentRequest, Service, StaffMember
|
|
from .services import (
|
|
decodevin,
|
|
get_make,
|
|
get_model,
|
|
)
|
|
from .utils import (
|
|
CarFinanceCalculator,
|
|
create_estimate_,
|
|
get_car_finance_data,
|
|
get_finance_data,
|
|
get_item_transactions,
|
|
handle_payment,
|
|
reserve_car,
|
|
# send_email,
|
|
get_user_type,
|
|
set_bill_payment,
|
|
set_invoice_payment,
|
|
CarTransfer,
|
|
)
|
|
from .tasks import create_accounts_for_make, create_user_dealer, send_email
|
|
|
|
# djago easy audit log
|
|
# from easyaudit.models import RequestEvent, CRUDEvent, LoginEvent
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
|
|
class Hash(Func):
|
|
"""
|
|
Represents a function used to compute a hash value.
|
|
|
|
This class serves as a placeholder to specify a particular hash
|
|
computation function. It extends the `Func` base class and is used
|
|
primarily to work with hash-related operations within the underlying
|
|
implementation. The specific hash algorithm can be modified or accessed
|
|
through the `function` attribute.
|
|
|
|
:ivar function: Specifies the hash computation function.
|
|
:type function: str
|
|
"""
|
|
|
|
function = "get_hash"
|
|
|
|
|
|
def switch_language(request):
|
|
"""
|
|
Switches the current language context for the user based on a request parameter, modifies the URL path
|
|
accordingly, and updates session and cookies with the new language preference.
|
|
|
|
:param request: The HTTP request object containing information about the user request, including
|
|
the desired language to switch to and the referring URL.
|
|
- "GET" dictionary is accessed to retrieve the desired language parameter.
|
|
- "META" dictionary is used to extract the referring URL via "HTTP_REFERER".
|
|
|
|
:return: A redirect response object pointing to the modified URL with the updated language
|
|
preference, if the requested language is valid. Otherwise, redirects to the default URL.
|
|
"""
|
|
language = request.GET.get("language", "en")
|
|
referer = request.META.get("HTTP_REFERER", "/")
|
|
parsed_url = urlparse(referer)
|
|
path_parts = parsed_url.path.split("/")
|
|
|
|
if path_parts[1] in dict(settings.LANGUAGES):
|
|
path_parts.pop(1)
|
|
|
|
new_path = "/".join(path_parts)
|
|
new_url = urlunparse(
|
|
(
|
|
parsed_url.scheme,
|
|
parsed_url.netloc,
|
|
new_path,
|
|
parsed_url.params,
|
|
parsed_url.query,
|
|
parsed_url.fragment,
|
|
)
|
|
)
|
|
|
|
if language in dict(settings.LANGUAGES):
|
|
logger.debug(f"Switching language to: {language}")
|
|
response = redirect(new_url)
|
|
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
|
|
translation.activate(language)
|
|
request.session[settings.LANGUAGE_COOKIE_NAME] = language
|
|
logger.debug(
|
|
f"Language switched to: {language}, Session: {request.session[settings.LANGUAGE_COOKIE_NAME]}"
|
|
)
|
|
return response
|
|
else:
|
|
logger.warning(f"Invalid language code: {language}")
|
|
return redirect("/")
|
|
|
|
|
|
def dealer_signup(request):
|
|
from django_q.tasks import async_task
|
|
|
|
"""
|
|
Handles the dealer signup wizard process, including forms validation, user and group
|
|
creation, permissions assignment, and dealer data storage. This view supports GET
|
|
requests for rendering the signup wizard page, and POST requests for processing
|
|
submitted data. The function also differentiates between requests sent with the
|
|
"Hx-Request" header for partial form validations in the wizard.
|
|
|
|
:param request: The HTTP request object representing the client request. It contains
|
|
metadata about the request and the POST data for creating the dealer.
|
|
:type request: django.http.HttpRequest
|
|
:param args: Optional positional arguments passed to the view during the call.
|
|
:type args: tuple
|
|
:param kwargs: Optional keyword arguments passed to the view during the call.
|
|
:type kwargs: dict
|
|
:return: A rendered signup wizard page or a JSON response indicating operation success
|
|
or failure.
|
|
:rtype: Union[django.http.HttpResponse, django.http.JsonResponse]
|
|
"""
|
|
if request.method == "POST":
|
|
try:
|
|
data = json.loads(request.body)
|
|
email = data.get("email")
|
|
password = data.get("password")
|
|
password_confirm = data.get("confirm_password")
|
|
name = data.get("name")
|
|
arabic_name = data.get("arabic_name")
|
|
phone = data.get("phone_number")
|
|
crn = data.get("crn")
|
|
vrn = data.get("vrn")
|
|
address = data.get("address")
|
|
|
|
except json.JSONDecodeError:
|
|
pass
|
|
if User.objects.filter(email=email).exists():
|
|
return JsonResponse({"error": _("Email already exists")}, status=400)
|
|
if not re.match(
|
|
r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$",
|
|
email,
|
|
):
|
|
return JsonResponse(
|
|
{"error": _("Please enter a valid email address")}, status=400
|
|
)
|
|
|
|
if len(password) < 8:
|
|
return JsonResponse(
|
|
{"error": _("Password must be at least 8 characters")}, status=400
|
|
)
|
|
|
|
if password != password_confirm:
|
|
return JsonResponse({"error": _("Passwords do not match")}, status=400)
|
|
try:
|
|
async_task(
|
|
create_user_dealer,
|
|
email,
|
|
password,
|
|
name,
|
|
arabic_name,
|
|
phone,
|
|
crn,
|
|
vrn,
|
|
address,
|
|
)
|
|
logger.info(f"Delear created succesfully with emailID {email}")
|
|
return JsonResponse({"message": _("User created successfully")}, status=200)
|
|
except Exception as e:
|
|
logger.error(f"Error creating dealer with email id:{email} error:{e}")
|
|
return JsonResponse({"error": str(e)}, status=400)
|
|
return render(
|
|
request,
|
|
"account/signup-wizar.html",
|
|
)
|
|
|
|
|
|
# class HomeView(LoginRequiredMixin, TemplateView):
|
|
# """
|
|
# HomeView class responsible for rendering the home page.
|
|
|
|
# This class ensures that only authenticated users can access the home page.
|
|
# Unauthenticated users are redirected to the welcome page. It is built on top of
|
|
# Django's TemplateView and includes additional functionality by inheriting from
|
|
# LoginRequiredMixin. The purpose of this class is to control the accessibility
|
|
# of the main index page of the application and manage context data for frontend
|
|
# rendering.
|
|
|
|
# :ivar template_name: The path to the template used for rendering the view's
|
|
# output.
|
|
# :type template_name: str
|
|
# """
|
|
|
|
# template_name = "index.html"
|
|
|
|
|
|
@login_required
|
|
def HomeView(request, dealer_slug=None):
|
|
dealer_slug = request.dealer.slug
|
|
if request.is_sales and not request.is_manager and not request.is_dealer:
|
|
return redirect("sales_dashboard", dealer_slug=dealer_slug)
|
|
else:
|
|
return redirect("general_dashboard", dealer_slug=dealer_slug)
|
|
|
|
|
|
class TestView(TemplateView):
|
|
"""
|
|
Represents a view for displaying a list of cars.
|
|
|
|
This class is a Django TemplateView-based view that renders a specific template
|
|
to display a list of cars. It uses a pre-defined template file and can be
|
|
extended or customized to provide additional functionality for rendering
|
|
content in a Django application.
|
|
|
|
:ivar template_name: Specifies the path to the HTML template file used for
|
|
rendering the cars list view.
|
|
:type template_name: str
|
|
"""
|
|
|
|
template_name = "inventory/cars_list_api.html"
|
|
|
|
|
|
@login_required
|
|
def general_dashboard(request, dealer_slug):
|
|
"""
|
|
Renders the dealer dashboard with key performance indicators and chart data.
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
vat = models.VatRate.objects.filter(dealer=dealer, is_active=True).first()
|
|
VAT_RATE = vat.rate
|
|
today_local = timezone.localdate()
|
|
|
|
# ----------------------------------------------------
|
|
# 1. Date Filtering
|
|
# ----------------------------------------------------
|
|
start_date_str = request.GET.get("start_date")
|
|
end_date_str = request.GET.get("end_date")
|
|
|
|
if start_date_str and end_date_str:
|
|
start_date = timezone.datetime.strptime(start_date_str, "%Y-%m-%d").date()
|
|
end_date = timezone.datetime.strptime(end_date_str, "%Y-%m-%d").date()
|
|
else:
|
|
start_date = today_local - timedelta(days=30)
|
|
end_date = today_local
|
|
|
|
# ----------------------------------------------------
|
|
# 2. Inventory KPIs
|
|
# ----------------------------------------------------
|
|
active_cars = models.Car.objects.filter(dealer=dealer).exclude(status="sold")
|
|
|
|
total_cars_in_inventory = active_cars.count()
|
|
total_inventory_value = active_cars.aggregate(total=Sum("cost_price"))["total"] or 0
|
|
new_cars_qs = active_cars.filter(stock_type="new")
|
|
total_new_cars_in_inventory = new_cars_qs.count()
|
|
new_car_value = new_cars_qs.aggregate(total=Sum("cost_price"))["total"] or 0
|
|
used_cars_qs = active_cars.filter(stock_type="used")
|
|
total_used_cars_in_inventory = used_cars_qs.count()
|
|
used_car_value = used_cars_qs.aggregate(total=Sum("cost_price"))["total"] or 0
|
|
|
|
aging_threshold_days = 60
|
|
aging_inventory_count = active_cars.filter(
|
|
receiving_date__date__lte=today_local - timedelta(days=aging_threshold_days)
|
|
).count()
|
|
|
|
# ----------------------------------------------------
|
|
# 3. Sales KPIs (filtered by date)
|
|
# ----------------------------------------------------
|
|
cars_sold_filtered = models.Car.objects.filter(
|
|
dealer=dealer,
|
|
status="sold",
|
|
sold_date__date__gte=start_date,
|
|
sold_date__date__lte=end_date,
|
|
)
|
|
|
|
# General sales KPIs
|
|
total_cars_sold = cars_sold_filtered.count()
|
|
total_cost_of_cars_sold = (
|
|
cars_sold_filtered.aggregate(total=Sum("cost_price"))["total"] or 0
|
|
)
|
|
total_revenue_from_cars = (
|
|
cars_sold_filtered.aggregate(
|
|
total=Sum(F("marked_price") - F("discount_amount"))
|
|
)["total"]
|
|
or 0
|
|
)
|
|
|
|
total_vat_collected_from_cars = (
|
|
cars_sold_filtered.annotate(
|
|
final_price=F("marked_price") - F("discount_amount")
|
|
).aggregate(total=Sum(F("final_price") * VAT_RATE))["total"]
|
|
or 0
|
|
)
|
|
|
|
net_profit_from_cars = total_revenue_from_cars - total_cost_of_cars_sold
|
|
total_discount = (
|
|
cars_sold_filtered.aggregate(total=Sum("discount_amount"))["total"] or 0
|
|
)
|
|
|
|
# Sales breakdown by type
|
|
new_cars_sold = cars_sold_filtered.filter(stock_type="new")
|
|
total_new_cars_sold = new_cars_sold.count()
|
|
total_cost_of_new_cars_sold = (
|
|
new_cars_sold.aggregate(total=Sum("cost_price"))["total"] or 0
|
|
)
|
|
# total_revenue_from_new_cars=sum([ car.final_price for car in new_cars_sold])
|
|
total_revenue_from_new_cars = (
|
|
new_cars_sold.aggregate(total=Sum(F("marked_price") - F("discount_amount")))[
|
|
"total"
|
|
]
|
|
or 0
|
|
)
|
|
|
|
total_vat_collected_from_new_cars = (
|
|
new_cars_sold.annotate(
|
|
final_price=F("marked_price") - F("discount_amount")
|
|
).aggregate(total=Sum(F("final_price") * VAT_RATE))["total"]
|
|
or 0
|
|
)
|
|
|
|
net_profit_from_new_cars = total_revenue_from_new_cars - total_cost_of_new_cars_sold
|
|
|
|
used_cars_sold = cars_sold_filtered.filter(stock_type="used")
|
|
total_used_cars_sold = used_cars_sold.count()
|
|
total_cost_of_used_cars_sold = (
|
|
used_cars_sold.aggregate(total=Sum("cost_price"))["total"] or 0
|
|
)
|
|
total_revenue_from_used_cars = (
|
|
used_cars_sold.aggregate(total=Sum(F("marked_price") - F("discount_amount")))[
|
|
"total"
|
|
]
|
|
or 0
|
|
)
|
|
|
|
total_vat_collected_from_used_cars = (
|
|
used_cars_sold.annotate(
|
|
final_price=F("marked_price") - F("discount_amount")
|
|
).aggregate(total=Sum(F("final_price") * VAT_RATE))["total"]
|
|
or 0
|
|
)
|
|
|
|
net_profit_from_used_cars = (
|
|
total_revenue_from_used_cars - total_cost_of_used_cars_sold
|
|
)
|
|
|
|
# Service & Overall KPIs
|
|
total_revenue_from_services = sum(
|
|
[car.get_additional_services()["total"] for car in cars_sold_filtered]
|
|
)
|
|
total_vat_collected_from_services = sum(
|
|
[car.get_additional_services()["services_vat"] for car in cars_sold_filtered]
|
|
)
|
|
total_vat_collected = (
|
|
total_vat_collected_from_cars + total_vat_collected_from_services
|
|
)
|
|
total_revenue_generated = total_revenue_from_cars + total_revenue_from_services
|
|
# total_expenses=sum([x.amount_paid for x in dealer.entity.get_bills().filter(bill_items__item_role="expense")])
|
|
|
|
total_expenses = (
|
|
dealer.entity.get_bills()
|
|
.filter(bill_items__item_role="expense")
|
|
.aggregate(total=Sum("amount_paid"))["total"]
|
|
or 0
|
|
)
|
|
gross_profit = net_profit_from_cars + total_revenue_from_services - total_expenses
|
|
|
|
# ----------------------------------------------------
|
|
# 4. Chart Data Aggregation
|
|
# ----------------------------------------------------
|
|
monthly_sales_data = (
|
|
cars_sold_filtered.annotate(month=ExtractMonth("sold_date"))
|
|
.values("month")
|
|
.annotate(
|
|
total_cars=Count("pk"),
|
|
total_revenue=Sum(F("marked_price") - F("discount_amount")),
|
|
total_profit=Sum(
|
|
F("marked_price") - F("discount_amount") - F("cost_price")
|
|
),
|
|
)
|
|
.order_by("month")
|
|
)
|
|
|
|
monthly_cars_sold = [0] * 12
|
|
monthly_revenue = [0] * 12
|
|
monthly_net_profit = [0] * 12
|
|
|
|
for data in monthly_sales_data:
|
|
month_index = data["month"] - 1
|
|
monthly_cars_sold[month_index] = data["total_cars"]
|
|
monthly_revenue[month_index] = (
|
|
float(data["total_revenue"]) if data["total_revenue"] else 0
|
|
)
|
|
monthly_net_profit[month_index] = (
|
|
float(data["total_profit"]) if data["total_profit"] else 0
|
|
)
|
|
|
|
monthly_cars_sold_json = json.dumps(monthly_cars_sold)
|
|
monthly_revenue_json = json.dumps(monthly_revenue)
|
|
monthly_net_profit_json = json.dumps(monthly_net_profit)
|
|
|
|
# ----------------------------------------------------
|
|
# Sales by MAKE
|
|
# ----------------------------------------------------
|
|
sales_by_make_data = (
|
|
cars_sold_filtered.values("id_car_make__name")
|
|
.annotate(car_count=Count("id_car_make__name"))
|
|
.order_by("-car_count")
|
|
)
|
|
|
|
sales_by_make_labels = [data["id_car_make__name"] for data in sales_by_make_data]
|
|
sales_by_make_counts = [data["car_count"] for data in sales_by_make_data]
|
|
|
|
# ----------------------------------------------------
|
|
# DATA FOR CAR SALES BY MODELS (for the new interactive chart)
|
|
# ----------------------------------------------------
|
|
|
|
# Get the selected make from the URL query parameter
|
|
selected_make_sales = request.GET.get("make_sold", None)
|
|
|
|
# Get a list of all unique makes for the dropdown
|
|
all_makes_sold = list(
|
|
cars_sold_filtered.values_list("id_car_make__name", flat=True)
|
|
.distinct()
|
|
.order_by("id_car_make__name")
|
|
)
|
|
|
|
if selected_make_sales:
|
|
# If a make is selected, filter the queryset
|
|
sales_data_by_model = (
|
|
cars_sold_filtered.filter(id_car_make__name=selected_make_sales)
|
|
.values("id_car_model__name")
|
|
.annotate(count=Count("id_car_model__name"))
|
|
.order_by("-count")
|
|
)
|
|
else:
|
|
# If no make is selected, pass an empty list or some default data
|
|
sales_data_by_model = []
|
|
|
|
# 1. Inventory by Make (Pie Chart)
|
|
|
|
inventory_by_make_data = (
|
|
active_cars.values("id_car_make__name")
|
|
.annotate(car_count=Count("id_car_make__name"))
|
|
.order_by("-car_count")
|
|
)
|
|
|
|
inventory_by_make_labels = [
|
|
data["id_car_make__name"] for data in inventory_by_make_data
|
|
]
|
|
inventory_by_make_counts = [data["car_count"] for data in inventory_by_make_data]
|
|
|
|
# 2. Inventory by Model (Bar Chart)
|
|
selected_make_inventory = request.GET.get("make_inventory", None)
|
|
|
|
# Get all unique makes in inventory for the dropdown
|
|
all_makes_inventory = list(
|
|
active_cars.values_list("id_car_make__name", flat=True)
|
|
.distinct()
|
|
.order_by("id_car_make__name")
|
|
)
|
|
|
|
if selected_make_inventory:
|
|
inventory_data_by_model = (
|
|
active_cars.filter(id_car_make__name=selected_make_inventory)
|
|
.values("id_car_model__name")
|
|
.annotate(count=Count("id_car_model__name"))
|
|
.order_by("-count")
|
|
)
|
|
else:
|
|
# Default data
|
|
inventory_data_by_model = []
|
|
|
|
context = {
|
|
"start_date": start_date,
|
|
"end_date": end_date,
|
|
"today": today_local,
|
|
# Inventory KPIs
|
|
"total_cars_in_inventory": total_cars_in_inventory,
|
|
"total_inventory_value": total_inventory_value,
|
|
"total_new_cars_in_inventory": total_new_cars_in_inventory,
|
|
"total_used_cars_in_inventory": total_used_cars_in_inventory,
|
|
"new_car_value": new_car_value,
|
|
"used_car_value": used_car_value,
|
|
"aging_inventory_count": aging_inventory_count,
|
|
# Sales KPIs
|
|
"total_cars_sold": total_cars_sold,
|
|
"total_cost_of_cars_sold": total_cost_of_cars_sold,
|
|
"total_revenue_from_cars": total_revenue_from_cars,
|
|
"net_profit_from_cars": net_profit_from_cars,
|
|
"total_vat_collected_from_cars": total_vat_collected_from_cars,
|
|
"total_discount_on_cars": total_discount,
|
|
# Sales by Type
|
|
"total_new_cars_sold": total_new_cars_sold,
|
|
"total_used_cars_sold": total_used_cars_sold,
|
|
"total_cost_of_new_cars_sold": total_cost_of_new_cars_sold,
|
|
"total_revenue_from_new_cars": total_revenue_from_new_cars,
|
|
"net_profit_from_new_cars": net_profit_from_new_cars,
|
|
"total_vat_collected_from_new_cars": total_vat_collected_from_new_cars,
|
|
"total_cost_of_used_cars_sold": total_cost_of_used_cars_sold,
|
|
"total_revenue_from_used_cars": total_revenue_from_used_cars,
|
|
"net_profit_from_used_cars": net_profit_from_used_cars,
|
|
"total_vat_collected_from_used_cars": total_vat_collected_from_used_cars,
|
|
# Services and Overall KPIs
|
|
"total_revenue_from_services": total_revenue_from_services,
|
|
"total_vat_collected_from_services": total_vat_collected_from_services,
|
|
"total_revenue_generated": total_revenue_generated,
|
|
"total_vat_collected": total_vat_collected,
|
|
"total_expenses": total_expenses,
|
|
"gross_profit": gross_profit,
|
|
# Chart Data
|
|
"monthly_cars_sold_json": monthly_cars_sold_json,
|
|
"monthly_revenue_json": monthly_revenue_json,
|
|
"monthly_net_profit_json": monthly_net_profit_json,
|
|
# Sales Chart Data
|
|
"sales_by_make_labels_json": json.dumps(sales_by_make_labels),
|
|
"sales_by_make_counts_json": json.dumps(sales_by_make_counts),
|
|
"all_makes_sold": all_makes_sold,
|
|
"selected_make_sales": selected_make_sales,
|
|
"sales_data_by_model_json": json.dumps(list(sales_data_by_model)),
|
|
# New Inventory Chart Data
|
|
"inventory_by_make_labels_json": json.dumps(inventory_by_make_labels),
|
|
"inventory_by_make_counts_json": json.dumps(inventory_by_make_counts),
|
|
"all_makes_inventory": all_makes_inventory,
|
|
"selected_make_inventory": selected_make_inventory,
|
|
"inventory_data_by_model_json": json.dumps(list(inventory_data_by_model)),
|
|
}
|
|
|
|
return render(request, "dashboards/general_dashboard.html", context)
|
|
|
|
|
|
@login_required
|
|
def sales_dashboard(request, dealer_slug):
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
today_local = timezone.localdate()
|
|
|
|
# ----------------------------------------------------
|
|
# 1. Date Filtering
|
|
# ----------------------------------------------------
|
|
start_date_str = request.GET.get("start_date")
|
|
end_date_str = request.GET.get("end_date")
|
|
|
|
if start_date_str and end_date_str:
|
|
start_date = timezone.datetime.strptime(start_date_str, "%Y-%m-%d").date()
|
|
end_date = timezone.datetime.strptime(end_date_str, "%Y-%m-%d").date()
|
|
else:
|
|
start_date = today_local - timedelta(days=30)
|
|
end_date = today_local
|
|
|
|
# Filter leads by date range and dealer
|
|
leads_filtered = models.Lead.objects.filter(
|
|
dealer=dealer, created__date__gte=start_date, created__date__lte=end_date
|
|
)
|
|
total_leads = leads_filtered.count()
|
|
|
|
# ----------------------------------------------------
|
|
# 2. Lead Sources Chart Logic
|
|
# ----------------------------------------------------
|
|
# Group leads by source and count them
|
|
# This generates a list of dictionaries like [{'source': 'Showroom', 'count': 45}, ...]
|
|
lead_sources_data = (
|
|
leads_filtered.values("source")
|
|
.annotate(count=Count("source"))
|
|
.order_by("-count")
|
|
)
|
|
|
|
# Separate the labels and counts for the chart
|
|
lead_sources_labels = [item["source"] for item in lead_sources_data]
|
|
lead_sources_counts = [item["count"] for item in lead_sources_data]
|
|
|
|
# ----------------------------------------------------
|
|
# 2. Lead Funnel Chart Logic
|
|
# ----------------------------------------------------
|
|
opportunity_filtered = models.Opportunity.objects.filter(
|
|
dealer=dealer, created__date__gte=start_date, created__date__lte=end_date
|
|
)
|
|
opportunity_stage_data = (
|
|
opportunity_filtered.values("stage")
|
|
.annotate(count=Count("stage"))
|
|
.order_by("-count")
|
|
)
|
|
# Separate the labels and counts for the chart
|
|
opportunity_stage_labels = [item["stage"] for item in opportunity_stage_data]
|
|
opportunity_stage_counts = [item["count"] for item in opportunity_stage_data]
|
|
|
|
# 2. Inventory KPIs
|
|
# ----------------------------------------------------
|
|
active_cars = models.Car.objects.filter(dealer=dealer).exclude(status="sold")
|
|
|
|
total_cars_in_inventory = active_cars.count()
|
|
new_cars_qs = active_cars.filter(stock_type="new")
|
|
total_new_cars_in_inventory = new_cars_qs.count()
|
|
used_cars_qs = active_cars.filter(stock_type="used")
|
|
total_used_cars_in_inventory = used_cars_qs.count()
|
|
aging_threshold_days = 60
|
|
aging_inventory_count = active_cars.filter(
|
|
receiving_date__date__lte=today_local - timedelta(days=aging_threshold_days)
|
|
).count()
|
|
|
|
context = {
|
|
"start_date": start_date,
|
|
"end_date": end_date,
|
|
"lead_sources_labels_json": json.dumps(lead_sources_labels),
|
|
"lead_sources_counts_json": json.dumps(lead_sources_counts),
|
|
"opportunity_stage_labels_json": json.dumps(opportunity_stage_labels),
|
|
"opportunity_stage_counts_json": json.dumps(opportunity_stage_counts),
|
|
# Inventory KPIs
|
|
"total_cars_in_inventory": total_cars_in_inventory,
|
|
"total_new_cars_in_inventory": total_new_cars_in_inventory,
|
|
"total_used_cars_in_inventory": total_used_cars_in_inventory,
|
|
"aging_inventory_count": aging_inventory_count,
|
|
"total_leads": total_leads,
|
|
}
|
|
|
|
return render(request, "dashboards/sales_dashboard.html", context)
|
|
|
|
|
|
def aging_inventory_list_view(request, dealer_slug):
|
|
"""
|
|
Renders a paginated list of aging inventory for a specific dealer, with filters.
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
today_local = timezone.localdate()
|
|
aging_threshold_days = 60
|
|
|
|
# Get filter parameters from the request
|
|
selected_make = request.GET.get("make")
|
|
selected_model = request.GET.get("model")
|
|
selected_series = request.GET.get(
|
|
"series"
|
|
) # Changed 'serie' to 'series' for consistency
|
|
selected_year = request.GET.get("year")
|
|
selected_stock_type = request.GET.get("stock_type")
|
|
|
|
# Start with the base queryset for all aging cars.
|
|
aging_cars_queryset = models.Car.objects.filter(
|
|
dealer=dealer,
|
|
receiving_date__date__lt=today_local - timedelta(days=aging_threshold_days),
|
|
).exclude(status="sold")
|
|
|
|
# Apply filters to the queryset if they exist. Chaining is fine here.
|
|
if selected_make:
|
|
aging_cars_queryset = aging_cars_queryset.filter(
|
|
id_car_make__name=selected_make
|
|
)
|
|
if selected_model:
|
|
aging_cars_queryset = aging_cars_queryset.filter(
|
|
id_car_model__name=selected_model
|
|
)
|
|
if selected_series:
|
|
aging_cars_queryset = aging_cars_queryset.filter(
|
|
id_car_series__name=selected_series
|
|
)
|
|
if selected_year:
|
|
aging_cars_queryset = aging_cars_queryset.filter(
|
|
id_car_year__year=selected_year
|
|
)
|
|
if selected_stock_type:
|
|
aging_cars_queryset = aging_cars_queryset.filter(stock_type=selected_stock_type)
|
|
|
|
total_aging_inventory_value = aging_cars_queryset.aggregate(
|
|
total=Sum("cost_price")
|
|
)["total"]
|
|
count_of_aging_cars = aging_cars_queryset.count()
|
|
|
|
# Get distinct values for filter dropdowns based on the initial, unfiltered aging cars queryset.
|
|
# This ensures all possible filter options are always available.
|
|
aging_base_queryset = models.Car.objects.filter(
|
|
dealer=dealer,
|
|
receiving_date__date__lt=today_local - timedelta(days=aging_threshold_days),
|
|
).exclude(status="sold")
|
|
|
|
all_makes = (
|
|
aging_base_queryset.values_list("id_car_make__name", flat=True)
|
|
.distinct()
|
|
.order_by("id_car_make__name")
|
|
)
|
|
all_models = (
|
|
aging_base_queryset.values_list("id_car_model__name", flat=True)
|
|
.distinct()
|
|
.order_by("id_car_model__name")
|
|
)
|
|
all_series = (
|
|
aging_base_queryset.values_list("id_car_serie__name", flat=True)
|
|
.distinct()
|
|
.order_by("id_car_serie__name")
|
|
)
|
|
all_stock_types = (
|
|
aging_base_queryset.values_list("stock_type", flat=True)
|
|
.distinct()
|
|
.order_by("stock_type")
|
|
)
|
|
all_years = (
|
|
aging_base_queryset.values_list("year", flat=True).distinct().order_by("-year")
|
|
)
|
|
|
|
#
|
|
# Set up pagination
|
|
paginator = Paginator(aging_cars_queryset, 10)
|
|
page_number = request.GET.get("page")
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
# Iterate only on the cars for the current page to add the age attribute.
|
|
for car in page_obj.object_list:
|
|
car.age_in_days = (today_local - car.receiving_date.date()).days
|
|
|
|
context = {
|
|
"is_paginated": page_obj.has_other_pages,
|
|
"cars": page_obj.object_list,
|
|
"selected_make": selected_make,
|
|
"selected_model": selected_model,
|
|
"selected_series": selected_series, # Corrected variable name
|
|
"selected_year": selected_year,
|
|
"selected_stock_type": selected_stock_type,
|
|
"all_makes": all_makes,
|
|
"all_models": all_models,
|
|
"all_series": all_series,
|
|
"all_stock_types": all_stock_types,
|
|
"all_years": all_years,
|
|
"total_aging_inventory_value": total_aging_inventory_value,
|
|
"page_obj": page_obj,
|
|
"count_of_aging_cars": count_of_aging_cars,
|
|
}
|
|
|
|
return render(request, "dashboards/aging_inventory_list.html", context)
|
|
|
|
|
|
def terms_and_privacy(request):
|
|
return render(request, "terms_and_privacy.html")
|
|
|
|
|
|
def WelcomeView(request):
|
|
"""
|
|
Handles the rendering and context data for the Welcome view.
|
|
|
|
This class serves as a Django TemplateView for the "welcome.html" template. It
|
|
is primarily responsible for providing the necessary context data, including
|
|
user-specific information and the list of plans, to the template for rendering
|
|
the welcome page.
|
|
|
|
:ivar template_name: Path to the template used by the view.
|
|
:type template_name: str
|
|
"""
|
|
if request.user.is_authenticated:
|
|
return redirect("home", dealer_slug=request.dealer.slug)
|
|
plan_list = Plan.objects.all()
|
|
context = {"plan_list": plan_list}
|
|
return render(request, "welcome.html", context)
|
|
|
|
|
|
class CarCreateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView
|
|
):
|
|
"""
|
|
Manages the creation of a new car entry in the inventory system.
|
|
|
|
This class is responsible for handling the creation of a new car, including form
|
|
display and data submission. It ensures the user has appropriate permissions and
|
|
customizes the available vendors for the user depending on their dealer entity.
|
|
|
|
:ivar model: Specifies the Car model that this view interacts with.
|
|
:type model: models.Car
|
|
:ivar form_class: Defines the form class to be used for creating or editing `Car` instances.
|
|
:type form_class: forms.CarForm
|
|
:ivar template_name: Name of the template to render the car creation form.
|
|
:type template_name: str
|
|
:ivar permission_required: Permissions required to add a car.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = models.Car
|
|
form_class = forms.CarForm
|
|
template_name = "inventory/car_form.html"
|
|
permission_required = ["inventory.add_car"]
|
|
success_message = _("Car Added successfully to the inventory")
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
form.fields["vendor"].queryset = dealer.vendors.filter(active=True)
|
|
return form
|
|
|
|
def get_success_url(self):
|
|
"""Determine the redirect URL based on user choice."""
|
|
if self.request.POST.get("add_another"):
|
|
return reverse(
|
|
"car_add", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
|
|
)
|
|
return reverse(
|
|
"inventory_stats", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
|
|
)
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
form.instance.dealer = dealer
|
|
form.save()
|
|
messages.success(self.request, _("Car saved successfully"))
|
|
return super().form_valid(form)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
dealer = get_user_type(self.request)
|
|
context = super().get_context_data(**kwargs)
|
|
context["vendor_exists"] = dealer.vendors.filter(active=True).exists()
|
|
return context
|
|
|
|
|
|
def car_history(request, slug):
|
|
"""
|
|
Fetch and display the history of activities related to a specific car.
|
|
|
|
This view retrieves a car object based on its primary key (pk) and gathers
|
|
all related activity records where the content type corresponds to the car
|
|
model. The retrieved data is then rendered into a specified HTML template
|
|
for presentation.
|
|
|
|
:param request: The HTTP request object that contains metadata about the
|
|
request made by the client.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the car object to retrieve.
|
|
:type pk: int
|
|
:return: An HTTP response with the rendered car history HTML template,
|
|
including the car and its associated activities as context data.
|
|
:rtype: HttpResponse
|
|
"""
|
|
car = get_object_or_404(models.Car, slug=slug)
|
|
activities = models.Activity.objects.filter(
|
|
content_type__model="car", object_id=car.id
|
|
)
|
|
return render(
|
|
request, "inventory/car_history.html", {"car": car, "activities": activities}
|
|
)
|
|
|
|
|
|
class AjaxHandlerView(LoginRequiredMixin, View):
|
|
"""
|
|
Handles AJAX requests for various car-related operations.
|
|
|
|
This class enables dynamic handling of AJAX requests related to vehicle
|
|
data, such as retrieving models, series, trims, specifications, and other
|
|
related details. It checks the requested action, delegates it to the
|
|
corresponding handler method, and returns an appropriate response.
|
|
|
|
:ivar request: Django request object containing HTTP request details.
|
|
"""
|
|
|
|
def get(self, request, dealer_slug, *args, **kwargs):
|
|
action = request.GET.get("action")
|
|
handlers = {
|
|
"decode_vin": self.decode_vin,
|
|
"get_models": self.get_models,
|
|
"get_series": self.get_series,
|
|
"get_trims": self.get_trims,
|
|
"get_specifications": self.get_specifications,
|
|
"get_equipments": self.get_equipments,
|
|
"get_options": self.get_options,
|
|
}
|
|
handler = handlers.get(action)
|
|
if handler:
|
|
return handler(request)
|
|
else:
|
|
return JsonResponse({"error": "Invalid action"}, status=400)
|
|
|
|
def decode_vin(self, request):
|
|
dealer = request.dealer
|
|
vin_no = request.GET.get("vin_no")
|
|
car_existed = models.Car.objects.filter(dealer=dealer, vin=vin_no).exists()
|
|
|
|
if car_existed:
|
|
return JsonResponse({"error": _("VIN number exists")}, status=400)
|
|
|
|
if not vin_no or len(vin_no.strip()) != 17:
|
|
return JsonResponse(
|
|
{"success": False, "error": _("Invalid VIN number provided")},
|
|
status=400,
|
|
)
|
|
|
|
vin_no = vin_no.strip()
|
|
vin_data = {}
|
|
decoding_method = ""
|
|
if result := decodevin(vin_no):
|
|
manufacturer_name, model_name, year_model = result.values()
|
|
car_make = get_make(manufacturer_name)
|
|
if not car_make:
|
|
logger.info(
|
|
f"VIN decoded using {decoding_method}: Make={manufacturer_name}, Model={model_name}, Year={year_model}"
|
|
)
|
|
return JsonResponse(
|
|
{
|
|
"success": False,
|
|
"error": _("Manufacturer not found in the database"),
|
|
},
|
|
status=404,
|
|
)
|
|
car_model = get_model(model_name, car_make)
|
|
|
|
logger.info(
|
|
f"VIN decoded using {decoding_method}: Make={manufacturer_name}, Model={model_name}, Year={year_model}"
|
|
)
|
|
|
|
if not car_make:
|
|
return JsonResponse(
|
|
{
|
|
"success": False,
|
|
"error": _("Manufacturer not found in the database"),
|
|
},
|
|
status=404,
|
|
)
|
|
vin_data["make_id"] = car_make.id_car_make
|
|
vin_data["name"] = car_make.name
|
|
vin_data["arabic_name"] = car_make.arabic_name
|
|
|
|
if not car_model:
|
|
vin_data["model_id"] = ""
|
|
else:
|
|
vin_data["model_id"] = car_model.id_car_model
|
|
vin_data["year"] = year_model
|
|
return JsonResponse({"success": True, "data": vin_data})
|
|
|
|
# manufacturer_name = model_name = year_model = None
|
|
return JsonResponse(
|
|
{"success": False, "error": _("VIN not found in all sources")},
|
|
status=404,
|
|
)
|
|
|
|
def get_models(self, request):
|
|
make_id = request.GET.get("make_id")
|
|
car_models = (
|
|
models.CarModel.objects.filter(id_car_make=make_id)
|
|
.values("id_car_model", "name", "arabic_name")
|
|
.order_by("name")
|
|
)
|
|
return JsonResponse(list(car_models), safe=False)
|
|
|
|
def get_series(self, request):
|
|
model_id = request.GET.get("model_id")
|
|
year = request.GET.get("year")
|
|
|
|
model_id = int(model_id)
|
|
year = int(year)
|
|
|
|
query = Q(id_car_model=model_id) & (
|
|
Q(year_begin__lte=year, year_end__gte=year)
|
|
| Q(year_end__isnull=True)
|
|
| Q(year_begin__isnull=True)
|
|
)
|
|
try:
|
|
series = models.CarSerie.objects.filter(query).values(
|
|
"id_car_serie", "name", "arabic_name", "generation_name"
|
|
)
|
|
logger.debug(
|
|
f"Successfully fetched car series with query: {query}. Found {len(series)} results."
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Error fetching car series with query '{query}'. Details: {e}",
|
|
exc_info=True,
|
|
)
|
|
return JsonResponse({"error": _("Server error occurred")}, status=500)
|
|
return JsonResponse(list(series), safe=False)
|
|
|
|
def get_trims(self, request):
|
|
serie_id = request.GET.get("serie_id")
|
|
# model_id = request.GET.get('model_id')
|
|
trims = models.CarTrim.objects.filter(id_car_serie=serie_id).values(
|
|
"id_car_trim", "name", "arabic_name"
|
|
)
|
|
return JsonResponse(list(trims), safe=False)
|
|
|
|
def get_specifications(self, request):
|
|
trim_id = request.GET.get("trim_id")
|
|
car_spec_values = models.CarSpecificationValue.objects.filter(
|
|
id_car_trim=trim_id
|
|
)
|
|
lang = translation.get_language()
|
|
specs_by_parent = {}
|
|
for value in car_spec_values:
|
|
specification = value.id_car_specification
|
|
parent = specification.id_parent
|
|
parent_id = parent.id_car_specification if parent else 0
|
|
if lang == "ar":
|
|
parent_name = parent.arabic_name if parent else "Root"
|
|
else:
|
|
parent_name = parent.name if parent else "Root"
|
|
if parent_id not in specs_by_parent:
|
|
specs_by_parent[parent_id] = {
|
|
"parent_name": parent_name,
|
|
"specifications": [],
|
|
}
|
|
spec_data = {
|
|
"specification_id": specification.id_car_specification,
|
|
"s_name": specification.arabic_name
|
|
if lang == "ar"
|
|
else specification.name,
|
|
"s_value": value.value,
|
|
"s_unit": value.unit if value.unit else "",
|
|
"trim_name": value.id_car_trim.name,
|
|
}
|
|
specs_by_parent[parent_id]["specifications"].append(spec_data)
|
|
serialized_specs = [
|
|
{"parent_name": v["parent_name"], "specifications": v["specifications"]}
|
|
for v in specs_by_parent.values()
|
|
]
|
|
return JsonResponse(serialized_specs, safe=False)
|
|
|
|
def get_equipments(self, request):
|
|
trim_id = request.GET.get("trim_id")
|
|
equipments = (
|
|
models.CarEquipment.objects.filter(id_car_trim=trim_id)
|
|
.values("id_car_equipment", "name")
|
|
.order_by("name")
|
|
)
|
|
return JsonResponse(list(equipments), safe=False)
|
|
|
|
def get_options(self, request):
|
|
equipment_id = request.GET.get("equipment_id")
|
|
car_option_values = models.CarOptionValue.objects.filter(
|
|
id_car_equipment=equipment_id
|
|
)
|
|
|
|
options_by_parent = {}
|
|
for value in car_option_values:
|
|
option = value.id_car_option
|
|
parent = option.id_parent
|
|
parent_id = parent.id_car_option if parent else 0
|
|
parent_name = parent.name if parent else "Root"
|
|
if parent_id not in options_by_parent:
|
|
options_by_parent[parent_id] = {
|
|
"parent_name": parent_name,
|
|
"options": [],
|
|
}
|
|
option_data = {
|
|
"option_id": option.id_car_option,
|
|
"option_name": option.name,
|
|
"is_base": value.is_base,
|
|
"equipment_name": value.id_car_equipment.name,
|
|
}
|
|
options_by_parent[parent_id]["options"].append(option_data)
|
|
serialized_options = [
|
|
{"parent_name": v["parent_name"], "options": v["options"]}
|
|
for v in options_by_parent.values()
|
|
]
|
|
return JsonResponse(serialized_options, safe=False)
|
|
|
|
|
|
@method_decorator(csrf_exempt, name="dispatch")
|
|
class SearchCodeView(LoginRequiredMixin, View):
|
|
template_name = "inventory/scan_vin.html"
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
return render(request, self.template_name)
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
image_file = request.FILES.get("image")
|
|
|
|
if not image_file:
|
|
return JsonResponse({"success": False, "error": _("No image provided")})
|
|
|
|
try:
|
|
# --- Logging the start of image processing ---
|
|
logger.debug(
|
|
f"Received image '{image_file.name}' ({image_file.size} bytes) for barcode/QR scan from user: {request.user.username}."
|
|
)
|
|
|
|
np_arr = np.frombuffer(image_file.read(), np.uint8)
|
|
image = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
|
|
if image is None:
|
|
logger.error(
|
|
f"Invalid image format or corrupted file received for scan from user: {request.user.username}. "
|
|
f"Filename: {image_file.name}."
|
|
)
|
|
raise ValueError("Invalid image format")
|
|
|
|
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
|
decoded_objects = decode(gray)
|
|
|
|
if not decoded_objects:
|
|
# --- Logging for no QR/Barcode detected ---
|
|
logger.info(
|
|
f"No QR/Barcode detected in image '{image_file.name}' "
|
|
f"from user: {request.user.username}."
|
|
)
|
|
return JsonResponse(
|
|
{"success": False, "error": _("No QR/Barcode detected")}
|
|
)
|
|
|
|
code = decoded_objects[0].data.decode("utf-8").strip()
|
|
# Use INFO or WARNING, as it's a common user-facing scenario, not necessarily a server error.
|
|
logger.info(
|
|
f"Scanned VIN '{code}' from image '{image_file.name}' not found in database for user: {request.user.username}."
|
|
)
|
|
car = get_object_or_404(models.Car, vin=code)
|
|
logger.info(
|
|
f"Successfully found car (VIN: {code}, ID: {car.pk}) based on scanned code for user: {request.user.username}."
|
|
)
|
|
return JsonResponse(
|
|
{
|
|
"success": True,
|
|
"code": code,
|
|
"redirect_url": reverse("car_detail", args=[car.slug]),
|
|
}
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(
|
|
f"An unexpected error occurred during barcode/QR scan process for {image_file}"
|
|
f"from user: {request.user.username}. Error: {e}",
|
|
exc_info=True, # CRUCIAL: Get the full traceback
|
|
)
|
|
return JsonResponse({"success": False, "error": str(e)})
|
|
|
|
|
|
class CarInventory(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Represents the car inventory listing view for a dealership.
|
|
|
|
This class is a Django ListView-based implementation to display a list of cars
|
|
filtered by specific criteria such as make, model, and trim, while supporting user
|
|
authentication and permissions. It includes pagination and ordering features and
|
|
integrates search filtering functionality.
|
|
|
|
:ivar model: The model associated with the list view.
|
|
:type model: models.Car
|
|
:ivar home_label: The label of the home section of the application, used for UI
|
|
or breadcrumbs. Defined as a translatable string.
|
|
:type home_label: str
|
|
:ivar template_name: The template path used for rendering the car inventory
|
|
list view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable containing the
|
|
car objects in the template.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of car objects displayed per page.
|
|
:type paginate_by: int
|
|
:ivar ordering: The default ordering of car objects in the list view.
|
|
:type ordering: list
|
|
:ivar permission_required: The permission(s) required to access this view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = models.Car
|
|
home_label = _("inventory")
|
|
template_name = "inventory/car_inventory.html"
|
|
context_object_name = "cars"
|
|
paginate_by = 20
|
|
ordering = ["receiving_date"]
|
|
permission_required = ["inventory.view_car"]
|
|
|
|
def get_queryset(self, *args, **kwargs):
|
|
query = self.request.GET.get("q")
|
|
make = models.CarMake.objects.get(slug=self.kwargs["make_id"])
|
|
model = models.CarModel.objects.get(slug=self.kwargs["model_id"])
|
|
trim = models.CarTrim.objects.get(slug=self.kwargs["trim_id"])
|
|
|
|
dealer = get_user_type(self.request)
|
|
cars = models.Car.objects.filter(
|
|
dealer=dealer,
|
|
id_car_make=make,
|
|
id_car_model=model,
|
|
id_car_trim=trim,
|
|
).order_by("receiving_date")
|
|
|
|
return apply_search_filters(cars, query)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["query"] = self.request.GET.get("q", "")
|
|
context["make_id"] = self.kwargs["make_id"]
|
|
context["model_id"] = self.kwargs["model_id"]
|
|
context["trim_id"] = self.kwargs["trim_id"]
|
|
return context
|
|
|
|
|
|
class CarColorCreate(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView
|
|
):
|
|
"""
|
|
View for creating a new car color.
|
|
|
|
This view is used to add new colors to a specific car by associating
|
|
the color with a car instance. It ensures that the user is logged in
|
|
and has the necessary permissions to add car colors. The form used in
|
|
this view is populated dynamically with respect to the related car
|
|
object that is retrieved based on the provided car primary key.
|
|
|
|
:ivar model: Reference to the model representing car colors.
|
|
:type model: models.CarColors
|
|
:ivar form_class: The form class used for car color creation.
|
|
:type form_class: forms.CarColorsForm
|
|
:ivar template_name: Path to the template used while rendering the view.
|
|
:type template_name: str
|
|
:ivar permission_required: List of permissions required by the view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = models.CarColors
|
|
form_class = forms.CarColorsForm
|
|
template_name = "inventory/add_colors.html"
|
|
permission_required = ["inventory.add_carcolors"]
|
|
success_message = _("Car colors details added successfully")
|
|
|
|
def form_valid(self, form):
|
|
car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
|
form.instance.car = car
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"car_detail",
|
|
kwargs={
|
|
"dealer_slug": self.request.dealer.slug,
|
|
"slug": self.kwargs["slug"],
|
|
},
|
|
)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
|
return context
|
|
|
|
|
|
class CarColorsUpdateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
|
):
|
|
model = models.CarColors
|
|
form_class = forms.CarColorsForm
|
|
template_name = "inventory/add_colors.html"
|
|
success_message = _("Car Colors details updated successfully")
|
|
permission_required = ["inventory.change_carcolors"]
|
|
|
|
def get_object(self, queryset=None):
|
|
"""
|
|
Retrieves the CarColors instance associated with the Car slug from the URL.
|
|
This ensures we are updating the colors for the correct car.
|
|
"""
|
|
|
|
# Get the car_slug from the URL keywords arguments
|
|
slug = self.kwargs.get("slug")
|
|
|
|
# If no car_slug is provided, it's an invalid request
|
|
if not slug:
|
|
# You might want to raise Http404 or a more specific error here
|
|
raise ValueError("Car slug is required to identify the colors to update.")
|
|
|
|
return get_object_or_404(models.CarColors, car__slug=slug)
|
|
|
|
def get_success_url(self):
|
|
"""
|
|
Redirects to the car's detail page using its slug after a successful update.
|
|
"""
|
|
# self.object refers to the CarColors instance that was just updated.
|
|
# self.object.car then refers to the associated Car instance.
|
|
return reverse(
|
|
"car_detail",
|
|
kwargs={
|
|
"dealer_slug": self.request.dealer.slug,
|
|
"slug": self.object.car.slug,
|
|
},
|
|
)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""
|
|
Adds the related Car object to the template context.
|
|
"""
|
|
context = super().get_context_data(**kwargs)
|
|
# self.object is already available here from get_object()
|
|
context["car"] = self.object.car
|
|
context["page_title"] = _("Update Colors for %(car_name)s") % {
|
|
"car_name": context["car"]
|
|
}
|
|
return context
|
|
|
|
|
|
class CarListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Represents a view for listing and managing a collection of cars.
|
|
|
|
This class is used to display a paginated list of cars according to specific
|
|
filters and permissions. It ensures that only users with the required
|
|
permissions can access the view and handles dynamic filtering of cars based
|
|
on user input. The view also populates additional context data,
|
|
such as car statistics and related make, model, and year information,
|
|
to support detailed customization in templates.
|
|
|
|
:ivar model: Specifies the model associated with this view.
|
|
:type model: models.Car
|
|
:ivar template_name: The name of the template used for this view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the variable used in the template to
|
|
represent the objects being displayed.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of items to display per page in the paginated list.
|
|
:type paginate_by: int
|
|
:ivar permission_required: The permission required to access this view.
|
|
:type permission_required: str
|
|
"""
|
|
|
|
model = models.Car
|
|
template_name = "inventory/car_list_view.html"
|
|
context_object_name = "cars"
|
|
paginate_by = 10
|
|
permission_required = "inventory.view_car"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
dealer = get_user_type(self.request)
|
|
cars = models.Car.objects.filter(dealer=dealer).order_by("receiving_date")
|
|
context["stats"] = {
|
|
"all": cars.count(),
|
|
"available": cars.filter(status="available").count(),
|
|
"reserved": cars.filter(status="reserved").count(),
|
|
"sold": cars.filter(status="sold").count(),
|
|
"transfer": cars.filter(status="transfer").count(),
|
|
}
|
|
context["make"] = models.CarMake.objects.filter(
|
|
is_sa_import=True, car__in=cars
|
|
).distinct()
|
|
context["model"] = models.CarModel.objects.none()
|
|
context["year"] = models.Car.objects.none()
|
|
make = self.request.GET.get("make")
|
|
model = self.request.GET.get("model")
|
|
if make:
|
|
make_ = models.CarMake.objects.get(id_car_make=int(make))
|
|
context["model"] = make_.carmodel_set.filter(car__in=cars).distinct()
|
|
if make and model:
|
|
make_ = models.CarMake.objects.get(id_car_make=int(make))
|
|
model_ = models.CarModel.objects.get(id_car_model=int(model))
|
|
context["year"] = (
|
|
models.Car.objects.filter(id_car_make=make_, id_car_model=model_)
|
|
.values_list("year")
|
|
.distinct()
|
|
)
|
|
return context
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
qs = super().get_queryset()
|
|
qs = qs.filter(dealer=dealer)
|
|
# status = self.request.GET.get("status")
|
|
# status = self.request.GET.get("status")
|
|
search = self.request.GET.get("search")
|
|
make = self.request.GET.get("make", None)
|
|
model = self.request.GET.get("model", None)
|
|
year = self.request.GET.get("year", None)
|
|
car_status = self.request.GET.get("car_status", None)
|
|
print(car_status)
|
|
if car_status:
|
|
qs = qs.filter(status=car_status)
|
|
if search:
|
|
query = (
|
|
Q(vin__icontains=search)
|
|
| Q(id_car_make__name__icontains=search)
|
|
| Q(id_car_model__name__icontains=search)
|
|
| Q(id_car_trim__name__icontains=search)
|
|
| Q(vin=search)
|
|
)
|
|
qs = qs.filter(query)
|
|
if any([make, model, year, car_status]):
|
|
query = Q()
|
|
if make:
|
|
query &= Q(id_car_make=int(make))
|
|
if model:
|
|
query &= Q(id_car_model=model)
|
|
if year:
|
|
query &= Q(year=year)
|
|
if car_status:
|
|
query &= Q(status=car_status)
|
|
qs = qs.filter(query)
|
|
return qs
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.view_car")
|
|
def inventory_stats_view(request, dealer_slug):
|
|
"""
|
|
Handle the inventory stats view for a dealer, displaying detailed information
|
|
about the cars, including counts grouped by make, model, and trim.
|
|
|
|
The function fetches all cars associated with the authenticated dealer, calculates
|
|
the inventory statistics (e.g., total cars, reserved cars, and cars categorized
|
|
by make, model, and trim levels), and prepares the data to be rendered in a
|
|
template.
|
|
|
|
:param request: The HTTP request object from the client.
|
|
:type request: HttpRequest
|
|
:return: An HTTP response containing structured inventory data rendered in the
|
|
"inventory/inventory_stats.html" template.
|
|
:rtype: HttpResponse
|
|
"""
|
|
|
|
# Base queryset for cars belonging to the dealer
|
|
cars = models.Car.objects.filter(dealer=request.dealer)
|
|
# Count for total, reserved, showroom, and unreserved cars
|
|
total_cars = cars.count()
|
|
reserved_cars = models.CarReservation.objects.count()
|
|
# showroom_cars = cars.filter(location='showroom').count()
|
|
# unreserved_cars = total_cars - reserved_cars
|
|
|
|
# Annotate total cars by make, model, and trim
|
|
cars = cars.select_related("id_car_make", "id_car_model", "id_car_trim").annotate(
|
|
make_total=Count("id_car_make"),
|
|
model_total=Count("id_car_model"),
|
|
trim_total=Count("id_car_trim"),
|
|
)
|
|
|
|
inventory = {}
|
|
for car in cars:
|
|
make = car.id_car_make
|
|
if make.id_car_make not in inventory:
|
|
inventory[make.id_car_make] = {
|
|
"make_id": make.id_car_make,
|
|
"slug": make.slug,
|
|
"make_name": make.get_local_name(),
|
|
"total_cars": 0,
|
|
"models": {},
|
|
}
|
|
inventory[make.id_car_make]["total_cars"] += 1
|
|
|
|
model = car.id_car_model
|
|
if model and model.id_car_model not in inventory[make.id_car_make]["models"]:
|
|
inventory[make.id_car_make]["models"][model.id_car_model] = {
|
|
"model_id": model.id_car_model,
|
|
"slug": model.slug,
|
|
"model_name": model.get_local_name(),
|
|
"total_cars": 0,
|
|
"trims": {},
|
|
}
|
|
try:
|
|
logger.debug(
|
|
f"Attempting to update inventory for Car ID: {car.id}, Make: {make.name} ({make.id_car_make}), "
|
|
f"Model: {model.name} ({model.id_car_model})."
|
|
)
|
|
inventory[make.id_car_make]["models"][model.id_car_model]["total_cars"] += 1
|
|
|
|
trim = car.id_car_trim
|
|
if (
|
|
trim
|
|
and trim.id_car_trim
|
|
not in inventory[make.id_car_make]["models"][model.id_car_model][
|
|
"trims"
|
|
]
|
|
):
|
|
inventory[make.id_car_make]["models"][model.id_car_model]["trims"][
|
|
trim.id_car_trim
|
|
] = {
|
|
"trim_id": trim.id_car_trim,
|
|
"slug": trim.slug,
|
|
"trim_name": trim.name,
|
|
"total_cars": 0,
|
|
}
|
|
inventory[make.id_car_make]["models"][model.id_car_model]["trims"][
|
|
trim.id_car_trim
|
|
]["total_cars"] += 1
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Error updating inventory counts for car ID {getattr(car, 'id', 'N/A')}. "
|
|
f"Make ID: {getattr(make, 'id_car_make', 'N/A')}, Model ID: {getattr(model, 'id_car_model', 'N/A')}, "
|
|
f"Trim ID: {getattr(trim, 'id_car_trim', 'N/A')}. Error: {e}",
|
|
exc_info=True,
|
|
)
|
|
print(e)
|
|
result = {
|
|
"total_cars": total_cars,
|
|
"reserved_cars": reserved_cars,
|
|
"makes": [
|
|
{
|
|
"make_id": make_data["make_id"],
|
|
"slug": make_data["slug"],
|
|
"make_name": make_data["make_name"],
|
|
"total_cars": make_data["total_cars"],
|
|
"models": [
|
|
{
|
|
"model_id": model_data["model_id"],
|
|
"slug": model_data["slug"],
|
|
"model_name": model_data["model_name"],
|
|
"total_cars": model_data["total_cars"],
|
|
"trims": list(model_data["trims"].values()),
|
|
}
|
|
for model_data in make_data["models"].values()
|
|
],
|
|
}
|
|
for make_data in inventory.values()
|
|
],
|
|
}
|
|
return render(
|
|
request,
|
|
"inventory/inventory_stats.html",
|
|
{"inventory": result, "empty_state_value": _("car")},
|
|
)
|
|
|
|
|
|
# @login_required
|
|
# def inventory_stats_view(request):
|
|
# """
|
|
# Handle the inventory stats view for a dealer, displaying detailed information
|
|
# about the cars, including counts grouped by make, model, and trim.
|
|
|
|
# The function fetches all cars associated with the authenticated dealer, calculates
|
|
# the inventory statistics (e.g., total cars, reserved cars, and cars categorized
|
|
# by make, model, and trim levels), and prepares the data to be rendered in a
|
|
# template.
|
|
|
|
# :param request: The HTTP request object from the client.
|
|
# :type request: HttpRequest
|
|
# :return: An HTTP response containing structured inventory data rendered in the
|
|
# "inventory/inventory_stats.html" template.
|
|
# :rtype: HttpResponse
|
|
# """
|
|
# dealer = get_user_type(request)
|
|
|
|
# # Base queryset for cars belonging to the dealer
|
|
# # Ordering here is important for consistent pagination results
|
|
# cars = models.Car.objects.filter(dealer=dealer).order_by(
|
|
# 'id_car_make__name', 'id_car_model__name', 'id_car_trim__name'
|
|
# ) # Added ordering for consistent pagination
|
|
|
|
# # Count for total, reserved, showroom, and unreserved cars
|
|
# total_cars = cars.count()
|
|
# reserved_cars = models.CarReservation.objects.filter(car__dealer=dealer).count() # Filter reservations by dealer's cars
|
|
|
|
# # We need to process the cars into the inventory structure FIRST,
|
|
# # then paginate the list of makes.
|
|
# inventory_data = {}
|
|
# for car in cars:
|
|
# make = car.id_car_make
|
|
# if make.id_car_make not in inventory_data:
|
|
# inventory_data[make.id_car_make] = {
|
|
# "make_id": make.id_car_make,
|
|
# "slug": make.slug,
|
|
# "make_name": make.get_local_name(),
|
|
# "total_cars": 0,
|
|
# "models": {},
|
|
# }
|
|
# inventory_data[make.id_car_make]["total_cars"] += 1
|
|
|
|
# model = car.id_car_model
|
|
# # Ensure model exists before trying to access its attributes
|
|
# if model:
|
|
# if model.id_car_model not in inventory_data[make.id_car_make]["models"]:
|
|
# inventory_data[make.id_car_make]["models"][model.id_car_model] = {
|
|
# "model_id": model.id_car_model,
|
|
# "slug": model.slug,
|
|
# "model_name": model.get_local_name(),
|
|
# "total_cars": 0,
|
|
# "trims": {},
|
|
# }
|
|
# inventory_data[make.id_car_make]["models"][model.id_car_model]["total_cars"] += 1
|
|
|
|
# trim = car.id_car_trim
|
|
# if trim: # Ensure trim exists
|
|
# if trim.id_car_trim not in inventory_data[make.id_car_make]["models"][model.id_car_model]["trims"]:
|
|
# inventory_data[make.id_car_make]["models"][model.id_car_model]["trims"][
|
|
# trim.id_car_trim
|
|
# ] = {
|
|
# "trim_id": trim.id_car_trim,
|
|
# "slug": trim.slug,
|
|
# "trim_name": trim.name,
|
|
# "total_cars": 0,
|
|
# }
|
|
# inventory_data[make.id_car_make]["models"][model.id_car_model]["trims"][
|
|
# trim.id_car_trim
|
|
# ]["total_cars"] += 1
|
|
|
|
# # Convert the inventory dictionary into a list of makes for pagination
|
|
# # Sort the makes by name for consistent pagination
|
|
# all_makes = sorted(inventory_data.values(), key=lambda x: x['make_name'])
|
|
|
|
# # --- Pagination Logic ---
|
|
# items_per_page = 10 # You can adjust this number
|
|
# paginator = Paginator(all_makes, items_per_page)
|
|
|
|
# page_number = request.GET.get('page')
|
|
# try:
|
|
# page_obj = paginator.get_page(page_number)
|
|
# except PageNotAnInteger:
|
|
# page_obj = paginator.get_page(1)
|
|
# except EmptyPage:
|
|
# page_obj = paginator.get_page(paginator.num_pages)
|
|
|
|
# # The 'makes' list for the current page
|
|
# # Ensure models and trims are also converted to lists within each make on the current page
|
|
# paginated_makes_list = []
|
|
# for make_data in page_obj.object_list:
|
|
# make_data_copy = make_data.copy() # Avoid modifying the original dict during iteration
|
|
# make_data_copy['models'] = []
|
|
# for model_id, model_data in make_data['models'].items():
|
|
# model_data_copy = model_data.copy()
|
|
# model_data_copy['trims'] = list(model_data['trims'].values())
|
|
# make_data_copy['models'].append(model_data_copy)
|
|
# paginated_makes_list.append(make_data_copy)
|
|
|
|
|
|
# result = {
|
|
# "total_cars": total_cars,
|
|
# "reserved_cars": reserved_cars,
|
|
# "makes": paginated_makes_list, # This is now the paginated list of makes
|
|
# }
|
|
|
|
# # print(result["makes"]) # For debugging
|
|
# context = {
|
|
# "inventory": result,
|
|
# "page_obj": page_obj, # Pass the page_obj to the template
|
|
# "is_paginated": page_obj.has_other_pages(), # To use in template for conditional rendering
|
|
# }
|
|
# return render(request, "inventory/inventory_stats.html", context)
|
|
|
|
|
|
class CarDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Provides a detailed view of a single car instance.
|
|
|
|
This class-based view is designed to display detailed information about a
|
|
single car instance from the inventory. It utilizes Django's built-in
|
|
DetailView along with mixins to enforce authentication and permission
|
|
requirements. The view ensures that only authenticated users with the proper
|
|
permissions can access car details.
|
|
|
|
:ivar model: The model associated with this view, representing the Car model.
|
|
:type model: Model
|
|
:ivar template_name: The path to the template used to render the detailed
|
|
car view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable that contains
|
|
the car object.
|
|
:type context_object_name: str
|
|
:ivar permission_required: A list of permissions required to access the
|
|
view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = models.Car
|
|
template_name = "inventory/car_detail.html"
|
|
context_object_name = "car"
|
|
permission_required = ["inventory.view_car"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
form = forms.CarDetailsEstimateCreate()
|
|
form.fields["customer"].queryset = form.fields["customer"].queryset.filter(
|
|
dealer=self.request.dealer
|
|
)
|
|
context["estimate_form"] = form
|
|
context["active_estimates"] = self.object.get_active_estimates()
|
|
|
|
return context
|
|
|
|
|
|
def CarFinanceUpdateView(request, dealer_slug, slug):
|
|
car = get_object_or_404(models.Car, slug=slug)
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
|
|
if request.method == "POST":
|
|
form = forms.CarFinanceForm(request.POST, instance=car)
|
|
if form.is_valid():
|
|
form.save()
|
|
return redirect("car_detail", dealer_slug=dealer.slug, slug=car.slug)
|
|
else:
|
|
messages.error(request, "Please correct the errors below.")
|
|
else:
|
|
form = forms.CarFinanceForm(instance=car)
|
|
|
|
return render(
|
|
request,
|
|
"inventory/car_finance_form.html",
|
|
{"car": car, "dealer": dealer, "form": form},
|
|
)
|
|
|
|
|
|
class CarUpdateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
|
):
|
|
"""
|
|
Represents a Django view dedicated to updating Car model instances.
|
|
|
|
This view facilitates secure and authorized updates to car-related information
|
|
through a web interface. It ensures permission checks, requires user login for
|
|
access, and communicates feedback upon successful updates.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: models.Model
|
|
:ivar form_class: The form class used for updating the model instance.
|
|
:type form_class: django.forms.ModelForm
|
|
:ivar template_name: The path to the template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar success_message: Message displayed upon successful update.
|
|
:type success_message: str
|
|
:ivar permission_required: List of permissions required to access this view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
model = models.Car
|
|
form_class = forms.CarUpdateForm
|
|
template_name = "inventory/car_edit.html"
|
|
success_message = _("Car updated successfully")
|
|
permission_required = ["inventory.change_car"]
|
|
|
|
def get_success_url(self):
|
|
return reverse(
|
|
"car_detail",
|
|
kwargs={"dealer_slug": self.request.dealer.slug, "slug": self.object.slug},
|
|
)
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
dealer = get_user_type(self.request)
|
|
form.fields["vendor"].queryset = dealer.vendors.filter(active=True)
|
|
return form
|
|
|
|
|
|
class CarDeleteView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, DeleteView
|
|
):
|
|
"""
|
|
Handles the deletion of Car objects in the inventory application.
|
|
|
|
This view ensures that only authorized users can delete a car. It requires users to be logged in
|
|
and have the appropriate permissions. Upon successful deletion, a success message is displayed,
|
|
and the user is redirected to the inventory statistics page.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: models.Car
|
|
:ivar template_name: The path to the HTML template used for confirming a car deletion.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to after successful deletion.
|
|
:type success_url: str
|
|
:ivar permission_required: A list of permission strings required to access this view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
model = models.Car
|
|
template_name = "inventory/car_confirm_delete.html"
|
|
|
|
permission_required = ["inventory.delete_car"]
|
|
|
|
def delete(self, request, *args, **kwargs):
|
|
messages.success(request, _("Car deleted successfully"))
|
|
return super().delete(request, *args, **kwargs)
|
|
|
|
def get_success_url(self):
|
|
"""
|
|
Returns the URL to redirect to after a successful car deletion.
|
|
It dynamically includes the dealer_slug from the URL.
|
|
"""
|
|
|
|
dealer_slug = self.kwargs.get("dealer_slug")
|
|
if dealer_slug:
|
|
return reverse_lazy("car_list", kwargs={"dealer_slug": dealer_slug})
|
|
else:
|
|
messages.error(
|
|
self.request, _("Could not determine dealer for redirection.")
|
|
)
|
|
return reverse_lazy("home")
|
|
|
|
|
|
class CarLocationCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
|
"""
|
|
Handles the creation of new car locations.
|
|
|
|
This view allows authenticated and authorized users to add new car locations
|
|
to the system. The form allows users to specify details regarding the car's
|
|
location. Permissions are required to ensure only authorized users are
|
|
permitted to perform this action. Upon successful form submission, the
|
|
user is redirected to the car's detail page and a success message is displayed.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: models.CarLocation
|
|
:ivar form_class: The form class used to create or update the model.
|
|
:type form_class: forms.CarLocationForm
|
|
:ivar template_name: Path to the template used by the view.
|
|
:type template_name: str
|
|
:ivar permission_required: List of permissions required to create a car location.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = models.CarLocation
|
|
form_class = forms.CarLocationForm
|
|
template_name = "inventory/car_location_form.html"
|
|
permission_required = ["inventory.add_carlocation"]
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy("car_detail", kwargs={"slug": self.object.car.slug})
|
|
|
|
def form_valid(self, form):
|
|
form.instance.car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
|
dealer = get_user_type(self.request)
|
|
form.instance.owner = dealer
|
|
form.save()
|
|
messages.success(self.request, _("Location saved successfully"))
|
|
return super().form_valid(form)
|
|
|
|
|
|
class CarLocationUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
|
"""
|
|
Provides functionality to update the location of a car.
|
|
|
|
This view is a subclass of `LoginRequiredMixin` and `PermissionRequiredMixin`, ensuring
|
|
that only authenticated and authorized users with the appropriate permissions can access it.
|
|
It provides a form to update car location details and handles the necessary updates to the
|
|
location record, associating it with the appropriate car and owner. Upon successful update,
|
|
a success message is displayed to the user, and the user is redirected to the car detail page.
|
|
|
|
:ivar model: The model associated with this view. Represents car location.
|
|
:type model: models.CarLocation
|
|
:ivar form_class: The form class used for updating car location.
|
|
:type form_class: forms.CarLocationForm
|
|
:ivar template_name: The path to the template used to render the view.
|
|
:type template_name: str
|
|
:ivar permission_required: Permissions required to access this view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = models.CarLocation
|
|
form_class = forms.CarLocationForm
|
|
template_name = "inventory/car_location_form.html"
|
|
permission_required = ["inventory.update_carlocation"]
|
|
|
|
# def get_initial(self):
|
|
# initial = super().get_initial()
|
|
# initial["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
|
# return initial
|
|
|
|
def form_valid(self, form):
|
|
form.instance.car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
|
dealer = get_user_type(self.request)
|
|
form.instance.owner = dealer
|
|
form.save()
|
|
messages.success(self.request, _("Location updated successfully"))
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy("car_detail", kwargs={"slug": self.object.car.slug})
|
|
|
|
|
|
class CarTransferCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
|
"""
|
|
A view for creating car transfers.
|
|
|
|
This class-based view allows authenticated users to create car transfers. It handles the
|
|
selection of cars and dealers involved in the transfer process. The view inherits from
|
|
`LoginRequiredMixin` to ensure only logged-in users can access it, and `CreateView` to
|
|
simplify the implementation of the creation logic.
|
|
|
|
:ivar model: The model class associated with the view.
|
|
:type model: models.CarTransfer
|
|
:ivar form_class: The form class used to render and validate the input data.
|
|
:type form_class: forms.CarTransferForm
|
|
:ivar template_name: The path to the template used to render the view.
|
|
:type template_name: str
|
|
"""
|
|
|
|
model = models.CarTransfer
|
|
form_class = forms.CarTransferForm
|
|
template_name = "inventory/car_transfer_form.html"
|
|
permission_required = ["inventory.add_cartransfer"]
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
form.fields["to_dealer"].queryset = models.Dealer.objects.exclude(
|
|
pk=get_user_type(self.request).pk
|
|
).all()
|
|
form.fields["car"].queryset = models.Car.objects.filter(
|
|
slug=self.kwargs["slug"]
|
|
)
|
|
return form
|
|
|
|
def get_initial(self):
|
|
initial = super().get_initial()
|
|
initial["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
|
return initial
|
|
|
|
def form_valid(self, form):
|
|
form.instance.from_dealer = get_user_type(self.request)
|
|
form.instance.car.status = "transfer"
|
|
form.instance.car.save()
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy("car_detail", kwargs={"slug": self.object.car.slug})
|
|
|
|
|
|
class CarTransferDetailView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, DetailView
|
|
):
|
|
"""
|
|
Provides a detailed view of a specific car transfer record.
|
|
|
|
This class-based view is used to display details of a car transfer for
|
|
authenticated users. It ensures that only authorized users can access this
|
|
information and renders the data along with additional context such as
|
|
specific actions tied to the transfer. The view is associated with a Django
|
|
model for car transfers and uses a specific template to display information.
|
|
|
|
:ivar model: The model associated with this view, which represents car
|
|
transfer records.
|
|
:type model: Type[models.CarTransfer]
|
|
:ivar template_name: The path to the template used for rendering the car
|
|
transfer details.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context object used in the template
|
|
to reference the car transfer record.
|
|
:type context_object_name: str
|
|
"""
|
|
|
|
model = models.CarTransfer
|
|
template_name = "inventory/transfer_details.html"
|
|
context_object_name = "transfer"
|
|
permission_required = ["inventory.view_cartransfer"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["action"] = self.request.GET.get("action")
|
|
return context
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.change_cartransfer")
|
|
def car_transfer_approve(request, slug, transfer_pk):
|
|
"""
|
|
Approves or cancels a car transfer request based on the action parameter. This view
|
|
handles the workflow of updating the transfer status and notifying the involved parties
|
|
accordingly. If the transfer is canceled, it reverts the car status to "available" and
|
|
deactivates the transfer record. If approved, it notifies the recipient dealer and allows
|
|
the request to proceed for further actions.
|
|
|
|
:param request: The HTTP request object containing metadata and the action parameter.
|
|
:param car_pk: Primary key of the car involved in the transfer.
|
|
:param transfer_pk: Primary key of the transfer request to approve or cancel.
|
|
:return: An HTTP response redirecting to the car detail page of the specified car.
|
|
"""
|
|
car = get_object_or_404(models.Car, slug=slug)
|
|
transfer = get_object_or_404(models.CarTransfer, pk=transfer_pk)
|
|
action = request.GET.get("action")
|
|
if action == "cancel":
|
|
transfer.status = "cancel"
|
|
transfer.active = False
|
|
transfer.save()
|
|
transfer.car.status = "available"
|
|
transfer.car.save()
|
|
messages.success(request, _("Car transfer canceled successfully"))
|
|
models.Notification.objects.create(
|
|
user=transfer.from_dealer.user,
|
|
message=f"Car transfer request from {transfer.to_dealer} is canceled.",
|
|
)
|
|
return redirect("car_detail", slug=car.slug)
|
|
transfer.status = "approved"
|
|
transfer.save()
|
|
url = request.build_absolute_uri(
|
|
reverse(
|
|
"transfer_preview", kwargs={"slug": car.slug, "transfer_pk": transfer.pk}
|
|
)
|
|
)
|
|
models.Notification.objects.create(
|
|
user=transfer.to_dealer.user,
|
|
message=f"Car transfer request from {transfer.from_dealer} is waiting for your acceptance. <a href='{url}'> Accept</a>",
|
|
)
|
|
messages.success(request, _("Car transfer approved successfully"))
|
|
return redirect("car_detail", slug=car.slug)
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.change_cartransfer")
|
|
def car_transfer_accept_reject(request, slug, transfer_pk):
|
|
"""
|
|
Handles the acceptance or rejection of a car transfer request. Based on the
|
|
`status` parameter obtained from the query string, the function updates the
|
|
transfer status to either 'accept' or 'reject'. If the transfer is accepted, it
|
|
initiates the car transfer process. Appropriate notifications are sent, and
|
|
activity records are created for both acceptance and rejection actions.
|
|
|
|
:param request: The HTTP request object which contains metadata about
|
|
the request made by the user, including session and user information.
|
|
:param car_pk: The primary key of the car to be transferred.
|
|
:param transfer_pk: The primary key of the car transfer request to be processed.
|
|
:return: An HTTP redirect response to the 'inventory_stats' view.
|
|
"""
|
|
car = get_object_or_404(models.Car, slug=slug)
|
|
transfer = get_object_or_404(models.CarTransfer, pk=transfer_pk)
|
|
status = request.GET.get("status")
|
|
if status == "rejected":
|
|
transfer.status = "reject"
|
|
transfer.active = False
|
|
messages.success(request, _("Car transfer rejected successfully"))
|
|
models.Notification.objects.create(
|
|
user=transfer.from_dealer.user,
|
|
message=f"Car transfer request from {transfer.to_dealer} is rejected.",
|
|
)
|
|
transfer.save()
|
|
elif status == "accepted":
|
|
transfer.status = "accept"
|
|
transfer.save()
|
|
transfer_process = CarTransfer(car, transfer)
|
|
success = transfer_process.transfer_car()
|
|
if success:
|
|
messages.success(request, _("Car Transfer Completed successfully."))
|
|
models.Activity.objects.create(
|
|
content_object=car,
|
|
notes=f"Transfered from {transfer.from_dealer} to {transfer.to_dealer}",
|
|
created_by=request.user,
|
|
)
|
|
models.Notification.objects.create(
|
|
user=transfer.from_dealer.user,
|
|
message=f"Car transfer request from {transfer.to_dealer} is completed.",
|
|
)
|
|
return redirect("inventory_stats")
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.view_cartransfer")
|
|
def CarTransferPreviewView(request, slug, transfer_pk):
|
|
"""
|
|
Handles the preview of car transfer details and ensures that a user has appropriate
|
|
permissions to view the transfer based on their associated dealer.
|
|
|
|
This view checks if the car transfer's destination dealer matches the current user's
|
|
associated dealer type. If not, the user is redirected to the car detail page. Otherwise,
|
|
it renders the transfer preview page with the relevant transfer details.
|
|
|
|
:param request: The HTTP request object
|
|
:type request: django.http.HttpRequest
|
|
:param car_pk: The primary key of the car related to the transfer
|
|
:type car_pk: int
|
|
:param transfer_pk: The primary key of the car transfer to preview
|
|
:type transfer_pk: int
|
|
:return: An HTTP response rendering the transfer preview page or redirecting
|
|
to the car detail page
|
|
:rtype: django.http.HttpResponse
|
|
"""
|
|
transfer = get_object_or_404(models.CarTransfer, pk=transfer_pk)
|
|
if transfer.to_dealer != get_user_type(request):
|
|
return redirect("car_detail", slug=slug)
|
|
return render(request, "inventory/transfer_preview.html", {"transfer": transfer})
|
|
|
|
|
|
class CustomCardCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
|
"""
|
|
Represents a view for creating a custom card associated with a specific car.
|
|
|
|
This view ensures that the user is authenticated before allowing access and
|
|
associates the created custom card with a specific car. It handles form validation,
|
|
context data injection, and determines the success URL upon successful form submission.
|
|
|
|
:ivar model: The model associated with the view, which is `CustomCard`.
|
|
:type model: models.CustomCard
|
|
:ivar form_class: The form class used to create a new instance of `CustomCard`.
|
|
:type form_class: forms.CustomCardForm
|
|
:ivar template_name: The name of the template used to render the view.
|
|
:type template_name: str
|
|
"""
|
|
|
|
model = models.CustomCard
|
|
form_class = forms.CustomCardForm
|
|
template_name = "inventory/add_custom_card.html"
|
|
permission_required = "inventory.add_customcard"
|
|
|
|
def form_valid(self, form):
|
|
car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
|
form.instance.car = car
|
|
return super().form_valid(form)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
|
return context
|
|
|
|
def get_success_url(self):
|
|
messages.success(self.request, _("Custom Card added successfully"))
|
|
return reverse_lazy(
|
|
"car_detail",
|
|
kwargs={
|
|
"dealer_slug": self.request.dealer.slug,
|
|
"slug": self.kwargs["slug"],
|
|
},
|
|
)
|
|
|
|
|
|
class CarRegistrationCreateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, CreateView
|
|
):
|
|
"""
|
|
Handles the creation of new car registration records.
|
|
|
|
This view is responsible for rendering the car registration creation form,
|
|
validating input, and saving the data to the database. It ensures that the
|
|
current user is authenticated and associates the registration with a specific
|
|
car, identified by its primary key.
|
|
|
|
Inherits from:
|
|
- LoginRequiredMixin: Ensures the user is logged in to access the view.
|
|
- CreateView: Provides built-in functionality for creating and saving model
|
|
instances.
|
|
|
|
:ivar model: The model linked to this view, representing car registrations.
|
|
:type model: models.CarRegistration
|
|
:ivar form_class: The form class used for creating car registration instances.
|
|
:type form_class: forms.CarRegistrationForm
|
|
:ivar template_name: The path to the HTML template used for rendering the car
|
|
registration form.
|
|
:type template_name: str
|
|
"""
|
|
|
|
model = models.CarRegistration
|
|
form_class = forms.CarRegistrationForm
|
|
template_name = "inventory/car_registration_form.html"
|
|
permission_required = "inventory.add_carregistration"
|
|
|
|
def form_valid(self, form):
|
|
car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
|
form.instance.car = car
|
|
return super().form_valid(form)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
|
return context
|
|
|
|
def get_success_url(self):
|
|
messages.success(self.request, _("Registration added successfully"))
|
|
return reverse_lazy(
|
|
"car_detail",
|
|
kwargs={
|
|
"dealer_slug": self.request.dealer.slug,
|
|
"slug": self.kwargs["slug"],
|
|
},
|
|
)
|
|
|
|
|
|
@login_required()
|
|
def reserve_car_view(request, dealer_slug, slug):
|
|
"""
|
|
Handles car reservation requests. This view requires the user to be logged in
|
|
and processes only POST requests. When invoked, it checks if the specified car
|
|
is already reserved. If not, it proceeds to reserve the car for the user and
|
|
sends an appropriate response. If the car is already reserved or if the request
|
|
method is invalid, it provides corresponding error messages or responses.
|
|
|
|
:param request: The HTTP request object.
|
|
:type request: HttpRequest
|
|
:param car_id: The unique identifier of the car to be reserved.
|
|
:type car_id: int
|
|
:return: A response indicating the result of the reservation process.
|
|
:rtype: HttpResponse or JsonResponse
|
|
"""
|
|
if request.method == "POST":
|
|
car = get_object_or_404(models.Car, slug=slug)
|
|
if car.is_reserved():
|
|
messages.error(request, _("This car is already reserved"))
|
|
return redirect("car_detail", slug=car.slug)
|
|
response = reserve_car(car, request)
|
|
return response
|
|
return JsonResponse(
|
|
{"success": False, "message": "Invalid request method"}, status=400
|
|
)
|
|
|
|
|
|
@login_required
|
|
def manage_reservation(request, dealer_slug, reservation_id):
|
|
"""
|
|
Handles the management of a car reservation, providing options to renew or
|
|
cancel an existing reservation associated with the logged-in user.
|
|
|
|
Renewing a reservation extends the reservation period by an additional
|
|
24 hours. Canceling a reservation deletes it and updates the car's status
|
|
to AVAILABLE. All actions require a valid reservation and are performed
|
|
based on the current user's authentication and request type.
|
|
|
|
:param request: Django HttpRequest object representing the client's request.
|
|
:type request: HttpRequest
|
|
:param reservation_id: The unique identifier of the car reservation to manage.
|
|
:type reservation_id: int
|
|
:return: On POST requests, returns an HTTP redirect or JSON response
|
|
based on the action performed. On other request methods,
|
|
returns a JSON response with an error message.
|
|
:rtype: JsonResponse or HttpResponseRedirect
|
|
"""
|
|
reservation = get_object_or_404(
|
|
models.CarReservation, pk=reservation_id, reserved_by=request.user
|
|
)
|
|
|
|
if request.method == "POST":
|
|
action = request.POST.get("action")
|
|
if action == "renew":
|
|
reservation.reserved_until = timezone.now() + timezone.timedelta(hours=24)
|
|
reservation.save()
|
|
messages.success(request, _("Reservation renewed successfully"))
|
|
return redirect(
|
|
"car_detail", dealer_slug=request.dealer.slug, slug=reservation.car.slug
|
|
)
|
|
|
|
elif action == "cancel":
|
|
car = reservation.car
|
|
reservation.delete()
|
|
car.status = models.CarStatusChoices.AVAILABLE
|
|
car.save()
|
|
messages.success(request, _("Reservation canceled successfully"))
|
|
return redirect(
|
|
"car_detail", dealer_slug=request.dealer.slug, slug=reservation.car.slug
|
|
)
|
|
|
|
else:
|
|
return JsonResponse(
|
|
{"success": False, "message": _("Invalid action")}, status=400
|
|
)
|
|
|
|
return JsonResponse(
|
|
{"success": False, "message": _("Invalid request method")}, status=400
|
|
)
|
|
|
|
|
|
class DealerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Represents a detailed view for a Dealer model.
|
|
|
|
This class extends Django's `DetailView` to provide a detailed view of a dealer.
|
|
It includes additional context data such as the count of staff members, cars
|
|
associated with the dealer, available car makes, and dynamically fetched quotas.
|
|
The class also ensures that users must be logged in to access the detailed view.
|
|
|
|
:ivar model: The model associated with this view (Dealer model).
|
|
:type model: django.db.models.Model
|
|
:ivar template_name: Path to the template used to render the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name used to refer to the object in the template context.
|
|
:type context_object_name: str
|
|
"""
|
|
|
|
model = models.Dealer
|
|
template_name = "dealers/dealer_detail.html"
|
|
context_object_name = "dealer"
|
|
permission_required = "inventory.view_dealer"
|
|
|
|
def get_queryset(self):
|
|
return models.Dealer.objects.annotate(
|
|
staff_count=Coalesce(
|
|
Count("staff"), Value(0)
|
|
) # Get the number of staff members
|
|
)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
dealer = self.object
|
|
car_makes = models.CarMake.objects.filter(
|
|
car_dealers__dealer=dealer, is_sa_import=True
|
|
)
|
|
staff_count = dealer.staff_count
|
|
cars_count = models.Car.objects.filter(dealer=dealer).count()
|
|
|
|
context["car_makes"] = car_makes
|
|
context["staff_count"] = staff_count
|
|
context["cars_count"] = cars_count
|
|
context["allowed_users"] = dealer.user_quota
|
|
context["allowed_cars"] = dealer.car_quota
|
|
context["vatform"] = forms.VatRateForm(initial={"rate": dealer.vat_rate})
|
|
context["quota_display"] = (
|
|
f"{staff_count}/{dealer.user_quota}" if dealer.user_quota else "0"
|
|
)
|
|
return context
|
|
|
|
|
|
from .forms import VatRateForm
|
|
|
|
|
|
@login_required
|
|
def dealer_vat_rate_update(request, slug):
|
|
dealer = get_object_or_404(models.Dealer, slug=slug)
|
|
|
|
vat_rate_instance, created = models.VatRate.objects.get_or_create(dealer=dealer)
|
|
|
|
if request.method == "POST":
|
|
form = VatRateForm(request.POST, instance=vat_rate_instance)
|
|
|
|
if form.is_valid():
|
|
form.save()
|
|
messages.success(request, _("VAT rate updated successfully"))
|
|
return redirect("dealer_detail", slug=slug)
|
|
else:
|
|
messages.error(request, _("Please enter valid vat rate between 0 and 1."))
|
|
redirect("dealer_detail", slug=slug)
|
|
|
|
return redirect("dealer_detail", slug=slug)
|
|
|
|
|
|
class DealerUpdateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
|
):
|
|
"""
|
|
Handles the update functionality for the Dealer model.
|
|
|
|
This class-based view allows authenticated users to update an existing
|
|
Dealer instance using a form. Upon successful update, a success message
|
|
is displayed, and the user is redirected to the detail view of the updated
|
|
Dealer.
|
|
|
|
:ivar model: The model class associated with this view.
|
|
:type model: models.Dealer
|
|
:ivar form_class: The form class used for this view.
|
|
:type form_class: forms.DealerForm
|
|
:ivar template_name: The template used to render the form for this view.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to after a successful form submission.
|
|
:type success_url: str
|
|
:ivar success_message: The message displayed upon a successful update.
|
|
:type success_message: str
|
|
"""
|
|
|
|
model = models.Dealer
|
|
form_class = forms.DealerForm
|
|
template_name = "dealers/dealer_form.html"
|
|
success_url = reverse_lazy("dealer_detail")
|
|
success_message = _("Dealer updated successfully")
|
|
permission_required = ["inventory.change_dealer"]
|
|
|
|
def get_success_url(self):
|
|
return reverse("dealer_detail", kwargs={"slug": self.object.slug})
|
|
|
|
|
|
class StaffDetailView(LoginRequiredMixin, DetailView):
|
|
"""
|
|
Represents a detailed view for a Dealer model.
|
|
|
|
This class extends Django's `DetailView` to provide a detailed view of a dealer.
|
|
It includes additional context data such as the count of staff members, cars
|
|
associated with the dealer, available car makes, and dynamically fetched quotas.
|
|
The class also ensures that users must be logged in to access the detailed view.
|
|
|
|
:ivar model: The model associated with this view (Dealer model).
|
|
:type model: django.db.models.Model
|
|
:ivar template_name: Path to the template used to render the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name used to refer to the object in the template context.
|
|
:type context_object_name: str
|
|
"""
|
|
|
|
model = models.Staff
|
|
template_name = "staff/staff_detail.html"
|
|
context_object_name = "staff"
|
|
|
|
|
|
class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
View for displaying a list of customers.
|
|
|
|
This class-based view is used to display a paginated and searchable list of
|
|
customers. It ensures that the user has the required permissions and is
|
|
logged in before they can access the view. The view fetches a list of
|
|
customers related to the current user's entity and applies any search
|
|
filters based on the user's query. The data is rendered using a specified
|
|
template.
|
|
|
|
:ivar model: The model associated with the view, representing customer data.
|
|
:type model: type[CustomerModel]
|
|
:ivar home_label: The label used for navigation purposes in the UI.
|
|
:type home_label: str
|
|
:ivar context_object_name: The name of the context variable in the template.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: Number of items displayed per page for pagination.
|
|
:type paginate_by: int
|
|
:ivar template_name: The path of the template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar ordering: The default ordering applied to the queryset of customers.
|
|
:type ordering: list
|
|
:ivar permission_required: A list of permissions required to access the view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = models.Customer
|
|
home_label = _("customers")
|
|
context_object_name = "customers"
|
|
paginate_by = 30
|
|
template_name = "customers/customer_list.html"
|
|
ordering = ["-created"]
|
|
permission_required = ["inventory.view_customer"]
|
|
|
|
def get_queryset(self):
|
|
query = self.request.GET.get("q")
|
|
dealer = get_user_type(self.request)
|
|
customers = dealer.customers.filter(active=True)
|
|
return apply_search_filters(customers, query)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["query"] = self.request.GET.get("q", "")
|
|
context["empty_state_value"] = _("customers")
|
|
return context
|
|
|
|
|
|
# class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
# """
|
|
# CustomerDetailView handles retrieving and presenting detailed information about
|
|
# a specific customer. It ensures that the user is authenticated and has the
|
|
# necessary permissions before accessing the customer's details. This view
|
|
# provides context data including estimates and invoices related to the customer.
|
|
|
|
# :ivar model: The model associated with the view.
|
|
# :type model: CustomerModel
|
|
# :ivar template_name: The path to the template used for rendering the view.
|
|
# :type template_name: str
|
|
# :ivar context_object_name: The name of the variable in the template context
|
|
# for the object being viewed.
|
|
# :type context_object_name: str
|
|
# :ivar permission_required: The list of permissions required to access this view.
|
|
# :type permission_required: list[str]
|
|
# """
|
|
|
|
# model = models.Customer
|
|
# template_name = "customers/view_customer.html"
|
|
# context_object_name = "customer"
|
|
# permission_required = ["inventory.view_customer"]
|
|
|
|
# def get_context_data(self, **kwargs):
|
|
# dealer = get_user_type(self.request)
|
|
# entity = dealer.entity
|
|
# context = super().get_context_data(**kwargs)
|
|
|
|
# context["notes"] = models.Notes.objects.filter(
|
|
# dealer=dealer,
|
|
# content_type__model="customer", object_id=self.object.id
|
|
# )
|
|
# estimates = entity.get_estimates().filter(customer=self.object.customer_model)
|
|
# invoices = entity.get_invoices().filter(customer=self.object.customer_model)
|
|
|
|
# total = estimates.count() + invoices.count()
|
|
|
|
# context["estimates"] = estimates
|
|
# context["invoices"] = invoices
|
|
# context["total"] = total
|
|
# context["note_form"] = forms.NoteForm()
|
|
# return context
|
|
|
|
|
|
class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
CustomerDetailView handles retrieving and presenting detailed information about
|
|
a specific customer. It ensures that the user is authenticated and has the
|
|
necessary permissions before accessing the customer's details. This view
|
|
provides context data including estimates and invoices related to the customer.
|
|
|
|
:ivar model: The model associated with the view.
|
|
:type model: CustomerModel
|
|
:ivar template_name: The path to the template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the variable in the template context
|
|
for the object being viewed.
|
|
:type context_object_name: str
|
|
:ivar permission_required: The list of permissions required to access this view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
model = models.Customer
|
|
template_name = "customers/view_customer.html"
|
|
context_object_name = "customer"
|
|
permission_required = ["inventory.view_customer"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
dealer = get_user_type(self.request)
|
|
entity = dealer.entity
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
context["notes"] = models.Notes.objects.filter(
|
|
dealer=dealer, content_type__model="customer", object_id=self.object.id
|
|
)
|
|
estimates = entity.get_estimates().filter(customer=self.object.customer_model)
|
|
invoices = entity.get_invoices().filter(customer=self.object.customer_model)
|
|
context["leads"] = self.object.customer_leads.all()
|
|
|
|
total = estimates.count() + invoices.count()
|
|
|
|
context["estimates"] = estimates
|
|
context["invoices"] = invoices
|
|
context["total"] = total
|
|
context["note_form"] = forms.NoteForm()
|
|
return context
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.add_notes", raise_exception=True)
|
|
def add_note_to_customer(request, dealer_slug, slug):
|
|
"""
|
|
This function allows authenticated users to add a note to a specific customer. The
|
|
note creation is handled by a form, which is validated after submission. If the form
|
|
is valid, the note is saved and associated with the specified customer. On successful
|
|
submission, the user is redirected to the customer detail page. If the request method
|
|
is not POST, an empty form is rendered.
|
|
|
|
:param request: The HTTP request object containing metadata and the method type
|
|
(e.g., GET, POST). Should be an HttpRequest instance.
|
|
:param customer_id: The unique identifier (UUID) of the customer to whom the note
|
|
is to be added.
|
|
:return: An HTTP response. In the case of a successful POST request, the function
|
|
returns a redirect response to the customer detail page. For a GET or invalid
|
|
POST request, it renders the note form template with context including
|
|
the form and customer.
|
|
"""
|
|
# get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
customer = get_object_or_404(models.Customer, slug=slug)
|
|
if request.method == "POST":
|
|
form = forms.NoteForm(request.POST)
|
|
if form.is_valid():
|
|
note = form.save(commit=False)
|
|
note.content_object = customer
|
|
|
|
note.created_by = request.user
|
|
note.save()
|
|
return redirect(
|
|
"customer_detail", dealer_slug=dealer_slug, slug=customer.slug
|
|
)
|
|
|
|
form = forms.NoteForm()
|
|
return render(
|
|
request, "customers/note_form.html", {"form": form, "customer": customer}
|
|
)
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.add_activity", raise_exception=True)
|
|
def add_activity_to_customer(request, pk):
|
|
"""
|
|
Adds an activity to a specific customer.
|
|
|
|
This function allows adding a new activity to a customer identified by their
|
|
primary key (`pk`). It retrieves the customer object, processes the form for
|
|
activity creation, and saves it. If the request method is POST, it validates
|
|
the form and associates the activity with the respective customer. Upon
|
|
successful save, it redirects to the customer detail view. If the request
|
|
method is GET, it renders a form for activity submission.
|
|
|
|
:param request: The HTTP request object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the customer to which the activity will be added.
|
|
:type pk: int
|
|
:return: An HTTP response rendered with the activity form in the context of
|
|
the customer, or a redirect response to the customer detail view upon
|
|
successful activity creation.
|
|
:rtype: HttpResponse
|
|
"""
|
|
customer = get_object_or_404(CustomerModel, pk=pk)
|
|
if request.method == "POST":
|
|
form = forms.ActivityForm(request.POST)
|
|
if form.is_valid():
|
|
activity = form.save(commit=False)
|
|
activity.content_object = customer
|
|
activity.created_by = request.user
|
|
activity.save()
|
|
return redirect("customer_detail", pk=pk)
|
|
else:
|
|
form = forms.ActivityForm()
|
|
return render(
|
|
request, "crm/add_activity.html", {"form": form, "customer": customer}
|
|
)
|
|
|
|
|
|
class CustomerCreateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView
|
|
):
|
|
"""
|
|
# Handles the creation of a new customer within the system. This view ensures that proper permissions
|
|
# and request methods are utilized. It provides feedback to the user about the success or failure of
|
|
# the customer creation process. When the form is submitted and valid, it checks for duplicate
|
|
# customers based on the email provided before proceeding with the customer creation.
|
|
|
|
# :param request: The HTTP request object containing metadata about the request initiated by the user.
|
|
# :type request: HttpRequest
|
|
# :return: The rendered form page or a redirect to the customer list page upon successful creation.
|
|
# :rtype: HttpResponse
|
|
# :raises PermissionDenied: If the user does not have the required permissions to access the view.
|
|
#"""
|
|
|
|
model = models.Customer
|
|
form_class = forms.CustomerForm
|
|
permission_required = ["inventory.add_customer"]
|
|
permission_required = ["inventory.add_customer"]
|
|
template_name = "customers/customer_form.html"
|
|
success_url = reverse_lazy("customer_list")
|
|
success_message = _("Customer created successfully")
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
if customer := models.Customer.objects.filter(
|
|
dealer=dealer, email=form.instance.email
|
|
).first():
|
|
if not customer.active:
|
|
messages.error(
|
|
self.request,
|
|
_(
|
|
"Customer Account with this email is Deactivated,Please Contact Admin"
|
|
),
|
|
)
|
|
else:
|
|
messages.error(
|
|
self.request, _("Customer with this email already exists")
|
|
)
|
|
return redirect("customer_create")
|
|
dealer = get_user_type(self.request)
|
|
form.instance.dealer = dealer
|
|
# try:
|
|
# user = form.instance.create_user_model()
|
|
# logger.info(
|
|
# f"Successfully created Customer with '{user.username}' (ID: {user.id}) "
|
|
# f"with email '{user.email}' for dealer '{dealer.name}'."
|
|
# )
|
|
# except IntegrityError as e:
|
|
# if "UNIQUE constraint" in str(e):
|
|
# messages.error(self.request, _("Email already exists"))
|
|
# logger.info(
|
|
# f"Attempted to create user with existing email '{form.instance.email}' "
|
|
# f"for dealer '{dealer.name}'. Message: '{e}'"
|
|
# )
|
|
# else:
|
|
# logger.error(
|
|
# f"An unexpected IntegrityError occurred while creating user(customer) with email '{form.instance.email}' "
|
|
# f"for dealer '{dealer.name}'. Error: {e}",
|
|
# exc_info=True,
|
|
# )
|
|
# messages.error(self.request, str(e))
|
|
# return redirect("customer_create")
|
|
customer = form.instance.create_customer_model()
|
|
|
|
# form.instance.user = user
|
|
form.instance.customer_model = customer
|
|
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"customer_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
|
|
)
|
|
|
|
|
|
class CustomerUpdateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
|
):
|
|
"""
|
|
# Updates the details of an existing customer in the database. This view is
|
|
# accessible only to logged-in users with the appropriate permissions. It
|
|
# handles both GET (form rendering with pre-filled customer data) and POST
|
|
# (submitting updates) requests. Data validation and customer updates are
|
|
# conducted based on the received form data.
|
|
|
|
# :param request: The HTTP request object used to determine the request method,
|
|
# access user session details, and provide request data such as POST content.
|
|
# Expected to contain the updated customer data if request method is POST.
|
|
# :type request: HttpRequest
|
|
|
|
# :param pk: The primary key of the CustomerModel object that is to be updated.
|
|
# :type pk: int
|
|
|
|
# :return: A rendered HTML template displaying the customer form pre-filled
|
|
# with existing data if a GET request is received. On successful form
|
|
# submission (POST request), redirects to the customer list page
|
|
# and displays a success message. In case of invalid data or errors,
|
|
# returns the rendered form template with the validation errors.
|
|
# :rtype: HttpResponse
|
|
#"""
|
|
|
|
model = models.Customer
|
|
form_class = forms.CustomerForm
|
|
permission_required = ["inventory.change_customer"]
|
|
permission_required = ["inventory.change_customer"]
|
|
template_name = "customers/customer_form.html"
|
|
success_url = reverse_lazy("customer_list")
|
|
success_message = _("Customer updated successfully")
|
|
|
|
def form_valid(self, form):
|
|
# form.instance.update_user_model()
|
|
form.instance.update_customer_model()
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"customer_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
|
|
)
|
|
|
|
|
|
@permission_required("inventory.delete_customer", raise_exception=True)
|
|
@login_required
|
|
def delete_customer(request, dealer_slug, slug):
|
|
"""
|
|
Deletes a customer from the system and deactivates the corresponding user account.
|
|
|
|
This function retrieves a customer object based on the primary key (pk),
|
|
sets their active status to False, and deactivates the linked user account
|
|
(using the email associated with the customer). After saving these changes,
|
|
it displays a success message to the user and redirects to the customer list page.
|
|
|
|
:param request: A HttpRequest object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: Primary key of the customer to be deleted.
|
|
:type pk: int
|
|
:return: A redirect response to the customer list page.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
customer = get_object_or_404(models.Customer, slug=slug)
|
|
customer.deactivate_account()
|
|
messages.success(request, _("Customer deactivated successfully"))
|
|
return redirect("customer_list", dealer_slug=dealer_slug)
|
|
|
|
|
|
class VendorListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Represents a view for displaying a paginated list of vendors, accessible only
|
|
to authenticated users.
|
|
|
|
This class inherits from ``LoginRequiredMixin`` and ``ListView`` and is
|
|
designed to display a paginated list of vendors associated with a user. It
|
|
utilizes filters and search capabilities based on user input and is rendered
|
|
using a specified template. The list is ordered by the creation date of the
|
|
vendors in descending order.
|
|
|
|
:ivar model: The model that this view interacts with.
|
|
:type model: VendorModel
|
|
:ivar context_object_name: The name of the context variable used in the
|
|
template for the list of vendors.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of vendors to display per page in the paginated
|
|
view.
|
|
:type paginate_by: int
|
|
:ivar template_name: The path to the template used to render the list of
|
|
vendors.
|
|
:type template_name: str
|
|
:ivar ordering: The default ordering applied to the queryset. Vendors are
|
|
ordered by their creation date in descending order.
|
|
:type ordering: list
|
|
"""
|
|
|
|
model = models.Vendor
|
|
context_object_name = "vendors"
|
|
paginate_by = 30
|
|
template_name = "vendors/vendors_list.html"
|
|
permission_required = ["inventory.view_vendor"]
|
|
|
|
def get_queryset(self):
|
|
query = self.request.GET.get("q")
|
|
dealer = get_user_type(self.request)
|
|
vendors = super().get_queryset().filter(dealer=dealer, active=True)
|
|
if query:
|
|
return apply_search_filters(vendors, query)
|
|
return vendors
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.view_vendor", raise_exception=True)
|
|
def vendorDetailView(request, dealer_slug, slug):
|
|
"""
|
|
Fetches and renders the detail view for a specific vendor.
|
|
|
|
This function retrieves a vendor object based on the primary key (pk)
|
|
provided in the URL, ensures the user is logged in to access the
|
|
view, and renders the vendor detail template with the vendor's context.
|
|
|
|
:param request: The HTTP request object.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the vendor to retrieve.
|
|
:type pk: int
|
|
:return: An HttpResponse object containing the rendered vendor detail page.
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
vendor = get_object_or_404(models.Vendor, slug=slug, dealer=dealer)
|
|
cars = vendor.cars.all()
|
|
total_cars_from_vendor = cars.count()
|
|
vendor_makes = cars.values("id_car_make__name").annotate(
|
|
make_count=Count("id_car_make__name")
|
|
)
|
|
vendor_bills = BillModel.objects.filter(vendor=vendor.vendor_model)
|
|
paginator = Paginator(vendor_bills, 20)
|
|
page_number = request.GET.get("page")
|
|
page_obj = paginator.get_page(page_number)
|
|
return render(
|
|
request,
|
|
template_name="vendors/view_vendor.html",
|
|
context={
|
|
"vendor": vendor,
|
|
"vendor_bills": page_obj,
|
|
"total_cars_from_vendor": total_cars_from_vendor,
|
|
"vendor_makes": vendor_makes,
|
|
},
|
|
)
|
|
|
|
|
|
class VendorCreateView(
|
|
LoginRequiredMixin,
|
|
PermissionRequiredMixin,
|
|
SuccessMessageMixin,
|
|
CreateView,
|
|
):
|
|
"""
|
|
Handles the creation of a new vendor.
|
|
|
|
This view allows authenticated users to create a new vendor entry. It uses the
|
|
Vendor model and its corresponding form for creation. A success message is displayed
|
|
upon successful creation, and the user is redirected to the list of vendors.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: models.Vendor
|
|
:ivar form_class: The form class used for creating a vendor.
|
|
:type form_class: forms.VendorForm
|
|
:ivar template_name: The name of the template used to render the view.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to after successful vendor creation.
|
|
:type success_url: str
|
|
:ivar success_message: The message displayed upon successful creation.
|
|
:type success_message: str
|
|
"""
|
|
|
|
model = models.Vendor
|
|
form_class = forms.VendorForm
|
|
template_name = "vendors/vendor_form.html"
|
|
success_message = _("Vendor created successfully")
|
|
permission_required = ["inventory.add_vendor"]
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
if vendor := models.Vendor.objects.filter(
|
|
dealer=dealer, email=form.instance.email
|
|
).first():
|
|
if not vendor.active:
|
|
messages.error(
|
|
self.request,
|
|
_(
|
|
"Vendor Account with this email is Deactivated,Please Contact Admin"
|
|
),
|
|
)
|
|
else:
|
|
messages.error(self.request, _("Vendor with this email already exists"))
|
|
return redirect("vendor_create", dealer_slug=self.kwargs["dealer_slug"])
|
|
dealer = get_user_type(self.request)
|
|
form.instance.dealer = dealer
|
|
form.instance.save()
|
|
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"vendor_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
|
|
)
|
|
|
|
|
|
class VendorUpdateView(
|
|
LoginRequiredMixin,
|
|
PermissionRequiredMixin,
|
|
SuccessMessageMixin,
|
|
UpdateView,
|
|
):
|
|
"""
|
|
View for updating vendor information.
|
|
|
|
This class-based view is used to handle the update of vendor information.
|
|
It ensures that only authenticated users can access this page, provides
|
|
form handling functionality, and includes a success message upon completion.
|
|
The form fields are dynamically populated and validated before vendor
|
|
information is updated in the database.
|
|
|
|
:ivar model: The model that this view is based on.
|
|
:type model: models.Vendor
|
|
:ivar form_class: The form class used to validate and process vendor data.
|
|
:type form_class: forms.VendorForm
|
|
:ivar template_name: The path to the HTML template used to render this view.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to after successful data submission.
|
|
:type success_url: str
|
|
:ivar success_message: The message to display upon successful data update.
|
|
:type success_message: str
|
|
"""
|
|
|
|
model = models.Vendor
|
|
form_class = forms.VendorForm
|
|
template_name = "vendors/vendor_form.html"
|
|
success_message = _("Vendor updated successfully")
|
|
permission_required = ["inventory.change_vendor"]
|
|
|
|
# def get_initial(self):
|
|
# initial = super().get_initial()
|
|
# initial = self.object.additional_info
|
|
# return initial
|
|
|
|
def form_valid(self, form):
|
|
# instance = form.save(commit=False)
|
|
print(self.request.POST)
|
|
# instance.vendor_name = self.request.POST["name"]
|
|
# instance.vendor_number = self.request.POST["crn"]
|
|
# instance.address_1 = self.request.POST["address"]
|
|
# instance.phone = self.request.POST["phone_number"]
|
|
# instance.email = self.request.POST["email"]
|
|
# instance.tax_id_number = self.request.POST["vrn"]
|
|
# additionals = form.cleaned_data
|
|
# additionals["phone_number"] = str(additionals["phone_number"])
|
|
# instance.additional_info = additionals
|
|
# instance.save()
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"vendor_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
|
|
)
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.delete_vendor")
|
|
def delete_vendor(request, dealer_slug, slug):
|
|
"""
|
|
Deletes an existing vendor record from the database.
|
|
|
|
This function allows users with valid authentication to delete a specified
|
|
vendor object by its primary key. Upon successful deletion, a success message
|
|
is displayed, and the user is redirected to the vendor list page.
|
|
|
|
:param request: HttpRequest object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: Primary key of the vendor object to be deleted.
|
|
:type pk: int
|
|
:return: HttpResponseRedirect object for redirecting to the vendor list page.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
vendor = get_object_or_404(models.Vendor, slug=slug)
|
|
vendor.active = False
|
|
vendor.vendor_model.active = False
|
|
vendor.save()
|
|
messages.success(request, _("Vendor deleted successfully"))
|
|
return redirect("vendor_list", dealer_slug=dealer_slug)
|
|
|
|
|
|
# group
|
|
class GroupListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Represents a view for listing groups for a logged-in user.
|
|
|
|
This view is designed for authenticated users, inheriting functionalities
|
|
from `LoginRequiredMixin` to enforce authentication checks and `ListView`
|
|
to handle the display of a list of groups. It queries the groups related
|
|
to the user type (dealer) and displays the results in a paginated format
|
|
using a specified template.
|
|
|
|
:ivar model: The model used for retrieving group data.
|
|
:type model: type
|
|
:ivar context_object_name: The name of the context variable used to contain
|
|
the queryset of groups.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of groups listed on each page.
|
|
:type paginate_by: int
|
|
:ivar template_name: The path to the template used for rendering the group list.
|
|
:type template_name: str
|
|
"""
|
|
|
|
model = models.CustomGroup
|
|
context_object_name = "groups"
|
|
paginate_by = 10
|
|
template_name = "groups/group_list.html"
|
|
permission_required = ["inventory.view_customgroup"]
|
|
|
|
def get_queryset(self):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
return dealer.groups.all()
|
|
|
|
|
|
class GroupDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Represents the detail view for a specific group.
|
|
|
|
Handles the display of detailed information about a specific CustomGroup
|
|
instance. Requires the user to be logged in to access this view. The class
|
|
is designed to fetch a specific group instance and render its details using
|
|
the specified template.
|
|
|
|
:ivar model: The model that represents the group details being viewed.
|
|
:type model: models.CustomGroup
|
|
:ivar template_name: The name of the template used to render the group detail
|
|
view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The context variable name under which the group
|
|
instance will be available in the template.
|
|
:type context_object_name: str
|
|
"""
|
|
|
|
model = models.CustomGroup
|
|
template_name = "groups/group_detail.html"
|
|
context_object_name = "group"
|
|
permission_required = ["inventory.view_customgroup"]
|
|
|
|
|
|
class GroupCreateView(
|
|
LoginRequiredMixin,
|
|
PermissionRequiredMixin,
|
|
SuccessMessageMixin,
|
|
CreateView,
|
|
):
|
|
"""
|
|
Represents a view for creating a new group in the application.
|
|
|
|
This class provides a form-based interface for authenticated users to create
|
|
new group entities. It utilizes mixins to enforce login requirements and display
|
|
success messages upon successful group creation.
|
|
|
|
:ivar model: The model associated with the view.
|
|
:type model: models.CustomGroup
|
|
:ivar form_class: The form class used to create a new group.
|
|
:type form_class: forms.GroupForm
|
|
:ivar template_name: The template used to render the form for creating a group.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to be redirected to when the group creation is successful.
|
|
:type success_url: str
|
|
:ivar success_message: A message displayed upon successful creation of a group.
|
|
:type success_message: str
|
|
"""
|
|
|
|
model = models.CustomGroup
|
|
form_class = forms.GroupForm
|
|
template_name = "groups/group_form.html"
|
|
success_message = _("Group created successfully")
|
|
permission_required = ["inventory.add_customgroup"]
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
instance = form.save(commit=False)
|
|
group_name = f"{dealer.slug}_{instance.name}"
|
|
|
|
logger.debug(
|
|
f"Attempting to create or get Django Group '{group_name}' "
|
|
f"for dealer '{dealer.name}' (ID: {dealer.id})."
|
|
)
|
|
|
|
try:
|
|
group, created = Group.objects.get_or_create(name=group_name)
|
|
if created:
|
|
logger.info(f"Successfully created new Django Group: '{group_name}'.")
|
|
else:
|
|
logger.info(
|
|
f"Django Group '{group_name}' already exists and was retrieved."
|
|
)
|
|
instance.dealer = dealer
|
|
instance.group = group
|
|
instance.save()
|
|
logger.info(
|
|
f"Successfully created CustomGroup '{instance.name}' (ID: {instance.id}) "
|
|
f"linked to Django Group '{group.name}' for dealer '{dealer.name}'."
|
|
)
|
|
|
|
except IntegrityError as e:
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
print(e)
|
|
messages.error(self.request, _("Group name already exists"))
|
|
logger.error(
|
|
f"An unexpected IntegrityError occurred during CustomGroup creation "
|
|
f"for name '{instance.name}' (derived Django Group name '{group_name}') "
|
|
f"for dealer '{dealer.name}'. Error: {e}",
|
|
exc_info=True, # CRUCIAL: Get the full traceback for unexpected errors
|
|
)
|
|
return redirect("group_create", dealer_slug=dealer.slug)
|
|
|
|
if created:
|
|
group_manager, _ = models.CustomGroup.objects.get_or_create(
|
|
name=instance.name, dealer=dealer, group=group
|
|
)
|
|
# group_manager.set_default_permissions()
|
|
dealer.user.groups.add(group)
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy("group_list", args=[self.request.dealer.slug])
|
|
|
|
|
|
class GroupUpdateView(
|
|
LoginRequiredMixin,
|
|
PermissionRequiredMixin,
|
|
SuccessMessageMixin,
|
|
UpdateView,
|
|
):
|
|
"""
|
|
Handles the update of group objects with permission control and custom behavior.
|
|
|
|
This view allows users with login credentials to update existing group
|
|
objects. It ensures that changes made to groups adhere to default
|
|
permissions and adds necessary naming conventions based on user type.
|
|
Upon successful update, it redirects the user to the group list view and
|
|
displays a success message.
|
|
|
|
:ivar model: Specifies the model to be used for the update operation.
|
|
:type model: models.CustomGroup
|
|
:ivar form_class: Specifies the form class to be used for validation
|
|
and data manipulation.
|
|
:type form_class: forms.GroupForm
|
|
:ivar template_name: File path to the template that renders the form view.
|
|
:type template_name: str
|
|
:ivar success_url: URL to redirect upon successful form submission.
|
|
:type success_url: str
|
|
:ivar success_message: Message displayed upon successful update of a group.
|
|
:type success_message: str
|
|
"""
|
|
|
|
model = models.CustomGroup
|
|
form_class = forms.GroupForm
|
|
template_name = "groups/group_form.html"
|
|
success_message = _("Group updated successfully")
|
|
permission_required = ["inventory.change_customgroup"]
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
instance = form.save(commit=False)
|
|
instance.set_default_permissions()
|
|
instance.group.name = f"{dealer.slug}_{instance.name}"
|
|
instance.save()
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy("group_list", args=[self.request.dealer.slug])
|
|
|
|
|
|
@login_required
|
|
def GroupDeleteview(request, dealer_slug, pk):
|
|
"""
|
|
Handles the deletion of a specific group instance. This view ensures that only
|
|
authenticated users can perform the deletion. Upon successful deletion, a
|
|
success message is displayed, and the user is redirected to the group list page.
|
|
|
|
:param request: The HTTP request object that contains metadata about the
|
|
request context and user information. Must be an authenticated user.
|
|
:param pk: The primary key of the group instance to be deleted.
|
|
It specifies which group to retrieve and delete.
|
|
:return: The HTTP response that redirects the user to the group list page
|
|
after the group is successfully deleted.
|
|
"""
|
|
get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
group = get_object_or_404(models.CustomGroup, pk=pk)
|
|
group.delete()
|
|
messages.success(request, _("Group deleted successfully"))
|
|
return redirect("group_list", dealer_slug=dealer_slug)
|
|
|
|
|
|
# @login_required
|
|
# def GroupPermissionView(request, dealer_slug, pk):
|
|
# # Verify dealer and group exist
|
|
# get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
# customgroup = get_object_or_404(models.CustomGroup, pk=pk)
|
|
|
|
# if request.method == "POST":
|
|
# form = forms.PermissionForm(request.POST, instance=customgroup)
|
|
# if form.is_valid():
|
|
# # Clear existing permissions
|
|
# customgroup.clear_permissions()
|
|
|
|
# # Add new permissions from form
|
|
# permissions = form.cleaned_data.get('permissions', [])
|
|
# for permission in permissions:
|
|
# customgroup.add_permission(permission)
|
|
|
|
# messages.success(request, _("Permissions updated successfully"))
|
|
# return redirect("group_detail", dealer_slug=dealer_slug, pk=customgroup.pk)
|
|
# else:
|
|
# # Initial form with current permissions
|
|
# form = forms.PermissionForm(instance=customgroup)
|
|
|
|
# group_permission_ids = set(customgroup.permissions.values_list('id', flat=True))
|
|
|
|
# # Mark permissions as checked in the form data
|
|
# for app_label, model in form.grouped_permissions.items():
|
|
# print(app_label, model)
|
|
# for mo, perms in model.items():
|
|
# for perm in perms:
|
|
# perm.is_checked = perm.id in group_permission_ids
|
|
|
|
|
|
# return render(request,"groups/group_permission_form.html", {
|
|
# "group": customgroup,
|
|
# "form": form,
|
|
# "group_permission_apps": set(customgroup.group.permissions.values_list('content_type__app_label', flat=True)),
|
|
# "group_permission_models": set(customgroup.group.permissions.values_list('content_type__model', flat=True))
|
|
# })
|
|
@login_required
|
|
@permission_required("inventory.change_customgroup")
|
|
def GroupPermissionView(request, dealer_slug, pk):
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.db import transaction
|
|
from django.db.models import Q
|
|
|
|
get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
customgroup = get_object_or_404(models.CustomGroup, pk=pk)
|
|
group = customgroup.group
|
|
# Define ALL permissions you want to manage
|
|
MODEL_LIST = [
|
|
("inventory", "car"),
|
|
# ("inventory", "carfinance"),
|
|
("inventory", "carlocation"),
|
|
("inventory", "customcard"),
|
|
("inventory", "cartransfer"),
|
|
("inventory", "carcolors"),
|
|
("inventory", "carequipment"),
|
|
("inventory", "interiorcolors"),
|
|
("inventory", "exteriorcolors"),
|
|
("inventory", "lead"),
|
|
("inventory", "customgroup"),
|
|
("inventory", "saleorder"),
|
|
("inventory", "payment"),
|
|
("inventory", "staff"),
|
|
("inventory", "schedule"),
|
|
("inventory", "activity"),
|
|
("inventory", "opportunity"),
|
|
("inventory", "carreservation"),
|
|
("inventory", "customer"),
|
|
("inventory", "organization"),
|
|
("inventory", "additionalservices"),
|
|
("inventory", "notes"),
|
|
("inventory", "tasks"),
|
|
("inventory", "activity"),
|
|
("inventory", "vendor"),
|
|
("inventory", "poitemsuploaded"),
|
|
("django_ledger", "purchaseordermodel"),
|
|
("django_ledger", "bankaccountmodel"),
|
|
("django_ledger", "estimatemodel"),
|
|
("django_ledger", "accountmodel"),
|
|
("django_ledger", "chartofaccountmodel"),
|
|
("django_ledger", "billmodel"),
|
|
("django_ledger", "itemmodel"),
|
|
("django_ledger", "invoicemodel"),
|
|
("django_ledger", "vendormodel"),
|
|
("django_ledger", "journalentrymodel"),
|
|
("django_ledger", "ledgermodel"),
|
|
("django_ledger", "transactionmodel"),
|
|
]
|
|
CUSTOM_PERMISSIONS = [
|
|
("django_ledger", "can_approve_estimatemodel"),
|
|
("django_ledger", "can_approve_billmodel"),
|
|
]
|
|
|
|
if request.method == "POST":
|
|
# Get user info for logging
|
|
user_id = request.user.id if request.user.is_authenticated else "Anonymous"
|
|
user_username = (
|
|
request.user.username if request.user.is_authenticated else "anonymous"
|
|
)
|
|
|
|
try:
|
|
selected_ids = [int(id) for id in request.POST.getlist("permissions", [])]
|
|
|
|
# Get content types for model permissions
|
|
model_content_types = ContentType.objects.filter(
|
|
app_label__in=[m[0] for m in MODEL_LIST],
|
|
model__in=[m[1] for m in MODEL_LIST],
|
|
)
|
|
|
|
# Get all valid permissions (model CRUD + custom)
|
|
valid_perms = Permission.objects.filter(
|
|
# Model CRUD permissions
|
|
Q(content_type__in=model_content_types)
|
|
|
|
|
# Custom permissions
|
|
Q(
|
|
content_type__app_label__in=[p[0] for p in CUSTOM_PERMISSIONS],
|
|
codename__in=[p[1] for p in CUSTOM_PERMISSIONS],
|
|
),
|
|
id__in=selected_ids,
|
|
)
|
|
|
|
with transaction.atomic():
|
|
group.permissions.clear()
|
|
if valid_perms.exists():
|
|
group.permissions.add(*valid_perms)
|
|
|
|
# --- Logging for successful permission update ---
|
|
logger.info(
|
|
f"Permissions for Group '{group.name}' (ID: {group.id}) "
|
|
f"successfully updated by user {user_username} (ID: {user_id}). "
|
|
f"Total permissions granted: {valid_perms.count()}."
|
|
)
|
|
messages.success(request, _("Permissions updated successfully"))
|
|
return redirect("group_detail", dealer_slug=dealer_slug, pk=customgroup.pk)
|
|
|
|
except Exception as e:
|
|
# --- Logging for error during permission update ---
|
|
logger.error(
|
|
f"Error updating permissions for Group '{group.name}' (ID: {group.id}) "
|
|
f"by user {user_username} (ID: {user_id}). Error: {e}",
|
|
exc_info=True, # CRUCIAL: Includes the full traceback
|
|
)
|
|
messages.error(request, _("Error updating permissions: ") + str(e))
|
|
|
|
# GET request handling
|
|
# Get permissions for models (CRUD)
|
|
model_perms = Permission.objects.filter(
|
|
content_type__in=ContentType.objects.filter(
|
|
app_label__in=[m[0] for m in MODEL_LIST],
|
|
model__in=[m[1] for m in MODEL_LIST],
|
|
)
|
|
)
|
|
|
|
# Get custom permissions
|
|
custom_perms = Permission.objects.filter(
|
|
content_type__app_label__in=[p[0] for p in CUSTOM_PERMISSIONS],
|
|
codename__in=[p[1] for p in CUSTOM_PERMISSIONS],
|
|
)
|
|
|
|
# Combine all permissions
|
|
all_permissions = model_perms | custom_perms
|
|
all_permissions = all_permissions.select_related("content_type").order_by(
|
|
"content_type__app_label", "content_type__model", "codename"
|
|
)
|
|
|
|
# Group permissions with custom ones in a special section
|
|
grouped_permissions = {}
|
|
for perm in all_permissions:
|
|
app_label = perm.content_type.app_label
|
|
|
|
# Check if this is a custom permission
|
|
is_custom = any(
|
|
p[0] == app_label and p[1] == perm.codename for p in CUSTOM_PERMISSIONS
|
|
)
|
|
|
|
if is_custom:
|
|
# Group custom permissions under "Custom" model name
|
|
model = "Custom"
|
|
else:
|
|
model = perm.content_type.model
|
|
|
|
if app_label not in grouped_permissions:
|
|
grouped_permissions[app_label] = {}
|
|
if model not in grouped_permissions[app_label]:
|
|
grouped_permissions[app_label][model] = []
|
|
|
|
grouped_permissions[app_label][model].append(perm)
|
|
|
|
# Get currently assigned permission IDs
|
|
group_permission_ids = set(
|
|
group.permissions.filter(
|
|
id__in=all_permissions.values_list("id", flat=True)
|
|
).values_list("id", flat=True)
|
|
)
|
|
|
|
return render(
|
|
request,
|
|
"groups/group_permission_form.html",
|
|
{
|
|
"group": customgroup,
|
|
"grouped_permissions": grouped_permissions,
|
|
"group_permission_ids": group_permission_ids,
|
|
"group_permission_apps": set(
|
|
group.permissions.filter(
|
|
content_type__app_label__in=grouped_permissions.keys()
|
|
).values_list("content_type__app_label", flat=True)
|
|
),
|
|
"group_permission_models": set(
|
|
group.permissions.filter(
|
|
content_type__app_label__in=grouped_permissions.keys()
|
|
).values_list("content_type__model", flat=True)
|
|
),
|
|
},
|
|
)
|
|
# if request.method == "POST":
|
|
# try:
|
|
# selected_ids = [int(id) for id in request.POST.getlist('permissions', [])]
|
|
|
|
# # Get all permission types for our models
|
|
# content_types = ContentType.objects.filter(
|
|
# app_label__in=[m[0] for m in MODEL_LIST],
|
|
# model__in=[m[1] for m in MODEL_LIST]
|
|
# )
|
|
|
|
# # Get valid permissions that exist for these models
|
|
# valid_perms = Permission.objects.filter(
|
|
# content_type__in=content_types,
|
|
# id__in=selected_ids
|
|
# )
|
|
|
|
# # Atomic transaction to ensure data consistency
|
|
# with transaction.atomic():
|
|
# # Clear current permissions
|
|
# group.permissions.clear()
|
|
|
|
# # Add new permissions if any were selected
|
|
# if valid_perms.exists():
|
|
# group.permissions.add(*valid_perms)
|
|
|
|
# messages.success(request, _("Permissions updated successfully"))
|
|
# return redirect("group_detail", dealer_slug=dealer_slug, pk=customgroup.pk)
|
|
|
|
# except Exception as e:
|
|
# messages.error(request, _("Error updating permissions: ") + str(e))
|
|
|
|
# # GET request handling
|
|
# content_types = ContentType.objects.filter(
|
|
# app_label__in=[m[0] for m in MODEL_LIST],
|
|
# model__in=[m[1] for m in MODEL_LIST]
|
|
# )
|
|
|
|
# # Get all permissions for these content types
|
|
# all_permissions = Permission.objects.filter(
|
|
# content_type__in=content_types
|
|
# ).select_related('content_type').order_by('content_type__app_label', 'content_type__model', 'codename')
|
|
|
|
# # Group permissions by app and model
|
|
# grouped_permissions = {}
|
|
# for perm in all_permissions:
|
|
# app_label = perm.content_type.app_label
|
|
# model = perm.content_type.model
|
|
|
|
# if app_label not in grouped_permissions:
|
|
# grouped_permissions[app_label] = {}
|
|
# if model not in grouped_permissions[app_label]:
|
|
# grouped_permissions[app_label][model] = []
|
|
|
|
# grouped_permissions[app_label][model].append(perm)
|
|
|
|
# # Get currently assigned permission IDs
|
|
# group_permission_ids = set(
|
|
# group.permissions.filter(
|
|
# content_type__in=content_types
|
|
# ).values_list('id', flat=True)
|
|
# )
|
|
|
|
# return render(request, "groups/group_permission_form.html", {
|
|
# "group": customgroup,
|
|
# "grouped_permissions": grouped_permissions,
|
|
# "group_permission_ids": group_permission_ids,
|
|
# "group_permission_apps": set(
|
|
# group.permissions.filter(
|
|
# content_type__app_label__in=[m[0] for m in MODEL_LIST]
|
|
# ).values_list('content_type__app_label', flat=True)
|
|
# ),
|
|
# "group_permission_models": set(
|
|
# group.permissions.filter(
|
|
# content_type__model__in=[m[1] for m in MODEL_LIST]
|
|
# ).values_list('content_type__model', flat=True)
|
|
# )
|
|
# })
|
|
|
|
|
|
# Users
|
|
@login_required
|
|
@permission_required("inventory.change_staff")
|
|
def UserGroupView(request, dealer_slug, slug):
|
|
"""
|
|
Handles the assignment of user groups to a specific staff member. This view
|
|
allows updating the groups a staff member belongs to via a form submission.
|
|
It processes both GET and POST requests, ensuring appropriate group
|
|
assignments are managed and feedback is provided to the user via messages.
|
|
|
|
:param request: HttpRequest object representing the HTTP request.
|
|
:type request: HttpRequest
|
|
:param pk: Primary key of the staff member whose groups are being updated.
|
|
:type pk: int
|
|
|
|
:return: Renders the user group form for GET requests or redirects to the
|
|
user detail page after successful submission for POST requests.
|
|
:rtype: HttpResponse or HttpResponseRedirect
|
|
"""
|
|
get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
staff = get_object_or_404(models.Staff, slug=slug)
|
|
if request.method == "POST":
|
|
form = forms.UserGroupForm(request.POST)
|
|
groups = request.POST.getlist("name")
|
|
|
|
staff.clear_groups()
|
|
for i in groups:
|
|
cg = models.CustomGroup.objects.get(id=int(i))
|
|
staff.add_group(cg.group)
|
|
|
|
messages.success(request, _("Group added successfully"))
|
|
return redirect("user_detail", dealer_slug=dealer_slug, slug=staff.slug)
|
|
|
|
form = forms.UserGroupForm(initial={"name": staff.groups})
|
|
form.fields["name"].queryset = models.CustomGroup.objects.filter(
|
|
dealer=staff.dealer
|
|
)
|
|
return render(request, "users/user_group_form.html", {"staff": staff, "form": form})
|
|
|
|
|
|
class UserListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Represents a view for listing users with pagination and filters.
|
|
|
|
This class is designed to display a list view of staff users for a specific
|
|
dealer. It supports pagination and search filtering functionality. This view
|
|
requires the user to be logged in, as it inherits from `LoginRequiredMixin`.
|
|
|
|
:ivar model: The model that the view will work with.
|
|
:type model: Type[models.Staff]
|
|
:ivar context_object_name: The name of the context variable that will contain
|
|
the list of users.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of items to display per page.
|
|
:type paginate_by: int
|
|
:ivar template_name: The path to the template used for rendering the view's
|
|
page.
|
|
:type template_name: str
|
|
"""
|
|
|
|
model = models.Staff
|
|
context_object_name = "users"
|
|
paginate_by = 10
|
|
template_name = "users/user_list.html"
|
|
permission_required = ["inventory.view_staff"]
|
|
|
|
def get_queryset(self):
|
|
query = self.request.GET.get("q")
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
staff = models.Staff.objects.filter(dealer=dealer, active=True).all()
|
|
return apply_search_filters(staff, query)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["no_staff_message"] = _("staff")
|
|
return context
|
|
|
|
|
|
class UserDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Represents a detailed view for displaying user-specific information.
|
|
|
|
This class-based view is used to display detailed information for
|
|
a specific user. It ensures that only logged-in users can access
|
|
this view, leveraging the ``LoginRequiredMixin`` for authentication.
|
|
The data for the view comes from the ``models.Staff`` model and
|
|
is rendered using the specified HTML template.
|
|
|
|
:ivar model: The data model associated with this view.
|
|
:type model: models.Staff
|
|
:ivar template_name: Path to the HTML template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: Name of the context variable available in the template.
|
|
:type context_object_name: str
|
|
"""
|
|
|
|
model = models.Staff
|
|
template_name = "users/user_detail.html"
|
|
context_object_name = "user_"
|
|
permission_required = ["inventory.view_staff"]
|
|
|
|
|
|
class UserCreateView(
|
|
LoginRequiredMixin,
|
|
PermissionRequiredMixin,
|
|
SuccessMessageMixin,
|
|
CreateView,
|
|
):
|
|
"""
|
|
Manages the creation of a new staff user and their associated details.
|
|
|
|
This view is responsible for handling the creation of a new staff user in the
|
|
system. It ensures quota limits are adhered to, validates form input, saves user
|
|
details, and associates them with services and permissions.
|
|
|
|
:ivar model: The database model associated with this view.
|
|
:type model: django.db.models.Model
|
|
|
|
:ivar form_class: The form class used to validate and create a staff user.
|
|
:type form_class: type
|
|
|
|
:ivar template_name: The template used to render the user creation form.
|
|
:type template_name: str
|
|
|
|
:ivar success_url: The URL to redirect to after successfully creating a user.
|
|
:type success_url: str
|
|
|
|
:ivar success_message: The success message displayed upon user creation.
|
|
:type success_message: str
|
|
"""
|
|
|
|
model = models.Staff
|
|
form_class = forms.StaffForm
|
|
template_name = "users/user_form.html"
|
|
success_url = reverse_lazy("user_list")
|
|
success_message = _("User created successfully")
|
|
permission_required = ["inventory.add_staff"]
|
|
staff_pk = None
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
form.fields["group"].queryset = models.CustomGroup.objects.filter(dealer=dealer)
|
|
return form
|
|
|
|
def form_valid(self, form):
|
|
staff = form.save(commit=False)
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
# if dealer.is_staff_exceed_quota_limit:
|
|
# messages.error(
|
|
# self.request,
|
|
# _(
|
|
# "You have reached the maximum number of staff users allowed for your plan"
|
|
# ),
|
|
# )
|
|
# return self.form_invalid(form)
|
|
|
|
email = form.cleaned_data["email"]
|
|
if (
|
|
models.Staff.objects.filter(user__email=email).exists()
|
|
or models.Dealer.objects.filter(user__email=email).exists()
|
|
):
|
|
messages.error(
|
|
self.request,
|
|
_(
|
|
"A user with this email already exists. Please use a different email."
|
|
),
|
|
)
|
|
return redirect("user_create", dealer_slug=dealer.slug)
|
|
|
|
password = "Tenhal@123"
|
|
|
|
user, created = User.objects.get_or_create(
|
|
email=email,
|
|
defaults={
|
|
"first_name": staff.first_name,
|
|
"last_name": staff.last_name,
|
|
"username": email,
|
|
"password": password,
|
|
},
|
|
)
|
|
user.is_staff = True
|
|
user.save()
|
|
|
|
# staff_member, _ = StaffMember.objects.get_or_create(user=user)
|
|
# for service in form.cleaned_data["service_offered"]:
|
|
# staff_member.services_offered.add(service)
|
|
staff.user = user
|
|
staff.dealer = dealer
|
|
staff.save()
|
|
self.staff_pk = staff.pk
|
|
for customgroup in form.cleaned_data["group"]:
|
|
staff.add_group(customgroup.group)
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"staff_password_reset", args=[self.request.dealer.slug, self.staff_pk]
|
|
)
|
|
# return reverse_lazy("user_list", args=[self.request.dealer.slug])
|
|
|
|
|
|
class UserUpdateView(
|
|
LoginRequiredMixin,
|
|
PermissionRequiredMixin,
|
|
SuccessMessageMixin,
|
|
UpdateView,
|
|
):
|
|
"""
|
|
UserUpdateView updates information for a user with specific details.
|
|
|
|
This view handles updating user details such as email, name, phone number, and services
|
|
offered. It leverages Django's `UpdateView` to manage the update operation. The view
|
|
disables editing of email addresses, initializes specific fields with data associated
|
|
with the user, and processes input to manage related services. Validation of the form and
|
|
saving changes are customized to align with specific business logic.
|
|
|
|
:ivar model: The model class associated with the view, used for retrieving the object to update.
|
|
:type model: models.Staff
|
|
:ivar form_class: The form class used to render and process the form for updating the user.
|
|
:type form_class: forms.StaffForm
|
|
:ivar template_name: The template used for rendering the form in the UI.
|
|
:type template_name: str
|
|
:ivar success_url: URL to redirect to after a successful form submission.
|
|
:type success_url: str
|
|
:ivar success_message: Message displayed to the user after a successful update.
|
|
:type success_message: str
|
|
"""
|
|
|
|
model = models.Staff
|
|
form_class = forms.StaffForm
|
|
template_name = "users/user_form.html"
|
|
success_url = reverse_lazy("user_list")
|
|
success_message = _("User updated successfully")
|
|
permission_required = ["inventory.change_staff"]
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs["instance"] = self.get_object() # Pass the Staff instance to the form
|
|
return kwargs
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
form.fields["group"].queryset = models.CustomGroup.objects.filter(dealer=dealer)
|
|
form.fields["email"].disabled = True
|
|
return form
|
|
|
|
def get_initial(self):
|
|
initial = super().get_initial()
|
|
initial["email"] = self.object.user.email
|
|
initial["group"] = self.object.groups
|
|
|
|
return initial
|
|
|
|
def form_valid(self, form):
|
|
# services = form.cleaned_data["service_offered"]
|
|
# if not services:
|
|
# self.object.staff_member.services_offered.clear()
|
|
# else:
|
|
# for service in services:
|
|
# self.object.staff_member.services_offered.add(service)
|
|
|
|
staff = form.save(commit=False)
|
|
# staff.name = form.cleaned_data["name"]
|
|
staff.arabic_name = form.cleaned_data["arabic_name"]
|
|
staff.phone_number = form.cleaned_data["phone_number"]
|
|
for customgroup in form.cleaned_data["group"]:
|
|
staff.add_group(customgroup.group, True)
|
|
staff.save()
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy("user_list", args=[self.request.dealer.slug])
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.delete_staff", login_url="login")
|
|
def UserDeleteview(request, dealer_slug, slug):
|
|
"""
|
|
Deletes a user and its associated staff member from the database and redirects
|
|
to the user list page. Displays a success message upon successful deletion
|
|
of the user.
|
|
|
|
:param request: The HTTP request object representing the incoming request.
|
|
:param pk: The primary key (ID) of the staff member to be deleted.
|
|
:return: An HTTP redirect to the user list page.
|
|
|
|
"""
|
|
get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
staff = get_object_or_404(models.Staff, slug=slug)
|
|
staff.deactivate_account()
|
|
messages.success(request, _("User deleted successfully"))
|
|
return redirect("user_list", dealer_slug=dealer_slug)
|
|
|
|
|
|
class OrganizationListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Represents a view to display a paginated list of organizations for a dealer.
|
|
|
|
This class inherits from `LoginRequiredMixin` to ensure that only
|
|
authenticated users can access the list, and from `ListView` to provide
|
|
a generic implementation to render lists of database objects. It is designed
|
|
specifically to show organizations related to a dealer entity and includes
|
|
search functionality based on a query parameter.
|
|
|
|
:ivar model: Specifies the model to fetch data from.
|
|
:type model: type[CustomerModel]
|
|
:ivar template_name: The template used to render the organization list page.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable for the organization list.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of organizations displayed per page.
|
|
:type paginate_by: int
|
|
"""
|
|
|
|
model = models.Organization
|
|
template_name = "organizations/organization_list.html"
|
|
context_object_name = "organizations"
|
|
paginate_by = 20
|
|
permission_required = ["inventory.view_organization"]
|
|
|
|
def get_queryset(self):
|
|
query = self.request.GET.get("q")
|
|
dealer = get_user_type(self.request)
|
|
organization = dealer.organizations.filter(active=True)
|
|
|
|
return apply_search_filters(organization, query)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["empty_state_value"] = _("organization")
|
|
return context
|
|
|
|
|
|
class OrganizationDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Handles displaying detailed information about an organization.
|
|
|
|
This view is a detailed representation of an individual organization, which
|
|
provides the necessary data for templates to render the information. It is
|
|
intended to be used for displaying details of a `CustomerModel` instance.
|
|
Requires the user to be logged in to access this view.
|
|
|
|
:ivar model: Specifies the model that this view will interact with.
|
|
:type model: type[CustomerModel]
|
|
:ivar template_name: The name of the template to be used for rendering the
|
|
organization's detail information.
|
|
:type template_name: str
|
|
:ivar context_object_name: The context variable name to be used in the
|
|
template for accessing the organization's data.
|
|
:type context_object_name: str
|
|
"""
|
|
|
|
model = models.Organization
|
|
template_name = "organizations/organization_detail.html"
|
|
context_object_name = "organization"
|
|
permission_required = ["inventory.view_organization"]
|
|
|
|
|
|
class OrganizationCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
|
"""
|
|
# Handles the creation of a new organization via a web form. This view allows the
|
|
# authenticated user to submit data for creating an organization. If a POST request
|
|
# is received, it validates the data, checks for duplicate organizations, and
|
|
# creates a customer linked to the organization, including its associated
|
|
# information such as address, phone number, and logo. Upon success, the user
|
|
# is redirected to the organization list, and a success message is displayed.
|
|
|
|
# :param request: The HTTP request object containing data for creating an organization.
|
|
# :type request: HttpRequest
|
|
# :return: An HTTP response object rendering the organization create form page or
|
|
# redirecting the user after a successful creation.
|
|
# :rtype: HttpResponse
|
|
#"""
|
|
|
|
model = models.Organization
|
|
form_class = forms.OrganizationForm
|
|
permission_required = ["inventory.add_organization"]
|
|
template_name = "organizations/organization_form.html"
|
|
success_url = reverse_lazy("organization_list")
|
|
success_message = _("Organization created successfully")
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
if organization := models.Organization.objects.filter(
|
|
dealer=dealer, email=form.instance.email
|
|
).first():
|
|
if not organization.active:
|
|
messages.error(
|
|
self.request,
|
|
_(
|
|
"Organization Account with this email is Deactivated,Please Contact Admin"
|
|
),
|
|
)
|
|
else:
|
|
messages.error(
|
|
self.request, _("Organization with this email already exists")
|
|
)
|
|
return redirect("organization_create")
|
|
dealer = get_user_type(self.request)
|
|
form.instance.dealer = dealer
|
|
# user = form.instance.create_user_model()
|
|
customer = form.instance.create_customer_model()
|
|
# form.instance.user = user
|
|
form.instance.customer_model = customer
|
|
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"organization_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
|
|
)
|
|
|
|
|
|
class OrganizationUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
|
"""
|
|
# Handles the update of an organization instance. This view fetches the organization
|
|
# based on the provided primary key (pk) and renders a form for editing the
|
|
# organization attributes. When a POST request is made, this view validates and
|
|
# processes the form data, updates the organization instance, and saves the changes.
|
|
# If the request method is not POST, it initializes the form with existing organization
|
|
# data for rendering.
|
|
|
|
# :param request: The HTTP request object. Must be authenticated via login.
|
|
# :type request: HttpRequest
|
|
# :param pk: The primary key of the organization to be updated.
|
|
# :type pk: int
|
|
# :return: An HTTP response object. Either renders the organization form or redirects
|
|
# to the organization list upon successful update.
|
|
# :rtype: HttpResponse
|
|
#"""
|
|
|
|
model = models.Organization
|
|
form_class = forms.OrganizationForm
|
|
permission_required = ["inventory.change_organization"]
|
|
template_name = "organizations/organization_form.html"
|
|
success_url = reverse_lazy("organization_list")
|
|
success_message = _("Organization updated successfully")
|
|
|
|
def form_valid(self, form):
|
|
# form.instance.update_user_model()
|
|
form.instance.update_customer_model()
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"organization_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
|
|
)
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.delete_organization")
|
|
def OrganizationDeleteView(request, dealer_slug, slug):
|
|
"""
|
|
Handles the deletion of an organization based on the provided primary key (pk). Looks up
|
|
the organization and its corresponding user by email, attempts to delete both, and provides
|
|
appropriate success or error feedback to the user. In case of failure, an error message is shown,
|
|
while successful deletion redirects to the organization list.
|
|
|
|
:param request: The HTTP request object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the organization to be deleted.
|
|
:type pk: int
|
|
:return: An HTTP response redirecting to the organization list view.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
organization = get_object_or_404(models.Organization, slug=slug)
|
|
organization.deactivate_account()
|
|
messages.success(request, _("Organization Deactivated successfully"))
|
|
return redirect(
|
|
reverse_lazy("organization_list", kwargs={"dealer_slug": dealer_slug})
|
|
)
|
|
|
|
|
|
class RepresentativeListView(LoginRequiredMixin, ListView):
|
|
"""
|
|
Represents a view for displaying a paginated list of representatives.
|
|
|
|
This view handles the functionality of displaying and paginating a list
|
|
of representatives for the logged-in user. It utilizes search filters to
|
|
allow querying representatives based on the search term provided in the
|
|
request. The view restricts access to logged-in users only.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: models.Representative
|
|
:ivar template_name: Name of the template used to render the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: Name of the context variable used to access
|
|
representatives in the template.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of representatives displayed per page.
|
|
:type paginate_by: int
|
|
"""
|
|
|
|
model = models.Representative
|
|
template_name = "representatives/representative_list.html"
|
|
context_object_name = "representatives"
|
|
paginate_by = 10
|
|
|
|
def get_queryset(self):
|
|
query = self.request.GET.get("q")
|
|
dealer = get_user_type(self.request)
|
|
representative = models.Representative.objects.filter(dealer=dealer)
|
|
return apply_search_filters(representative, query)
|
|
|
|
|
|
class RepresentativeDetailView(LoginRequiredMixin, DetailView):
|
|
"""
|
|
Represents a detailed view for Representative instances.
|
|
|
|
This class-based view is used to provide detailed representation for
|
|
``Representative`` model instances. It ensures that only authenticated
|
|
users can access the view by utilizing ``LoginRequiredMixin``. The
|
|
template used to render the view and the context name for the object
|
|
are also specified for use in a Django template.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: Type of the model (models.Representative)
|
|
:ivar template_name: The path to the template used to render this view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable containing
|
|
the object.
|
|
:type context_object_name: str
|
|
"""
|
|
|
|
model = models.Representative
|
|
template_name = "representatives/representative_detail.html"
|
|
context_object_name = "representative"
|
|
|
|
|
|
class RepresentativeCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
|
|
"""
|
|
Handles the creation of a Representative object.
|
|
|
|
This class is a view that provides the interface and functionality to create
|
|
a new representative in the application. It is designed to ensure that only
|
|
authenticated users with a valid dealer association can create representatives.
|
|
A success message is displayed upon successful creation of a representative.
|
|
|
|
:ivar model: The model that this view will work with, which is Representative.
|
|
:type model: django.db.models.Model
|
|
:ivar form_class: The form class used for creating a representative.
|
|
:type form_class: django.forms.ModelForm
|
|
:ivar template_name: The name of the template used to render the create view.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to upon successful form submission.
|
|
:type success_url: str
|
|
:ivar success_message: The success message displayed after creating a representative.
|
|
:type success_message: str
|
|
"""
|
|
|
|
model = models.Representative
|
|
form_class = forms.RepresentativeForm
|
|
template_name = "representatives/representative_form.html"
|
|
success_url = reverse_lazy("representative_list")
|
|
success_message = _("Representative created successfully")
|
|
|
|
def form_valid(self, form):
|
|
if form.is_valid():
|
|
form.instance.dealer = self.request.dealer
|
|
form.save()
|
|
return super().form_valid(form)
|
|
else:
|
|
return form.errors
|
|
|
|
|
|
class RepresentativeUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
|
"""
|
|
Provides functionality for updating a representative's details.
|
|
|
|
This class-based view allows authenticated users, equipped with
|
|
the required permissions, to update information for a specific
|
|
representative. It uses a predefined form for input, renders the
|
|
appropriate update template, and provides success messaging upon
|
|
successful completion. It also redirects to a predefined success
|
|
URL once the update operation is complete.
|
|
|
|
:ivar model: The model representing a representative that is being
|
|
updated.
|
|
:type model: Type[models.Representative]
|
|
:ivar form_class: The form class used for providing input fields to
|
|
update representative details.
|
|
:type form_class: Type[forms.RepresentativeForm]
|
|
:ivar template_name: The template used to render the representative
|
|
update page.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to which the user is redirected following
|
|
a successful update.
|
|
:type success_url: str
|
|
:ivar success_message: The message displayed upon a successful update
|
|
operation.
|
|
:type success_message: str
|
|
"""
|
|
|
|
model = models.Representative
|
|
form_class = forms.RepresentativeForm
|
|
template_name = "representatives/representative_form.html"
|
|
success_url = reverse_lazy("representative_list")
|
|
success_message = _("Representative updated successfully")
|
|
|
|
|
|
class RepresentativeDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
|
|
"""
|
|
Handles the deletion of a representative.
|
|
|
|
This view provides functionality to delete a representative from the system.
|
|
It ensures that only authenticated users can perform the deletion and displays
|
|
a success message upon successful deletion. The deletion is confirmed via a
|
|
template, and upon success, the user is redirected to the representative list page.
|
|
|
|
:ivar model: The model representing the representative.
|
|
:type model: models.Representative
|
|
:ivar template_name: The template used to confirm the deletion of a representative.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to after successful deletion.
|
|
:type success_url: str
|
|
:ivar success_message: The success message displayed after a representative is deleted.
|
|
:type success_message: str
|
|
"""
|
|
|
|
model = models.Representative
|
|
template_name = "representatives/representative_confirm_delete.html"
|
|
success_url = reverse_lazy("representative_list")
|
|
success_message = _("Representative deleted successfully")
|
|
|
|
|
|
# BANK ACCOUNT
|
|
class BankAccountListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Provides a view for listing bank accounts associated with a specific entity
|
|
and applying search filters if provided. Ensures user authentication and
|
|
required permissions are implemented.
|
|
|
|
This view is used to display the list of bank accounts in a paginated format.
|
|
It filters the list of bank accounts based on the associated entity of the
|
|
dealer (user type) and applies search filters when needed. It requires the
|
|
user to be logged in and have the specified permissions to view the resource.
|
|
|
|
:ivar model: The model to fetch data for the bank account listing.
|
|
:type model: type[BankAccountModel]
|
|
:ivar template_name: The template used to render the bank account list.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable containing the
|
|
list of bank accounts.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of records displayed per page in pagination.
|
|
:type paginate_by: int
|
|
:ivar permission_required: The required permissions to access the view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
model = BankAccountModel
|
|
template_name = "ledger/bank_accounts/bank_account_list.html"
|
|
context_object_name = "bank_accounts"
|
|
paginate_by = 30
|
|
permission_required = ["django_ledger.view_bankaccountmodel"]
|
|
|
|
def get_queryset(self):
|
|
query = self.request.GET.get("q")
|
|
dealer = self.request.dealer
|
|
bank_accounts = BankAccountModel.objects.filter(entity_model=dealer.entity)
|
|
return apply_search_filters(bank_accounts, query)
|
|
|
|
|
|
class BankAccountCreateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView
|
|
):
|
|
"""
|
|
Represents a view for creating a new bank account.
|
|
|
|
This class is a Django CreateView that handles the creation of a new bank account
|
|
within the system. It integrates functionalities for login requirements, permission
|
|
enforcement, and success messages upon successful creation. The view is initialized
|
|
with a specific model, form class, and template to render the form. A success message
|
|
and redirection URL are specified for after the account creation. Additionally, it
|
|
checks permissions required to access this view.
|
|
|
|
:ivar model: The model to be used for the creation of a bank account.
|
|
:type model: BankAccountModel
|
|
:ivar form_class: The form class to be used for validating and handling bank account
|
|
creation.
|
|
:type form_class: BankAccountCreateForm
|
|
:ivar template_name: The template to render the form for bank account creation.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to after successful bank account creation.
|
|
:type success_url: str
|
|
:ivar success_message: The success message to display upon successful creation.
|
|
:type success_message: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = BankAccountModel
|
|
form_class = BankAccountCreateForm
|
|
template_name = "ledger/bank_accounts/bank_account_form.html"
|
|
success_message = _("Bank account created successfully")
|
|
permission_required = ["django_ledger.view_bankaccountmodel"]
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
form.instance.entity_model = dealer.entity
|
|
return super().form_valid(form)
|
|
|
|
def get_form_kwargs(self):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
entity = dealer.entity
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs["entity_slug"] = entity.slug
|
|
kwargs["user_model"] = entity.admin
|
|
return kwargs
|
|
|
|
def get_form(self, form_class=None):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
form = super().get_form(form_class)
|
|
account_qs = dealer.entity.get_default_coa_accounts().filter(
|
|
role__in=[
|
|
roles.ASSET_CA_CASH,
|
|
roles.LIABILITY_CL_ACC_PAYABLE,
|
|
roles.LIABILITY_LTL_MORTGAGE_PAYABLE,
|
|
]
|
|
)
|
|
form.fields["account_model"].queryset = account_qs
|
|
|
|
return form
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"bank_account_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
|
|
)
|
|
|
|
|
|
class BankAccountDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Manages the detailed view of a bank account.
|
|
|
|
Provides a detailed view for a specific bank account by leveraging Django's
|
|
DetailView. This class ensures that only authenticated users with the relevant
|
|
permissions can access the view. It is tailored for displaying necessary
|
|
details about the bank account.
|
|
|
|
:ivar model: The model associated with the view.
|
|
:type model: type[BankAccountModel]
|
|
:ivar template_name: Path to the HTML template used to render the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable used to represent
|
|
the specific bank account instance in the template.
|
|
:type context_object_name: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
model = BankAccountModel
|
|
template_name = "ledger/bank_accounts/bank_account_detail.html"
|
|
context_object_name = "bank_account"
|
|
permission_required = ["django_ledger.view_bankaccountmodel"]
|
|
|
|
def get_queryset(self):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
query = self.request.GET.get("q")
|
|
qs = self.model.objects.filter(entity_model=dealer.entity)
|
|
if query:
|
|
qs = apply_search_filters(qs, query)
|
|
return qs
|
|
return qs
|
|
|
|
|
|
class BankAccountUpdateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
|
):
|
|
"""
|
|
Represents a view for updating bank account details in the system.
|
|
|
|
This class is responsible for providing functionality to update existing
|
|
bank account information within the system. It ensures that only logged-in
|
|
users with the appropriate permissions can access and update the data. The
|
|
view also provides user feedback regarding successful updates using a
|
|
success message and redirects to the bank account list upon completion.
|
|
|
|
:ivar model: Defines the model associated with the view.
|
|
:type model: BankAccountModel
|
|
:ivar form_class: Specifies the form class used for updating bank account information.
|
|
:type form_class: BankAccountUpdateForm
|
|
:ivar template_name: Path to the template used for rendering the update form.
|
|
:type template_name: str
|
|
:ivar success_url: URL to redirect to upon successful update of a bank account.
|
|
:type success_url: str
|
|
:ivar success_message: Message displayed to the user upon successful update.
|
|
:type success_message: str
|
|
:ivar permission_required: List of permissions required to access the update view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = BankAccountModel
|
|
form_class = BankAccountUpdateForm
|
|
template_name = "ledger/bank_accounts/bank_account_form.html"
|
|
success_message = _("Bank account updated successfully")
|
|
permission_required = ["django_ledger.view_bankaccountmodel"]
|
|
|
|
def get_form_kwargs(self):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
entity = dealer.entity
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs["entity_slug"] = entity.slug
|
|
kwargs["user_model"] = entity.admin
|
|
return kwargs
|
|
|
|
def get_form(self, form_class=None):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
form = super().get_form(form_class)
|
|
account_qs = dealer.entity.get_default_coa_accounts().filter(
|
|
role__in=[
|
|
roles.ASSET_CA_CASH,
|
|
roles.LIABILITY_CL_ACC_PAYABLE,
|
|
roles.LIABILITY_LTL_MORTGAGE_PAYABLE,
|
|
]
|
|
)
|
|
form.fields["account_model"].queryset = account_qs
|
|
return form
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"bank_account_detail",
|
|
kwargs={"dealer_slug": self.kwargs["dealer_slug"], "pk": self.object.pk},
|
|
)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.delete_bankaccountmodel", raise_exception=True)
|
|
def bank_account_delete(request, dealer_slug, pk):
|
|
"""
|
|
Delete a bank account entry from the database.
|
|
|
|
This view handles the deletion of a bank account record specified by its
|
|
primary key (pk). It renders a deletion confirmation page and processes the
|
|
deletion if the request method is POST. Upon successful deletion, the user is
|
|
redirected to the list of bank accounts and a success message is displayed.
|
|
|
|
:param request: The HTTP request object representing the client's request.
|
|
It contains data such as request type (GET or POST) and session
|
|
information.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the bank account model instance to be deleted.
|
|
:type pk: int
|
|
:return: Returns an HttpResponse object. This can be an HTTP redirect to the
|
|
bank account list page upon successful deletion, or an HTML response
|
|
rendering the confirmation template if accessed via GET.
|
|
:rtype: HttpResponse
|
|
"""
|
|
get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
bank_account = get_object_or_404(BankAccountModel, pk=pk)
|
|
|
|
bank_account.delete()
|
|
messages.success(request, _("Bank account deleted successfully"))
|
|
return redirect("bank_account_list", dealer_slug=dealer_slug)
|
|
|
|
|
|
# Accounts
|
|
|
|
|
|
class AccountListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
View for displaying a list of accounts.
|
|
|
|
AccountListView is responsible for displaying a paginated list of accounts
|
|
that users can view based on their permissions. This view is restricted
|
|
to users who are logged in and have the required permissions. It also
|
|
provides functionality to filter and search the available accounts.
|
|
|
|
:ivar model: The Django model used for this view. Determines which
|
|
database records are displayed in the account list.
|
|
:type model: type[Model]
|
|
:ivar template_name: Path to the template file used for rendering the
|
|
account list view.
|
|
:type template_name: str
|
|
:ivar context_object_name: Name of the variable containing the accounts
|
|
list passed to the template.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: Number of accounts to display per page.
|
|
:type paginate_by: int
|
|
:ivar permission_required: Permissions required to access this view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
model = AccountModel
|
|
template_name = "ledger/coa_accounts/account_list.html"
|
|
context_object_name = "accounts"
|
|
permission_required = ["django_ledger.view_accountmodel"]
|
|
|
|
def get_queryset(self):
|
|
query = self.request.GET.get("q")
|
|
dealer = get_user_type(self.request)
|
|
coa = (
|
|
ChartOfAccountModel.objects.get(
|
|
entity=dealer.entity, pk=self.kwargs["coa_pk"]
|
|
)
|
|
or self.request.entity.get_default_coa()
|
|
)
|
|
accounts = coa.get_coa_accounts()
|
|
return apply_search_filters(accounts, query)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["url_kwargs"] = self.kwargs
|
|
context["coa_pk"] = self.kwargs["coa_pk"]
|
|
return context
|
|
|
|
|
|
class AccountCreateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView
|
|
):
|
|
"""
|
|
View for creating an account in the ledger system.
|
|
|
|
This class provides functionality for rendering a form to create a new account,
|
|
validating the form, setting default account properties based on the current
|
|
user's entity, and saving the new account. It is designed to ensure that only
|
|
authorized users with the required permissions can create accounts. The view
|
|
also provides feedback to the user upon successful account creation.
|
|
|
|
:ivar model: Defines the model associated with this view. In this case, the model
|
|
represents accounts in the ledger system.
|
|
:type model: type
|
|
|
|
:ivar form_class: Defines the form class used for creating new accounts. This
|
|
ensures data validation and captures input for new account creation.
|
|
:type form_class: type
|
|
|
|
:ivar template_name: Specifies the template used to render the account creation form.
|
|
:type template_name: str
|
|
|
|
:ivar success_url: URL to which the user is redirected upon successful account creation.
|
|
:type success_url: str
|
|
|
|
:ivar success_message: Feedback message displayed to the user upon successfully
|
|
creating an account.
|
|
:type success_message: str
|
|
|
|
:ivar permission_required: List of permissions required to access this view. Enforces
|
|
the permission checking to prevent unauthorized access.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = AccountModel
|
|
form_class = AccountModelCreateForm
|
|
template_name = "ledger/coa_accounts/account_form.html"
|
|
success_message = _("Account created successfully")
|
|
permission_required = ["django_ledger.add_accountmodel"]
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
coa = (
|
|
ChartOfAccountModel.objects.get(
|
|
entity=dealer.entity, pk=self.kwargs["coa_pk"]
|
|
)
|
|
or self.request.entity.get_default_coa()
|
|
)
|
|
form.instance.entity_model = dealer.entity
|
|
form.instance.coa_model = coa
|
|
form.instance.depth = 0
|
|
form.instance.path = form.instance.code
|
|
return super().form_valid(form)
|
|
|
|
def get_form_kwargs(self):
|
|
dealer = get_user_type(self.request)
|
|
kwargs = super().get_form_kwargs()
|
|
coa = (
|
|
ChartOfAccountModel.objects.get(
|
|
entity=dealer.entity, pk=self.kwargs["coa_pk"]
|
|
)
|
|
or self.request.entity.get_default_coa()
|
|
)
|
|
kwargs["coa_model"] = coa
|
|
return kwargs
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
entity = get_user_type(self.request).entity
|
|
coa = (
|
|
ChartOfAccountModel.objects.get(entity=entity, pk=self.kwargs["coa_pk"])
|
|
or self.request.entity.get_default_coa()
|
|
)
|
|
form.initial["coa_model"] = coa
|
|
return form
|
|
|
|
def get_success_url(self):
|
|
return reverse(
|
|
"account_list",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"coa_pk": self.kwargs["coa_pk"],
|
|
},
|
|
)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["url_kwargs"] = self.kwargs
|
|
coa_pk = context["url_kwargs"]["coa_pk"]
|
|
try:
|
|
context["coa_model"] = ChartOfAccountModel.objects.get(
|
|
entity=self.request.entity, pk=coa_pk
|
|
)
|
|
except Exception:
|
|
context["coa_model"] = self.request.entity.get_default_coa()
|
|
return context
|
|
|
|
|
|
class AccountDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Represents the detailed view for an account with additional context data related to account
|
|
transactions and permissions.
|
|
|
|
This class provides a detailed view for an account in the system. It includes functionality
|
|
for generating contextual data, managing permissions, and customizing rendering templates.
|
|
The view calculates total debits, credits, and provides transaction details for the account.
|
|
|
|
:ivar model: The Django model class representing the account data.
|
|
:type model: Type[AccountModel]
|
|
:ivar template_name: The path to the template used to render this view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The context variable name representing the account object.
|
|
:type context_object_name: str
|
|
:ivar slug_field: The field in the model used to retrieve the account instance based on a slug.
|
|
:type slug_field: str
|
|
:ivar DEFAULT_TXS_DAYS: Default number of days to filter transactions.
|
|
:type DEFAULT_TXS_DAYS: int
|
|
:ivar permission_required: Permissions required to access this view.
|
|
:type permission_required: list[str]
|
|
:ivar extra_context: Additional context data passed to the template.
|
|
:type extra_context: dict
|
|
"""
|
|
|
|
model = AccountModel
|
|
template_name = "ledger/coa_accounts/account_detail.html"
|
|
context_object_name = "account"
|
|
slug_field = "uuid"
|
|
DEFAULT_TXS_DAYS = 30
|
|
permission_required = ["django_ledger.view_accountmodel"]
|
|
extra_context = {
|
|
"DEFAULT_TXS_DAYS": DEFAULT_TXS_DAYS,
|
|
"header_subtitle_icon": "ic:round-account-tree",
|
|
}
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
account_model: AccountModel = context["object"]
|
|
context["header_title"] = f"Account {account_model.code} - {account_model.name}"
|
|
context["page_title"] = f"Account {account_model.code} - {account_model.name}"
|
|
context["total_debits"] = sum(
|
|
x.amount for x in account_model.transactionmodel_set.filter(tx_type="debit")
|
|
)
|
|
context["total_credits"] = sum(
|
|
x.amount
|
|
for x in account_model.transactionmodel_set.filter(tx_type="credit")
|
|
)
|
|
|
|
account_model.transactionmodel_set.all().posted().order_by(
|
|
"journal_entry__timestamp"
|
|
).select_related(
|
|
"journal_entry",
|
|
"journal_entry__entity_unit",
|
|
"journal_entry__ledger__billmodel",
|
|
"journal_entry__ledger__invoicemodel",
|
|
)
|
|
context["url_kwargs"] = self.kwargs
|
|
return context
|
|
|
|
|
|
class AccountUpdateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
|
):
|
|
"""
|
|
Represents a view for updating an existing account.
|
|
|
|
This class provides functionality to update an account's details using a form.
|
|
The user must be logged in and have the necessary permissions to access this
|
|
view. Upon successful update of the account, a success message is displayed
|
|
and the user is redirected to the account list page.
|
|
|
|
:ivar model: The model associated with this view which represents the account.
|
|
:type model: AccountModel
|
|
:ivar form_class: The form class used for updating account details.
|
|
:type form_class: AccountModelUpdateForm
|
|
:ivar template_name: The path to the template used for rendering the update view.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to upon success.
|
|
:type success_url: str
|
|
:ivar success_message: The success message displayed after updating an account
|
|
successfully.
|
|
:type success_message: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list of str
|
|
"""
|
|
|
|
model = AccountModel
|
|
form_class = AccountModelUpdateForm
|
|
template_name = "ledger/coa_accounts/account_form.html"
|
|
success_message = _("Account updated successfully")
|
|
permission_required = ["django_ledger.view_accountmodel"]
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
form.fields["_ref_node_id"].widget = HiddenInput()
|
|
form.fields["_position"].widget = HiddenInput()
|
|
return form
|
|
|
|
def form_valid(self, form):
|
|
form.instance.coa_model = (
|
|
ChartOfAccountModel.objects.get(pk=self.kwargs["coa_pk"])
|
|
or self.request.entity.get_default_coa()
|
|
)
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"account_list",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"coa_pk": self.kwargs["coa_pk"],
|
|
},
|
|
)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["url_kwargs"] = self.kwargs
|
|
coa_pk = context["url_kwargs"]["coa_pk"]
|
|
try:
|
|
context["coa_model"] = (
|
|
ChartOfAccountModel.objects.get(pk=coa_pk)
|
|
or self.request.entity.get_default_coa()
|
|
)
|
|
except Exception:
|
|
context["coa_model"] = self.request.entity.get_default_coa()
|
|
return context
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.delete_accountmodel")
|
|
def account_delete(request, dealer_slug, coa_pk, pk):
|
|
"""
|
|
Handles the deletion of an account object identified by its primary key (pk). Ensures
|
|
that the user has the necessary permissions to perform the deletion. Successfully
|
|
deletes the account and redirects to the account list view with a success message.
|
|
|
|
:param request: The HTTP request object representing the current user and request data.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the account to be deleted.
|
|
:type pk: int
|
|
:return: An HTTP redirect response to the account list page.
|
|
:rtype: HttpResponse
|
|
"""
|
|
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
account = get_object_or_404(AccountModel, pk=pk)
|
|
|
|
account.delete()
|
|
messages.success(request, _("Account deleted successfully"))
|
|
return redirect("account_list", dealer_slug=dealer_slug, coa_pk=coa_pk)
|
|
|
|
|
|
# Sales list
|
|
@login_required
|
|
@permission_required("inventory.view_saleorder", raise_exception=True)
|
|
def sales_list_view(request, dealer_slug):
|
|
"""
|
|
Handles the retrieval and presentation of a paginated list of item transactions for
|
|
sales, specific to the logged-in user's entity. Requires the user to have appropriate
|
|
permissions to view the list.
|
|
|
|
:param request: The HTTP request object containing metadata about the request,
|
|
such as HTTP method, user credentials, and sent data.
|
|
:type request: HttpRequest
|
|
|
|
:return: An HTTP response with the rendered sales list page containing the paginated
|
|
item transactions specific to the user's entity.
|
|
:rtype: HttpResponse
|
|
"""
|
|
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
staff = getattr(request.user, "staff", None)
|
|
qs = []
|
|
try:
|
|
if any([request.is_dealer, request.is_manager, request.is_accountant]):
|
|
qs = models.ExtraInfo.get_sale_orders(
|
|
staff=staff, is_dealer=True, dealer=dealer
|
|
)
|
|
elif request.is_staff:
|
|
qs = models.ExtraInfo.get_sale_orders(staff=staff, dealer=dealer)
|
|
except Exception as e:
|
|
print(e)
|
|
|
|
search_query = request.GET.get("q", None)
|
|
if search_query:
|
|
qs = qs.filter(
|
|
Q(customer__phone_number__icontains=search_query)
|
|
| Q(customer__customer_name__icontains=search_query)
|
|
).distinct()
|
|
|
|
paginator = Paginator(qs, 30)
|
|
page_number = request.GET.get("page")
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
context = {"txs": page_obj, "page_obj": page_obj}
|
|
return render(request, "sales/sales_list.html", context)
|
|
|
|
|
|
class SaleOrderDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
model = models.SaleOrder
|
|
template_name = "sales/saleorder_detail.html"
|
|
context_object_name = "sale_order"
|
|
permission_required = ["inventory.view_saleorder"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
dealer = self.request.dealer
|
|
context = super().get_context_data(**kwargs)
|
|
sale_order = self.get_object()
|
|
data = get_finance_data(sale_order.estimate, dealer)
|
|
print(data)
|
|
# Add additional context data
|
|
context["status_choices"] = dict(models.SaleOrder.STATUS_CHOICES)
|
|
context["page_title"] = _("Sales Order Details")
|
|
# Calculate any additional properties you want to display
|
|
context["is_delivered"] = sale_order.status == "DELIVERED"
|
|
context["is_cancelled"] = sale_order.status == "CANCELLED"
|
|
context["is_pending_approval"] = sale_order.status == "PENDING_APPROVAL"
|
|
context["data"] = data
|
|
|
|
return context
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
sale_order = self.get_object()
|
|
status = request.POST.get("status")
|
|
if status:
|
|
sale_order.status = status
|
|
sale_order.save()
|
|
messages.success(request, _("Sale order status updated"))
|
|
return redirect(
|
|
"order_detail", dealer_slug=sale_order.dealer.slug, pk=sale_order.pk
|
|
)
|
|
|
|
|
|
# Estimates
|
|
class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Handles the display of a paginated list of estimates for a specific entity.
|
|
|
|
This class-based view displays estimates related to an entity associated
|
|
with the logged-in user. It renders a paginated list of estimates on a
|
|
template and allows filtering of estimates based on their status. Access
|
|
to this view is restricted to users with the required permissions.
|
|
|
|
:ivar model: The database model associated with the view.
|
|
:type model: Model
|
|
:ivar template_name: The path to the template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable representing
|
|
the list of estimates.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of estimates displayed per page.
|
|
:type paginate_by: int
|
|
:ivar permission_required: List of permissions required to view this page.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = EstimateModel
|
|
template_name = "sales/estimates/estimate_list.html"
|
|
context_object_name = "estimates"
|
|
paginate_by = 20
|
|
permission_required = ["django_ledger.view_estimatemodel"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
|
|
if any(
|
|
[
|
|
self.request.is_dealer,
|
|
self.request.is_manager,
|
|
self.request.is_accountant,
|
|
]
|
|
):
|
|
qs = models.ExtraInfo.objects.filter(
|
|
dealer=dealer,
|
|
content_type=ContentType.objects.get_for_model(EstimateModel),
|
|
related_content_type=ContentType.objects.get_for_model(models.Staff),
|
|
).union(
|
|
models.ExtraInfo.objects.filter(
|
|
dealer=dealer,
|
|
content_type=ContentType.objects.get_for_model(EstimateModel),
|
|
related_content_type=ContentType.objects.get_for_model(User),
|
|
)
|
|
)
|
|
|
|
elif self.request.is_staff and self.request.is_sales:
|
|
qs = models.ExtraInfo.objects.filter(
|
|
dealer=dealer,
|
|
content_type=ContentType.objects.get_for_model(EstimateModel),
|
|
related_content_type=ContentType.objects.get_for_model(models.Staff),
|
|
related_object_id=self.request.staff.pk,
|
|
)
|
|
qs = EstimateModel.objects.filter(pk__in=[x.content_object.pk for x in qs])
|
|
search_query = self.request.GET.get("q", None)
|
|
if search_query:
|
|
qs = qs.filter(
|
|
Q(estimate_number__icontains=search_query)
|
|
| Q(customer__customer_name__icontains=search_query)
|
|
).distinct()
|
|
context["staff_estimates"] = qs
|
|
return context
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
entity = dealer.entity
|
|
status = self.request.GET.get("status")
|
|
|
|
queryset = entity.get_estimates()
|
|
if status:
|
|
queryset = queryset.filter(status=status)
|
|
search_query = self.request.GET.get("q", None)
|
|
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(estimate_number__icontains=search_query)
|
|
| Q(customer__customer_name__icontains=search_query)
|
|
).distinct()
|
|
|
|
return queryset
|
|
|
|
|
|
# @csrf_exempt
|
|
@login_required
|
|
@permission_required("django_ledger.add_estimatemodel", raise_exception=True)
|
|
def create_estimate(request, dealer_slug, slug=None):
|
|
"""
|
|
Creates a new estimate based on the provided data and saves it. This function processes
|
|
a POST request and expects a JSON payload containing details of the estimate such as
|
|
title, customer, terms, items, and quantities. It validates the input data, ensures
|
|
availability of stocks, and updates or creates the corresponding estimate in the database.
|
|
|
|
If `pk` is provided, it links the created estimate with an existing opportunity. It handles
|
|
the reservation of cars and updates the stock information accordingly.
|
|
|
|
:param request: The HttpRequest object containing user-specific data and state.
|
|
:type request: HttpRequest
|
|
:param pk: An optional primary key of the existing opportunity to associate with
|
|
the created estimate.
|
|
:type pk: int, optional
|
|
:return: A JsonResponse object with status and either the created quotation URL
|
|
or an error message. If the request method is not POST, it renders the
|
|
estimate creation form.
|
|
:rtype: JsonResponse or HttpResponse
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
entity = dealer.entity
|
|
|
|
if request.method == "POST":
|
|
# try:
|
|
data = json.loads(request.body)
|
|
title = data.get("title")
|
|
customer_id = data.get("customer")
|
|
customer = models.Customer.objects.filter(
|
|
pk=int(customer_id), dealer=dealer
|
|
).first()
|
|
|
|
items = data.get("item", [])
|
|
quantities = data.get("quantity", [])
|
|
|
|
if not all([items, quantities]):
|
|
return JsonResponse(
|
|
{"status": "error", "message": _("Items and Quantities are required")},
|
|
status=400,
|
|
)
|
|
|
|
if isinstance(quantities, list):
|
|
if "0" in quantities:
|
|
return JsonResponse(
|
|
{
|
|
"status": "error",
|
|
"message": _("Quantity must be greater than zero"),
|
|
}
|
|
)
|
|
else:
|
|
if int(quantities) <= 0:
|
|
return JsonResponse(
|
|
{
|
|
"status": "error",
|
|
"message": _("Quantity must be greater than zero"),
|
|
}
|
|
)
|
|
if isinstance(items, list):
|
|
for item, quantity in zip(items, quantities):
|
|
if (
|
|
int(quantity)
|
|
> models.Car.objects.filter(hash=item, status="available").count()
|
|
):
|
|
return JsonResponse(
|
|
{
|
|
"status": "error",
|
|
"message": _(
|
|
"Quantity must be less than or equal to the number of cars in stock"
|
|
),
|
|
},
|
|
)
|
|
else:
|
|
if (
|
|
int(quantities)
|
|
> models.Car.objects.filter(hash=items, status="available").count()
|
|
):
|
|
return JsonResponse(
|
|
{
|
|
"status": "error",
|
|
"message": _(
|
|
"Quantity must be less than or equal to the number of cars in stock"
|
|
),
|
|
},
|
|
)
|
|
estimate = entity.create_estimate(
|
|
estimate_title=title,
|
|
customer_model=customer.customer_model,
|
|
contract_terms="fixed",
|
|
)
|
|
if isinstance(items, list):
|
|
item_quantity_map = {}
|
|
for item, quantity in zip(items, quantities):
|
|
if item in item_quantity_map:
|
|
item_quantity_map[item] += int(quantity)
|
|
else:
|
|
item_quantity_map[item] = int(quantity)
|
|
item_list = list(item_quantity_map.keys())
|
|
quantity_list = list(item_quantity_map.values())
|
|
|
|
items_list = [
|
|
{"item_id": item_list[i], "quantity": quantity_list[i]}
|
|
for i in range(len(item_list))
|
|
]
|
|
items_txs = []
|
|
for item in items_list:
|
|
car_instance = models.Car.objects.filter(
|
|
hash=item.get("item_id"),
|
|
# finances__is_sold=False,
|
|
colors__isnull=False,
|
|
marked_price__gt=1,
|
|
status="available",
|
|
).all()
|
|
|
|
for i in car_instance[: int(quantities[0])]:
|
|
print(i)
|
|
items_txs.append(
|
|
{
|
|
"item_number": i.item_model.item_number,
|
|
"quantity": 1,
|
|
"unit_cost": round(float(i.marked_price)),
|
|
"unit_revenue": round(float(i.marked_price)),
|
|
"total_amount": round(
|
|
float(i.final_price_plus_vat)
|
|
), # TODO : check later
|
|
}
|
|
)
|
|
|
|
estimate_itemtxs = {
|
|
item.get("item_number"): {
|
|
"unit_cost": item.get("unit_cost"),
|
|
"unit_revenue": item.get("unit_revenue"),
|
|
"quantity": item.get("quantity"),
|
|
"total_amount": item.get("total_amount"),
|
|
}
|
|
for item in items_txs
|
|
}
|
|
# else:
|
|
# item = entity.get_items_all().filter(pk=items).first()
|
|
# instance = models.Car.objects.get(vin=item.name)
|
|
# estimate_itemtxs = {
|
|
# item.item_number: {
|
|
# "unit_cost": instance.cost_price,
|
|
# "unit_revenue": instance.marked_price,
|
|
# "quantity": Decimal(quantities),
|
|
# "total_amount": instance.final_price_plus_vat * int(quantities),# TODO : check later
|
|
# }
|
|
# }
|
|
|
|
try:
|
|
estimate.migrate_itemtxs(
|
|
itemtxs=estimate_itemtxs,
|
|
commit=True,
|
|
operation=EstimateModel.ITEMIZE_APPEND,
|
|
)
|
|
except Exception as e:
|
|
estimate.delete()
|
|
return JsonResponse(
|
|
{
|
|
"status": "error",
|
|
"message": e,
|
|
}
|
|
)
|
|
if isinstance(items, list):
|
|
for item in estimate_itemtxs.keys():
|
|
item_instance = entity.get_items_all().filter(item_number=item).first()
|
|
|
|
# else:
|
|
# item_instance = ItemModel.objects.filter(
|
|
# additioinal_info__car_info__hash=items
|
|
# ).first()
|
|
# instance = models.Car.objects.get(hash=item)
|
|
# reserve_car(instance, request) #TODO
|
|
|
|
opportunity_id = data.get("opportunity_id")
|
|
if opportunity_id != "None":
|
|
opportunity = models.Opportunity.objects.get(slug=opportunity_id)
|
|
opportunity.estimate = estimate
|
|
opportunity.save()
|
|
|
|
if request.is_staff:
|
|
models.ExtraInfo.objects.create(
|
|
dealer=dealer,
|
|
content_object=estimate,
|
|
related_object=request.staff,
|
|
created_by=request.user,
|
|
)
|
|
else:
|
|
models.ExtraInfo.objects.create(
|
|
dealer=dealer,
|
|
content_object=estimate,
|
|
related_object=request.user,
|
|
created_by=request.user,
|
|
data={"vat_rate": 0.15, "discount": 0},
|
|
)
|
|
|
|
url = reverse(
|
|
"estimate_detail", kwargs={"dealer_slug": dealer.slug, "pk": estimate.pk}
|
|
)
|
|
return JsonResponse(
|
|
{
|
|
"status": "success",
|
|
"message": _("Quotation created successfully"),
|
|
"url": f"{url}",
|
|
}
|
|
)
|
|
#######################################
|
|
##GET
|
|
form = forms.EstimateModelCreateForm()
|
|
form.fields["customer"].queryset = models.Customer.objects.filter(
|
|
dealer=dealer, active=True
|
|
)
|
|
|
|
if slug:
|
|
opportunity = models.Opportunity.objects.get(slug=slug)
|
|
customer = opportunity.customer
|
|
print(customer)
|
|
form.fields["customer"].queryset = models.Customer.objects.filter(
|
|
pk=customer.pk
|
|
)
|
|
form.initial["customer"] = customer
|
|
|
|
car_list = (
|
|
models.Car.objects.filter(
|
|
dealer=dealer,
|
|
colors__isnull=False,
|
|
marked_price__gt=1,
|
|
status="available",
|
|
)
|
|
.annotate(
|
|
exterior_color=F("colors__exterior__rgb"),
|
|
interior_color=F("colors__interior__rgb"),
|
|
color_name=F("colors__exterior__arabic_name"),
|
|
)
|
|
.values_list(
|
|
"id_car_make__arabic_name",
|
|
"id_car_model__arabic_name",
|
|
"id_car_serie__arabic_name",
|
|
"id_car_trim__arabic_name",
|
|
"exterior_color",
|
|
"interior_color",
|
|
"color_name",
|
|
"hash",
|
|
"id_car_make__logo",
|
|
)
|
|
.annotate(hash_count=Count("hash"))
|
|
.distinct()
|
|
)
|
|
print(car_list)
|
|
context = {
|
|
"form": form,
|
|
"items": [
|
|
{
|
|
"make": x[0],
|
|
"model": x[1],
|
|
"serie": x[2],
|
|
"trim": x[3],
|
|
"exterior_color": x[4],
|
|
"interior_color": x[5],
|
|
"color_name": x[6],
|
|
"hash": x[7],
|
|
"logo": settings.MEDIA_URL + x[8],
|
|
"hash_count": x[9],
|
|
}
|
|
for x in car_list
|
|
],
|
|
"opportunity_id": slug if slug else None,
|
|
"customer_count": entity.get_customers().count(),
|
|
"no_items_message": _(
|
|
"Please add at least one car or complete the car info before creating a quotation."
|
|
),
|
|
"no_items_button": _("Add car"),
|
|
"no_customers_message": _(
|
|
"Please add at least one customer before creating a quotation."
|
|
),
|
|
"no_customers_button": _("Add Customer"),
|
|
}
|
|
|
|
return render(request, "sales/estimates/estimate_form.html", context)
|
|
|
|
|
|
class EstimateDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Represents the detailed view for an EstimateModel instance.
|
|
|
|
This class provides functionality to display detailed information about
|
|
a specific EstimateModel instance. It ensures the user is authenticated
|
|
and has the required permissions to access the estimate details. The class
|
|
also integrates additional financial and invoice-related data into the
|
|
context for more comprehensive display and functionality.
|
|
|
|
:ivar model: Specifies the model associated with the view.
|
|
:type model: ModelBase
|
|
:ivar template_name: Path to the template used for rendering the detailed view.
|
|
:type template_name: str
|
|
:ivar context_object_name: Name used to refer to the EstimateModel instance in the context.
|
|
:type context_object_name: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = EstimateModel
|
|
template_name = "sales/estimates/estimate_detail.html"
|
|
context_object_name = "estimate"
|
|
permission_required = ["django_ledger.view_estimatemodel"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
estimate = kwargs.get("object")
|
|
|
|
if estimate.get_itemtxs_data():
|
|
# calculator = CarFinanceCalculator(estimate)
|
|
# finance_data = calculator.get_finance_data()
|
|
finance_data = get_finance_data(estimate, dealer)
|
|
|
|
invoice_obj = InvoiceModel.objects.all().filter(ce_model=estimate).first()
|
|
kwargs["data"] = finance_data
|
|
kwargs["customer_obj"] = estimate.customer.customer_set.first()
|
|
kwargs["dealer_info"] = dealer
|
|
|
|
kwargs["invoice"] = invoice_obj
|
|
try:
|
|
car = estimate.get_itemtxs_data()[0].first().item_model.car
|
|
extra_info = models.ExtraInfo.objects.get(
|
|
dealer=dealer,
|
|
content_type=ContentType.objects.get_for_model(EstimateModel),
|
|
object_id=estimate.pk
|
|
)
|
|
try:
|
|
additionals = extra_info.data.get("additionals")
|
|
if additionals:
|
|
selected_items = models.AdditionalServices.objects.filter(dealer=dealer,pk__in=additionals)
|
|
else:
|
|
selected_items = []
|
|
except Exception as e:
|
|
selected_items = []
|
|
if estimate.is_draft() or estimate.is_review():
|
|
kwargs["grand_total"] = finance_data.get("final_price") + sum([x.price_ for x in selected_items])
|
|
else:
|
|
kwargs["grand_total"] = finance_data.get("grand_total")
|
|
form = forms.AdditionalFinancesForm()
|
|
form.fields["additional_finances"].queryset = form.fields[
|
|
"additional_finances"
|
|
].queryset.filter(dealer=dealer) #
|
|
form.initial["additional_finances"] = selected_items
|
|
kwargs["additionals_form"] = form
|
|
kwargs["additional_finances"] = selected_items
|
|
|
|
except Exception as e:
|
|
logger.error(e)
|
|
return super().get_context_data(**kwargs)
|
|
|
|
|
|
class EstimatePrintView(EstimateDetailView):
|
|
"""
|
|
A view to render a printer-friendly version of the estimate.
|
|
It reuses the data-fetching logic from EstimateDetailView but
|
|
uses a dedicated, stripped-down print template.
|
|
"""
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
context = self.get_context_data(object=self.object)
|
|
|
|
# lang = request.GET.get('lang', 'ar')
|
|
|
|
if request.GET.get("lang") == "en":
|
|
template_path = "sales/estimates/estimate_preview_en.html"
|
|
else:
|
|
template_path = "sales/estimates/estimate_preview_ar.html"
|
|
|
|
html_string = render_to_string(template_path, context)
|
|
|
|
base_url = request.build_absolute_uri("/")
|
|
pdf_file = HTML(string=html_string, base_url=base_url).write_pdf()
|
|
|
|
response = HttpResponse(pdf_file, content_type="application/pdf")
|
|
response["Content-Disposition"] = (
|
|
f'attachment; filename="estimate_{self.object.estimate_number}.pdf"'
|
|
)
|
|
|
|
return response
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.add_saleorder", raise_exception=True)
|
|
def create_sale_order(request, dealer_slug, pk):
|
|
"""
|
|
Creates a sale order for a given estimate and updates associated item and car data.
|
|
|
|
This view is responsible for handling the submission of a sale order form linked to
|
|
a specific estimate. It ensures that the estimate is approved if not already, updates
|
|
the status of the related car items as sold, and redirects to the estimate's detailed
|
|
view upon successful creation of the sale order. If the request method is not POST, it
|
|
renders the form for the user to input sale order details, along with other contextual
|
|
information like estimate data and car finance details.
|
|
|
|
:param request: HTTP request object.
|
|
:type request: HttpRequest
|
|
:param pk: Primary key of the estimate to create a sale order for.
|
|
:type pk: int
|
|
:return: An HTTP response rendering the sale order form if the method is GET or invalid
|
|
POST data, or redirects to the estimate detail view upon successful creation.
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
estimate = get_object_or_404(EstimateModel, pk=pk)
|
|
items = estimate.get_itemtxs_data()[0].all()
|
|
if request.method == "POST":
|
|
form = forms.SaleOrderForm(request.POST)
|
|
if form.is_valid():
|
|
instance = form.save(commit=False)
|
|
instance.dealer = dealer
|
|
instance.estimate = estimate
|
|
instance.customer = estimate.customer.customer_set.first()
|
|
instance.created_by = request.user
|
|
instance.last_modified_by = request.user
|
|
instance.save()
|
|
|
|
if not estimate.is_approved():
|
|
estimate.mark_as_approved()
|
|
estimate.save()
|
|
for item in estimate.get_itemtxs_data()[0].all():
|
|
try:
|
|
# item.item_model.additional_info["car_info"]["status"] = "sold"
|
|
item.item_model.save()
|
|
logger.debug(
|
|
f"Car status updated to 'sold' for item.item_model PK: {getattr(item.item_model, 'pk', 'N/A')}."
|
|
)
|
|
|
|
except KeyError:
|
|
logger.warning(
|
|
f"KeyError: 'car_info' or 'status' key missing when attempting to update status to 'sold' for item.item_model PK: {getattr(item.item_model, 'pk', 'N/A')}."
|
|
)
|
|
pass
|
|
item.item_model.car.sold_date = (
|
|
timezone.now()
|
|
) # to be checked added by faheed
|
|
item.item_model.car.save() # to be checked added byfaheed
|
|
item.item_model.car.mark_as_sold()
|
|
messages.success(request, "Sale Order created successfully")
|
|
|
|
else:
|
|
print(form.errors)
|
|
messages.error(request, "Invalid form data")
|
|
return redirect("estimate_detail", dealer_slug=dealer_slug, pk=estimate.pk)
|
|
|
|
form = forms.SaleOrderForm()
|
|
# customer = estimate.customer.customer_set.first()
|
|
# form.fields["estimate"].queryset = EstimateModel.objects.filter(pk=pk)
|
|
# form.initial["estimate"] = estimate
|
|
# form.fields["customer"].queryset = models.Customer.objects.filter(pk=customer.pk)
|
|
# form.initial["customer"] = customer
|
|
# if hasattr(estimate, "opportunity"):
|
|
# form.initial["opportunity"] = estimate.opportunity
|
|
# else:
|
|
# form.fields["opportunity"].widget = HiddenInput()
|
|
|
|
# calculator = CarFinanceCalculator(estimate)
|
|
finance_data = get_finance_data(estimate, dealer)
|
|
return render(
|
|
request,
|
|
"sales/estimates/sale_order_form.html",
|
|
{"form": form, "estimate": estimate, "items": items, "data": finance_data},
|
|
)
|
|
|
|
|
|
@login_required
|
|
@require_POST
|
|
def update_estimate_discount(request, dealer_slug, pk):
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
estimate = get_object_or_404(EstimateModel, pk=pk)
|
|
extra_info = models.ExtraInfo.objects.get(
|
|
dealer=dealer,
|
|
content_type=ContentType.objects.get_for_model(EstimateModel),
|
|
object_id=estimate.pk,
|
|
)
|
|
# calculator = CarFinanceCalculator(estimate)
|
|
# finance_data = calculator.get_finance_data()
|
|
discount_amount = request.POST.get("discount_amount", 0)
|
|
finance_data = get_finance_data(estimate, dealer)
|
|
car = finance_data.get("car")
|
|
if Decimal(discount_amount) >= car.marked_price:
|
|
messages.error(
|
|
request, _("Discount amount cannot be greater than marked price")
|
|
)
|
|
return redirect("estimate_detail", dealer_slug=dealer_slug, pk=pk)
|
|
|
|
if Decimal(discount_amount) > car.marked_price * Decimal("0.5"):
|
|
messages.warning(
|
|
request,
|
|
_(
|
|
"Discount amount is greater than 50% of the marked price, proceed with caution."
|
|
),
|
|
)
|
|
else:
|
|
messages.success(request, _("Discount updated successfully"))
|
|
extra_info.data.update({"discount": Decimal(discount_amount)})
|
|
extra_info.save()
|
|
return redirect("estimate_detail", dealer_slug=dealer_slug, pk=pk)
|
|
|
|
|
|
@login_required
|
|
@require_POST
|
|
def update_estimate_additionals(request, dealer_slug, pk):
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
form = forms.AdditionalFinancesForm(request.POST)
|
|
if request.method == "POST":
|
|
if form.is_valid():
|
|
estimate = get_object_or_404(EstimateModel, pk=pk)
|
|
car = estimate.get_itemtxs_data()[0].first().item_model.car
|
|
additionals = form.cleaned_data["additional_finances"]
|
|
# car.additional_services.set(additionals)
|
|
additionals = [additional.pk for additional in additionals]
|
|
|
|
extra_info = models.ExtraInfo.objects.get(
|
|
dealer=dealer,
|
|
content_type=ContentType.objects.get_for_model(EstimateModel),
|
|
object_id=estimate.pk,
|
|
)
|
|
extra_info.data.update({"additionals": additionals})
|
|
extra_info.save()
|
|
car.save()
|
|
messages.success(request, "Additional Finances updated successfully")
|
|
return redirect("estimate_detail", dealer_slug=dealer_slug, pk=pk)
|
|
|
|
|
|
class SaleOrderDetail(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
model = models.SaleOrder
|
|
template_name = "sales/orders/order_details.html"
|
|
context_object_name = "saleorder"
|
|
permission_required = ["inventory.view_saleorder"]
|
|
|
|
def get_object(self, queryset=None):
|
|
order_pk = self.kwargs.get("order_pk")
|
|
print("hi")
|
|
return models.SaleOrder.objects.get(
|
|
pk=order_pk,
|
|
)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
saleorder = kwargs.get("object")
|
|
dealer = self.request.dealer
|
|
estimate = saleorder.estimatep
|
|
if estimate.get_itemtxs_data():
|
|
# calculator = CarFinanceCalculator(estimate)
|
|
# finance_data = calculator.get_finance_data()
|
|
finance_data = get_finance_data(estimate, dealer)
|
|
kwargs["data"] = finance_data
|
|
return super().get_context_data(**kwargs)
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.view_saleorder", raise_exception=True)
|
|
def preview_sale_order(request, dealer_slug, pk):
|
|
"""
|
|
Handles rendering of the sale order preview page for a specific estimate.
|
|
|
|
This view retrieves an `EstimateModel` object based on the provided primary
|
|
key (`pk`), fetches related car finance data, and renders the preview of the
|
|
sale order associated with the given estimate.
|
|
|
|
:param request: The HTTP request object
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the `EstimateModel` to retrieve
|
|
:type pk: int
|
|
:return: HTTP response containing the rendered sale order preview page
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
estimate = get_object_or_404(EstimateModel, pk=pk)
|
|
data = get_car_finance_data(estimate)
|
|
return render(
|
|
request,
|
|
"sales/estimates/sale_order_preview.html",
|
|
{"order": estimate.sale_orders.first(), "data": data, "estimate": estimate},
|
|
)
|
|
|
|
|
|
class PaymentRequest(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Represents a detailed view of a payment request for an estimate.
|
|
|
|
This class is a Django DetailView that leverages mixins for login
|
|
and permission requirements. It displays details of an estimate
|
|
and fetches related car data based on the estimate's items.
|
|
It ensures only authorized users can access the payment request details.
|
|
|
|
:ivar model: The Django model associated with this view. It is used
|
|
to fetch and display detailed information for an estimate.
|
|
:type model: EstimateModel
|
|
:ivar template_name: The template utilized to render the detailed
|
|
payment request page for estimates.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context object to be
|
|
accessible in the template for the detailed view.
|
|
:type context_object_name: str
|
|
:ivar permission_required: Permissions required for accessing
|
|
this view. The user must have the specified permissions.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = EstimateModel
|
|
template_name = "sales/estimates/payment_request_detail.html"
|
|
context_object_name = "estimate"
|
|
permission_required = ["django_ledger.view_invoicemodel"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["cars"] = [
|
|
models.Car.objects.get(vin=car.item_model.name)
|
|
for car in context["estimate"].get_itemtxs_data()[0].all()
|
|
]
|
|
|
|
return context
|
|
|
|
|
|
class EstimatePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Represents a view for previewing an estimate with user-specific permissions and
|
|
context data processing. This class provides functionality to render a detailed
|
|
view of an estimate while ensuring user authentication and permission verification.
|
|
|
|
This view is primarily used in a sales module to preview the detailed financial
|
|
breakdown of an estimate, including tax, discount, additional services, and total
|
|
amount.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: Type[models.Model]
|
|
:ivar context_object_name: The name of the context variable containing the object.
|
|
:type context_object_name: str
|
|
:ivar template_name: The path to the template used for rendering this view.
|
|
:type template_name: str
|
|
:ivar permission_required: List of permissions required to access this view.
|
|
:type permission_required: List[str]
|
|
"""
|
|
|
|
model = EstimateModel
|
|
context_object_name = "estimate"
|
|
template_name = "sales/estimates/estimate_preview.html"
|
|
permission_required = ["django_ledger.view_estimatemodel"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
estimate = kwargs.get("object")
|
|
if estimate.get_itemtxs_data():
|
|
# data = get_financial_values(estimate)
|
|
# calculator = CarFinanceCalculator(estimate)
|
|
kwargs["data"] = get_finance_data(estimate, self.request.dealer)
|
|
|
|
return super().get_context_data(**kwargs)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.change_estimatemodel", raise_exception=True)
|
|
def estimate_mark_as(request, dealer_slug, pk):
|
|
"""
|
|
Marks an estimate with a specified status based on the requested action and
|
|
permissions. The marking possibilities include review, approval, rejection,
|
|
completion, and cancellation. The function validates whether the estimate
|
|
can transition to the desired status before updating it. It also handles
|
|
notifications and updates related entities if required, such as car status
|
|
changes upon cancellation.
|
|
|
|
:param request: The HTTP request object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the estimate to be marked.
|
|
:type pk: int
|
|
:return: A redirect response to the estimate detail view.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
if not (
|
|
request.user.has_perm("django_ledger.can_approve_estimatemodel")
|
|
or request.user.has_perm("django_ledger.change_estimatemodel")
|
|
):
|
|
raise PermissionDenied
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
estimate = get_object_or_404(EstimateModel, pk=pk)
|
|
mark = request.GET.get("mark")
|
|
|
|
if mark:
|
|
if mark == "review":
|
|
if not estimate.can_review():
|
|
messages.error(request, _("Quotation is not ready for review"))
|
|
return redirect(
|
|
"estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk
|
|
)
|
|
estimate.mark_as_review()
|
|
elif mark == "approved":
|
|
if not estimate.can_approve():
|
|
messages.error(request, _("Quotation is not ready for approval"))
|
|
return redirect(
|
|
"estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk
|
|
)
|
|
estimate.mark_as_approved()
|
|
estimate.save()
|
|
# Reserve The Car
|
|
car = estimate.get_itemtxs_data()[0].first().item_model.car
|
|
reserve_car(car, request)
|
|
extra_info = models.ExtraInfo.objects.get(
|
|
dealer=dealer,
|
|
content_type=ContentType.objects.get_for_model(EstimateModel),
|
|
object_id=estimate.pk
|
|
)
|
|
try:
|
|
additionals = extra_info.data.get("additionals")
|
|
if additionals:
|
|
selected_items = models.AdditionalServices.objects.filter(dealer=dealer,pk__in=additionals)
|
|
else:
|
|
selected_items = []
|
|
except Exception as e:
|
|
logger.error(e)
|
|
selected_items = []
|
|
if selected_items:
|
|
car.additional_services.clear()
|
|
car.additional_services.set(selected_items)
|
|
|
|
messages.success(request, _("Quotation approved successfully"))
|
|
return redirect("estimate_list", dealer_slug=dealer.slug)
|
|
elif mark == "rejected":
|
|
if not estimate.can_cancel():
|
|
messages.error(request, _("Quotation is not ready for rejection"))
|
|
return redirect(
|
|
"estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk
|
|
)
|
|
estimate.mark_as_canceled()
|
|
messages.success(request, _("Quotation canceled successfully"))
|
|
elif mark == "completed":
|
|
if not estimate.can_complete():
|
|
messages.error(request, _("Quotation is not ready for completion"))
|
|
return redirect(
|
|
"estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk
|
|
)
|
|
elif mark == "canceled":
|
|
if not estimate.can_cancel():
|
|
messages.error(request, _("Quotation is not ready for cancellation"))
|
|
return redirect(
|
|
"estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk
|
|
)
|
|
estimate.mark_as_canceled()
|
|
try:
|
|
car = models.Car.objects.get(
|
|
vin=estimate.get_itemtxs_data()[0].first().item_model.name
|
|
)
|
|
car.status = "available"
|
|
car.save()
|
|
logger.debug(
|
|
f"Car VIN '{car.vin}' status updated to 'available' for Estimate ID: {getattr(estimate, 'pk', 'N/A')}."
|
|
)
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Failed to update car status to 'available' for Estimate ID: {getattr(estimate, 'pk', 'N/A')}. "
|
|
f"Attempted VIN: '{car.vin}'. Error: {e}",
|
|
exc_info=True,
|
|
)
|
|
|
|
messages.success(request, _("Quotation canceled successfully"))
|
|
estimate.save()
|
|
messages.success(request, _("Quotation marked as ") + mark.upper())
|
|
|
|
return redirect("estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk)
|
|
|
|
|
|
# Invoice
|
|
class InvoiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Handles the display and management of a list of invoices.
|
|
|
|
This class-based view provides functionality for displaying a paginated list of invoices
|
|
while ensuring that only authenticated and authorized users can access the view. It allows
|
|
for applying search filters to the displayed invoices, based on user inputs. The view is
|
|
designed to work as part of the Django framework, and utilizes models, templates, and
|
|
permissions specific to the application.
|
|
|
|
:ivar model: The model representing invoices.
|
|
:type model: type
|
|
:ivar template_name: Path to the template used to render the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: Name used to reference the list of invoices in the template.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of invoices to display per page.
|
|
:type paginate_by: int
|
|
:ivar permission_required: List of permissions required for accessing this view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = InvoiceModel
|
|
template_name = "sales/invoices/invoice_list.html"
|
|
context_object_name = "invoices"
|
|
paginate_by = 20
|
|
permission_required = ["django_ledger.view_invoicemodel"]
|
|
|
|
def get_queryset(self):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
entity = dealer.entity
|
|
staff = getattr(self.request.user, "staff", None)
|
|
qs = None
|
|
try:
|
|
if any(
|
|
[
|
|
self.request.is_dealer,
|
|
self.request.is_manager,
|
|
self.request.is_accountant,
|
|
]
|
|
):
|
|
qs = models.ExtraInfo.get_invoices(
|
|
staff=staff, is_dealer=True, dealer=dealer
|
|
)
|
|
elif self.request.is_staff:
|
|
qs = models.ExtraInfo.get_invoices(staff=staff, dealer=dealer)
|
|
except Exception as e:
|
|
print(e)
|
|
|
|
query = self.request.GET.get("q")
|
|
invoices = qs
|
|
# invoices = dealer.entity.get_invoices()
|
|
return apply_search_filters(invoices, query)
|
|
|
|
|
|
class InvoiceDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Handles the detailed view for an invoice.
|
|
|
|
This class is responsible for displaying detailed information about a specific
|
|
invoice. It uses Django's DetailView to render the details, requires the user
|
|
to be logged in, and enforces specific permissions for viewing invoices. The
|
|
class also processes and includes additional invoice-specific data into the
|
|
context provided to the template.
|
|
|
|
:ivar model: Specifies the model to be used for the detail view.
|
|
:type model: Type[InvoiceModel]
|
|
:ivar template_name: Path to the template used for rendering the invoice details.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable representing the object.
|
|
:type context_object_name: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = InvoiceModel
|
|
template_name = "sales/invoices/invoice_detail.html"
|
|
context_object_name = "invoice"
|
|
permission_required = ["django_ledger.view_invoicemodel"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
invoice = kwargs.get("object")
|
|
|
|
if invoice.get_itemtxs_data():
|
|
# calculator = CarFinanceCalculator(invoice)
|
|
# finance_data = calculator.get_finance_data()
|
|
finance_data = get_finance_data(invoice, self.request.dealer)
|
|
kwargs["data"] = finance_data
|
|
kwargs["payments"] = JournalEntryModel.objects.filter(
|
|
ledger=invoice.ledger
|
|
).all()
|
|
|
|
return super().get_context_data(**kwargs)
|
|
|
|
|
|
class DraftInvoiceModelUpdateFormView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
|
):
|
|
"""
|
|
Representation of a form view for updating draft invoices.
|
|
|
|
This class inherits from Django's login and permission mixins as well as
|
|
UpdateView, providing the functionality required to update an existing
|
|
instance of `InvoiceModel` using the associated form class. It enforces
|
|
that the user logged in has the required permissions to view invoices
|
|
and redirects to the invoice list upon successful update. This form
|
|
view specifically customizes the form initialization logic to include
|
|
additional data based on the requesting user's type and associated
|
|
entity.
|
|
|
|
:ivar model: The Django model that this view will operate upon.
|
|
:type model: Type[InvoiceModel]
|
|
:ivar form_class: The form class to be used for updating `InvoiceModel`
|
|
instances.
|
|
:type form_class: Type[DraftInvoiceModelUpdateForm]
|
|
:ivar template_name: The path to the template used to render this view.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to upon successful form submission.
|
|
:type success_url: str
|
|
:ivar permission_required: The list of permissions required to access
|
|
this view.
|
|
:type permission_required: List[str]
|
|
"""
|
|
|
|
model = InvoiceModel
|
|
form_class = DraftInvoiceModelUpdateForm
|
|
template_name = "sales/invoices/draft_invoice_update.html"
|
|
success_url = reverse_lazy("invoice_list")
|
|
permission_required = ["django_ledger.change_invoicemodel"]
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
kwargs["entity_slug"] = dealer.entity
|
|
kwargs["user_model"] = dealer.entity.admin
|
|
return kwargs
|
|
|
|
|
|
class ApprovedInvoiceModelUpdateFormView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
|
):
|
|
"""
|
|
Handles the view for updating approved invoice models.
|
|
|
|
This class-based view is used to update the details of an approved invoice. It
|
|
inherits from ``LoginRequiredMixin``, ``PermissionRequiredMixin``, and
|
|
``UpdateView`` to ensure secured access to the functionality and integrates
|
|
with Django's permission system. Users must have the required permission to
|
|
access this view. It utilizes a custom update form and is configured with
|
|
specific success URLs.
|
|
|
|
:ivar model: The model associated with this view, which is ``InvoiceModel``.
|
|
:type model: type
|
|
:ivar form_class: The form class used for handling updates, which is
|
|
``ApprovedInvoiceModelUpdateForm``.
|
|
:type form_class: type
|
|
:ivar template_name: The path to the template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar success_url: URL to redirect upon successful operation. This uses
|
|
``reverse_lazy`` to point to the invoice list by default.
|
|
:type success_url: django.urls.reverse_lazy
|
|
:ivar permission_required: The permission required to access this view. It is
|
|
set to ``django_ledger.view_invoicemodel`` by default.
|
|
:type permission_required: list of str
|
|
"""
|
|
|
|
model = InvoiceModel
|
|
form_class = ApprovedInvoiceModelUpdateForm
|
|
template_name = "sales/invoices/approved_invoice_update.html"
|
|
success_url = reverse_lazy("invoice_list")
|
|
permission_required = ["django_ledger.change_invoicemodel"]
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
kwargs["entity_slug"] = dealer.entity
|
|
kwargs["user_model"] = dealer.entity.admin
|
|
return kwargs
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"invoice_detail",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"entity_slug": self.kwargs["entity_slug"],
|
|
"pk": self.object.pk,
|
|
},
|
|
)
|
|
|
|
|
|
class PaidInvoiceModelUpdateFormView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
|
):
|
|
"""
|
|
Handles the update process for paid invoices.
|
|
|
|
This view allows updating invoice details for paid invoices within the system.
|
|
The user must be logged in and have the necessary permissions for accessing
|
|
and modifying invoice-related data. Additionally, the view ensures that any
|
|
required validation is performed and only executes updates when the desired
|
|
business logic conditions are met. It inherits from `UpdateView` and includes
|
|
custom behavior for handling form validation, success URLs, and associated
|
|
permissions.
|
|
|
|
:ivar model: The model class associated with the view. Represents the invoice
|
|
data being managed.
|
|
:type model: type
|
|
:ivar form_class: The form class used to render and validate the update form.
|
|
:type form_class: type
|
|
:ivar template_name: Path to the template used to render the update view.
|
|
:type template_name: str
|
|
:ivar success_url: Default URL to redirect to after a successful update. This
|
|
can be overridden in specific cases.
|
|
:type success_url: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list of str
|
|
"""
|
|
|
|
model = InvoiceModel
|
|
form_class = PaidInvoiceModelUpdateForm
|
|
template_name = "sales/invoices/paid_invoice_update.html"
|
|
success_url = reverse_lazy("invoice_list")
|
|
permission_required = ["django_ledger.change_invoicemodel"]
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
kwargs["entity_slug"] = dealer.entity
|
|
kwargs["user_model"] = dealer.entity.admin
|
|
return kwargs
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"invoice_detail",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"entity_slug": self.kwargs["entity_slug"],
|
|
"pk": self.object.pk,
|
|
},
|
|
)
|
|
|
|
def form_valid(self, form):
|
|
invoice = form.save()
|
|
|
|
if invoice.get_amount_open() > 0:
|
|
messages.error(self.request, "Invoice is not fully paid")
|
|
return redirect(
|
|
"invoice_detail",
|
|
dealer_slug=self.kwargs["dealer_slug"],
|
|
entity_slug=self.kwargs["entity_slug"],
|
|
pk=invoice.pk,
|
|
)
|
|
else:
|
|
invoice.post_ledger()
|
|
invoice.save()
|
|
return super().form_valid(form)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.change_invoicemodel", raise_exception=True)
|
|
def invoice_mark_as(request, dealer_slug, pk):
|
|
"""
|
|
Marks an invoice as approved if it meets the required conditions.
|
|
|
|
This view is responsible for marking an invoice as approved based on the provided
|
|
`mark` parameter. If the `mark` parameter is specified as "accept" and the invoice
|
|
is eligible for approval, it gets approved and saved. Otherwise, an error message
|
|
is displayed. The function requires the user to be logged in and to have the
|
|
appropriate permission to change the InvoiceModel.
|
|
|
|
:param request: The HTTP request object containing metadata about the request.
|
|
:type request: django.http.HttpRequest
|
|
:param pk: The primary key of the invoice to be processed.
|
|
:type pk: int
|
|
:return: An HTTP redirect response to the invoice detail page after processing.
|
|
:rtype: django.http.HttpResponse
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
invoice = get_object_or_404(InvoiceModel, pk=pk)
|
|
mark = request.GET.get("mark")
|
|
if mark and mark == "accept":
|
|
if not invoice.can_approve():
|
|
messages.error(request, "invoice is not ready for approval")
|
|
return redirect(
|
|
"invoice_detail",
|
|
dealer_slug=dealer_slug,
|
|
entity_slug=request.entity.slug,
|
|
pk=invoice.pk,
|
|
)
|
|
invoice.mark_as_approved(
|
|
entity_slug=dealer.entity.slug, user_model=dealer.entity.admin
|
|
)
|
|
invoice.save()
|
|
return redirect(
|
|
"invoice_detail",
|
|
dealer_slug=dealer_slug,
|
|
entity_slug=request.entity.slug,
|
|
pk=invoice.pk,
|
|
)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.add_invoicemodel", raise_exception=True)
|
|
def invoice_create(request, dealer_slug, pk):
|
|
"""
|
|
Handles the creation of a new invoice associated with a given estimate. It validates
|
|
the submitted data through a form, processes the invoice, updates related models, and
|
|
finalizes the estimate. If successful, redirects to the detailed view of the created
|
|
invoice. If the submitted data is invalid or the request is not a POST request, renders
|
|
the invoice creation form.
|
|
|
|
:param request: The HTTP request object.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the estimate associated with the invoice.
|
|
:type pk: int
|
|
:return: An HTTP response. Redirects to the "invoice_detail" view upon successful invoice
|
|
creation or renders the invoice creation form template otherwise.
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
estimate = get_object_or_404(EstimateModel, pk=pk)
|
|
entity = dealer.entity
|
|
|
|
if request.method == "POST":
|
|
form = forms.InvoiceModelCreateForm(
|
|
request.POST, entity_slug=entity.slug, user_model=entity.admin
|
|
)
|
|
if form.is_valid():
|
|
invoice = form.save(commit=False)
|
|
ledger = entity.create_ledger(name=str(invoice.pk))
|
|
invoice.ledgar = ledger
|
|
ledger.invoicemodel = invoice
|
|
ledger.save()
|
|
invoice.save()
|
|
|
|
# calculator = CarFinanceCalculator(estimate)
|
|
# finance_data = calculator.get_finance_data()
|
|
|
|
finance_data = get_finance_data(estimate, dealer)
|
|
car = finance_data.get("car")
|
|
invoice_itemtxs = {
|
|
car.item_model.item_number: {
|
|
"unit_cost": finance_data.get("grand_total"),
|
|
"quantity": 1,
|
|
"total_amount": finance_data.get("grand_total"),
|
|
}
|
|
}
|
|
|
|
invoice_itemtxs = invoice.migrate_itemtxs(
|
|
itemtxs=invoice_itemtxs,
|
|
commit=True,
|
|
operation=InvoiceModel.ITEMIZE_APPEND,
|
|
)
|
|
sale_order = estimate.sale_orders.first()
|
|
sale_order.invoice = invoice
|
|
invoice.bind_estimate(estimate)
|
|
invoice.mark_as_review()
|
|
estimate.mark_as_completed()
|
|
sale_order.save()
|
|
estimate.save()
|
|
invoice.save()
|
|
messages.success(request, "Invoice created successfully")
|
|
return redirect(
|
|
"invoice_detail",
|
|
dealer_slug=dealer.slug,
|
|
entity_slug=entity.slug,
|
|
pk=invoice.pk,
|
|
)
|
|
else:
|
|
print(form.errors)
|
|
form = forms.InvoiceModelCreateForm(
|
|
entity_slug=entity.slug, user_model=entity.admin
|
|
)
|
|
form.initial.update(
|
|
{
|
|
"customer": estimate.customer,
|
|
"cash_account": dealer.settings.invoice_cash_account,
|
|
"prepaid_account": dealer.settings.invoice_prepaid_account,
|
|
"unearned_account": dealer.settings.invoice_unearned_account,
|
|
}
|
|
)
|
|
|
|
context = {
|
|
"form": form,
|
|
"estimate": estimate,
|
|
}
|
|
return render(request, "sales/invoices/invoice_create.html", context)
|
|
|
|
|
|
class InvoicePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Represents a detailed view for previewing an invoice.
|
|
|
|
This class provides a mechanism to render a preview of an invoice for authorized
|
|
users. It utilizes Django's class-based views by extending `DetailView` and includes
|
|
necessary mixins for login and permission checks. The purpose of this class is to ensure
|
|
secured access to the invoice preview while generating additional context data needed
|
|
for rendering finance-related information.
|
|
|
|
:ivar model: The Django model class this view will represent.
|
|
:type model: InvoiceModel
|
|
:ivar context_object_name: Name of the context object used in the template.
|
|
:type context_object_name: str
|
|
:ivar template_name: Path to the template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = InvoiceModel
|
|
context_object_name = "invoice"
|
|
permission_required = ["django_ledger.view_invoicemodel"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
invoice = kwargs.get("object")
|
|
if invoice.get_itemtxs_data():
|
|
# calculator = CarFinanceCalculator(invoice)
|
|
finance_data = get_finance_data(invoice, dealer)
|
|
kwargs["data"] = finance_data
|
|
kwargs["dealer_info"] = dealer
|
|
kwargs["customer_obj"] = invoice.customer.customer_set.first()
|
|
return super().get_context_data(**kwargs)
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
context = self.get_context_data(object=self.object)
|
|
|
|
# lang = request.GET.get('lang', 'ar')
|
|
|
|
if request.GET.get("lang") == "en":
|
|
template_path = "sales/invoices/invoice_preview_en.html"
|
|
elif request.GET.get("lang") == "ar":
|
|
template_path = "sales/invoices/invoice_preview_ar.html"
|
|
else:
|
|
# just for preview not for download
|
|
return render(request, "sales/invoices/invoice_preview.html", context)
|
|
|
|
html_string = render_to_string(template_path, context)
|
|
|
|
base_url = request.build_absolute_uri("/")
|
|
pdf_file = HTML(string=html_string, base_url=base_url).write_pdf()
|
|
|
|
response = HttpResponse(pdf_file, content_type="application/pdf")
|
|
response["Content-Disposition"] = (
|
|
f'attachment; filename="invoice_{self.object.invoice_number}.pdf"'
|
|
)
|
|
|
|
return response
|
|
|
|
|
|
# payments
|
|
|
|
|
|
class InvoiceModelUpdateView(InvoiceModelUpdateViewBase):
|
|
template_name = "sales/invoices/invoice_update.html"
|
|
permission_required = ["django_ledger.change_invoicemodel"]
|
|
|
|
|
|
# def PaymentCreateView(request,dealer_slug,entity_slug,invoice_pk):
|
|
# from django_ledger.forms.invoice import AccruedAndApprovedInvoiceModelUpdateForm
|
|
# invoice = get_object_or_404(InvoiceModel,pk=invoice_pk)
|
|
|
|
# form = AccruedAndApprovedInvoiceModelUpdateForm(entity_slug=entity_slug,user_model=request.dealer.user)
|
|
# if request.method == "POST":
|
|
# if form.is_valid():
|
|
# invoice_model: InvoiceModel = form.save(commit=False)
|
|
# if invoice_model.can_migrate():
|
|
# invoice_model.migrate_state(
|
|
# user_model=request.dealer.user,
|
|
# entity_slug=entity_slug
|
|
# )
|
|
# invoice_model.save()
|
|
# messages.success(request, "Invoice updated successfully")
|
|
# return redirect("invoice_detail", dealer_slug=dealer_slug,entity_slug=entity_slug, invoice_pk=invoice_model.pk)
|
|
# else:
|
|
# print(form.errors)
|
|
|
|
# context = { "invoice": invoice, "form": form }
|
|
# return render(request, "sales/payments/payment_form1.html", context)
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.add_payment", raise_exception=True)
|
|
def PaymentCreateView(request, dealer_slug, pk):
|
|
"""
|
|
Handles the creation of a payment entry associated with an invoice or bill. Validates
|
|
the payment data against the model's current state and reflects the changes in
|
|
invoice or bill records. Provides appropriate error messages for invalid conditions
|
|
such as exceeding payable amounts or attempting payment for already fully paid models.
|
|
|
|
If successfully processed, the payment details are saved, and the model is updated
|
|
accordingly. This view regulates payment for dealer-associated entities while
|
|
ensuring the model consistency.
|
|
|
|
The view renders a form to submit payment details, and pre-populates the form fields
|
|
with default data for the associated model if necessary.
|
|
|
|
:param request: The HTTP request object containing user request data and session
|
|
information. This is required to handle the request and apply the appropriate
|
|
processing rules.
|
|
:param pk: The primary key of the invoice or bill being processed. It is used to
|
|
load the appropriate model instance for payment processing.
|
|
:return: An HTTP response object. Depending on the circumstances, the response may
|
|
redirect to the detail view of the processed invoice or bill, re-render the
|
|
payment form with error messages or indicate success in payment creation.
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
invoice = InvoiceModel.objects.filter(pk=pk).first()
|
|
# bill = BillModel.objects.filter(pk=pk).first()
|
|
model = invoice
|
|
|
|
entity = dealer.entity
|
|
form = forms.PaymentForm()
|
|
|
|
if request.method == "POST":
|
|
form = forms.PaymentForm(request.POST)
|
|
|
|
user_id = request.user.id if request.user.is_authenticated else "Anonymous"
|
|
user_username = (
|
|
request.user.username if request.user.is_authenticated else "anonymous"
|
|
)
|
|
|
|
if form.is_valid():
|
|
amount = form.cleaned_data.get("amount")
|
|
invoice = form.cleaned_data.get("invoice")
|
|
# bill = form.cleaned_data.get("bill")
|
|
payment_method = form.cleaned_data.get("payment_method")
|
|
response = redirect(
|
|
"invoice_detail",
|
|
dealer_slug=dealer.slug,
|
|
entity_slug=entity.slug,
|
|
pk=model.pk,
|
|
) # if invoice else "bill_detail"
|
|
# model = invoice if invoice else bill
|
|
|
|
if not model.is_approved():
|
|
model.mark_as_approved(user_model=entity.admin)
|
|
if amount < invoice.amount_due:
|
|
messages.error(request, _("Amount cannot be less than due amount"))
|
|
return response
|
|
if model.amount_paid == model.amount_due:
|
|
messages.error(request, _("fully paid"))
|
|
return response
|
|
if model.amount_paid + amount > model.amount_due:
|
|
messages.error(request, _("Amount exceeds due amount"))
|
|
return response
|
|
|
|
try:
|
|
if invoice:
|
|
set_invoice_payment(dealer, entity, invoice, amount, payment_method)
|
|
logger.info(
|
|
f"User {user_username} (ID: {user_id}) successfully processed payment for Invoice ID: {invoice.pk} (Dealer: {dealer.slug}) Amount: {amount}, Method: {payment_method}."
|
|
)
|
|
# elif bill:
|
|
# set_bill_payment(dealer, entity, bill, amount, payment_method)
|
|
# logger.info(
|
|
# f"User {user_username} (ID: {user_id}) successfully processed payment for Bill ID: {bill.pk} (Dealer: {dealer.slug}) Amount: {amount}, Method: {payment_method}."
|
|
# )
|
|
|
|
messages.success(request, _("Payment created successfully"))
|
|
return response
|
|
except Exception as e:
|
|
logger.error(
|
|
f"User {user_username} (ID: {user_id}) encountered error creating payment "
|
|
f"for (Dealer: {dealer.slug}). "
|
|
f"Attempted Amount: {amount}, Method: {payment_method}. Error: {e}",
|
|
exc_info=True,
|
|
)
|
|
messages.error(request, f"Error creating payment: {str(e)}")
|
|
else:
|
|
messages.error(request, f"Invalid form data: {str(form.errors)}")
|
|
form = forms.PaymentForm()
|
|
if model:
|
|
form.initial["amount"] = model.amount_due - model.amount_paid
|
|
if isinstance(model, InvoiceModel):
|
|
form.initial["invoice"] = model
|
|
form.fields["bill"].widget = HiddenInput()
|
|
elif isinstance(model, BillModel):
|
|
form.initial["bill"] = model
|
|
form.fields["invoice"].widget = HiddenInput()
|
|
return render(
|
|
request, "sales/payments/payment_form1.html", {"model": model, "form": form}
|
|
)
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.view_payment", raise_exception=True)
|
|
def PaymentListView(request, dealer_slug):
|
|
"""
|
|
Handles the view for listing payment information associated with the journals of a specific
|
|
entity. This view is protected to ensure only authenticated and authorized users can
|
|
access it.
|
|
|
|
The function retrieves the related dealer object based on the current user session, extracts
|
|
the associated entity, and fetches all journal entries linked to the entity. This data is
|
|
then passed into the template for rendering.
|
|
|
|
:param request: The HTTP request object containing user context.
|
|
:type request: HttpRequest
|
|
|
|
:return: The rendered HTML response displaying the list of payments.
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
entity = dealer.entity
|
|
|
|
journals = JournalEntryModel.objects.filter(ledger__entity=entity).all()
|
|
|
|
paginator = Paginator(journals, 30)
|
|
page_number = request.GET.get("page")
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
return render(request, "sales/payments/payment_list.html", {"page_obj": page_obj})
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.view_payment", raise_exception=True)
|
|
def PaymentDetailView(request, dealer_slug, pk):
|
|
"""
|
|
This function handles the detail view for a payment by fetching a journal entry
|
|
and its associated transactions. It ensures that the request is authenticated
|
|
and the user has permission to view the journal entry model.
|
|
|
|
:param request: The HTTP request object.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the journal entry for which details are to be fetched.
|
|
:type pk: int
|
|
:return: An HTTP response rendering the payment details template with the journal
|
|
entry and its associated transactions.
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
journal = JournalEntryModel.objects.filter(pk=pk).first()
|
|
transactions = (
|
|
TransactionModel.objects.filter(journal_entry=journal)
|
|
.order_by("account__code")
|
|
.all()
|
|
)
|
|
return render(
|
|
request,
|
|
"sales/payments/payment_details.html",
|
|
{"journal": journal, "transactions": transactions},
|
|
)
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.change_payment", raise_exception=True)
|
|
def payment_mark_as_paid(request, dealer_slug, pk):
|
|
"""
|
|
Marks an invoice as paid if it meets the conditions of being fully paid and eligible
|
|
for payment. Also ensures that related ledger journal entries are locked and posted
|
|
when the payment is marked successfully.
|
|
|
|
This function is protected with both `login_required` and
|
|
`permission_required` decorators, ensuring that only logged-in users with
|
|
appropriate permissions can execute it.
|
|
|
|
:param request: HttpRequest object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: Primary key of the invoice to mark as paid.
|
|
:type pk: int
|
|
:return: Redirect response to the invoice detail page.
|
|
:rtype: HttpResponseRedirect
|
|
:raises: In case of an exception during the process, an error message is
|
|
displayed to the user through Django's messaging framework.
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
invoice = get_object_or_404(InvoiceModel, pk=pk)
|
|
if request.method == "POST":
|
|
# Get user info for logging
|
|
user_id = request.user.id if request.user.is_authenticated else "Anonymous"
|
|
user_username = (
|
|
request.user.username if request.user.is_authenticated else "anonymous"
|
|
)
|
|
|
|
try:
|
|
if invoice.amount_due == invoice.amount_paid:
|
|
if not invoice.is_paid() and invoice.can_pay():
|
|
invoice.mark_as_paid(
|
|
entity_slug=invoice.ledger.entity.slug,
|
|
user_model=invoice.ledger.entity.admin,
|
|
)
|
|
invoice.save()
|
|
|
|
invoice.ledger.lock_journal_entries()
|
|
invoice.ledger.post_journal_entries()
|
|
|
|
# invoice.ledger.post()
|
|
invoice.ledger.save()
|
|
# --- Log successful operation ---
|
|
logger.info(
|
|
f"User {user_username} (ID: {user_id}) successfully marked Invoice ID: {invoice.pk} "
|
|
f"as paid and processed ledger entries for Dealer: {dealer_slug}."
|
|
)
|
|
messages.success(request, _("Payment created successfully"))
|
|
else:
|
|
logger.warning(
|
|
f"User {user_username} (ID: {user_id}) attempted to mark Invoice ID: {invoice.pk} "
|
|
f"as paid, but it is not fully paid (Due: {invoice.amount_due}, Paid: {invoice.amount_paid}). "
|
|
f"Operation halted for Dealer: {dealer_slug}."
|
|
)
|
|
messages.error(
|
|
request,
|
|
_("Invoice is not fully paid, Payment cannot be marked as paid"),
|
|
)
|
|
except Exception as e:
|
|
logger.error(
|
|
f"User {user_username} (ID: {user_id}) encountered an error while marking Invoice ID: {invoice.pk} "
|
|
f"as paid or processing ledger entries for Dealer: {dealer_slug}. Error: {e}",
|
|
exc_info=True,
|
|
)
|
|
messages.error(request, f"Error: {str(e)}")
|
|
return redirect(
|
|
"invoice_detail",
|
|
dealer_slug=dealer_slug,
|
|
entity_slug=request.entity.slug,
|
|
pk=invoice.pk,
|
|
)
|
|
|
|
|
|
# activity log
|
|
class UserActivityLogListView(LoginRequiredMixin, ListView):
|
|
"""
|
|
Represents a view to display a paginated list of user activity logs.
|
|
|
|
This class is used to display a list of user activity logs in a paginated
|
|
manner. It retrieves the logs from the `UserActivityLog` model and allows
|
|
basic filtering of logs by user email through the URL query parameters.
|
|
The view requires the user to be authenticated and utilizes the
|
|
`LoginRequiredMixin` to enforce this.
|
|
|
|
:ivar model: The model associated with the view.
|
|
:type model: models.UserActivityLog
|
|
:ivar template_name: The template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable representing
|
|
the list of logs.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of logs displayed per page.
|
|
:type paginate_by: int
|
|
"""
|
|
|
|
model = models.UserActivityLog
|
|
template_name = "dealers/activity_log.html"
|
|
context_object_name = "logs"
|
|
paginate_by = 20
|
|
|
|
def get_queryset(self):
|
|
queryset = super().get_queryset()
|
|
if "user" in self.request.GET:
|
|
queryset = queryset.filter(user__email=self.request.GET["user"])
|
|
return queryset[:100] # will update later with better pagination
|
|
|
|
|
|
def lead_view(request):
|
|
return render(request, "crm/leads/lead_view.html")
|
|
|
|
|
|
# CRM RELATED VIEWS
|
|
class LeadListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Represents a view to display a paginated list of leads, ensuring user permissions
|
|
and type-specific filtering.
|
|
|
|
This class is used for rendering a list of leads associated with a logged-in user
|
|
based on their type (dealer or staff member). It combines login and permission
|
|
controls with pagination to ensure proper access and management of leads
|
|
in a CRM context.
|
|
|
|
:ivar model: The model to be used for retrieving leads.
|
|
:type model: type[Lead]
|
|
:ivar template_name: Path to the template for rendering the lead list.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable to contain the leads.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: Number of items to display per page.
|
|
:type paginate_by: int
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
model = models.Lead
|
|
template_name = "crm/leads/lead_list.html"
|
|
context_object_name = "leads"
|
|
paginate_by = 30
|
|
permission_required = ["inventory.view_lead"]
|
|
|
|
def get_queryset(self):
|
|
# print(self.request.is_dealer)
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
query = self.request.GET.get("q")
|
|
qs = models.Lead.objects.filter(dealer=dealer).exclude(status="converted")
|
|
if query:
|
|
qs = qs.filter(
|
|
Q(first_name__icontains=query)
|
|
| Q(last_name__icontains=query)
|
|
| Q(id_car_make__name__icontains=query)
|
|
| Q(id_car_model__name__icontains=query)
|
|
| Q(phone_number__icontains=query)
|
|
| Q(next_action__icontains=query)
|
|
| Q(staff__first_name__icontains=query)
|
|
| Q(staff__last_name__icontains=query)
|
|
)
|
|
|
|
if self.request.is_dealer: # or self.request.is_manager:
|
|
return qs
|
|
if self.request.is_staff:
|
|
return qs.filter(staff=self.request.staff)
|
|
return models.Lead.objects.none()
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["empty_state_value"] = _("lead")
|
|
return context
|
|
|
|
|
|
class LeadDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
View that provides detailed information about a specific lead.
|
|
|
|
This class-based view is designed for displaying detailed information associated with a
|
|
particular lead, including related notes, emails, activities, status history, and a lead
|
|
transfer form. It combines multiple mixins to enforce login and permission requirements
|
|
for accessing the data. This view is tailored to the CRM module for managing leads.
|
|
|
|
:ivar model: The model associated with this view, which is the Lead model.
|
|
:type model: models.Lead
|
|
:ivar template_name: Path to the template used to render detailed lead information.
|
|
:type template_name: str
|
|
:ivar permission_required: List of permissions required to access this view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = models.Lead
|
|
template_name = "crm/leads/lead_detail.html"
|
|
permission_required = ["inventory.view_lead"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
context = super().get_context_data(**kwargs)
|
|
context["notes"] = models.Notes.objects.filter(
|
|
content_type__model="lead", object_id=self.object.id
|
|
)
|
|
email_qs = models.Email.objects.filter(
|
|
content_type__model="lead", object_id=self.object.id
|
|
)
|
|
context["emails"] = {
|
|
"sent": email_qs.filter(status="SENT"),
|
|
"draft": email_qs.filter(status="DRAFT"),
|
|
}
|
|
context["activities"] = models.Activity.objects.filter(
|
|
content_type__model="lead", object_id=self.object.id
|
|
)
|
|
context["tasks"] = models.Tasks.objects.filter(
|
|
content_type__model="lead", object_id=self.object.id
|
|
)
|
|
context["schedules"] = models.Schedule.objects.filter(
|
|
dealer=dealer,
|
|
content_type__model="lead",
|
|
object_id=self.object.id,
|
|
scheduled_by=self.request.user,
|
|
).order_by("-created_at")
|
|
|
|
context["status_history"] = models.LeadStatusHistory.objects.filter(
|
|
lead=self.object
|
|
)
|
|
context["transfer_form"] = forms.LeadTransferForm()
|
|
context["transfer_form"].fields["transfer_to"].queryset = (
|
|
models.Staff.objects.select_related("user")
|
|
.filter(
|
|
dealer=dealer,
|
|
user__groups__permissions__codename__contains="can_reassign_lead",
|
|
)
|
|
.exclude(user=self.request.user)
|
|
.distinct()
|
|
)
|
|
|
|
context["activity_form"] = forms.ActivityForm()
|
|
context["staff_task_form"] = forms.StaffTaskForm()
|
|
context["note_form"] = forms.NoteForm()
|
|
context["schedule_form"] = forms.ScheduleForm()
|
|
return context
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.add_lead", raise_exception=True)
|
|
def lead_create(request, dealer_slug):
|
|
"""
|
|
Handles the creation of a new lead in the inventory system.
|
|
|
|
This function manages the rendering and processing of a form for creating a new
|
|
lead. It filters options for car models in the form based on the selected car
|
|
make. For POST requests, it validates and processes the submitted form data
|
|
and creates the lead if valid. It also creates a corresponding customer in the
|
|
ledger system if one does not exist for the provided email.
|
|
|
|
:param request: The HTTP request object containing request data.
|
|
:type request: HttpRequest
|
|
|
|
:return: An HTTP response object rendering the lead creation form or redirecting to the lead list page upon success.
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
|
|
if request.method == "POST":
|
|
# get username for logging
|
|
user_username = (
|
|
request.user.username if request.user.is_authenticated else "anonymous"
|
|
)
|
|
|
|
# Log intent before trying to mark as paid
|
|
|
|
form = forms.LeadForm(request.POST)
|
|
|
|
# Filter car models based on the selected make (for POST requests)
|
|
if "id_car_make" in request.POST:
|
|
form.fields["id_car_model"].queryset = models.CarModel.objects.filter(
|
|
id_car_make=int(request.POST["id_car_make"])
|
|
)
|
|
try:
|
|
if form.is_valid():
|
|
instance = form.save(commit=False)
|
|
instance.dealer = dealer
|
|
instance.staff = form.cleaned_data.get("staff")
|
|
|
|
if instance.lead_type == "customer":
|
|
customer = models.Customer.objects.filter(
|
|
dealer=dealer, email=instance.email
|
|
).first()
|
|
if not customer:
|
|
customer = models.Customer(
|
|
dealer=dealer,
|
|
address=instance.address,
|
|
phone_number=instance.phone_number,
|
|
email=instance.email,
|
|
first_name=instance.first_name,
|
|
last_name=instance.last_name,
|
|
active=False,
|
|
)
|
|
# customer.create_user_model(for_lead=True)
|
|
customer.create_customer_model(for_lead=True)
|
|
customer.save()
|
|
instance.customer = customer
|
|
|
|
if instance.lead_type == "organization":
|
|
organization = models.Organization.objects.filter(
|
|
dealer=dealer, email=instance.email
|
|
).first()
|
|
if not organization:
|
|
organization = models.Organization(
|
|
dealer=dealer,
|
|
address=instance.address,
|
|
phone_number=instance.phone_number,
|
|
email=instance.email,
|
|
name=instance.first_name + " " + instance.last_name,
|
|
active=False,
|
|
)
|
|
# organization.create_user_model(for_lead=True)
|
|
organization.create_customer_model(for_lead=True)
|
|
organization.save()
|
|
instance.organization = organization
|
|
instance.next_action = LeadStatus.NEW
|
|
instance.save()
|
|
logger.info(
|
|
f"lead created successfully for dealer {dealer_slug} by user:{user_username}"
|
|
)
|
|
messages.success(request, _("Lead created successfully"))
|
|
return redirect(
|
|
"lead_detail", dealer_slug=dealer_slug, slug=instance.slug
|
|
)
|
|
else:
|
|
logger.error(
|
|
f"error creating leading for dealer {dealer_slug} by user:{user_username}"
|
|
)
|
|
messages.error(
|
|
request, f"Lead was not created ... : {str(form.errors)}"
|
|
)
|
|
return render(request, "crm/leads/lead_form.html", {"form": form})
|
|
|
|
except Exception as e:
|
|
messages.error(request, f"Lead was not created ... : {str(e)}")
|
|
|
|
form = forms.LeadForm()
|
|
form.fields["staff"].queryset = form.fields["staff"].queryset.filter(active=True)
|
|
form.filter_qs(dealer=dealer)
|
|
|
|
if make := request.GET.get("id_car_make", None):
|
|
qs = models.CarModel.objects.filter(id_car_make=int(make))
|
|
form.fields["id_car_model"].queryset = qs
|
|
form.fields["id_car_model"].choices = [
|
|
(obj.id_car_model, obj.get_local_name()) for obj in qs
|
|
]
|
|
|
|
else:
|
|
dealer_make_list = models.DealersMake.objects.filter(dealer=dealer).values_list(
|
|
"car_make", flat=True
|
|
)
|
|
qs = form.fields["id_car_make"].queryset.filter(
|
|
is_sa_import=True, pk__in=dealer_make_list
|
|
)
|
|
# print(qs)
|
|
form.fields["staff"].queryset = (
|
|
form.fields["staff"]
|
|
.queryset.select_related("user")
|
|
.filter(
|
|
dealer=dealer,
|
|
user__groups__permissions__codename__contains="add_lead",
|
|
)
|
|
.distinct()
|
|
)
|
|
|
|
if request.is_staff:
|
|
form.initial["staff"] = request.staff
|
|
form.fields["staff"].widget.attrs.update(
|
|
{"readonly": "true", "required": "true"}
|
|
)
|
|
form.fields["staff"].queryset = models.Staff.objects.filter(
|
|
dealer=dealer, pk=request.staff.pk
|
|
)
|
|
qs = qs.order_by("name")
|
|
form.fields["id_car_make"].queryset = qs
|
|
form.fields["id_car_make"].choices = [
|
|
(obj.id_car_make, obj.get_local_name()) for obj in qs
|
|
]
|
|
|
|
if first_make := qs.first():
|
|
qs = first_make.carmodel_set.all()
|
|
form.fields["id_car_model"].queryset = qs
|
|
form.fields["id_car_model"].choices = [
|
|
(obj.id_car_model, obj.get_local_name()) for obj in qs
|
|
]
|
|
|
|
return render(request, "crm/leads/lead_form.html", {"form": form})
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.view_lead", raise_exception=True)
|
|
def lead_tracking(request, dealer_slug):
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
staff = (
|
|
models.Staff.objects.select_related("user")
|
|
.filter(dealer=dealer, user=request.user)
|
|
.first()
|
|
)
|
|
|
|
if staff:
|
|
qs = models.Lead.objects.filter(dealer=dealer, staff=staff)
|
|
else:
|
|
qs = models.Lead.objects.filter(dealer=dealer)
|
|
leads = qs
|
|
won = qs.filter(status="won")
|
|
new = qs.filter(status="new")
|
|
lose = qs.filter(status="lose")
|
|
negotiation = qs.filter(status="negotiation")
|
|
follow_up = qs.filter(next_action__in=["call", "meeting"])
|
|
|
|
context = {
|
|
"new": new,
|
|
"follow_up": follow_up,
|
|
"won": won,
|
|
"lose": lose,
|
|
"negotiation": negotiation,
|
|
"leads": leads,
|
|
"empty_state_value": _("lead"),
|
|
}
|
|
return render(request, "crm/leads/lead_tracking.html", context)
|
|
|
|
|
|
# @require_POST
|
|
@login_required
|
|
@permission_required("inventory.change_lead", raise_exception=True)
|
|
def update_lead_actions(request, dealer_slug):
|
|
# get the user info for logging
|
|
|
|
user_username = (
|
|
request.user.username if request.user.is_authenticated else "anonymous"
|
|
)
|
|
logger.debug(
|
|
f"User {user_username} is attempting to update lead actions "
|
|
f"for dealer '{dealer_slug}'. Request POST data: {request.POST.dict()}"
|
|
)
|
|
try:
|
|
lead_id = request.POST.get("lead_id")
|
|
current_action = request.POST.get("current_action")
|
|
next_action = request.POST.get("next_action")
|
|
next_action_date = request.POST.get("next_action_date", None)
|
|
lead = models.Lead.objects.get(id=lead_id)
|
|
|
|
if not all([lead_id, current_action, next_action]):
|
|
# Log for missing required fields
|
|
logger.warning(
|
|
f"User {user_username} submitted incomplete data to update lead actions "
|
|
f"for dealer '{dealer_slug}'. Missing fields: lead_id='{lead_id}', current_action='{current_action}', next_action='{next_action}'."
|
|
)
|
|
messages.error(request, _("All fields are required"))
|
|
return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug)
|
|
# return JsonResponse(
|
|
# {"success": False, "message": "All fields are required"}, status=400
|
|
# )
|
|
|
|
# Get the lead
|
|
|
|
# Update lead fields
|
|
|
|
lead.status = current_action
|
|
lead.next_action = next_action
|
|
|
|
# Log before updating lead fields
|
|
logger.debug(
|
|
f"User {user_username} found Lead ID: {lead.pk} ('{lead.slug}') "
|
|
f"for update. Current action: '{current_action}', Next action: '{next_action}', Next action date: '{next_action_date}'."
|
|
)
|
|
|
|
# Parse the datetime string
|
|
try:
|
|
if next_action_date:
|
|
lead.next_action_date = next_action_date
|
|
next_action_datetime = datetime.strptime(
|
|
next_action_date, "%Y-%m-%dT%H:%M"
|
|
)
|
|
lead.next_action_date = timezone.make_aware(next_action_datetime)
|
|
logger.debug(
|
|
f"Lead ID: {lead.pk} next_action_date parsed to {lead.next_action_date}."
|
|
)
|
|
except ValueError as ve:
|
|
# Log for invalid date format
|
|
logger.warning(
|
|
f"submitted invalid date format ('{next_action_date}') "
|
|
f"for Lead ID: {lead.pk}. Error: {ve}"
|
|
)
|
|
messages.error(request, _("Invalid date format"))
|
|
return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug)
|
|
# return JsonResponse(
|
|
# {"success": False, "message": "Invalid date format"}, status=400
|
|
# )
|
|
# Save the lead
|
|
lead.save()
|
|
# --- Logging for successful update (main try block success) ---
|
|
logger.info(
|
|
f"User {user_username} successfully updated Lead ID: {lead.pk} ('{lead.slug}'). "
|
|
f"New Status: '{lead.status}', Next Action: '{lead.next_action}', Next Action Date: '{lead.next_action_date}'."
|
|
)
|
|
messages.success(request, _("Actions updated successfully"))
|
|
return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug)
|
|
# return JsonResponse(
|
|
# {"success": True, "message": "Actions updated successfully"}
|
|
# )
|
|
|
|
except models.Lead.DoesNotExist:
|
|
# --- Logging for Lead not found ---
|
|
logger.warning(
|
|
f"User {user_username} attempted to update non-existent Lead with ID: '{lead_id}' "
|
|
f"for dealer '{dealer_slug}'. Returning 404."
|
|
)
|
|
messages.error(request, _("Lead not found"))
|
|
return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug)
|
|
# return JsonResponse({"success": False, "message": "Lead not found"}, status=404)
|
|
except Exception as e:
|
|
involved_lead_id = request.POST.get("lead_id", "N/A")
|
|
logger.error(
|
|
f"User {user_username} encountered an unexpected error while updating Lead ID: '{involved_lead_id}' "
|
|
f"for dealer '{dealer_slug}'. Error: {e}",
|
|
exc_info=True, # CRUCIAL: Includes the full traceback
|
|
)
|
|
messages.error(request, _("An error occurred while updating lead actions"))
|
|
return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug)
|
|
# return JsonResponse({"success": False, "message": str(e)}, status=500)
|
|
|
|
|
|
def lead_update(request, dealer_slug, slug):
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
lead = get_object_or_404(models.Lead, slug=slug)
|
|
form = forms.LeadForm(instance=lead)
|
|
if "HX-Request" in request.headers:
|
|
make_id = request.GET.get("id_car_make")
|
|
make = models.CarMake.objects.get(pk=make_id)
|
|
form.fields["id_car_model"].queryset = make.carmodel_set.all()
|
|
else:
|
|
form.fields[
|
|
"id_car_model"
|
|
].queryset = form.instance.id_car_make.carmodel_set.all()
|
|
form.fields["staff"].queryset = (
|
|
form.fields["staff"]
|
|
.queryset.select_related("user")
|
|
.filter(
|
|
dealer=dealer,
|
|
user__groups__permissions__codename__contains="add_lead",
|
|
)
|
|
.distinct()
|
|
)
|
|
context = {"form": form}
|
|
return render(request, "crm/leads/lead_form.html", context)
|
|
|
|
|
|
class LeadUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
|
"""
|
|
Handles the update view for Lead objects.
|
|
|
|
This class is used to manage the process of updating Lead objects within the
|
|
application. It provides integration with Django's authentication and
|
|
permissions systems, ensuring that only authorized users can access this view.
|
|
Additionally, it customizes the behavior of the form to dynamically filter
|
|
available options for the 'id_car_model' field based on the related
|
|
'id_car_make' field.
|
|
|
|
:ivar model: Model class representing a Lead object.
|
|
:type model: models.Lead
|
|
:ivar form_class: Form class used for the Lead update form.
|
|
:type form_class: forms.LeadForm
|
|
:ivar template_name: Path to the template used for rendering the Lead update
|
|
form.
|
|
:type template_name: str
|
|
:ivar success_url: URL to redirect to upon successful Lead update.
|
|
:type success_url: str
|
|
:ivar permission_required: List of permissions required for accessing this view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = models.Lead
|
|
form_class = forms.LeadForm
|
|
template_name = "crm/leads/lead_form.html"
|
|
success_url = reverse_lazy("lead_list")
|
|
permission_required = ["inventory.change_lead"]
|
|
|
|
def get_form(self, form_class=None):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug"))
|
|
form = super().get_form(form_class)
|
|
form.fields[
|
|
"id_car_model"
|
|
].queryset = form.instance.id_car_make.carmodel_set.all()
|
|
form.fields["staff"].queryset = (
|
|
form.fields["staff"]
|
|
.queryset.select_related("user")
|
|
.filter(
|
|
dealer=dealer,
|
|
user__groups__permissions__codename__contains="add_lead",
|
|
)
|
|
.distinct()
|
|
)
|
|
|
|
return form
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"lead_list", kwargs={"dealer_slug": self.kwargs.get("dealer_slug")}
|
|
)
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.delete_lead", raise_exception=True)
|
|
def LeadDeleteView(request, dealer_slug, slug):
|
|
"""
|
|
Handles the deletion of a Lead along with its associated customer and potentially
|
|
a related user account in the system. Ensures proper permissions and login
|
|
are required before the operation is performed. Provides a success message
|
|
after the deletion is complete.
|
|
|
|
:param request: The HTTP request object specific to the current user.
|
|
:param pk: The primary key identifier of the Lead to be deleted.
|
|
:return: An HTTP redirect response to the lead list page.
|
|
"""
|
|
# get the user info for logging
|
|
user_username = (
|
|
request.user.username if request.user.is_authenticated else "anonymous"
|
|
)
|
|
|
|
lead = get_object_or_404(models.Lead, slug=slug)
|
|
|
|
# Log intent before attempting deletion
|
|
logger.debug(
|
|
f"User {user_username} attempting to delete Lead ID: {lead.pk} ('{lead.slug}') "
|
|
f"and its associated customer/user for dealer '{dealer_slug}'."
|
|
)
|
|
|
|
try:
|
|
User.objects.get(email=lead.customer.email).delete()
|
|
lead.customer.delete()
|
|
# --- Single-line log for successful associated user/customer deletion ---
|
|
logger.info(
|
|
f"User {user_username} successfully deleted associated user and customer "
|
|
f"for Lead ID: {lead.pk} ('{lead.slug}') (Email: {lead.customer.email})."
|
|
)
|
|
except Exception as e:
|
|
# --- Single-line log for error during associated user/customer deletion ---
|
|
logger.error(
|
|
f"User {user_username} encountered an error deleting associated user/customer "
|
|
f"for Lead ID: {lead.slug} ('{lead.slug}') (Email: {getattr(lead.customer, 'email', 'N/A')}). " # Safely get email
|
|
f"Error: {e}",
|
|
exc_info=True,
|
|
)
|
|
print(e)
|
|
lead_id_final = lead.pk # Capture before deletion
|
|
lead_name_final = lead.slug
|
|
lead.delete()
|
|
# Log the final lead deletion, which happens unconditionally after the try-except
|
|
logger.info(
|
|
f"User {user_username} successfully deleted Lead ID: {lead_id_final} ('{lead_name_final}') "
|
|
f"for dealer '{dealer_slug}'."
|
|
)
|
|
messages.success(request, _("Lead deleted successfully"))
|
|
return redirect("lead_list", dealer_slug=dealer_slug)
|
|
|
|
|
|
# @login_required #TODO:delete later
|
|
# @permission_required("inventory.add_notes", raise_exception=True)
|
|
# def add_note_to_lead(request, dealer_slug, slug):
|
|
# """
|
|
# Adds a note to a specific lead. This view is accessible only to authenticated
|
|
# users. The function handles the POST request to create a new note associated
|
|
# with a lead. Upon successful submission, the note is linked to the provided lead,
|
|
# and the user is redirected to the lead's detail page. If the request method is
|
|
# not POST, it initializes an empty form to add a note.
|
|
|
|
# :param request: The HTTP request object
|
|
# :type request: HttpRequest
|
|
# :param pk: The primary key of the lead to which the note will be added
|
|
# :type pk: int
|
|
# :return: HTTP response object. Redirects to the lead detail page on successful
|
|
# note creation or renders the note form template for GET or invalid POST requests.
|
|
# :rtype: HttpResponse
|
|
# """
|
|
# lead = get_object_or_404(models.Lead, slug=slug)
|
|
# if request.method == "POST":
|
|
# form = forms.NoteForm(request.POST)
|
|
# if form.is_valid():
|
|
# note = form.save(commit=False)
|
|
# note.content_object = lead
|
|
# note.created_by = request.user
|
|
# note.save()
|
|
# messages.success(request, _("Note added successfully"))
|
|
# return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug)
|
|
# else:
|
|
# form = forms.NoteForm()
|
|
# return render(request, "crm/note_form.html", {"form": form, "lead": lead})
|
|
|
|
|
|
# @login_required
|
|
# @permission_required("inventory.add_notes", raise_exception=True)
|
|
# def add_note_to_opportunity(request, dealer_slug, slug):
|
|
# """
|
|
# Add a note to a specific opportunity identified by its primary key.
|
|
|
|
# This function handles the addition of a note to an existing opportunity
|
|
# by processing a POST request that includes the note content. If the note
|
|
# content is missing, an error message will be displayed. If the operation
|
|
# is successful, the note is saved, and a success message is returned.
|
|
|
|
# :param request: The HTTP request object representing the client's request.
|
|
# :param pk: The primary key of the Opportunity to which the note is to be added.
|
|
# :type pk: int
|
|
# :return: A redirect response to the detailed view of the opportunity.
|
|
# """
|
|
# opportunity = get_object_or_404(models.Opportunity, slug=slug)
|
|
# dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
# if request.method == "POST":
|
|
# notes = request.POST.get("notes")
|
|
# if not notes:
|
|
# messages.error(request, _("Notes field is required"))
|
|
# else:
|
|
# models.Notes.objects.create(
|
|
# dealer=dealer,
|
|
# content_object=opportunity,
|
|
# created_by=request.user,
|
|
# note=notes,
|
|
# )
|
|
# messages.success(request, _("Note added successfully"))
|
|
# return redirect(
|
|
# "opportunity_detail", dealer_slug=dealer_slug, slug=opportunity.slug
|
|
# )
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.delete_notes", raise_exception=True)
|
|
def delete_note(request, dealer_slug, pk):
|
|
"""
|
|
Deletes a specific note created by the currently logged-in user and redirects
|
|
to the lead detail page. If the note does not exist or the user is not the creator,
|
|
a 404 error will be raised. A success message is displayed upon successful deletion
|
|
of the note.
|
|
|
|
:param request: The HTTP request object associated with the current request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the note to be deleted.
|
|
:type pk: int
|
|
:return: An HTTP redirection to the lead detail page of the corresponding note's lead.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
try:
|
|
note = get_object_or_404(models.Notes, pk=pk, created_by=request.user)
|
|
if isinstance(note.content_object, models.Customer):
|
|
url = "customer_detail"
|
|
slug = note.content_object.slug
|
|
elif isinstance(note.content_object, models.Lead):
|
|
url = "lead_detail"
|
|
slug = note.content_object.slug
|
|
if hasattr(note.content_object, "opportunity"):
|
|
url = "opportunity_detail"
|
|
slug = note.content_object.opportunity.slug
|
|
elif isinstance(note.content_object, models.Opportunity):
|
|
url = "opportunity_detail"
|
|
slug = note.content_object.slug
|
|
|
|
note.delete()
|
|
messages.success(request, _("Note deleted successfully."))
|
|
except Exception as e:
|
|
print("Errroooorrr: ", e)
|
|
print(url)
|
|
print(dealer_slug)
|
|
return redirect(url, dealer_slug=dealer_slug, slug=slug)
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.change_lead", raise_exception=True)
|
|
def lead_convert(request, dealer_slug, slug):
|
|
"""
|
|
Converts a lead into a customer and creates a corresponding opportunity.
|
|
|
|
The function ensures that leads are not converted to customers more than once.
|
|
If the lead has already been converted, an error is displayed. Otherwise,
|
|
the lead is converted into a customer and a new opportunity is created.
|
|
Upon successful conversion, the user is redirected to the lead list view
|
|
and a success message is displayed.
|
|
|
|
:param request: The HTTP request object representing the user's request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the lead to be converted.
|
|
:type pk: int
|
|
:return: An HTTP response redirecting to the lead list view.
|
|
:rtype: HttpResponse
|
|
"""
|
|
lead = get_object_or_404(models.Lead, slug=slug)
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
if hasattr(lead, "opportunity"):
|
|
messages.error(request, _("Lead is already converted to customer"))
|
|
else:
|
|
customer = lead.convert_to_customer()
|
|
models.Opportunity.objects.create(
|
|
dealer=dealer,
|
|
customer=customer,
|
|
lead=lead,
|
|
probability=50,
|
|
stage=models.Stage.NEGOTIATION,
|
|
staff=lead.staff,
|
|
)
|
|
messages.success(request, _("Lead converted to customer successfully"))
|
|
return redirect("lead_list", dealer_slug=dealer_slug)
|
|
|
|
|
|
@login_required
|
|
@require_POST
|
|
@permission_required("inventory.add_schedule", raise_exception=True)
|
|
def schedule_event(request, dealer_slug, content_type, slug):
|
|
"""
|
|
Handles the scheduling of a lead for an appointment.
|
|
|
|
This function ensures that only staff members with the appropriate permissions
|
|
can schedule leads. If the user is not a staff member or does not have the
|
|
required inventory permissions, they are redirected with an appropriate error
|
|
message. The function creates an appointment request and an associated appointment
|
|
record upon successful submission of the scheduling form.
|
|
|
|
:param request: The HTTP request object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the lead to be scheduled.
|
|
:type pk: int
|
|
:return: A rendered template or a redirection to another view based on the request
|
|
method and validity of the form submission.
|
|
:rtype: HttpResponse
|
|
"""
|
|
user_username = (
|
|
request.user.username if request.user.is_authenticated else "anonymous"
|
|
)
|
|
# Log the attempt to retrieve the model dynamically
|
|
logger.debug(
|
|
f"User {user_username} attempting to retrieve model "
|
|
f"for content_type '{content_type}' for dealer '{dealer_slug}'."
|
|
)
|
|
try:
|
|
model = apps.get_model(f"inventory.{content_type}")
|
|
except LookupError:
|
|
# --- Single-line log for LookupError (Model not found) ---
|
|
logger.warning(
|
|
f"User {user_username} requested an invalid model content_type: '{content_type}'. "
|
|
f"Raising Http404 for dealer '{dealer_slug}'."
|
|
)
|
|
raise Http404("Model not found")
|
|
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
obj = get_object_or_404(model, slug=slug)
|
|
|
|
# if not request.is_staff:
|
|
# messages.error(request, _("You do not have permission to schedule."))
|
|
# return redirect(f"{content_type}_detail", dealer_slug=dealer_slug, slug=slug)
|
|
|
|
if request.method == "POST":
|
|
from django_q.models import Schedule as DjangoQSchedule
|
|
|
|
form = forms.ScheduleForm(request.POST)
|
|
if form.is_valid():
|
|
reminder = form.cleaned_data["reminder"]
|
|
instance = form.save(commit=False)
|
|
instance.dealer = dealer
|
|
instance.content_object = obj
|
|
instance.scheduled_by = request.user
|
|
|
|
if obj.customer:
|
|
instance.customer = obj.customer.customer_model
|
|
elif obj.organization:
|
|
instance.cutsomer = obj.organization.customer_model
|
|
|
|
# service = Service.objects.get(name=instance.scheduled_type)
|
|
# # Log attempt to create AppointmentRequest
|
|
# logger.debug(
|
|
# f"User {user_username} attempting to create AppointmentRequest "
|
|
# f"for {content_type} ID: {obj.pk} ('{obj.slug}'). Service: '{service.name}', "
|
|
# f"Scheduled At: '{instance.scheduled_at}', Duration: '{instance.duration}'."
|
|
# )
|
|
|
|
# try:
|
|
# appointment_request = AppointmentRequest.objects.create(
|
|
# date=instance.scheduled_at.date(),
|
|
|
|
# service=service,
|
|
# staff_member=request.user.staffmember,
|
|
# )
|
|
# except ValidationError as e:
|
|
# messages.error(request, str(e))
|
|
# return redirect(request.META.get("HTTP_REFERER"))
|
|
|
|
# client = get_object_or_404(User, email=instance.customer.email)
|
|
# Create Appointment
|
|
# Appointment.objects.create(
|
|
# client=client,
|
|
# appointment_request=appointment_request,
|
|
# phone=instance.customer.phone,
|
|
# address=instance.customer.address_1,
|
|
# )
|
|
|
|
instance.save()
|
|
models.Activity.objects.create(
|
|
dealer=dealer,
|
|
content_object=obj,
|
|
notes=instance.notes,
|
|
created_by=request.user,
|
|
activity_type=instance.scheduled_type,
|
|
)
|
|
if reminder:
|
|
scheduled_at_aware = (
|
|
timezone.make_aware(
|
|
instance.scheduled_at, timezone.get_current_timezone()
|
|
)
|
|
if timezone.is_naive(instance.scheduled_at)
|
|
else instance.scheduled_at
|
|
)
|
|
|
|
reminder_time = scheduled_at_aware - timezone.timedelta(minutes=15)
|
|
# Only schedule if the reminder time is in the future
|
|
# Reminder emails are scheduled to be sent 15 minutes before the scheduled time
|
|
if reminder_time > timezone.now():
|
|
DjangoQSchedule.objects.create(
|
|
name=f"send_schedule_reminder_email_to_{instance.scheduled_by.email}_for_{content_type}_with_PK_{instance.pk}",
|
|
func="inventory.tasks.send_schedule_reminder_email",
|
|
args=f'"{instance.pk}"',
|
|
schedule_type=DjangoQSchedule.ONCE,
|
|
next_run=reminder_time,
|
|
hook="inventory.tasks.log_email_status",
|
|
)
|
|
messages.success(request, _("Appointment Created Successfully"))
|
|
|
|
return redirect(
|
|
f"{content_type}_detail", dealer_slug=dealer_slug, slug=slug
|
|
)
|
|
|
|
else:
|
|
# Log for invalid form data
|
|
logger.warning(
|
|
f"User {user_username} submitted invalid schedule form data "
|
|
f"for Lead ID: {obj.pk} ('{obj.slug}'). Errors: {form.errors.as_json()}"
|
|
)
|
|
messages.error(request, f"Invalid form data: {str(form.errors)}")
|
|
return redirect(request.META.get("HTTP_REFERER"))
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.change_lead", raise_exception=True)
|
|
def lead_transfer(request, dealer_slug, slug):
|
|
"""
|
|
Handles the transfer of a lead to a different staff member. This view is accessible
|
|
only to authenticated users with the 'inventory.change_lead' permission. If the
|
|
request method is POST and the form data is valid, the lead's staff is updated
|
|
accordingly, saved, and a success message is displayed. Otherwise, an error message
|
|
is shown. In both cases, the user is redirected to the lead listing page.
|
|
|
|
:param request: The HTTP request object.
|
|
:param pk: The primary key of the lead to be transferred.
|
|
:return: An HTTP redirect response to the lead list view.
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
lead = get_object_or_404(models.Lead, slug=slug)
|
|
if request.method == "POST":
|
|
form = forms.LeadTransferForm(request.POST)
|
|
if form.is_valid():
|
|
lead.staff = form.cleaned_data["transfer_to"]
|
|
lead.save()
|
|
models.Notification.objects.create(
|
|
user=lead.staff.user,
|
|
message=f"You have been assigned a new lead: {lead.full_name}.",
|
|
)
|
|
messages.success(request, _("Lead transferred successfully"))
|
|
else:
|
|
messages.error(request, f"Invalid form data: {str(form.errors)}")
|
|
return redirect("lead_detail", dealer_slug=dealer.slug, slug=lead.slug)
|
|
|
|
|
|
@login_required
|
|
def send_lead_email(request, dealer_slug, slug, email_pk=None):
|
|
"""
|
|
Handles sending emails related to a lead. Supports creating drafts, sending emails, and rendering
|
|
an email composition page. Changes on the lead or email objects, such as marking a lead as contacted
|
|
or an email as sent, are recorded in associated activity logs.
|
|
|
|
:param request: The HTTP request object. This contains user information, method type, and data such as
|
|
GET or POST payload used to draft or send the email appropriately.
|
|
Type: HttpRequest
|
|
:param pk: The primary key of the lead to which the email action is associated. It's used to retrieve
|
|
the lead object from the database.
|
|
Type: int
|
|
:param email_pk: Optional parameter representing the primary key of an email template. When provided,
|
|
the respective email content is pre-populated into the email composition form.
|
|
Defaults to None.
|
|
Type: Optional[int]
|
|
:return: When successfully sending an email, redirects the user to the lead list page. On draft mode
|
|
or email composition rendering, a response object is returned to render the respective page.
|
|
Type: HttpResponse
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
lead = get_object_or_404(models.Lead, slug=slug)
|
|
status = request.GET.get("status")
|
|
user_username = (
|
|
request.user.username if request.user.is_authenticated else "anonymous"
|
|
)
|
|
|
|
if status == "draft":
|
|
models.Email.objects.create(
|
|
content_object=lead,
|
|
created_by=request.user,
|
|
from_email="manager@tenhal.com",
|
|
to_email=request.GET.get("to"),
|
|
subject=request.GET.get("subject"),
|
|
message=request.GET.get("message"),
|
|
status=models.EmailStatus.DRAFT,
|
|
)
|
|
models.Activity.objects.create(
|
|
dealer=dealer,
|
|
content_object=lead,
|
|
notes="Email Draft",
|
|
created_by=request.user,
|
|
activity_type=models.ActionChoices.EMAIL,
|
|
)
|
|
messages.success(request, _("Email Draft successfully"))
|
|
|
|
# try:
|
|
# if getattr(lead, "opportunity", None):
|
|
# # Log success when opportunity exists and redirecting
|
|
# logger.info(
|
|
# f"User {user_username} successfully drafted email for Lead ID: {lead.pk} ('{lead.slug}'). "
|
|
# f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail."
|
|
# )
|
|
# # response = HttpResponse(
|
|
# # redirect(
|
|
# # "opportunity_detail",
|
|
# # dealer_slug=dealer_slug,
|
|
# # slug=lead.opportunity.slug,
|
|
# # )
|
|
# # )
|
|
# # response["HX-Redirect"] = reverse(
|
|
# # "opportunity_detail", args=[lead.opportunity.slug]
|
|
# # )
|
|
|
|
# else:
|
|
# # Log success when no opportunity and redirecting to lead detail
|
|
# logger.info(
|
|
# f"User {user_username} successfully drafted email for Lead ID: {lead.pk} ('{lead.slug}'). "
|
|
# f"Lead has no Opportunity, redirecting to lead detail."
|
|
# )
|
|
# # response = HttpResponse()
|
|
# # response["HX-Redirect"] = reverse(
|
|
# # "lead_detail", dealer_slug=dealer_slug, slug=lead.slug
|
|
# # )
|
|
# return response
|
|
# except models.Lead.opportunity.RelatedObjectDoesNotExist:
|
|
# # --- Log when Lead.opportunity does not exist (Draft status) ---
|
|
# logger.info(
|
|
# f"User {user_username} drafted email for Lead ID: {lead.pk} ('{lead.slug}'). "
|
|
# f"Lead's opportunity does not exist. Redirecting to lead list."
|
|
# )
|
|
# return response
|
|
# return redirect("lead_list", dealer_slug=dealer.slug)
|
|
|
|
if request.method == "POST":
|
|
email_pk = request.POST.get("email_pk")
|
|
if email_pk not in [None, "None", ""]:
|
|
email = get_object_or_404(models.Email, pk=int(email_pk))
|
|
email.status = models.EmailStatus.SENT
|
|
email.save()
|
|
else:
|
|
models.Email.objects.create(
|
|
content_object=lead,
|
|
created_by=request.user,
|
|
from_email="manager@tenhal.com",
|
|
to_email=request.POST.get("to"),
|
|
subject=request.POST.get("subject"),
|
|
message=request.POST.get("message"),
|
|
status=models.EmailStatus.SENT,
|
|
)
|
|
send_email(
|
|
"manager@tenhal.com",
|
|
request.POST.get("to"),
|
|
request.POST.get("subject"),
|
|
request.POST.get("message"),
|
|
)
|
|
models.Activity.objects.create(
|
|
dealer=dealer,
|
|
content_object=lead,
|
|
notes="Email sent",
|
|
created_by=request.user,
|
|
activity_type=models.ActionChoices.EMAIL,
|
|
)
|
|
messages.success(request, _("Email sent successfully"))
|
|
response = HttpResponse()
|
|
response["HX-Refresh"] = "true"
|
|
return response
|
|
# try:
|
|
# if lead.opportunity:
|
|
# # Log success when opportunity exists and redirecting after sending email
|
|
# logger.info(
|
|
# f"User {user_username} successfully sent email for Lead ID: {lead.pk} ('{lead.slug}'). "
|
|
# f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail."
|
|
# )
|
|
# return response
|
|
# # return redirect(
|
|
# # "opportunity_detail",
|
|
# # dealer_slug=dealer_slug,
|
|
# # slug=lead.opportunity.slug,
|
|
# # )
|
|
# except models.Lead.opportunity.RelatedObjectDoesNotExist:
|
|
# # --- Log when Lead.opportunity does not exist (POST request for sending) ---
|
|
# logger.info(
|
|
# f"User {user_username} sent email for Lead ID: {lead.pk} ('{lead.slug}'). "
|
|
# f"Lead's opportunity does not exist. Redirecting to lead list."
|
|
# )
|
|
# return response
|
|
# return redirect("lead_list", dealer_slug=dealer_slug)
|
|
msg = f"""
|
|
السلام عليكم {lead.full_name},
|
|
|
|
شكراً لزيارتك لـ {lead.dealer.name}! لقد كان من دواعي سرورنا مساعدتك اليوم.
|
|
|
|
لقد أنشأنا ملفاً شخصياً لك في نظامنا لتتبع تفضيلاتك والسيارات التي تهتم بها. سنتواصل معك قريباً للمتابعة والإجابة على أي أسئلة أخرى قد تكون لديك.
|
|
|
|
في هذه الأثناء، لا تتردد في الاتصال بنا مباشرة أو زيارتنا مرة أخرى في أي وقت يناسبك.
|
|
|
|
نتطلع إلى مساعدتك في العثور على سيارتك القادمة!
|
|
|
|
تحياتي،
|
|
{lead.dealer.arabic_name}
|
|
{lead.dealer.address}
|
|
{lead.dealer.phone_number}
|
|
-----
|
|
Dear {lead.full_name},
|
|
|
|
Thank you for visiting {lead.dealer.name}! It was a pleasure to assist you today.
|
|
|
|
We've created a profile for you in our system to keep track of your preferences and the vehicles you're interested in. We'll be in touch shortly to follow up and answer any further questions you may have.
|
|
|
|
In the meantime, feel free to contact us directly at {lead.dealer.phone_number} or visit us again at your convenience.
|
|
|
|
We look forward to helping you find your next car!
|
|
|
|
Best regards,
|
|
{lead.dealer.name}
|
|
{lead.dealer.address}
|
|
{lead.dealer.phone_number}
|
|
|
|
"""
|
|
subject = ""
|
|
if email_pk:
|
|
email = get_object_or_404(models.Email, pk=email_pk)
|
|
msg = email.message
|
|
subject = email.subject
|
|
return render(
|
|
request,
|
|
"crm/leads/lead_send.html",
|
|
{"lead": lead, "message": msg, "subject": subject, "email_pk": email_pk},
|
|
)
|
|
|
|
|
|
class OpportunityCreateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, CreateView, SuccessMessageMixin
|
|
):
|
|
"""
|
|
Handles the creation of Opportunity instances through a form while enforcing
|
|
specific user access control and initial data population. This view ensures
|
|
an authenticated user can create opportunities tied to their dealer type and
|
|
allows pre-filling data when linked to an existing Lead.
|
|
|
|
Allows authenticated users to create a new Opportunity instance, associating it
|
|
with the requesting user's dealer type and prefilling some fields based on
|
|
existing Lead data, if applicable.
|
|
|
|
:ivar model: The database model associated with this view, which is used to
|
|
represent Opportunity data.
|
|
:type model: models.Opportunity
|
|
:ivar form_class: The form class used for creating new Opportunity instances.
|
|
:type form_class: forms.OpportunityForm
|
|
:ivar template_name: The template used to render the Opportunity creation form.
|
|
:type template_name: str
|
|
"""
|
|
|
|
model = models.Opportunity
|
|
form_class = forms.OpportunityForm
|
|
template_name = "crm/opportunities/opportunity_form.html"
|
|
success_message = _("Opportunity created successfully.")
|
|
permission_required = ["inventory.add_opportunity"]
|
|
|
|
def get_initial(self):
|
|
initial = super().get_initial()
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug"))
|
|
if self.kwargs.get("slug", None):
|
|
lead = models.Lead.objects.get(slug=self.kwargs.get("slug"), dealer=dealer)
|
|
initial["lead"] = lead
|
|
initial["stage"] = models.Stage.QUALIFICATION
|
|
return initial
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug"))
|
|
instance = form.save(commit=False)
|
|
instance.dealer = dealer
|
|
instance.staff = instance.lead.staff
|
|
instance.customer = instance.lead.customer
|
|
instance.save()
|
|
instance.lead.convert_to_customer()
|
|
instance.lead.save()
|
|
return super().form_valid(form)
|
|
|
|
def get_form(self, form_class=None):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug"))
|
|
form = super().get_form(form_class)
|
|
form.fields["car"].queryset = models.Car.objects.filter(
|
|
dealer=dealer, status="available", marked_price__gt=0
|
|
)
|
|
if self.request.is_dealer:
|
|
form.fields["lead"].queryset = models.Lead.objects.filter(dealer=dealer)
|
|
elif self.request.is_staff:
|
|
form.fields["lead"].queryset = models.Lead.objects.filter(
|
|
dealer=dealer, staff=self.request.staff
|
|
)
|
|
return form
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"opportunity_detail",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs.get("dealer_slug"),
|
|
"slug": self.object.slug,
|
|
},
|
|
)
|
|
|
|
|
|
class OpportunityUpdateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
|
):
|
|
"""
|
|
Handles the update functionality for Opportunity objects.
|
|
|
|
This class-based view is responsible for handling the update of existing
|
|
Opportunity instances. It uses a Django form that is specified by the
|
|
`form_class` attribute and renders a template to display and process the
|
|
update form. Access to this view is restricted to authenticated users, as
|
|
it inherits from `LoginRequiredMixin`.
|
|
|
|
It defines the model to be updated and the form template to be used. Upon
|
|
successful update, it redirects the user to the detail page of the updated
|
|
opportunity instance.
|
|
|
|
:ivar model: The model associated with this view. Represents the Opportunity model.
|
|
:type model: django.db.models.Model
|
|
:ivar form_class: The form class used to manage the Opportunity update process.
|
|
:type form_class: django.forms.ModelForm
|
|
:ivar template_name: The path to the template used to render the opportunity
|
|
update form.
|
|
:type template_name: str
|
|
"""
|
|
|
|
model = models.Opportunity
|
|
form_class = forms.OpportunityForm
|
|
template_name = "crm/opportunities/opportunity_form.html"
|
|
success_message = _("Opportunity updated successfully.")
|
|
permission_required = ["inventory.change_opportunity"]
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug"))
|
|
staff = getattr(self.request.user, "staff", None)
|
|
form.fields["car"].queryset = models.Car.objects.filter(
|
|
dealer=dealer, status="available", marked_price__gt=0
|
|
)
|
|
form.fields["lead"].queryset = models.Lead.objects.filter(
|
|
dealer=dealer, staff=staff
|
|
)
|
|
return form
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"opportunity_detail",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs.get("dealer_slug"),
|
|
"slug": self.object.slug,
|
|
},
|
|
)
|
|
|
|
|
|
class OpportunityStageUpdateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
|
):
|
|
"""
|
|
Handles the update functionality for Opportunity objects.
|
|
|
|
This class-based view is responsible for handling the update of existing
|
|
Opportunity instances. It uses a Django form that is specified by the
|
|
`form_class` attribute and renders a template to display and process the
|
|
update form. Access to this view is restricted to authenticated users, as
|
|
it inherits from `LoginRequiredMixin`.
|
|
|
|
It defines the model to be updated and the form template to be used. Upon
|
|
successful update, it redirects the user to the detail page of the updated
|
|
opportunity instance.
|
|
|
|
:ivar model: The model associated with this view. Represents the Opportunity model.
|
|
:type model: django.db.models.Model
|
|
:ivar form_class: The form class used to manage the Opportunity update process.
|
|
:type form_class: django.forms.ModelForm
|
|
:ivar template_name: The path to the template used to render the opportunity
|
|
update form.
|
|
:type template_name: str
|
|
"""
|
|
|
|
model = models.Opportunity
|
|
form_class = forms.OpportunityStageForm
|
|
success_message = _("Opportunity Stage updated successfully.")
|
|
permission_required = ["inventory.change_opportunity"]
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"opportunity_detail",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs.get("dealer_slug"),
|
|
"slug": self.object.slug,
|
|
},
|
|
)
|
|
|
|
|
|
class OpportunityDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
Handles the detailed view of an Opportunity object.
|
|
|
|
Displays detailed information about a specific opportunity, including its
|
|
status, stage, notes, activities, and related emails. This view utilizes
|
|
a form to manage and update the status and stage of the opportunity
|
|
through HTTPX requests. Suitable for use in CRM applications.
|
|
|
|
:ivar model: The model for which this view is being implemented.
|
|
:type model: models.Opportunity
|
|
:ivar template_name: The template used to render this view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The context variable name for the model object in the template.
|
|
:type context_object_name: str
|
|
"""
|
|
|
|
model = models.Opportunity
|
|
template_name = "crm/opportunities/opportunity_detail.html"
|
|
context_object_name = "opportunity"
|
|
permission_required = ["inventory.view_opportunity"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug"))
|
|
context = super().get_context_data(**kwargs)
|
|
form = forms.OpportunityStatusForm()
|
|
url = reverse(
|
|
"opportunity_update_status",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"slug": self.object.slug,
|
|
},
|
|
)
|
|
form.fields["status"].widget.attrs["hx-get"] = url
|
|
form.fields["stage"].widget.attrs["hx-get"] = url
|
|
form.fields["stage"].initial = self.object.stage
|
|
context["status_form"] = form
|
|
context["lead_notes"] = models.Notes.objects.filter(
|
|
content_type__model="lead", object_id=self.object.id
|
|
).order_by("-created")
|
|
context["lead_notes"] = models.Notes.objects.filter(
|
|
content_type__model="lead", object_id=self.object.lead.pk
|
|
).order_by("-created")
|
|
context["notes"] = models.Notes.objects.filter(
|
|
content_type__model="opportunity", object_id=self.object.id
|
|
).order_by("-created")
|
|
context["lead_activities"] = models.Activity.objects.filter(
|
|
content_type__model="lead", object_id=self.object.lead.pk
|
|
)
|
|
context["activities"] = models.Activity.objects.filter(
|
|
content_type__model="opportunity", object_id=self.object.id
|
|
)
|
|
lead_email_qs = models.Email.objects.filter(
|
|
content_type__model="lead", object_id=self.object.lead.pk
|
|
)
|
|
email_qs = models.Email.objects.filter(
|
|
content_type__model="opportunity", object_id=self.object.id
|
|
)
|
|
context["emails"] = {
|
|
"sent": email_qs.filter(status="SENT"),
|
|
"draft": email_qs.filter(status="DRAFT"),
|
|
}
|
|
context["lead_emails"] = {
|
|
"sent": lead_email_qs.filter(status="SENT"),
|
|
"draft": lead_email_qs.filter(status="DRAFT"),
|
|
}
|
|
context["staff_task_form"] = forms.StaffTaskForm()
|
|
context["note_form"] = forms.NoteForm()
|
|
context["lead_tasks"] = models.Tasks.objects.filter(
|
|
content_type__model="lead", object_id=self.object.lead.pk
|
|
)
|
|
context["tasks"] = models.Tasks.objects.filter(
|
|
content_type__model="opportunity", object_id=self.object.id
|
|
)
|
|
qs = models.Schedule.objects.filter(
|
|
dealer=dealer,
|
|
content_type__model="lead",
|
|
object_id=self.object.lead.id,
|
|
scheduled_by=self.request.user,
|
|
)
|
|
|
|
context["schedules"] = qs | models.Schedule.objects.filter(
|
|
dealer=dealer,
|
|
content_type__model="opportunity",
|
|
object_id=self.object.id,
|
|
scheduled_by=self.request.user,
|
|
)
|
|
context["schedules"] = context["schedules"].order_by("-created_at")
|
|
context["upcoming_events"] = {
|
|
"schedules": qs.filter(scheduled_at__gt=timezone.now())[:5],
|
|
}
|
|
context["schedule_form"] = forms.ScheduleForm()
|
|
context["stage_form"] = forms.OpportunityStageForm()
|
|
return context
|
|
|
|
|
|
class OpportunityListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
model = models.Opportunity
|
|
template_name = "crm/opportunities/opportunity_list.html"
|
|
context_object_name = "opportunities"
|
|
paginate_by = 30
|
|
permission_required = ["inventory.view_opportunity"]
|
|
permission_required = ["inventory.view_opportunity"]
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
|
|
if self.request.is_dealer:
|
|
queryset = models.Opportunity.objects.filter(dealer=dealer)
|
|
elif self.request.is_staff:
|
|
staff = self.request.staff
|
|
queryset = models.Opportunity.objects.filter(
|
|
dealer=dealer, lead__staff=staff
|
|
)
|
|
|
|
# Stage filter
|
|
stage = self.request.GET.get("stage")
|
|
print(stage)
|
|
if stage:
|
|
queryset = queryset.filter(stage=stage)
|
|
|
|
# Sorting
|
|
sort = self.request.GET.get("sort", "newest")
|
|
if sort == "newest":
|
|
queryset = queryset.order_by("-created")
|
|
elif sort == "highest":
|
|
queryset = queryset.order_by("-expected_revenue")
|
|
elif sort == "closing":
|
|
queryset = queryset.order_by("expected_close_date")
|
|
|
|
# Search filter
|
|
search = self.request.GET.get("q")
|
|
if search:
|
|
queryset = queryset.filter(
|
|
Q(customer__first_name__icontains=search)
|
|
| Q(customer__last_name__icontains=search)
|
|
| Q(customer__email__icontains=search)
|
|
)
|
|
|
|
return queryset
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["stage_choices"] = models.Stage.choices
|
|
context["empty_state_value"] = _("opportunity")
|
|
return context
|
|
|
|
def get_template_names(self):
|
|
return self.template_name
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.delete_opportunity", raise_exception=True)
|
|
def delete_opportunity(request, dealer_slug, pk):
|
|
"""
|
|
Deletes an opportunity object from the database and redirects to the opportunity
|
|
list view. If the opportunity with the specified primary key is not found, a 404
|
|
error will be raised. Displays a success message after deletion.
|
|
|
|
:param request: The HTTP request object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the opportunity object to be deleted.
|
|
:type pk: int
|
|
:return: An HTTP response redirecting to the opportunity list view.
|
|
:rtype: HttpResponse
|
|
"""
|
|
get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
opportunity = get_object_or_404(models.Opportunity, pk=pk)
|
|
opportunity.delete()
|
|
messages.success(request, _("Opportunity deleted successfully"))
|
|
return redirect("opportunity_list", dealer_slug=dealer_slug)
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.change_opportunity", raise_exception=True)
|
|
def opportunity_update_status(request, dealer_slug, slug):
|
|
"""
|
|
Update the status and/or stage of a specific Opportunity instance. This is a
|
|
view function, which is generally tied to a URL endpoint in a Django application.
|
|
The function is accessible only to authenticated users due to the
|
|
`@login_required` decorator.
|
|
|
|
The function retrieves the `Opportunity` instance based on the primary key
|
|
in the URL, updates the status or stage of the instance based on query
|
|
parameters in the request, saves the changes, and then redirects to the
|
|
detail view of the `Opportunity`. Additionally, a success message is displayed,
|
|
and a custom header (`HX-Refresh`) is added to the response.
|
|
|
|
:param request: The HTTP request object containing details of the user's
|
|
request, such as query parameters for status and stage. This is a
|
|
mandatory parameter provided by Django's view function framework.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the `Opportunity` instance to be updated.
|
|
:type pk: int
|
|
|
|
:return: An HTTP response object that redirects to the updated Opportunity's
|
|
detail page, with a success message and the "HX-Refresh" header to trigger
|
|
frontend behavior.
|
|
:rtype: HttpResponse
|
|
"""
|
|
opportunity = get_object_or_404(models.Opportunity, slug=slug)
|
|
status = request.GET.get("status")
|
|
stage = request.GET.get("stage")
|
|
if status:
|
|
opportunity.status = status
|
|
if stage:
|
|
opportunity.stage = stage
|
|
opportunity.save()
|
|
messages.success(request, _("Opportunity status updated successfully"))
|
|
response = HttpResponse(
|
|
redirect("opportunity_detail", dealer_slug=dealer_slug, slug=opportunity.slug)
|
|
)
|
|
response["HX-Refresh"] = "true"
|
|
return response
|
|
|
|
|
|
class NotificationListView(LoginRequiredMixin, ListView):
|
|
"""
|
|
Handles the display and management of notification history for a user.
|
|
|
|
This class provides functionality to display a paginated list of user notifications
|
|
sorted by their creation time in descending order. It ensures that the user is logged
|
|
in before accessing the notifications and filters the displayed notifications specific
|
|
to the logged-in user.
|
|
|
|
:ivar model: Specifies the model to retrieve data from.
|
|
:type model: models.Notification
|
|
:ivar template_name: Specifies the template to render the notifications page.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the variable accessible in the template to
|
|
refer to the list of notifications.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: Defines the number of notifications to display per page.
|
|
:type paginate_by: int
|
|
:ivar ordering: Defines the default ordering of notifications.
|
|
:type ordering: str
|
|
"""
|
|
|
|
model = models.Notification
|
|
template_name = "crm/notifications_history.html"
|
|
context_object_name = "notifications"
|
|
paginate_by = 20
|
|
ordering = "-created"
|
|
|
|
def get_queryset(self):
|
|
return models.Notification.objects.filter(user=self.request.user)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
user_notifications = self.get_queryset()
|
|
|
|
# Calculate the number of total, read and unread notifications
|
|
context["total_count"] = user_notifications.count()
|
|
context["read_count"] = user_notifications.filter(is_read=True).count()
|
|
context["unread_count"] = user_notifications.filter(is_read=False).count()
|
|
|
|
return context
|
|
|
|
|
|
class ItemServiceCreateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView
|
|
):
|
|
"""
|
|
Manages the creation of additional services with permission checks, success messages,
|
|
and validation of VAT and dealer-specific pricing.
|
|
|
|
This class-based view facilitates the creation of additional service items within
|
|
a custom Django application framework. It includes mixins for login-required access,
|
|
permission control, and success message handling. The form is validated to apply VAT
|
|
rates to taxable services and associates the service with the dealer determined from
|
|
the logged-in user's context.
|
|
|
|
:ivar model: The model representing additional services.
|
|
:type model: models.AdditionalServices
|
|
:ivar form_class: The form class used for creating a service.
|
|
:type form_class: forms.AdditionalServiceForm
|
|
:ivar template_name: The template used to render the service creation page.
|
|
:type template_name: str
|
|
:ivar success_url: The redirect URL upon successful form submission.
|
|
:type success_url: str
|
|
:ivar success_message: The success message displayed upon successful form submission.
|
|
:type success_message: str
|
|
:ivar context_object_name: The context name used to refer to the service object in templates.
|
|
:type context_object_name: str
|
|
:ivar permission_required: The permissions required for accessing this view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = models.AdditionalServices
|
|
form_class = forms.AdditionalServiceForm
|
|
template_name = "items/service/service_create.html"
|
|
success_message = _("Service created successfully")
|
|
context_object_name = "service"
|
|
permission_required = ["inventory.add_additionalservices"]
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
vat = models.VatRate.objects.get(dealer=dealer, is_active=True)
|
|
|
|
form.instance.dealer = dealer
|
|
# if form.instance.taxable:
|
|
# form.instance.price = (form.instance.price * vat.rate) + form.instance.price
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"item_service_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
|
|
)
|
|
|
|
|
|
class ItemServiceUpdateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
|
):
|
|
"""
|
|
Handles update functionality for additional services in the system.
|
|
|
|
This view enables authenticated and authorized users to update instances of
|
|
the `AdditionalServices` model. It requires the user to have the necessary
|
|
permissions and provides success feedback upon successful update. Additionally,
|
|
it calculates and updates the price of the service based on the tax rate if
|
|
the service is taxable.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: models.AdditionalServices
|
|
:ivar form_class: The form class used for handling AdditionalService updates.
|
|
:type form_class: forms.AdditionalServiceForm
|
|
:ivar template_name: The template used for rendering the service update page.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect after a successful update.
|
|
:type success_url: str
|
|
:ivar success_message: The success message displayed after a successful update.
|
|
:type success_message: str
|
|
:ivar context_object_name: The context variable name for the object to be displayed.
|
|
:type context_object_name: str
|
|
:ivar permission_required: Permissions required for accessing this view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
model = models.AdditionalServices
|
|
form_class = forms.AdditionalServiceForm
|
|
template_name = "items/service/service_create.html"
|
|
success_message = _("Service updated successfully")
|
|
context_object_name = "service"
|
|
permission_required = ["inventory.change_additionalservices"]
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
vat = models.VatRate.objects.get(dealer=dealer, is_active=True)
|
|
uom = dealer.entity.get_uom_all().filter(unit_abbr=form.instance.uom).first()
|
|
form.instance.dealer = dealer
|
|
form.instance.uom = uom.name
|
|
form.instance.item.uom = uom
|
|
|
|
# if form.instance.taxable:
|
|
# form.instance.price = (form.instance.price * vat.rate) + form.instance.price
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"item_service_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
|
|
)
|
|
|
|
|
|
class ItemServiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Handles the listing of additional services for a dealer.
|
|
|
|
This class is responsible for displaying a paginated list of additional
|
|
services specific to a dealer. It enforces login, permission checking,
|
|
and customizes the queryset to return only services associated with the
|
|
currently logged-in dealer.
|
|
|
|
:ivar model: The model representing the additional services data.
|
|
:type model: Model
|
|
:ivar template_name: Path to the template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The name of the context variable for the
|
|
object list.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: Number of items displayed per page.
|
|
:type paginate_by: int
|
|
:ivar permission_required: List of permissions required to access the
|
|
view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = models.AdditionalServices
|
|
template_name = "items/service/service_list.html"
|
|
context_object_name = "services"
|
|
paginate_by = 20
|
|
permission_required = ["inventory.view_additionalservices"]
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
query = self.request.GET.get("q")
|
|
qs = models.AdditionalServices.objects.filter(dealer=dealer).all()
|
|
if query:
|
|
qs = qs.filter(
|
|
Q(name__icontains=query)
|
|
| Q(id__icontains=query)
|
|
| Q(uom__icontains=query)
|
|
)
|
|
return qs
|
|
|
|
|
|
class ItemServiceDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
model = models.AdditionalServices
|
|
template_name = "items/service/service_detail.html"
|
|
context_object_name = "service"
|
|
permission_required = ["inventory.view_additionalservices"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
sold_cars = models.Car.objects.filter(
|
|
status="sold",
|
|
)
|
|
context["total_services_price"] = (
|
|
self.object.price * self.object.additionals.filter(status="sold").count()
|
|
)
|
|
return context
|
|
|
|
|
|
class ItemExpenseCreateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView
|
|
):
|
|
"""
|
|
Represents a view for creating item expense entries.
|
|
|
|
This class is responsible for handling the creation of expense items in the
|
|
application. It integrates with Django's class-based views and includes
|
|
customizations specific to the current user and their associated entity.
|
|
Permission control ensures only authorized users can create expenses.
|
|
|
|
:ivar model: The database model used for this view.
|
|
:type model: ItemModel
|
|
:ivar form_class: The form class used to render and validate input.
|
|
:type form_class: ExpenseItemCreateForm
|
|
:ivar template_name: The path to the template used to render the view.
|
|
:type template_name: str
|
|
:ivar success_url: The URL to redirect to upon successful form submission.
|
|
:type success_url: str
|
|
:ivar permission_required: A list of permissions required to access the view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
model = ItemModel
|
|
form_class = ExpenseItemCreateForm
|
|
template_name = "items/expenses/expense_create.html"
|
|
success_url = reverse_lazy("item_expense_list")
|
|
success_message = _("Expense created successfully")
|
|
permission_required = ["django_ledger.add_itemmodel"]
|
|
|
|
def get_form_kwargs(self):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs["entity_slug"] = dealer.entity.slug
|
|
kwargs["user_model"] = dealer.entity.admin
|
|
return kwargs
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
form.instance.entity = dealer.entity
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"item_expense_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
|
|
)
|
|
|
|
|
|
class ItemExpenseUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
|
"""
|
|
Manages the update view for item expenses.
|
|
|
|
This class enables authenticated users with the necessary permissions to update
|
|
information related to item expenses. It integrates form handling and permission
|
|
requirements, ensuring entity-specific contextual data is appended to the form
|
|
during updates.
|
|
|
|
:ivar model: The model associated with the update operation.
|
|
:type model: ItemModel
|
|
:ivar form_class: The form used to validate and process updates.
|
|
:type form_class: ExpenseItemUpdateForm
|
|
:ivar template_name: Path to the template used for rendering the update view.
|
|
:type template_name: str
|
|
:ivar success_url: URL where the user is redirected after a successful update.
|
|
:type success_url: str
|
|
:ivar permission_required: List of permissions required to access this view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
model = ItemModel
|
|
form_class = ExpenseItemUpdateForm
|
|
template_name = "items/expenses/expense_update.html"
|
|
success_url = reverse_lazy("item_expense_list")
|
|
permission_required = ["django_ledger.change_itemmodel"]
|
|
|
|
def get_form_kwargs(self):
|
|
dealer = get_user_type(self.request)
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs["entity_slug"] = dealer.entity.slug
|
|
kwargs["user_model"] = dealer.entity.admin
|
|
return kwargs
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_user_type(self.request)
|
|
form.instance.entity = dealer.entity
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy(
|
|
"item_expense_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}
|
|
)
|
|
|
|
|
|
class ItemExpenseListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Handles the display of a list of item expenses.
|
|
|
|
This class-based view is responsible for displaying a paginated list of item
|
|
expenses related to a specific entity. It uses the Django generic `ListView`
|
|
class and integrates with permissions and login requirements. It retrieves
|
|
data through the `get_queryset` method by invoking a specific function on
|
|
the user's associated entity object.
|
|
|
|
:ivar model: The model associated with the view.
|
|
:type model: type[ItemModel]
|
|
:ivar template_name: The template used to render the view.
|
|
:type template_name: str
|
|
:ivar context_object_name: The context variable name used in the template.
|
|
:type context_object_name: str
|
|
:ivar paginate_by: The number of items displayed per page.
|
|
:type paginate_by: int
|
|
:ivar permission_required: The list of required permissions to access the view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
model = ItemModel
|
|
template_name = "items/expenses/expenses_list.html"
|
|
context_object_name = "expenses"
|
|
paginate_by = 20
|
|
permission_required = ["django_ledger.view_itemmodel"]
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
query = self.request.GET.get("q")
|
|
qs = dealer.entity.get_items_expenses()
|
|
if query:
|
|
qs = apply_search_filters(qs, query)
|
|
return qs
|
|
|
|
|
|
class ItemExpenseDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
queryset = ItemModel.objects.filter(item_role="expense")
|
|
template_name = "items/expenses/expense_detail.html"
|
|
context_object_name = "expense"
|
|
permission_required = ["django_ledger.view_itemmodel"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
# Get the related bills queryset
|
|
bills_list = self.object.billmodel_set.all().order_by("-created")
|
|
|
|
# Paginate the bills
|
|
paginator = Paginator(bills_list, 10) # Show 10 bills per page
|
|
page_number = self.request.GET.get("page")
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
# Add the paginated bills to the context
|
|
context["page_obj"] = page_obj
|
|
context["entity"] = get_user_type(self.request).entity
|
|
return context
|
|
|
|
|
|
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"
|
|
paginate_by = 20
|
|
permission_required = ["django_ledger.view_billmodel"]
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
qs = dealer.entity.get_bills()
|
|
query = self.request.GET.get("q")
|
|
if query:
|
|
qs = qs.filter(
|
|
Q(bill_number__icontains=query)
|
|
| Q(vendor__vendor_name__icontains=query)
|
|
)
|
|
return qs
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["entity"] = get_user_type(self.request).entity
|
|
return context
|
|
|
|
|
|
class BillModelCreateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView
|
|
):
|
|
template_name = "bill/bill_create.html"
|
|
PAGE_TITLE = _("Create Bill")
|
|
permission_required = "django_ledger.add_billmodel"
|
|
extra_context = {
|
|
"page_title": PAGE_TITLE,
|
|
"header_title": PAGE_TITLE,
|
|
"header_subtitle_icon": "uil:bill",
|
|
}
|
|
for_purchase_order = False
|
|
for_estimate = False
|
|
success_message = _("Bill created successfully")
|
|
|
|
# Get user info for logging
|
|
|
|
def get(self, request, dealer_slug, **kwargs):
|
|
user_username = (
|
|
request.user.username if request.user.is_authenticated else "anonymous"
|
|
)
|
|
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
if not request.user.is_authenticated:
|
|
return HttpResponseForbidden()
|
|
|
|
if self.for_estimate and "ce_pk" in self.kwargs:
|
|
estimate_qs = EstimateModel.objects.for_entity(
|
|
entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin
|
|
)
|
|
estimate_model: EstimateModel = get_object_or_404(
|
|
estimate_qs, uuid__exact=self.kwargs["ce_pk"]
|
|
)
|
|
if not estimate_model.can_bind():
|
|
logger.warning(
|
|
f"User {user_username} attempted to create bill from Estimate ID: {estimate_model.uuid}, "
|
|
f"but estimate cannot be bound (already bound/invalid state). Returning 404."
|
|
)
|
|
return HttpResponseNotFound("404 Not Found")
|
|
logger.debug(
|
|
f"User {user_username} confirmed Estimate ID: {estimate_model.uuid} "
|
|
f"can be bound for bill creation."
|
|
)
|
|
return super(BillModelCreateView, self).get(request, dealer_slug, **kwargs)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(BillModelCreateView, self).get_context_data(**kwargs)
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
user_username = (
|
|
self.request.user.username
|
|
if self.request.user.is_authenticated
|
|
else "anonymous"
|
|
)
|
|
|
|
if self.for_purchase_order:
|
|
po_pk = self.kwargs["po_pk"]
|
|
po_item_uuids_qry_param = self.request.GET.get("item_uuids")
|
|
if po_item_uuids_qry_param:
|
|
try:
|
|
po_item_uuids = po_item_uuids_qry_param.split(",")
|
|
# --- Single-line log for successful parsing ---
|
|
logger.debug(
|
|
f"User {user_username} successfully parsed item_uuids from query param "
|
|
f"for Purchase Order ID: {po_pk} "
|
|
)
|
|
except:
|
|
# --- Single-line log for error during parsing ---
|
|
logger.warning(
|
|
f"User {user_username}submitted invalid item_uuids query parameter "
|
|
f"'{po_item_uuids_qry_param}' for Purchase Order ID: {po_pk} "
|
|
f"Returning BadRequest."
|
|
)
|
|
return HttpResponseBadRequest()
|
|
else:
|
|
logger.warning(
|
|
f"User {user_username} attempted to create bill from Purchase Order ID: {po_pk} "
|
|
f"Returning BadRequest."
|
|
)
|
|
return HttpResponseBadRequest()
|
|
|
|
po_qs = PurchaseOrderModel.objects.for_entity(
|
|
entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin
|
|
).prefetch_related("itemtransactionmodel_set")
|
|
po_model: PurchaseOrderModel = get_object_or_404(po_qs, uuid__exact=po_pk)
|
|
po_itemtxs_qs = po_model.itemtransactionmodel_set.filter(
|
|
bill_model__isnull=True, uuid__in=po_item_uuids
|
|
)
|
|
context["po_model"] = po_model
|
|
context["po_itemtxs_qs"] = po_itemtxs_qs
|
|
form_action = (
|
|
reverse(
|
|
"bill-create-po",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"entity_slug": self.kwargs["entity_slug"],
|
|
"po_pk": po_model.uuid,
|
|
},
|
|
)
|
|
+ f"?item_uuids={po_item_uuids_qry_param}"
|
|
)
|
|
elif self.for_estimate:
|
|
estimate_qs = EstimateModel.objects.for_entity(
|
|
entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin
|
|
)
|
|
estimate_uuid = self.kwargs["ce_pk"]
|
|
estimate_model: EstimateModel = get_object_or_404(
|
|
estimate_qs, uuid__exact=estimate_uuid
|
|
)
|
|
form_action = reverse(
|
|
"bill-create-estimate",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"entity_slug": self.kwargs["entity_slug"],
|
|
"ce_pk": estimate_model.uuid,
|
|
},
|
|
)
|
|
else:
|
|
form_action = reverse(
|
|
"bill-create",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"entity_slug": self.kwargs["entity_slug"],
|
|
},
|
|
)
|
|
context["form_action_url"] = form_action
|
|
return context
|
|
|
|
def get_initial(self):
|
|
return {"date_draft": get_localdate()}
|
|
|
|
def get_form(self, form_class=None):
|
|
# form = super().get_form(form_class)
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
form = BillModelCreateForm(entity_model=dealer.entity, **self.get_form_kwargs())
|
|
form.initial["prepaid_account"] = (
|
|
models.DealerSettings.objects.filter(dealer=dealer)
|
|
.first()
|
|
.bill_prepaid_account
|
|
or None
|
|
)
|
|
form.initial["unearned_account"] = (
|
|
models.DealerSettings.objects.filter(dealer=dealer)
|
|
.first()
|
|
.bill_unearned_account
|
|
or None
|
|
)
|
|
form.initial["cash_account"] = (
|
|
models.DealerSettings.objects.filter(dealer=dealer)
|
|
.first()
|
|
.bill_cash_account
|
|
or None
|
|
)
|
|
return form
|
|
|
|
def form_valid(self, form):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
bill_model: BillModel = form.save(commit=False)
|
|
ledger_model, bill_model = bill_model.configure(
|
|
entity_slug=dealer.entity,
|
|
user_model=dealer.entity.admin,
|
|
commit_ledger=True,
|
|
)
|
|
|
|
if self.for_estimate:
|
|
ce_pk = self.kwargs["ce_pk"]
|
|
estimate_model_qs = EstimateModel.objects.for_entity(
|
|
entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin
|
|
)
|
|
|
|
estimate_model = get_object_or_404(estimate_model_qs, uuid__exact=ce_pk)
|
|
bill_model.bind_estimate(estimate_model=estimate_model, commit=False)
|
|
|
|
elif self.for_purchase_order:
|
|
po_pk = self.kwargs["po_pk"]
|
|
item_uuids = self.request.GET.get("item_uuids")
|
|
if not item_uuids:
|
|
return HttpResponseBadRequest()
|
|
item_uuids = item_uuids.split(",")
|
|
po_qs = PurchaseOrderModel.objects.for_entity(
|
|
entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin
|
|
)
|
|
po_model: PurchaseOrderModel = get_object_or_404(po_qs, uuid__exact=po_pk)
|
|
|
|
user_username = (
|
|
self.request.user.username
|
|
if self.request.user.is_authenticated
|
|
else "anonymous"
|
|
)
|
|
try:
|
|
bill_model.can_bind_po(po_model, raise_exception=True)
|
|
logger.info(
|
|
f"User {user_username} successfully validated binding of Bill ID: {getattr(bill_model, 'pk', 'N/A')} "
|
|
f"to Purchase Order ID: {getattr(po_model, 'pk', 'N/A')}."
|
|
)
|
|
except ValidationError as e:
|
|
messages.add_message(
|
|
self.request,
|
|
message=e.message,
|
|
level=messages.ERROR,
|
|
extra_tags="is-danger",
|
|
)
|
|
# --- Single-line log for ValidationError during binding validation ---
|
|
logger.warning(
|
|
f"User {user_username} encountered a validation error "
|
|
f"while attempting to bind Bill ID: {getattr(bill_model, 'pk', 'N/A')} "
|
|
f"to Purchase Order ID: {getattr(po_model, 'pk', 'N/A')}. Error: {e.message}"
|
|
)
|
|
return self.render_to_response(self.get_context_data(form=form))
|
|
|
|
po_model_items_qs = po_model.itemtransactionmodel_set.filter(
|
|
uuid__in=item_uuids
|
|
)
|
|
|
|
if po_model.is_contract_bound():
|
|
bill_model.ce_model_id = po_model.ce_model_id
|
|
|
|
bill_model.update_amount_due()
|
|
bill_model.get_state(commit=True)
|
|
bill_model.clean()
|
|
bill_model.save()
|
|
po_model_items_qs.update(bill_model=bill_model)
|
|
return HttpResponseRedirect(self.get_success_url())
|
|
|
|
return super(BillModelCreateView, self).form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
entity_slug = self.kwargs["entity_slug"]
|
|
if self.for_purchase_order:
|
|
po_pk = self.kwargs["po_pk"]
|
|
return reverse(
|
|
"purchase_order_update",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"entity_slug": entity_slug,
|
|
"po_pk": po_pk,
|
|
},
|
|
)
|
|
elif self.for_estimate:
|
|
return reverse(
|
|
"customer-estimate-detail",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"entity_slug": entity_slug,
|
|
"ce_pk": self.kwargs["ce_pk"],
|
|
},
|
|
)
|
|
bill_model: BillModel = self.object
|
|
return reverse(
|
|
"bill-detail",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"entity_slug": entity_slug,
|
|
"bill_pk": bill_model.uuid,
|
|
},
|
|
)
|
|
|
|
def get_queryset(self):
|
|
qs = super().get_queryset()
|
|
return qs.select_related(
|
|
"ledger",
|
|
"ledger__entity",
|
|
"vendor",
|
|
"cash_account",
|
|
"prepaid_account",
|
|
"unearned_account",
|
|
"cash_account__coa_model",
|
|
"prepaid_account__coa_model",
|
|
"unearned_account__coa_model",
|
|
)
|
|
|
|
|
|
class BillModelDetailView(BillModelDetailViewBase):
|
|
template_name = "bill/bill_detail.html"
|
|
permission_required = ["django_ledger.view_billmodel"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(BillModelDetailView, self).get_context_data(**kwargs)
|
|
context["dealer"] = self.request.dealer
|
|
return context
|
|
|
|
def get_queryset(self):
|
|
qs = super().get_queryset()
|
|
return qs.select_related(
|
|
"ledger",
|
|
"ledger__entity",
|
|
"vendor",
|
|
"cash_account",
|
|
"prepaid_account",
|
|
"unearned_account",
|
|
"cash_account__coa_model",
|
|
"prepaid_account__coa_model",
|
|
"unearned_account__coa_model",
|
|
)
|
|
|
|
|
|
class BillModelUpdateView(BillModelUpdateViewBase):
|
|
permission_required = ["django_ledger.change_billmodel"]
|
|
|
|
|
|
class OrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Represents a view for listing sale orders.
|
|
|
|
This class provides the functionality to display a list of sale orders
|
|
within a web template. It includes permission-based access control, and
|
|
retrieves sale orders corresponding to the entity of the logged-in dealer.
|
|
|
|
:ivar model: The model associated with this view.
|
|
:type model: models.SaleOrder
|
|
:ivar template_name: The name of the template to be rendered.
|
|
:type template_name: str
|
|
:ivar context_object_name: The context variable name to be used for the object list.
|
|
:type context_object_name: str
|
|
:ivar permission_required: A list of permissions required to access this view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
model = models.SaleOrder
|
|
template_name = "sales/orders/order_list.html"
|
|
context_object_name = "orders"
|
|
paginate_by = 30
|
|
permission_required = ["inventory.view_saleorder"]
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
qs = super().get_queryset().select_related("customer", "estimate", "invoice")
|
|
return qs.filter(estimate__entity=dealer.entity)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.change_estimatemodel", raise_exception=True)
|
|
def send_email_view(request, dealer_slug, pk):
|
|
"""
|
|
View function to send an email for an estimate. This function allows authenticated and
|
|
authorized users to send an email containing the estimate details to the customer.
|
|
The email includes a link to preview the estimate and a message template in multiple
|
|
languages. If the estimate does not have any associated items, the user will receive
|
|
an error message. Upon successfully sending the email, the estimate is marked as reviewed.
|
|
|
|
:param request: The HttpRequest object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the estimate to be sent via email.
|
|
:type pk: int
|
|
:return: An HttpResponseRedirect to the estimate detail view.
|
|
:rtype: HttpResponseRedirect
|
|
:raises Http404: If the estimate with the given primary key does not exist.
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
estimate = get_object_or_404(EstimateModel, pk=pk)
|
|
|
|
if not estimate.get_itemtxs_data()[0]:
|
|
messages.error(request, _("Quotation has no items"))
|
|
return redirect("estimate_detail", dealer_slug=dealer_slug, pk=estimate.pk)
|
|
|
|
link = request.build_absolute_uri(
|
|
reverse_lazy(
|
|
"estimate_preview", kwargs={"dealer_slug": dealer_slug, "pk": estimate.pk}
|
|
)
|
|
)
|
|
|
|
msg = f"""
|
|
السلام عليكم،
|
|
|
|
عزيزي {estimate.customer.customer_name}،
|
|
|
|
يسعدني أن أشارككم عرض السعر الذي طلبتموه. يرجى الاطلاع على التفاصيل الكاملة والأسعار من خلال الرابط أدناه.
|
|
|
|
حرصنا على أن يكون عرضنا مناسباً وشفافاً. إذا كانت لديكم أي استفسارات أو ملاحظات، فلا تترددوا في التواصل معنا.
|
|
|
|
رابط عرض السعر:
|
|
{link}
|
|
|
|
نأمل أن ينال العرض إعجابكم ونتطلع إلى بدء العمل قريباً!
|
|
|
|
تحياتي،
|
|
|
|
{dealer.get_local_name()}
|
|
{dealer.phone_number}
|
|
Haikal | هيكل
|
|
-----
|
|
Dear {estimate.customer.customer_name},
|
|
|
|
I hope this email finds you well.
|
|
|
|
Following up on our conversation, I'm excited to share the quotation for your review. Please find the detailed pricing and information by clicking on the link below.
|
|
|
|
We've done our best to provide you with a fair and competitive offer. If you have any questions or would like to discuss it further, please don't hesitate to reach out.
|
|
|
|
Quotation Link:
|
|
{link}
|
|
|
|
We look forward to hearing from you and hopefully moving forward with your project!
|
|
|
|
Best regards,
|
|
|
|
{dealer.get_local_name()}
|
|
{dealer.phone_number}
|
|
Haikal
|
|
"""
|
|
# subject = _("Quotation")
|
|
|
|
send_email(
|
|
str(settings.DEFAULT_FROM_EMAIL),
|
|
estimate.customer.email,
|
|
"عرض سعر - Quotation",
|
|
msg,
|
|
)
|
|
|
|
# estimate.mark_as_review()
|
|
messages.success(request, _("Email sent successfully"))
|
|
|
|
return redirect("estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk)
|
|
|
|
|
|
# errors
|
|
def custom_page_not_found_view(request, exception=None):
|
|
"""
|
|
Custom handler for 404 errors that renders a custom HTML page
|
|
upon encountering a Page Not Found error.
|
|
|
|
:param request: The HttpRequest object associated with the request.
|
|
:type request: HttpRequest
|
|
:param exception: The exception that triggered the 404 error, if any.
|
|
:type exception: Exception, optional
|
|
:return: An HttpResponse object rendering the "errors/404.html" template.
|
|
:rtype: HttpResponse
|
|
"""
|
|
return render(request, "errors/404.html", {})
|
|
|
|
|
|
def custom_error_view(request, exception=None):
|
|
"""
|
|
Handles rendering the custom error page for HTTP 500 errors.
|
|
|
|
This function is called when an unhandled exception occurs in the application. It renders
|
|
a predefined template for server errors, providing a consistent error page layout
|
|
to the users.
|
|
|
|
:param request: The HTTP request instance which triggered the error.
|
|
:type request: HttpRequest
|
|
:param exception: The exception that caused the error to trigger. Defaults to None.
|
|
:type exception: Exception, optional
|
|
:return: An HttpResponse object representing the rendered error page.
|
|
:rtype: HttpResponse
|
|
"""
|
|
return render(request, "errors/500.html", {})
|
|
|
|
|
|
def custom_permission_denied_view(request, exception=None):
|
|
"""
|
|
Handles the custom view for 403 Forbidden permission denied errors. This
|
|
function renders a custom template for the error page when users are denied
|
|
permission to access a particular resource or view.
|
|
|
|
:param request: Django HttpRequest object containing metadata about the request.
|
|
:type request: HttpRequest
|
|
:param exception: Optional exception that caused the 403 error.
|
|
Defaults to None.
|
|
:type exception: Exception | None
|
|
:return: HttpResponse object rendering the 403.html error page.
|
|
:rtype: HttpResponse
|
|
"""
|
|
return render(request, "errors/403.html", {})
|
|
|
|
|
|
def custom_bad_request_view(request, exception=None):
|
|
"""
|
|
Handles custom bad request responses by rendering a specified HTML
|
|
template for 400 status errors.
|
|
|
|
:return: Rendered HTTP response rendering the error page.
|
|
:rtype: HttpResponse
|
|
"""
|
|
return render(request, "errors/400.html", {})
|
|
|
|
|
|
# BALANCE SHEET
|
|
class BaseBalanceSheetRedirectView(RedirectView):
|
|
"""
|
|
Handles redirection for balance sheet views of entities.
|
|
|
|
This class is specifically designed to redirect users to the
|
|
appropriate year's balance sheet for an entity. It determines
|
|
the URL based on the current year and the provided entity slug
|
|
in the request. Additionally, it provides a URL for login redirection
|
|
if required.
|
|
|
|
:ivar url: The original URL or base URL to redirect users.
|
|
:type url: str
|
|
"""
|
|
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
year = get_localdate().year
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
return reverse(
|
|
"entity-bs-year",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"entity_slug": self.kwargs["entity_slug"],
|
|
"year": year,
|
|
},
|
|
)
|
|
|
|
def get_login_url(self):
|
|
return reverse("account_login")
|
|
|
|
|
|
class FiscalYearBalanceSheetViewBase(AuthorizedEntityMixin, FiscalYearBalanceSheetView):
|
|
"""
|
|
Defines a base view for the fiscal year balance sheet.
|
|
|
|
This class serves as a base view for displaying the balance sheet of a fiscal
|
|
year. It provides functionality for rendering the associated template and
|
|
managing user login requirements. It integrates with Django's security system
|
|
to ensure appropriate access control.
|
|
|
|
:ivar template_name: The template used for rendering the balance sheet view.
|
|
:type template_name: str
|
|
"""
|
|
|
|
template_name = "ledger/reports/balance_sheet.html"
|
|
|
|
def get_login_url(self):
|
|
return reverse("account_login")
|
|
|
|
|
|
class QuarterlyBalanceSheetView(FiscalYearBalanceSheetViewBase, QuarterlyReportMixIn):
|
|
"""
|
|
Represents a quarterly balance sheet view.
|
|
|
|
The purpose of this class is to provide a representation and handling
|
|
for the balance sheet data specific to fiscal quarters. It extends from
|
|
`FiscalYearBalanceSheetViewBase` to inherit year-level balance sheet
|
|
operations and incorporates functionality from `QuarterlyReportMixIn`
|
|
to manage quarterly-specific logic. Additionally, the `DjangoLedgerSecurityMixIn`
|
|
is used to handle security features related to Django ledger operations.
|
|
|
|
:ivar fiscal_year: The fiscal year associated with the quarterly balance sheet.
|
|
:type fiscal_year: int
|
|
:ivar quarter: The specific quarter (e.g., Q1, Q2, Q3, Q4) for the balance sheet.
|
|
:type quarter: int
|
|
:ivar balance_data: The data structure representing the detailed financial
|
|
information for the balance sheet of the quarter.
|
|
:type balance_data: dict
|
|
"""
|
|
|
|
|
|
class MonthlyBalanceSheetView(
|
|
FiscalYearBalanceSheetViewBase,
|
|
MonthlyReportMixIn,
|
|
):
|
|
"""
|
|
Represents the view for the monthly balance sheet.
|
|
|
|
This class is responsible for handling the presentation and processing of
|
|
monthly balance sheet data. It incorporates necessary mix-ins to provide
|
|
functionality specific to fiscal year balance sheet viewing, monthly reporting,
|
|
and security validation within the Django ledger context.
|
|
|
|
:ivar fiscal_year: Stores the fiscal year information associated with the
|
|
balance sheet.
|
|
:type fiscal_year: int
|
|
:ivar monthly_reports: Holds a collection of monthly report instances that
|
|
pertain to the balance sheet.
|
|
:type monthly_reports: List[MonthlyReport]
|
|
:ivar user_permissions: Manages user-specific permissions for accessing and
|
|
modifying the balance sheet data.
|
|
:type user_permissions: Dict[str, Any]
|
|
"""
|
|
|
|
|
|
class DateBalanceSheetView(
|
|
FiscalYearBalanceSheetViewBase,
|
|
DateReportMixIn,
|
|
):
|
|
"""
|
|
Represents a balance sheet view for a specific date.
|
|
|
|
DateBalanceSheetView extends functionality for displaying balance sheets
|
|
specific to certain dates. This class combines features from various mixins
|
|
to enforce security, handle date-based reporting, and provide fiscal year
|
|
context. It is used in scenarios where financial reports for a particular
|
|
date are required to ensure accurate and filtered viewing of accounting
|
|
data.
|
|
|
|
:ivar fiscal_year: Represents the fiscal year context for the balance sheet.
|
|
:type fiscal_year: FiscalYear
|
|
:ivar report_date: The specific date for which the balance sheet report
|
|
is generated.
|
|
:type report_date: datetime.date
|
|
:ivar is_authenticated: Indicates whether the user accessing the
|
|
balance sheet has been authenticated.
|
|
:type is_authenticated: bool
|
|
:ivar account_data: Contains processed account data used in the balance sheet
|
|
for computations or display purposes.
|
|
:type account_data: dict
|
|
"""
|
|
|
|
|
|
# Income Statement -----------
|
|
|
|
|
|
class BaseIncomeStatementRedirectViewBase(
|
|
AuthorizedEntityMixin, BaseIncomeStatementRedirectView
|
|
):
|
|
"""
|
|
The BaseIncomeStatementRedirectViewBase class provides functionality for handling
|
|
redirects in the context of income statements. This class combines features from
|
|
both BaseIncomeStatementRedirectView and DjangoLedgerSecurityMixIn to ensure
|
|
secure handling of data and user authentication.
|
|
|
|
This class operates by calculating the current year, determining the user's dealer
|
|
type, and generating a URL to redirect to an income statement view associated with a
|
|
specific entity for the calculated year. Additionally, it defines a method to generate
|
|
the login URL for unauthenticated users.
|
|
|
|
:ivar request: The HTTP request object containing user session and other request-related data.
|
|
:type request: HttpRequest
|
|
"""
|
|
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
year = get_localdate().year
|
|
dealer = get_object_or_404(models.Dealer, slug=kwargs["dealer_slug"])
|
|
return reverse(
|
|
"entity-ic-year",
|
|
kwargs={
|
|
"dealer_slug": dealer.slug,
|
|
"entity_slug": dealer.entity.slug,
|
|
"year": year,
|
|
},
|
|
)
|
|
|
|
def get_login_url(self):
|
|
return reverse("account_login")
|
|
|
|
|
|
class FiscalYearIncomeStatementViewBase(
|
|
AuthorizedEntityMixin, FiscalYearIncomeStatementView
|
|
):
|
|
"""
|
|
Represents a base view for fiscal year income statement.
|
|
|
|
This class serves as a base view for generating and rendering the fiscal
|
|
year income statement within the application. It combines multiple
|
|
functionalities, including template rendering and permission handling,
|
|
to provide a secure and user-friendly interface.
|
|
|
|
:ivar template_name: Path to the HTML template used for rendering the
|
|
income statement view.
|
|
:type template_name: str
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list[str]
|
|
"""
|
|
|
|
template_name = "ledger/reports/income_statement.html"
|
|
permission_required = ["django_ledger.view_ledgermodel"]
|
|
|
|
def get_login_url(self):
|
|
return reverse("account_login")
|
|
|
|
|
|
class QuarterlyIncomeStatementView(
|
|
FiscalYearIncomeStatementViewBase, QuarterlyReportMixIn
|
|
):
|
|
"""
|
|
Represents a detailed view for a quarterly income statement.
|
|
|
|
This class is responsible for providing a structured view of a company's
|
|
quarterly income statement. It combines the functionality of multiple
|
|
base classes to deliver a comprehensive representation of quarterly report
|
|
data while ensuring integration with Django Ledger security protocols.
|
|
|
|
The class serves as a specialized view tied to specific fiscal data,
|
|
enabling efficient management and rendering of quarterly financial reports.
|
|
|
|
:ivar fiscal_year: The fiscal year associated with the quarterly income
|
|
statement.
|
|
:type fiscal_year: int
|
|
:ivar quarter: The quarter number (e.g., 1 for Q1, 2 for Q2, etc.) of the
|
|
income statement.
|
|
:type quarter: int
|
|
:ivar revenue: The total revenue recorded in the quarterly income statement.
|
|
:type revenue: float
|
|
:ivar net_income: The net income calculated for the quarter.
|
|
:type net_income: float
|
|
:ivar expenses: The total expenses incurred during the quarter.
|
|
:type expenses: float
|
|
"""
|
|
|
|
|
|
class MonthlyIncomeStatementView(
|
|
FiscalYearIncomeStatementViewBase,
|
|
MonthlyReportMixIn,
|
|
):
|
|
"""
|
|
Represents the view for a monthly income statement in the financial
|
|
application.
|
|
|
|
This class is designed to support the presentation and display of
|
|
financial income statement data on a monthly basis. It incorporates
|
|
functionality from multiple mixins to include specific fiscal year
|
|
operations, monthly report behaviors, and security aspects tailored
|
|
to the Django Ledger system.
|
|
|
|
:ivar fiscal_year: The fiscal year associated with the income
|
|
statement.
|
|
:type fiscal_year: int
|
|
:ivar month: The month for which the income statement is viewed.
|
|
:type month: int
|
|
:ivar data: The financial data contained in the income statement.
|
|
:type data: dict
|
|
:ivar authorized_user: The user authorized to access this income
|
|
statement view.
|
|
:type authorized_user: str
|
|
"""
|
|
|
|
|
|
class DateModelIncomeStatementView(
|
|
FiscalYearIncomeStatementViewBase,
|
|
DateReportMixIn,
|
|
):
|
|
"""
|
|
Represents a detailed view of an income statement for a fiscal year with additional
|
|
capabilities for handling dates and security integrations.
|
|
|
|
This class is a specialized combination of views and mixins designed to manage income
|
|
statements with added functionality for date-based operations and Django-based ledger
|
|
security. It inherits behaviors for fiscal year reports, date handling, and security
|
|
enforcements, providing a cohesive interface for managing income statement data.
|
|
|
|
:ivar fiscal_year_data: Contains detailed income statement data for a specific fiscal year.
|
|
:type fiscal_year_data: dict
|
|
:ivar report_date: Represents the date of the associated report.
|
|
:type report_date: datetime.date
|
|
:ivar user_permissions: Tracks security permissions associated with the income statement for
|
|
authenticated users.
|
|
:type user_permissions: dict
|
|
"""
|
|
|
|
|
|
# Cash Flow -----------
|
|
|
|
|
|
class BaseCashFlowStatementRedirectViewBase(
|
|
AuthorizedEntityMixin, BaseCashFlowStatementRedirectView
|
|
):
|
|
"""
|
|
Base class for handling cash flow statement redirection views.
|
|
|
|
This class is designed to manage the redirection of users to the
|
|
appropriate cash flow statement view for a particular year and entity.
|
|
It incorporates security features and functionalities specific to dealers
|
|
or entities by extending relevant mixins.
|
|
|
|
:ivar request: The HTTP request object associated with the view.
|
|
:type request: HttpRequest
|
|
"""
|
|
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
year = get_localdate().year
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
return reverse(
|
|
"entity-cf-year",
|
|
kwargs={
|
|
"dealer_slug": dealer.slug,
|
|
"entity_slug": dealer.entity.slug,
|
|
"year": year,
|
|
},
|
|
)
|
|
|
|
def get_login_url(self):
|
|
return reverse("account_login")
|
|
|
|
|
|
class FiscalYearCashFlowStatementViewBase(
|
|
AuthorizedEntityMixin, FiscalYearCashFlowStatementView
|
|
):
|
|
"""
|
|
Represents a base view for fiscal year cash flow statements.
|
|
|
|
This class is utilized to handle the rendering and permissions control
|
|
for fiscal year cash flow statement views. It inherits functionality
|
|
from FiscalYearCashFlowStatementView and DjangoLedgerSecurityMixIn
|
|
to integrate with the Django framework's security and reporting
|
|
mechanisms.
|
|
|
|
:ivar template_name: Template path to render the view.
|
|
:type template_name: str
|
|
:ivar permission_required: List of permissions required for accessing
|
|
the view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
template_name = "ledger/reports/cash_flow_statement.html"
|
|
permission_required = ["django_ledger.view_ledgermodel"]
|
|
|
|
def get_login_url(self):
|
|
return reverse("account_login")
|
|
|
|
|
|
class QuarterlyCashFlowStatementView(
|
|
FiscalYearCashFlowStatementViewBase,
|
|
QuarterlyReportMixIn,
|
|
):
|
|
"""
|
|
Represents a view model for quarterly cash flow statements.
|
|
|
|
This class is designed to encapsulate and handle data related to
|
|
quarterly cash flow statements, extending the functionality from
|
|
base classes and mix-ins for specific behaviors. It combines features
|
|
from fiscal year cash flow statements, quarterly reporting utilities,
|
|
and Django ledger security mechanisms to provide comprehensive support
|
|
for managing and displaying financial data for quarterly cash flows.
|
|
|
|
:ivar fiscal_year: The fiscal year associated with the cash flow
|
|
statement.
|
|
:type fiscal_year: int
|
|
:ivar quarter: The quarter associated with the cash flow statement,
|
|
typically denoted as an integer from 1 to 4.
|
|
:type quarter: int
|
|
:ivar cash_flow_data: A structure containing detailed cash flow
|
|
information for the given quarter and fiscal year.
|
|
:type cash_flow_data: dict
|
|
:ivar is_audited: Boolean flag indicating whether the quarterly cash
|
|
flow statement has been audited.
|
|
:type is_audited: bool
|
|
"""
|
|
|
|
|
|
class MonthlyCashFlowStatementView(
|
|
FiscalYearCashFlowStatementViewBase,
|
|
MonthlyReportMixIn,
|
|
):
|
|
"""
|
|
Represents a view for monthly cash flow statements.
|
|
|
|
This class combines the functionality of fiscal year cash flow statement views,
|
|
monthly reporting capabilities, and security features related to Django Ledger.
|
|
It is utilized to generate or retrieve cash flow statements on a monthly basis,
|
|
ensuring seamless integration with other components that extend or implement
|
|
financial reporting logic.
|
|
|
|
:ivar attribute1: Description of attribute1.
|
|
:type attribute1: type
|
|
:ivar attribute2: Description of attribute2.
|
|
:type attribute2: type
|
|
"""
|
|
|
|
|
|
class DateCashFlowStatementView(
|
|
FiscalYearCashFlowStatementViewBase,
|
|
DateReportMixIn,
|
|
):
|
|
"""
|
|
Representation of a detailed view for a cash flow statement associated with a specific fiscal year,
|
|
including reporting capabilities and security features. This class extends functionality from
|
|
multiple mix-in classes to provide a comprehensive cash flow statement representation with date-specific
|
|
reporting.
|
|
|
|
:ivar field1: Description of field1.
|
|
:type field1: type
|
|
:ivar field2: Description of field2.
|
|
:type field2: type
|
|
"""
|
|
|
|
|
|
# Dashboard
|
|
|
|
|
|
class EntityModelDetailHandlerViewBase(
|
|
EntityModelDetailHandlerView,
|
|
):
|
|
"""
|
|
Handles detailed views for Entity Models along with redirection logic
|
|
customized for different users and units.
|
|
|
|
This class extends functionality from `EntityModelDetailHandlerView` and
|
|
integrates additional security measures using `DjangoLedgerSecurityMixIn`.
|
|
It provides a mechanism to construct redirection URLs based on the specifics
|
|
of the current request, such as the user's dealer type, the unit slug, and the
|
|
current localized date. Typically used for redirecting to appropriate dashboards
|
|
with monthly data filtered by either entity or unit.
|
|
|
|
:ivar request: The HTTP request object associated with the current view.
|
|
:type request: HttpRequest
|
|
"""
|
|
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
loc_date = get_localdate()
|
|
dealer = get_user_type(self.request)
|
|
unit_slug = self.get_unit_slug()
|
|
if unit_slug:
|
|
return reverse(
|
|
"unit-dashboard-month",
|
|
kwargs={
|
|
"entity_slug": dealer.entity.slug,
|
|
"unit_slug": unit_slug,
|
|
"year": loc_date.year,
|
|
"month": loc_date.month,
|
|
},
|
|
)
|
|
return reverse(
|
|
"entity-dashboard-month",
|
|
kwargs={
|
|
"entity_slug": dealer.entity.slug,
|
|
"year": loc_date.year,
|
|
"month": loc_date.month,
|
|
},
|
|
)
|
|
|
|
|
|
class EntityModelDetailBaseViewBase(
|
|
EntityModelDetailBaseView,
|
|
):
|
|
"""
|
|
Represents a base view that extends functionality for displaying detailed
|
|
dashboard information about an entity model. Handles context population for
|
|
dynamic elements like charts and titles specific to the entity or an optional
|
|
unit.
|
|
|
|
This class is designed as part of a Django application, utilizing mixins for
|
|
enhanced security and inheriting a basic entity model detail view. It is
|
|
customized to render a specific dashboard template and manage context data
|
|
for charts and other display elements relevant to the entity.
|
|
|
|
:ivar template_name: Path to the HTML template used for rendering the
|
|
dashboard.
|
|
:type template_name: str
|
|
"""
|
|
|
|
template_name = "ledger/reports/dashboard.html"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
dealer = get_user_type(self.request)
|
|
entity_model: EntityModel = dealer.entity
|
|
context["page_title"] = entity_model.name
|
|
context["header_title"] = entity_model.name
|
|
context["header_subtitle"] = _("Dashboard")
|
|
context["header_subtitle_icon"] = "mdi:monitor-dashboard"
|
|
|
|
unit_slug = context.get("unit_slug", self.get_unit_slug())
|
|
KWARGS = dict(entity_slug=self.kwargs["entity_slug"])
|
|
|
|
if unit_slug:
|
|
KWARGS["unit_slug"] = unit_slug
|
|
|
|
url_pointer = "entity" if not unit_slug else "unit"
|
|
context["pnl_chart_id"] = f"djl-entity-pnl-chart-{randint(10000, 99999)}"
|
|
context["pnl_chart_endpoint"] = reverse(
|
|
f"django_ledger:{url_pointer}-json-pnl", kwargs=KWARGS
|
|
)
|
|
context["payables_chart_id"] = (
|
|
f"djl-entity-payables-chart-{randint(10000, 99999)}"
|
|
)
|
|
context["payables_chart_endpoint"] = reverse(
|
|
f"django_ledger:{url_pointer}-json-net-payables", kwargs=KWARGS
|
|
)
|
|
context["receivables_chart_id"] = (
|
|
f"djl-entity-receivables-chart-{randint(10000, 99999)}"
|
|
)
|
|
context["receivables_chart_endpoint"] = reverse(
|
|
f"django_ledger:{url_pointer}-json-net-receivables", kwargs=KWARGS
|
|
)
|
|
|
|
return context
|
|
|
|
|
|
class FiscalYearEntityModelDashboardView(
|
|
EntityModelDetailBaseViewBase,
|
|
):
|
|
"""
|
|
Represents a dashboard view for fiscal year entity models.
|
|
|
|
This class serves as a view to display details related to fiscal year
|
|
entity models while enforcing permissions and login requirements.
|
|
It integrates security features and provides functionalities specific
|
|
to managing fiscal year entity models within the dashboard.
|
|
|
|
:ivar permission_required: List of permissions required to access the view.
|
|
:type permission_required: list
|
|
"""
|
|
|
|
permission_required = ["django_ledger.view_ledgermodel"]
|
|
|
|
def get_login_url(self):
|
|
return reverse("account_login")
|
|
|
|
|
|
class QuarterlyEntityDashboardView(
|
|
FiscalYearEntityModelDashboardView,
|
|
QuarterlyReportMixIn,
|
|
):
|
|
"""
|
|
Represents a dashboard view for quarterly entities.
|
|
|
|
This class integrates the functionalities of `FiscalYearEntityModelDashboardView`,
|
|
`QuarterlyReportMixIn`, and `DjangoLedgerSecurityMixIn` to provide a dashboard
|
|
view that displays and organizes quarterly financial metrics and reports for a
|
|
specific fiscal year entity. It facilitates secure access to financial data and
|
|
provides interfaces to analyze quarterly reports.
|
|
|
|
:ivar quarters: A list of quarters included in the dashboard view.
|
|
:type quarters: list
|
|
:ivar entity: The entity for which the dashboard view is generated.
|
|
:type entity: object
|
|
:ivar fiscal_year: The fiscal year associated with the dashboard.
|
|
:type fiscal_year: int
|
|
:ivar reporting_data: A collection of data specifically prepared for
|
|
quarterly reporting.
|
|
:type reporting_data: dict
|
|
"""
|
|
|
|
|
|
class MonthlyEntityDashboardView(
|
|
FiscalYearEntityModelDashboardView,
|
|
MonthlyReportMixIn,
|
|
):
|
|
"""
|
|
Represents a dashboard view for a specific entity's monthly report.
|
|
|
|
This class combines functionalities from the base dashboard view for
|
|
fiscal year entities, as well as specific monthly report and security
|
|
features, to provide a comprehensive dashboard handler. It is designed
|
|
to work within the Django Ledger system, facilitating secured and
|
|
contextual data display tailored for monthly reporting of financial
|
|
entities.
|
|
|
|
:ivar fiscal_year: Represents the fiscal year associated with the
|
|
dashboard view.
|
|
:type fiscal_year: int
|
|
:ivar entity_id: The unique identifier for the financial entity
|
|
being reported on in the dashboard.
|
|
:type entity_id: int
|
|
:ivar report_month: The specific month for which the report is
|
|
generated and displayed.
|
|
:type report_month: str
|
|
:ivar user_permissions: Permissions associated with the currently
|
|
authenticated user to enforce security on
|
|
sensitive actions or data.
|
|
:type user_permissions: list
|
|
"""
|
|
|
|
|
|
class DateEntityDashboardView(
|
|
FiscalYearEntityModelDashboardView,
|
|
DateReportMixIn,
|
|
):
|
|
"""
|
|
Represents a dashboard view for date-based entity data visualization.
|
|
|
|
This class integrates date-based information into the dashboard view,
|
|
extending the functionality of `FiscalYearEntityModelDashboardView`.
|
|
It utilizes mixins to incorporate report generation and security features
|
|
specific to the Django Ledger framework.
|
|
|
|
:ivar fiscal_year: The fiscal year associated with the dashboard data.
|
|
:type fiscal_year: FiscalYear
|
|
:ivar date_report_data: Data pertaining to the date-specific reports displayed.
|
|
:type date_report_data: dict
|
|
:ivar user_permissions: The security permissions granted to the user for viewing
|
|
or interacting with the dashboard.
|
|
:type user_permissions: set
|
|
"""
|
|
|
|
|
|
class PayableNetAPIView(EntityUnitMixIn, View):
|
|
"""
|
|
Handles the retrieval of net payable data for authenticated users.
|
|
|
|
The PayableNetAPIView is designed to process GET requests for retrieving
|
|
a summary of unpaid bills, specifically for authenticated users associated
|
|
with an entity. This view ensures that only authorized users, based on their
|
|
authentication state and type, can access this sensitive financial data. It
|
|
leverages the DjangoLedgerSecurityMixIn and EntityUnitMixIn for security
|
|
enhancements and entity-specific processing.
|
|
|
|
:ivar http_method_names: HTTP methods supported by this view.
|
|
:type http_method_names: list[str]
|
|
"""
|
|
|
|
http_method_names = ["get"]
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
if request.user.is_authenticated:
|
|
dealer = get_user_type(request)
|
|
bill_qs = BillModel.objects.for_entity(
|
|
entity_slug=dealer.entity.slug,
|
|
user_model=dealer.entity.admin,
|
|
).unpaid()
|
|
|
|
net_summary = accruable_net_summary(bill_qs)
|
|
net_payables = {"net_payable_data": net_summary}
|
|
|
|
return JsonResponse({"results": net_payables})
|
|
|
|
return JsonResponse({"message": _("Unauthorized")}, status=401)
|
|
|
|
|
|
class ReceivableNetAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View):
|
|
"""
|
|
Handles the retrieval of net receivables summary for authenticated users.
|
|
|
|
This view is part of a ledger system and is designed to provide a net summary
|
|
of unpaid invoices for an authenticated user. If the user is not authenticated,
|
|
the view will return an unauthorized response. The view supports only GET
|
|
requests.
|
|
|
|
Class Attributes:
|
|
:ivar http_method_names: Restricts the allowed HTTP methods for this view.
|
|
:type http_method_names: list[str]
|
|
"""
|
|
|
|
http_method_names = ["get"]
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
if request.user.is_authenticated:
|
|
dealer = get_user_type(request)
|
|
invoice_qs = InvoiceModel.objects.for_entity(
|
|
entity_slug=dealer.entity.slug,
|
|
user_model=dealer.entity.admin,
|
|
).unpaid()
|
|
|
|
net_summary = accruable_net_summary(invoice_qs)
|
|
|
|
net_receivable = {"net_receivable_data": net_summary}
|
|
|
|
return JsonResponse({"results": net_receivable})
|
|
|
|
return JsonResponse({"message": _("Unauthorized")}, status=401)
|
|
|
|
|
|
class PnLAPIView(DjangoLedgerSecurityMixIn, EntityUnitMixIn, View):
|
|
"""
|
|
APIView for handling Profit and Loss (PnL) data retrieval.
|
|
|
|
This class provides an endpoint to handle the retrieval of Profit and Loss data
|
|
for an entity. It uses authentication to validate the user's access and retrieves
|
|
PnL data based on the user's entity and optional query parameters such as dates.
|
|
The retrieved data is organized and returned in a JSON format.
|
|
|
|
:ivar http_method_names: Restricts HTTP methods that can be used with this view.
|
|
:type http_method_names: list[str]
|
|
"""
|
|
|
|
http_method_names = ["get"]
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
if request.user.is_authenticated:
|
|
dealer = get_user_type(request)
|
|
entity = EntityModel.objects.for_user(user_model=dealer.entity.admin).get(
|
|
slug__exact=dealer.entity.slug
|
|
)
|
|
|
|
unit_slug = self.get_unit_slug()
|
|
|
|
io_digest = entity.digest(
|
|
user_model=self.request.user,
|
|
unit_slug=unit_slug,
|
|
equity_only=True,
|
|
signs=False,
|
|
by_period=True,
|
|
process_groups=True,
|
|
from_date=self.request.GET.get("fromDate"),
|
|
to_date=self.request.GET.get("toDate"),
|
|
# todo: For PnL to display proper period values must not use closing entries.
|
|
use_closing_entries=False,
|
|
)
|
|
|
|
io_data = io_digest.get_io_data()
|
|
group_balance_by_period = io_data["group_balance_by_period"]
|
|
group_balance_by_period = dict(
|
|
sorted((k, v) for k, v in group_balance_by_period.items())
|
|
)
|
|
|
|
entity_data = {
|
|
f"{month_name[k[1]]} {k[0]}": {d: float(f) for d, f in v.items()}
|
|
for k, v in group_balance_by_period.items()
|
|
}
|
|
|
|
entity_pnl = {
|
|
"entity_slug": entity.slug,
|
|
"entity_name": entity.name,
|
|
"pnl_data": entity_data,
|
|
}
|
|
|
|
return JsonResponse({"results": entity_pnl})
|
|
|
|
return JsonResponse({"message": _("Unauthorized")}, status=401)
|
|
|
|
|
|
# class EmployeeCalendarView(LoginRequiredMixin, ListView):
|
|
# """
|
|
# Provides a view for displaying the employee's calendar in a list format.
|
|
|
|
# Displays a list of appointments for logged-in users. This view ensures that
|
|
# only appointments relevant to the logged-in user as a dealer or staff member
|
|
# are displayed. Supports search functionality to filter displayed appointments
|
|
# based on provided query parameters.
|
|
|
|
# :ivar template_name: Path to the HTML template used to render the view.
|
|
# :type template_name: str
|
|
# :ivar model: Model object to interact with appointments data.
|
|
# :type model: Appointment
|
|
# :ivar context_object_name: Name of the context variable that contains the
|
|
# list of appointments in the template.
|
|
# :type context_object_name: str
|
|
# """
|
|
|
|
# template_name = "crm/employee_calendar.html"
|
|
# model = Appointment
|
|
# context_object_name = "appointments"
|
|
|
|
# def get_queryset(self):
|
|
# query = self.request.GET.get("q")
|
|
# staff = getattr(self.request, "staff", None)
|
|
# if staff:
|
|
# appointments = Appointment.objects.filter(
|
|
# appointment_request__staff_member=staff,
|
|
# ppointment_request__date__gt=timezone.now(),
|
|
# )
|
|
# appointments = Appointment.objects.filter(
|
|
# appointment_request__date__gt=timezone.now()
|
|
# )
|
|
# return apply_search_filters(appointments, query)
|
|
|
|
|
|
def apply_search_filters(queryset, query):
|
|
"""
|
|
Apply search filters to a queryset based on a query string.
|
|
|
|
This function filters the provided queryset by applying a Q object for all
|
|
CharField, TextField, or EmailField fields in the model. It checks if the
|
|
query exists within these fields using case-insensitive containment (icontains).
|
|
If the query string is empty or None, the original queryset is returned
|
|
without any modifications.
|
|
|
|
:param queryset: The initial queryset to apply the search filters on.
|
|
:param query: The search string to match against the model fields. If None
|
|
or an empty string, no filtering is applied.
|
|
:return: A filtered queryset that satisfies the search condition.
|
|
"""
|
|
if not query:
|
|
return queryset
|
|
|
|
search_filters = Q()
|
|
model = queryset.model
|
|
|
|
for field in model._meta.get_fields():
|
|
if hasattr(field, "attname") and field.get_internal_type() in [
|
|
"CharField",
|
|
"TextField",
|
|
"EmailField",
|
|
]:
|
|
search_filters |= Q(**{f"{field.name}__icontains": query})
|
|
return queryset.filter(search_filters).distinct()
|
|
|
|
|
|
class CarListViewTable(LoginRequiredMixin, ExportMixin, SingleTableView):
|
|
"""
|
|
Displays a table view of cars for a user with proper permissions.
|
|
|
|
This class-based view leverages functionality for login-required access, data
|
|
exporting, and integration with a single table representation. It is designed
|
|
to fetch and display a list of car objects related to a specific dealer in
|
|
a tabular format. The view also renders a predefined template to display
|
|
the data appropriately.
|
|
|
|
:ivar model: Specifies the model for the table view (Car).
|
|
:type model: models.Car
|
|
:ivar table_class: Represents the table class to be used for rendering
|
|
the data in tabular format.
|
|
:type table_class: tables.CarTable
|
|
:ivar template_name: Defines the template to be used for rendering the view.
|
|
:type template_name: str
|
|
"""
|
|
|
|
model = models.Car
|
|
table_class = tables.CarTable
|
|
template_name = "inventory/car_list_table.html"
|
|
|
|
def get_queryset(self):
|
|
dealer = get_user_type(self.request)
|
|
return models.Car.objects.select_related(
|
|
"finances", "colors__exterior", "colors__interior"
|
|
).filter(dealer=dealer)
|
|
|
|
|
|
@login_required
|
|
def DealerSettingsView(request, slug):
|
|
"""
|
|
Handles dealer settings view where dealers can update their financial and
|
|
payment account settings. This view ensures validation and reassigns form
|
|
fields dynamically based on the dealer's account roles.
|
|
|
|
:param request: The HTTP request object received from the client. This parameter
|
|
contains data including HTTP headers, session, and POST data if applicable.
|
|
:type request: HttpRequest
|
|
:param pk: Primary key representing the dealer for whom the settings are being
|
|
retrieved or modified. This identifier is used to fetch or update dealer-
|
|
specific settings from the database.
|
|
:type pk: int
|
|
:return: An HTTP response rendering the dealer settings form or redirecting
|
|
to the dealer detail view after successful form submission.
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer_setting = get_object_or_404(models.DealerSettings, dealer__slug=slug)
|
|
dealer = get_user_type(request)
|
|
if request.method == "POST":
|
|
form = forms.DealerSettingsForm(request.POST, instance=dealer_setting)
|
|
if form.is_valid():
|
|
instance = form.save(commit=False)
|
|
instance.dealer = dealer
|
|
instance.save()
|
|
messages.success(request, _("Settings updated"))
|
|
return redirect("dealer_detail", slug=dealer.slug)
|
|
else:
|
|
print(form.errors)
|
|
form = forms.DealerSettingsForm(instance=dealer_setting, initial={"dealer": dealer})
|
|
form.fields[
|
|
"invoice_cash_account"
|
|
].queryset = dealer.entity.get_default_coa_accounts().filter(
|
|
role=roles.ASSET_CA_CASH
|
|
)
|
|
form.fields[
|
|
"invoice_prepaid_account"
|
|
].queryset = dealer.entity.get_default_coa_accounts().filter(
|
|
role=roles.ASSET_CA_RECEIVABLES
|
|
)
|
|
form.fields[
|
|
"invoice_unearned_account"
|
|
].queryset = dealer.entity.get_default_coa_accounts().filter(
|
|
role=roles.LIABILITY_CL_DEFERRED_REVENUE
|
|
)
|
|
form.fields[
|
|
"invoice_tax_payable_account"
|
|
].queryset = dealer.entity.get_default_coa_accounts().filter(
|
|
role=roles.LIABILITY_CL_TAXES_PAYABLE
|
|
)
|
|
form.fields[
|
|
"invoice_vehicle_sale_account"
|
|
].queryset = dealer.entity.get_default_coa_accounts().filter(
|
|
role=roles.INCOME_OPERATIONAL
|
|
)
|
|
form.fields[
|
|
"invoice_cost_of_good_sold_account"
|
|
].queryset = dealer.entity.get_default_coa_accounts().filter(role=roles.COGS)
|
|
form.fields[
|
|
"invoice_inventory_account"
|
|
].queryset = dealer.entity.get_default_coa_accounts().filter(
|
|
role=roles.ASSET_CA_INVENTORY
|
|
)
|
|
|
|
form.fields[
|
|
"bill_cash_account"
|
|
].queryset = dealer.entity.get_default_coa_accounts().filter(
|
|
role=roles.ASSET_CA_CASH
|
|
)
|
|
form.fields[
|
|
"bill_prepaid_account"
|
|
].queryset = dealer.entity.get_default_coa_accounts().filter(
|
|
role=roles.ASSET_CA_PREPAID
|
|
)
|
|
form.fields[
|
|
"bill_unearned_account"
|
|
].queryset = dealer.entity.get_default_coa_accounts().filter(
|
|
role=roles.LIABILITY_CL_ACC_PAYABLE
|
|
)
|
|
return render(request, "account/user_settings.html", {"form": form})
|
|
|
|
|
|
@login_required
|
|
def schedule_cancel(request, dealer_slug, pk):
|
|
"""
|
|
Cancel a schedule by updating its status to "Canceled". The function is protected
|
|
by a login requirement, ensuring only authenticated users can execute it. It
|
|
retrieves the schedule object by its primary key, updates its status, saves the
|
|
changes, and returns an HTTP response with status code 200.
|
|
|
|
:param request: The HTTP request object representing the user's request.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key of the schedule to be canceled.
|
|
:type pk: int
|
|
:return: An HTTP response object with a 200 status code upon successful execution.
|
|
:rtype: HttpResponse
|
|
"""
|
|
get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
schedule = get_object_or_404(models.Schedule, pk=pk)
|
|
schedule.status = "Canceled"
|
|
schedule.save()
|
|
response = HttpResponse()
|
|
response.status_code = 200
|
|
return response
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.change_dealer", raise_exception=True)
|
|
def assign_car_makes(request, dealer_slug):
|
|
"""
|
|
Assigns car makes to a dealer.
|
|
|
|
This function handles both the display and processing of a form that allows
|
|
a dealer to assign or modify their associated car makes. If the request
|
|
method is POST, it validates and saves the submitted form data. If the
|
|
method is not POST, it displays a form prefilled with the existing car makes
|
|
associated with the dealer.
|
|
|
|
:param request: The HTTP request object containing information about the
|
|
current request.
|
|
:type request: HttpRequest
|
|
:return: A rendered HTML response for GET requests, or a redirect to the
|
|
dealer detail page after successful form submission.
|
|
:rtype: HttpResponse
|
|
"""
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
if request.method == "POST":
|
|
form = forms.DealersMakeForm(request.POST, dealer=dealer)
|
|
if form.is_valid():
|
|
makes = form.cleaned_data["car_makes"]
|
|
# create_accounts_for_make(dealer, makes)
|
|
form.save()
|
|
return redirect("dealer_detail", slug=dealer.slug)
|
|
else:
|
|
print(form.errors)
|
|
else:
|
|
# Pre-fill the form with existing selections
|
|
existing_car_makes = models.DealersMake.objects.filter(
|
|
dealer=dealer
|
|
).values_list("car_make", flat=True)
|
|
form = forms.DealersMakeForm(
|
|
initial={"car_makes": existing_car_makes}, dealer=dealer
|
|
)
|
|
for choice in form.fields["car_makes"].choices:
|
|
print(choice[0].instance)
|
|
break
|
|
return render(request, "dealers/assign_car_makes.html", {"form": form})
|
|
|
|
|
|
class LedgerModelListView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, ListView, ArchiveIndexView
|
|
):
|
|
"""
|
|
Provides a view for listing ledger entries in the system.
|
|
|
|
This class-based view combines the functionality of LoginRequiredMixin,
|
|
ListView, and ArchiveIndexView to display a filtered and ordered
|
|
list of LedgerModel entries based on the user's type and entity. It
|
|
also allows toggling between different subsets of data, such as
|
|
all entries, current entries, or visible entries.
|
|
|
|
:ivar model: The model that this view displays.
|
|
:type model: type[LedgerModel]
|
|
:ivar context_object_name: The name of the context variable containing the list of LedgerModel instances.
|
|
:type context_object_name: str
|
|
:ivar template_name: The template used to render this view.
|
|
:type template_name: str
|
|
:ivar date_field: The date field used for ordering and archive functionality.
|
|
:type date_field: str
|
|
:ivar ordering: Default ordering for the queryset.
|
|
:type ordering: str
|
|
:ivar show_all: Determines whether all ledger entries should be shown.
|
|
:type show_all: bool
|
|
:ivar show_current: Determines whether only current ledger entries should be shown.
|
|
:type show_current: bool
|
|
:ivar show_visible: Determines whether only visible ledger entries should be shown.
|
|
:type show_visible: bool
|
|
:ivar allow_empty: Allows rendering of the page even if the queryset is empty.
|
|
:type allow_empty: bool
|
|
"""
|
|
|
|
model = LedgerModel
|
|
context_object_name = "ledgers"
|
|
template_name = "ledger/ledger/ledger_list.html"
|
|
permission_required = ["django_ledger.view_ledgermodel"]
|
|
date_field = "created"
|
|
ordering = "-created"
|
|
show_all = False
|
|
show_current = False
|
|
show_visible = False
|
|
allow_empty = True
|
|
paginate_by = 30
|
|
|
|
def get_queryset(self):
|
|
qs = super().get_queryset()
|
|
qs = qs.filter(entity=self.request.entity)
|
|
qs = qs.select_related("billmodel", "invoicemodel")
|
|
qs = qs.order_by("-created")
|
|
if self.show_all:
|
|
return qs
|
|
if self.show_current:
|
|
qs = qs.current()
|
|
if self.show_visible:
|
|
qs = qs.visible()
|
|
return qs
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["entity_slug"] = self.request.dealer.entity.slug
|
|
return context
|
|
|
|
|
|
class LedgerModelDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
"""
|
|
This class provides a detailed view of a specific ledger entry.
|
|
|
|
The LedgerModelDetailView is implemented as a Django DetailView, enabling it to
|
|
retrieve and display detailed information about a single ledger instance.
|
|
It ensures that only authenticated users have access to this view by using the
|
|
LoginRequiredMixin.
|
|
|
|
:ivar model: The model associated with this view, representing ledger entries.
|
|
:type model: type[LedgerModel]
|
|
:ivar context_object_name: The name of the context variable available in the
|
|
template for the detailed ledger.
|
|
:type context_object_name: str
|
|
:ivar template_name: The path to the template used to render the detailed ledger view.
|
|
:type template_name: str
|
|
"""
|
|
|
|
model = LedgerModel
|
|
context_object_name = "ledger"
|
|
template_name = "ledger/ledger/ledger_detail.html"
|
|
permission_required = "django_ledger.view_ledgermodel"
|
|
|
|
|
|
class LedgerModelCreateView(LedgerModelCreateViewBase, SuccessMessageMixin):
|
|
"""
|
|
Handles the creation of LedgerModel entities.
|
|
|
|
This class provides the logic to manage the creation process of
|
|
LedgerModel instances. It determines the appropriate form for creating
|
|
a LedgerModel, validates the submitted form, and redirects the user
|
|
upon success. The view is specific to a particular user type (dealer).
|
|
|
|
:ivar template_name: Specifies the path to the template used for the
|
|
form rendering.
|
|
:type template_name: str
|
|
"""
|
|
|
|
template_name = "ledger/ledger/ledger_form.html"
|
|
permission_required = ["django_ledger.add_ledgermodel"]
|
|
success_message = _("Ledger created successfully")
|
|
|
|
def get_form(self, form_class=None):
|
|
return LedgerModelCreateForm(
|
|
entity_slug=self.request.dealer.entity.slug,
|
|
user_model=self.request.entity.admin,
|
|
**self.get_form_kwargs(),
|
|
)
|
|
|
|
def form_valid(self, form):
|
|
form.fields["entity"] = self.request.dealer.entity
|
|
return super().form_valid(form)
|
|
|
|
def get_success_url(self):
|
|
messages.success(self.request, self.success_message)
|
|
return reverse(
|
|
"ledger_list",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"entity_slug": self.kwargs["entity_slug"],
|
|
},
|
|
)
|
|
|
|
|
|
class LedgerModelModelActionView(LedgerModelModelActionViewBase):
|
|
"""
|
|
Represents a view for handling actions related to the Ledger model.
|
|
|
|
This class extends the LedgerModelModelActionViewBase and is responsible for
|
|
providing functionality to determine the redirection URL after performing
|
|
specific actions on a Ledger model instance.
|
|
|
|
:ivar template_name: Name of the template used for rendering the view.
|
|
:type template_name: str
|
|
:ivar model: The model associated with this view.
|
|
:type model: type[Model]
|
|
"""
|
|
|
|
def get_redirect_url(self, *args, **kwargs):
|
|
return reverse(
|
|
"ledger_list",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"entity_slug": self.kwargs["entity_slug"],
|
|
},
|
|
)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.delete_ledgermodel", raise_exception=True)
|
|
def LedgerModelDeleteView(request, dealer_slug, entity_slug, ledger_pk):
|
|
ledger = LedgerModel.objects.filter(pk=ledger_pk).first()
|
|
if request.method == "POST":
|
|
ledger.delete()
|
|
messages.success(request, _("Ledger deleted successfully"))
|
|
return redirect("ledger_list", dealer_slug=dealer_slug, entity_slug=entity_slug)
|
|
return render(request, "ledger/ledger/ledger_delete.html", {"ledger_model": ledger})
|
|
|
|
|
|
# class LedgerModelDeleteView(DeleteView, SuccessMessageMixin):
|
|
# """
|
|
# Handles the deletion of a Ledger model instance.
|
|
|
|
# Provides functionality for rendering a confirmation template and deleting a
|
|
# ledger instance from the system. Extends functionality for managing success
|
|
# messages and redirections upon successful deletion.
|
|
|
|
# :ivar template_name: Path to the template used for rendering the delete
|
|
# confirmation view.
|
|
# :type template_name: str
|
|
# :ivar success_message: Success message displayed upon successful deletion
|
|
# of the ledger instance.
|
|
# :type success_message: str
|
|
# """
|
|
|
|
# template_name = "ledger/ledger/ledger_delete.html"
|
|
# pk_url_kwarg = 'ledger_pk'
|
|
# context_object_name = 'ledger_model'
|
|
|
|
# success_message = _("Ledger deleted successfully")
|
|
# permission_required = ["django_ledger.delete_ledgermodel"]
|
|
|
|
# def get_success_url(self):
|
|
# return reverse(
|
|
# "ledger_list",
|
|
# kwargs={
|
|
# "dealer_slug": self.kwargs["dealer_slug"],
|
|
# "entity_slug": self.kwargs["entity_slug"],
|
|
# },
|
|
# )
|
|
|
|
|
|
class JournalEntryListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
"""
|
|
Represents a view to list all journal entries for a specific ledger.
|
|
|
|
This class inherits from Django's `ListView` and `LoginRequiredMixin` to provide
|
|
a secure and paginated list of journal entries associated with a specific ledger.
|
|
It ensures the user is authenticated before access to the journal entries is
|
|
granted. The view customizes the queryset and context data to include only
|
|
entries tied to the given ledger specified by its primary key.
|
|
|
|
:ivar model: The model associated with the view.
|
|
:type model: JournalEntryModel
|
|
:ivar context_object_name: The name of the context variable which will hold the
|
|
list of journal entries in the template.
|
|
:type context_object_name: str
|
|
:ivar template_name: The path to the HTML template that renders the list view.
|
|
:type template_name: str
|
|
"""
|
|
|
|
model = JournalEntryModel
|
|
context_object_name = "journal_entries"
|
|
template_name = "ledger/journal_entry/journal_entry_list.html"
|
|
permission_required = ["django_ledger.view_journalentrymodel"]
|
|
ordering = ["-timestamp"]
|
|
|
|
def get_queryset(self):
|
|
qs = super().get_queryset()
|
|
ledger = LedgerModel.objects.filter(pk=self.kwargs["pk"]).first()
|
|
qs = qs.filter(ledger=ledger)
|
|
return qs
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["ledger"] = LedgerModel.objects.filter(pk=self.kwargs["pk"]).first()
|
|
return context
|
|
|
|
|
|
class JournalEntryCreateView(
|
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView
|
|
):
|
|
"""
|
|
View for creating a new journal entry.
|
|
|
|
This class-based view allows authenticated users to create new journal entries
|
|
associated with a specific ledger. It includes functionality for handling
|
|
context data, form initialization, and redirection upon successful submission
|
|
of the form.
|
|
|
|
:ivar model: The model used for creating the journal entry.
|
|
:type model: JournalEntryModel
|
|
:ivar template_name: The template used to render the form.
|
|
:type template_name: str
|
|
:ivar form_class: The form class used to create the journal entry.
|
|
:type form_class: forms.JournalEntryModelCreateForm
|
|
:ivar ledger_model: The ledger model associated with this journal entry.
|
|
:type ledger_model: LedgerModel or None
|
|
:ivar success_message: The message displayed upon successfully creating a journal entry.
|
|
:type success_message: str
|
|
"""
|
|
|
|
model = JournalEntryModel
|
|
template_name = "ledger/journal_entry/journal_entry_form.html"
|
|
permission_required = ["django_ledger.add_journalentrymodel"]
|
|
form_class = forms.JournalEntryModelCreateForm
|
|
ledger_model = None
|
|
success_message = _("Journal Entry created successfully")
|
|
|
|
def get_form(self, form_class=None):
|
|
dealer = get_user_type(self.request)
|
|
ledger = LedgerModel.objects.filter(pk=self.kwargs["pk"]).first()
|
|
form = forms.JournalEntryModelCreateForm(
|
|
entity_model=dealer.entity, ledger_model=ledger, **self.get_form_kwargs()
|
|
)
|
|
form.fields.pop("entity_unit")
|
|
return form
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["ledger"] = LedgerModel.objects.filter(pk=self.kwargs["pk"]).first()
|
|
return context
|
|
|
|
def get_success_url(self):
|
|
ledger = LedgerModel.objects.filter(pk=self.kwargs["pk"]).first()
|
|
return reverse(
|
|
"journalentry_list",
|
|
kwargs={"dealer_slug": self.kwargs["dealer_slug"], "pk": ledger.pk},
|
|
)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.delete_journalentrymodel", raise_exception=True)
|
|
def JournalEntryDeleteView(request, dealer_slug, pk):
|
|
"""
|
|
Handles the deletion of a specific journal entry. This view facilitates
|
|
the deletion of a journal entry identified by its primary key (pk). If the
|
|
deletion is successful, the user is redirected to the list of journal entries
|
|
for the associated ledger. If the deletion cannot proceed, an error message
|
|
is displayed, and the user is redirected back to the journal entry list.
|
|
|
|
:param request: The HTTP request object.
|
|
:type request: HttpRequest
|
|
:param pk: The primary key (pk) of the journal entry to be deleted.
|
|
:type pk: int
|
|
:return: A rendered HTML response for GET requests or a redirect upon
|
|
successful/failed deletion.
|
|
:rtype: HttpResponse
|
|
"""
|
|
journal_entry = get_object_or_404(JournalEntryModel, pk=pk)
|
|
if request.method == "POST":
|
|
ledger = journal_entry.ledger
|
|
if not journal_entry.can_delete():
|
|
messages.error(request, _("Journal Entry cannot be deleted"))
|
|
return redirect("journalentry_list", dealer_slug=dealer_slug, pk=ledger.pk)
|
|
journal_entry.delete()
|
|
messages.success(request, "Journal Entry deleted")
|
|
return redirect("journalentry_list", dealer_slug=dealer_slug, pk=ledger.pk)
|
|
return render(
|
|
request,
|
|
"ledger/journal_entry/journal_entry_delete.html",
|
|
{"journal_entry": journal_entry},
|
|
)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.view_transactionmodel", raise_exception=True)
|
|
def JournalEntryTransactionsView(request, dealer_slug, pk):
|
|
"""
|
|
Handles the retrieval and display of journal entry transactions for a specific journal
|
|
entry instance. It retrieves the journal entry and its associated transactions, ordering
|
|
the transactions by account code. The data is then rendered using the specified template.
|
|
|
|
:param request: The HTTP request object.
|
|
:type request: django.http.HttpRequest
|
|
:param pk: The primary key of the journal entry to be retrieved.
|
|
:type pk: int
|
|
:return: An HTTP response with the rendered template, including the journal entry and
|
|
its transactions.
|
|
:rtype: django.http.HttpResponse
|
|
"""
|
|
get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
journal = JournalEntryModel.objects.filter(pk=pk).first()
|
|
qs = TransactionModel.objects.filter(journal_entry=journal).all()
|
|
transactions = qs.annotate(
|
|
debit_credit_sort_order=Case(
|
|
When(tx_type="debit", then=Value(0)),
|
|
When(tx_type="credit", then=Value(1)),
|
|
output_field=IntegerField(),
|
|
)
|
|
).order_by("debit_credit_sort_order")
|
|
return render(
|
|
request,
|
|
"ledger/journal_entry/journal_entry_transactions.html",
|
|
{"journal_entry": journal, "transactions": transactions},
|
|
)
|
|
|
|
|
|
class JournalEntryModelTXSDetailView(JournalEntryModelTXSDetailViewBase):
|
|
"""
|
|
Represents a detailed view of journal entry transactions in the ledger.
|
|
|
|
This class is used to render the transactions associated with a specific
|
|
journal entry. It extends the base view functionality and is tailored for a
|
|
transaction details page.
|
|
|
|
:ivar template_name: The path to the template used to render the journal
|
|
entry transactions view.
|
|
:type template_name: str
|
|
"""
|
|
|
|
template_name = "ledger/journal_entry/journal_entry_txs.html"
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.change_ledgermodel", raise_exception=True)
|
|
def ledger_lock_all_journals(request, dealer_slug, entity_slug, pk):
|
|
"""
|
|
Locks all journals associated with a specific ledger. If the ledger is already locked,
|
|
it will notify the user through an error message. Otherwise, it initiates the locking of
|
|
related journal entries, locks the ledger itself, and saves the changes to the database.
|
|
After the operation, it redirects the user to the journal entry list associated with the
|
|
ledger.
|
|
|
|
:param request: HttpRequest object representing the current request.
|
|
:type request: HttpRequest
|
|
:param entity_slug: The slug identifier of the entity.
|
|
:type entity_slug: str
|
|
:param pk: The primary key of the ledger to be locked.
|
|
:type pk: int
|
|
:return: HttpResponse redirecting to the journal entry list page of the locked ledger.
|
|
:rtype: HttpResponse
|
|
"""
|
|
get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
ledger = LedgerModel.objects.filter(pk=pk).first()
|
|
if ledger.is_locked():
|
|
messages.error(request, _("Ledger is already locked"))
|
|
return redirect("journalentry_list", dealer_slug=dealer_slug, pk=ledger.pk)
|
|
ledger.lock_journal_entries()
|
|
ledger.lock()
|
|
ledger.save()
|
|
return redirect("journalentry_list", dealer_slug=dealer_slug, pk=ledger.pk)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.change_ledgermodel", raise_exception=True)
|
|
def ledger_unlock_all_journals(request, dealer_slug, entity_slug, pk):
|
|
"""
|
|
Unlocks all journal entries associated with a specific ledger. This function first checks if the
|
|
ledger is locked. If it is already unlocked, it shows an error message and redirects the user
|
|
to the journal entry list page. If the ledger is locked, it unlocks the ledger, saves changes,
|
|
and iterates through all the locked journal entries within the ledger to unlock them as well.
|
|
Afterward, it redirects the user to the journal entry list page.
|
|
|
|
:param request: The HTTP request object containing user session and request metadata.
|
|
:type request: HttpRequest
|
|
:param entity_slug: A unique string slug representing the specific entity or organization context.
|
|
:type entity_slug: str
|
|
:param pk: The primary key of the ledger record to be unlocked.
|
|
:type pk: int
|
|
:return: A redirection to the journal entry list page for the specified ledger.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
ledger = LedgerModel.objects.filter(pk=pk).first()
|
|
if not ledger.is_locked():
|
|
messages.error(request, _("Ledger is already Unlocked"))
|
|
return redirect("journalentry_list", dealer_slug=dealer_slug, pk=ledger.pk)
|
|
|
|
ledger.unlock()
|
|
ledger.save()
|
|
qs = ledger.journal_entries.locked()
|
|
for je in qs:
|
|
je.unlock()
|
|
je.save()
|
|
return redirect("journalentry_list", dealer_slug=dealer_slug, pk=ledger.pk)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.change_ledgermodel", raise_exception=True)
|
|
def ledger_post_all_journals(request, dealer_slug, entity_slug, pk):
|
|
"""
|
|
Posts all journal entries associated with a ledger. This function updates the ledger's
|
|
state to reflect that its journal entries have been posted. If the ledger is already
|
|
posted, an error message is displayed and the user is redirected.
|
|
|
|
:param request: The HTTP request object used for processing the action.
|
|
:type request: HttpRequest
|
|
:param entity_slug: A string representing the specific entity slug for the ledger context.
|
|
:type entity_slug: str
|
|
:param pk: The primary key of the ledger to be posted.
|
|
:type pk: int
|
|
:return: A redirect to the journal entry list view for the specified ledger.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
ledger = LedgerModel.objects.filter(pk=pk).first()
|
|
if ledger.is_posted():
|
|
messages.error(request, _("Ledger is already posted"))
|
|
return redirect("journalentry_list", dealer_slug=dealer_slug, pk=ledger.pk)
|
|
ledger.post_journal_entries()
|
|
ledger.post()
|
|
ledger.save()
|
|
return redirect("journalentry_list", dealer_slug=dealer_slug, pk=ledger.pk)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.change_ledgermodel", raise_exception=True)
|
|
def ledger_unpost_all_journals(request, dealer_slug, entity_slug, pk):
|
|
"""
|
|
Unposts all journal entries for a specified ledger and marks the ledger as unposted.
|
|
|
|
This function identifies a ledger by its primary key (pk) and checks if it is
|
|
already posted. If the ledger is not posted, an error message is displayed.
|
|
If it is posted, the function iterates through its posted journal entries,
|
|
marks them as unposted, and saves the changes. Finally, it marks the ledger itself
|
|
as unposted and saves it.
|
|
|
|
:param request: The HTTP request object from the client.
|
|
:type request: HttpRequest
|
|
:param entity_slug: A slug identifying the entity associated with the ledger.
|
|
:type entity_slug: str
|
|
:param pk: The primary key of the ledger whose journal entries are being unposted.
|
|
:type pk: int
|
|
:return: An HTTP redirect response object directing the client to the journal entry list
|
|
page for the specified ledger.
|
|
:rtype: HttpResponseRedirect
|
|
"""
|
|
get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
ledger = LedgerModel.objects.filter(pk=pk).first()
|
|
if not ledger.is_posted():
|
|
messages.error(request, _("Ledger is already Unposted"))
|
|
return redirect("journalentry_list", dealer_slug=dealer_slug, pk=ledger.pk)
|
|
qs = ledger.journal_entries.posted()
|
|
for je in qs:
|
|
je.mark_as_unposted()
|
|
je.save()
|
|
|
|
ledger.unpost()
|
|
ledger.save()
|
|
return redirect("journalentry_list", dealer_slug=dealer_slug, pk=ledger.pk)
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.change_dealer", raise_exception=True)
|
|
def pricing_page(request, dealer_slug):
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
vat = models.VatRate.objects.filter(dealer=dealer).first()
|
|
now = datetime.now().date() + timedelta(days=15)
|
|
if (
|
|
not hasattr(dealer.user, "userplan")
|
|
or dealer.is_plan_expired
|
|
or dealer.user.userplan.expire <= now
|
|
):
|
|
plan_list = PlanPricing.objects.annotate(
|
|
price_with_tax=Round(F("price") * vat.rate + F("price"), 2)
|
|
).all()
|
|
|
|
form = forms.PaymentPlanForm()
|
|
return render(
|
|
request, "pricing_page.html", {"plan_list": plan_list, "form": form}
|
|
)
|
|
else:
|
|
messages.info(request, _("You already have an plan!!"))
|
|
return redirect("home", dealer_slug=dealer_slug)
|
|
|
|
|
|
# @login_required
|
|
# @permission_required("inventory.change_dealer", raise_exception=True)
|
|
# @require_POST
|
|
# def submit_plan(request, dealer_slug):
|
|
# logger.info(f"dealer slug : {dealer_slug}")
|
|
# if request.method == "GET":
|
|
# logger.info("method is GET, redirecting to pricing page")
|
|
# return redirect("pricing_page", dealer_slug=dealer_slug)
|
|
# dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
# logger.info(f"Selected dealer : {dealer}")
|
|
# selected_plan_id = request.POST.get("selected_plan")
|
|
# logger.info(f"Selected plan id : {selected_plan_id}")
|
|
# pp = PlanPricing.objects.get(pk=selected_plan_id)
|
|
# logger.info(f"Selected plan pricing : {pp}")
|
|
# order = None
|
|
# try:
|
|
# order = Order.objects.create(
|
|
# user=dealer.user,
|
|
# plan=pp.plan,
|
|
# pricing=pp.pricing,
|
|
# amount=pp.price,
|
|
# currency="SA",
|
|
# tax=15,
|
|
# status=1,
|
|
# )
|
|
# logger.info(f"order created {order}")
|
|
# except Exception as e:
|
|
# logger.error(e)
|
|
# if not order:
|
|
# messages.error(request, _("Error creating order"))
|
|
# logger.error("unable to create order")
|
|
# return redirect("pricing_page", dealer_slug=dealer_slug)
|
|
# transaction_url = handle_payment(request, order)
|
|
# logger.info(f"Redirecting to : {transaction_url}")
|
|
# return redirect(transaction_url)
|
|
def submit_plan(request, dealer_slug):
|
|
if request.method == "GET":
|
|
return redirect("pricing_page", dealer_slug=dealer_slug)
|
|
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
selected_plan_id = request.POST.get("selected_plan")
|
|
if not selected_plan_id:
|
|
messages.error(request, _("No plan selected."))
|
|
return redirect("pricing_page", dealer_slug=dealer_slug)
|
|
|
|
# Store plan & dealer info in session for use in callback
|
|
request.session["pending_plan_id"] = selected_plan_id
|
|
request.session["pending_dealer_slug"] = dealer_slug
|
|
|
|
# Initiate payment WITHOUT creating order
|
|
transaction_url, error = handle_payment(request, dealer)
|
|
if not transaction_url:
|
|
messages.error(request, _(f"Payment initiation failed. {error}"))
|
|
return redirect("pricing_page", dealer_slug=dealer_slug)
|
|
|
|
return redirect(transaction_url)
|
|
|
|
|
|
# @login_required
|
|
def payment_callback(request, dealer_slug):
|
|
from django.db import transaction
|
|
|
|
payment_id = request.GET.get("id")
|
|
payment_status = request.GET.get("status")
|
|
message = request.GET.get("message", "")
|
|
|
|
if not payment_id:
|
|
logger.error("Missing payment ID in callback")
|
|
return render(request, "payment_failed.html", {"message": "Invalid request"})
|
|
|
|
logger.info(
|
|
f"Received payment callback for dealer_slug: {dealer_slug}, payment_id: {payment_id}, status: {payment_status}"
|
|
)
|
|
|
|
with transaction.atomic():
|
|
history = (
|
|
models.PaymentHistory.objects
|
|
.select_for_update()
|
|
.filter(transaction_id=payment_id)
|
|
.first()
|
|
)
|
|
|
|
if not history:
|
|
logger.error(f"No PaymentHistory found for transaction_id: {payment_id}")
|
|
return render(
|
|
request, "payment_failed.html", {"message": "Invalid transaction"}
|
|
)
|
|
|
|
if history.status == "paid":
|
|
logger.info("Payment already processed. Redirecting to home.")
|
|
return redirect("home")
|
|
|
|
if history.status == "processing":
|
|
logger.warning(f"Payment {payment_id} is already being processed. Skipping.")
|
|
return redirect("home")
|
|
|
|
if history.status == "failed" and payment_status != "paid":
|
|
logger.warning(f"Payment {payment_id} already failed. Ignoring.")
|
|
return render(request, "payment_failed.html", {"message": message or "Payment failed"})
|
|
|
|
history.status = "processing"
|
|
history.save(update_fields=["status"])
|
|
|
|
if payment_status == "paid":
|
|
logger.info(f"Payment successful for transaction ID {payment_id}. Creating order...")
|
|
|
|
metadata = history.user_data
|
|
if isinstance(metadata, str):
|
|
try:
|
|
metadata = json.loads(metadata)
|
|
except json.JSONDecodeError:
|
|
logger.error(f"Failed to decode metadata JSON: {metadata}")
|
|
metadata = {}
|
|
|
|
plan_pricing_id = metadata.get("plan_pricing_id")
|
|
dealer_slug_from_meta = metadata.get("dealer_slug")
|
|
|
|
if not plan_pricing_id or dealer_slug_from_meta != dealer_slug:
|
|
logger.error("Invalid metadata in payment callback")
|
|
history.status = "failed"
|
|
history.save(update_fields=["status"])
|
|
return render(request, "payment_failed.html", {"message": "Invalid payment data"})
|
|
|
|
try:
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
pp = get_object_or_404(PlanPricing, pk=plan_pricing_id)
|
|
|
|
with transaction.atomic():
|
|
history.refresh_from_db()
|
|
if history.status == "paid":
|
|
logger.info("Payment was already completed by another request. Skipping.")
|
|
return redirect("home")
|
|
|
|
order = Order.objects.create(
|
|
user=dealer.user,
|
|
plan=pp.plan,
|
|
pricing=pp.pricing,
|
|
amount=pp.price,
|
|
currency="SAR",
|
|
tax=15,
|
|
status=Order.STATUS.NEW,
|
|
)
|
|
logger.info(f"Order {order.id} created for user {dealer.user}")
|
|
|
|
billing_info, created = BillingInfo.objects.get_or_create(
|
|
user=dealer.user,
|
|
defaults={
|
|
"tax_number": dealer.vrn,
|
|
"name": dealer.arabic_name,
|
|
"street": dealer.address,
|
|
"zipcode": dealer.entity.zip_code or " ",
|
|
"city": dealer.entity.city or " ",
|
|
"country": dealer.entity.country or " ",
|
|
},
|
|
)
|
|
|
|
# Create UserPlan if missing
|
|
if not hasattr(order.user, "userplan"):
|
|
UserPlan.objects.create(
|
|
user=order.user,
|
|
plan=order.plan,
|
|
)
|
|
logger.info(f"Created new UserPlan for user {order.user} with plan {order.plan}.")
|
|
else:
|
|
logger.info(f"UserPlan already exists for user {order.user}.")
|
|
|
|
order.complete_order()
|
|
|
|
history.status = "paid"
|
|
history.order = order
|
|
history.save(update_fields=["status"])
|
|
|
|
invoice = order.get_invoices().first()
|
|
logger.info(f"Order {order.id} completed successfully.")
|
|
return render(
|
|
request, "payment_success.html", {"order": order, "invoice": invoice}
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.exception(f"Error processing paid payment {payment_id}: {e}")
|
|
# Mark as failed
|
|
history.status = "failed"
|
|
history.save(update_fields=["status"])
|
|
return render(request, "payment_failed.html", {"message": "Payment processing error"})
|
|
|
|
finally:
|
|
try:
|
|
if dealer := getattr(order.user, "dealer", None):
|
|
if not dealer.user.is_active:
|
|
dealer.user.is_active = True
|
|
dealer.user.save()
|
|
for staff in dealer.get_staff():
|
|
if not staff.user.is_active:
|
|
staff.activate_account()
|
|
except Exception as ex:
|
|
logger.warning(f"Failed to activate dealer/staff: {ex}")
|
|
|
|
elif payment_status == "failed":
|
|
logger.warning(f"Payment failed for transaction ID {payment_id}. Message: {message}")
|
|
history.status = "failed"
|
|
history.save(update_fields=["status"])
|
|
return render(request, "payment_failed.html", {"message": message})
|
|
|
|
else:
|
|
logger.warning(f"Unknown payment status: {payment_status}")
|
|
history.status = "failed"
|
|
history.save(update_fields=["status"])
|
|
return render(request, "payment_failed.html", {"message": "Unknown payment status"})
|
|
|
|
# @login_required
|
|
# @permission_required("inventory.change_dealer", raise_exception=True)
|
|
# def payment_callback(request, dealer_slug):
|
|
# message = request.GET.get("message")
|
|
# dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
# payment_id = request.GET.get("id")
|
|
# history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first()
|
|
# payment_status = request.GET.get("status")
|
|
# logger.info(f"Received payment callback for dealer_slug: {dealer_slug}, payment_id: {payment_id}, status: {payment_status}")
|
|
# order = Order.objects.filter(user=dealer.user, status=1).first() # Status 1 = NEW
|
|
# logger.info(f"Retrieved order {order.id} for user {order.user}.")
|
|
# if history.status == "paid":
|
|
# logger.info(f"Payment history already marked as paid for transaction ID {payment_id}. Redirecting to home.")
|
|
# return redirect('home')
|
|
# if payment_status == "paid":
|
|
# logger.info(f"Payment successful for transaction ID {payment_id}. Processing order completion.")
|
|
# billing_info, created = BillingInfo.objects.get_or_create(
|
|
# user=dealer.user,
|
|
# defaults={
|
|
# 'tax_number': dealer.vrn,
|
|
# 'name': dealer.arabic_name,
|
|
# 'street': dealer.address,
|
|
# 'zipcode': dealer.entity.zip_code or " ",
|
|
# 'city': dealer.entity.city or " ",
|
|
# 'country': dealer.entity.country or " ",
|
|
# }
|
|
# )
|
|
# if created:
|
|
# logger.info(f"Created new billing info for user {dealer.user}.")
|
|
# else:
|
|
# logger.debug(f"Billing info already exists for user {dealer.user}.")
|
|
|
|
# if not hasattr(order.user, 'userplan'):
|
|
# logger.info(f"Creating new UserPlan for user {order.user} with plan {order.plan}.")
|
|
# UserPlan.objects.create(
|
|
# user=order.user,
|
|
# plan=order.plan,
|
|
# # expire=datetime.now().date() + timedelta(days=order.get_plan_pricing().pricing.period)
|
|
# )
|
|
# else:
|
|
# logger.info(f"UserPlan already exists for user {order.user}.")
|
|
|
|
# try:
|
|
# logger.info(f"Processing order completion for {order.user} - upgrading to {order.plan}")
|
|
# order.complete_order()
|
|
# history.status = "paid"
|
|
# history.save()
|
|
# logger.info(f"Order {order.id} for user {order.user} completed successfully. Payment history updated.")
|
|
# invoice = order.get_invoices().first()
|
|
# logger.info(f"Redirecting to payment success page with invoice {invoice.id}.")
|
|
# return render(
|
|
# request,
|
|
# "payment_success.html",
|
|
# {"order": order, "invoice": invoice}
|
|
# )
|
|
|
|
# except Exception as e:
|
|
# logger.exception(f"Error completing order {order.id} for user {order.user}: {e}")
|
|
# logger.error(f"Plan activation failed: {str(e)}")
|
|
# history.status = "failed"
|
|
# history.save()
|
|
# logger.info(f"Redirecting to payment failed page with message {message}.")
|
|
# return render(request, "payment_failed.html", {"message": "Plan activation error"})
|
|
# finally:
|
|
# if dealer := getattr(order.user,"dealer", None):
|
|
# logger.info(f"Activating dealer {dealer} and its staff.")
|
|
# if not dealer.user.is_active:
|
|
# logger.info(f"Activating dealer {dealer}.")
|
|
# dealer.user.is_active = True
|
|
# dealer.user.save()
|
|
# for staff in dealer.get_staff():
|
|
# logger.info(f"Activating staff {staff}.")
|
|
# if not staff.user.is_active:
|
|
# staff.activate_account()
|
|
# logger.info(f"Order {order.id} for user {order.user} completed successfully. Payment history updated.")
|
|
# elif payment_status == "failed":
|
|
# logger.warning(f"Payment failed for transaction ID {payment_id}. Message: {message}")
|
|
# history.status = "failed"
|
|
# history.save()
|
|
# logger.info(f"Redirecting to payment failed page with message {message}.")
|
|
# return render(request, "payment_failed.html", {"message": message})
|
|
|
|
# logger.info(f"Redirecting to payment failed page with message {message}.")
|
|
# return render(request, "payment_failed.html", {"message": "Unknown payment status"})
|
|
# def payment_callback(request, dealer_slug):
|
|
# message = request.GET.get("message")
|
|
# dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
# payment_id = request.GET.get("id")
|
|
# history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first()
|
|
# payment_status = request.GET.get("status")
|
|
# order = Order.objects.filter(user=dealer.user, status=1).first()
|
|
|
|
# if payment_status == "paid":
|
|
# billing_info, created = BillingInfo.objects.get_or_create(
|
|
# user=dealer.user,
|
|
# tax_number=dealer.vrn,
|
|
# name=dealer.arabic_name,
|
|
# street=dealer.address,
|
|
# zipcode=dealer.entity.zip_code if dealer.entity.zip_code else " ",
|
|
# city=dealer.entity.city if dealer.entity.city else " ",
|
|
# country=dealer.entity.country if dealer.entity.country else " ",
|
|
# )
|
|
# if created:
|
|
# userplan = UserPlan.objects.create(
|
|
# user=request.user,
|
|
# plan=order.plan,
|
|
# active=True,
|
|
# )
|
|
# userplan.initialize()
|
|
|
|
# order.complete_order()
|
|
# history.status = "paid"
|
|
# history.save()
|
|
# invoice = order.get_invoices().first()
|
|
# return render(
|
|
# request, "payment_success.html", {"order": order, "invoice": invoice}
|
|
# )
|
|
|
|
# elif payment_status == "failed":
|
|
# history.status = "failed"
|
|
# history.save()
|
|
|
|
# return render(request, "payment_failed.html", {"message": message})
|
|
|
|
|
|
@login_required
|
|
async def sse_stream(request):
|
|
import asyncio
|
|
|
|
def event_generator():
|
|
last_id = int(request.GET.get("last_id", 0))
|
|
|
|
# Use async-for over async queryset
|
|
async def fetch_notifications():
|
|
while True:
|
|
# 🔥 Fully async ORM query
|
|
notifications = (
|
|
models.Notification.objects.filter(
|
|
user=request.user, id__gt=last_id, is_read=False
|
|
)
|
|
.order_by("created")
|
|
.values("id", "message", "created")
|
|
)
|
|
|
|
# 🔥 Async iteration over queryset
|
|
async 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 # Update last_id for next loop
|
|
|
|
# Send keep-alive every 3 seconds
|
|
yield ":keep-alive\n\n"
|
|
await asyncio.sleep(3) # Non-blocking sleep
|
|
|
|
return fetch_notifications()
|
|
|
|
return StreamingHttpResponse(
|
|
event_generator(),
|
|
content_type="text/event-stream",
|
|
headers={
|
|
"Cache-Control": "no-cache",
|
|
"Connection": "keep-alive",
|
|
"X-Accel-Buffering": "no",
|
|
},
|
|
)
|
|
|
|
|
|
@login_required
|
|
def fetch_notifications(request):
|
|
notifications = models.Notification.objects.filter(
|
|
user=request.user, is_read=False
|
|
).order_by("-created")[:10] # Get 10 most recent
|
|
|
|
return JsonResponse({"notifications": list(notifications.values())})
|
|
|
|
|
|
@login_required
|
|
def mark_notification_as_read(request, notification_id):
|
|
notification = get_object_or_404(
|
|
models.Notification, id=notification_id, user=request.user
|
|
)
|
|
notification.is_read = True
|
|
notification.save()
|
|
return JsonResponse({"status": "success"})
|
|
|
|
|
|
@login_required
|
|
def mark_all_notifications_as_read(request):
|
|
models.Notification.objects.filter(user=request.user, is_read=False).update(
|
|
is_read=True
|
|
)
|
|
messages.success(request, _("All notifications marked as read."))
|
|
return redirect("notifications_history")
|
|
|
|
|
|
@login_required
|
|
def notifications_history(request):
|
|
models.Notification.objects.filter(user=request.user, is_read=False).update(
|
|
is_read=True
|
|
)
|
|
return JsonResponse({"status": "success"})
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.add_activity", raise_exception=True)
|
|
def add_activity(request, dealer_slug, content_type, slug):
|
|
# Get user information for logging
|
|
user_username = (
|
|
request.user.username if request.user.is_authenticated else "anonymous"
|
|
)
|
|
|
|
# Log the attempt to retrieve the model dynamically
|
|
logger.debug(
|
|
f"User {user_username} attempting to retrieve model "
|
|
f"for content_type '{content_type}' for dealer '{dealer_slug}'."
|
|
)
|
|
try:
|
|
model = apps.get_model(f"inventory.{content_type}")
|
|
except LookupError:
|
|
# --- Log for LookupError (Model not found) ---
|
|
logger.warning(
|
|
f"User {user_username} requested an invalid model content_type: '{content_type}'. "
|
|
f"Raising Http404."
|
|
)
|
|
raise Http404("Model not found")
|
|
|
|
obj = get_object_or_404(model, slug=slug)
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
if request.method == "POST":
|
|
form = forms.ActivityForm(request.POST)
|
|
if form.is_valid():
|
|
activity = form.save(commit=False)
|
|
activity.dealer = dealer
|
|
activity.content_object = obj
|
|
activity.created_by = request.user
|
|
activity.notes = form.cleaned_data["notes"]
|
|
activity.activity_type = form.cleaned_data["activity_type"]
|
|
|
|
activity.save()
|
|
# --- Log for successful activity creation ---
|
|
logger.info(
|
|
f"User {user_username} successfully added activity (Type: {activity.activity_type}) "
|
|
f"for {content_type} ID: {obj.slug} ('{obj.name if hasattr(obj, 'name') else obj}') "
|
|
f"on dealer '{dealer_slug}'. Notes: '{activity.notes[:50]}...'."
|
|
)
|
|
messages.success(request, _("Activity added successfully"))
|
|
else:
|
|
# --- Log for invalid form data ---
|
|
logger.warning(
|
|
f"User {user_username} submitted invalid activity form data "
|
|
f"for {content_type} ID: {obj.slug} (Dealer: {dealer_slug}). Errors: {form.errors.as_json()}"
|
|
)
|
|
messages.error(request, _("Activity form is not valid"))
|
|
return redirect(f"{content_type}_detail", dealer_slug=dealer_slug, slug=slug)
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.add_tasks", raise_exception=True)
|
|
def add_task(request, dealer_slug, content_type, slug):
|
|
# Get user information for logging
|
|
user_username = (
|
|
request.user.username if request.user.is_authenticated else "anonymous"
|
|
)
|
|
# Log the attempt to retrieve the model dynamically
|
|
logger.debug(
|
|
f"User {user_username} attempting to retrieve model "
|
|
f"for content_type '{content_type}' for dealer '{dealer_slug}'."
|
|
)
|
|
try:
|
|
model = apps.get_model(f"inventory.{content_type}")
|
|
except LookupError:
|
|
# --- Single-line log for LookupError (Model not found) ---
|
|
logger.warning(
|
|
f"User {user_username} requested an invalid model content_type: '{content_type}'. "
|
|
f"Raising Http404 for dealer '{dealer_slug}'."
|
|
)
|
|
raise Http404("Model not found")
|
|
|
|
obj = get_object_or_404(model, slug=slug)
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
if request.method == "POST":
|
|
form = forms.StaffTaskForm(request.POST)
|
|
if form.is_valid():
|
|
task = form.save(commit=False)
|
|
task.dealer = dealer
|
|
task.content_object = obj
|
|
task.assigned_to = request.user
|
|
task.created_by = request.user
|
|
task.due_date = form.cleaned_data["due_date"]
|
|
task.save()
|
|
models.Activity.objects.create(
|
|
dealer=dealer,
|
|
content_object=obj,
|
|
activity_type="task",
|
|
created_by=request.user,
|
|
notes="Task Added",
|
|
)
|
|
# --- Log for successful task creation ---
|
|
logger.info(
|
|
f"User {user_username} successfully added task "
|
|
f"(Assigned to: {task.assigned_to.email}) for {content_type} ID: {obj.slug} "
|
|
f"on dealer '{dealer_slug}'. Due: {task.due_date}, Notes: '{task.description}'."
|
|
)
|
|
messages.success(request, _("Task added successfully"))
|
|
else:
|
|
# --- Log for invalid form data ---
|
|
logger.warning(
|
|
f"User {user_username} submitted invalid task form data "
|
|
f"for {content_type} ID: {obj.slug} (Dealer: {dealer_slug}). Errors: {form.errors.as_json()}"
|
|
)
|
|
messages.error(request, _("Task form is not valid"))
|
|
|
|
return redirect(f"{content_type}_detail", dealer_slug=dealer_slug, slug=slug)
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.change_tasks", raise_exception=True)
|
|
def update_task(request, dealer_slug, pk):
|
|
task = get_object_or_404(models.Tasks, pk=pk)
|
|
|
|
if request.method == "POST":
|
|
task.completed = False if task.completed else True
|
|
task.save()
|
|
|
|
# tasks = models.Tasks.objects.filter(content_type__model=content_type, object_id=obj.id)
|
|
|
|
return render(request, "partials/task.html", {"task": task})
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.change_schedule", raise_exception=True)
|
|
def update_schedule(request, dealer_slug, pk):
|
|
task = get_object_or_404(models.Schedule, pk=pk)
|
|
if request.method == "POST":
|
|
task.completed = False if task.completed else True
|
|
task.save()
|
|
print("task")
|
|
|
|
return render(request, "partials/task.html", {"task": task})
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.add_notes", raise_exception=True)
|
|
def add_note(request, dealer_slug, content_type, slug):
|
|
# Get user information for logging
|
|
user_username = (
|
|
request.user.username if request.user.is_authenticated else "anonymous"
|
|
)
|
|
# Log the attempt to retrieve the model dynamically
|
|
logger.debug(
|
|
f"User {user_username} attempting to retrieve model "
|
|
f"for content_type '{content_type}' for dealer '{dealer_slug}'."
|
|
)
|
|
try:
|
|
model = apps.get_model(f"inventory.{content_type}")
|
|
except LookupError:
|
|
# --- Single-line log for LookupError (Model not found) ---
|
|
logger.warning(
|
|
f"User {user_username} requested an invalid model content_type: '{content_type}'. "
|
|
f"Raising Http404 for dealer '{dealer_slug}'."
|
|
)
|
|
raise Http404("Model not found")
|
|
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
obj = get_object_or_404(model, slug=slug)
|
|
if request.method == "POST":
|
|
form = forms.NoteForm(request.POST)
|
|
if form.is_valid():
|
|
note = form.save(commit=False)
|
|
note.dealer = dealer
|
|
note.content_object = obj
|
|
note.created_by = request.user
|
|
|
|
note.save()
|
|
models.Activity.objects.create(
|
|
dealer=dealer,
|
|
content_object=obj,
|
|
activity_type="note",
|
|
created_by=request.user,
|
|
notes="Note Added",
|
|
)
|
|
# --- Single-line log for successful note creation ---
|
|
logger.info(
|
|
f"User {user_username} successfully added a note "
|
|
f"for {content_type} ID: {obj.slug} (Dealer: {dealer_slug}). "
|
|
f"Note: '{note.note[:50]}...'."
|
|
)
|
|
messages.success(request, _("Note added successfully"))
|
|
else:
|
|
# --- Single-line log for invalid form data ---
|
|
logger.warning(
|
|
f"User {user_username} submitted invalid note form data "
|
|
f"for {content_type} ID: {obj.slug} (Dealer: {dealer_slug}). Errors: {form.errors.as_text()}"
|
|
)
|
|
messages.error(request, _("Note form is not valid"))
|
|
return redirect(f"{content_type}_detail", dealer_slug=dealer_slug, slug=slug)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
@permission_required("inventory.change_notes", raise_exception=True)
|
|
def update_note(request, dealer_slug, pk):
|
|
print(pk)
|
|
note = get_object_or_404(models.Notes, pk=pk)
|
|
# lead = get_object_or_404(models.Lead, pk=note.content_object.id)
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
if request.method == "POST":
|
|
note.note = request.POST.get("note")
|
|
note.save()
|
|
messages.success(request, _("Note updated successfully"))
|
|
|
|
return redirect(request.META.get("HTTP_REFERER", "/"))
|
|
|
|
|
|
# Admin Management
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.change_dealer", raise_exception=True)
|
|
def management_view(request, dealer_slug):
|
|
get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
return render(request, "admin_management/management.html")
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.change_dealer", raise_exception=True)
|
|
def user_management(request, dealer_slug):
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
context = {
|
|
"customers": models.Customer.objects.filter(active=False, dealer=dealer),
|
|
"organizations": models.Organization.objects.filter(
|
|
active=False, dealer=dealer
|
|
),
|
|
"vendors": models.Vendor.objects.filter(active=False, dealer=dealer),
|
|
"staff": models.Staff.objects.filter(active=False, dealer=dealer),
|
|
}
|
|
return render(request, "admin_management/user_management.html", context)
|
|
|
|
|
|
# @login_required
|
|
# @permission_required("inventory.change_dealer", raise_exception=True)
|
|
# def AuditLogDashboardView(request, dealer_slug):
|
|
# """
|
|
# Displays audit logs (User Actions, Login Events, Request Events) with pagination.
|
|
# Log type is determined by the 'q' query parameter (e.g., ?q=userActions).
|
|
# Pagination page number is passed as a query parameter (e.g., ?page=2).
|
|
# """
|
|
# get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
# q = request.GET.get("q") # Get the log type from the 'q' query parameter
|
|
# current_pagination_page = request.GET.get("page", 1)
|
|
# context = {}
|
|
# template_name = None
|
|
# logs_per_page = 30 # Define logs per page once
|
|
|
|
# # --- Determine Data Source and Template based on 'q' parameter ---
|
|
# if (
|
|
# q == "userRequests"
|
|
# ): # This block handles cases where 'q' is 'requestEvents', None, or any other invalid value.
|
|
# # It defaults to Request Logs if 'q' is not 'userActions' or 'loginEvents'.
|
|
# template_name = "admin_management/request_logs.html"
|
|
# context["title"] = "Request Logs Dashboard"
|
|
# request_events = RequestEvent.objects.all().order_by("-datetime")
|
|
# paginator = Paginator(request_events, logs_per_page)
|
|
# try:
|
|
# page_obj = paginator.page(current_pagination_page)
|
|
# except PageNotAnInteger:
|
|
# page_obj = paginator.page(1)
|
|
# except EmptyPage:
|
|
# page_obj = paginator.page(paginator.num_pages)
|
|
|
|
# elif q == "loginEvents":
|
|
# template_name = "admin_management/auth_logs.html"
|
|
# context["title"] = "Login Events Dashboard"
|
|
# auth_events = LoginEvent.objects.all().order_by("-datetime")
|
|
# paginator = Paginator(auth_events, logs_per_page)
|
|
# try:
|
|
# page_obj = paginator.page(current_pagination_page)
|
|
# except PageNotAnInteger:
|
|
# page_obj = paginator.page(1)
|
|
# except EmptyPage:
|
|
# page_obj = paginator.page(paginator.num_pages)
|
|
|
|
# else:
|
|
# template_name = "admin_management/model_logs.html"
|
|
# context["title"] = "User Actions Dashboard"
|
|
|
|
# # OPTIMIZATION: Get the QuerySet but don't evaluate it yet
|
|
# model_events_queryset = CRUDEvent.objects.all().order_by("-datetime")
|
|
|
|
# # 1. Paginate the raw QuerySet FIRST
|
|
# paginator = Paginator(model_events_queryset, logs_per_page)
|
|
|
|
# try:
|
|
# # Get the page object, which contains only the raw QuerySet objects for the current page
|
|
# page_obj_raw = paginator.page(current_pagination_page)
|
|
# except PageNotAnInteger:
|
|
# page_obj_raw = paginator.page(1)
|
|
# except EmptyPage:
|
|
# page_obj_raw = paginator.page(paginator.num_pages)
|
|
|
|
# # 2. Now, process 'field_changes' ONLY for the events on the current page
|
|
# processed_model_events_for_page = []
|
|
# for (
|
|
# event
|
|
# ) in page_obj_raw.object_list: # Loop only through the current page's items
|
|
# event_data = {
|
|
# "datetime": event.datetime,
|
|
# "user": event.user,
|
|
# "event_type_display": event.get_event_type_display(),
|
|
# "model_name": event.content_type.model,
|
|
# "object_id": event.object_id,
|
|
# "object_repr": event.object_repr,
|
|
# "field_changes": [],
|
|
# }
|
|
|
|
# if event.changed_fields:
|
|
# try:
|
|
# changes = json.loads(event.changed_fields)
|
|
# if isinstance(changes, dict):
|
|
# for field_name, values in changes.items():
|
|
# old_value = (
|
|
# values[0]
|
|
# if isinstance(values, list) and len(values) > 0
|
|
# else None
|
|
# )
|
|
# new_value = (
|
|
# values[1]
|
|
# if isinstance(values, list) and len(values) > 1
|
|
# else None
|
|
# )
|
|
# event_data["field_changes"].append(
|
|
# {
|
|
# "field": field_name,
|
|
# "old": old_value,
|
|
# "new": new_value,
|
|
# }
|
|
# )
|
|
# elif changes is None:
|
|
# event_data["field_changes"].append(
|
|
# {
|
|
# "field": "Info",
|
|
# "old": "",
|
|
# "new": "No specific field changes recorded (JSON was null)",
|
|
# }
|
|
# )
|
|
# else: # Handle valid JSON but not a dictionary (e.g., "[]", 123)
|
|
# event_data["field_changes"].append(
|
|
# {
|
|
# "field": "Error",
|
|
# "old": "",
|
|
# "new": f"Unexpected JSON format: {type(changes).__name__}",
|
|
# }
|
|
# )
|
|
# except json.JSONDecodeError:
|
|
# # Handle invalid JSON; you might log this error
|
|
# event_data["field_changes"].append(
|
|
# {
|
|
# "field": "Error",
|
|
# "old": "",
|
|
# "new": "Invalid JSON in changed_fields",
|
|
# }
|
|
# )
|
|
# processed_model_events_for_page.append(event_data)
|
|
|
|
# # 3. Replace the object_list of the original page_obj with the processed data
|
|
# # This keeps all pagination properties (has_next, number, etc.) intact.
|
|
# page_obj_raw.object_list = processed_model_events_for_page
|
|
# page_obj = page_obj_raw # This will be passed to the context
|
|
|
|
# # Pass the final page object to the context
|
|
# context["page_obj"] = page_obj
|
|
|
|
# return render(request, template_name, context)
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.change_dealer", raise_exception=True)
|
|
def activate_account(request, dealer_slug, content_type, slug):
|
|
get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
|
|
# Get user information for logging
|
|
user_username = (
|
|
request.user.username if request.user.is_authenticated else "anonymous"
|
|
)
|
|
# Log the attempt to retrieve the model dynamically
|
|
logger.debug(
|
|
f"User {user_username}attempting to retrieve model "
|
|
f"for content_type '{content_type}' for dealer '{dealer_slug}' in activate_account view."
|
|
)
|
|
try:
|
|
model = apps.get_model(f"inventory.{content_type}")
|
|
except LookupError:
|
|
# --- Single-line log for LookupError (Model not found) ---
|
|
logger.warning(
|
|
f"User {user_username} requested an invalid model content_type: '{content_type}'. "
|
|
f"Raising Http404 in activate_account view for dealer '{dealer_slug}'."
|
|
)
|
|
raise Http404("Model not found")
|
|
|
|
obj = get_object_or_404(model, slug=slug)
|
|
if request.method == "POST":
|
|
obj.activate_account()
|
|
# --- Single-line log for successful account activation ---
|
|
logger.info(
|
|
f"User {user_username} successfully activated account "
|
|
f"for {content_type} ID: {obj.slug} ('{obj.name if hasattr(obj, 'name') else obj}') "
|
|
f"on dealer '{dealer_slug}'."
|
|
)
|
|
messages.success(request, _("Account activated successfully"))
|
|
return redirect("user_management", dealer_slug=dealer_slug)
|
|
return render(
|
|
request, "admin_management/confirm_activate_account.html", {"obj": obj}
|
|
)
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.change_dealer", raise_exception=True)
|
|
def permenant_delete_account(request, dealer_slug, content_type, slug):
|
|
get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
# Get user information for logging
|
|
user_username = (
|
|
request.user.username if request.user.is_authenticated else "anonymous"
|
|
)
|
|
logger.debug(
|
|
f"User {user_username} attempting to retrieve model "
|
|
f"for content_type '{content_type}' for permanent account deletion."
|
|
)
|
|
try:
|
|
model = apps.get_model(f"inventory.{content_type}")
|
|
except LookupError:
|
|
# Log if model not found
|
|
logger.warning(
|
|
f"User {user_username} requested an invalid model content_type: '{content_type}'. "
|
|
f"Raising Http404 for permanent account deletion (Dealer: {dealer_slug})."
|
|
)
|
|
raise Http404("Model not found")
|
|
|
|
obj = get_object_or_404(model, slug=slug)
|
|
if request.method == "POST":
|
|
logger.debug(
|
|
f"User {user_username} attempting to permanently delete account "
|
|
f"for {content_type} ID: {obj.slug} ('{obj.name if hasattr(obj, 'name') else obj}') " # Attempt to get name
|
|
f"on dealer '{dealer_slug}'."
|
|
)
|
|
try:
|
|
obj.permenant_delete()
|
|
# Log successful permanent deletion
|
|
logger.info(
|
|
f"User {user_username} successfully permanently deleted account "
|
|
f"for {content_type} ID: {obj.slug} ('{obj.name if hasattr(obj, 'name') else obj}') "
|
|
f"on dealer '{dealer_slug}'."
|
|
)
|
|
messages.success(request, _("Account Deleted successfully"))
|
|
except RestrictedError:
|
|
# Log restricted deletion
|
|
logger.warning(
|
|
f"User {user_username} attempted to permanently delete account "
|
|
f"for {content_type} ID: {obj.slug} ('{obj.name if hasattr(obj, 'name') else obj}') "
|
|
f"on dealer '{dealer_slug}', but deletion was restricted due to related objects."
|
|
)
|
|
messages.error(
|
|
request,
|
|
_("You cannot delete this account,it is related to another account"),
|
|
)
|
|
except Exception as e:
|
|
# Log generic error during permanent deletion
|
|
logger.error(
|
|
f"User {user_username} encountered an unexpected error "
|
|
f"while permanently deleting account for {content_type} ID: {obj.slug} "
|
|
f"('{obj.name if hasattr(obj, 'name') else obj}') on dealer '{dealer_slug}'. Error: {e}",
|
|
exc_info=True,
|
|
)
|
|
messages.error(request, _(f"Error deleting account: {e}"))
|
|
return redirect("user_management", dealer_slug=dealer_slug)
|
|
return render(
|
|
request, "admin_management/permenant_delete_account.html", {"obj": obj}
|
|
)
|
|
|
|
|
|
#####################################################################
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.add_purchaseordermodel", raise_exception=True)
|
|
def PurchaseOrderCreateView(request, dealer_slug, entity_slug):
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
entity = dealer.entity
|
|
|
|
# Get user information for logging
|
|
user_username = (
|
|
request.user.username if request.user.is_authenticated else "anonymous"
|
|
)
|
|
|
|
if request.method == "POST":
|
|
# Log the attempt to create a purchase order
|
|
logger.debug(
|
|
f"User {user_username} attempting to create a Purchase Order "
|
|
f"for Entity ID: {entity.pk} ('{entity.name}'). Title: '{request.POST.get('po_title')}'."
|
|
)
|
|
try:
|
|
po = entity.create_purchase_order(po_title=request.POST.get("po_title"))
|
|
po.entity = entity
|
|
po.save()
|
|
# --- Single-line log for successful purchase order creation ---
|
|
logger.info(
|
|
f"User {user_username} successfully created Purchase Order ID: {po.pk} "
|
|
f"('{po.po_title}') for Entity ID: {entity.pk} ('{entity.name}')."
|
|
)
|
|
except ValidationError as e:
|
|
# --- Single-line log for ValidationError ---
|
|
logger.warning(
|
|
f"User {user_username} encountered a validation error "
|
|
f"while creating a Purchase Order for Entity ID: {entity.pk} ('{entity.name}'). "
|
|
f"Error: {e}"
|
|
)
|
|
messages.error(request, str(e))
|
|
return redirect(
|
|
"purchase_order_create",
|
|
dealer_slug=dealer.slug,
|
|
entity_slug=entity.slug,
|
|
)
|
|
messages.success(request, _("Purchase order created successfully"))
|
|
return redirect(
|
|
"purchase_order_detail",
|
|
dealer_slug=dealer.slug,
|
|
entity_slug=entity.slug,
|
|
pk=po.pk,
|
|
)
|
|
|
|
form = PurchaseOrderModelCreateForm(
|
|
entity_slug=entity.slug,
|
|
user_model=entity.admin,
|
|
)
|
|
return render(request, "purchase_orders/po_form.html", {"form": form})
|
|
|
|
|
|
@login_required
|
|
def InventoryItemCreateView(request, dealer_slug):
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
for_po = request.GET.get("for_po")
|
|
entity = dealer.entity
|
|
coa = entity.get_default_coa()
|
|
|
|
inventory_accounts = entity.get_coa_accounts().filter(role="asset_ca_inv")
|
|
cogs_accounts = entity.get_coa_accounts().filter(role="cogs_regular")
|
|
|
|
if request.method == "POST":
|
|
response = HttpResponse()
|
|
response["HX-Refresh"] = "true"
|
|
|
|
name = request.POST.get("name")
|
|
account = request.POST.get("account")
|
|
account = inventory_accounts.get(pk=account)
|
|
inventory_name = None
|
|
if name:
|
|
inventory_name = name
|
|
else:
|
|
make = request.POST.get("make")
|
|
model = request.POST.get("model")
|
|
serie = request.POST.get("serie")
|
|
trim = request.POST.get("trim")
|
|
year = request.POST.get("year")
|
|
exterior = request.POST.get("exterior")
|
|
interior = 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)
|
|
exterior_name = models.ExteriorColors.objects.get(
|
|
pk=request.POST.get("exterior")
|
|
)
|
|
interior_name = models.InteriorColors.objects.get(
|
|
pk=request.POST.get("interior")
|
|
)
|
|
|
|
inventory_name = f"{make_name.name} || {model_name.name} || {serie_name.name} || {trim_name.name} || {year} || {exterior_name.name} || {interior_name.name}"
|
|
display_name = f"{make_name.name} {model_name.name} {serie_name.name} {trim_name.name} {year} {exterior_name.name}"
|
|
|
|
if (
|
|
inventory := entity.get_items_inventory()
|
|
.filter(name=inventory_name)
|
|
.first()
|
|
):
|
|
messages.error(request, _("Inventory item already exists"))
|
|
return response
|
|
uom = entity.get_uom_all().filter(name="Unit").first()
|
|
if not uom:
|
|
uom = entity.create_uom(name="Unit", unit_abbr="unit")
|
|
item = entity.create_item_inventory(
|
|
name=display_name,
|
|
uom_model=uom,
|
|
item_type=ItemModel.ITEM_TYPE_MATERIAL,
|
|
inventory_account=account,
|
|
coa_model=coa,
|
|
)
|
|
item.additional_info.update(
|
|
{
|
|
"make": make,
|
|
"model": model,
|
|
"serie": serie,
|
|
"trim": trim,
|
|
"year": year,
|
|
"exterior": exterior,
|
|
"interior": interior,
|
|
}
|
|
)
|
|
item.save()
|
|
messages.success(request, _("Inventory item created successfully"))
|
|
return response
|
|
|
|
if for_po:
|
|
form = forms.CSVUploadForm()
|
|
form.fields["year"].widget.attrs["hx-get"] = reverse(
|
|
"inventory_items_filter", kwargs={"dealer_slug": dealer.slug}
|
|
)
|
|
form.fields["vendor"].queryset = dealer.vendors.filter(active=True)
|
|
context = {
|
|
"make_data": models.CarMake.objects.filter(is_sa_import=True),
|
|
"inventory_accounts": inventory_accounts,
|
|
"cogs_accounts": cogs_accounts,
|
|
"form": form,
|
|
}
|
|
return render(request, "purchase_orders/car_inventory_item_form.html", context)
|
|
return render(
|
|
request,
|
|
"purchase_orders/inventory_item_form.html",
|
|
{
|
|
"make_data": models.CarMake.objects.filter(is_sa_import=True),
|
|
"inventory_accounts": inventory_accounts,
|
|
"cogs_accounts": cogs_accounts,
|
|
},
|
|
)
|
|
|
|
|
|
@login_required
|
|
@permission_required("django_ledger.view_purchaseordermodel", raise_exception=True)
|
|
def inventory_items_filter(request, dealer_slug):
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
year = request.GET.get("year", None)
|
|
make = request.GET.get("make")
|
|
model = request.GET.get("model")
|
|
serie = request.GET.get("serie")
|
|
|
|
model_data = models.CarModel.objects.none()
|
|
serie_data = models.CarSerie.objects.none()
|
|
trim_data = models.CarTrim.objects.none()
|
|
if make:
|
|
make = models.CarMake.objects.get(pk=make)
|
|
model_data = make.carmodel_set.all()
|
|
elif model:
|
|
model = models.CarModel.objects.get(pk=model)
|
|
serie_data = model.carserie_set.all()
|
|
if year:
|
|
serie_data = serie_data.filter(year_begin__lte=year, year_end__gte=year)
|
|
print(serie_data)
|
|
elif serie:
|
|
serie = models.CarSerie.objects.get(pk=serie)
|
|
trim_data = serie.cartrim_set.all()
|
|
context = {
|
|
"model_data": model_data,
|
|
"serie_data": serie_data,
|
|
"trim_data": trim_data,
|
|
}
|
|
return render(request, "purchase_orders/car_inventory_item_form.html", context)
|
|
|
|
|
|
class PurchaseOrderDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
slug_url_kwarg = "po_pk"
|
|
slug_field = "uuid"
|
|
context_object_name = "po_model"
|
|
extra_context = {"header_subtitle_icon": "uil:bill", "hide_menu": True}
|
|
template_name = "purchase_orders/po_detail.html"
|
|
permission_required = ["django_ledger.view_purchaseordermodel"]
|
|
|
|
def get_queryset(self):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
self.queryset = dealer.entity.get_purchase_orders().select_related(
|
|
"entity", "ce_model"
|
|
)
|
|
return super().get_queryset()
|
|
|
|
def get_context_data(self, *, object_list=None, **kwargs):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
context["entity_slug"] = dealer.entity.slug
|
|
po_model: PurchaseOrderModel = self.object
|
|
title = f"Purchase Order {po_model.po_number}"
|
|
context["page_title"] = title
|
|
context["header_title"] = title
|
|
context["po_ready_to_fulfill"] = all(
|
|
[
|
|
item
|
|
for item in po_model.get_itemtxs_data()[0]
|
|
if item.po_item_status == "received"
|
|
]
|
|
)
|
|
po_model: PurchaseOrderModel = self.object
|
|
po_items_qs, item_data = po_model.get_itemtxs_data(
|
|
queryset=po_model.itemtransactionmodel_set.all().select_related(
|
|
"item_model", "bill_model"
|
|
)
|
|
)
|
|
context["po_items"] = po_items_qs
|
|
context["po_total_amount"] = sum(
|
|
i["po_total_amount"]
|
|
for i in po_items_qs.values("po_total_amount", "po_item_status")
|
|
if i["po_item_status"] != "cancelled"
|
|
)
|
|
items = [
|
|
{"total": x.total_amount, "q": x.quantity}
|
|
for x in po_model.get_itemtxs_data()[0].all()
|
|
]
|
|
po_quantity = sum(item["q"] for item in items)
|
|
context["po_quantity"] = po_quantity
|
|
return context
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
context = self.get_context_data(object=self.object)
|
|
po_items_qs, item_data = self.object.get_itemtxs_data(
|
|
queryset=self.object.itemtransactionmodel_set.all().select_related(
|
|
"item_model", "bill_model"
|
|
)
|
|
)
|
|
|
|
if self.object.po_status == "fulfilled":
|
|
context["po_items_list"] = po_items_qs
|
|
context["vendor"] = po_items_qs.first().bill_model.vendor
|
|
context["dealer"] = request.dealer
|
|
|
|
# Check if PDF format is requested
|
|
if request.GET.get("format") == "pdf":
|
|
# Use a separate, print-friendly template for the PDF
|
|
if request.GET.get("lang") == "en":
|
|
html_string = render_to_string(
|
|
"purchase_orders/po_detail_en_pdf.html", context
|
|
)
|
|
else:
|
|
html_string = render_to_string(
|
|
"purchase_orders/po_detail_ar_pdf.html", context
|
|
)
|
|
|
|
base_url = request.build_absolute_uri("/")
|
|
pdf = HTML(string=html_string, base_url=base_url).write_pdf()
|
|
|
|
response = HttpResponse(pdf, content_type="application/pdf")
|
|
response["Content-Disposition"] = (
|
|
f'attachment; filename="PO_{self.object.po_number}.pdf"'
|
|
)
|
|
return response
|
|
|
|
# If not a PDF request, return the standard HTML response
|
|
return self.render_to_response(context)
|
|
|
|
|
|
class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|
model = PurchaseOrderModel
|
|
context_object_name = "purchase_orders"
|
|
paginate_by = 20
|
|
template_name = "purchase_orders/po_list.html"
|
|
permission_required = ["django_ledger.view_purchaseordermodel"]
|
|
|
|
def get_queryset(self):
|
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
query = self.request.GET.get("q")
|
|
qs = self.model.objects.filter(entity=dealer.entity)
|
|
if query:
|
|
qs = qs.filter(
|
|
Q(po_number__icontains=query)
|
|
| Q(po_status__icontains=query)
|
|
| Q(po_title__icontains=query)
|
|
)
|
|
return qs
|
|
return qs
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
dealer = get_user_type(self.request)
|
|
vendors = models.Vendor.objects.filter(dealer=dealer)
|
|
context = super().get_context_data(**kwargs)
|
|
context["entity_slug"] = dealer.entity.slug
|
|
context["vendors"] = vendors
|
|
context["empty_state_value"] = _("purchase order")
|
|
|
|
return context
|
|
|
|
|
|
class PurchaseOrderUpdateView(PurchaseOrderModelUpdateViewBase):
|
|
pass
|
|
|
|
|
|
class BasePurchaseOrderActionActionView(BasePurchaseOrderActionActionViewBase):
|
|
permission_required = "django_ledger.change_purchaseordermodel"
|
|
|
|
|
|
class PurchaseOrderModelDeleteView(PurchaseOrderModelDeleteViewBase):
|
|
permission_required = "django_ledger.delete_purchaseordermodel"
|
|
|
|
def get_success_url(self):
|
|
messages.add_message(
|
|
self.request,
|
|
message="PO deleted successfully.",
|
|
level=messages.SUCCESS,
|
|
)
|
|
return reverse(
|
|
"purchase_order_list",
|
|
kwargs={
|
|
"dealer_slug": self.kwargs["dealer_slug"],
|
|
"entity_slug": self.kwargs["entity_slug"],
|
|
},
|
|
)
|
|
|
|
|
|
class PurchaseOrderMarkAsDraftView(BasePurchaseOrderActionActionView):
|
|
action_name = "mark_as_draft"
|
|
|
|
|
|
class PurchaseOrderMarkAsReviewView(BasePurchaseOrderActionActionView):
|
|
action_name = "mark_as_review"
|
|
|
|
|
|
class PurchaseOrderMarkAsApprovedView(BasePurchaseOrderActionActionView):
|
|
action_name = "mark_as_approved"
|
|
|
|
|
|
class PurchaseOrderMarkAsFulfilledView(BasePurchaseOrderActionActionView):
|
|
action_name = "mark_as_fulfilled"
|
|
|
|
|
|
class PurchaseOrderMarkAsCanceledView(BasePurchaseOrderActionActionView):
|
|
action_name = "mark_as_canceled"
|
|
|
|
|
|
class PurchaseOrderMarkAsVoidView(BasePurchaseOrderActionActionView):
|
|
action_name = "mark_as_void"
|
|
|
|
|
|
##############################bil
|
|
class BaseBillActionView(BaseBillActionViewBase):
|
|
pass
|
|
|
|
|
|
class BillModelActionMarkAsDraftView(BaseBillActionView):
|
|
action_name = "mark_as_draft"
|
|
|
|
|
|
class BillModelActionMarkAsInReviewView(BaseBillActionView):
|
|
action_name = "mark_as_review"
|
|
|
|
|
|
class BillModelActionMarkAsApprovedView(BaseBillActionView):
|
|
action_name = "mark_as_approved"
|
|
|
|
|
|
class BillModelActionMarkAsPaidView(BaseBillActionView):
|
|
action_name = "mark_as_paid"
|
|
|
|
|
|
class BillModelActionDeleteView(BaseBillActionView):
|
|
action_name = "mark_as_delete"
|
|
|
|
|
|
class BillModelActionVoidView(BaseBillActionView):
|
|
action_name = "mark_as_void"
|
|
|
|
|
|
class BillModelActionCanceledView(BaseBillActionView):
|
|
action_name = "mark_as_canceled"
|
|
|
|
|
|
class BillModelActionLockLedgerView(BaseBillActionView):
|
|
action_name = "lock_ledger"
|
|
|
|
|
|
class BillModelActionUnlockLedgerView(BaseBillActionView):
|
|
action_name = "unlock_ledger"
|
|
|
|
|
|
class BillModelActionForceMigrateView(BaseBillActionView):
|
|
action_name = "migrate_state"
|
|
|
|
|
|
###############################################################
|
|
###############################################################
|
|
|
|
|
|
@login_required
|
|
def view_items_inventory(request, dealer_slug, entity_slug, po_pk):
|
|
get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
po = PurchaseOrderModel.objects.get(pk=po_pk)
|
|
items = po.items.all()
|
|
|
|
return render(
|
|
request, "purchase_orders/po_upload_cars.html", {"po": po, "items": items}
|
|
)
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.add_poitemsuploaded", raise_exception=True)
|
|
def upload_cars(request, dealer_slug, pk=None):
|
|
item = None
|
|
po_item = None
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
response = redirect("upload_cars", dealer_slug=dealer_slug)
|
|
|
|
# Get user information for logging
|
|
user_username = (
|
|
request.user.username if request.user.is_authenticated else "anonymous"
|
|
)
|
|
|
|
if pk:
|
|
# Log retrieval of PO item for upload
|
|
logger.debug(
|
|
f"User {user_username} retrieved ItemTransactionModel PK: {pk} for car upload."
|
|
)
|
|
item = get_object_or_404(ItemTransactionModel, pk=pk)
|
|
|
|
po_item = models.PoItemsUploaded.objects.get(dealer=dealer, item=item)
|
|
|
|
response = redirect("upload_cars", dealer_slug=dealer_slug, pk=pk)
|
|
if po_item.status == "uploaded":
|
|
messages.add_message(
|
|
request, messages.SUCCESS, "Item uploaded Sucessfully."
|
|
)
|
|
return redirect(
|
|
"view_items_inventory",
|
|
dealer_slug=dealer_slug,
|
|
entity_slug=dealer.entity.slug,
|
|
po_pk=item.po_model.pk,
|
|
)
|
|
|
|
if request.method == "POST":
|
|
csv_file = request.FILES.get("csv_file")
|
|
marked_price = request.POST.get("marked_price")
|
|
# Log initial attempt to process data from item or POST
|
|
logger.debug(
|
|
f"User {user_username} starting car data processing for upload. "
|
|
f"Source: {'Existing Item PK: ' + str(pk) if pk else 'Form submission'}."
|
|
)
|
|
try:
|
|
if item:
|
|
# data = [x.strip() for x in item.item_model.name.split("||")]
|
|
make = models.CarMake.objects.get(pk=item.item_model.additional_info.get("make"))
|
|
model = models.CarModel.objects.get(pk=item.item_model.additional_info.get("model"))
|
|
trim = models.CarTrim.objects.get(pk=item.item_model.additional_info.get("trim"))
|
|
serie = models.CarSerie.objects.get(pk=item.item_model.additional_info.get("serie"))
|
|
year = item.item_model.additional_info.get("year")
|
|
exterior = models.ExteriorColors.objects.get(
|
|
pk=item.item_model.additional_info.get("exterior")
|
|
)
|
|
interior = models.InteriorColors.objects.get(
|
|
pk=item.item_model.additional_info.get("interior")
|
|
)
|
|
receiving_date = timezone.now()
|
|
vendor_model = item.bill_model.vendor
|
|
vendor = models.Vendor.objects.get(vendor_model=vendor_model)
|
|
logger.debug(
|
|
f"User {user_username} extracted car details from existing Item PK: {pk}."
|
|
)
|
|
else:
|
|
make = models.CarMake.objects.get(pk=request.POST.get("make"))
|
|
model = models.CarModel.objects.get(pk=request.POST.get("model"))
|
|
serie = models.CarSerie.objects.get(pk=request.POST.get("serie"))
|
|
trim = models.CarTrim.objects.get(pk=request.POST.get("trim"))
|
|
exterior = models.ExteriorColors.objects.get(
|
|
pk=request.POST.get("exterior")
|
|
)
|
|
interior = models.InteriorColors.objects.get(
|
|
pk=request.POST.get("interior")
|
|
)
|
|
year = request.POST.get("year")
|
|
receiving_date = datetime.strptime(
|
|
request.POST.get("receiving_date"), "%Y-%m-%d"
|
|
)
|
|
vendor = models.Vendor.objects.get(pk=request.POST.get("vendor"))
|
|
logger.debug(
|
|
f"User {user_username} extracted car details from form submission."
|
|
)
|
|
|
|
except Exception as e:
|
|
# --- Log for errors during data extraction/retrieval ---
|
|
logger.error(
|
|
f"User {user_username} encountered an error while preparing car data "
|
|
f"for upload (Item PK: {pk if pk else 'N/A'}, Dealer: {dealer_slug}). Error: {e}",
|
|
exc_info=True,
|
|
)
|
|
messages.error(request, f"Error processing CSV: {str(e)}")
|
|
return response
|
|
|
|
if not csv_file.name.endswith(".csv"):
|
|
logger.warning(
|
|
f"User {user_username} attempted to upload a non-CSV file "
|
|
f"('{csv_file.name}') for car upload. Dealer: {dealer_slug}."
|
|
)
|
|
messages.error(request, "Please upload a CSV file")
|
|
return response
|
|
try:
|
|
# Read the file content
|
|
file_content = csv_file.read().decode("utf-8")
|
|
csv_data = io.StringIO(file_content)
|
|
reader = csv.DictReader(csv_data)
|
|
data = [x for x in reader]
|
|
if len(data) < item.quantity:
|
|
messages.error(
|
|
request,
|
|
f"CSV file has {len(data)} rows, but the quantity of the item is {item.quantity}.",
|
|
)
|
|
return response
|
|
for row in data:
|
|
# Log VIN decoding and initial validation for each row
|
|
logger.debug(
|
|
f"Processing VIN: {row.get('vin', 'N/A')} from CSV row by user {user_username}."
|
|
)
|
|
if result := decodevin(row["vin"]):
|
|
if models.Car.objects.filter(vin=row["vin"]).exists():
|
|
messages.error(request, f"vin {row['vin']} already exists")
|
|
return response
|
|
manufacturer_name, model_name, year_model = result.values()
|
|
car_make = get_make(manufacturer_name)
|
|
car_model = get_model(model_name, car_make)
|
|
if (
|
|
not all([car_make]) or (make.pk != car_make.pk)
|
|
# not all([car_make, car_model])
|
|
# or (make.pk != car_make.pk)
|
|
# or (model.pk != car_model.pk)
|
|
):
|
|
logger.warning(
|
|
f"User {user_username} uploaded CSV with VIN '{row['vin']}' "
|
|
f"having data mismatch (Make/Model from VIN vs. expected). "
|
|
f"VIN Make: '{manufacturer_name}', Expected Make: '{make.name}'. "
|
|
f"VIN Model: '{model_name}', Expected Model: '{model.name}'. "
|
|
f"Returning error."
|
|
)
|
|
messages.error(
|
|
request,
|
|
f"invalid data at vin {row['vin']}, Please upload a valid CSV file",
|
|
)
|
|
return response
|
|
|
|
cars_created = 0
|
|
for row in data:
|
|
car = models.Car.objects.create(
|
|
dealer=dealer,
|
|
vin=row["vin"],
|
|
id_car_make=make,
|
|
id_car_model=model,
|
|
id_car_serie=serie,
|
|
id_car_trim=trim,
|
|
year=int(year_model),
|
|
vendor=vendor,
|
|
receiving_date=receiving_date,
|
|
cost_price=po_item.item.unit_cost,
|
|
marked_price=marked_price or 0,
|
|
)
|
|
|
|
car.add_colors(exterior=exterior, interior=interior)
|
|
cars_created += 1
|
|
logger.debug(
|
|
f"User {user_username} created Car ID: {car.pk} (VIN: {car.vin}). "
|
|
f"Count: {cars_created}."
|
|
)
|
|
|
|
if po_item:
|
|
po_item.status = "uploaded"
|
|
po_item.save()
|
|
logger.info(
|
|
f"User {user_username} updated PoItemsUploaded status to 'uploaded' for Item PK: {item.pk}."
|
|
)
|
|
return redirect("home")
|
|
# --- Log for successful CSV import and car creation ---
|
|
logger.info(
|
|
f"User {user_username} successfully imported {cars_created} cars "
|
|
f"for Item PK: {pk if pk else 'N/A'} (Dealer: {dealer_slug})."
|
|
)
|
|
messages.success(request, f"Successfully imported {cars_created} cars")
|
|
return redirect(
|
|
"view_items_inventory",
|
|
dealer_slug=dealer_slug,
|
|
slug_entity=dealer.entity.slug,
|
|
po_pk=item.po_model.pk,
|
|
)
|
|
|
|
except Exception as e:
|
|
# --- Log for general errors during CSV processing or car creation ---
|
|
logger.error(
|
|
f"User {user_username} encountered an unexpected error "
|
|
f"during CSV file processing or car creation for Item PK: {pk if pk else 'N/A'} "
|
|
f"(Dealer: {dealer_slug}). Error: {e}",
|
|
exc_info=True, # Crucial for full traceback
|
|
)
|
|
messages.error(request, f"Error processing CSV: {str(e)}")
|
|
return response
|
|
|
|
form = forms.CSVUploadForm()
|
|
form.fields["vendor"].queryset = dealer.vendors.filter(active=True).all()
|
|
|
|
return render(
|
|
request,
|
|
"csv_upload.html",
|
|
{
|
|
"make_data": models.CarMake.objects.filter(is_sa_import=True),
|
|
"form": form,
|
|
"item": item,
|
|
},
|
|
)
|
|
|
|
|
|
###############################################################
|
|
###############################################################
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.add_poitemsuploaded", raise_exception=True)
|
|
@require_POST
|
|
def bulk_update_car_price(request):
|
|
if request.method == "POST":
|
|
cars = request.POST.getlist("car")
|
|
price = request.POST.get("price")
|
|
|
|
if not price or int(price) <= 0:
|
|
messages.error(request, "Please enter a valid price")
|
|
elif not cars:
|
|
messages.error(request, "No cars selected for price update")
|
|
else:
|
|
for car_pk in cars:
|
|
car = models.Car.objects.get(pk=car_pk)
|
|
car.cost_price = Decimal(price)
|
|
car.save()
|
|
messages.success(request, "Price updated successfully")
|
|
|
|
response = HttpResponse()
|
|
response["HX-Redirect"] = reverse("car_list")
|
|
return response
|
|
|
|
|
|
class InventoryListView(InventoryListViewBase):
|
|
template_name = "inventory/list.html"
|
|
permission_required = ["django_ledger.view_purchaseordermodel"]
|
|
|
|
|
|
@login_required
|
|
def purchase_report_view(request, dealer_slug):
|
|
start_date_str = request.GET.get("start_date")
|
|
end_date_str = request.GET.get("end_date")
|
|
|
|
pos = request.entity.get_purchase_orders()
|
|
|
|
if start_date_str:
|
|
try:
|
|
start_date = datetime.strptime(start_date_str, "%Y-%m-%d").date()
|
|
pos = pos.filter(created__date__gte=start_date)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
if end_date_str:
|
|
try:
|
|
end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date()
|
|
pos = pos.filter(created__date__lte=end_date)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
data = []
|
|
total_po_amount = 0
|
|
total_po_cars = 0
|
|
|
|
for po in pos:
|
|
items = [
|
|
{"total": x.total_amount, "q": x.quantity}
|
|
for x in po.get_itemtxs_data()[0].all()
|
|
]
|
|
|
|
po_amount = sum(item["total"] for item in items)
|
|
po_quantity = sum(item["q"] for item in items)
|
|
|
|
total_po_amount += po_amount
|
|
total_po_cars += po_quantity
|
|
|
|
bills = po.get_po_bill_queryset()
|
|
vendors = set([bill.vendor.vendor_name for bill in bills])
|
|
vendors_str = ", ".join(sorted(list(vendors))) if vendors else "N/A"
|
|
|
|
data.append(
|
|
{
|
|
"po_number": po.po_number,
|
|
"po_created": po.created,
|
|
"po_status": po.po_status,
|
|
"po_fulfilled_date": po.date_fulfilled,
|
|
"po_amount": po_amount,
|
|
"po_quantity": po_quantity,
|
|
"vendors_str": vendors_str,
|
|
}
|
|
)
|
|
|
|
current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
context = {
|
|
"dealer": request.entity.name,
|
|
"time": current_time,
|
|
"data": data,
|
|
"total_po_amount": total_po_amount,
|
|
"total_po_cars": total_po_cars,
|
|
"current_time": current_time,
|
|
"start_date": start_date_str,
|
|
"end_date": end_date_str,
|
|
}
|
|
|
|
return render(request, "ledger/reports/purchase_report.html", context)
|
|
|
|
|
|
def purchase_report_csv_export(request, dealer_slug):
|
|
response = HttpResponse(content_type="text/csv")
|
|
current_time = timezone.now().strftime("%Y-%m-%d_%H%M%S")
|
|
filename = f"purchase_report_{dealer_slug}_{current_time}.csv"
|
|
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
|
|
|
writer = csv.writer(response)
|
|
|
|
header = [
|
|
"PO Number",
|
|
"Created Date",
|
|
"Status",
|
|
"Fulfilled Date",
|
|
"PO Amount",
|
|
"PO Quantity",
|
|
"Vendors",
|
|
]
|
|
writer.writerow(header)
|
|
|
|
pos = request.entity.get_purchase_orders()
|
|
start_date_str = request.GET.get("start_date")
|
|
end_date_str = request.GET.get("end_date")
|
|
|
|
if start_date_str:
|
|
try:
|
|
start_date = datetime.strptime(start_date_str, "%Y-%m-%d").date()
|
|
pos = pos.filter(created__date__gte=start_date)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
if end_date_str:
|
|
try:
|
|
end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date()
|
|
pos = pos.filter(created__date__lte=end_date)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
for po in pos:
|
|
po_amount = 0
|
|
po_quantity = 0
|
|
items = [
|
|
{"total": x.total_amount, "q": x.quantity}
|
|
for x in po.get_itemtxs_data()[0].all()
|
|
]
|
|
|
|
for item in items:
|
|
po_amount += item["total"]
|
|
po_quantity += item["q"]
|
|
|
|
bills = po.get_po_bill_queryset()
|
|
vendors = set([bill.vendor.vendor_name for bill in bills])
|
|
vendors_str = ", ".join(sorted(list(vendors))) if vendors else "N/A"
|
|
|
|
writer.writerow(
|
|
[
|
|
po.po_number,
|
|
po.created.strftime("%Y-%m-%d %H:%M:%S") if po.created else "",
|
|
po.get_po_status_display(),
|
|
po.date_fulfilled.strftime("%Y-%m-%d") if po.date_fulfilled else "",
|
|
f"{po_amount:.2f}",
|
|
po_quantity,
|
|
vendors_str,
|
|
]
|
|
)
|
|
return response
|
|
|
|
|
|
@login_required
|
|
def car_sale_report_view(request, dealer_slug):
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
vat = models.VatRate.objects.filter(dealer=dealer, is_active=True).first()
|
|
VAT_RATE = vat.rate if vat else 0
|
|
|
|
cars_sold = models.Car.objects.filter(dealer=dealer, status="sold")
|
|
|
|
# Get filter parameters from the request
|
|
selected_make = request.GET.get("make")
|
|
selected_model = request.GET.get("model")
|
|
selected_serie = request.GET.get("serie")
|
|
selected_year = request.GET.get("year")
|
|
selected_stock_type = request.GET.get("stock_type")
|
|
start_date_str = request.GET.get("start_date")
|
|
end_date_str = request.GET.get("end_date")
|
|
|
|
# Apply filters to the queryset
|
|
if selected_make:
|
|
cars_sold = cars_sold.filter(id_car_make__name=selected_make)
|
|
if selected_model:
|
|
cars_sold = cars_sold.filter(id_car_model__name=selected_model)
|
|
if selected_serie:
|
|
cars_sold = cars_sold.filter(id_car_serie__name=selected_serie)
|
|
if selected_year:
|
|
cars_sold = cars_sold.filter(year=selected_year)
|
|
if selected_stock_type:
|
|
cars_sold = cars_sold.filter(stock_type=selected_stock_type)
|
|
|
|
# Corrected: Apply date filters using the 'sold_date' field
|
|
if start_date_str:
|
|
try:
|
|
start_date = datetime.strptime(start_date_str, "%Y-%m-%d").date()
|
|
cars_sold = cars_sold.filter(sold_date__gte=start_date)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
if end_date_str:
|
|
try:
|
|
end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date()
|
|
cars_sold = cars_sold.filter(sold_date__lte=end_date)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
# Calculate summary data for the filtered results
|
|
total_cars_sold = cars_sold.count()
|
|
total_revenue_from_cars = (
|
|
cars_sold.aggregate(total=Sum(F("marked_price") - F("discount_amount")))[
|
|
"total"
|
|
]
|
|
or 0
|
|
)
|
|
|
|
total_vat_on_cars = (
|
|
cars_sold.annotate(
|
|
final_price=F("marked_price") - F("discount_amount")
|
|
).aggregate(total=Sum(F("final_price") * VAT_RATE))["total"]
|
|
or 0
|
|
)
|
|
|
|
total_revenue_from_additonals = sum(
|
|
[car.get_additional_services()["total"] for car in cars_sold]
|
|
)
|
|
total_vat_from_additonals = sum(
|
|
[car.get_additional_services()["services_vat"] for car in cars_sold]
|
|
)
|
|
total_vat_collected = total_vat_on_cars + total_vat_from_additonals
|
|
total_revenue_collected = total_revenue_from_cars + total_revenue_from_additonals
|
|
|
|
total_discount = cars_sold.aggregate(total=Sum("discount_amount"))["total"] or 0
|
|
|
|
current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
# Get distinct makes for the initial dropdown, other dropdowns will be populated via AJAX
|
|
base_sold_cars_queryset = models.Car.objects.filter(dealer=dealer, status="sold")
|
|
makes = (
|
|
base_sold_cars_queryset.values_list("id_car_make__name", flat=True)
|
|
.distinct()
|
|
.order_by("id_car_make__name")
|
|
)
|
|
|
|
context = {
|
|
"cars_sold": cars_sold,
|
|
"total_cars_sold": total_cars_sold,
|
|
"current_time": current_time,
|
|
"dealer": dealer,
|
|
"total_revenue_from_cars": total_revenue_from_cars,
|
|
"total_revenue_from_additonals": total_revenue_from_additonals,
|
|
"total_revenue_collected": total_revenue_collected,
|
|
"total_vat_on_cars": total_vat_on_cars,
|
|
"total_vat_from_additonals": total_vat_from_additonals,
|
|
"total_vat_collected": total_vat_collected,
|
|
"total_discount": total_discount,
|
|
"makes": makes,
|
|
"selected_make": selected_make,
|
|
"selected_model": selected_model,
|
|
"selected_serie": selected_serie,
|
|
"selected_year": selected_year,
|
|
"selected_stock_type": selected_stock_type,
|
|
"start_date": start_date_str,
|
|
"end_date": end_date_str,
|
|
}
|
|
|
|
return render(request, "ledger/reports/car_sale_report.html", context)
|
|
|
|
|
|
### 2. Updated `get_filtered_choices`
|
|
|
|
|
|
@login_required
|
|
def get_filtered_choices(request, dealer_slug):
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
|
|
# Get all filter parameters from the request
|
|
selected_make = request.GET.get("make")
|
|
selected_model = request.GET.get("model")
|
|
selected_serie = request.GET.get("serie")
|
|
start_date_str = request.GET.get("start_date")
|
|
end_date_str = request.GET.get("end_date")
|
|
|
|
# Start with the base queryset
|
|
queryset = models.Car.objects.filter(dealer=dealer, status="sold")
|
|
|
|
# Apply filters based on what is selected
|
|
if selected_make:
|
|
queryset = queryset.filter(id_car_make__name=selected_make)
|
|
|
|
if selected_model:
|
|
queryset = queryset.filter(id_car_model__name=selected_model)
|
|
|
|
if selected_serie:
|
|
queryset = queryset.filter(id_car_serie__name=selected_serie)
|
|
|
|
# Corrected: Apply date filters to the AJAX queryset
|
|
if start_date_str:
|
|
try:
|
|
start_date = datetime.strptime(start_date_str, "%Y-%m-%d").date()
|
|
queryset = queryset.filter(sold_date__gte=start_date)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
if end_date_str:
|
|
try:
|
|
end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date()
|
|
queryset = queryset.filter(sold_date__lte=end_date)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
data = {
|
|
"models": list(
|
|
queryset.values_list("id_car_model__name", flat=True)
|
|
.distinct()
|
|
.order_by("id_car_model__name")
|
|
),
|
|
"series": list(
|
|
queryset.values_list("id_car_serie__name", flat=True)
|
|
.distinct()
|
|
.order_by("id_car_serie__name")
|
|
),
|
|
"years": list(
|
|
queryset.values_list("year", flat=True).distinct().order_by("-year")
|
|
),
|
|
"stock_types": list(
|
|
queryset.values_list("stock_type", flat=True)
|
|
.distinct()
|
|
.order_by("stock_type")
|
|
),
|
|
}
|
|
return JsonResponse(data)
|
|
|
|
|
|
### 3. Updated `car_sale_report_csv_export`
|
|
|
|
|
|
@login_required
|
|
def car_sale_report_csv_export(request, dealer_slug):
|
|
response = HttpResponse(content_type="text/csv")
|
|
current_time = timezone.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
filename = f"sales_report_{dealer_slug}_{current_time}.csv"
|
|
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
|
|
|
writer = csv.writer(response)
|
|
|
|
# Define the CSV header based on your HTML table headers
|
|
header = [
|
|
"VIN",
|
|
"Make",
|
|
"Model",
|
|
"Year",
|
|
"Serie",
|
|
"Trim",
|
|
"Mileage",
|
|
"Stock Type",
|
|
"Created Date",
|
|
"Sold Date",
|
|
"Cost Price",
|
|
"Marked Price",
|
|
"Discount Amount",
|
|
"Selling Price",
|
|
"VAT on Car",
|
|
"Services Price",
|
|
"VAT on Services",
|
|
"Final Total",
|
|
"Invoice Number",
|
|
]
|
|
writer.writerow(header)
|
|
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
cars_sold = models.Car.objects.filter(dealer=dealer, status="sold")
|
|
|
|
# Apply filters from the request, just like in your HTML view
|
|
selected_make = request.GET.get("make")
|
|
selected_model = request.GET.get("model")
|
|
selected_serie = request.GET.get("serie")
|
|
selected_year = request.GET.get("year")
|
|
selected_stock_type = request.GET.get("stock_type")
|
|
start_date_str = request.GET.get("start_date")
|
|
end_date_str = request.GET.get("end_date")
|
|
|
|
if selected_make:
|
|
cars_sold = cars_sold.filter(id_car_make__name=selected_make)
|
|
if selected_model:
|
|
cars_sold = cars_sold.filter(id_car_model__name=selected_model)
|
|
if selected_serie:
|
|
cars_sold = cars_sold.filter(id_car_serie__name=selected_serie)
|
|
if selected_year:
|
|
cars_sold = cars_sold.filter(year=selected_year)
|
|
if selected_stock_type:
|
|
cars_sold = cars_sold.filter(stock_type=selected_stock_type)
|
|
|
|
# Corrected: Apply date filters for CSV export
|
|
if start_date_str:
|
|
try:
|
|
start_date = datetime.strptime(start_date_str, "%Y-%m-%d").date()
|
|
cars_sold = cars_sold.filter(sold_date__gte=start_date)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
if end_date_str:
|
|
try:
|
|
end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date()
|
|
cars_sold = cars_sold.filter(sold_date__lte=end_date)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
# Write the data for the filtered cars
|
|
for car in cars_sold:
|
|
additional_services = car.get_additional_services()
|
|
services_total_price = additional_services["total"]
|
|
services_vat_amount = additional_services["services_vat"]
|
|
|
|
invoice_number = None
|
|
sold_date = None
|
|
if car.invoice:
|
|
invoice_number = car.invoice.invoice_number
|
|
sold_date = car.invoice.date_paid
|
|
|
|
writer.writerow(
|
|
[
|
|
car.vin,
|
|
car.id_car_make.name,
|
|
car.id_car_model.name,
|
|
car.year,
|
|
car.id_car_serie.name,
|
|
car.id_car_trim.name,
|
|
car.mileage if car.mileage else "0",
|
|
car.stock_type,
|
|
car.created_at.strftime("%Y-%m-%d %H:%M:%S") if car.created_at else "",
|
|
sold_date.strftime("%Y-%m-%d %H:%M:%S") if sold_date else "",
|
|
car.cost_price,
|
|
car.marked_price,
|
|
car.discount_amount,
|
|
car.final_price,
|
|
car.vat_amount,
|
|
services_total_price,
|
|
services_vat_amount,
|
|
car.final_price_plus_services_plus_vat,
|
|
invoice_number,
|
|
]
|
|
)
|
|
|
|
return response
|
|
|
|
|
|
@login_required
|
|
# @permission_required('inventory.view_staff')
|
|
def staff_password_reset_view(request, dealer_slug, user_pk):
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
staff = models.Staff.objects.filter(dealer=dealer, pk=user_pk).first()
|
|
|
|
if request.method == "POST":
|
|
form = forms.CustomSetPasswordForm(staff.user, request.POST)
|
|
if form.is_valid():
|
|
form.save()
|
|
messages.success(
|
|
request,
|
|
_("Your password has been set. You may go ahead and log in now."),
|
|
)
|
|
return redirect("user_detail", dealer_slug=dealer_slug, slug=staff.slug)
|
|
else:
|
|
messages.error(request, _(f"Invalid password. {str(form.errors)}"))
|
|
form = forms.CustomSetPasswordForm(staff.user)
|
|
return render(request, "users/user_password_reset.html", {"form": form})
|
|
|
|
|
|
class RecallListView(ListView):
|
|
model = models.Recall
|
|
template_name = "recalls/recall_list.html"
|
|
context_object_name = "recalls"
|
|
paginate_by = 20
|
|
|
|
def get_queryset(self):
|
|
queryset = (
|
|
super()
|
|
.get_queryset()
|
|
.annotate(
|
|
dealer_count=Count("notifications", distinct=True),
|
|
car_count=Count("notifications__cars_affected", distinct=True),
|
|
)
|
|
)
|
|
return queryset.select_related("make", "model", "serie", "trim")
|
|
|
|
|
|
class RecallDetailView(DetailView):
|
|
model = models.Recall
|
|
template_name = "recalls/recall_detail.html"
|
|
context_object_name = "recall"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["notifications"] = self.object.notifications.select_related("dealer")
|
|
return context
|
|
|
|
|
|
def RecallFilterView(request):
|
|
context = {"make_data": models.CarMake.objects.all()}
|
|
if request.method == "POST":
|
|
make = request.POST.get("make")
|
|
model = request.POST.get("model")
|
|
serie = request.POST.get("serie")
|
|
trim = request.POST.get("trim")
|
|
year = request.POST.get("year")
|
|
url = reverse("recall_create")
|
|
url += f"?make={make}&model={model}&serie={serie}&trim={trim}&year={year}"
|
|
cars = models.Car.objects.filter(
|
|
id_car_make=make,
|
|
id_car_model=model,
|
|
id_car_serie=serie,
|
|
id_car_trim=trim,
|
|
year=year,
|
|
)
|
|
context["url"] = url
|
|
context["cars"] = cars
|
|
return render(request, "recalls/recall_filter.html", context)
|
|
|
|
|
|
class RecallCreateView(FormView):
|
|
template_name = "recalls/recall_create.html"
|
|
form_class = forms.RecallCreateForm
|
|
success_url = reverse_lazy("recall_success")
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
make = self.request.GET.get("make")
|
|
model = self.request.GET.get("model")
|
|
serie = self.request.GET.get("serie")
|
|
trim = self.request.GET.get("trim")
|
|
year = self.request.GET.get("year")
|
|
if make:
|
|
qs = models.CarMake.objects.filter(pk=make)
|
|
form.fields["make"].queryset = qs
|
|
form.initial["make"] = qs.first()
|
|
if model:
|
|
qs = models.CarModel.objects.filter(pk=model)
|
|
form.fields["model"].queryset = qs
|
|
form.initial["model"] = qs.first()
|
|
if serie:
|
|
qs = models.CarSerie.objects.filter(pk=serie)
|
|
form.fields["serie"].queryset = qs
|
|
form.initial["serie"] = qs.first()
|
|
if trim:
|
|
qs = models.CarTrim.objects.filter(pk=trim)
|
|
form.fields["trim"].queryset = qs
|
|
form.initial["trim"] = qs.first()
|
|
if year:
|
|
form.fields["year_from"].initial = year
|
|
form.fields["year_to"].initial = year
|
|
return form
|
|
|
|
def get_initial(self):
|
|
initial = super().get_initial()
|
|
|
|
if self.request.method == "GET":
|
|
initial.update(self.request.GET.dict())
|
|
return initial
|
|
|
|
def form_valid(self, form):
|
|
recall = form.save(commit=False)
|
|
recall.created_by = self.request.user
|
|
recall.save()
|
|
|
|
# Get affected cars based on recall criteria
|
|
cars = models.Car.objects.all()
|
|
|
|
if recall.make:
|
|
cars = cars.filter(id_car_make=recall.make)
|
|
|
|
if recall.model:
|
|
cars = cars.filter(id_car_model=recall.model)
|
|
|
|
if recall.serie:
|
|
cars = cars.filter(id_car_serie=recall.serie)
|
|
|
|
if recall.trim:
|
|
cars = cars.filter(id_car_trim=recall.trim)
|
|
|
|
if recall.year_from:
|
|
cars = cars.filter(year__gte=recall.year_from)
|
|
|
|
if recall.year_to:
|
|
cars = cars.filter(year__lte=recall.year_to)
|
|
|
|
# Group cars by dealer and send notifications
|
|
dealers = models.Dealer.objects.filter(cars__in=cars).distinct()
|
|
|
|
for dealer in dealers:
|
|
dealer_cars = cars.filter(dealer=dealer)
|
|
notification = models.RecallNotification.objects.create(
|
|
recall=recall, dealer=dealer
|
|
)
|
|
notification.cars_affected.set(dealer_cars)
|
|
|
|
# Send email
|
|
self.send_notification_email(dealer, recall, dealer_cars)
|
|
|
|
messages.success(
|
|
self.request, _("Recall created and notifications sent successfully")
|
|
)
|
|
return super().form_valid(form)
|
|
|
|
def send_notification_email(self, dealer, recall, cars):
|
|
subject = f"Recall Notification: {recall.title}"
|
|
message = render_to_string(
|
|
"recalls/email/recall_notification.txt",
|
|
{
|
|
"dealer": dealer,
|
|
"recall": recall,
|
|
"cars": cars,
|
|
},
|
|
)
|
|
send_email(
|
|
subject,
|
|
message,
|
|
"noreply@yourdomain.com",
|
|
[dealer.user.email],
|
|
)
|
|
|
|
|
|
class RecallSuccessView(TemplateView):
|
|
template_name = "recalls/recall_success.html"
|
|
|
|
|
|
@login_required
|
|
def schedule_calendar(request, dealer_slug):
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
user_schedules = models.Schedule.objects.filter(
|
|
dealer=dealer, scheduled_by=request.user
|
|
).order_by("scheduled_at")
|
|
upcoming_schedules = user_schedules.filter(
|
|
scheduled_at__gte=timezone.now()
|
|
).order_by("scheduled_at")
|
|
context = {"schedules": user_schedules, "upcoming_schedules": upcoming_schedules}
|
|
return render(request, "schedule_calendar.html", context)
|
|
|
|
|
|
# Support
|
|
@login_required
|
|
def help_center(request):
|
|
return render(request, "support/help_center.html")
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.add_ticket")
|
|
def create_ticket(request, dealer_slug):
|
|
if not request.is_dealer:
|
|
return redirect("home")
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
if request.method == "POST":
|
|
form = forms.TicketForm(request.POST)
|
|
if form.is_valid():
|
|
instance = form.save(commit=False)
|
|
instance.dealer = dealer
|
|
instance.save()
|
|
messages.success(
|
|
request, "Your support ticket has been submitted successfully!"
|
|
)
|
|
return redirect("ticket_list", dealer_slug=dealer.slug)
|
|
else:
|
|
form = forms.TicketForm()
|
|
|
|
return render(request, "support/create_ticket.html", {"form": form})
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.view_ticket")
|
|
def ticket_list(request, dealer_slug):
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
tickets = models.Ticket.objects.filter(dealer=dealer).order_by("-created_at")
|
|
query = request.GET.get("q")
|
|
if query:
|
|
tickets = tickets.filter(Q(id__icontains=query) | Q(subject__icontains=query))
|
|
|
|
return render(request, "support/ticket_list.html", {"tickets": tickets})
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.change_ticket")
|
|
def ticket_detail(request, dealer_slug, ticket_id):
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
ticket = models.Ticket.objects.get(dealer=dealer, id=ticket_id)
|
|
return render(request, "support/ticket_detail.html", {"ticket": ticket})
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.change_ticket")
|
|
def ticket_mark_resolved(request, ticket_id):
|
|
ticket = models.Ticket.objects.get(id=ticket_id)
|
|
ticket.status = "resolved"
|
|
ticket.save()
|
|
messages.success(request, "Ticket marked as resolved successfully!")
|
|
subject = "Ticket Resolved"
|
|
message = f"Your support ticket has been resolved. Please check the details below:\n\nTicket ID: {ticket.id}\nSubject: {ticket.subject}\nDescription: {ticket.description}"
|
|
send_email(settings.SUPPORT_EMAIL, ticket.dealer.user.email, subject, message)
|
|
return render(request, "support/ticket_detail.html", {"ticket": ticket})
|
|
|
|
|
|
@login_required
|
|
@permission_required("inventory.change_ticket")
|
|
def ticket_update(request, ticket_id):
|
|
ticket = models.Ticket.objects.get(id=ticket_id)
|
|
|
|
if request.method == "POST":
|
|
form = forms.TicketResolutionForm(request.POST, instance=ticket)
|
|
if form.is_valid():
|
|
form.save()
|
|
messages.success(
|
|
request, f"Ticket has been marked as {ticket.get_status_display()}."
|
|
)
|
|
return redirect(
|
|
"ticket_detail", dealer_slug=ticket.dealer.slug, ticket_id=ticket.id
|
|
)
|
|
else:
|
|
form = forms.TicketResolutionForm(instance=ticket)
|
|
|
|
return render(
|
|
request, "support/ticket_update.html", {"ticket": ticket, "form": form}
|
|
)
|
|
|
|
|
|
# class ChartOfAccountModelListView(ChartOfAccountModelListViewBase):
|
|
# template_name = 'chart_of_accounts/coa_list.html'
|
|
# permission_required = 'django_ledger.view_chartofaccountmodel'
|
|
class ChartOfAccountModelCreateView(ChartOfAccountModelCreateViewBase):
|
|
template_name = "chart_of_accounts/coa_create.html"
|
|
permission_required = "django_ledger.add_chartofaccountmodel"
|
|
|
|
|
|
class ChartOfAccountModelListView(ChartOfAccountModelListViewBase):
|
|
template_name = "chart_of_accounts/coa_list.html"
|
|
permission_required = "django_ledger.view_chartofaccountmodel"
|
|
|
|
|
|
class ChartOfAccountModelUpdateView(ChartOfAccountModelUpdateViewBase):
|
|
template_name = "chart_of_accounts/coa_update.html"
|
|
permission_required = "django_ledger.change_chartofaccountmodel"
|
|
|
|
|
|
class CharOfAccountModelActionView(CharOfAccountModelActionViewBase):
|
|
permission_required = "django_ledger.change_chartofaccountmodel"
|
|
|
|
|
|
class CarDealershipSignUpView(CreateView):
|
|
model = models.UserRegistration
|
|
form_class = forms.CarDealershipRegistrationForm
|
|
template_name = "account/signup-wizard.html"
|
|
success_url = reverse_lazy("registration_success")
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["title"] = _("Car Dealership Registration")
|
|
return context
|
|
|
|
def form_valid(self, form):
|
|
response = super().form_valid(form)
|
|
messages.success(
|
|
self.request,
|
|
_("Your request has been submitted. We will contact you soon."),
|
|
)
|
|
return response
|
|
|
|
|
|
def payment_result(request):
|
|
s = request.GET.get("status")
|
|
if s == "success":
|
|
return render(request, "plans/payment_success.html")
|
|
return render(request, "plans/payment_failed.html")
|
|
|
|
|
|
@require_POST
|
|
def create_estimate_for_car(request, dealer_slug, slug):
|
|
car = get_object_or_404(models.Car, slug=slug)
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
|
|
if request.method == "POST":
|
|
form = forms.CarDetailsEstimateCreate(request.POST)
|
|
if form.is_valid():
|
|
customer = form.cleaned_data["customer"]
|
|
estimate = create_estimate_(dealer, car, customer)
|
|
|
|
if request.is_staff:
|
|
models.ExtraInfo.objects.create(
|
|
dealer=dealer,
|
|
content_object=estimate,
|
|
related_object=request.staff,
|
|
created_by=request.user,
|
|
data={"vat_rate": dealer.vat_rate, "discount": 0},
|
|
)
|
|
else:
|
|
models.ExtraInfo.objects.create(
|
|
dealer=dealer,
|
|
content_object=estimate,
|
|
related_object=request.user,
|
|
created_by=request.user,
|
|
data={"vat_rate": dealer.vat_rate, "discount": 0},
|
|
)
|
|
|
|
messages.success(request, "Estimate created successfully.")
|
|
return redirect("estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk)
|
|
else:
|
|
messages.error(request, "Please correct the errors below.")
|
|
return redirect("car_detail", dealer_slug=dealer.slug, slug=car.slug)
|
|
|
|
|
|
@require_POST
|
|
def estimate_create_from_opportunity(request, dealer_slug, slug):
|
|
opportunity = get_object_or_404(models.Opportunity, slug=slug)
|
|
if opportunity.estimate:
|
|
messages.error(
|
|
request,
|
|
"An estimate has already been created for this opportunity.",
|
|
)
|
|
return redirect(
|
|
"opportunity_detail", dealer_slug=dealer_slug, slug=opportunity.slug
|
|
)
|
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
car = opportunity.car
|
|
customer = opportunity.customer
|
|
|
|
if not all([dealer, car, customer]):
|
|
messages.error(request, "Please correct the errors below.")
|
|
return redirect(
|
|
"opportunity_detail", dealer_slug=dealer.slug, slug=opportunity.slug
|
|
)
|
|
|
|
estimate = create_estimate_(dealer, car, customer)
|
|
|
|
if request.is_staff:
|
|
models.ExtraInfo.objects.create(
|
|
dealer=dealer,
|
|
content_object=estimate,
|
|
related_object=request.staff,
|
|
created_by=request.user,
|
|
data={"vat_rate": dealer.vat_rate, "discount": 0},
|
|
)
|
|
else:
|
|
models.ExtraInfo.objects.create(
|
|
dealer=dealer,
|
|
content_object=estimate,
|
|
related_object=request.user,
|
|
created_by=request.user,
|
|
data={"vat_rate": dealer.vat_rate, "discount": 0},
|
|
)
|
|
|
|
messages.success(request, "Estimate created successfully.")
|
|
return redirect("estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk)
|