indexing and query optimization

This commit is contained in:
ismail 2025-07-13 16:25:21 +03:00
parent db751f7837
commit 0bc0d17f43
16 changed files with 554 additions and 45 deletions

View File

@ -4,6 +4,8 @@ from django.conf.urls.static import static
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns
from inventory import views
from debug_toolbar.toolbar import debug_toolbar_urls
# import debug_toolbar
from schema_graph.views import Schema
@ -15,7 +17,7 @@ urlpatterns = [
path("api-auth/", include("rest_framework.urls")),
path("api/", include("api.urls")),
# path('dj-rest-auth/', include('dj_rest_auth.urls')),
]
] + debug_toolbar_urls()
urlpatterns += i18n_patterns(
path("admin/", admin.site.urls),
path("switch_language/", views.switch_language, name="switch_language"),
@ -32,3 +34,5 @@ urlpatterns += i18n_patterns(
)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Binary file not shown.

View File

@ -43,6 +43,7 @@ from django.core.serializers.json import DjangoJSONEncoder
from appointment.models import StaffMember
from plans.quota import get_user_quota
from plans.models import UserPlan
from django.db.models import Q
# from plans.models import AbstractPlan
# from simple_history.models import HistoricalRecords
@ -243,6 +244,11 @@ class CarMake(models.Model, LocalizedNameMixin):
class Meta:
verbose_name = _("Make")
indexes = [
models.Index(fields=['name'], name='car_make_name_idx'),
models.Index(fields=['is_sa_import'], name='car_make_sa_import_idx'),
models.Index(fields=['car_type'], name='car_make_type_idx'),
]
class CarModel(models.Model, LocalizedNameMixin):
@ -272,6 +278,11 @@ class CarModel(models.Model, LocalizedNameMixin):
class Meta:
verbose_name = _("Model")
indexes = [
models.Index(fields=['id_car_make'], name='car_model_make_idx'),
models.Index(fields=['name'], name='car_model_name_idx'),
models.Index(fields=['id_car_make', 'name'], name='car_model_make_name_idx'),
]
class CarSerie(models.Model, LocalizedNameMixin):
@ -306,6 +317,12 @@ class CarSerie(models.Model, LocalizedNameMixin):
class Meta:
verbose_name = _("Series")
indexes = [
models.Index(fields=['id_car_model'], name='car_serie_model_idx'),
models.Index(fields=['year_begin', 'year_end'], name='car_serie_years_idx'),
models.Index(fields=['name'], name='car_serie_name_idx'),
models.Index(fields=['generation_name'], name='car_serie_generation_idx'),
]
class CarTrim(models.Model, LocalizedNameMixin):
@ -339,6 +356,11 @@ class CarTrim(models.Model, LocalizedNameMixin):
class Meta:
verbose_name = _("Trim")
indexes = [
models.Index(fields=['id_car_serie'], name='car_trim_serie_idx'),
models.Index(fields=['start_production_year', 'end_production_year'], name='car_trim_prod_years_idx'),
models.Index(fields=['name'], name='car_trim_name_idx'),
]
class CarEquipment(models.Model, LocalizedNameMixin):
@ -359,6 +381,11 @@ class CarEquipment(models.Model, LocalizedNameMixin):
class Meta:
verbose_name = _("Equipment")
indexes = [
models.Index(fields=['id_car_trim'], name='car_equipment_trim_idx'),
models.Index(fields=['year_begin'], name='car_equipment_year_idx'),
models.Index(fields=['name'], name='car_equipment_name_idx'),
]
class CarSpecification(models.Model, LocalizedNameMixin):
@ -390,6 +417,10 @@ class CarSpecification(models.Model, LocalizedNameMixin):
class Meta:
verbose_name = _("Specification")
indexes = [
models.Index(fields=['id_parent'], name='car_spec_parent_idx'),
models.Index(fields=['name'], name='car_spec_name_idx'),
]
class CarSpecificationValue(models.Model):
@ -406,6 +437,11 @@ class CarSpecificationValue(models.Model):
class Meta:
verbose_name = _("Specification Value")
indexes = [
models.Index(fields=['id_car_trim'], name='car_spec_val_trim_idx'),
models.Index(fields=['id_car_specification'], name='car_spec_val_spec_idx'),
models.Index(fields=['id_car_trim', 'id_car_specification'], name='car_spec_val_trim_spec_idx'),
]
class CarOption(models.Model, LocalizedNameMixin):
@ -437,6 +473,10 @@ class CarOption(models.Model, LocalizedNameMixin):
class Meta:
verbose_name = _("Option")
indexes = [
models.Index(fields=['id_parent'], name='car_option_parent_idx'),
models.Index(fields=['name'], name='car_option_name_idx'),
]
class CarOptionValue(models.Model):
@ -456,6 +496,12 @@ class CarOptionValue(models.Model):
class Meta:
verbose_name = _("Option Value")
indexes = [
models.Index(fields=['id_car_option'], name='car_opt_val_option_idx'),
models.Index(fields=['id_car_equipment'], name='car_opt_val_equipment_idx'),
models.Index(fields=['is_base'], name='car_opt_val_is_base_idx'),
models.Index(fields=['id_car_option', 'id_car_equipment'], name='cov_option_equipment_idx'),
]
class CarTransferStatusChoices(models.TextChoices):
@ -614,6 +660,26 @@ class Car(Base):
class Meta:
verbose_name = _("Car")
verbose_name_plural = _("Cars")
indexes = [
models.Index(fields=['vin'], name='car_vin_idx'),
models.Index(fields=['year'], name='car_year_idx'),
models.Index(fields=['status'], name='car_status_idx'),
models.Index(fields=['dealer'], name='car_dealer_idx'),
models.Index(fields=['vendor'], name='car_vendor_idx'),
models.Index(fields=['id_car_make'], name='car_make_idx'),
models.Index(fields=['id_car_model'], name='car_model_idx'),
models.Index(fields=['id_car_serie'], name='car_serie_idx'),
models.Index(fields=['id_car_trim'], name='car_trim_idx'),
models.Index(fields=['id_car_make', 'id_car_model'], name='car_make_model_idx'),
models.Index(fields=['id_car_make', 'year'], name='car_make_year_idx'),
models.Index(fields=['dealer', 'status'], name='car_dealer_status_idx'),
models.Index(fields=['vendor', 'status'], name='car_vendor_status_idx'),
models.Index(fields=['year', 'status'], name='car_year_status_idx'),
models.Index(fields=['status'], name='car_active_status_idx',
condition=Q(status=CarStatusChoices.AVAILABLE)),
]
def __str__(self):
make = self.id_car_make.name if self.id_car_make else "Unknown Make"
@ -891,6 +957,12 @@ class CarFinance(models.Model):
class Meta:
verbose_name = _("Car Financial Details")
verbose_name_plural = _("Car Financial Details")
indexes = [
models.Index(fields=['car'], name='car_finance_car_idx'),
models.Index(fields=['cost_price'], name='car_finance_cost_price_idx'),
models.Index(fields=['selling_price'], name='car_finance_selling_price_idx'),
models.Index(fields=['discount_amount'], name='car_finance_discount_idx'),
]
class ExteriorColors(models.Model, LocalizedNameMixin):
@ -901,6 +973,10 @@ class ExteriorColors(models.Model, LocalizedNameMixin):
class Meta:
verbose_name = _("Exterior Colors")
verbose_name_plural = _("Exterior Colors")
indexes = [
models.Index(fields=['name'], name='exterior_color_name_idx'),
models.Index(fields=['arabic_name'], name='exterior_color_arabic_name_idx'),
]
def __str__(self):
return f"{self.name} ({self.rgb})"
@ -914,6 +990,10 @@ class InteriorColors(models.Model, LocalizedNameMixin):
class Meta:
verbose_name = _("Interior Colors")
verbose_name_plural = _("Interior Colors")
indexes = [
models.Index(fields=['name'], name='interior_color_name_idx'),
models.Index(fields=['arabic_name'], name='interior_color_arabic_name_idx'),
]
def __str__(self):
return f"{self.name} ({self.rgb})"
@ -932,6 +1012,11 @@ class CarColors(models.Model):
verbose_name = _("Color")
verbose_name_plural = _("Colors")
unique_together = ("car", "exterior", "interior")
indexes = [
models.Index(fields=['exterior'], name='car_colors_exterior_idx'),
models.Index(fields=['interior'], name='car_colors_interior_idx'),
models.Index(fields=['exterior', 'interior'], name='car_colors_ext_int_combo_idx'),
]
def __str__(self):
return f"{self.car} ({self.exterior.name}) ({self.interior.name})"
@ -1108,6 +1193,7 @@ class Dealer(models.Model, LocalizedNameMixin):
class Meta:
verbose_name = _("Dealer")
verbose_name_plural = _("Dealers")
indexes = [models.Index(fields=["name"])]
# permissions = [
# ('change_dealer_type', 'Can change dealer type'),
# ]
@ -1188,7 +1274,7 @@ class Staff(models.Model, LocalizedNameMixin):
@property
def groups(self):
return CustomGroup.objects.filter(pk__in=[x.customgroup.pk for x in self.user.groups.all()])
return CustomGroup.objects.select_related("group").filter(pk__in=[x.customgroup.pk for x in self.user.groups.all()])
def clear_groups(self):
self.remove_superuser_permission()
@ -1217,6 +1303,10 @@ class Staff(models.Model, LocalizedNameMixin):
class Meta:
verbose_name = _("Staff")
verbose_name_plural = _("Staff")
indexes = [
models.Index(fields=["name"]),
models.Index(fields=["staff_type"]),
]
permissions = []
def __str__(self):
@ -1372,6 +1462,13 @@ class Customer(models.Model):
class Meta:
verbose_name = _("Customer")
verbose_name_plural = _("Customers")
indexes = [
models.Index(fields=["title"]),
models.Index(fields=["first_name"]),
models.Index(fields=["last_name"]),
models.Index(fields=["email"]),
models.Index(fields=["phone_number"]),
]
def __str__(self):
# middle = f" {self.middle_name}" if self.middle_name else ""
@ -1510,6 +1607,11 @@ class Organization(models.Model, LocalizedNameMixin):
class Meta:
verbose_name = _("Organization")
verbose_name_plural = _("Organizations")
indexes = [
models.Index(fields=["name"]),
models.Index(fields=["email"]),
models.Index(fields=["phone_number"]),
]
def __str__(self):
return self.name
@ -1699,6 +1801,17 @@ class Lead(models.Model):
class Meta:
verbose_name = _("Lead")
verbose_name_plural = _("Leads")
indexes = [
models.Index(fields=["dealer"]),
models.Index(fields=["customer"]),
models.Index(fields=["organization"]),
models.Index(fields=["staff"]),
models.Index(fields=["first_name"]),
models.Index(fields=["last_name"]),
models.Index(fields=["email"]),
models.Index(fields=["phone_number"]),
models.Index(fields=["created"]),
]
def __str__(self):
return f"{self.first_name} {self.last_name}"
@ -1873,6 +1986,14 @@ class Schedule(models.Model):
class Meta:
ordering = ["-scheduled_at"]
verbose_name = _("Schedule")
verbose_name_plural = _("Schedules")
indexes = [
models.Index(fields=["dealer"]),
models.Index(fields=["customer"]),
models.Index(fields=["content_type", "object_id"]),
models.Index(fields=["scheduled_at"]),
]
class LeadStatusHistory(models.Model):
@ -2045,6 +2166,14 @@ class Opportunity(models.Model):
class Meta:
verbose_name = _("Opportunity")
verbose_name_plural = _("Opportunities")
indexes = [
models.Index(fields=["dealer"]),
models.Index(fields=["customer"]),
models.Index(fields=["car"]),
models.Index(fields=["lead"]),
models.Index(fields=["organization"]),
models.Index(fields=["created"]),
]
def __str__(self):
if self.customer:
@ -2069,6 +2198,19 @@ class Notes(models.Model):
class Meta:
verbose_name = _("Note")
verbose_name_plural = _("Notes")
indexes = [
models.Index(fields=['dealer'], name='note_dealer_idx'),
models.Index(fields=['created_by'], name='note_created_by_idx'),
models.Index(fields=['content_type'], name='note_content_type_idx'),
models.Index(fields=['content_type', 'object_id'], name='note_content_object_idx'),
models.Index(fields=['created'], name='note_created_date_idx'),
models.Index(fields=['updated'], name='note_updated_date_idx'),
models.Index(fields=['dealer', 'created'], name='note_dealer_created_idx'),
models.Index(fields=['content_type', 'object_id', 'created'],
name='note_content_obj_created_idx'),
]
def __str__(self):
return f"Note by {self.created_by.first_name} on {self.content_object}"
@ -2099,6 +2241,19 @@ class Tasks(models.Model):
class Meta:
verbose_name = _("Task")
verbose_name_plural = _("Tasks")
indexes = [
models.Index(fields=['dealer'], name='task_dealer_idx'),
models.Index(fields=['created_by'], name='task_created_by_idx'),
models.Index(fields=['content_type'], name='task_content_type_idx'),
models.Index(fields=['content_type', 'object_id'], name='task_content_object_idx'),
models.Index(fields=['created'], name='task_created_date_idx'),
models.Index(fields=['updated'], name='task_updated_date_idx'),
models.Index(fields=['dealer', 'created'], name='task_dealer_created_idx'),
models.Index(fields=['content_type', 'object_id', 'created'],
name='task_content_obj_created_idx'),
]
def __str__(self):
return f"Task by {self.created_by.email} on {self.content_object}"
@ -2127,6 +2282,17 @@ class Email(models.Model):
class Meta:
verbose_name = _("Email")
verbose_name_plural = _("Emails")
indexes = [
models.Index(fields=['created_by'], name='email_created_by_idx'),
models.Index(fields=['content_type'], name='email_content_type_idx'),
models.Index(fields=['content_type', 'object_id'], name='email_content_object_idx'),
models.Index(fields=['created'], name='email_created_date_idx'),
models.Index(fields=['updated'], name='email_updated_date_idx'),
models.Index(fields=['content_type', 'object_id', 'created'],
name='email_content_obj_created_idx'),
]
def __str__(self):
return f"Email by {self.created_by.first_name} on {self.content_object}"
@ -2152,6 +2318,17 @@ class Activity(models.Model):
class Meta:
verbose_name = _("Activity")
verbose_name_plural = _("Activities")
indexes = [
models.Index(fields=['created_by'], name='activity_created_by_idx'),
models.Index(fields=['content_type'], name='activity_content_type_idx'),
models.Index(fields=['content_type', 'object_id'], name='activity_content_object_idx'),
models.Index(fields=['created'], name='activity_created_date_idx'),
models.Index(fields=['updated'], name='activity_updated_date_idx'),
models.Index(fields=['content_type', 'object_id', 'created'],
name='a_content_obj_created_idx'),
]
def __str__(self):
return f"{self.get_activity_type_display()} by {self.created_by.get_full_name} on {self.content_object}"
@ -2170,6 +2347,12 @@ class Notification(models.Model):
verbose_name_plural = _("Notifications")
ordering = ["-created"]
indexes = [
models.Index(fields=['user'], name='notification_user_idx'),
models.Index(fields=['is_read'], name='notification_is_read_idx'),
models.Index(fields=['created'], name='notification_created_date_idx'),
]
def __str__(self):
return self.message
@ -2233,6 +2416,12 @@ class Vendor(models.Model, LocalizedNameMixin):
class Meta:
verbose_name = _("Vendor")
verbose_name_plural = _("Vendors")
indexes = [
models.Index(fields=['slug'], name='vendor_slug_idx'),
models.Index(fields=['active'], name='vendor_active_idx'),
models.Index(fields=['crn'], name='vendor_crn_idx'),
models.Index(fields=['vrn'], name='vendor_vrn_idx'),
]
def __str__(self):
return self.name
@ -2474,9 +2663,21 @@ class SaleOrder(models.Model):
)
class Meta:
verbose_name = "Sales Order"
verbose_name_plural = "Sales Orders"
verbose_name = _("Sales Order")
verbose_name_plural = _("Sales Orders")
ordering = ["-order_date"] # Order by most recent first
indexes = [
models.Index(fields=["dealer"]),
models.Index(fields=["estimate"]),
models.Index(fields=["invoice"]),
models.Index(fields=["opportunity"]),
models.Index(fields=["customer"]),
models.Index(fields=["status"]),
models.Index(fields=["order_date"]),
models.Index(fields=["expected_delivery_date"]),
models.Index(fields=["actual_delivery_date"]),
models.Index(fields=["cancelled_date"]),
]
def save(self, *args, **kwargs):
if not self.formatted_order_id:
@ -2534,6 +2735,14 @@ class CustomGroup(models.Model):
group = models.OneToOneField(
"auth.Group", verbose_name=_("Group"), on_delete=models.CASCADE
)
class Meta:
verbose_name = _("Custom Group")
verbose_name_plural = _("Custom Groups")
indexes = [
models.Index(fields=["name"]),
models.Index(fields=["dealer"]),
models.Index(fields=["group"]),
]
@property
def entity(self):
@ -2669,7 +2878,7 @@ class CustomGroup(models.Model):
app="inventory",
allowed_models=[
"saleorder",
"payment",
# "payment",
"staff",
"schedule",
"activity",
@ -2939,6 +3148,13 @@ class PoItemsUploaded(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = _("PO Items")
verbose_name_plural = _("PO Items")
indexes = [
models.Index(fields=["po"]),
models.Index(fields=["item"]),
]
def get_name(self):
return self.item.item.name.split('||')
class ExtraInfo(models.Model):
@ -2990,7 +3206,9 @@ class ExtraInfo(models.Model):
models.Index(fields=['content_type', 'object_id']),
models.Index(fields=['related_content_type', 'related_object_id']),
]
verbose_name_plural = "Extra Info"
verbose_name_plural = _("Extra Info")
verbose_name = _("Extra Info")
def __str__(self):
return f"ExtraInfo for {self.content_object} ({self.content_type})"

View File

@ -316,12 +316,18 @@ class BasePurchaseOrderActionActionView(LoginRequiredMixin,
)
except ValidationError as e:
# --- Single-line log for ValidationError ---
print(f"User {user_username} encountered a validation error "
f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. "
f"Error: {e}")
logger.warning(
f"User {user_username} encountered a validation error "
f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. "
f"Error: {e}"
)
except AttributeError as e:
print(f"User {user_username} encountered an AttributeError "
f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. "
f"Error: {e}")
logger.warning(
f"User {user_username} encountered an AttributeError "
f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. "
@ -421,11 +427,16 @@ class BillModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateVie
user_model=dealer.entity.admin,
instance=self.object
)
return form_class(
form = form_class(
entity_model=entity_model,
user_model=dealer.entity.admin,
**self.get_form_kwargs()
)
try:
form.initial['amount_paid'] = self.object.get_itemtxs_data()[1]["total_amount__sum"]
except Exception as e:
print(e)
return form
def get_form_class(self):
bill_model: BillModel = self.object

View File

@ -1,10 +1,10 @@
from decimal import Decimal
from django.urls import reverse
from inventory.tasks import create_coa_accounts, create_make_accounts
from django.contrib.auth.models import Group
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from appointment.models import Service
from django.utils.translation import gettext_lazy as _
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth import get_user_model
@ -941,6 +941,11 @@ def create_po_item_upload(sender,instance,created,**kwargs):
dealer = models.Dealer.objects.get(entity=instance.entity)
models.PoItemsUploaded.objects.create(dealer=dealer,po=instance, item=item, status="fulfilled")
@receiver(post_save, sender=models.Staff)
def add_service_to_staff(sender,instance,created,**kwargs):
if created:
for service in Service.objects.all():
instance.staff_member.services_offered.add(service)
##########################################################
######################Notification########################

View File

@ -473,7 +473,7 @@ def set_invoice_payment(dealer, entity, invoice, amount, payment_method):
calculator = CarFinanceCalculator(invoice)
finance_data = calculator.get_finance_data()
# handle_account_process(invoice, amount, finance_data)
handle_account_process(invoice, amount, finance_data)
invoice.make_payment(amount)
invoice.save()

View File

@ -206,7 +206,7 @@ from .tasks import create_accounts_for_make, create_user_dealer, send_email
# djago easy audit log
from easyaudit.models import RequestEvent, CRUDEvent, LoginEvent
from django_q.tasks import async_task
from django.db.models import Prefetch
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
@ -1142,7 +1142,7 @@ class CarListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
def get_queryset(self):
dealer = get_user_type(self.request)
qs = super().get_queryset()
qs = super().get_queryset().prefetch_related("id_car_make", "id_car_model", "id_car_trim","finances","colors")
qs = qs.filter(dealer=dealer)
status = self.request.GET.get("status")
search = self.request.GET.get("search")
@ -1194,8 +1194,193 @@ def inventory_stats_view(request, dealer_slug):
"inventory/inventory_stats.html" template.
:rtype: HttpResponse
"""
# cars = (
# models.Car.objects
# .filter(dealer=request.dealer)
# .select_related(
# "id_car_make",
# "id_car_model",
# "id_car_trim__id_car_serie__id_car_model__id_car_make"
# )
# .only(
# "id",
# "id_car_make__id_car_make",
# "id_car_make__slug",
# "id_car_make__name",
# "id_car_make__arabic_name",
# "id_car_model__id_car_model",
# "id_car_model__slug",
# "id_car_model__name",
# "id_car_model__arabic_name",
# "id_car_trim__id_car_trim",
# "id_car_trim__slug",
# "id_car_trim__name",
# "id_car_trim__id_car_serie__id_car_model__id_car_make",
# "id_car_trim__id_car_serie__id_car_model__id_car_model",
# )
# .order_by('id_car_make', 'id_car_model', 'id_car_trim')
# )
# Base queryset for cars belonging to the dealer
# # Get counts in optimized queries
# total_cars = cars.count()
# reserved_cars = models.CarReservation.objects.filter(
# car__dealer=request.dealer
# ).count()
# # Prefetch related data if needed for additional fields
# cars = cars.prefetch_related(
# Prefetch('colors', queryset=models.CarColors.objects.select_related('exterior', 'interior'))
# )
# # Get distinct makes, models, trims in database-compatible way
# makes = (
# cars.order_by('id_car_make')
# .values_list('id_car_make', flat=True)
# .distinct()
# )
# _models = (
# cars.order_by('id_car_model')
# .values_list('id_car_model', flat=True)
# .distinct()
# )
# trims = (
# cars.order_by('id_car_trim')
# .values_list('id_car_trim', flat=True)
# .distinct()
# )
# # Get counts by make/model/trim
# make_counts = dict(
# cars.values_list('id_car_make')
# .annotate(count=Count('id'))
# .order_by('id_car_make')
# )
# model_counts = dict(
# cars.values_list('id_car_model')
# .annotate(count=Count('id'))
# .order_by('id_car_model')
# )
# trim_counts = dict(
# cars.values_list('id_car_trim')
# .annotate(count=Count('id'))
# .order_by('id_car_trim')
# )
# # Build inventory structure
# inventory = {}
# # Process makes
# make_objects = {
# m.id_car_make: m for m in
# models.CarMake.objects.filter(id_car_make__in=makes)
# .only('id_car_make', 'slug', 'name', 'arabic_name')
# }
# for make_id in makes:
# if not make_id:
# continue
# make_obj = make_objects.get(make_id)
# if not make_obj:
# continue
# inventory[make_id] = {
# "make_id": make_id,
# "slug": make_obj.slug,
# "make_name": make_obj.get_local_name(),
# "total_cars": make_counts.get(make_id, 0),
# "models": {},
# }
# # Process models
# model_objects = {
# m.id_car_model: m for m in
# models.CarModel.objects.filter(id_car_model__in=_models)
# .select_related('id_car_make')
# .only('id_car_model', 'slug', 'name', 'arabic_name', 'id_car_make')
# }
# for model_id in _models:
# if not model_id:
# continue
# model_obj = model_objects.get(model_id)
# if not model_obj:
# continue
# make_id = model_obj.id_car_make.id_car_make
# if make_id not in inventory:
# continue
# inventory[make_id]["models"][model_id] = {
# "model_id": model_id,
# "slug": model_obj.slug,
# "model_name": model_obj.get_local_name(),
# "total_cars": model_counts.get(model_id, 0),
# "trims": {},
# }
# # Process trims
# trim_objects = {
# t.id_car_trim: t for t in
# models.CarTrim.objects.filter(id_car_trim__in=trims)
# .select_related('id_car_serie__id_car_model__id_car_make')
# .only('id_car_trim', 'slug', 'name', 'id_car_serie__id_car_model__id_car_make')
# }
# for trim_id in trims:
# if not trim_id:
# continue
# trim_obj = trim_objects.get(trim_id)
# if not trim_obj:
# continue
# make_id = trim_obj.id_car_serie.id_car_model.id_car_make.id_car_make
# model_id = trim_obj.id_car_serie.id_car_model.id_car_model
# if make_id not in inventory or model_id not in inventory[make_id]["models"]:
# continue
# inventory[make_id]["models"][model_id]["trims"][trim_id] = {
# "trim_id": trim_id,
# "slug": trim_obj.slug,
# "trim_name": trim_obj.name,
# "total_cars": trim_counts.get(trim_id, 0),
# }
# # Convert to final structure
# 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()
# if model_data["model_id"] # Skip empty models
# ],
# }
# for make_data in inventory.values()
# if make_data["make_id"] # Skip empty makes
# ],
# }
###############################################
# # Base queryset for cars belonging to the dealer
cars = models.Car.objects.filter(dealer=request.dealer)
# Count for total, reserved, showroom, and unreserved cars
@ -1435,6 +1620,18 @@ class CarDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
context_object_name = "car"
permission_required = ["inventory.view_car"]
def get_queryset(self):
qs = super().get_queryset()
qs = qs.select_related(
"id_car_make",
"id_car_model",
"id_car_trim",
"colors",
"finances",
"vendor",
"registrations"
)
return qs
class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
"""
@ -3862,7 +4059,7 @@ class BankAccountDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
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')
@ -5587,25 +5784,25 @@ class LeadListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
def get_queryset(self):
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")
qs = models.Lead.objects.select_related("staff","id_car_make","id_car_model","customer").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(email__icontains=query)
| Q(phone_number__icontains=query)
| Q(next_action__icontains=query)
| Q(staff__name__icontains=query))
| Q(last_name__icontains=query)
| Q(id_car_make__name__icontains=query)
| Q(id_car_model__name__icontains=query)
| Q(email__icontains=query)
| Q(phone_number__icontains=query)
| Q(next_action__icontains=query)
| Q(staff__name__icontains=query))
if self.request.is_dealer:
return qs
if self.request.user.is_staff:
staff = getattr(self.request.user.staffmember, "staff", None)
return qs.filter(staff=staff)
return models.Lead.objects.none()
class LeadDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""
@ -6674,9 +6871,9 @@ class OpportunityListView(LoginRequiredMixin,PermissionRequiredMixin, ListView):
Q(customer__first_name__icontains=search)
| Q(customer__last_name__icontains=search)
| Q(customer__email__icontains=search)
)
# Stage filter
stage = self.request.GET.get("stage")
@ -7095,7 +7292,7 @@ class ItemExpenseListView(LoginRequiredMixin, PermissionRequiredMixin, ListView)
# def get_queryset(self):
# dealer = get_user_type(self.request)
# return dealer.entity.get_items_expenses()
def get_queryset(self):
dealer = get_user_type(self.request)
query=self.request.GET.get('q')
@ -9971,9 +10168,9 @@ class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListVie
if query:
qs=apply_search_filters(qs,query)
return qs
# def get_queryset(self):
# dealer = get_user_type(self.request)
# entity = dealer.entity
@ -10107,11 +10304,28 @@ class BillModelActionMarkAsInReviewView(BaseBillActionView):
class BillModelActionMarkAsApprovedView(BaseBillActionView):
action_name = "mark_as_approved"
def get_redirect_url(self, dealer_slug, entity_slug, bill_pk, *args, **kwargs):
if self.request.is_manager:
messages.add_message(
self.request,
message="Bill updated successfully.",
level=messages.SUCCESS,
)
return reverse("home",kwargs={"dealer_slug": dealer_slug})
return reverse(
"bill-update",
kwargs={
"dealer_slug": dealer_slug,
"entity_slug": entity_slug,
"bill_pk": bill_pk,
},
)
class BillModelActionMarkAsPaidView(BaseBillActionView):
action_name = "mark_as_paid"
class BillModelActionDeleteView(BaseBillActionView):
action_name = "mark_as_delete"

