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 datetime
import django.core.validators import django.core.validators
@ -7,6 +7,7 @@ import django.utils.timezone
import inventory.mixins import inventory.mixins
import inventory.models import inventory.models
import phonenumber_field.modelfields import phonenumber_field.modelfields
import uuid
from decimal import Decimal from decimal import Decimal
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@ -28,7 +29,10 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='Car', name='Car',
fields=[ 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')), ('vin', models.CharField(max_length=17, unique=True, verbose_name='VIN')),
('year', models.IntegerField(verbose_name='Year')), ('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')), ('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)), ('name', models.CharField(blank=True, max_length=255, null=True)),
('arabic_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)), ('year_begin', models.IntegerField(blank=True, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
], ],
options={ options={
'verbose_name': 'Equipment', 'verbose_name': 'Equipment',
@ -61,6 +66,7 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id_car_make', models.AutoField(primary_key=True, serialize=False)), ('id_car_make', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=255, null=True)), ('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)), ('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')), ('logo', models.ImageField(blank=True, null=True, upload_to='car_make', verbose_name='logo')),
('is_sa_import', models.BooleanField(default=False)), ('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)), ('id_car_model', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=255, null=True)), ('name', models.CharField(blank=True, max_length=255, null=True)),
('arabic_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')), ('id_car_make', models.ForeignKey(db_column='id_car_make', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake')),
], ],
options={ options={
@ -184,6 +191,7 @@ class Migration(migrations.Migration):
('id_car_option', models.AutoField(primary_key=True, serialize=False)), ('id_car_option', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=255, null=True)), ('name', models.CharField(blank=True, max_length=255, null=True)),
('arabic_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')), ('id_parent', models.ForeignKey(blank=True, db_column='id_parent', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.caroption')),
], ],
options={ options={
@ -230,6 +238,7 @@ class Migration(migrations.Migration):
('year_begin', models.IntegerField(blank=True, null=True)), ('year_begin', models.IntegerField(blank=True, null=True)),
('year_end', 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)), ('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')), ('id_car_model', models.ForeignKey(db_column='id_car_model', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel')),
], ],
options={ options={
@ -248,6 +257,7 @@ class Migration(migrations.Migration):
('id_car_specification', models.AutoField(primary_key=True, serialize=False)), ('id_car_specification', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)), ('name', models.CharField(max_length=255)),
('arabic_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')), ('id_parent', models.ForeignKey(blank=True, db_column='id_parent', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carspecification')),
], ],
options={ options={
@ -263,6 +273,7 @@ class Migration(migrations.Migration):
('arabic_name', models.CharField(blank=True, max_length=255, null=True)), ('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('start_production_year', models.IntegerField(blank=True, null=True)), ('start_production_year', models.IntegerField(blank=True, null=True)),
('end_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')), ('id_car_serie', models.ForeignKey(db_column='id_car_serie', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carserie')),
], ],
options={ 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 django.contrib.auth.models import Permission
from decimal import Decimal from decimal import Decimal
from django.utils.text import slugify
from django.utils import timezone from django.utils import timezone
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
import hashlib import hashlib
@ -30,6 +32,22 @@ from plans.models import UserPlan,Quota,PlanQuota
# from plans.models import AbstractPlan # from plans.models import AbstractPlan
# from simple_history.models import HistoricalRecords # 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): class DealerUserManager(UserManager):
def create_user_with_dealer( def create_user_with_dealer(
@ -162,11 +180,16 @@ class CarType(models.IntegerChoices):
class CarMake(models.Model, LocalizedNameMixin): class CarMake(models.Model, LocalizedNameMixin):
id_car_make = models.AutoField(primary_key=True) id_car_make = models.AutoField(primary_key=True)
name = models.CharField(max_length=255, blank=True, null=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) arabic_name = models.CharField(max_length=255, blank=True, null=True)
logo = models.ImageField(_("logo"), upload_to="car_make", blank=True, null=True) logo = models.ImageField(_("logo"), upload_to="car_make", blank=True, null=True)
is_sa_import = models.BooleanField(default=False) is_sa_import = models.BooleanField(default=False)
car_type = models.SmallIntegerField(choices=CarType.choices, blank=True, null=True) 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): def __str__(self):
return self.name 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") id_car_make = models.ForeignKey(CarMake, models.DO_NOTHING, db_column="id_car_make")
name = models.CharField(max_length=255, blank=True, null=True) name = models.CharField(max_length=255, blank=True, null=True)
arabic_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): def __str__(self):
return self.name return self.name
@ -196,6 +224,11 @@ class CarSerie(models.Model, LocalizedNameMixin):
year_begin = models.IntegerField(blank=True, null=True) year_begin = models.IntegerField(blank=True, null=True)
year_end = 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) 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): def __str__(self):
return self.name return self.name
@ -213,6 +246,11 @@ class CarTrim(models.Model, LocalizedNameMixin):
arabic_name = models.CharField(max_length=255, blank=True, null=True) arabic_name = models.CharField(max_length=255, blank=True, null=True)
start_production_year = models.IntegerField(blank=True, null=True) start_production_year = models.IntegerField(blank=True, null=True)
end_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): def __str__(self):
return self.name return self.name
@ -227,6 +265,11 @@ class CarEquipment(models.Model, LocalizedNameMixin):
name = models.CharField(max_length=255, blank=True, null=True) name = models.CharField(max_length=255, blank=True, null=True)
arabic_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) 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): def __str__(self):
return self.name return self.name
@ -242,6 +285,11 @@ class CarSpecification(models.Model, LocalizedNameMixin):
id_parent = models.ForeignKey( id_parent = models.ForeignKey(
"self", models.DO_NOTHING, db_column="id_parent", blank=True, null=True "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): def __str__(self):
return self.name return self.name
@ -273,6 +321,11 @@ class CarOption(models.Model, LocalizedNameMixin):
id_parent = models.ForeignKey( id_parent = models.ForeignKey(
"self", models.DO_NOTHING, db_column="id_parent", blank=True, null=True "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): def __str__(self):
return self.name return self.name
@ -368,7 +421,7 @@ class AdditionalServices(models.Model, LocalizedNameMixin):
return self.name + " - " + str(self.price) return self.name + " - " + str(self.price)
class Car(models.Model): class Car(Base):
vin = models.CharField(max_length=17, unique=True, verbose_name=_("VIN")) vin = models.CharField(max_length=17, unique=True, verbose_name=_("VIN"))
dealer = models.ForeignKey( dealer = models.ForeignKey(
"Dealer", models.DO_NOTHING, related_name="cars", verbose_name=_("Dealer") "Dealer", models.DO_NOTHING, related_name="cars", verbose_name=_("Dealer")
@ -432,6 +485,7 @@ class Car(models.Model):
# history = HistoricalRecords() # history = HistoricalRecords()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.slug = slugify(self.vin)
self.hash = self.get_hash self.hash = self.get_hash
super(Car, self).save(*args, **kwargs) 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" trim = self.id_car_trim.name if self.id_car_trim else "Unknown Trim"
return f"{self.year} - {make} - {model} - {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): def get_reservation(self):
return self.reservations.filter(reserved_until__gt=now()).first() return self.reservations.filter(reserved_until__gt=now()).first()
def is_reserved(self): def is_reserved(self):
@ -507,7 +564,7 @@ class Car(models.Model):
"mileage": self.mileage, "mileage": self.mileage,
"receiving_date": self.receiving_date.strftime('%Y-%m-%d %H:%M:%S'), "receiving_date": self.receiving_date.strftime('%Y-%m-%d %H:%M:%S'),
'hash': self.get_hash, 'hash': self.get_hash,
"id": self.id, "id": str(self.id),
} }
def get_specifications(self): 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")) joined_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Joined At"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated 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() objects = DealerUserManager()
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
@property @property
def active_plan(self): def active_plan(self):
@ -997,6 +1059,7 @@ class ActionChoices(models.TextChoices):
CALL = "call", _("Call") CALL = "call", _("Call")
SMS = "sms", _("SMS") SMS = "sms", _("SMS")
EMAIL = "email", _("Email") EMAIL = "email", _("Email")
MEETING = "meeting", _("Meeting")
WHATSAPP = "whatsapp", _("WhatsApp") WHATSAPP = "whatsapp", _("WhatsApp")
VISIT = "visit", _("Visit") VISIT = "visit", _("Visit")
LEAD_NEGOTIATION = "negotiation", _("Negotiation") LEAD_NEGOTIATION = "negotiation", _("Negotiation")
@ -1072,6 +1135,12 @@ class Customer(models.Model):
) )
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) 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: class Meta:
verbose_name = _("Customer") verbose_name = _("Customer")
@ -1348,7 +1417,12 @@ class Lead(models.Model):
auto_now_add=True, verbose_name=_("Created"), db_index=True auto_now_add=True, verbose_name=_("Created"), db_index=True
) )
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) 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: class Meta:
verbose_name = _("Lead") verbose_name = _("Lead")
verbose_name_plural = _("Leads") verbose_name_plural = _("Leads")
@ -1684,7 +1758,12 @@ class Vendor(models.Model, LocalizedNameMixin):
upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo") upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo")
) )
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) 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: class Meta:
verbose_name = _("Vendor") verbose_name = _("Vendor")
verbose_name_plural = _("Vendors") verbose_name_plural = _("Vendors")

