diff --git a/inventory/management/commands/generate_slugs.py b/inventory/management/commands/generate_slugs.py new file mode 100644 index 00000000..6c9dfdfa --- /dev/null +++ b/inventory/management/commands/generate_slugs.py @@ -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}') \ No newline at end of file diff --git a/inventory/migrations/0001_initial.py b/inventory/migrations/0001_initial.py index 17edcafb..72926847 100644 --- a/inventory/migrations/0001_initial.py +++ b/inventory/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.7 on 2025-05-18 11:18 +# Generated by Django 5.1.7 on 2025-05-18 15:52 import datetime import django.core.validators @@ -7,6 +7,7 @@ import django.utils.timezone import inventory.mixins import inventory.models import phonenumber_field.modelfields +import uuid from decimal import Decimal from django.conf import settings from django.db import migrations, models @@ -28,7 +29,10 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Car', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='Primary Key')), + ('slug', models.SlugField(blank=True, help_text='Slug for the object. If not provided, it will be generated automatically.', null=True, unique=True, verbose_name='Slug')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')), ('vin', models.CharField(max_length=17, unique=True, verbose_name='VIN')), ('year', models.IntegerField(verbose_name='Year')), ('status', models.CharField(choices=[('available', 'Available'), ('sold', 'Sold'), ('hold', 'Hold'), ('damaged', 'Damaged'), ('reserved', 'Reserved'), ('transfer', 'Transfer')], default='available', max_length=10, verbose_name='Status')), @@ -50,6 +54,7 @@ class Migration(migrations.Migration): ('name', models.CharField(blank=True, max_length=255, null=True)), ('arabic_name', models.CharField(blank=True, max_length=255, null=True)), ('year_begin', models.IntegerField(blank=True, null=True)), + ('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)), ], options={ 'verbose_name': 'Equipment', @@ -61,6 +66,7 @@ class Migration(migrations.Migration): fields=[ ('id_car_make', models.AutoField(primary_key=True, serialize=False)), ('name', models.CharField(blank=True, max_length=255, null=True)), + ('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)), ('arabic_name', models.CharField(blank=True, max_length=255, null=True)), ('logo', models.ImageField(blank=True, null=True, upload_to='car_make', verbose_name='logo')), ('is_sa_import', models.BooleanField(default=False)), @@ -166,6 +172,7 @@ class Migration(migrations.Migration): ('id_car_model', models.AutoField(primary_key=True, serialize=False)), ('name', models.CharField(blank=True, max_length=255, null=True)), ('arabic_name', models.CharField(blank=True, max_length=255, null=True)), + ('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)), ('id_car_make', models.ForeignKey(db_column='id_car_make', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake')), ], options={ @@ -184,6 +191,7 @@ class Migration(migrations.Migration): ('id_car_option', models.AutoField(primary_key=True, serialize=False)), ('name', models.CharField(blank=True, max_length=255, null=True)), ('arabic_name', models.CharField(blank=True, max_length=255, null=True)), + ('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)), ('id_parent', models.ForeignKey(blank=True, db_column='id_parent', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.caroption')), ], options={ @@ -230,6 +238,7 @@ class Migration(migrations.Migration): ('year_begin', models.IntegerField(blank=True, null=True)), ('year_end', models.IntegerField(blank=True, null=True)), ('generation_name', models.CharField(blank=True, max_length=255, null=True)), + ('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)), ('id_car_model', models.ForeignKey(db_column='id_car_model', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel')), ], options={ @@ -248,6 +257,7 @@ class Migration(migrations.Migration): ('id_car_specification', models.AutoField(primary_key=True, serialize=False)), ('name', models.CharField(max_length=255)), ('arabic_name', models.CharField(max_length=255)), + ('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)), ('id_parent', models.ForeignKey(blank=True, db_column='id_parent', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carspecification')), ], options={ @@ -263,6 +273,7 @@ class Migration(migrations.Migration): ('arabic_name', models.CharField(blank=True, max_length=255, null=True)), ('start_production_year', models.IntegerField(blank=True, null=True)), ('end_production_year', models.IntegerField(blank=True, null=True)), + ('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)), ('id_car_serie', models.ForeignKey(db_column='id_car_serie', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carserie')), ], options={ diff --git a/inventory/migrations/0002_dealer_slug.py b/inventory/migrations/0002_dealer_slug.py new file mode 100644 index 00000000..85bf7f83 --- /dev/null +++ b/inventory/migrations/0002_dealer_slug.py @@ -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), + ), + ] diff --git a/inventory/migrations/0003_customer_slug.py b/inventory/migrations/0003_customer_slug.py new file mode 100644 index 00000000..1fa22a02 --- /dev/null +++ b/inventory/migrations/0003_customer_slug.py @@ -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), + ), + ] diff --git a/inventory/migrations/0004_vendor_slug.py b/inventory/migrations/0004_vendor_slug.py new file mode 100644 index 00000000..d7237ab6 --- /dev/null +++ b/inventory/migrations/0004_vendor_slug.py @@ -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'), + ), + ] diff --git a/inventory/migrations/0005_lead_slug.py b/inventory/migrations/0005_lead_slug.py new file mode 100644 index 00000000..7e6da6ba --- /dev/null +++ b/inventory/migrations/0005_lead_slug.py @@ -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), + ), + ] diff --git a/inventory/migrations/0006_alter_activity_activity_type.py b/inventory/migrations/0006_alter_activity_activity_type.py new file mode 100644 index 00000000..cd86d3fa --- /dev/null +++ b/inventory/migrations/0006_alter_activity_activity_type.py @@ -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'), + ), + ] diff --git a/inventory/models.py b/inventory/models.py index 03f37493..99504962 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -1,5 +1,7 @@ +import uuid from django.contrib.auth.models import Permission from decimal import Decimal +from django.utils.text import slugify from django.utils import timezone from django.core.validators import MinValueValidator, MaxValueValidator import hashlib @@ -30,6 +32,22 @@ from plans.models import UserPlan,Quota,PlanQuota # from plans.models import AbstractPlan # from simple_history.models import HistoricalRecords +class Base(models.Model): + id = models.UUIDField(unique=True, editable=False, default=uuid.uuid4, primary_key=True,verbose_name=_("Primary Key")) + slug = models.SlugField(null=True, blank=True, unique=True,verbose_name=_("Slug"), + help_text=_("Slug for the object. If not provided, it will be generated automatically.")) + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) + updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) + def clean(self): + if isinstance(self.id, str): + try: + uuid.UUID(self.id) + except ValueError: + raise ValidationError({'id': 'Invalid UUID format'}) + super().clean() + class Meta: + abstract = True + class DealerUserManager(UserManager): def create_user_with_dealer( @@ -162,11 +180,16 @@ class CarType(models.IntegerChoices): class CarMake(models.Model, LocalizedNameMixin): id_car_make = models.AutoField(primary_key=True) name = models.CharField(max_length=255, blank=True, null=True) + slug = models.SlugField(max_length=255, unique=True, blank=True, null=True) arabic_name = models.CharField(max_length=255, blank=True, null=True) logo = models.ImageField(_("logo"), upload_to="car_make", blank=True, null=True) is_sa_import = models.BooleanField(default=False) car_type = models.SmallIntegerField(choices=CarType.choices, blank=True, null=True) + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) def __str__(self): return self.name @@ -178,6 +201,11 @@ class CarModel(models.Model, LocalizedNameMixin): id_car_make = models.ForeignKey(CarMake, models.DO_NOTHING, db_column="id_car_make") name = models.CharField(max_length=255, blank=True, null=True) arabic_name = models.CharField(max_length=255, blank=True, null=True) + slug = models.SlugField(max_length=255, unique=True, blank=True, null=True) + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) def __str__(self): return self.name @@ -196,6 +224,11 @@ class CarSerie(models.Model, LocalizedNameMixin): year_begin = models.IntegerField(blank=True, null=True) year_end = models.IntegerField(blank=True, null=True) generation_name = models.CharField(max_length=255, blank=True, null=True) + slug = models.SlugField(max_length=255, unique=True, blank=True, null=True) + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) def __str__(self): return self.name @@ -213,6 +246,11 @@ class CarTrim(models.Model, LocalizedNameMixin): arabic_name = models.CharField(max_length=255, blank=True, null=True) start_production_year = models.IntegerField(blank=True, null=True) end_production_year = models.IntegerField(blank=True, null=True) + slug = models.SlugField(max_length=255, unique=True, blank=True, null=True) + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) def __str__(self): return self.name @@ -227,6 +265,11 @@ class CarEquipment(models.Model, LocalizedNameMixin): name = models.CharField(max_length=255, blank=True, null=True) arabic_name = models.CharField(max_length=255, blank=True, null=True) year_begin = models.IntegerField(blank=True, null=True) + slug = models.SlugField(max_length=255, unique=True, blank=True, null=True) + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) def __str__(self): return self.name @@ -242,6 +285,11 @@ class CarSpecification(models.Model, LocalizedNameMixin): id_parent = models.ForeignKey( "self", models.DO_NOTHING, db_column="id_parent", blank=True, null=True ) + slug = models.SlugField(max_length=255, unique=True, blank=True, null=True) + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) def __str__(self): return self.name @@ -273,6 +321,11 @@ class CarOption(models.Model, LocalizedNameMixin): id_parent = models.ForeignKey( "self", models.DO_NOTHING, db_column="id_parent", blank=True, null=True ) + slug = models.SlugField(max_length=255, unique=True, blank=True, null=True) + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) def __str__(self): return self.name @@ -368,7 +421,7 @@ class AdditionalServices(models.Model, LocalizedNameMixin): return self.name + " - " + str(self.price) -class Car(models.Model): +class Car(Base): vin = models.CharField(max_length=17, unique=True, verbose_name=_("VIN")) dealer = models.ForeignKey( "Dealer", models.DO_NOTHING, related_name="cars", verbose_name=_("Dealer") @@ -432,6 +485,7 @@ class Car(models.Model): # history = HistoricalRecords() def save(self, *args, **kwargs): + self.slug = slugify(self.vin) self.hash = self.get_hash super(Car, self).save(*args, **kwargs) @@ -445,6 +499,9 @@ class Car(models.Model): trim = self.id_car_trim.name if self.id_car_trim else "Unknown Trim" return f"{self.year} - {make} - {model} - {trim}" + @property + def product(self): + return self.dealer.entity.get_items_all().filter(name=self.vin).first() def get_reservation(self): return self.reservations.filter(reserved_until__gt=now()).first() def is_reserved(self): @@ -507,7 +564,7 @@ class Car(models.Model): "mileage": self.mileage, "receiving_date": self.receiving_date.strftime('%Y-%m-%d %H:%M:%S'), 'hash': self.get_hash, - "id": self.id, + "id": str(self.id), } def get_specifications(self): @@ -830,9 +887,14 @@ class Dealer(models.Model, LocalizedNameMixin): ) joined_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Joined At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) + slug = models.SlugField(max_length=255, unique=True, blank=True, null=True) objects = DealerUserManager() + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) @property def active_plan(self): @@ -997,6 +1059,7 @@ class ActionChoices(models.TextChoices): CALL = "call", _("Call") SMS = "sms", _("SMS") EMAIL = "email", _("Email") + MEETING = "meeting", _("Meeting") WHATSAPP = "whatsapp", _("WhatsApp") VISIT = "visit", _("Visit") LEAD_NEGOTIATION = "negotiation", _("Negotiation") @@ -1072,6 +1135,12 @@ class Customer(models.Model): ) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) + slug = models.SlugField(max_length=255, unique=True, editable=False, null=True, blank=True) + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(f"{self.first_name} {self.last_name}") + super().save(*args, **kwargs) class Meta: verbose_name = _("Customer") @@ -1348,7 +1417,12 @@ class Lead(models.Model): auto_now_add=True, verbose_name=_("Created"), db_index=True ) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) + slug = models.SlugField(unique=True, blank=True, null=True) + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(f"{self.first_name} {self.last_name}") + super(Lead, self).save(*args, **kwargs) class Meta: verbose_name = _("Lead") verbose_name_plural = _("Leads") @@ -1684,7 +1758,12 @@ class Vendor(models.Model, LocalizedNameMixin): upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo") ) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) + slug = models.SlugField(max_length=255, unique=True, verbose_name=_("Slug"), null=True,blank=True) + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) class Meta: verbose_name = _("Vendor") verbose_name_plural = _("Vendors") diff --git a/inventory/signals.py b/inventory/signals.py index 404c6e06..4a94604b 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -621,7 +621,7 @@ def create_make_ledger_accounts(sender, instance, created, **kwargs): # @receiver(post_save, sender=VendorModel) -# def create_vendor_accounts(sender, instance, created, **kwargs): +# def create_vendor_accounts(sender, instance, created, **kwargs):Dealer) # if created: # entity = instance.entity_model # coa = entity.get_default_coa() diff --git a/inventory/urls.py b/inventory/urls.py index 6f94d6e7..3c1eb498 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -53,7 +53,7 @@ urlpatterns = [ path("submit_plan/", views.submit_plan, name="submit_plan"), path('payment-callback/', views.payment_callback, name='payment_callback'), # - path("dealers//settings/", views.DealerSettingsView, name="dealer_settings"), + path("dealers//settings/", views.DealerSettingsView, name="dealer_settings"), path("dealers/assign-car-makes/", views.assign_car_makes, name="assign_car_makes"), path("dashboards/manager/", views.ManagerDashboard.as_view(), name="manager_dashboard"), path("dashboards/sales/", views.SalesDashboard.as_view(), name="sales_dashboard"), @@ -61,9 +61,9 @@ urlpatterns = [ path('cars/inventory/table/', views.CarListViewTable.as_view(), name="car_table"), path("export/format/", TableExport, name="export"), # Dealer URLs - path("dealers//", views.DealerDetailView.as_view(), name="dealer_detail"), + path("dealers//", views.DealerDetailView.as_view(), name="dealer_detail"), path( - "dealers//update/", + "dealers//update/", views.DealerUpdateView.as_view(), name="dealer_update", ), @@ -76,7 +76,7 @@ urlpatterns = [ # CRM URLs path("customers/", views.CustomerListView.as_view(), name="customer_list"), path( - "customers//", + "customers//", views.CustomerDetailView.as_view(), name="customer_detail", ), @@ -84,18 +84,18 @@ urlpatterns = [ "customers/create/", views.CustomerCreateView.as_view(), name="customer_create" ), path( - "customers//update/", + "customers//update/", views.CustomerUpdateView.as_view(), name="customer_update", ), - path("customers//delete/", views.delete_customer, name="customer_delete"), + path("customers//delete/", views.delete_customer, name="customer_delete"), path( - "customers//opportunities/create/", + "customers//opportunities/create/", views.OpportunityCreateView.as_view(), name="create_opportunity", ), path( - "customers//add-note/", + "customers//add-note/", views.add_note_to_customer, name="add_note_to_customer", ), @@ -105,13 +105,13 @@ urlpatterns = [ path("crm/leads/", views.LeadListView.as_view(), name="lead_list"), path( - "crm/leads//view/", views.LeadDetailView.as_view(), name="lead_detail" + "crm/leads//view/", views.LeadDetailView.as_view(), name="lead_detail" ), path("crm/leads/create/", views.lead_create, name="lead_create"), path( "crm/leads//update/", views.LeadUpdateView.as_view(), name="lead_update" ), - path("crm/leads//delete/", views.LeadDeleteView, name="lead_delete"), + path("crm/leads//delete/", views.LeadDeleteView, name="lead_delete"), path("crm/leads//lead-convert/", views.lead_convert, name="lead_convert"), path("crm/leads//add-note/", views.add_note_to_lead, name="add_note_to_lead"), path('crm/leads//update-note/', views.update_note, name='update_note_to_lead'), @@ -122,27 +122,27 @@ urlpatterns = [ name="update_task", ), path( - "crm///add-task/", + "crm///add-task/", views.add_task, name="add_task", ), path( - "crm///add-activity/", + "crm///add-activity/", views.add_activity, name="add_activity", ), path( - "crm/leads//send_lead_email/", + "crm/leads//send_lead_email/", views.send_lead_email, name="send_lead_email", ), path( - "crm/leads//send_lead_email/", + "crm/leads//send_lead_email/", views.send_lead_email, name="send_lead_email_with_template", ), path( - "crm/leads//schedule/", + "crm/leads//schedule/", views.schedule_lead, name="schedule_lead", ), @@ -229,33 +229,34 @@ urlpatterns = [ path('crm/calender/', views.EmployeeCalendarView.as_view(), name='calendar_list'), # Vendor URLs path("vendors", views.VendorListView.as_view(), name="vendor_list"), - path("vendors//", views.vendorDetailView, name="vendor_detail"), + path("vendors//", views.vendorDetailView, name="vendor_detail"), path("vendors/create/", views.VendorCreateView.as_view(), name="vendor_create"), path( - "vendors//update/", + "vendors//update/", views.VendorUpdateView.as_view(), name="vendor_update", ), path( - "vendors//delete/", + "vendors//delete/", views.delete_vendor, name="vendor_delete", ), # Car URLs + path("cars/add/", views.CarCreateView.as_view(), name="car_add"), path("cars/inventory/", views.CarInventory.as_view(), name="car_inventory_all"), path( - "cars/inventory////", + "cars/inventory////", views.CarInventory.as_view(), name="car_inventory", ), path("cars/inventory/stats", views.inventory_stats_view, name="inventory_stats"), path("cars/inventory/list", views.CarListView.as_view(), name="car_list"), - path("cars//", views.CarDetailView.as_view(), name="car_detail"), - path("cars//history/", views.car_history, name="car_history"), - path("cars//update/", views.CarUpdateView.as_view(), name="car_update"), - path("cars//delete/", views.CarDeleteView.as_view(), name="car_delete"), + path("cars//", views.CarDetailView.as_view(), name="car_detail"), + path("cars//history/", views.car_history, name="car_history"), + path("cars//update/", views.CarUpdateView.as_view(), name="car_update"), + path("cars//delete/", views.CarDeleteView.as_view(), name="car_delete"), path( - "cars//finance/create/", + "cars//finance/create/", views.CarFinanceCreateView.as_view(), name="car_finance_create", ), @@ -264,43 +265,42 @@ urlpatterns = [ views.CarFinanceUpdateView.as_view(), name="car_finance_update", ), - path("cars/add/", views.CarCreateView.as_view(), name="car_add"), path("ajax/", views.AjaxHandlerView.as_view(), name="ajax_handler"), path( - "cars//add-color/", views.CarColorCreate.as_view(), name="add_color" + "cars//add-color/", views.CarColorCreate.as_view(), name="add_color" ), path( - "cars//location/add/", + "cars//location/add/", views.CarLocationCreateView.as_view(), name="add_car_location", ), path( - "cars//location//update", + "cars//location//update", views.CarLocationUpdateView.as_view(), name="update_car_location", ), path( - "cars//location/update/", + "cars//location/update/", views.CarTransferCreateView.as_view(), name="transfer", ), path( - "cars//location/detail/", + "cars//location/detail/", views.CarTransferDetailView.as_view(), name="transfer_detail", ), path( - "cars//location//transfer_approve/", + "cars//location//transfer_approve/", views.car_transfer_approve, name="transfer_confirm", ), path( - "cars//location//transfer_accept_reject/", + "cars//location//transfer_accept_reject/", views.car_transfer_accept_reject, name="transfer_accept_reject", ), path( - "cars//location//preview/", + "cars//location//preview/", views.CarTransferPreviewView, name="transfer_preview", ), @@ -308,7 +308,7 @@ path( views.SearchCodeView.as_view(), name="car_search"), # path('cars//colors//update/',views.CarColorUpdateView.as_view(),name='color_update'), - path("cars/reserve//", + path("cars/reserve//", views.reserve_car_view, name="reserve_car"), path( @@ -317,11 +317,11 @@ path( name="reservations", ), path( - "cars//add-custom-card/", + "cars//add-custom-card/", views.CustomCardCreateView.as_view(), name="add_custom_card", ), - path('cars//add-registration/', + path('cars//add-registration/', views.CarRegistrationCreateView.as_view(), name='add_registration'), diff --git a/inventory/utils.py b/inventory/utils.py index 12354c6e..12b9d12b 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -213,7 +213,7 @@ def reserve_car(car, request): except Exception as e: messages.error(request, f"Error reserving car: {e}") - return redirect("car_detail", pk=car.pk) + return redirect("car_detail", slug=car.slug) def calculate_vat_amount(amount): diff --git a/inventory/views.py b/inventory/views.py index 6bb7de9b..b4db49ba 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -604,7 +604,7 @@ class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): return context -def car_history(request, pk): +def car_history(request, slug): """ Fetch and display the history of activities related to a specific car. @@ -622,7 +622,7 @@ def car_history(request, pk): including the car and its associated activities as context data. :rtype: HttpResponse """ - car = get_object_or_404(models.Car, pk=pk) + car = get_object_or_404(models.Car, slug=slug) activities = models.Activity.objects.filter( content_type__model="car", object_id=car.id ) @@ -859,7 +859,7 @@ class SearchCodeView(LoginRequiredMixin, View): return JsonResponse({ "success": True, "code": code, - "redirect_url": reverse("car_detail", args=[car.pk]) + "redirect_url": reverse("car_detail", args=[car.slug]) }) except Exception as e: @@ -903,16 +903,16 @@ class CarInventory(LoginRequiredMixin, PermissionRequiredMixin, ListView): def get_queryset(self, *args, **kwargs): query = self.request.GET.get("q") - make_id = self.kwargs["make_id"] - model_id = self.kwargs["model_id"] - trim_id = self.kwargs["trim_id"] + make = models.CarMake.objects.get(slug=self.kwargs["make_id"]) + model = models.CarModel.objects.get(slug=self.kwargs["model_id"]) + trim = models.CarTrim.objects.get(slug=self.kwargs["trim_id"]) dealer = get_user_type(self.request) cars = models.Car.objects.filter( dealer=dealer, - id_car_make=make_id, - id_car_model=model_id, - id_car_trim=trim_id, + id_car_make=make, + id_car_model=model, + id_car_trim=trim, ).order_by("receiving_date") return apply_search_filters(cars, query) @@ -951,16 +951,16 @@ class CarColorCreate(LoginRequiredMixin, PermissionRequiredMixin, CreateView): permission_required = ["inventory.view_car"] def form_valid(self, form): - car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) + car = get_object_or_404(models.Car, slug=self.kwargs["slug"]) form.instance.car = car return super().form_valid(form) def get_success_url(self): - return reverse_lazy("car_detail", kwargs={"pk": self.kwargs["car_pk"]}) + return reverse_lazy("car_detail", kwargs={"slug": self.kwargs["slug"]}) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["car"] = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) + context["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"]) return context @@ -1103,6 +1103,7 @@ def inventory_stats_view(request): if make.id_car_make not in inventory: inventory[make.id_car_make] = { "make_id": make.id_car_make, + "slug": make.slug, "make_name": make.get_local_name(), "total_cars": 0, "models": {}, @@ -1113,6 +1114,7 @@ def inventory_stats_view(request): if model and model.id_car_model not in inventory[make.id_car_make]["models"]: inventory[make.id_car_make]["models"][model.id_car_model] = { "model_id": model.id_car_model, + "slug": model.slug, "model_name": model.get_local_name(), "total_cars": 0, "trims": {}, @@ -1132,6 +1134,7 @@ def inventory_stats_view(request): trim.id_car_trim ] = { "trim_id": trim.id_car_trim, + "slug": trim.slug, "trim_name": trim.name, "total_cars": 0, } @@ -1146,11 +1149,13 @@ def inventory_stats_view(request): "makes": [ { "make_id": make_data["make_id"], + "slug": make_data["slug"], "make_name": make_data["make_name"], "total_cars": make_data["total_cars"], "models": [ { "model_id": model_data["model_id"], + "slug": model_data["slug"], "model_name": model_data["model_name"], "total_cars": model_data["total_cars"], "trims": list(model_data["trims"].values()), @@ -1161,7 +1166,7 @@ def inventory_stats_view(request): for make_data in inventory.values() ], } - + print(result['makes']) return render(request, "inventory/inventory_stats.html", {"inventory": result}) @@ -1218,7 +1223,7 @@ class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi permission_required = ["inventory.add_carfinance"] def dispatch(self, request, *args, **kwargs): - self.car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) + self.car = get_object_or_404(models.Car, slug=self.kwargs["slug"]) return super().dispatch(request, *args, **kwargs) def form_valid(self, form): @@ -1227,7 +1232,7 @@ class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi return super().form_valid(form) def get_success_url(self): - return reverse("car_detail", kwargs={"pk": self.car.pk}) + return reverse("car_detail", kwargs={"slug": self.car.slug}) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -1273,7 +1278,7 @@ class CarFinanceUpdateView( permission_required = ["inventory.change_carfinance"] def get_success_url(self): - return reverse("car_detail", kwargs={"pk": self.object.car.pk}) + return reverse("car_detail", kwargs={"slug": self.object.car.slug}) def get_form_kwargs(self): kwargs = super().get_form_kwargs() @@ -1325,7 +1330,7 @@ class CarUpdateView( permission_required = ["inventory.change_car"] def get_success_url(self): - return reverse("car_detail", kwargs={"pk": self.object.pk}) + return reverse("car_detail", kwargs={"slug": self.object.slug}) def get_form(self, form_class=None): form = super().get_form(form_class) @@ -1388,10 +1393,10 @@ class CarLocationCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateV permission_required = ["inventory.add_carlocation"] def get_success_url(self): - return reverse_lazy("car_detail", kwargs={"pk": self.object.car.pk}) + return reverse_lazy("car_detail", kwargs={"slug": self.object.car.slug}) def form_valid(self, form): - form.instance.car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) + form.instance.car = get_object_or_404(models.Car, slug=self.kwargs["slug"]) dealer = get_user_type(self.request) form.instance.owner = dealer form.save() @@ -1425,11 +1430,11 @@ class CarLocationUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateV # def get_initial(self): # initial = super().get_initial() - # initial["car"] = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) + # initial["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"]) # return initial def form_valid(self, form): - form.instance.car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) + form.instance.car = get_object_or_404(models.Car, slug=self.kwargs["slug"]) dealer = get_user_type(self.request) form.instance.owner = dealer form.save() @@ -1437,7 +1442,7 @@ class CarLocationUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateV return super().form_valid(form) def get_success_url(self): - return reverse_lazy("car_detail", kwargs={"pk": self.object.car.pk}) + return reverse_lazy("car_detail", kwargs={"slug": self.object.car.slug}) class CarTransferCreateView(LoginRequiredMixin, CreateView): @@ -1465,12 +1470,12 @@ class CarTransferCreateView(LoginRequiredMixin, CreateView): form.fields["to_dealer"].queryset = models.Dealer.objects.exclude( pk=get_user_type(self.request).pk ).all() - form.fields["car"].queryset = models.Car.objects.filter(pk=self.kwargs["pk"]) + form.fields["car"].queryset = models.Car.objects.filter(slug=self.kwargs["slug"]) return form def get_initial(self): initial = super().get_initial() - initial["car"] = get_object_or_404(models.Car, pk=self.kwargs["pk"]) + initial["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"]) return initial def form_valid(self, form): @@ -1480,7 +1485,7 @@ class CarTransferCreateView(LoginRequiredMixin, CreateView): return super().form_valid(form) def get_success_url(self): - return reverse_lazy("car_detail", kwargs={"pk": self.object.car.pk}) + return reverse_lazy("car_detail", kwargs={"slug": self.object.car.slug}) @@ -1516,7 +1521,7 @@ class CarTransferDetailView(LoginRequiredMixin, SuccessMessageMixin, DetailView) @login_required -def car_transfer_approve(request, car_pk, transfer_pk): +def car_transfer_approve(request, slug, transfer_pk): """ Approves or cancels a car transfer request based on the action parameter. This view handles the workflow of updating the transfer status and notifying the involved parties @@ -1529,7 +1534,7 @@ def car_transfer_approve(request, car_pk, transfer_pk): :param transfer_pk: Primary key of the transfer request to approve or cancel. :return: An HTTP response redirecting to the car detail page of the specified car. """ - car = get_object_or_404(models.Car, pk=car_pk) + car = get_object_or_404(models.Car, slug=slug) transfer = get_object_or_404(models.CarTransfer, pk=transfer_pk) action = request.GET.get("action") if action == "cancel": @@ -1543,12 +1548,12 @@ def car_transfer_approve(request, car_pk, transfer_pk): user=transfer.from_dealer.user, message=f"Car transfer request from {transfer.to_dealer} is canceled.", ) - return redirect("car_detail", pk=car.pk) + return redirect("car_detail", slug=car.slug) transfer.status = "approved" transfer.save() url = request.build_absolute_uri( reverse( - "transfer_preview", kwargs={"car_pk": car.pk, "transfer_pk": transfer.pk} + "transfer_preview", kwargs={"slug": car.slug, "transfer_pk": transfer.pk} ) ) models.Notification.objects.create( @@ -1556,11 +1561,11 @@ def car_transfer_approve(request, car_pk, transfer_pk): message=f"Car transfer request from {transfer.from_dealer} is waiting for your acceptance. Accept", ) messages.success(request, _("Car transfer approved successfully")) - return redirect("car_detail", pk=car.pk) + return redirect("car_detail", slug=car.slug) @login_required -def car_transfer_accept_reject(request, car_pk, transfer_pk): +def car_transfer_accept_reject(request, slug, transfer_pk): """ Handles the acceptance or rejection of a car transfer request. Based on the `status` parameter obtained from the query string, the function updates the @@ -1574,7 +1579,7 @@ def car_transfer_accept_reject(request, car_pk, transfer_pk): :param transfer_pk: The primary key of the car transfer request to be processed. :return: An HTTP redirect response to the 'inventory_stats' view. """ - car = get_object_or_404(models.Car, pk=car_pk) + car = get_object_or_404(models.Car, slug=slug) transfer = get_object_or_404(models.CarTransfer, pk=transfer_pk) status = request.GET.get("status") if status == "rejected": @@ -1606,7 +1611,7 @@ def car_transfer_accept_reject(request, car_pk, transfer_pk): @login_required -def CarTransferPreviewView(request, car_pk, transfer_pk): +def CarTransferPreviewView(request, slug, transfer_pk): """ Handles the preview of car transfer details and ensures that a user has appropriate permissions to view the transfer based on their associated dealer. @@ -1627,7 +1632,7 @@ def CarTransferPreviewView(request, car_pk, transfer_pk): """ transfer = get_object_or_404(models.CarTransfer, pk=transfer_pk) if transfer.to_dealer != get_user_type(request): - return redirect("car_detail", pk=car_pk) + return redirect("car_detail", slug=slug) return render(request, "inventory/transfer_preview.html", {"transfer": transfer}) @@ -1651,18 +1656,18 @@ class CustomCardCreateView(LoginRequiredMixin, CreateView): template_name = "inventory/add_custom_card.html" def form_valid(self, form): - car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) + car = get_object_or_404(models.Car, slug=self.kwargs["slug"]) form.instance.car = car return super().form_valid(form) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["car"] = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) + context["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"]) return context def get_success_url(self): messages.success(self.request, _("Custom Card added successfully")) - return reverse_lazy("car_detail", kwargs={"pk": self.kwargs["car_pk"]}) + return reverse_lazy("car_detail", kwargs={"slug": self.kwargs["slug"]}) class CarRegistrationCreateView(LoginRequiredMixin, CreateView): @@ -1692,22 +1697,22 @@ class CarRegistrationCreateView(LoginRequiredMixin, CreateView): template_name = "inventory/car_registration_form.html" def form_valid(self, form): - car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) + car = get_object_or_404(models.Car, slug=self.kwargs["slug"]) form.instance.car = car return super().form_valid(form) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["car"] = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) + context["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"]) return context def get_success_url(self): messages.success(self.request, _("Registration added successfully")) - return reverse_lazy("car_detail", kwargs={"pk": self.kwargs["car_pk"]}) + return reverse_lazy("car_detail", kwargs={"slug": self.kwargs["slug"]}) @login_required() -def reserve_car_view(request, car_id): +def reserve_car_view(request, slug): """ Handles car reservation requests. This view requires the user to be logged in and processes only POST requests. When invoked, it checks if the specified car @@ -1723,10 +1728,10 @@ def reserve_car_view(request, car_id): :rtype: HttpResponse or JsonResponse """ if request.method == "POST": - car = get_object_or_404(models.Car, pk=car_id) + car = get_object_or_404(models.Car, slug=slug) if car.is_reserved(): messages.error(request, _("This car is already reserved")) - return redirect("car_detail", pk=car.pk) + return redirect("car_detail", slug=car.slug) response = reserve_car(car, request) return response return JsonResponse( @@ -1764,7 +1769,7 @@ def manage_reservation(request, reservation_id): reservation.reserved_until = timezone.now() + timezone.timedelta(hours=24) reservation.save() messages.success(request, _("Reservation renewed successfully")) - return redirect("car_detail", pk=reservation.car.pk) + return redirect("car_detail", slug=reservation.car.slug) elif action == "cancel": car = reservation.car @@ -1772,7 +1777,7 @@ def manage_reservation(request, reservation_id): car.status = models.CarStatusChoices.AVAILABLE car.save() messages.success(request, _("Reservation canceled successfully")) - return redirect("car_detail", pk=reservation.car.pk) + return redirect("car_detail", slug=reservation.car.slug) else: return JsonResponse( @@ -1857,7 +1862,7 @@ class DealerUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): success_message = _("Dealer updated successfully") def get_success_url(self): - return reverse("dealer_detail", kwargs={"pk": self.object.pk}) + return reverse("dealer_detail", kwargs={"slug": self.object.slug}) class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): @@ -2148,7 +2153,7 @@ class VendorListView(LoginRequiredMixin, ListView): @login_required -def vendorDetailView(request, pk): +def vendorDetailView(request, slug): """ Fetches and renders the detail view for a specific vendor. @@ -2163,7 +2168,7 @@ def vendorDetailView(request, pk): :return: An HttpResponse object containing the rendered vendor detail page. :rtype: HttpResponse """ - vendor = get_object_or_404(models.Vendor, pk=pk) + vendor = get_object_or_404(models.Vendor, slug=slug) return render( request, template_name="vendors/view_vendor.html", context={"vendor": vendor} ) @@ -2258,7 +2263,7 @@ class VendorUpdateView( @login_required -def delete_vendor(request, pk): +def delete_vendor(request, slug): """ Deletes an existing vendor record from the database. @@ -2273,7 +2278,7 @@ def delete_vendor(request, pk): :return: HttpResponseRedirect object for redirecting to the vendor list page. :rtype: HttpResponseRedirect """ - vendor = get_object_or_404(models.Vendor, pk=pk) + vendor = get_object_or_404(models.Vendor, slug=slug) vendor.active = False vendor.vendor_model.active = False vendor.save() @@ -2367,7 +2372,7 @@ class GroupCreateView( def form_valid(self, form): dealer = get_user_type(self.request) instance = form.save(commit=False) - group = Group.objects.create(name=f"{dealer.pk}_{instance.name}") + group = Group.objects.create(name=f"{dealer.slug}_{instance.name}") instance.dealer = dealer instance.group = group instance.save() @@ -2410,7 +2415,7 @@ class GroupUpdateView( dealer = get_user_type(self.request) instance = form.save(commit=False) instance.set_defualt_permissions() - instance.group.name = f"{dealer.pk}_{instance.name}" + instance.group.name = f"{dealer.slug}_{instance.name}" instance.save() return super().form_valid(form) @@ -4812,7 +4817,7 @@ def add_note_to_lead(request, pk): note.created_by = request.user note.save() messages.success(request, _("Note added successfully")) - return redirect("lead_detail", pk=lead.pk) + return redirect("lead_detail", slug=lead.slug) else: form = forms.NoteForm() return render(request, "crm/note_form.html", {"form": form, "lead": lead}) @@ -4865,6 +4870,7 @@ def update_note(request, pk): """ note = get_object_or_404(models.Notes, pk=pk, created_by=request.user) lead_pk = note.content_object.pk + lead = models.Lead.objects.get(pk=lead_pk) if request.method == "POST": form = forms.NoteForm(request.POST, instance=note) @@ -4874,7 +4880,7 @@ def update_note(request, pk): updated_note.created_by = request.user updated_note.save() messages.success(request, _("Note updated successfully")) - return redirect("lead_detail", pk=lead_pk) + return redirect("lead_detail", slug=lead.slug) else: form = forms.NoteForm(instance=note) @@ -5035,7 +5041,7 @@ def lead_transfer(request, pk): @login_required -def send_lead_email(request, pk, email_pk=None): +def send_lead_email(request, slug, email_pk=None): """ Handles sending emails related to a lead. Supports creating drafts, sending emails, and rendering an email composition page. Changes on the lead or email objects, such as marking a lead as contacted @@ -5055,15 +5061,15 @@ def send_lead_email(request, pk, email_pk=None): or email composition rendering, a response object is returned to render the respective page. Type: HttpResponse """ - lead = get_object_or_404(models.Lead, pk=pk) + lead = get_object_or_404(models.Lead, slug=slug) status = request.GET.get("status") dealer = get_user_type(request) if status == 'draft': models.Email.objects.create(content_object=lead, created_by=request.user,from_email="manager@tenhal.com", to_email=request.GET.get("to"), subject=request.GET.get("subject"), message=request.GET.get("message"),status=models.EmailStatus.DRAFT) models.Activity.objects.create(dealer=dealer,content_object=lead, notes="Email Draft",created_by=request.user,activity_type=models.ActionChoices.EMAIL) messages.success(request, _("Email Draft successfully")) - response = HttpResponse(redirect("lead_detail", pk=lead.pk)) - response["HX-Redirect"] = reverse("lead_detail", args=[lead.pk]) + response = HttpResponse(redirect("lead_detail", slug=lead.slug)) + response["HX-Redirect"] = reverse("lead_detail", args=[lead.slug]) return response if request.method == "POST": @@ -7028,7 +7034,7 @@ class CarListViewTable(LoginRequiredMixin, ExportMixin, SingleTableView): @login_required -def DealerSettingsView(request, pk): +def DealerSettingsView(request, slug): """ Handles dealer settings view where dealers can update their financial and payment account settings. This view ensures validation and reassigns form @@ -7045,7 +7051,7 @@ def DealerSettingsView(request, pk): to the dealer detail view after successful form submission. :rtype: HttpResponse """ - dealer_setting = get_object_or_404(models.DealerSettings, dealer__pk=pk) + dealer_setting = get_object_or_404(models.DealerSettings, dealer__slug=slug) dealer = get_user_type(request) if request.method == "POST": form = forms.DealerSettingsForm(request.POST, instance=dealer_setting) @@ -7054,7 +7060,7 @@ def DealerSettingsView(request, pk): instance.dealer = dealer instance.save() messages.success(request, _('Settings updated')) - return redirect('dealer_detail', pk=dealer.pk) + return redirect('dealer_detail', slug=dealer.slug) else: print(form.errors) form = forms.DealerSettingsForm(instance=dealer_setting, initial={"dealer": dealer}) @@ -7134,7 +7140,7 @@ def assign_car_makes(request): makes = form.cleaned_data["car_makes"] create_accounts_for_make(dealer, makes) form.save() - return redirect("dealer_detail", pk=dealer.pk) + return redirect("dealer_detail", slug=dealer.slug) else: print(form.errors) else: @@ -7743,13 +7749,13 @@ def notifications_history(request): # ) # return render(request, 'activity_history.html') -def add_activity(request,content_type,pk): +def add_activity(request,content_type,slug): try: model = apps.get_model(f'inventory.{content_type}') except LookupError: raise Http404("Model not found") - obj = get_object_or_404(model, pk=pk) + obj = get_object_or_404(model, slug=slug) dealer = get_user_type(request) if request.method == "POST": form = forms.ActivityForm(request.POST) @@ -7765,14 +7771,14 @@ def add_activity(request,content_type,pk): messages.success(request, _("Activity added successfully")) else: messages.error(request, _("Activity form is not valid")) - return redirect(f"{content_type}_detail", pk=pk) -def add_task(request,content_type,pk): + return redirect(f"{content_type}_detail", slug=slug) +def add_task(request,content_type,slug): try: model = apps.get_model(f'inventory.{content_type}') except LookupError: raise Http404("Model not found") - obj = get_object_or_404(model, pk=pk) + obj = get_object_or_404(model, slug=slug) dealer = get_user_type(request) if request.method == "POST": form = forms.StaffTaskForm(request.POST) @@ -7788,16 +7794,22 @@ def add_task(request,content_type,pk): else: print(form.errors) messages.error(request, _("Task form is not valid")) - return redirect(f"{content_type}_detail", pk=pk) + return redirect(f"{content_type}_detail", slug=slug) def update_task(request,pk): task = get_object_or_404(models.Tasks, pk=pk) + lead = get_object_or_404(models.Lead, pk=task.content_object.id) + if request.method == "POST": task.completed = False if task.completed else True task.save() messages.success(request, _("Task updated successfully")) else: messages.error(request, _("Task form is not valid")) - response = HttpResponse() - response['HX-Refresh'] = 'true' - return response \ No newline at end of file + # response = HttpResponse() + # response['HX-Refresh'] = 'true' + # return response + tasks = models.Tasks.objects.filter( + content_type__model="lead", object_id=lead.id + ) + return render(request,'crm/leads/lead_detail.html',{'lead':lead,'tasks':tasks}) \ No newline at end of file diff --git a/load_initial_data.sh b/load_initial_data.sh index 295919ca..1f8885f5 100755 --- a/load_initial_data.sh +++ b/load_initial_data.sh @@ -2,22 +2,22 @@ echo "Loading initial data" echo "Loading carmake" -python3 manage.py loaddata --app carmake carmake_backup.json +python3 manage.py loaddata --app carmake carmake_backup_output.json echo "Loading carmodel" -python3 manage.py loaddata --app carmodel carmodel_backup.json +python3 manage.py loaddata --app carmodel carmodel_backup_output.json echo "Loading carserie" -python3 manage.py loaddata --app carserie carserie_backup.json +python3 manage.py loaddata --app carserie carserie_backup_output.json echo "Loading cartrim" -python3 manage.py loaddata --app cartrim cartrim_backup.json +python3 manage.py loaddata --app cartrim cartrim_backup_output.json echo "Loading caroption" -python3 manage.py loaddata --app caroption caroption_backup.json +python3 manage.py loaddata --app caroption caroption_backup_output.json echo "Loading carequipment" -python3 manage.py loaddata --app carequipment carequipment_backup.json +python3 manage.py loaddata --app carequipment carequipment_backup_output.json echo "Populating colors" python3 manage.py populate_colors @@ -26,4 +26,6 @@ python3 manage.py tenhal_plan python3 manage.py set_vat +python3 manage.py initial_services_offered + echo "Done" \ No newline at end of file diff --git a/slug_data.py b/slug_data.py new file mode 100644 index 00000000..5a3472a7 --- /dev/null +++ b/slug_data.py @@ -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) \ No newline at end of file diff --git a/static/images/customers/image_UGmtPMg.png b/static/images/customers/image_UGmtPMg.png new file mode 100644 index 00000000..fb612b2b Binary files /dev/null and b/static/images/customers/image_UGmtPMg.png differ diff --git a/static/images/logos/vendors/logo-for-the-word-daju-48a980_Bw4t8ED.jpg b/static/images/logos/vendors/logo-for-the-word-daju-48a980_Bw4t8ED.jpg new file mode 100644 index 00000000..f2cfb1d9 Binary files /dev/null and b/static/images/logos/vendors/logo-for-the-word-daju-48a980_Bw4t8ED.jpg differ diff --git a/static/images/logos/vendors/logo-for-the-word-daju-48a980_CfNtYr7.jpg b/static/images/logos/vendors/logo-for-the-word-daju-48a980_CfNtYr7.jpg new file mode 100644 index 00000000..f2cfb1d9 Binary files /dev/null and b/static/images/logos/vendors/logo-for-the-word-daju-48a980_CfNtYr7.jpg differ diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index 7a939487..4ec4d3e0 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -50,3 +50,15 @@ @keyframes spin { to { transform: rotate(360deg); } } + +.form-select select { + padding: 0 16px 0 48px; + right: auto; + left: 0; + direction: rtl; +} + +.form-select:after { + left: 16px; + right: auto; +} diff --git a/staticfiles/css/theme-rtl.css b/staticfiles/css/theme-rtl.css index 3d544a6f..1ce0ad73 100644 --- a/staticfiles/css/theme-rtl.css +++ b/staticfiles/css/theme-rtl.css @@ -3780,8 +3780,9 @@ textarea.form-control-lg { .form-select { --phoenix-form-select-bg-img: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUwIiBoZWlnaHQ9IjE1MCIgdmlld0JveD0iMCAwIDE1MCAxNTAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik03NS4zNDggMTI3LjE5MkM3Mi40MzgxIDEyNy4xOTIgNjkuODUxNCAxMjYuMjIyIDY3LjkxMTUgMTI0LjI4Mkw1LjgzMjE1IDYyLjIwMjNDMS42Mjg4NyA1OC4zMjIzIDEuNjI4ODcgNTEuNTMyNCA1LjgzMjE1IDQ3LjY1MjVDOS43MTIxMSA0My40NDkyIDE2LjUwMiA0My40NDkyIDIwLjM4MiA0Ny42NTI1TDc1LjM0OCAxMDIuMjk1TDEyOS45OTEgNDcuNjUyNUMxMzMuODcxIDQzLjQ0OTIgMTQwLjY2MSA0My40NDkyIDE0NC41NDEgNDcuNjUyNUMxNDguNzQ0IDUxLjUzMjQgMTQ4Ljc0NCA1OC4zMjIzIDE0NC41NDEgNjIuMjAyM0w4Mi40NjEzIDEyNC4yODJDODAuNTIxMyAxMjYuMjIyIDc3LjkzNDcgMTI3LjE5MiA3NS4zNDggMTI3LjE5MloiIGZpbGw9IiMzMTM3NEEiLz4KPC9zdmc+Cg=="); + align-content: start; display: block; - text-align: start; + text-align: right; width: 100%; padding: 0.5rem 1rem 0.5rem 2.5rem; font-size: 0.8rem; @@ -3808,9 +3809,12 @@ textarea.form-control-lg { } @media (prefers-reduced-motion: reduce) { .form-select { - -webkit-transition: none; + -webkit-transition:right; -o-transition: none; transition: none; + right: auto; + left: 0; + direction: rtl; } } .form-select:focus { diff --git a/staticfiles/css/theme.css b/staticfiles/css/theme.css index 05ea19bc..ca4d4ebb 100644 --- a/staticfiles/css/theme.css +++ b/staticfiles/css/theme.css @@ -1138,6 +1138,9 @@ select { select { word-wrap: normal; + padding: 0 16px 0 48px; + right: auto; + left: 0; } select:disabled { opacity: 1; @@ -3784,8 +3787,9 @@ textarea.form-control-lg { .form-select { --phoenix-form-select-bg-img: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUwIiBoZWlnaHQ9IjE1MCIgdmlld0JveD0iMCAwIDE1MCAxNTAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik03NS4zNDggMTI3LjE5MkM3Mi40MzgxIDEyNy4xOTIgNjkuODUxNCAxMjYuMjIyIDY3LjkxMTUgMTI0LjI4Mkw1LjgzMjE1IDYyLjIwMjNDMS42Mjg4NyA1OC4zMjIzIDEuNjI4ODcgNTEuNTMyNCA1LjgzMjE1IDQ3LjY1MjVDOS43MTIxMSA0My40NDkyIDE2LjUwMiA0My40NDkyIDIwLjM4MiA0Ny42NTI1TDc1LjM0OCAxMDIuMjk1TDEyOS45OTEgNDcuNjUyNUMxMzMuODcxIDQzLjQ0OTIgMTQwLjY2MSA0My40NDkyIDE0NC41NDEgNDcuNjUyNUMxNDguNzQ0IDUxLjUzMjQgMTQ4Ljc0NCA1OC4zMjIzIDE0NC41NDEgNjIuMjAyM0w4Mi40NjEzIDEyNC4yODJDODAuNTIxMyAxMjYuMjIyIDc3LjkzNDcgMTI3LjE5MiA3NS4zNDggMTI3LjE5MloiIGZpbGw9IiMzMTM3NEEiLz4KPC9zdmc+Cg=="); display: block; + align-content: start; width: 100%; - text-align: start; + text-align: right; padding: 0.5rem 2.5rem 0.5rem 1rem; font-size: 0.8rem; font-weight: 600; diff --git a/staticfiles/images/logos/vendors/logo-for-the-word-daju-48a980_Bw4t8ED.jpg b/staticfiles/images/logos/vendors/logo-for-the-word-daju-48a980_Bw4t8ED.jpg new file mode 100644 index 00000000..f2cfb1d9 Binary files /dev/null and b/staticfiles/images/logos/vendors/logo-for-the-word-daju-48a980_Bw4t8ED.jpg differ diff --git a/templates/components/activity_modal.html b/templates/components/activity_modal.html index fe9fa5f0..4e9c15cb 100644 --- a/templates/components/activity_modal.html +++ b/templates/components/activity_modal.html @@ -10,7 +10,7 @@ - {% include "components/activity_modal.html" with content_type="lead" pk=lead.pk %} + {% include "components/activity_modal.html" with content_type="lead" slug=lead.slug %} @@ -159,7 +159,7 @@
-
{{lead.full_name}} +
{{lead.full_name}}