View File

@ -130,4 +130,5 @@ html[dir="rtl"] .form-icon-container .form-control {
@keyframes spin {
to { transform: rotate(360deg); }
}
}

View File

@ -21,7 +21,7 @@
class="btn btn-phoenix-secondary w-100 mb-2">
<i class="fas fa-arrow-left me-2"></i>{% trans 'Back to Bill Detail' %}
</a>
<form action="{% url 'bill-update' dealer_slug=request.dealer.slug entity_slug=view.kwargs.entity_slug bill_pk=bill_model.uuid %}" method="post">
{% csrf_token %}
@ -33,15 +33,13 @@
<i class="fas fa-save me-2"></i>{% trans 'Save Bill' %}
</button>
</form>
<a href="{% url 'bill_list' request.dealer.slug %}"
class="btn btn-phoenix-info w-100 mb-2">
<i class="fas fa-list me-2"></i>{% trans 'Bill List' %}
</a>
</div>
</div>
</div>
@ -50,7 +48,6 @@
</div>
<!-- Bill Item Formset -->
<div class="col-12">
{% bill_item_formset_table itemtxs_formset %}

View File

@ -221,12 +221,15 @@
<div class="d-flex flex-wrap gap-2 mt-2">
<!-- Update Button -->
{% if perms.django_ledger.change_billmodel%}
<a href="{% url 'bill-update' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill.uuid %}" class="btn btn-phoenix-primary">
<i class="fas fa-edit me-2"></i>{% trans 'Update' %}
</a>
<button class="btn btn-phoenix-primary" {% if not request.is_accountant %} disabled {% endif %}>
<a href="{% url 'bill-update' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill.uuid %}">
<i class="fas fa-edit me-2"></i>{% trans 'Update' %}
</a>
</button>
<!-- Mark as Draft -->
{% if bill.can_draft %}
<button class="btn btn-phoenix-success"
{% if not request.is_accountant %} disabled {% endif %}
onclick="showPOModal('Mark as Draft', '{% url 'bill-action-mark-as-draft' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Draft')">
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Draft' %}
</button>
@ -234,6 +237,7 @@
<!-- Mark as Review -->
{% if bill.can_review %}
<button class="btn btn-phoenix-warning"
{% if not request.is_accountant %} disabled {% endif %}
onclick="showPOModal('Mark as Review', '{% url 'bill-action-mark-as-review' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Review')">
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Review' %}
</button>
@ -245,6 +249,11 @@
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Approved' %}
</button>
{% endif %}
{% if bill.can_approve and not request.is_manager %}
<button class="btn btn-phoenix-warning" disabled>
<i class="fas fa-hourglass-start me-2"></i><span class="text-warning">{% trans 'Waiting for Manager Approval' %}</span>
</button>
{% endif %}
<!-- Mark as Paid -->
{% if bill.can_pay %}
<button class="btn btn-phoenix-success"
@ -262,6 +271,7 @@
<!-- Cancel Button -->
{% if bill.can_cancel %}
<button class="btn btn-phoenix-danger"
{% if not request.is_accountant %} disabled {% endif %}
onclick="showPOModal('Mark as Canceled', '{% url 'bill-action-mark-as-canceled' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Canceled')">
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Canceled' %}
</button>