View File

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

View File

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

View File

@ -213,7 +213,7 @@ def reserve_car(car, request):
except Exception as e: except Exception as e:
messages.error(request, f"Error reserving car: {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): def calculate_vat_amount(amount):

View File

@ -604,7 +604,7 @@ class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
return context return context
def car_history(request, pk): def car_history(request, slug):
""" """
Fetch and display the history of activities related to a specific car. 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. including the car and its associated activities as context data.
:rtype: HttpResponse :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( activities = models.Activity.objects.filter(
content_type__model="car", object_id=car.id content_type__model="car", object_id=car.id
) )
@ -859,7 +859,7 @@ class SearchCodeView(LoginRequiredMixin, View):
return JsonResponse({ return JsonResponse({
"success": True, "success": True,
"code": code, "code": code,
"redirect_url": reverse("car_detail", args=[car.pk]) "redirect_url": reverse("car_detail", args=[car.slug])
}) })
except Exception as e: except Exception as e:
@ -903,16 +903,16 @@ class CarInventory(LoginRequiredMixin, PermissionRequiredMixin, ListView):
def get_queryset(self, *args, **kwargs): def get_queryset(self, *args, **kwargs):
query = self.request.GET.get("q") query = self.request.GET.get("q")
make_id = self.kwargs["make_id"] make = models.CarMake.objects.get(slug=self.kwargs["make_id"])
model_id = self.kwargs["model_id"] model = models.CarModel.objects.get(slug=self.kwargs["model_id"])
trim_id = self.kwargs["trim_id"] trim = models.CarTrim.objects.get(slug=self.kwargs["trim_id"])
dealer = get_user_type(self.request) dealer = get_user_type(self.request)
cars = models.Car.objects.filter( cars = models.Car.objects.filter(
dealer=dealer, dealer=dealer,
id_car_make=make_id, id_car_make=make,
id_car_model=model_id, id_car_model=model,
id_car_trim=trim_id, id_car_trim=trim,
).order_by("receiving_date") ).order_by("receiving_date")
return apply_search_filters(cars, query) return apply_search_filters(cars, query)
@ -951,16 +951,16 @@ class CarColorCreate(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
permission_required = ["inventory.view_car"] permission_required = ["inventory.view_car"]
def form_valid(self, form): 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 form.instance.car = car
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self): 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): def get_context_data(self, **kwargs):
context = super().get_context_data(**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 return context
@ -1103,6 +1103,7 @@ def inventory_stats_view(request):
if make.id_car_make not in inventory: if make.id_car_make not in inventory:
inventory[make.id_car_make] = { inventory[make.id_car_make] = {
"make_id": make.id_car_make, "make_id": make.id_car_make,
"slug": make.slug,
"make_name": make.get_local_name(), "make_name": make.get_local_name(),
"total_cars": 0, "total_cars": 0,
"models": {}, "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"]: 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] = { inventory[make.id_car_make]["models"][model.id_car_model] = {
"model_id": model.id_car_model, "model_id": model.id_car_model,
"slug": model.slug,
"model_name": model.get_local_name(), "model_name": model.get_local_name(),
"total_cars": 0, "total_cars": 0,
"trims": {}, "trims": {},
@ -1132,6 +1134,7 @@ def inventory_stats_view(request):
trim.id_car_trim trim.id_car_trim
] = { ] = {
"trim_id": trim.id_car_trim, "trim_id": trim.id_car_trim,
"slug": trim.slug,
"trim_name": trim.name, "trim_name": trim.name,
"total_cars": 0, "total_cars": 0,
} }
@ -1146,11 +1149,13 @@ def inventory_stats_view(request):
"makes": [ "makes": [
{ {
"make_id": make_data["make_id"], "make_id": make_data["make_id"],
"slug": make_data["slug"],
"make_name": make_data["make_name"], "make_name": make_data["make_name"],
"total_cars": make_data["total_cars"], "total_cars": make_data["total_cars"],
"models": [ "models": [
{ {
"model_id": model_data["model_id"], "model_id": model_data["model_id"],
"slug": model_data["slug"],
"model_name": model_data["model_name"], "model_name": model_data["model_name"],
"total_cars": model_data["total_cars"], "total_cars": model_data["total_cars"],
"trims": list(model_data["trims"].values()), "trims": list(model_data["trims"].values()),
@ -1161,7 +1166,7 @@ def inventory_stats_view(request):
for make_data in inventory.values() for make_data in inventory.values()
], ],
} }
print(result['makes'])
return render(request, "inventory/inventory_stats.html", {"inventory": result}) return render(request, "inventory/inventory_stats.html", {"inventory": result})
@ -1218,7 +1223,7 @@ class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi
permission_required = ["inventory.add_carfinance"] permission_required = ["inventory.add_carfinance"]
def dispatch(self, request, *args, **kwargs): 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) return super().dispatch(request, *args, **kwargs)
def form_valid(self, form): def form_valid(self, form):
@ -1227,7 +1232,7 @@ class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self): 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): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -1273,7 +1278,7 @@ class CarFinanceUpdateView(
permission_required = ["inventory.change_carfinance"] permission_required = ["inventory.change_carfinance"]
def get_success_url(self): 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): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
@ -1325,7 +1330,7 @@ class CarUpdateView(
permission_required = ["inventory.change_car"] permission_required = ["inventory.change_car"]
def get_success_url(self): 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): def get_form(self, form_class=None):
form = super().get_form(form_class) form = super().get_form(form_class)
@ -1388,10 +1393,10 @@ class CarLocationCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateV
permission_required = ["inventory.add_carlocation"] permission_required = ["inventory.add_carlocation"]
def get_success_url(self): 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): 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) dealer = get_user_type(self.request)
form.instance.owner = dealer form.instance.owner = dealer
form.save() form.save()
@ -1425,11 +1430,11 @@ class CarLocationUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateV
# def get_initial(self): # def get_initial(self):
# initial = super().get_initial() # 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 # return initial
def form_valid(self, form): 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) dealer = get_user_type(self.request)
form.instance.owner = dealer form.instance.owner = dealer
form.save() form.save()
@ -1437,7 +1442,7 @@ class CarLocationUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateV
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self): 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): class CarTransferCreateView(LoginRequiredMixin, CreateView):
@ -1465,12 +1470,12 @@ class CarTransferCreateView(LoginRequiredMixin, CreateView):
form.fields["to_dealer"].queryset = models.Dealer.objects.exclude( form.fields["to_dealer"].queryset = models.Dealer.objects.exclude(
pk=get_user_type(self.request).pk pk=get_user_type(self.request).pk
).all() ).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 return form
def get_initial(self): def get_initial(self):
initial = super().get_initial() 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 return initial
def form_valid(self, form): def form_valid(self, form):
@ -1480,7 +1485,7 @@ class CarTransferCreateView(LoginRequiredMixin, CreateView):
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self): 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 @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 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 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. :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. :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) transfer = get_object_or_404(models.CarTransfer, pk=transfer_pk)
action = request.GET.get("action") action = request.GET.get("action")
if action == "cancel": if action == "cancel":
@ -1543,12 +1548,12 @@ def car_transfer_approve(request, car_pk, transfer_pk):
user=transfer.from_dealer.user, user=transfer.from_dealer.user,
message=f"Car transfer request from {transfer.to_dealer} is canceled.", 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.status = "approved"
transfer.save() transfer.save()
url = request.build_absolute_uri( url = request.build_absolute_uri(
reverse( 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( 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>", 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")) messages.success(request, _("Car transfer approved successfully"))
return redirect("car_detail", pk=car.pk) return redirect("car_detail", slug=car.slug)
@login_required @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 Handles the acceptance or rejection of a car transfer request. Based on the
`status` parameter obtained from the query string, the function updates 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. :param transfer_pk: The primary key of the car transfer request to be processed.
:return: An HTTP redirect response to the 'inventory_stats' view. :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) transfer = get_object_or_404(models.CarTransfer, pk=transfer_pk)
status = request.GET.get("status") status = request.GET.get("status")
if status == "rejected": if status == "rejected":
@ -1606,7 +1611,7 @@ def car_transfer_accept_reject(request, car_pk, transfer_pk):
@login_required @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 Handles the preview of car transfer details and ensures that a user has appropriate
permissions to view the transfer based on their associated dealer. 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) transfer = get_object_or_404(models.CarTransfer, pk=transfer_pk)
if transfer.to_dealer != get_user_type(request): 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}) return render(request, "inventory/transfer_preview.html", {"transfer": transfer})
@ -1651,18 +1656,18 @@ class CustomCardCreateView(LoginRequiredMixin, CreateView):
template_name = "inventory/add_custom_card.html" template_name = "inventory/add_custom_card.html"
def form_valid(self, form): 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 form.instance.car = car
return super().form_valid(form) return super().form_valid(form)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**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 return context
def get_success_url(self): def get_success_url(self):
messages.success(self.request, _("Custom Card added successfully")) 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): class CarRegistrationCreateView(LoginRequiredMixin, CreateView):
@ -1692,22 +1697,22 @@ class CarRegistrationCreateView(LoginRequiredMixin, CreateView):
template_name = "inventory/car_registration_form.html" template_name = "inventory/car_registration_form.html"
def form_valid(self, form): 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 form.instance.car = car
return super().form_valid(form) return super().form_valid(form)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**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 return context
def get_success_url(self): def get_success_url(self):
messages.success(self.request, _("Registration added successfully")) 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() @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 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 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 :rtype: HttpResponse or JsonResponse
""" """
if request.method == "POST": 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(): if car.is_reserved():
messages.error(request, _("This car is already 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) response = reserve_car(car, request)
return response return response
return JsonResponse( return JsonResponse(
@ -1764,7 +1769,7 @@ def manage_reservation(request, reservation_id):
reservation.reserved_until = timezone.now() + timezone.timedelta(hours=24) reservation.reserved_until = timezone.now() + timezone.timedelta(hours=24)
reservation.save() reservation.save()
messages.success(request, _("Reservation renewed successfully")) 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": elif action == "cancel":
car = reservation.car car = reservation.car
@ -1772,7 +1777,7 @@ def manage_reservation(request, reservation_id):
car.status = models.CarStatusChoices.AVAILABLE car.status = models.CarStatusChoices.AVAILABLE
car.save() car.save()
messages.success(request, _("Reservation canceled successfully")) messages.success(request, _("Reservation canceled successfully"))
return redirect("car_detail", pk=reservation.car.pk) return redirect("car_detail", slug=reservation.car.slug)
else: else:
return JsonResponse( return JsonResponse(
@ -1857,7 +1862,7 @@ class DealerUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
success_message = _("Dealer updated successfully") success_message = _("Dealer updated successfully")
def get_success_url(self): 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): class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
@ -2148,7 +2153,7 @@ class VendorListView(LoginRequiredMixin, ListView):
@login_required @login_required
def vendorDetailView(request, pk): def vendorDetailView(request, slug):
""" """
Fetches and renders the detail view for a specific vendor. 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. :return: An HttpResponse object containing the rendered vendor detail page.
:rtype: HttpResponse :rtype: HttpResponse
""" """
vendor = get_object_or_404(models.Vendor, pk=pk) vendor = get_object_or_404(models.Vendor, slug=slug)
return render( return render(
request, template_name="vendors/view_vendor.html", context={"vendor": vendor} request, template_name="vendors/view_vendor.html", context={"vendor": vendor}
) )
@ -2258,7 +2263,7 @@ class VendorUpdateView(
@login_required @login_required
def delete_vendor(request, pk): def delete_vendor(request, slug):
""" """
Deletes an existing vendor record from the database. 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. :return: HttpResponseRedirect object for redirecting to the vendor list page.
:rtype: HttpResponseRedirect :rtype: HttpResponseRedirect
""" """
vendor = get_object_or_404(models.Vendor, pk=pk) vendor = get_object_or_404(models.Vendor, slug=slug)
vendor.active = False vendor.active = False
vendor.vendor_model.active = False vendor.vendor_model.active = False
vendor.save() vendor.save()
@ -2367,7 +2372,7 @@ class GroupCreateView(
def form_valid(self, form): def form_valid(self, form):
dealer = get_user_type(self.request) dealer = get_user_type(self.request)
instance = form.save(commit=False) 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.dealer = dealer
instance.group = group instance.group = group
instance.save() instance.save()
@ -2410,7 +2415,7 @@ class GroupUpdateView(
dealer = get_user_type(self.request) dealer = get_user_type(self.request)
instance = form.save(commit=False) instance = form.save(commit=False)
instance.set_defualt_permissions() instance.set_defualt_permissions()
instance.group.name = f"{dealer.pk}_{instance.name}" instance.group.name = f"{dealer.slug}_{instance.name}"
instance.save() instance.save()
return super().form_valid(form) return super().form_valid(form)
@ -4812,7 +4817,7 @@ def add_note_to_lead(request, pk):
note.created_by = request.user note.created_by = request.user
note.save() note.save()
messages.success(request, _("Note added successfully")) messages.success(request, _("Note added successfully"))
return redirect("lead_detail", pk=lead.pk) return redirect("lead_detail", slug=lead.slug)
else: else:
form = forms.NoteForm() form = forms.NoteForm()
return render(request, "crm/note_form.html", {"form": form, "lead": lead}) 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) note = get_object_or_404(models.Notes, pk=pk, created_by=request.user)
lead_pk = note.content_object.pk lead_pk = note.content_object.pk
lead = models.Lead.objects.get(pk=lead_pk)
if request.method == "POST": if request.method == "POST":
form = forms.NoteForm(request.POST, instance=note) form = forms.NoteForm(request.POST, instance=note)
@ -4874,7 +4880,7 @@ def update_note(request, pk):
updated_note.created_by = request.user updated_note.created_by = request.user
updated_note.save() updated_note.save()
messages.success(request, _("Note updated successfully")) messages.success(request, _("Note updated successfully"))
return redirect("lead_detail", pk=lead_pk) return redirect("lead_detail", slug=lead.slug)
else: else:
form = forms.NoteForm(instance=note) form = forms.NoteForm(instance=note)
@ -5035,7 +5041,7 @@ def lead_transfer(request, pk):
@login_required @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 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 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. or email composition rendering, a response object is returned to render the respective page.
Type: HttpResponse 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") status = request.GET.get("status")
dealer = get_user_type(request) dealer = get_user_type(request)
if status == 'draft': 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.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) 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")) messages.success(request, _("Email Draft successfully"))
response = HttpResponse(redirect("lead_detail", pk=lead.pk)) response = HttpResponse(redirect("lead_detail", slug=lead.slug))
response["HX-Redirect"] = reverse("lead_detail", args=[lead.pk]) response["HX-Redirect"] = reverse("lead_detail", args=[lead.slug])
return response return response
if request.method == "POST": if request.method == "POST":
@ -7028,7 +7034,7 @@ class CarListViewTable(LoginRequiredMixin, ExportMixin, SingleTableView):
@login_required @login_required
def DealerSettingsView(request, pk): def DealerSettingsView(request, slug):
""" """
Handles dealer settings view where dealers can update their financial and Handles dealer settings view where dealers can update their financial and
payment account settings. This view ensures validation and reassigns form 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. to the dealer detail view after successful form submission.
:rtype: HttpResponse :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) dealer = get_user_type(request)
if request.method == "POST": if request.method == "POST":
form = forms.DealerSettingsForm(request.POST, instance=dealer_setting) form = forms.DealerSettingsForm(request.POST, instance=dealer_setting)
@ -7054,7 +7060,7 @@ def DealerSettingsView(request, pk):
instance.dealer = dealer instance.dealer = dealer
instance.save() instance.save()
messages.success(request, _('Settings updated')) messages.success(request, _('Settings updated'))
return redirect('dealer_detail', pk=dealer.pk) return redirect('dealer_detail', slug=dealer.slug)
else: else:
print(form.errors) print(form.errors)
form = forms.DealerSettingsForm(instance=dealer_setting, initial={"dealer": dealer}) form = forms.DealerSettingsForm(instance=dealer_setting, initial={"dealer": dealer})
@ -7134,7 +7140,7 @@ def assign_car_makes(request):
makes = form.cleaned_data["car_makes"] makes = form.cleaned_data["car_makes"]
create_accounts_for_make(dealer, makes) create_accounts_for_make(dealer, makes)
form.save() form.save()
return redirect("dealer_detail", pk=dealer.pk) return redirect("dealer_detail", slug=dealer.slug)
else: else:
print(form.errors) print(form.errors)
else: else:
@ -7743,13 +7749,13 @@ def notifications_history(request):
# ) # )
# return render(request, 'activity_history.html') # return render(request, 'activity_history.html')
def add_activity(request,content_type,pk): def add_activity(request,content_type,slug):
try: try:
model = apps.get_model(f'inventory.{content_type}') model = apps.get_model(f'inventory.{content_type}')
except LookupError: except LookupError:
raise Http404("Model not found") 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) dealer = get_user_type(request)
if request.method == "POST": if request.method == "POST":
form = forms.ActivityForm(request.POST) form = forms.ActivityForm(request.POST)
@ -7765,14 +7771,14 @@ def add_activity(request,content_type,pk):
messages.success(request, _("Activity added successfully")) messages.success(request, _("Activity added successfully"))
else: else:
messages.error(request, _("Activity form is not valid")) messages.error(request, _("Activity form is not valid"))
return redirect(f"{content_type}_detail", pk=pk) return redirect(f"{content_type}_detail", slug=slug)
def add_task(request,content_type,pk): def add_task(request,content_type,slug):
try: try:
model = apps.get_model(f'inventory.{content_type}') model = apps.get_model(f'inventory.{content_type}')
except LookupError: except LookupError:
raise Http404("Model not found") 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) dealer = get_user_type(request)
if request.method == "POST": if request.method == "POST":
form = forms.StaffTaskForm(request.POST) form = forms.StaffTaskForm(request.POST)
@ -7788,16 +7794,22 @@ def add_task(request,content_type,pk):
else: else:
print(form.errors) print(form.errors)
messages.error(request, _("Task form is not valid")) 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): def update_task(request,pk):
task = get_object_or_404(models.Tasks, pk=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": if request.method == "POST":
task.completed = False if task.completed else True task.completed = False if task.completed else True
task.save() task.save()
messages.success(request, _("Task updated successfully")) messages.success(request, _("Task updated successfully"))
else: else:
messages.error(request, _("Task form is not valid")) messages.error(request, _("Task form is not valid"))
response = HttpResponse() # response = HttpResponse()
response['HX-Refresh'] = 'true' # response['HX-Refresh'] = 'true'
return response # 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 initial data"
echo "Loading carmake" 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" 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" 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" 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" 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" 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" echo "Populating colors"
python3 manage.py populate_colors python3 manage.py populate_colors
@ -26,4 +26,6 @@ python3 manage.py tenhal_plan
python3 manage.py set_vat python3 manage.py set_vat
python3 manage.py initial_services_offered
echo "Done" 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 { @keyframes spin {
to { transform: rotate(360deg); } 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 { .form-select {
--phoenix-form-select-bg-img: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUwIiBoZWlnaHQ9IjE1MCIgdmlld0JveD0iMCAwIDE1MCAxNTAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik03NS4zNDggMTI3LjE5MkM3Mi40MzgxIDEyNy4xOTIgNjkuODUxNCAxMjYuMjIyIDY3LjkxMTUgMTI0LjI4Mkw1LjgzMjE1IDYyLjIwMjNDMS42Mjg4NyA1OC4zMjIzIDEuNjI4ODcgNTEuNTMyNCA1LjgzMjE1IDQ3LjY1MjVDOS43MTIxMSA0My40NDkyIDE2LjUwMiA0My40NDkyIDIwLjM4MiA0Ny42NTI1TDc1LjM0OCAxMDIuMjk1TDEyOS45OTEgNDcuNjUyNUMxMzMuODcxIDQzLjQ0OTIgMTQwLjY2MSA0My40NDkyIDE0NC41NDEgNDcuNjUyNUMxNDguNzQ0IDUxLjUzMjQgMTQ4Ljc0NCA1OC4zMjIzIDE0NC41NDEgNjIuMjAyM0w4Mi40NjEzIDEyNC4yODJDODAuNTIxMyAxMjYuMjIyIDc3LjkzNDcgMTI3LjE5MiA3NS4zNDggMTI3LjE5MloiIGZpbGw9IiMzMTM3NEEiLz4KPC9zdmc+Cg=="); --phoenix-form-select-bg-img: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUwIiBoZWlnaHQ9IjE1MCIgdmlld0JveD0iMCAwIDE1MCAxNTAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik03NS4zNDggMTI3LjE5MkM3Mi40MzgxIDEyNy4xOTIgNjkuODUxNCAxMjYuMjIyIDY3LjkxMTUgMTI0LjI4Mkw1LjgzMjE1IDYyLjIwMjNDMS42Mjg4NyA1OC4zMjIzIDEuNjI4ODcgNTEuNTMyNCA1LjgzMjE1IDQ3LjY1MjVDOS43MTIxMSA0My40NDkyIDE2LjUwMiA0My40NDkyIDIwLjM4MiA0Ny42NTI1TDc1LjM0OCAxMDIuMjk1TDEyOS45OTEgNDcuNjUyNUMxMzMuODcxIDQzLjQ0OTIgMTQwLjY2MSA0My40NDkyIDE0NC41NDEgNDcuNjUyNUMxNDguNzQ0IDUxLjUzMjQgMTQ4Ljc0NCA1OC4zMjIzIDE0NC41NDEgNjIuMjAyM0w4Mi40NjEzIDEyNC4yODJDODAuNTIxMyAxMjYuMjIyIDc3LjkzNDcgMTI3LjE5MiA3NS4zNDggMTI3LjE5MloiIGZpbGw9IiMzMTM3NEEiLz4KPC9zdmc+Cg==");
align-content: start;
display: block; display: block;
text-align: start; text-align: right;
width: 100%; width: 100%;
padding: 0.5rem 1rem 0.5rem 2.5rem; padding: 0.5rem 1rem 0.5rem 2.5rem;
font-size: 0.8rem; font-size: 0.8rem;
@ -3808,9 +3809,12 @@ textarea.form-control-lg {
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.form-select { .form-select {
-webkit-transition: none; -webkit-transition:right;
-o-transition: none; -o-transition: none;
transition: none; transition: none;
right: auto;
left: 0;
direction: rtl;
} }
} }
.form-select:focus { .form-select:focus {

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -10,7 +10,7 @@
</button> </button>
</div> </div>
<div class="modal-body"> <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 %} {% csrf_token %}
<div class="mb-2 form-group"> <div class="mb-2 form-group">
<select class="form-select" name="activity_type" id="activity_type"> <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="tab-pane fade" id="tab-emails" role="tabpanel" aria-labelledby="emails-tab">
<div class="mb-1 d-flex justify-content-between align-items-center"> <div class="mb-1 d-flex justify-content-between align-items-center">
<h3 class="mb-0" id="scrollspyEmails">{{ _("Emails") }}</h3> <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"> <button type="button" class="btn btn-sm btn-phoenix-primary">
<span class="fas fa-plus me-1"></span> <span class="fas fa-plus me-1"></span>
{% trans 'Send Email' %} {% trans 'Send Email' %}
@ -448,7 +448,7 @@
</td> </td>
<td class="sent align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2">{{email.from_email}}</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="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> <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> </tr>
{% endfor %} {% endfor %}
@ -496,10 +496,10 @@
</thead> </thead>
<tbody class="list" id="all-email-table-body"> <tbody class="list" id="all-email-table-body">
{% for task in tasks %} {% 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"> <td class="fs-9 align-middle px-0 py-3">
<div class="form-check mb-0 fs-8"> <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> </div>
</td> </td>
<td class="subject order align-middle white-space-nowrap py-2 ps-0"><a class="fw-semibold text-primary" href="">{{task.title}}</a> <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> </div>
<!-- activity Modal --> <!-- 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 --> <!-- task Modal -->
<div class="modal fade" id="taskModal" tabindex="-1" aria-labelledby="taskModalLabel" aria-hidden="true"> <div class="modal fade" id="taskModal" tabindex="-1" aria-labelledby="taskModalLabel" aria-hidden="true">
@ -565,7 +565,7 @@
</button> </button>
</div> </div>
<div class="modal-body"> <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 %} {% csrf_token %}
{{ staff_task_form|crispy }} {{ staff_task_form|crispy }}
<button type="submit" class="btn btn-success w-100">{% trans 'Save' %}</button> <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> <p>{% trans "Are you sure you want to delete this lead?" %}</p>
</div> </div>
<div class="modal-footer flex justify-content-center border-top-0"> <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" %} {% trans "Yes" %}
</a> </a>
</div> </div>
@ -159,7 +159,7 @@
<tr class="hover-actions-trigger btn-reveal-trigger position-static"> <tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="name align-middle white-space-nowrap ps-0"> <td class="name align-middle white-space-nowrap ps-0">
<div class="d-flex align-items-center"> <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"> <div class="d-flex align-items-center">
<p class="mb-0 text-body-highlight fw-semibold fs-9 me-2"></p> <p class="mb-0 text-body-highlight fw-semibold fs-9 me-2"></p>
{% if lead.status == "new" %} {% if lead.status == "new" %}
@ -187,11 +187,11 @@
<div class="accordion" id="accordionExample"> <div class="accordion" id="accordionExample">
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header" id="headingTwo"> <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}}) View Schedules ({{lead.get_latest_schedules.count}})
</button> </button>
</h2> </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="accordion-body pt-0">
<div class="d-flex flex-column gap-2"> <div class="d-flex flex-column gap-2">
<table><tbody> <table><tbody>

View File

@ -8,7 +8,7 @@
<div class="card email-content"> <div class="card email-content">
<h5 class="card-header">Send Mail</h5> <h5 class="card-header">Send Mail</h5>
<div class="card-body"> <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 %} {% csrf_token %}
<div class="row g-3 mb-2"> <div class="row g-3 mb-2">
<div class="col-12"> <div class="col-12">
@ -26,8 +26,8 @@
</div> </div>
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<div class="d-flex gap-2"> <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 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.pk %}?status=draft" class="btn btn-secondary text-white fs-10 text-decoration-none">Save as Draft</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> <button class="btn btn-primary fs-10" type="submit">Send<span class="fa-solid fa-paper-plane ms-1"></span></button>
</div> </div>
</div> </div>

View File

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

View File

@ -70,7 +70,7 @@
</td> </td>
<td class="name align-middle white-space-nowrap ps-0"> <td class="name align-middle white-space-nowrap ps-0">
<div class="d-flex align-items-center"> <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 class="d-flex align-items-center">
</div> </div>
</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="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"> <td class="align-middle white-space-nowrap text-end pe-0 ps-4">
{% if perms.django_ledger.change_customermodel %} {% 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> <i class="fas fa-pen"></i>
</a> </a>
{% endif %} {% endif %}
{% if perms.django_ledger.delete_customermodel %} {% if perms.django_ledger.delete_customermodel %}
<button class="btn btn-phoenix-danger btn-sm delete-btn" <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-message="{{ _("Are you sure you want to delete this customer")}}"
data-bs-toggle="modal" data-bs-target="#deleteModal"> data-bs-toggle="modal" data-bs-target="#deleteModal">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>

View File

@ -17,7 +17,7 @@
<div class="col-auto"> <div class="col-auto">
{% if perms.django_ledger.delete_customermodel %} {% if perms.django_ledger.delete_customermodel %}
<button class="btn btn-phoenix-danger btn-sm delete-btn" <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-message="Are you sure you want to delete this customer?"
data-bs-toggle="modal" data-bs-target="#deleteModal"> data-bs-toggle="modal" data-bs-target="#deleteModal">
<i class="fas fa-trash me-1"> </i>{{ _("Delete") }} <i class="fas fa-trash me-1"> </i>{{ _("Delete") }}
@ -27,7 +27,7 @@
<div class="col-auto"> <div class="col-auto">
{% if perms.django_ledger.change_customermodel %} {% 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 %} {% endif %}
</div> </div>
</div> </div>
@ -81,7 +81,7 @@
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center justify-content-end"> <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> <span class="fas fa-plus me-1"></span>
{% trans 'Add Note' %} {% trans 'Add Note' %}
</a> </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> <a href="{% url 'account_change_password' %}" class="btn btn-phoenix-danger"><span class="fas fa-key me-2"></span>{{ _("Change Password") }}</a>
</div> </div>
<div class="col-auto"> <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> </div>
</div> </div>

View File

@ -415,7 +415,7 @@
<ul class="nav d-flex flex-column mb-2 pb-1"> <ul class="nav d-flex flex-column mb-2 pb-1">
{% if request.is_dealer %} {% if request.is_dealer %}
<li class="nav-item"> <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> </li>
{% else %} {% else %}
<li class="nav-item"> <li class="nav-item">
@ -432,7 +432,7 @@
{% endif %} {% endif %}
<li class="nav-item"> <li class="nav-item">
{% if request.is_dealer %} {% 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 %} {% endif %}
</li> </li>
<li class="nav-item"> <li class="nav-item">

View File

@ -1,7 +1,7 @@
{% load i18n %} {% load i18n %}
{% load crispy_forms_filters %} {% 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 %} {% csrf_token %}
{{ form|crispy }} {{ form|crispy }}
<div class="d-flex gap-1"> <div class="d-flex gap-1">

View File

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

View File

@ -70,7 +70,7 @@
<span class="badge badge-phoenix badge-phoenix-info"><span class="badge-label">{{_("Used")}}</span></span> <span class="badge badge-phoenix badge-phoenix-info"><span class="badge-label">{{_("Used")}}</span></span>
{% endif %} {% endif %}
</td> </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> <td class="align-middle white-space-nowrap text-center fw-bold">{{ car.year }}</td>
{% if car.colors %} {% if car.colors %}
<td class="align-middle white-space-nowrap text-body fs-9 text-start"> <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> <span class="fw-light">{{ car.receiving_date|timesince }}</span>
</td> </td>
<td class="align-middle white-space-nowrap text-end pe-0 ps-4"> <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> </td>
</tr> </tr>
{% empty %} {% empty %}

View File

@ -117,10 +117,10 @@
</tr> </tr>
</thead> </thead>
<tbody class="list" id="project-list-table-body"> <tbody class="list" id="project-list-table-body">
{% for car in page_obj %} {% for car in cars %}
<tr class="position-static"> <tr class="position-static">
<td class="align-middle white-space-nowrap ps-1"> <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>
<td class="align-middle white-space-nowrap"> <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> <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"> <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> <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"> <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> <a class="dropdown-item" href="#!">{{ _("Export") }}</a>
</div> </div>
</div> </div>

View File

@ -2,7 +2,7 @@
{% load crispy_forms_filters %} {% load crispy_forms_filters %}
<div class="w-100 g-3"> <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 %} {% csrf_token %}
{{ form|crispy }} {{ form|crispy }}
<div class="d-flex gap-1"> <div class="d-flex gap-1">

View File

@ -67,7 +67,7 @@
<ul> <ul>
{% for trim in model.trims %} {% for trim in model.trims %}
<li> <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 }} {{ trim.trim_name }}
</a>&nbsp;-&nbsp;{% trans "Total" %}: </a>&nbsp;-&nbsp;{% trans "Total" %}:
<strong>{{ trim.total_cars }}</strong></li> <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="" /> <div class="avatar avatar-xl me-3"><img class="rounded-circle" src="{% static 'images/icons/picture.svg' %}" alt="" />
{% endif %} {% endif %}
</div> </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"> <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>--> <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> </div>
@ -101,12 +101,12 @@
<div class="btn-reveal-trigger position-static"> <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> <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"> <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" %} {% trans "Edit" %}
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<button class="delete-btn dropdown-item text-danger" <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-message="{{ _("Are you sure you want to delete this vendor")}}?"
data-bs-toggle="modal" data-bs-target="#deleteModal"> data-bs-toggle="modal" data-bs-target="#deleteModal">
{{ _("Delete") }} {{ _("Delete") }}

View File

@ -28,12 +28,12 @@
</ul> </ul>
</div> </div>
<div class="card-footer d-flex"> <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" %} {% trans "Edit" %}
<i class="fa fa-pencil"></i> <i class="fa fa-pencil"></i>
</a> </a>
<button class="btn btn-phoenix-danger btn-sm delete-btn" <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-message="{{ _("Are you sure you want to delete this vendor")}}?"
data-bs-toggle="modal" data-bs-target="#deleteModal"> data-bs-toggle="modal" data-bs-target="#deleteModal">
{{ _("Delete") }} {{ _("Delete") }}