This commit is contained in:
KhanFaheed 2025-05-19 14:34:49 +03:00
commit c427a8d4db
38 changed files with 563 additions and 178 deletions

View File

@ -0,0 +1,120 @@
from inventory.models import *
from django.core.management.base import BaseCommand
from django.db import transaction, models
from django.utils.text import slugify
from django.db.models import Case, When, Value
class Command(BaseCommand):
help = 'Generate slugs for model instances with proper empty value handling'
def add_arguments(self, parser):
parser.add_argument(
'--model',
type=str,
required=True,
help='Model name (format: "app_label.ModelName")'
)
parser.add_argument(
'--field',
type=str,
default='name',
help='Field to use as slug source (default: "name")'
)
parser.add_argument(
'--batch-size',
type=int,
default=1000,
help='Number of records to process at once (default: 1000)'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Test without actually saving changes'
)
parser.add_argument(
'--fill-empty',
action='store_true',
help='Fill empty slugs with model-ID when source field is empty'
)
def handle(self, *args, **options):
model = self.get_model(options['model'])
source_field = options['field']
batch_size = options['batch_size']
dry_run = options['dry_run']
fill_empty = options['fill_empty']
queryset = model.objects.filter(models.Q(slug__isnull=True) | models.Q(slug=''))
total_count = queryset.count()
processed = 0
empty_source = 0
self.stdout.write(
self.style.SUCCESS(
f'Generating slugs for {total_count} {model._meta.model_name} records '
f'using field "{source_field}" (batch size: {batch_size})'
)
)
with transaction.atomic():
if dry_run:
self.stdout.write(self.style.WARNING('DRY RUN - No changes will be saved'))
transaction.set_rollback(True)
for offset in range(0, total_count, batch_size):
batch = queryset[offset:offset + batch_size]
updates = []
for obj in batch:
source_value = getattr(obj, source_field, '')
if not source_value:
if fill_empty:
# Fallback to model-ID when source field is empty
new_slug = f"{model._meta.model_name.lower()}-{obj.pk}"
empty_source += 1
else:
self.stdout.write(
self.style.WARNING(
f'Skipping {obj} (empty {source_field})'
)
)
continue
else:
slug_base = slugify(str(source_value))[:50] # Ensure string and truncate
new_slug = f"{slug_base}-{obj.pk}" # Guaranteed unique
updates.append((obj.pk, new_slug))
processed += 1
if updates and not dry_run:
cases = [When(pk=pk, then=Value(slug)) for pk, slug in updates]
model.objects.filter(pk__in=[u[0] for u in updates]).update(
slug=Case(*cases, output_field=models.CharField())
)
self.stdout.write(
f'Processed batch {offset//batch_size + 1}: '
f'{min(offset + batch_size, total_count)}/{total_count}'
)
stats = [
f"Total processed: {processed}",
f"Records with empty source field: {empty_source}",
f"Skipped records: {total_count - processed - empty_source}"
]
self.stdout.write(
self.style.SUCCESS('\n'.join(stats))
)
def get_model(self, model_path):
"""Get model class from 'app_label.ModelName' string"""
from django.apps import apps
try:
app_label, model_name = model_path.split('.')
return apps.get_model(app_label, model_name)
except ValueError:
raise self.style.ERROR('Model must be specified as "app_label.ModelName"')
except LookupError as e:
raise self.style.ERROR(f'Model not found: {e}')

View File