{% if lead.status == "new" %} @@ -187,11 +187,11 @@

-

-
+
diff --git a/templates/crm/leads/lead_send.html b/templates/crm/leads/lead_send.html index 3f5bef33..4cddacd6 100644 --- a/templates/crm/leads/lead_send.html +++ b/templates/crm/leads/lead_send.html @@ -8,7 +8,7 @@ {% endif %} -
{{ customer.created|date }} {% if perms.django_ledger.change_customermodel %} - + {% endif %} {% if perms.django_ledger.delete_customermodel %}
{% trans 'Location'|capfirst %} {% 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 %} - + {% trans "transfer"|capfirst %} {% else %} {% trans "No location available." %} - + {% trans "Add" %} {% endif %} @@ -176,8 +175,8 @@
{% if not car.get_transfer %} {% if perms.inventory.change_car %} - {% trans "Edit" %} - + {% trans "Edit" %} + {% trans "Sell to another dealer"|capfirst %} {% endif %} @@ -242,7 +241,7 @@ {% else %}

{% trans "No finance details available." %}

{% if perms.inventory.add_carfinance %} - + {% trans "Add" %} {% endif %} @@ -288,7 +287,7 @@
{% if perms.inventory.change_carcolors %} - + {% trans "Add" %} {% endif %} @@ -454,9 +453,7 @@ - - - + {% csrf_token %}
@@ -493,8 +490,6 @@