View File

@ -137,6 +137,29 @@
<div class="d-flex align-items-center mb-1"><span class="me-2 uil uil-file-check-alt"></span>
<h5 class="text-body-highlight fw-bold mb-0">{{ _("Lead Source")}}</h5>
</div>
{% if lead.source == 'REFERRALS' %}
<span class="ms-2 fa fa-users"></span>
{% elif lead.source == 'WHATSAPP' %}
<span class="ms-2 fa fa-whatsapp"></span>
{% elif lead.source == 'SHOWROOM' %}
<span class="ms-2 fa fa-building"></span>
{% elif lead.source == 'TIKTOK' %}
<span class="ms-2 fa fa-tiktok"></span>
{% elif lead.source == 'INSTAGRAM' %}
<span class="ms-2 fa fa-instagram"></span>
{% elif lead.source == 'X' %}
<span class="ms-2 fa fa-times-circle"></span>
{% elif lead.source == 'FACEBOOK' %}
<span class="ms-2 fa fa-facebook-f"></span>
{% elif lead.source == 'MOTORY' %}
<span class="ms-2 fa fa-car-side"></span>
{% elif lead.source == 'INFLUENCERS' %}
<span class="ms-2 fa fa-user-check"></span>
{% elif lead.source == 'YOUTUBE' %}
<span class="ms-2 fa fa-youtube"></span>
{% elif lead.source == 'CAMPAIGN' %}
<span class="ms-2 fa fa-bullhorn"></span>
{% endif %}
<span class="text-body-secondary">{{ lead.source|upper }}</span>
</div>
<div class="mb-3">
@ -490,6 +513,7 @@
<th class="sort align-middle pe-3 text-uppercase" scope="col" data-sort="sent" style="width:15%; min-width:130px">Assigned to</th>
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="date" style="min-width:165px">Due Date</th>
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="date" style="min-width:165px">Completed</th>
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="date" style="min-width:165px"></th>
</tr>
</thead>
<tbody class="list" id="all-tasks-table-body">