@ -1,4 +1,4 @@
# Generated by Django 5.1.7 on 2025-05-18 11:18
# Generated by Django 5.1.7 on 2025-05-18 15:52
import datetime
import django.core.validators
@ -7,6 +7,7 @@ import django.utils.timezone
import inventory.mixins
import inventory.models
import phonenumber_field.modelfields
import uuid
from decimal import Decimal
from django.conf import settings
from django.db import migrations, models
@ -28,7 +29,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Car',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='Primary Key')),
('slug', models.SlugField(blank=True, help_text='Slug for the object. If not provided, it will be generated automatically.', null=True, unique=True, verbose_name='Slug')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
('vin', models.CharField(max_length=17, unique=True, verbose_name='VIN')),
('year', models.IntegerField(verbose_name='Year')),
('status', models.CharField(choices=[('available', 'Available'), ('sold', 'Sold'), ('hold', 'Hold'), ('damaged', 'Damaged'), ('reserved', 'Reserved'), ('transfer', 'Transfer')], default='available', max_length=10, verbose_name='Status')),
@ -50,6 +54,7 @@ class Migration(migrations.Migration):
('name', models.CharField(blank=True, max_length=255, null=True)),
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('year_begin', models.IntegerField(blank=True, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
],
options={
'verbose_name': 'Equipment',
@ -61,6 +66,7 @@ class Migration(migrations.Migration):
fields=[
('id_car_make', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=255, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('logo', models.ImageField(blank=True, null=True, upload_to='car_make', verbose_name='logo')),
('is_sa_import', models.BooleanField(default=False)),
@ -166,6 +172,7 @@ class Migration(migrations.Migration):
('id_car_model', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=255, null=True)),
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('id_car_make', models.ForeignKey(db_column='id_car_make', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake')),
],
options={
@ -184,6 +191,7 @@ class Migration(migrations.Migration):
('id_car_option', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=255, null=True)),
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('id_parent', models.ForeignKey(blank=True, db_column='id_parent', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.caroption')),
],
options={
@ -230,6 +238,7 @@ class Migration(migrations.Migration):
('year_begin', models.IntegerField(blank=True, null=True)),
('year_end', models.IntegerField(blank=True, null=True)),
('generation_name', models.CharField(blank=True, max_length=255, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('id_car_model', models.ForeignKey(db_column='id_car_model', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel')),
],
options={
@ -248,6 +257,7 @@ class Migration(migrations.Migration):
('id_car_specification', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)),
('arabic_name', models.CharField(max_length=255)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('id_parent', models.ForeignKey(blank=True, db_column='id_parent', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carspecification')),
],
options={
@ -263,6 +273,7 @@ class Migration(migrations.Migration):
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('start_production_year', models.IntegerField(blank=True, null=True)),
('end_production_year', models.IntegerField(blank=True, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('id_car_serie', models.ForeignKey(db_column='id_car_serie', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carserie')),
],
options={

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-05-18 16:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='dealer',
name='slug',
field=models.SlugField(blank=True, max_length=255, null=True, unique=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-05-18 16:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0002_dealer_slug'),
]
operations = [
migrations.AddField(
model_name='customer',
name='slug',
field=models.SlugField(blank=True, editable=False, max_length=255, null=True, unique=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-05-18 16:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0003_customer_slug'),
]
operations = [
migrations.AddField(
model_name='vendor',
name='slug',
field=models.SlugField(blank=True, max_length=255, null=True, unique=True, verbose_name='Slug'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-05-18 16:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0004_vendor_slug'),
]
operations = [
migrations.AddField(
model_name='lead',
name='slug',
field=models.SlugField(blank=True, null=True, unique=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-05-18 16:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0005_lead_slug'),
]
operations = [
migrations.AlterField(
model_name='activity',
name='activity_type',
field=models.CharField(choices=[('call', 'Call'), ('sms', 'SMS'), ('email', 'Email'), ('meeting', 'Meeting'), ('whatsapp', 'WhatsApp'), ('visit', 'Visit'), ('negotiation', 'Negotiation'), ('follow_up', 'Follow Up'), ('won', 'Won'), ('lost', 'Lost'), ('closed', 'Closed'), ('converted', 'Converted'), ('transfer', 'Transfer'), ('add_car', 'Add Car'), ('sale_car', 'Sale Car'), ('reserve_car', 'Reserve Car'), ('transfer_car', 'Transfer Car'), ('remove_car', 'Remove Car'), ('create_quotation', 'Create Quotation'), ('cancel_quotation', 'Cancel Quotation'), ('create_order', 'Create Order'), ('cancel_order', 'Cancel Order'), ('create_invoice', 'Create Invoice'), ('cancel_invoice', 'Cancel Invoice')], max_length=50, verbose_name='Activity Type'),
),
]

View File

@ -1,5 +1,7 @@
import uuid
from django.contrib.auth.models import Permission
from decimal import Decimal
from django.utils.text import slugify
from django.utils import timezone
from django.core.validators import MinValueValidator, MaxValueValidator
import hashlib
@ -30,6 +32,22 @@ from plans.models import UserPlan,Quota,PlanQuota
# from plans.models import AbstractPlan
# from simple_history.models import HistoricalRecords
class Base(models.Model):
id = models.UUIDField(unique=True, editable=False, default=uuid.uuid4, primary_key=True,verbose_name=_("Primary Key"))
slug = models.SlugField(null=True, blank=True, unique=True,verbose_name=_("Slug"),
help_text=_("Slug for the object. If not provided, it will be generated automatically."))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
def clean(self):
if isinstance(self.id, str):
try:
uuid.UUID(self.id)
except ValueError:
raise ValidationError({'id': 'Invalid UUID format'})
super().clean()
class Meta:
abstract = True
class DealerUserManager(UserManager):
def create_user_with_dealer(
@ -162,11 +180,16 @@ class CarType(models.IntegerChoices):
class CarMake(models.Model, LocalizedNameMixin):
id_car_make = models.AutoField(primary_key=True)
name = models.CharField(max_length=255, blank=True, null=True)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
arabic_name = models.CharField(max_length=255, blank=True, null=True)
logo = models.ImageField(_("logo"), upload_to="car_make", blank=True, null=True)
is_sa_import = models.BooleanField(default=False)
car_type = models.SmallIntegerField(choices=CarType.choices, blank=True, null=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def __str__(self):
return self.name
@ -178,6 +201,11 @@ class CarModel(models.Model, LocalizedNameMixin):
id_car_make = models.ForeignKey(CarMake, models.DO_NOTHING, db_column="id_car_make")
name = models.CharField(max_length=255, blank=True, null=True)
arabic_name = models.CharField(max_length=255, blank=True, null=True)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def __str__(self):
return self.name
@ -196,6 +224,11 @@ class CarSerie(models.Model, LocalizedNameMixin):
year_begin = models.IntegerField(blank=True, null=True)
year_end = models.IntegerField(blank=True, null=True)
generation_name = models.CharField(max_length=255, blank=True, null=True)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def __str__(self):
return self.name
@ -213,6 +246,11 @@ class CarTrim(models.Model, LocalizedNameMixin):
arabic_name = models.CharField(max_length=255, blank=True, null=True)
start_production_year = models.IntegerField(blank=True, null=True)
end_production_year = models.IntegerField(blank=True, null=True)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def __str__(self):
return self.name
@ -227,6 +265,11 @@ class CarEquipment(models.Model, LocalizedNameMixin):
name = models.CharField(max_length=255, blank=True, null=True)
arabic_name = models.CharField(max_length=255, blank=True, null=True)
year_begin = models.IntegerField(blank=True, null=True)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def __str__(self):
return self.name
@ -242,6 +285,11 @@ class CarSpecification(models.Model, LocalizedNameMixin):
id_parent = models.ForeignKey(
"self", models.DO_NOTHING, db_column="id_parent", blank=True, null=True
)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def __str__(self):
return self.name
@ -273,6 +321,11 @@ class CarOption(models.Model, LocalizedNameMixin):
id_parent = models.ForeignKey(
"self", models.DO_NOTHING, db_column="id_parent", blank=True, null=True
)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def __str__(self):
return self.name
@ -368,7 +421,7 @@ class AdditionalServices(models.Model, LocalizedNameMixin):
return self.name + " - " + str(self.price)
class Car(models.Model):
class Car(Base):
vin = models.CharField(max_length=17, unique=True, verbose_name=_("VIN"))
dealer = models.ForeignKey(
"Dealer", models.DO_NOTHING, related_name="cars", verbose_name=_("Dealer")
@ -432,6 +485,7 @@ class Car(models.Model):
# history = HistoricalRecords()
def save(self, *args, **kwargs):
self.slug = slugify(self.vin)
self.hash = self.get_hash
super(Car, self).save(*args, **kwargs)
@ -445,6 +499,9 @@ class Car(models.Model):
trim = self.id_car_trim.name if self.id_car_trim else "Unknown Trim"
return f"{self.year} - {make} - {model} - {trim}"
@property
def product(self):
return self.dealer.entity.get_items_all().filter(name=self.vin).first()
def get_reservation(self):
return self.reservations.filter(reserved_until__gt=now()).first()
def is_reserved(self):
@ -507,7 +564,7 @@ class Car(models.Model):
"mileage": self.mileage,
"receiving_date": self.receiving_date.strftime('%Y-%m-%d %H:%M:%S'),
'hash': self.get_hash,
"id": self.id,
"id": str(self.id),
}
def get_specifications(self):
@ -830,9 +887,14 @@ class Dealer(models.Model, LocalizedNameMixin):
)
joined_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Joined At"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
objects = DealerUserManager()
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
@property
def active_plan(self):
@ -997,6 +1059,7 @@ class ActionChoices(models.TextChoices):
CALL = "call", _("Call")
SMS = "sms", _("SMS")
EMAIL = "email", _("Email")
MEETING = "meeting", _("Meeting")
WHATSAPP = "whatsapp", _("WhatsApp")
VISIT = "visit", _("Visit")
LEAD_NEGOTIATION = "negotiation", _("Negotiation")
@ -1072,6 +1135,12 @@ class Customer(models.Model):
)
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
slug = models.SlugField(max_length=255, unique=True, editable=False, null=True, blank=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(f"{self.first_name} {self.last_name}")
super().save(*args, **kwargs)
class Meta:
verbose_name = _("Customer")
@ -1348,7 +1417,12 @@ class Lead(models.Model):
auto_now_add=True, verbose_name=_("Created"), db_index=True
)
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
slug = models.SlugField(unique=True, blank=True, null=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(f"{self.first_name} {self.last_name}")
super(Lead, self).save(*args, **kwargs)
class Meta:
verbose_name = _("Lead")
verbose_name_plural = _("Leads")
@ -1684,7 +1758,12 @@ class Vendor(models.Model, LocalizedNameMixin):
upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo")
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
slug = models.SlugField(max_length=255, unique=True, verbose_name=_("Slug"), null=True,blank=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
class Meta:
verbose_name = _("Vendor")
verbose_name_plural = _("Vendors")

View File

@ -621,7 +621,7 @@ def create_make_ledger_accounts(sender, instance, created, **kwargs):
# @receiver(post_save, sender=VendorModel)
# def create_vendor_accounts(sender, instance, created, **kwargs):
# def create_vendor_accounts(sender, instance, created, **kwargs):Dealer)
# if created:
# entity = instance.entity_model
# coa = entity.get_default_coa()

View File

@ -53,7 +53,7 @@ urlpatterns = [
path("submit_plan/", views.submit_plan, name="submit_plan"),
path('payment-callback/', views.payment_callback, name='payment_callback'),
#
path("dealers/<int:pk>/settings/", views.DealerSettingsView, name="dealer_settings"),
path("dealers/<slug:slug>/settings/", views.DealerSettingsView, name="dealer_settings"),
path("dealers/assign-car-makes/", views.assign_car_makes, name="assign_car_makes"),
path("dashboards/manager/", views.ManagerDashboard.as_view(), name="manager_dashboard"),
path("dashboards/sales/", views.SalesDashboard.as_view(), name="sales_dashboard"),
@ -61,9 +61,9 @@ urlpatterns = [
path('cars/inventory/table/', views.CarListViewTable.as_view(), name="car_table"),
path("export/format/", TableExport, name="export"),
# Dealer URLs
path("dealers/<int:pk>/", views.DealerDetailView.as_view(), name="dealer_detail"),
path("dealers/<slug:slug>/", views.DealerDetailView.as_view(), name="dealer_detail"),
path(
"dealers/<int:pk>/update/",
"dealers/<slug:slug>/update/",
views.DealerUpdateView.as_view(),
name="dealer_update",
),
@ -76,7 +76,7 @@ urlpatterns = [
# CRM URLs
path("customers/", views.CustomerListView.as_view(), name="customer_list"),
path(
"customers/<int:pk>/",
"customers/<slug:slug>/",
views.CustomerDetailView.as_view(),
name="customer_detail",
),
@ -84,18 +84,18 @@ urlpatterns = [
"customers/create/", views.CustomerCreateView.as_view(), name="customer_create"
),
path(
"customers/<int:pk>/update/",
"customers/<slug:slug>/update/",
views.CustomerUpdateView.as_view(),
name="customer_update",
),
path("customers/<int:pk>/delete/", views.delete_customer, name="customer_delete"),
path("customers/<slug:slug>/delete/", views.delete_customer, name="customer_delete"),
path(
"customers/<str:customer_id>/opportunities/create/",
"customers/<slug:slug>/opportunities/create/",
views.OpportunityCreateView.as_view(),
name="create_opportunity",
),
path(
"customers/<int:pk>/add-note/",
"customers/<slug:slug>/add-note/",
views.add_note_to_customer,
name="add_note_to_customer",
),
@ -105,13 +105,13 @@ urlpatterns = [
path("crm/leads/", views.LeadListView.as_view(), name="lead_list"),
path(
"crm/leads/<int:pk>/view/", views.LeadDetailView.as_view(), name="lead_detail"
"crm/leads/<slug:slug>/view/", views.LeadDetailView.as_view(), name="lead_detail"
),
path("crm/leads/create/", views.lead_create, name="lead_create"),
path(
"crm/leads/<int:pk>/update/", views.LeadUpdateView.as_view(), name="lead_update"
),
path("crm/leads/<int:pk>/delete/", views.LeadDeleteView, name="lead_delete"),
path("crm/leads/<slug:slug>/delete/", views.LeadDeleteView, name="lead_delete"),
path("crm/leads/<int:pk>/lead-convert/", views.lead_convert, name="lead_convert"),
path("crm/leads/<int:pk>/add-note/", views.add_note_to_lead, name="add_note_to_lead"),
path('crm/leads/<int:pk>/update-note/', views.update_note, name='update_note_to_lead'),
@ -122,27 +122,27 @@ urlpatterns = [
name="update_task",
),
path(
"crm/<str:content_type>/<int:pk>/add-task/",
"crm/<str:content_type>/<slug:slug>/add-task/",
views.add_task,
name="add_task",
),
path(
"crm/<str:content_type>/<int:pk>/add-activity/",
"crm/<str:content_type>/<slug:slug>/add-activity/",
views.add_activity,
name="add_activity",
),
path(
"crm/leads/<int:pk>/send_lead_email/",
"crm/leads/<slug:slug>/send_lead_email/",
views.send_lead_email,
name="send_lead_email",
),
path(
"crm/leads/<int:pk>/send_lead_email/<int:email_pk>",
"crm/leads/<slug:slug>/send_lead_email/<int:email_pk>",
views.send_lead_email,
name="send_lead_email_with_template",
),
path(
"crm/leads/<int:pk>/schedule/",
"crm/leads/<slug:slug>/schedule/",
views.schedule_lead,
name="schedule_lead",
),
@ -229,33 +229,34 @@ urlpatterns = [
path('crm/calender/', views.EmployeeCalendarView.as_view(), name='calendar_list'),
# Vendor URLs
path("vendors", views.VendorListView.as_view(), name="vendor_list"),
path("vendors/<int:pk>/", views.vendorDetailView, name="vendor_detail"),
path("vendors/<slug:slug>/", views.vendorDetailView, name="vendor_detail"),
path("vendors/create/", views.VendorCreateView.as_view(), name="vendor_create"),
path(
"vendors/<int:pk>/update/",
"vendors/<slug:slug>/update/",
views.VendorUpdateView.as_view(),
name="vendor_update",
),
path(
"vendors/<int:pk>/delete/",
"vendors/<slug:slug>/delete/",
views.delete_vendor,
name="vendor_delete",
),
# Car URLs
path("cars/add/", views.CarCreateView.as_view(), name="car_add"),
path("cars/inventory/", views.CarInventory.as_view(), name="car_inventory_all"),
path(
"cars/inventory/<int:make_id>/<int:model_id>/<int:trim_id>/",
"cars/inventory/<slug:make_id>/<slug:model_id>/<slug:trim_id>/",
views.CarInventory.as_view(),
name="car_inventory",
),
path("cars/inventory/stats", views.inventory_stats_view, name="inventory_stats"),
path("cars/inventory/list", views.CarListView.as_view(), name="car_list"),
path("cars/<int:pk>/", views.CarDetailView.as_view(), name="car_detail"),
path("cars/<int:pk>/history/", views.car_history, name="car_history"),
path("cars/<int:pk>/update/", views.CarUpdateView.as_view(), name="car_update"),
path("cars/<int:pk>/delete/", views.CarDeleteView.as_view(), name="car_delete"),
path("cars/<slug:slug>/", views.CarDetailView.as_view(), name="car_detail"),
path("cars/<slug:slug>/history/", views.car_history, name="car_history"),
path("cars/<slug:slug>/update/", views.CarUpdateView.as_view(), name="car_update"),
path("cars/<slug:slug>/delete/", views.CarDeleteView.as_view(), name="car_delete"),
path(
"cars/<int:car_pk>/finance/create/",
"cars/<slug:slug>/finance/create/",
views.CarFinanceCreateView.as_view(),
name="car_finance_create",
),
@ -264,43 +265,42 @@ urlpatterns = [
views.CarFinanceUpdateView.as_view(),
name="car_finance_update",
),
path("cars/add/", views.CarCreateView.as_view(), name="car_add"),
path("ajax/", views.AjaxHandlerView.as_view(), name="ajax_handler"),
path(
"cars/<int:car_pk>/add-color/", views.CarColorCreate.as_view(), name="add_color"
"cars/<slug:slug>/add-color/", views.CarColorCreate.as_view(), name="add_color"
),
path(
"cars/<int:car_pk>/location/add/",
"cars/<slug:slug>/location/add/",
views.CarLocationCreateView.as_view(),
name="add_car_location",
),
path(
"cars/<int:car_pk>/location/<int:pk>/update",
"cars/<slug:car_pk>/location/<int:pk>/update",
views.CarLocationUpdateView.as_view(),
name="update_car_location",
),
path(
"cars/<int:pk>/location/update/",
"cars/<slug:slug>/location/update/",
views.CarTransferCreateView.as_view(),
name="transfer",
),
path(
"cars/<int:pk>/location/detail/",
"cars/<slug:slug>/location/detail/",
views.CarTransferDetailView.as_view(),
name="transfer_detail",
),
path(
"cars/<int:car_pk>/location/<int:transfer_pk>/transfer_approve/",
"cars/<slug:slug>/location/<int:transfer_pk>/transfer_approve/",
views.car_transfer_approve,
name="transfer_confirm",
),
path(
"cars/<int:car_pk>/location/<int:transfer_pk>/transfer_accept_reject/",
"cars/<slug:slug>/location/<int:transfer_pk>/transfer_accept_reject/",
views.car_transfer_accept_reject,
name="transfer_accept_reject",
),
path(
"cars/<int:car_pk>/location/<int:transfer_pk>/preview/",
"cars/<slug:slug>/location/<int:transfer_pk>/preview/",
views.CarTransferPreviewView,
name="transfer_preview",
),
@ -308,7 +308,7 @@ path(
views.SearchCodeView.as_view(),
name="car_search"),
# path('cars/<int:car_pk>/colors/<int:pk>/update/',views.CarColorUpdateView.as_view(),name='color_update'),
path("cars/reserve/<int:car_id>/",
path("cars/reserve/<slug:slug>/",
views.reserve_car_view,
name="reserve_car"),
path(
@ -317,11 +317,11 @@ path(
name="reservations",
),
path(
"cars/<int:car_pk>/add-custom-card/",
"cars/<slug:slug>/add-custom-card/",
views.CustomCardCreateView.as_view(),
name="add_custom_card",
),
path('cars/<int:car_pk>/add-registration/',
path('cars/<slug:slug>/add-registration/',
views.CarRegistrationCreateView.as_view(),
name='add_registration'),

View File

@ -213,7 +213,7 @@ def reserve_car(car, request):
except Exception as e:
messages.error(request, f"Error reserving car: {e}")
return redirect("car_detail", pk=car.pk)
return redirect("car_detail", slug=car.slug)
def calculate_vat_amount(amount):

View File

@ -604,7 +604,7 @@ class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
return context
def car_history(request, pk):
def car_history(request, slug):
"""
Fetch and display the history of activities related to a specific car.
@ -622,7 +622,7 @@ def car_history(request, pk):
including the car and its associated activities as context data.
:rtype: HttpResponse
"""
car = get_object_or_404(models.Car, pk=pk)
car = get_object_or_404(models.Car, slug=slug)
activities = models.Activity.objects.filter(
content_type__model="car", object_id=car.id
)
@ -859,7 +859,7 @@ class SearchCodeView(LoginRequiredMixin, View):
return JsonResponse({
"success": True,
"code": code,
"redirect_url": reverse("car_detail", args=[car.pk])
"redirect_url": reverse("car_detail", args=[car.slug])
})
except Exception as e:
@ -903,16 +903,16 @@ class CarInventory(LoginRequiredMixin, PermissionRequiredMixin, ListView):
def get_queryset(self, *args, **kwargs):
query = self.request.GET.get("q")
make_id = self.kwargs["make_id"]
model_id = self.kwargs["model_id"]
trim_id = self.kwargs["trim_id"]
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,
id_car_model=model_id,
id_car_trim=trim_id,
id_car_make=make,
id_car_model=model,
id_car_trim=trim,
).order_by("receiving_date")
return apply_search_filters(cars, query)
@ -951,16 +951,16 @@ class CarColorCreate(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
permission_required = ["inventory.view_car"]
def form_valid(self, form):
car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
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={"pk": self.kwargs["car_pk"]})
return reverse_lazy("car_detail", kwargs={"slug": self.kwargs["slug"]})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["car"] = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
context["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"])
return context
@ -1103,6 +1103,7 @@ def inventory_stats_view(request):
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": {},
@ -1113,6 +1114,7 @@ def inventory_stats_view(request):
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": {},
@ -1132,6 +1134,7 @@ def inventory_stats_view(request):
trim.id_car_trim
] = {
"trim_id": trim.id_car_trim,
"slug": trim.slug,
"trim_name": trim.name,
"total_cars": 0,
}
@ -1146,11 +1149,13 @@ def inventory_stats_view(request):
"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()),
@ -1161,7 +1166,7 @@ def inventory_stats_view(request):
for make_data in inventory.values()
],
}
print(result['makes'])
return render(request, "inventory/inventory_stats.html", {"inventory": result})
@ -1218,7 +1223,7 @@ class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi
permission_required = ["inventory.add_carfinance"]
def dispatch(self, request, *args, **kwargs):
self.car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
self.car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
@ -1227,7 +1232,7 @@ class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi
return super().form_valid(form)
def get_success_url(self):
return reverse("car_detail", kwargs={"pk": self.car.pk})
return reverse("car_detail", kwargs={"slug": self.car.slug})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -1273,7 +1278,7 @@ class CarFinanceUpdateView(
permission_required = ["inventory.change_carfinance"]
def get_success_url(self):
return reverse("car_detail", kwargs={"pk": self.object.car.pk})
return reverse("car_detail", kwargs={"slug": self.object.car.slug})
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
@ -1325,7 +1330,7 @@ class CarUpdateView(
permission_required = ["inventory.change_car"]
def get_success_url(self):
return reverse("car_detail", kwargs={"pk": self.object.pk})
return reverse("car_detail", kwargs={"slug": self.object.slug})
def get_form(self, form_class=None):
form = super().get_form(form_class)
@ -1388,10 +1393,10 @@ class CarLocationCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateV
permission_required = ["inventory.add_carlocation"]
def get_success_url(self):
return reverse_lazy("car_detail", kwargs={"pk": self.object.car.pk})
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, pk=self.kwargs["car_pk"])
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()
@ -1425,11 +1430,11 @@ class CarLocationUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateV
# def get_initial(self):
# initial = super().get_initial()
# initial["car"] = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
# 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, pk=self.kwargs["car_pk"])
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()
@ -1437,7 +1442,7 @@ class CarLocationUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateV
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("car_detail", kwargs={"pk": self.object.car.pk})
return reverse_lazy("car_detail", kwargs={"slug": self.object.car.slug})
class CarTransferCreateView(LoginRequiredMixin, CreateView):
@ -1465,12 +1470,12 @@ class CarTransferCreateView(LoginRequiredMixin, CreateView):
form.fields["to_dealer"].queryset = models.Dealer.objects.exclude(
pk=get_user_type(self.request).pk
).all()
form.fields["car"].queryset = models.Car.objects.filter(pk=self.kwargs["pk"])
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, pk=self.kwargs["pk"])
initial["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"])
return initial
def form_valid(self, form):
@ -1480,7 +1485,7 @@ class CarTransferCreateView(LoginRequiredMixin, CreateView):
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("car_detail", kwargs={"pk": self.object.car.pk})
return reverse_lazy("car_detail", kwargs={"slug": self.object.car.slug})
@ -1516,7 +1521,7 @@ class CarTransferDetailView(LoginRequiredMixin, SuccessMessageMixin, DetailView)
@login_required
def car_transfer_approve(request, car_pk, transfer_pk):
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
@ -1529,7 +1534,7 @@ def car_transfer_approve(request, car_pk, transfer_pk):
:param transfer_pk: Primary key of the transfer request to approve or cancel.
:return: An HTTP response redirecting to the car detail page of the specified car.
"""
car = get_object_or_404(models.Car, pk=car_pk)
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":
@ -1543,12 +1548,12 @@ def car_transfer_approve(request, car_pk, transfer_pk):
user=transfer.from_dealer.user,
message=f"Car transfer request from {transfer.to_dealer} is canceled.",
)
return redirect("car_detail", pk=car.pk)
return redirect("car_detail", slug=car.slug)
transfer.status = "approved"
transfer.save()
url = request.build_absolute_uri(
reverse(
"transfer_preview", kwargs={"car_pk": car.pk, "transfer_pk": transfer.pk}
"transfer_preview", kwargs={"slug": car.slug, "transfer_pk": transfer.pk}
)
)
models.Notification.objects.create(
@ -1556,11 +1561,11 @@ def car_transfer_approve(request, car_pk, transfer_pk):
message=f"Car transfer request from {transfer.from_dealer} is waiting for your acceptance. <a href='{url}'> Accept</a>",
)
messages.success(request, _("Car transfer approved successfully"))
return redirect("car_detail", pk=car.pk)
return redirect("car_detail", slug=car.slug)
@login_required
def car_transfer_accept_reject(request, car_pk, transfer_pk):
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
@ -1574,7 +1579,7 @@ def car_transfer_accept_reject(request, car_pk, transfer_pk):
:param transfer_pk: The primary key of the car transfer request to be processed.
:return: An HTTP redirect response to the 'inventory_stats' view.
"""
car = get_object_or_404(models.Car, pk=car_pk)
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":
@ -1606,7 +1611,7 @@ def car_transfer_accept_reject(request, car_pk, transfer_pk):
@login_required
def CarTransferPreviewView(request, car_pk, transfer_pk):
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.
@ -1627,7 +1632,7 @@ def CarTransferPreviewView(request, car_pk, transfer_pk):
"""
transfer = get_object_or_404(models.CarTransfer, pk=transfer_pk)
if transfer.to_dealer != get_user_type(request):
return redirect("car_detail", pk=car_pk)
return redirect("car_detail", slug=slug)
return render(request, "inventory/transfer_preview.html", {"transfer": transfer})
@ -1651,18 +1656,18 @@ class CustomCardCreateView(LoginRequiredMixin, CreateView):
template_name = "inventory/add_custom_card.html"
def form_valid(self, form):
car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
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, pk=self.kwargs["car_pk"])
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={"pk": self.kwargs["car_pk"]})
return reverse_lazy("car_detail", kwargs={"slug": self.kwargs["slug"]})
class CarRegistrationCreateView(LoginRequiredMixin, CreateView):
@ -1692,22 +1697,22 @@ class CarRegistrationCreateView(LoginRequiredMixin, CreateView):
template_name = "inventory/car_registration_form.html"
def form_valid(self, form):
car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
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, pk=self.kwargs["car_pk"])
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={"pk": self.kwargs["car_pk"]})
return reverse_lazy("car_detail", kwargs={"slug": self.kwargs["slug"]})
@login_required()
def reserve_car_view(request, car_id):
def reserve_car_view(request, 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
@ -1723,10 +1728,10 @@ def reserve_car_view(request, car_id):
:rtype: HttpResponse or JsonResponse
"""
if request.method == "POST":
car = get_object_or_404(models.Car, pk=car_id)
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", pk=car.pk)
return redirect("car_detail", slug=car.slug)
response = reserve_car(car, request)
return response
return JsonResponse(
@ -1764,7 +1769,7 @@ def manage_reservation(request, reservation_id):
reservation.reserved_until = timezone.now() + timezone.timedelta(hours=24)
reservation.save()
messages.success(request, _("Reservation renewed successfully"))
return redirect("car_detail", pk=reservation.car.pk)
return redirect("car_detail", slug=reservation.car.slug)
elif action == "cancel":
car = reservation.car
@ -1772,7 +1777,7 @@ def manage_reservation(request, reservation_id):
car.status = models.CarStatusChoices.AVAILABLE
car.save()
messages.success(request, _("Reservation canceled successfully"))
return redirect("car_detail", pk=reservation.car.pk)
return redirect("car_detail", slug=reservation.car.slug)
else:
return JsonResponse(
@ -1857,7 +1862,7 @@ class DealerUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
success_message = _("Dealer updated successfully")
def get_success_url(self):
return reverse("dealer_detail", kwargs={"pk": self.object.pk})
return reverse("dealer_detail", kwargs={"slug": self.object.slug})
class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
@ -2148,7 +2153,7 @@ class VendorListView(LoginRequiredMixin, ListView):
@login_required
def vendorDetailView(request, pk):
def vendorDetailView(request, slug):
"""
Fetches and renders the detail view for a specific vendor.
@ -2163,7 +2168,7 @@ def vendorDetailView(request, pk):
:return: An HttpResponse object containing the rendered vendor detail page.
:rtype: HttpResponse
"""
vendor = get_object_or_404(models.Vendor, pk=pk)
vendor = get_object_or_404(models.Vendor, slug=slug)
return render(
request, template_name="vendors/view_vendor.html", context={"vendor": vendor}
)
@ -2258,7 +2263,7 @@ class VendorUpdateView(
@login_required
def delete_vendor(request, pk):
def delete_vendor(request, slug):
"""
Deletes an existing vendor record from the database.
@ -2273,7 +2278,7 @@ def delete_vendor(request, pk):
:return: HttpResponseRedirect object for redirecting to the vendor list page.
:rtype: HttpResponseRedirect
"""
vendor = get_object_or_404(models.Vendor, pk=pk)
vendor = get_object_or_404(models.Vendor, slug=slug)
vendor.active = False
vendor.vendor_model.active = False
vendor.save()
@ -2367,7 +2372,7 @@ class GroupCreateView(
def form_valid(self, form):
dealer = get_user_type(self.request)
instance = form.save(commit=False)
group = Group.objects.create(name=f"{dealer.pk}_{instance.name}")
group = Group.objects.create(name=f"{dealer.slug}_{instance.name}")
instance.dealer = dealer
instance.group = group
instance.save()
@ -2410,7 +2415,7 @@ class GroupUpdateView(
dealer = get_user_type(self.request)
instance = form.save(commit=False)
instance.set_defualt_permissions()
instance.group.name = f"{dealer.pk}_{instance.name}"
instance.group.name = f"{dealer.slug}_{instance.name}"
instance.save()
return super().form_valid(form)
@ -4812,7 +4817,7 @@ def add_note_to_lead(request, pk):
note.created_by = request.user
note.save()
messages.success(request, _("Note added successfully"))
return redirect("lead_detail", pk=lead.pk)
return redirect("lead_detail", slug=lead.slug)
else:
form = forms.NoteForm()
return render(request, "crm/note_form.html", {"form": form, "lead": lead})
@ -4865,6 +4870,7 @@ def update_note(request, pk):
"""
note = get_object_or_404(models.Notes, pk=pk, created_by=request.user)
lead_pk = note.content_object.pk
lead = models.Lead.objects.get(pk=lead_pk)
if request.method == "POST":
form = forms.NoteForm(request.POST, instance=note)
@ -4874,7 +4880,7 @@ def update_note(request, pk):
updated_note.created_by = request.user
updated_note.save()
messages.success(request, _("Note updated successfully"))
return redirect("lead_detail", pk=lead_pk)
return redirect("lead_detail", slug=lead.slug)
else:
form = forms.NoteForm(instance=note)
@ -5035,7 +5041,7 @@ def lead_transfer(request, pk):
@login_required
def send_lead_email(request, pk, email_pk=None):
def send_lead_email(request, 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
@ -5055,15 +5061,15 @@ def send_lead_email(request, pk, email_pk=None):
or email composition rendering, a response object is returned to render the respective page.
Type: HttpResponse
"""
lead = get_object_or_404(models.Lead, pk=pk)
lead = get_object_or_404(models.Lead, slug=slug)
status = request.GET.get("status")
dealer = get_user_type(request)
if status == 'draft':
models.Email.objects.create(content_object=lead, created_by=request.user,from_email="manager@tenhal.com", to_email=request.GET.get("to"), subject=request.GET.get("subject"), message=request.GET.get("message"),status=models.EmailStatus.DRAFT)
models.Activity.objects.create(dealer=dealer,content_object=lead, notes="Email Draft",created_by=request.user,activity_type=models.ActionChoices.EMAIL)
messages.success(request, _("Email Draft successfully"))
response = HttpResponse(redirect("lead_detail", pk=lead.pk))
response["HX-Redirect"] = reverse("lead_detail", args=[lead.pk])
response = HttpResponse(redirect("lead_detail", slug=lead.slug))
response["HX-Redirect"] = reverse("lead_detail", args=[lead.slug])
return response
if request.method == "POST":
@ -7028,7 +7034,7 @@ class CarListViewTable(LoginRequiredMixin, ExportMixin, SingleTableView):
@login_required
def DealerSettingsView(request, pk):
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
@ -7045,7 +7051,7 @@ def DealerSettingsView(request, pk):
to the dealer detail view after successful form submission.
:rtype: HttpResponse
"""
dealer_setting = get_object_or_404(models.DealerSettings, dealer__pk=pk)
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)
@ -7054,7 +7060,7 @@ def DealerSettingsView(request, pk):
instance.dealer = dealer
instance.save()
messages.success(request, _('Settings updated'))
return redirect('dealer_detail', pk=dealer.pk)
return redirect('dealer_detail', slug=dealer.slug)
else:
print(form.errors)
form = forms.DealerSettingsForm(instance=dealer_setting, initial={"dealer": dealer})
@ -7134,7 +7140,7 @@ def assign_car_makes(request):
makes = form.cleaned_data["car_makes"]
create_accounts_for_make(dealer, makes)
form.save()
return redirect("dealer_detail", pk=dealer.pk)
return redirect("dealer_detail", slug=dealer.slug)
else:
print(form.errors)
else:
@ -7743,13 +7749,13 @@ def notifications_history(request):
# )
# return render(request, 'activity_history.html')
def add_activity(request,content_type,pk):
def add_activity(request,content_type,slug):
try:
model = apps.get_model(f'inventory.{content_type}')
except LookupError:
raise Http404("Model not found")
obj = get_object_or_404(model, pk=pk)
obj = get_object_or_404(model, slug=slug)
dealer = get_user_type(request)
if request.method == "POST":
form = forms.ActivityForm(request.POST)
@ -7765,14 +7771,14 @@ def add_activity(request,content_type,pk):
messages.success(request, _("Activity added successfully"))
else:
messages.error(request, _("Activity form is not valid"))
return redirect(f"{content_type}_detail", pk=pk)
def add_task(request,content_type,pk):
return redirect(f"{content_type}_detail", slug=slug)
def add_task(request,content_type,slug):
try:
model = apps.get_model(f'inventory.{content_type}')
except LookupError:
raise Http404("Model not found")
obj = get_object_or_404(model, pk=pk)
obj = get_object_or_404(model, slug=slug)
dealer = get_user_type(request)
if request.method == "POST":
form = forms.StaffTaskForm(request.POST)
@ -7788,16 +7794,22 @@ def add_task(request,content_type,pk):
else:
print(form.errors)
messages.error(request, _("Task form is not valid"))
return redirect(f"{content_type}_detail", pk=pk)
return redirect(f"{content_type}_detail", slug=slug)
def update_task(request,pk):
task = get_object_or_404(models.Tasks, pk=pk)
lead = get_object_or_404(models.Lead, pk=task.content_object.id)
if request.method == "POST":
task.completed = False if task.completed else True
task.save()
messages.success(request, _("Task updated successfully"))
else:
messages.error(request, _("Task form is not valid"))
response = HttpResponse()
response['HX-Refresh'] = 'true'
return response
# response = HttpResponse()
# response['HX-Refresh'] = 'true'
# return response
tasks = models.Tasks.objects.filter(
content_type__model="lead", object_id=lead.id
)
return render(request,'crm/leads/lead_detail.html',{'lead':lead,'tasks':tasks})

View File

@ -2,22 +2,22 @@
echo "Loading initial data"
echo "Loading carmake"
python3 manage.py loaddata --app carmake carmake_backup.json
python3 manage.py loaddata --app carmake carmake_backup_output.json
echo "Loading carmodel"
python3 manage.py loaddata --app carmodel carmodel_backup.json
python3 manage.py loaddata --app carmodel carmodel_backup_output.json
echo "Loading carserie"
python3 manage.py loaddata --app carserie carserie_backup.json
python3 manage.py loaddata --app carserie carserie_backup_output.json
echo "Loading cartrim"
python3 manage.py loaddata --app cartrim cartrim_backup.json
python3 manage.py loaddata --app cartrim cartrim_backup_output.json
echo "Loading caroption"
python3 manage.py loaddata --app caroption caroption_backup.json
python3 manage.py loaddata --app caroption caroption_backup_output.json
echo "Loading carequipment"
python3 manage.py loaddata --app carequipment carequipment_backup.json
python3 manage.py loaddata --app carequipment carequipment_backup_output.json
echo "Populating colors"
python3 manage.py populate_colors
@ -26,4 +26,6 @@ python3 manage.py tenhal_plan
python3 manage.py set_vat
python3 manage.py initial_services_offered
echo "Done"

56
slug_data.py Normal file
View File

@ -0,0 +1,56 @@
import json
import time
from pathlib import Path
from slugify import slugify
def process_json_file(input_file, output_file=None, batch_size=10000):
"""Add slugs to JSON data file with optimal performance"""
if output_file is None:
output_file = f"{Path(input_file).stem}_with_slugs.json"
start_time = time.time()
with open(input_file, 'r', encoding='utf-8') as f:
data = json.load(f)
total = len(data)
processed = 0
print(f"Processing {total} records...")
for item in data:
# Generate slug from name field
name = item['fields'].get('name', '')
pk = item['pk']
if name:
slug = slugify(name)[:50] # Truncate to 50 chars
# Append PK to ensure uniqueness
item['fields']['slug'] = f"{slug}"
else:
# Fallback to model-pk if name is empty
model_name = item['model'].split('.')[-1]
item['fields']['slug'] = f"{model_name}-{pk}"
processed += 1
if processed % batch_size == 0:
print(f"Processed {processed}/{total} records...")
# Save the modified data
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
print(f"Completed in {time.time() - start_time:.2f} seconds")
print(f"Output saved to {output_file}")
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('input_file', help='Path to input JSON file')
parser.add_argument('-o', '--output', help='Output file path')
parser.add_argument('-b', '--batch', type=int, default=10000,
help='Progress reporting batch size')
args = parser.parse_args()
process_json_file(args.input_file, args.output, args.batch)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -50,3 +50,15 @@
@keyframes spin {
to { transform: rotate(360deg); }
}
.form-select select {
padding: 0 16px 0 48px;
right: auto;
left: 0;
direction: rtl;
}
.form-select:after {
left: 16px;
right: auto;
}

View File

@ -3780,8 +3780,9 @@ textarea.form-control-lg {
.form-select {
--phoenix-form-select-bg-img: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUwIiBoZWlnaHQ9IjE1MCIgdmlld0JveD0iMCAwIDE1MCAxNTAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik03NS4zNDggMTI3LjE5MkM3Mi40MzgxIDEyNy4xOTIgNjkuODUxNCAxMjYuMjIyIDY3LjkxMTUgMTI0LjI4Mkw1LjgzMjE1IDYyLjIwMjNDMS42Mjg4NyA1OC4zMjIzIDEuNjI4ODcgNTEuNTMyNCA1LjgzMjE1IDQ3LjY1MjVDOS43MTIxMSA0My40NDkyIDE2LjUwMiA0My40NDkyIDIwLjM4MiA0Ny42NTI1TDc1LjM0OCAxMDIuMjk1TDEyOS45OTEgNDcuNjUyNUMxMzMuODcxIDQzLjQ0OTIgMTQwLjY2MSA0My40NDkyIDE0NC41NDEgNDcuNjUyNUMxNDguNzQ0IDUxLjUzMjQgMTQ4Ljc0NCA1OC4zMjIzIDE0NC41NDEgNjIuMjAyM0w4Mi40NjEzIDEyNC4yODJDODAuNTIxMyAxMjYuMjIyIDc3LjkzNDcgMTI3LjE5MiA3NS4zNDggMTI3LjE5MloiIGZpbGw9IiMzMTM3NEEiLz4KPC9zdmc+Cg==");
align-content: start;
display: block;
text-align: start;
text-align: right;
width: 100%;
padding: 0.5rem 1rem 0.5rem 2.5rem;
font-size: 0.8rem;
@ -3808,9 +3809,12 @@ textarea.form-control-lg {
}
@media (prefers-reduced-motion: reduce) {
.form-select {
-webkit-transition: none;
-webkit-transition:right;
-o-transition: none;
transition: none;
right: auto;
left: 0;
direction: rtl;
}
}
.form-select:focus {

View File

@ -1138,6 +1138,9 @@ select {
select {
word-wrap: normal;
padding: 0 16px 0 48px;
right: auto;
left: 0;
}
select:disabled {
opacity: 1;
@ -3784,8 +3787,9 @@ textarea.form-control-lg {
.form-select {
--phoenix-form-select-bg-img: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUwIiBoZWlnaHQ9IjE1MCIgdmlld0JveD0iMCAwIDE1MCAxNTAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik03NS4zNDggMTI3LjE5MkM3Mi40MzgxIDEyNy4xOTIgNjkuODUxNCAxMjYuMjIyIDY3LjkxMTUgMTI0LjI4Mkw1LjgzMjE1IDYyLjIwMjNDMS42Mjg4NyA1OC4zMjIzIDEuNjI4ODcgNTEuNTMyNCA1LjgzMjE1IDQ3LjY1MjVDOS43MTIxMSA0My40NDkyIDE2LjUwMiA0My40NDkyIDIwLjM4MiA0Ny42NTI1TDc1LjM0OCAxMDIuMjk1TDEyOS45OTEgNDcuNjUyNUMxMzMuODcxIDQzLjQ0OTIgMTQwLjY2MSA0My40NDkyIDE0NC41NDEgNDcuNjUyNUMxNDguNzQ0IDUxLjUzMjQgMTQ4Ljc0NCA1OC4zMjIzIDE0NC41NDEgNjIuMjAyM0w4Mi40NjEzIDEyNC4yODJDODAuNTIxMyAxMjYuMjIyIDc3LjkzNDcgMTI3LjE5MiA3NS4zNDggMTI3LjE5MloiIGZpbGw9IiMzMTM3NEEiLz4KPC9zdmc+Cg==");
display: block;
align-content: start;
width: 100%;
text-align: start;
text-align: right;
padding: 0.5rem 2.5rem 0.5rem 1rem;
font-size: 0.8rem;
font-weight: 600;

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -10,7 +10,7 @@
</button>
</div>
<div class="modal-body">
<form action="{% url 'add_activity' content_type=content_type pk=pk %}" method="post" class="add_activity_form">
<form action="{% url 'add_activity' content_type=content_type slug=slug %}" method="post" class="add_activity_form">
{% csrf_token %}
<div class="mb-2 form-group">
<select class="form-select" name="activity_type" id="activity_type">

View File

@ -352,7 +352,7 @@
<div class="tab-pane fade" id="tab-emails" role="tabpanel" aria-labelledby="emails-tab">
<div class="mb-1 d-flex justify-content-between align-items-center">
<h3 class="mb-0" id="scrollspyEmails">{{ _("Emails") }}</h3>
<a href="{% url 'send_lead_email' lead.pk %}">
<a href="{% url 'send_lead_email' lead.slug %}">
<button type="button" class="btn btn-sm btn-phoenix-primary">
<span class="fas fa-plus me-1"></span>
{% trans 'Send Email' %}
@ -448,7 +448,7 @@
</td>
<td class="sent align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2">{{email.from_email}}</td>
<td class="date align-middle white-space-nowrap text-body py-2">{{email.created}}</td>
<td class="align-middle white-space-nowrap ps-3"><a class="text-body" href="{% url 'send_lead_email_with_template' lead.pk email.pk %}"><span class="fa-solid fa-email text-primary me-2"></span>Send</a></td>
<td class="align-middle white-space-nowrap ps-3"><a class="text-body" href="{% url 'send_lead_email_with_template' lead.slug email.pk %}"><span class="fa-solid fa-email text-primary me-2"></span>Send</a></td>
<td class="status align-middle fw-semibold text-end py-2"><span class="badge badge-phoenix fs-10 badge-phoenix-warning">draft</span></td>
</tr>
{% endfor %}
@ -496,10 +496,10 @@
</thead>
<tbody class="list" id="all-email-table-body">
{% for task in tasks %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static {% if task.completed %}completed-task{% endif %}">
<tr class="task-{{task.pk}} hover-actions-trigger btn-reveal-trigger position-static {% if task.completed %}completed-task{% endif %}">
<td class="fs-9 align-middle px-0 py-3">
<div class="form-check mb-0 fs-8">
<input class="form-check-input" type="checkbox" hx-post="{% url 'update_task' task.pk %}" hx-trigger="change" hx-swap="none" />
<input class="form-check-input" type="checkbox" hx-post="{% url 'update_task' task.pk %}" hx-trigger="change" hx-swap="outerHTML" hx-select=".task-{{task.pk}}" hx-target=".task-{{task.pk}}" />
</div>
</td>
<td class="subject order align-middle white-space-nowrap py-2 ps-0"><a class="fw-semibold text-primary" href="">{{task.title}}</a>
@ -552,7 +552,7 @@
</div>
<!-- activity Modal -->
{% include "components/activity_modal.html" with content_type="lead" pk=lead.pk %}
{% include "components/activity_modal.html" with content_type="lead" slug=lead.slug %}
<!-- task Modal -->
<div class="modal fade" id="taskModal" tabindex="-1" aria-labelledby="taskModalLabel" aria-hidden="true">
@ -565,7 +565,7 @@
</button>
</div>
<div class="modal-body">
<form action="{% url 'add_task' 'lead' lead.pk %}" method="post" class="add_task_form">
<form action="{% url 'add_task' 'lead' lead.slug %}" method="post" class="add_task_form">
{% csrf_token %}
{{ staff_task_form|crispy }}
<button type="submit" class="btn btn-success w-100">{% trans 'Save' %}</button>

View File

@ -148,7 +148,7 @@
<p>{% trans "Are you sure you want to delete this lead?" %}</p>
</div>
<div class="modal-footer flex justify-content-center border-top-0">
<a type="button" class="btn btn-sm btn-danger w-100" href="{% url 'lead_delete' lead.pk %}">
<a type="button" class="btn btn-sm btn-danger w-100" href="{% url 'lead_delete' lead.slug %}">
{% trans "Yes" %}
</a>
</div>
@ -159,7 +159,7 @@
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="name align-middle white-space-nowrap ps-0">
<div class="d-flex align-items-center">
<div><a class="fs-8 fw-bold" href="{% url 'lead_detail' lead.pk %}">{{lead.full_name}}</a>
<div><a class="fs-8 fw-bold" href="{% url 'lead_detail' lead.slug %}">{{lead.full_name}}</a>
<div class="d-flex align-items-center">
<p class="mb-0 text-body-highlight fw-semibold fs-9 me-2"></p>
{% if lead.status == "new" %}
@ -187,11 +187,11 @@
<div class="accordion" id="accordionExample">
<div class="accordion-item">
<h2 class="accordion-header" id="headingTwo">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse{{lead.pk}}" aria-expanded="false" aria-controls="collapseTwo">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse{{lead.slug}}" aria-expanded="false" aria-controls="collapseTwo">
View Schedules ({{lead.get_latest_schedules.count}})
</button>
</h2>
<div class="accordion-collapse collapse" id="collapse{{lead.pk}}" aria-labelledby="headingTwo" data-bs-parent="#accordionExample">
<div class="accordion-collapse collapse" id="collapse{{lead.slug}}" aria-labelledby="headingTwo" data-bs-parent="#accordionExample">
<div class="accordion-body pt-0">
<div class="d-flex flex-column gap-2">
<table><tbody>

View File

@ -8,7 +8,7 @@
<div class="card email-content">
<h5 class="card-header">Send Mail</h5>
<div class="card-body">
<form class="d-flex flex-column h-100" action="{% url 'send_lead_email' lead.pk %}" method="post">
<form class="d-flex flex-column h-100" action="{% url 'send_lead_email' lead.slug %}" method="post">
{% csrf_token %}
<div class="row g-3 mb-2">
<div class="col-12">
@ -26,8 +26,8 @@
</div>
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex gap-2">
<a href="{% url 'lead_detail' lead.pk %}" class="btn btn-link text-body fs-10 text-decoration-none">Discard</a>
<a hx-boost="true" hx-push-url='false' hx-include="#message,#subject,#to" href="{% url 'send_lead_email' lead.pk %}?status=draft" class="btn btn-secondary text-white fs-10 text-decoration-none">Save as Draft</a>
<a href="{% url 'lead_detail' lead.slug %}" class="btn btn-link text-body fs-10 text-decoration-none">Discard</a>
<a hx-boost="true" hx-push-url='false' hx-include="#message,#subject,#to" href="{% url 'send_lead_email' lead.slug %}?status=draft" class="btn btn-secondary text-white fs-10 text-decoration-none">Save as Draft</a>
<button class="btn btn-primary fs-10" type="submit">Send<span class="fa-solid fa-paper-plane ms-1"></span></button>
</div>
</div>

View File

@ -72,7 +72,7 @@
<div class="kanban-column">
<div class="kanban-header">New Leads ({{new|length}})</div>
{% for lead in new %}
<a href="{% url 'lead_detail' lead.id %}">
<a href="{% url 'lead_detail' lead.slug %}">
<div class="lead-card">
<strong>{{lead.full_name|capfirst}}</strong><br>
<small>{{lead.email}}</small><br>
@ -88,7 +88,7 @@
<div class="kanban-column">
<div class="kanban-header">Follow Ups ({{follow_up|length}})</div>
{% for lead in follow_up %}
<a href="{% url 'lead_detail' lead.id %}">
<a href="{% url 'lead_detail' lead.slug %}">
<div class="lead-card">
<strong>{{lead.full_name|capfirst}}</strong><br>
<small>{{lead.email}}</small><br>
@ -104,7 +104,7 @@
<div class="kanban-column">
<div class="kanban-header">Negotiation ({{negotiation|length}})</div>
{% for lead in negotiation %}
<a href="{% url 'lead_detail' lead.id %}">
<a href="{% url 'lead_detail' lead.slug %}">
<div class="lead-card">
<strong>{{lead.full_name|capfirst}}</strong><br>
<small>{{lead.email}}</small><br>
@ -120,7 +120,7 @@
<div class="kanban-column">
<div class="kanban-header">Won ({{won|length}})</div>
{% for lead in won %}
<a href="{% url 'lead_detail' lead.id %}">
<a href="{% url 'lead_detail' lead.slug %}">
<div class="lead-card">
<strong>{{lead.full_name|capfirst}}</strong><br>
<small>{{lead.email}}</small><br>
@ -136,7 +136,7 @@
<div class="kanban-column">
<div class="kanban-header">Lose ({{lose|length}})</div>
{% for lead in lose %}
<a href="{% url 'lead_detail' lead.id %}">
<a href="{% url 'lead_detail' lead.slug %}">
<div class="lead-card">
<strong>{{lead.full_name|capfirst}}</strong><br>
<small>{{lead.email}}</small><br>

View File

@ -70,7 +70,7 @@
</td>
<td class="name align-middle white-space-nowrap ps-0">
<div class="d-flex align-items-center">
<div><a class="fs-8 fw-bold" href="{% url 'customer_detail' customer.pk %}">{{ customer.full_name }}</a>
<div><a class="fs-8 fw-bold" href="{% url 'customer_detail' customer.slug %}">{{ customer.full_name }}</a>
<div class="d-flex align-items-center">
</div>
</div>
@ -91,13 +91,13 @@
<td class="date align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 text-body-tertiary">{{ customer.created|date }}</td>
<td class="align-middle white-space-nowrap text-end pe-0 ps-4">
{% if perms.django_ledger.change_customermodel %}
<a href="{% url 'customer_update' customer.pk %}" class="btn btn-sm btn-phoenix-primary me-2" data-url="{% url 'customer_update' customer.pk %}">
<a href="{% url 'customer_update' customer.slug %}" class="btn btn-sm btn-phoenix-primary me-2" data-url="{% url 'customer_update' customer.slug %}">
<i class="fas fa-pen"></i>
</a>
{% endif %}
{% if perms.django_ledger.delete_customermodel %}
<button class="btn btn-phoenix-danger btn-sm delete-btn"
data-url="{% url 'customer_delete' customer.pk %}"
data-url="{% url 'customer_delete' customer.slug %}"
data-message="{{ _("Are you sure you want to delete this customer")}}"
data-bs-toggle="modal" data-bs-target="#deleteModal">
<i class="fas fa-trash"></i>

View File

@ -17,7 +17,7 @@
<div class="col-auto">
{% if perms.django_ledger.delete_customermodel %}
<button class="btn btn-phoenix-danger btn-sm delete-btn"
data-url="{% url 'customer_delete' customer.pk %}"
data-url="{% url 'customer_delete' customer.slug %}"
data-message="Are you sure you want to delete this customer?"
data-bs-toggle="modal" data-bs-target="#deleteModal">
<i class="fas fa-trash me-1"> </i>{{ _("Delete") }}
@ -27,7 +27,7 @@
<div class="col-auto">
{% if perms.django_ledger.change_customermodel %}
<a href="{% url 'customer_update' customer.pk %}" class="btn btn-sm btn-phoenix-warning"><span class="fa-solid fa-pen-to-square me-2"></span>{{_("Update")}}</a>
<a href="{% url 'customer_update' customer.slug %}" class="btn btn-sm btn-phoenix-warning"><span class="fa-solid fa-pen-to-square me-2"></span>{{_("Update")}}</a>
{% endif %}
</div>
</div>
@ -81,7 +81,7 @@
<div class="card">
<div class="card-body">
<div class="d-flex align-items-center justify-content-end">
<a id="addBtn" href="#" class="btn btn-sm btn-phoenix-primary mb-3" data-url="{% url 'add_note_to_customer' customer.pk %}" data-bs-toggle="modal" data-bs-target="#noteModal" data-note-title="{{ _("Add") }}<i class='fa fa-plus-circle text-success ms-2'></i>">
<a id="addBtn" href="#" class="btn btn-sm btn-phoenix-primary mb-3" data-url="{% url 'add_note_to_customer' customer.slug %}" data-bs-toggle="modal" data-bs-target="#noteModal" data-note-title="{{ _("Add") }}<i class='fa fa-plus-circle text-success ms-2'></i>">
<span class="fas fa-plus me-1"></span>
{% trans 'Add Note' %}
</a>

View File

@ -10,7 +10,7 @@
<a href="{% url 'account_change_password' %}" class="btn btn-phoenix-danger"><span class="fas fa-key me-2"></span>{{ _("Change Password") }}</a>
</div>
<div class="col-auto">
<a class="btn btn-phoenix-secondary" href="{% url 'dealer_update' dealer.pk %}"><span class="fas fa-edit me-2 text-body-quaternary"></span>{{ _("Edit") }} </a>
<a class="btn btn-phoenix-secondary" href="{% url 'dealer_update' dealer.slug %}"><span class="fas fa-edit me-2 text-body-quaternary"></span>{{ _("Edit") }} </a>
</div>
</div>
</div>

View File

@ -415,7 +415,7 @@
<ul class="nav d-flex flex-column mb-2 pb-1">
{% if request.is_dealer %}
<li class="nav-item">
<a class="nav-link px-3 d-block" href="{% url 'dealer_detail' request.user.dealer.pk %}"> <span class="me-2 text-body align-bottom" data-feather="user"></span><span>{% translate 'profile'|capfirst %}</span></a>
<a class="nav-link px-3 d-block" href="{% url 'dealer_detail' request.user.dealer.slug %}"> <span class="me-2 text-body align-bottom" data-feather="user"></span><span>{% translate 'profile'|capfirst %}</span></a>
</li>
{% else %}
<li class="nav-item">
@ -432,7 +432,7 @@
{% endif %}
<li class="nav-item">
{% if request.is_dealer %}
<a class="nav-link px-3 d-block" href="{% url 'dealer_settings' request.user.dealer.pk %}"> <span class="me-2 text-body align-bottom" data-feather="settings"></span>{{ _("Settings") }}</a>
<a class="nav-link px-3 d-block" href="{% url 'dealer_settings' request.user.dealer.slug %}"> <span class="me-2 text-body align-bottom" data-feather="settings"></span>{{ _("Settings") }}</a>
{% endif %}
</li>
<li class="nav-item">

View File

@ -1,7 +1,7 @@
{% load i18n %}
{% load crispy_forms_filters %}
<form method="post" id="customCardForm" action="{% url 'add_custom_card' car.pk %}">
<form method="post" id="customCardForm" action="{% url 'add_custom_card' car.slug %}">
{% csrf_token %}
{{ form|crispy }}
<div class="d-flex gap-1">

View File

@ -155,16 +155,15 @@
</tr>
{% endif %}
<tr>
<th>{% trans 'Location'|capfirst %}</th>
<td>
{% if car.finances and not car.get_transfer %}
{% if car.location %} {% if car.location.is_owner_showroom %} {% trans 'Our Showroom' %} {% else %} {{ car.location.showroom.get_local_name }} {% endif %}
<a href="{% url 'update_car_location' car.pk car.location.pk%}" class="btn btn-phoenix-danger btn-sm">
<a href="{% url 'update_car_location' car.slug car.location.pk%}" class="btn btn-phoenix-danger btn-sm">
{% trans "transfer"|capfirst %}
</a>
{% else %} {% trans "No location available." %}
<a href="{% url 'add_car_location' car.pk %}" class="btn btn-phoenix-success btn-sm ms-2">
<a href="{% url 'add_car_location' car.slug %}" class="btn btn-phoenix-success btn-sm ms-2">
{% trans "Add" %}
</a>
{% endif %}
@ -176,8 +175,8 @@
<div>
{% if not car.get_transfer %}
{% if perms.inventory.change_car %}
<a href="{% url 'car_update' car.pk %}" class="btn btn-phoenix-warning btn-sm mt-1">{% trans "Edit" %}</a>
<a href="{% url 'transfer' car.pk %}" class="btn btn-phoenix-danger btn-sm">
<a href="{% url 'car_update' car.slug %}" class="btn btn-phoenix-warning btn-sm mt-1">{% trans "Edit" %}</a>
<a href="{% url 'transfer' car.slug %}" class="btn btn-phoenix-danger btn-sm">
{% trans "Sell to another dealer"|capfirst %}
</a>
{% endif %}
@ -242,7 +241,7 @@
{% else %}
<p>{% trans "No finance details available." %}</p>
{% if perms.inventory.add_carfinance %}
<a href="{% url 'car_finance_create' car.pk %}" class="btn btn-phoenix-success btn-sm mb-3">
<a href="{% url 'car_finance_create' car.slug %}" class="btn btn-phoenix-success btn-sm mb-3">
{% trans "Add" %}
</a>
{% endif %}
@ -288,7 +287,7 @@
<tr>
<td colspan="2">
{% if perms.inventory.change_carcolors %}
<a href="{% url 'add_color' car.pk %}" class="btn btn-phoenix-success btn-sm">
<a href="{% url 'add_color' car.slug %}" class="btn btn-phoenix-success btn-sm">
{% trans "Add" %}
</a>
{% endif %}
@ -454,9 +453,7 @@
<div class="modal-body">
{% trans 'Are you sure you want to reserve this car?' %}
</div>
<form method="POST" action="{% url 'reserve_car' car.id %}" class="form ">
<form method="POST" action="{% url 'reserve_car' car.slug %}" class="form ">
{% csrf_token %}
<div class="p-1">
<div class="d-flex gap-1">
@ -493,8 +490,6 @@
</div>
<script>
document.addEventListener("DOMContentLoaded", function () {
const csrftoken = getCookie("csrftoken");
const ajaxUrl = "{% url 'ajax_handler' %}";
@ -503,7 +498,7 @@
// When the modal is triggered, load the form
customCardModal.addEventListener("show.bs.modal", function () {
const url = "{% url 'add_custom_card' car.pk %}";
const url = "{% url 'add_custom_card' car.slug %}";
fetch(url)
.then((response) => response.text())
@ -525,7 +520,7 @@
// When the modal is triggered, load the form
registrationModal.addEventListener("show.bs.modal", function () {
const url = "{% url 'add_registration' car.pk %}";
const url = "{% url 'add_registration' car.slug %}";
fetch(url)
.then((response) => response.text())
@ -608,7 +603,7 @@
document.querySelectorAll(".reserve-btn").forEach((button) => {
button.addEventListener("click", async function () {
try {
const response = await fetch(`{% url 'reserve_car' car.pk %}`, {
const response = await fetch(`{% url 'reserve_car' car.slug %}`, {
method: "POST",
headers: {
"X-CSRFToken": csrfToken,

View File

@ -70,7 +70,7 @@
<span class="badge badge-phoenix badge-phoenix-info"><span class="badge-label">{{_("Used")}}</span></span>
{% endif %}
</td>
<td class="align-middle white-space-nowrap text-start"><a class="fs-9 fw-bold" href="{% url 'car_detail' car.pk %}">{{ car.vin }}</a></td>
<td class="align-middle white-space-nowrap text-start"><a class="fs-9 fw-bold" href="{% url 'car_detail' car.slug %}">{{ car.vin }}</a></td>
<td class="align-middle white-space-nowrap text-center fw-bold">{{ car.year }}</td>
{% if car.colors %}
<td class="align-middle white-space-nowrap text-body fs-9 text-start">
@ -111,7 +111,7 @@
<span class="fw-light">{{ car.receiving_date|timesince }}</span>
</td>
<td class="align-middle white-space-nowrap text-end pe-0 ps-4">
<a class="btn btn-sm btn-phoenix-success" href="{% url 'car_detail' car.pk %}">{% trans "view"|capfirst %}</a>
<a class="btn btn-sm btn-phoenix-success" href="{% url 'car_detail' car.slug %}">{% trans "view"|capfirst %}</a>
</td>
</tr>
{% empty %}

View File

@ -117,10 +117,10 @@
</tr>
</thead>
<tbody class="list" id="project-list-table-body">
{% for car in page_obj %}
{% for car in cars %}
<tr class="position-static">
<td class="align-middle white-space-nowrap ps-1">
<a class="fw-bold" href="{% url 'car_detail' car.pk %}">{{car.vin}}</a>
<a class="fw-bold" href="{% url 'car_detail' car.slug %}">{{car.vin}}</a>
</td>
<td class="align-middle white-space-nowrap">
<p class="text-body mb-0">{{car.id_car_make.get_local_name|default:car.id_car_make.name}}</p>
@ -164,7 +164,7 @@
<div class="btn-reveal-trigger position-static">
<button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal fs-10" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10"></span></button>
<div class="dropdown-menu dropdown-menu-end py-2">
<a class="dropdown-item" href="{% url 'car_detail' car.pk %}">{{ _("View") }}</a>
<a class="dropdown-item" href="{% url 'car_detail' car.slug %}">{{ _("View") }}</a>
<a class="dropdown-item" href="#!">{{ _("Export") }}</a>
</div>
</div>

View File

@ -2,7 +2,7 @@
{% load crispy_forms_filters %}
<div class="w-100 g-3">
<form method="post" id="registrationForm" action="{% url 'add_registration' car.pk %}">
<form method="post" id="registrationForm" action="{% url 'add_registration' car.slug %}">
{% csrf_token %}
{{ form|crispy }}
<div class="d-flex gap-1">

View File

@ -67,7 +67,7 @@
<ul>
{% for trim in model.trims %}
<li>
<a href="{% url 'car_inventory' make_id=make.make_id model_id=model.model_id trim_id=trim.trim_id %}">
<a href="{% url 'car_inventory' make_id=make.slug model_id=model.slug trim_id=trim.slug %}">
{{ trim.trim_name }}
</a>&nbsp;-&nbsp;{% trans "Total" %}:
<strong>{{ trim.total_cars }}</strong></li>

View File

@ -84,7 +84,7 @@
<div class="avatar avatar-xl me-3"><img class="rounded-circle" src="{% static 'images/icons/picture.svg' %}" alt="" />
{% endif %}
</div>
<div><a class="fs-8 fw-bold" href="{% url 'vendor_detail' vendor.pk%}">{{ vendor.arabic_name }}</a>
<div><a class="fs-8 fw-bold" href="{% url 'vendor_detail' vendor.slug%}">{{ vendor.arabic_name }}</a>
<div class="d-flex align-items-center">
<p class="mb-0 text-body-highlight fw-semibold fs-9 me-2">{{ vendor.name}}</p><!--<span class="badge badge-phoenix badge-phoenix-primary">{{ vendor.vendor_model.uuid }}</span>-->
</div>
@ -101,12 +101,12 @@
<div class="btn-reveal-trigger position-static">
<button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal fs-10" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10"></span></button>
<div class="dropdown-menu dropdown-menu-end py-2">
<a href="{% url 'vendor_update' vendor.pk %}" class="dropdown-item text-success-dark">
<a href="{% url 'vendor_update' vendor.slug %}" class="dropdown-item text-success-dark">
{% trans "Edit" %}
</a>
<div class="dropdown-divider"></div>
<button class="delete-btn dropdown-item text-danger"
data-url="{% url 'vendor_delete' vendor.pk %}"
data-url="{% url 'vendor_delete' vendor.slug %}"
data-message="{{ _("Are you sure you want to delete this vendor")}}?"
data-bs-toggle="modal" data-bs-target="#deleteModal">
{{ _("Delete") }}

View File

@ -28,12 +28,12 @@
</ul>
</div>
<div class="card-footer d-flex">
<a class="btn btn-sm btn-phoenix-primary me-1" href="{% url 'vendor_update' vendor.id %}">
<a class="btn btn-sm btn-phoenix-primary me-1" href="{% url 'vendor_update' vendor.slug %}">
{% trans "Edit" %}
<i class="fa fa-pencil"></i>
</a>
<button class="btn btn-phoenix-danger btn-sm delete-btn"
data-url="{% url 'vendor_delete' vendor.pk %}"
data-url="{% url 'vendor_delete' vendor.slug %}"
data-message="{{ _("Are you sure you want to delete this vendor")}}?"
data-bs-toggle="modal" data-bs-target="#deleteModal">
{{ _("Delete") }}