View File

@ -87,7 +87,7 @@
<span class="fas fa-ellipsis-h fs-10"></span>
</button>
<div class="dropdown-menu dropdown-menu-end py-2">
{% if perms.django_ledger.change_ledgermodel%}
{% if perms.django_ledger.change_ledgermodel %}
{% if ledger.can_lock %}
<a href="{% url 'ledger-action-lock' dealer_slug=request.dealer.slug entity_slug=entity_slug ledger_pk=ledger.uuid %}"
class="dropdown-item has-text-info has-text-weight-bold">{% trans 'Lock' %}</a>

View File

@ -1,3 +1,19 @@
<style>
.fade-out {
animation: fadeOut 1s ease-out forwards;
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
</style>
<li class="nav-item dropdown">
<!-- Notification counter -->
<div class="notification-count">
@ -236,6 +252,10 @@
notificationCard.classList.remove('unread');
notificationCard.classList.add('read');
updateCounter('decrement');
notificationCard.closest('.notification-card').classList.add('fade-out');
setTimeout(() => {
notificationCard.closest('.notification-card').remove();
}, 200);
}
}
});

View File

@ -18,7 +18,7 @@
<h3 class="">
{{ _("Purchase Orders") |capfirst }}
</h2>
{% if perms.django_ledger.add_purchaseordermodel%}
{% if perms.django_ledger.add_purchaseordermodel%}
<div>
{% if perms.django_ledger.add_purchaseordermodel %}
<a href="{% url 'purchase_order_create' request.dealer.slug request.dealer.entity.slug %}"
@ -74,7 +74,7 @@
{% endif %}
{% if po.po_status == 'fulfilled' %}
{% if perms.inventory.add_car %}
<a href="{% url 'view_items_inventory' dealer_slug=request.dealer.slug entity_slug=entity_slug po_pk=po.pk %}" class="dropdown-item text-success-dark">{% trans 'Add Inventory Items' %}</a>
<a href="{% url 'view_items_inventory' dealer_slug=request.dealer.slug entity_slug=entity_slug po_pk=po.pk %}" class="dropdown-item text-success-dark">{% trans 'Inventory Items' %}</a>
{% endif %}
{% else %}
<button class="dropdown-item text-warning-dark" disabled><span class="fas fa-exclamation-triangle me-1"></span> Fulfill the PO Before Viewing Inventory</button>

View File

@ -83,7 +83,12 @@
{% if perms.django_ledger.can_approve_estimatemodel %}
<button id="accept_estimate" onclick="setFormAction('approved')" class="btn btn-phoenix-secondary" data-bs-toggle="modal" data-bs-target="#confirmModal"><span class="d-none d-sm-inline-block"><i class="fa-solid fa-check-double"></i> {% trans 'Mark As Approved' %}</span></button>
{% endif %}
{% elif estimate.status == 'approved' %}
{% if estimate.can_approve and not request.is_manager %}
<button class="btn btn-phoenix-warning" disabled>
<i class="fas fa-hourglass-start me-2"></i><span class="text-warning">{% trans 'Waiting for Manager Approval' %}</span>
</button>
{% endif %}
{% elif estimate.status == 'approved' %}
{% if perms.django_ledger.change_estimatemodel %}
<a href="{% url 'send_email' request.dealer.slug estimate.pk %}" class="btn btn-phoenix-primary me-2"><span class="fa-regular fa-paper-plane me-sm-2"></span><span class="d-none d-sm-inline-block">{% trans 'Send Quotation' %}</span></a>
{% endif %}

View File

@ -82,7 +82,7 @@
</div>
</div>
<div class="d-flex align-items-center gap-2">
{% if perms.django_ledger.add_payment%}
{% if perms.inventory.add_payment%}
{% if invoice.invoice_status == 'in_review' %}
<button id="accept_invoice" class="btn btn-phoenix-secondary" data-bs-toggle="modal" data-bs-target="#confirmModal"><span class="d-none d-sm-inline-block"><i class="fa-solid fa-check-double"></i> {% trans 'Accept' %}</span></button>
{% endif %}