update on the slug for multiple models
@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
echo "Delete Old Migrations"
|
||||
find ./inventory -type f -iname "000*.py" -delete
|
||||
find ./inventory -type f -iname "00*.py" -delete
|
||||
|
||||
echo "Delete Old Cache"
|
||||
find ./car_inventory -type d -iname "__pycache__"|xargs rm -rf
|
||||
|
||||
120
inventory/management/commands/generate_slugs.py
Normal 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}')
|
||||
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-05 16:32
|
||||
# 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
|
||||
@ -25,6 +26,27 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Car',
|
||||
fields=[
|
||||
('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')),
|
||||
('stock_type', models.CharField(choices=[('new', 'New'), ('used', 'Used')], default='new', max_length=10, verbose_name='Stock Type')),
|
||||
('remarks', models.TextField(blank=True, null=True, verbose_name='Remarks')),
|
||||
('mileage', models.IntegerField(blank=True, null=True, verbose_name='Mileage')),
|
||||
('receiving_date', models.DateTimeField(verbose_name='Receiving Date')),
|
||||
('hash', models.CharField(blank=True, max_length=64, null=True, verbose_name='Hash')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Car',
|
||||
'verbose_name_plural': 'Cars',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CarEquipment',
|
||||
fields=[
|
||||
@ -32,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',
|
||||
@ -43,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)),
|
||||
@ -122,26 +146,6 @@ class Migration(migrations.Migration):
|
||||
},
|
||||
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Car',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('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')),
|
||||
('stock_type', models.CharField(choices=[('new', 'New'), ('used', 'Used')], default='new', max_length=10, verbose_name='Stock Type')),
|
||||
('remarks', models.TextField(blank=True, null=True, verbose_name='Remarks')),
|
||||
('mileage', models.IntegerField(blank=True, null=True, verbose_name='Mileage')),
|
||||
('receiving_date', models.DateTimeField(verbose_name='Receiving Date')),
|
||||
('hash', models.CharField(blank=True, max_length=64, null=True, verbose_name='Hash')),
|
||||
('vendor', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='cars', to='django_ledger.vendormodel', verbose_name='Vendor')),
|
||||
('id_car_make', models.ForeignKey(blank=True, db_column='id_car_make', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake', verbose_name='Make')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Car',
|
||||
'verbose_name_plural': 'Cars',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CarFinance',
|
||||
fields=[
|
||||
@ -157,12 +161,18 @@ class Migration(migrations.Migration):
|
||||
'verbose_name_plural': 'Car Financial Details',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='car',
|
||||
name='id_car_make',
|
||||
field=models.ForeignKey(blank=True, db_column='id_car_make', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake', verbose_name='Make'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CarModel',
|
||||
fields=[
|
||||
('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={
|
||||
@ -181,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={
|
||||
@ -227,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={
|
||||
@ -245,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={
|
||||
@ -260,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={
|
||||
@ -343,16 +357,18 @@ class Migration(migrations.Migration):
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(choices=[('mr', 'Mr'), ('mrs', 'Mrs'), ('ms', 'Ms'), ('miss', 'Miss'), ('dr', 'Dr'), ('prof', 'Prof'), ('prince', 'Prince'), ('princess', 'Princess'), ('company', 'Company'), ('na', 'N/A')], default='na', max_length=10, verbose_name='Title')),
|
||||
('first_name', models.CharField(max_length=50, verbose_name='First Name')),
|
||||
('middle_name', models.CharField(blank=True, max_length=50, null=True, verbose_name='Middle Name')),
|
||||
('last_name', models.CharField(max_length=50, verbose_name='Last Name')),
|
||||
('gender', models.CharField(choices=[('m', 'Male'), ('f', 'Female')], max_length=1, verbose_name='Gender')),
|
||||
('dob', models.DateField(verbose_name='Date of Birth')),
|
||||
('dob', models.DateField(blank=True, null=True, verbose_name='Date of Birth')),
|
||||
('email', models.EmailField(max_length=254, unique=True, verbose_name='Email')),
|
||||
('national_id', models.CharField(max_length=10, unique=True, verbose_name='National ID')),
|
||||
('national_id', models.CharField(blank=True, max_length=10, null=True, unique=True, verbose_name='National ID')),
|
||||
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', unique=True, verbose_name='Phone Number')),
|
||||
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')),
|
||||
('active', models.BooleanField(default=True, verbose_name='Active')),
|
||||
('image', models.ImageField(blank=True, null=True, upload_to='customers/', verbose_name='Image')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
||||
('customer_model', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_ledger.customermodel')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='customer_profile', to=settings.AUTH_USER_MODEL)),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='customers', to='inventory.dealer')),
|
||||
],
|
||||
@ -413,7 +429,7 @@ class Migration(migrations.Migration):
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('object_id', models.PositiveIntegerField()),
|
||||
('activity_type', models.CharField(choices=[('call', 'Call'), ('sms', 'SMS'), ('email', 'Email'), ('whatsapp', 'WhatsApp'), ('visit', 'Visit'), ('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')),
|
||||
('activity_type', models.CharField(choices=[('call', 'Call'), ('sms', 'SMS'), ('email', 'Email'), ('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')),
|
||||
('notes', models.TextField(blank=True, null=True, verbose_name='Notes')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
||||
@ -460,35 +476,6 @@ class Migration(migrations.Migration):
|
||||
'verbose_name_plural': 'Emails',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Lead',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('first_name', models.CharField(max_length=50, verbose_name='First Name')),
|
||||
('last_name', models.CharField(max_length=50, verbose_name='Last Name')),
|
||||
('email', models.EmailField(max_length=254, verbose_name='Email')),
|
||||
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
|
||||
('lead_type', models.CharField(choices=[('customer', 'Customer'), ('organization', 'Organization')], default='customer', max_length=50, verbose_name='Lead Type')),
|
||||
('year', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Year')),
|
||||
('source', models.CharField(choices=[('referrals', 'Referrals'), ('whatsapp', 'WhatsApp'), ('showroom', 'Showroom'), ('tiktok', 'TikTok'), ('instagram', 'Instagram'), ('x', 'X'), ('facebook', 'Facebook'), ('motory', 'Motory'), ('influencers', 'Influencers'), ('youtube', 'Youtube'), ('campaign', 'Campaign')], max_length=50, verbose_name='Source')),
|
||||
('channel', models.CharField(choices=[('walk_in', 'Walk In'), ('toll_free', 'Toll Free'), ('website', 'Website'), ('email', 'Email'), ('form', 'Form')], max_length=50, verbose_name='Channel')),
|
||||
('crn', models.CharField(blank=True, max_length=10, null=True, unique=True, verbose_name='Commercial Registration Number')),
|
||||
('vrn', models.CharField(blank=True, max_length=15, null=True, unique=True, verbose_name='VAT Registration Number')),
|
||||
('address', models.CharField(max_length=50, verbose_name='address')),
|
||||
('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')], default='medium', max_length=10, verbose_name='Priority')),
|
||||
('status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('contacted', 'Contacted'), ('converted', 'Converted'), ('canceled', 'Canceled')], db_index=True, default='new', max_length=50, verbose_name='Status')),
|
||||
('created', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Created')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
||||
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='leads', to='django_ledger.customermodel')),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leads', to='inventory.dealer')),
|
||||
('id_car_make', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake', verbose_name='Make')),
|
||||
('id_car_model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel', verbose_name='Model')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Lead',
|
||||
'verbose_name_plural': 'Leads',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Notes',
|
||||
fields=[
|
||||
@ -528,12 +515,16 @@ class Migration(migrations.Migration):
|
||||
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
|
||||
('crn', models.CharField(max_length=15, verbose_name='Commercial Registration Number')),
|
||||
('vrn', models.CharField(max_length=15, verbose_name='VAT Registration Number')),
|
||||
('email', models.EmailField(max_length=254, verbose_name='Email')),
|
||||
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
|
||||
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')),
|
||||
('logo', models.ImageField(blank=True, null=True, upload_to='logos', verbose_name='Logo')),
|
||||
('active', models.BooleanField(default=True, verbose_name='Active')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
||||
('customer_model', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_ledger.customermodel')),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='organizations', to='inventory.dealer')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='organization_profile', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Organization',
|
||||
@ -541,6 +532,40 @@ class Migration(migrations.Migration):
|
||||
},
|
||||
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Lead',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('first_name', models.CharField(max_length=50, verbose_name='First Name')),
|
||||
('last_name', models.CharField(max_length=50, verbose_name='Last Name')),
|
||||
('email', models.EmailField(max_length=254, verbose_name='Email')),
|
||||
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
|
||||
('lead_type', models.CharField(choices=[('customer', 'Customer'), ('organization', 'Organization')], default='customer', max_length=50, verbose_name='Lead Type')),
|
||||
('year', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Year')),
|
||||
('source', models.CharField(choices=[('referrals', 'Referrals'), ('whatsapp', 'WhatsApp'), ('showroom', 'Showroom'), ('tiktok', 'TikTok'), ('instagram', 'Instagram'), ('x', 'X'), ('facebook', 'Facebook'), ('motory', 'Motory'), ('influencers', 'Influencers'), ('youtube', 'Youtube'), ('campaign', 'Campaign')], max_length=50, verbose_name='Source')),
|
||||
('channel', models.CharField(choices=[('walk_in', 'Walk In'), ('toll_free', 'Toll Free'), ('website', 'Website'), ('email', 'Email'), ('form', 'Form')], max_length=50, verbose_name='Channel')),
|
||||
('crn', models.CharField(blank=True, max_length=10, null=True, unique=True, verbose_name='Commercial Registration Number')),
|
||||
('vrn', models.CharField(blank=True, max_length=15, null=True, unique=True, verbose_name='VAT Registration Number')),
|
||||
('address', models.CharField(max_length=50, verbose_name='address')),
|
||||
('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')], default='medium', max_length=10, verbose_name='Priority')),
|
||||
('status', models.CharField(choices=[('new', 'New'), ('follow_up', 'Follow-up'), ('negotiation', 'Negotiation'), ('won', 'Won'), ('lost', 'Lost'), ('closed', 'Closed')], db_index=True, default='new', max_length=50, verbose_name='Status')),
|
||||
('next_action', models.CharField(blank=True, max_length=255, null=True, verbose_name='Next Action')),
|
||||
('next_action_date', models.DateTimeField(blank=True, null=True, verbose_name='Next Action Date')),
|
||||
('is_converted', models.BooleanField(default=False)),
|
||||
('converted_at', models.DateTimeField(blank=True, null=True)),
|
||||
('created', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Created')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
||||
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='customer_leads', to='inventory.customer')),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leads', to='inventory.dealer')),
|
||||
('id_car_make', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake', verbose_name='Make')),
|
||||
('id_car_model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel', verbose_name='Model')),
|
||||
('organization', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='organization_leads', to='inventory.organization')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Lead',
|
||||
'verbose_name_plural': 'Leads',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Refund',
|
||||
fields=[
|
||||
@ -636,15 +661,14 @@ class Migration(migrations.Migration):
|
||||
name='Opportunity',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('stage', models.CharField(choices=[('prospect', 'Prospect'), ('proposal', 'Proposal'), ('negotiation', 'Negotiation'), ('closed_won', 'Closed Won'), ('closed_lost', 'Closed Lost')], max_length=20, verbose_name='Stage')),
|
||||
('status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('contacted', 'Contacted'), ('converted', 'Converted'), ('canceled', 'Canceled')], default='new', max_length=20, verbose_name='Status')),
|
||||
('stage', models.CharField(choices=[('discovery', 'Discovery'), ('proposal', 'Proposal'), ('negotiation', 'Negotiation'), ('closed_won', 'Closed Won'), ('closed_lost', 'Closed Lost')], max_length=20, verbose_name='Stage')),
|
||||
('probability', models.PositiveIntegerField(validators=[inventory.models.validate_probability])),
|
||||
('expected_revenue', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Expected Revenue')),
|
||||
('closing_date', models.DateField(blank=True, null=True, verbose_name='Closing Date')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
||||
('closed', models.BooleanField(default=False, verbose_name='Closed')),
|
||||
('car', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.car', verbose_name='Car')),
|
||||
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='django_ledger.customermodel')),
|
||||
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='django_ledger.customermodel')),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='inventory.dealer')),
|
||||
('estimate', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='opportunity', to='django_ledger.estimatemodel')),
|
||||
('lead', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='opportunity', to='inventory.lead')),
|
||||
@ -659,8 +683,8 @@ class Migration(migrations.Migration):
|
||||
name='LeadStatusHistory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('old_status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('contacted', 'Contacted'), ('converted', 'Converted'), ('canceled', 'Canceled')], max_length=50, verbose_name='Old Status')),
|
||||
('new_status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('contacted', 'Contacted'), ('converted', 'Converted'), ('canceled', 'Canceled')], max_length=50, verbose_name='New Status')),
|
||||
('old_status', models.CharField(choices=[('new', 'New'), ('follow_up', 'Follow-up'), ('negotiation', 'Negotiation'), ('won', 'Won'), ('lost', 'Lost'), ('closed', 'Closed')], max_length=50, verbose_name='Old Status')),
|
||||
('new_status', models.CharField(choices=[('new', 'New'), ('follow_up', 'Follow-up'), ('negotiation', 'Negotiation'), ('won', 'Won'), ('lost', 'Lost'), ('closed', 'Closed')], max_length=50, verbose_name='New Status')),
|
||||
('changed_at', models.DateTimeField(auto_now_add=True, verbose_name='Changed At')),
|
||||
('lead', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='status_history', to='inventory.lead')),
|
||||
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='status_changes', to='inventory.staff')),
|
||||
@ -675,6 +699,27 @@ class Migration(migrations.Migration):
|
||||
name='staff',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned', to='inventory.staff', verbose_name='Assigned'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Tasks',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('object_id', models.UUIDField()),
|
||||
('title', models.CharField(max_length=255, verbose_name='Title')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
|
||||
('due_date', models.DateField(verbose_name='Due Date')),
|
||||
('completed', models.BooleanField(default=False, verbose_name='Completed')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
||||
('assigned_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='tasks_assigned', to=settings.AUTH_USER_MODEL)),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='tasks_created', to=settings.AUTH_USER_MODEL)),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='inventory.dealer')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Task',
|
||||
'verbose_name_plural': 'Tasks',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserActivityLog',
|
||||
fields=[
|
||||
@ -704,7 +749,7 @@ class Migration(migrations.Migration):
|
||||
('logo', models.ImageField(blank=True, null=True, upload_to='logos/vendors', verbose_name='Logo')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vendors', to='inventory.dealer')),
|
||||
('vendor_model', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='django_ledger.vendormodel', verbose_name='Vendor Model')),
|
||||
('vendor_model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='django_ledger.vendormodel', verbose_name='Vendor Model')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Vendor',
|
||||
@ -712,6 +757,11 @@ class Migration(migrations.Migration):
|
||||
},
|
||||
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='car',
|
||||
name='vendor',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='cars', to='inventory.vendor', verbose_name='Vendor'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CarReservation',
|
||||
fields=[
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-06 12:57
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('django_ledger', '0021_alter_bankaccountmodel_account_model_and_more'),
|
||||
('inventory', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='vendor',
|
||||
name='vendor_model',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='django_ledger.vendormodel', verbose_name='Vendor Model'),
|
||||
),
|
||||
]
|
||||
18
inventory/migrations/0002_dealer_slug.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-06 14:31
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0002_alter_vendor_vendor_model'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='car',
|
||||
name='vendor',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='cars', to='inventory.vendor', verbose_name='Vendor'),
|
||||
),
|
||||
]
|
||||
18
inventory/migrations/0003_customer_slug.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-06 14:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0003_alter_car_vendor'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='customers/', verbose_name='Image'),
|
||||
),
|
||||
]
|
||||
18
inventory/migrations/0004_vendor_slug.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@ -1,20 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-06 14:54
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('django_ledger', '0021_alter_bankaccountmodel_account_model_and_more'),
|
||||
('inventory', '0004_customer_image'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='customer_model',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_ledger.customermodel'),
|
||||
),
|
||||
]
|
||||
18
inventory/migrations/0005_lead_slug.py
Normal 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),
|
||||
),
|
||||
]
|
||||
18
inventory/migrations/0006_alter_activity_activity_type.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@ -1,22 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-06 15:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0005_customer_customer_model'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='customer',
|
||||
name='middle_name',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='active',
|
||||
field=models.BooleanField(default=True, verbose_name='Active'),
|
||||
),
|
||||
]
|
||||
@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-06 16:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0006_remove_customer_middle_name_customer_active'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='customer_type',
|
||||
field=models.CharField(blank=True, choices=[('customer', 'Customer'), ('organization', 'Organization')], default='customer', max_length=15, null=True, verbose_name='Customer Type'),
|
||||
),
|
||||
]
|
||||
@ -1,32 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-07 09:33
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('django_ledger', '0021_alter_bankaccountmodel_account_model_and_more'),
|
||||
('inventory', '0007_customer_customer_type'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='customer',
|
||||
name='customer_type',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='customer_model',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_ledger.customermodel'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='user',
|
||||
field=models.OneToOneField(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='organization_profile', to=settings.AUTH_USER_MODEL),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-07 09:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0008_remove_customer_customer_type_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='email',
|
||||
field=models.EmailField(default='t@tenhal.sa', max_length=254, verbose_name='Email'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-07 09:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0009_organization_email'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='active',
|
||||
field=models.BooleanField(default=True, verbose_name='Active'),
|
||||
),
|
||||
]
|
||||
@ -1,24 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-07 14:32
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0010_organization_active'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='lead',
|
||||
name='organization',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='organization_leads', to='inventory.organization'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='lead',
|
||||
name='customer',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='customer_leads', to='inventory.customer'),
|
||||
),
|
||||
]
|
||||
@ -1,23 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-08 10:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0011_lead_organization_alter_lead_customer'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='customer',
|
||||
name='dob',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='Date of Birth'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customer',
|
||||
name='national_id',
|
||||
field=models.CharField(blank=True, max_length=10, null=True, unique=True, verbose_name='National ID'),
|
||||
),
|
||||
]
|
||||
@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-11 14:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0012_alter_customer_dob_alter_customer_national_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='lead',
|
||||
name='converted_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@ -1,50 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-11 14:26
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0013_lead_converted_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='lead',
|
||||
name='is_converted',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='lead',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('new', 'New'), ('follow_up', 'Needs Follow-up'), ('negotiation', 'Under Negotiation'), ('won', 'Converted'), ('lost', 'Lost'), ('closed', 'Closed')], db_index=True, default='new', max_length=50, verbose_name='Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='leadstatushistory',
|
||||
name='new_status',
|
||||
field=models.CharField(choices=[('new', 'New'), ('follow_up', 'Needs Follow-up'), ('negotiation', 'Under Negotiation'), ('won', 'Converted'), ('lost', 'Lost'), ('closed', 'Closed')], max_length=50, verbose_name='New Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='leadstatushistory',
|
||||
name='old_status',
|
||||
field=models.CharField(choices=[('new', 'New'), ('follow_up', 'Needs Follow-up'), ('negotiation', 'Under Negotiation'), ('won', 'Converted'), ('lost', 'Lost'), ('closed', 'Closed')], max_length=50, verbose_name='Old Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='opportunity',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('new', 'New'), ('follow_up', 'Needs Follow-up'), ('negotiation', 'Under Negotiation'), ('won', 'Converted'), ('lost', 'Lost'), ('closed', 'Closed')], default='new', max_length=20, verbose_name='Status'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LeadActivity',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('action', models.CharField(max_length=255)),
|
||||
('notes', models.TextField(blank=True)),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.staff')),
|
||||
('lead', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activities', to='inventory.lead')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -1,23 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-11 14:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0014_lead_is_converted_alter_lead_status_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='lead',
|
||||
name='next_action',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Next Action'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='lead',
|
||||
name='next_action_date',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='Next Action Date'),
|
||||
),
|
||||
]
|
||||
@ -1,21 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-11 15:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0015_lead_next_action_lead_next_action_date'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='activity',
|
||||
name='activity_type',
|
||||
field=models.CharField(choices=[('call', 'Call'), ('sms', 'SMS'), ('email', 'Email'), ('whatsapp', 'WhatsApp'), ('visit', 'Visit'), ('negotiation', 'Negotiation'), ('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'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='LeadActivity',
|
||||
),
|
||||
]
|
||||
@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-11 15:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0016_alter_activity_activity_type_delete_leadactivity'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='activity',
|
||||
name='activity_type',
|
||||
field=models.CharField(choices=[('call', 'Call'), ('sms', 'SMS'), ('email', 'Email'), ('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'),
|
||||
),
|
||||
]
|
||||
@ -1,44 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-13 15:19
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('inventory', '0017_alter_activity_activity_type'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='opportunity',
|
||||
name='closed',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='opportunity',
|
||||
name='status',
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Tasks',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('object_id', models.UUIDField()),
|
||||
('title', models.CharField(max_length=255, verbose_name='Title')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
|
||||
('due_date', models.DateField(verbose_name='Due Date')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
||||
('assigned_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='tasks_assigned', to=settings.AUTH_USER_MODEL)),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='tasks_created', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Task',
|
||||
'verbose_name_plural': 'Tasks',
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -1,20 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-13 16:22
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0018_remove_opportunity_closed_remove_opportunity_status_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='tasks',
|
||||
name='dealer',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='inventory.dealer'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-13 16:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0019_tasks_dealer'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='tasks',
|
||||
name='completed',
|
||||
field=models.BooleanField(default=False, verbose_name='Completed'),
|
||||
),
|
||||
]
|
||||
@ -1,28 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-14 10:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0020_tasks_completed'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='lead',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('new', 'New'), ('follow_up', 'Follow-up'), ('negotiation', 'Negotiation'), ('won', 'Won'), ('lost', 'Lost'), ('closed', 'Closed')], db_index=True, default='new', max_length=50, verbose_name='Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='leadstatushistory',
|
||||
name='new_status',
|
||||
field=models.CharField(choices=[('new', 'New'), ('follow_up', 'Follow-up'), ('negotiation', 'Negotiation'), ('won', 'Won'), ('lost', 'Lost'), ('closed', 'Closed')], max_length=50, verbose_name='New Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='leadstatushistory',
|
||||
name='old_status',
|
||||
field=models.CharField(choices=[('new', 'New'), ('follow_up', 'Follow-up'), ('negotiation', 'Negotiation'), ('won', 'Won'), ('lost', 'Lost'), ('closed', 'Closed')], max_length=50, verbose_name='Old Status'),
|
||||
),
|
||||
]
|
||||
@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-14 10:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0021_alter_lead_status_alter_leadstatushistory_new_status_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='expected_revenue',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Expected Revenue'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-14 10:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0022_opportunity_expected_revenue'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='opportunity',
|
||||
name='stage',
|
||||
field=models.CharField(choices=[('discovery', 'Discovery'), ('proposal', 'Proposal'), ('negotiation', 'Negotiation'), ('closed_won', 'Closed Won'), ('closed_lost', 'Closed Lost')], max_length=20, verbose_name='Stage'),
|
||||
),
|
||||
]
|
||||
@ -1,20 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-05-14 10:59
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('django_ledger', '0021_alter_bankaccountmodel_account_model_and_more'),
|
||||
('inventory', '0023_alter_opportunity_stage'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='opportunity',
|
||||
name='customer',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='django_ledger.customermodel'),
|
||||
),
|
||||
]
|
||||
@ -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")
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -53,7 +53,7 @@ urlpatterns = [
|
||||
path("submit_plan/", views.submit_plan, name="submit_plan"),
|
||||
path('payment-callback/', views.payment_callback, name='payment_callback'),
|
||||
#
|
||||
path("dealers/<int:pk>/settings/", views.DealerSettingsView, name="dealer_settings"),
|
||||
path("dealers/<slug:slug>/settings/", views.DealerSettingsView, name="dealer_settings"),
|
||||
path("dealers/assign-car-makes/", views.assign_car_makes, name="assign_car_makes"),
|
||||
path("dashboards/manager/", views.ManagerDashboard.as_view(), name="manager_dashboard"),
|
||||
path("dashboards/sales/", views.SalesDashboard.as_view(), name="sales_dashboard"),
|
||||
@ -61,9 +61,9 @@ urlpatterns = [
|
||||
path('cars/inventory/table/', views.CarListViewTable.as_view(), name="car_table"),
|
||||
path("export/format/", TableExport, name="export"),
|
||||
# Dealer URLs
|
||||
path("dealers/<int:pk>/", views.DealerDetailView.as_view(), name="dealer_detail"),
|
||||
path("dealers/<slug:slug>/", views.DealerDetailView.as_view(), name="dealer_detail"),
|
||||
path(
|
||||
"dealers/<int:pk>/update/",
|
||||
"dealers/<slug:slug>/update/",
|
||||
views.DealerUpdateView.as_view(),
|
||||
name="dealer_update",
|
||||
),
|
||||
@ -76,7 +76,7 @@ urlpatterns = [
|
||||
# CRM URLs
|
||||
path("customers/", views.CustomerListView.as_view(), name="customer_list"),
|
||||
path(
|
||||
"customers/<int:pk>/",
|
||||
"customers/<slug:slug>/",
|
||||
views.CustomerDetailView.as_view(),
|
||||
name="customer_detail",
|
||||
),
|
||||
@ -84,18 +84,18 @@ urlpatterns = [
|
||||
"customers/create/", views.CustomerCreateView.as_view(), name="customer_create"
|
||||
),
|
||||
path(
|
||||
"customers/<int:pk>/update/",
|
||||
"customers/<slug:slug>/update/",
|
||||
views.CustomerUpdateView.as_view(),
|
||||
name="customer_update",
|
||||
),
|
||||
path("customers/<int:pk>/delete/", views.delete_customer, name="customer_delete"),
|
||||
path("customers/<slug:slug>/delete/", views.delete_customer, name="customer_delete"),
|
||||
path(
|
||||
"customers/<str:customer_id>/opportunities/create/",
|
||||
"customers/<slug:slug>/opportunities/create/",
|
||||
views.OpportunityCreateView.as_view(),
|
||||
name="create_opportunity",
|
||||
),
|
||||
path(
|
||||
"customers/<int:pk>/add-note/",
|
||||
"customers/<slug:slug>/add-note/",
|
||||
views.add_note_to_customer,
|
||||
name="add_note_to_customer",
|
||||
),
|
||||
@ -105,13 +105,13 @@ urlpatterns = [
|
||||
|
||||
path("crm/leads/", views.LeadListView.as_view(), name="lead_list"),
|
||||
path(
|
||||
"crm/leads/<int:pk>/view/", views.LeadDetailView.as_view(), name="lead_detail"
|
||||
"crm/leads/<slug:slug>/view/", views.LeadDetailView.as_view(), name="lead_detail"
|
||||
),
|
||||
path("crm/leads/create/", views.lead_create, name="lead_create"),
|
||||
path(
|
||||
"crm/leads/<int:pk>/update/", views.LeadUpdateView.as_view(), name="lead_update"
|
||||
),
|
||||
path("crm/leads/<int:pk>/delete/", views.LeadDeleteView, name="lead_delete"),
|
||||
path("crm/leads/<slug:slug>/delete/", views.LeadDeleteView, name="lead_delete"),
|
||||
path("crm/leads/<int:pk>/lead-convert/", views.lead_convert, name="lead_convert"),
|
||||
path("crm/leads/<int:pk>/add-note/", views.add_note_to_lead, name="add_note_to_lead"),
|
||||
path('crm/leads/<int:pk>/update-note/', views.update_note, name='update_note_to_lead'),
|
||||
@ -122,27 +122,27 @@ urlpatterns = [
|
||||
name="update_task",
|
||||
),
|
||||
path(
|
||||
"crm/<str:content_type>/<int:pk>/add-task/",
|
||||
"crm/<str:content_type>/<slug:slug>/add-task/",
|
||||
views.add_task,
|
||||
name="add_task",
|
||||
),
|
||||
path(
|
||||
"crm/<str:content_type>/<int:pk>/add-activity/",
|
||||
"crm/<str:content_type>/<slug:slug>/add-activity/",
|
||||
views.add_activity,
|
||||
name="add_activity",
|
||||
),
|
||||
path(
|
||||
"crm/leads/<int:pk>/send_lead_email/",
|
||||
"crm/leads/<slug:slug>/send_lead_email/",
|
||||
views.send_lead_email,
|
||||
name="send_lead_email",
|
||||
),
|
||||
path(
|
||||
"crm/leads/<int:pk>/send_lead_email/<int:email_pk>",
|
||||
"crm/leads/<slug:slug>/send_lead_email/<int:email_pk>",
|
||||
views.send_lead_email,
|
||||
name="send_lead_email_with_template",
|
||||
),
|
||||
path(
|
||||
"crm/leads/<int:pk>/schedule/",
|
||||
"crm/leads/<slug:slug>/schedule/",
|
||||
views.schedule_lead,
|
||||
name="schedule_lead",
|
||||
),
|
||||
@ -229,33 +229,34 @@ urlpatterns = [
|
||||
path('crm/calender/', views.EmployeeCalendarView.as_view(), name='calendar_list'),
|
||||
# Vendor URLs
|
||||
path("vendors", views.VendorListView.as_view(), name="vendor_list"),
|
||||
path("vendors/<int:pk>/", views.vendorDetailView, name="vendor_detail"),
|
||||
path("vendors/<slug:slug>/", views.vendorDetailView, name="vendor_detail"),
|
||||
path("vendors/create/", views.VendorCreateView.as_view(), name="vendor_create"),
|
||||
path(
|
||||
"vendors/<int:pk>/update/",
|
||||
"vendors/<slug:slug>/update/",
|
||||
views.VendorUpdateView.as_view(),
|
||||
name="vendor_update",
|
||||
),
|
||||
path(
|
||||
"vendors/<int:pk>/delete/",
|
||||
"vendors/<slug:slug>/delete/",
|
||||
views.delete_vendor,
|
||||
name="vendor_delete",
|
||||
),
|
||||
# Car URLs
|
||||
path("cars/add/", views.CarCreateView.as_view(), name="car_add"),
|
||||
path("cars/inventory/", views.CarInventory.as_view(), name="car_inventory_all"),
|
||||
path(
|
||||
"cars/inventory/<int:make_id>/<int:model_id>/<int:trim_id>/",
|
||||
"cars/inventory/<slug:make_id>/<slug:model_id>/<slug:trim_id>/",
|
||||
views.CarInventory.as_view(),
|
||||
name="car_inventory",
|
||||
),
|
||||
path("cars/inventory/stats", views.inventory_stats_view, name="inventory_stats"),
|
||||
path("cars/inventory/list", views.CarListView.as_view(), name="car_list"),
|
||||
path("cars/<int:pk>/", views.CarDetailView.as_view(), name="car_detail"),
|
||||
path("cars/<int:pk>/history/", views.car_history, name="car_history"),
|
||||
path("cars/<int:pk>/update/", views.CarUpdateView.as_view(), name="car_update"),
|
||||
path("cars/<int:pk>/delete/", views.CarDeleteView.as_view(), name="car_delete"),
|
||||
path("cars/<slug:slug>/", views.CarDetailView.as_view(), name="car_detail"),
|
||||
path("cars/<slug:slug>/history/", views.car_history, name="car_history"),
|
||||
path("cars/<slug:slug>/update/", views.CarUpdateView.as_view(), name="car_update"),
|
||||
path("cars/<slug:slug>/delete/", views.CarDeleteView.as_view(), name="car_delete"),
|
||||
path(
|
||||
"cars/<int:car_pk>/finance/create/",
|
||||
"cars/<slug:slug>/finance/create/",
|
||||
views.CarFinanceCreateView.as_view(),
|
||||
name="car_finance_create",
|
||||
),
|
||||
@ -264,43 +265,42 @@ urlpatterns = [
|
||||
views.CarFinanceUpdateView.as_view(),
|
||||
name="car_finance_update",
|
||||
),
|
||||
path("cars/add/", views.CarCreateView.as_view(), name="car_add"),
|
||||
path("ajax/", views.AjaxHandlerView.as_view(), name="ajax_handler"),
|
||||
path(
|
||||
"cars/<int:car_pk>/add-color/", views.CarColorCreate.as_view(), name="add_color"
|
||||
"cars/<slug:slug>/add-color/", views.CarColorCreate.as_view(), name="add_color"
|
||||
),
|
||||
path(
|
||||
"cars/<int:car_pk>/location/add/",
|
||||
"cars/<slug:slug>/location/add/",
|
||||
views.CarLocationCreateView.as_view(),
|
||||
name="add_car_location",
|
||||
),
|
||||
path(
|
||||
"cars/<int:car_pk>/location/<int:pk>/update",
|
||||
"cars/<slug:car_pk>/location/<int:pk>/update",
|
||||
views.CarLocationUpdateView.as_view(),
|
||||
name="update_car_location",
|
||||
),
|
||||
path(
|
||||
"cars/<int:pk>/location/update/",
|
||||
"cars/<slug:slug>/location/update/",
|
||||
views.CarTransferCreateView.as_view(),
|
||||
name="transfer",
|
||||
),
|
||||
path(
|
||||
"cars/<int:pk>/location/detail/",
|
||||
"cars/<slug:slug>/location/detail/",
|
||||
views.CarTransferDetailView.as_view(),
|
||||
name="transfer_detail",
|
||||
),
|
||||
path(
|
||||
"cars/<int:car_pk>/location/<int:transfer_pk>/transfer_approve/",
|
||||
"cars/<slug:slug>/location/<int:transfer_pk>/transfer_approve/",
|
||||
views.car_transfer_approve,
|
||||
name="transfer_confirm",
|
||||
),
|
||||
path(
|
||||
"cars/<int:car_pk>/location/<int:transfer_pk>/transfer_accept_reject/",
|
||||
"cars/<slug:slug>/location/<int:transfer_pk>/transfer_accept_reject/",
|
||||
views.car_transfer_accept_reject,
|
||||
name="transfer_accept_reject",
|
||||
),
|
||||
path(
|
||||
"cars/<int:car_pk>/location/<int:transfer_pk>/preview/",
|
||||
"cars/<slug:slug>/location/<int:transfer_pk>/preview/",
|
||||
views.CarTransferPreviewView,
|
||||
name="transfer_preview",
|
||||
),
|
||||
@ -308,7 +308,7 @@ path(
|
||||
views.SearchCodeView.as_view(),
|
||||
name="car_search"),
|
||||
# path('cars/<int:car_pk>/colors/<int:pk>/update/',views.CarColorUpdateView.as_view(),name='color_update'),
|
||||
path("cars/reserve/<int:car_id>/",
|
||||
path("cars/reserve/<slug:slug>/",
|
||||
views.reserve_car_view,
|
||||
name="reserve_car"),
|
||||
path(
|
||||
@ -317,11 +317,11 @@ path(
|
||||
name="reservations",
|
||||
),
|
||||
path(
|
||||
"cars/<int:car_pk>/add-custom-card/",
|
||||
"cars/<slug:slug>/add-custom-card/",
|
||||
views.CustomCardCreateView.as_view(),
|
||||
name="add_custom_card",
|
||||
),
|
||||
path('cars/<int:car_pk>/add-registration/',
|
||||
path('cars/<slug:slug>/add-registration/',
|
||||
views.CarRegistrationCreateView.as_view(),
|
||||
name='add_registration'),
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -604,7 +604,7 @@ class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||||
|
||||
return context
|
||||
|
||||
def car_history(request, pk):
|
||||
def car_history(request, slug):
|
||||
"""
|
||||
Fetch and display the history of activities related to a specific car.
|
||||
|
||||
@ -622,7 +622,7 @@ def car_history(request, pk):
|
||||
including the car and its associated activities as context data.
|
||||
:rtype: HttpResponse
|
||||
"""
|
||||
car = get_object_or_404(models.Car, pk=pk)
|
||||
car = get_object_or_404(models.Car, slug=slug)
|
||||
activities = models.Activity.objects.filter(
|
||||
content_type__model="car", object_id=car.id
|
||||
)
|
||||
@ -859,7 +859,7 @@ class SearchCodeView(LoginRequiredMixin, View):
|
||||
return JsonResponse({
|
||||
"success": True,
|
||||
"code": code,
|
||||
"redirect_url": reverse("car_detail", args=[car.pk])
|
||||
"redirect_url": reverse("car_detail", args=[car.slug])
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
@ -903,16 +903,16 @@ class CarInventory(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
query = self.request.GET.get("q")
|
||||
make_id = self.kwargs["make_id"]
|
||||
model_id = self.kwargs["model_id"]
|
||||
trim_id = self.kwargs["trim_id"]
|
||||
make = models.CarMake.objects.get(slug=self.kwargs["make_id"])
|
||||
model = models.CarModel.objects.get(slug=self.kwargs["model_id"])
|
||||
trim = models.CarTrim.objects.get(slug=self.kwargs["trim_id"])
|
||||
|
||||
dealer = get_user_type(self.request)
|
||||
cars = models.Car.objects.filter(
|
||||
dealer=dealer,
|
||||
id_car_make=make_id,
|
||||
id_car_model=model_id,
|
||||
id_car_trim=trim_id,
|
||||
id_car_make=make,
|
||||
id_car_model=model,
|
||||
id_car_trim=trim,
|
||||
).order_by("receiving_date")
|
||||
|
||||
return apply_search_filters(cars, query)
|
||||
@ -951,16 +951,16 @@ class CarColorCreate(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||||
permission_required = ["inventory.view_car"]
|
||||
|
||||
def form_valid(self, form):
|
||||
car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
|
||||
car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
||||
form.instance.car = car
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("car_detail", kwargs={"pk": self.kwargs["car_pk"]})
|
||||
return reverse_lazy("car_detail", kwargs={"slug": self.kwargs["slug"]})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["car"] = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
|
||||
context["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
||||
return context
|
||||
|
||||
|
||||
@ -1103,6 +1103,7 @@ def inventory_stats_view(request):
|
||||
if make.id_car_make not in inventory:
|
||||
inventory[make.id_car_make] = {
|
||||
"make_id": make.id_car_make,
|
||||
"slug": make.slug,
|
||||
"make_name": make.get_local_name(),
|
||||
"total_cars": 0,
|
||||
"models": {},
|
||||
@ -1113,6 +1114,7 @@ def inventory_stats_view(request):
|
||||
if model and model.id_car_model not in inventory[make.id_car_make]["models"]:
|
||||
inventory[make.id_car_make]["models"][model.id_car_model] = {
|
||||
"model_id": model.id_car_model,
|
||||
"slug": model.slug,
|
||||
"model_name": model.get_local_name(),
|
||||
"total_cars": 0,
|
||||
"trims": {},
|
||||
@ -1132,6 +1134,7 @@ def inventory_stats_view(request):
|
||||
trim.id_car_trim
|
||||
] = {
|
||||
"trim_id": trim.id_car_trim,
|
||||
"slug": trim.slug,
|
||||
"trim_name": trim.name,
|
||||
"total_cars": 0,
|
||||
}
|
||||
@ -1146,11 +1149,13 @@ def inventory_stats_view(request):
|
||||
"makes": [
|
||||
{
|
||||
"make_id": make_data["make_id"],
|
||||
"slug": make_data["slug"],
|
||||
"make_name": make_data["make_name"],
|
||||
"total_cars": make_data["total_cars"],
|
||||
"models": [
|
||||
{
|
||||
"model_id": model_data["model_id"],
|
||||
"slug": model_data["slug"],
|
||||
"model_name": model_data["model_name"],
|
||||
"total_cars": model_data["total_cars"],
|
||||
"trims": list(model_data["trims"].values()),
|
||||
@ -1161,7 +1166,7 @@ def inventory_stats_view(request):
|
||||
for make_data in inventory.values()
|
||||
],
|
||||
}
|
||||
|
||||
print(result['makes'])
|
||||
return render(request, "inventory/inventory_stats.html", {"inventory": result})
|
||||
|
||||
|
||||
@ -1218,7 +1223,7 @@ class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi
|
||||
permission_required = ["inventory.add_carfinance"]
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
|
||||
self.car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
@ -1227,7 +1232,7 @@ class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("car_detail", kwargs={"pk": self.car.pk})
|
||||
return reverse("car_detail", kwargs={"slug": self.car.slug})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -1273,7 +1278,7 @@ class CarFinanceUpdateView(
|
||||
permission_required = ["inventory.change_carfinance"]
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("car_detail", kwargs={"pk": self.object.car.pk})
|
||||
return reverse("car_detail", kwargs={"slug": self.object.car.slug})
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
@ -1325,7 +1330,7 @@ class CarUpdateView(
|
||||
permission_required = ["inventory.change_car"]
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("car_detail", kwargs={"pk": self.object.pk})
|
||||
return reverse("car_detail", kwargs={"slug": self.object.slug})
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
@ -1388,10 +1393,10 @@ class CarLocationCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateV
|
||||
permission_required = ["inventory.add_carlocation"]
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("car_detail", kwargs={"pk": self.object.car.pk})
|
||||
return reverse_lazy("car_detail", kwargs={"slug": self.object.car.slug})
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
|
||||
form.instance.car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
||||
dealer = get_user_type(self.request)
|
||||
form.instance.owner = dealer
|
||||
form.save()
|
||||
@ -1425,11 +1430,11 @@ class CarLocationUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateV
|
||||
|
||||
# def get_initial(self):
|
||||
# initial = super().get_initial()
|
||||
# initial["car"] = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
|
||||
# initial["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
||||
# return initial
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
|
||||
form.instance.car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
||||
dealer = get_user_type(self.request)
|
||||
form.instance.owner = dealer
|
||||
form.save()
|
||||
@ -1437,7 +1442,7 @@ class CarLocationUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateV
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("car_detail", kwargs={"pk": self.object.car.pk})
|
||||
return reverse_lazy("car_detail", kwargs={"slug": self.object.car.slug})
|
||||
|
||||
|
||||
class CarTransferCreateView(LoginRequiredMixin, CreateView):
|
||||
@ -1465,12 +1470,12 @@ class CarTransferCreateView(LoginRequiredMixin, CreateView):
|
||||
form.fields["to_dealer"].queryset = models.Dealer.objects.exclude(
|
||||
pk=get_user_type(self.request).pk
|
||||
).all()
|
||||
form.fields["car"].queryset = models.Car.objects.filter(pk=self.kwargs["pk"])
|
||||
form.fields["car"].queryset = models.Car.objects.filter(slug=self.kwargs["slug"])
|
||||
return form
|
||||
|
||||
def get_initial(self):
|
||||
initial = super().get_initial()
|
||||
initial["car"] = get_object_or_404(models.Car, pk=self.kwargs["pk"])
|
||||
initial["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
||||
return initial
|
||||
|
||||
def form_valid(self, form):
|
||||
@ -1480,7 +1485,7 @@ class CarTransferCreateView(LoginRequiredMixin, CreateView):
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("car_detail", kwargs={"pk": self.object.car.pk})
|
||||
return reverse_lazy("car_detail", kwargs={"slug": self.object.car.slug})
|
||||
|
||||
|
||||
|
||||
@ -1516,7 +1521,7 @@ class CarTransferDetailView(LoginRequiredMixin, SuccessMessageMixin, DetailView)
|
||||
|
||||
|
||||
@login_required
|
||||
def car_transfer_approve(request, car_pk, transfer_pk):
|
||||
def car_transfer_approve(request, slug, transfer_pk):
|
||||
"""
|
||||
Approves or cancels a car transfer request based on the action parameter. This view
|
||||
handles the workflow of updating the transfer status and notifying the involved parties
|
||||
@ -1529,7 +1534,7 @@ def car_transfer_approve(request, car_pk, transfer_pk):
|
||||
:param transfer_pk: Primary key of the transfer request to approve or cancel.
|
||||
:return: An HTTP response redirecting to the car detail page of the specified car.
|
||||
"""
|
||||
car = get_object_or_404(models.Car, pk=car_pk)
|
||||
car = get_object_or_404(models.Car, slug=slug)
|
||||
transfer = get_object_or_404(models.CarTransfer, pk=transfer_pk)
|
||||
action = request.GET.get("action")
|
||||
if action == "cancel":
|
||||
@ -1543,12 +1548,12 @@ def car_transfer_approve(request, car_pk, transfer_pk):
|
||||
user=transfer.from_dealer.user,
|
||||
message=f"Car transfer request from {transfer.to_dealer} is canceled.",
|
||||
)
|
||||
return redirect("car_detail", pk=car.pk)
|
||||
return redirect("car_detail", slug=car.slug)
|
||||
transfer.status = "approved"
|
||||
transfer.save()
|
||||
url = request.build_absolute_uri(
|
||||
reverse(
|
||||
"transfer_preview", kwargs={"car_pk": car.pk, "transfer_pk": transfer.pk}
|
||||
"transfer_preview", kwargs={"slug": car.slug, "transfer_pk": transfer.pk}
|
||||
)
|
||||
)
|
||||
models.Notification.objects.create(
|
||||
@ -1556,11 +1561,11 @@ def car_transfer_approve(request, car_pk, transfer_pk):
|
||||
message=f"Car transfer request from {transfer.from_dealer} is waiting for your acceptance. <a href='{url}'> Accept</a>",
|
||||
)
|
||||
messages.success(request, _("Car transfer approved successfully"))
|
||||
return redirect("car_detail", pk=car.pk)
|
||||
return redirect("car_detail", slug=car.slug)
|
||||
|
||||
|
||||
@login_required
|
||||
def car_transfer_accept_reject(request, car_pk, transfer_pk):
|
||||
def car_transfer_accept_reject(request, slug, transfer_pk):
|
||||
"""
|
||||
Handles the acceptance or rejection of a car transfer request. Based on the
|
||||
`status` parameter obtained from the query string, the function updates the
|
||||
@ -1574,7 +1579,7 @@ def car_transfer_accept_reject(request, car_pk, transfer_pk):
|
||||
:param transfer_pk: The primary key of the car transfer request to be processed.
|
||||
:return: An HTTP redirect response to the 'inventory_stats' view.
|
||||
"""
|
||||
car = get_object_or_404(models.Car, pk=car_pk)
|
||||
car = get_object_or_404(models.Car, slug=slug)
|
||||
transfer = get_object_or_404(models.CarTransfer, pk=transfer_pk)
|
||||
status = request.GET.get("status")
|
||||
if status == "rejected":
|
||||
@ -1606,7 +1611,7 @@ def car_transfer_accept_reject(request, car_pk, transfer_pk):
|
||||
|
||||
|
||||
@login_required
|
||||
def CarTransferPreviewView(request, car_pk, transfer_pk):
|
||||
def CarTransferPreviewView(request, slug, transfer_pk):
|
||||
"""
|
||||
Handles the preview of car transfer details and ensures that a user has appropriate
|
||||
permissions to view the transfer based on their associated dealer.
|
||||
@ -1627,7 +1632,7 @@ def CarTransferPreviewView(request, car_pk, transfer_pk):
|
||||
"""
|
||||
transfer = get_object_or_404(models.CarTransfer, pk=transfer_pk)
|
||||
if transfer.to_dealer != get_user_type(request):
|
||||
return redirect("car_detail", pk=car_pk)
|
||||
return redirect("car_detail", slug=slug)
|
||||
return render(request, "inventory/transfer_preview.html", {"transfer": transfer})
|
||||
|
||||
|
||||
@ -1651,18 +1656,18 @@ class CustomCardCreateView(LoginRequiredMixin, CreateView):
|
||||
template_name = "inventory/add_custom_card.html"
|
||||
|
||||
def form_valid(self, form):
|
||||
car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
|
||||
car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
||||
form.instance.car = car
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["car"] = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
|
||||
context["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
messages.success(self.request, _("Custom Card added successfully"))
|
||||
return reverse_lazy("car_detail", kwargs={"pk": self.kwargs["car_pk"]})
|
||||
return reverse_lazy("car_detail", kwargs={"slug": self.kwargs["slug"]})
|
||||
|
||||
|
||||
class CarRegistrationCreateView(LoginRequiredMixin, CreateView):
|
||||
@ -1692,22 +1697,22 @@ class CarRegistrationCreateView(LoginRequiredMixin, CreateView):
|
||||
template_name = "inventory/car_registration_form.html"
|
||||
|
||||
def form_valid(self, form):
|
||||
car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
|
||||
car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
||||
form.instance.car = car
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["car"] = get_object_or_404(models.Car, pk=self.kwargs["car_pk"])
|
||||
context["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
messages.success(self.request, _("Registration added successfully"))
|
||||
return reverse_lazy("car_detail", kwargs={"pk": self.kwargs["car_pk"]})
|
||||
return reverse_lazy("car_detail", kwargs={"slug": self.kwargs["slug"]})
|
||||
|
||||
|
||||
@login_required()
|
||||
def reserve_car_view(request, car_id):
|
||||
def reserve_car_view(request, slug):
|
||||
"""
|
||||
Handles car reservation requests. This view requires the user to be logged in
|
||||
and processes only POST requests. When invoked, it checks if the specified car
|
||||
@ -1723,10 +1728,10 @@ def reserve_car_view(request, car_id):
|
||||
:rtype: HttpResponse or JsonResponse
|
||||
"""
|
||||
if request.method == "POST":
|
||||
car = get_object_or_404(models.Car, pk=car_id)
|
||||
car = get_object_or_404(models.Car, slug=slug)
|
||||
if car.is_reserved():
|
||||
messages.error(request, _("This car is already reserved"))
|
||||
return redirect("car_detail", pk=car.pk)
|
||||
return redirect("car_detail", slug=car.slug)
|
||||
response = reserve_car(car, request)
|
||||
return response
|
||||
return JsonResponse(
|
||||
@ -1764,7 +1769,7 @@ def manage_reservation(request, reservation_id):
|
||||
reservation.reserved_until = timezone.now() + timezone.timedelta(hours=24)
|
||||
reservation.save()
|
||||
messages.success(request, _("Reservation renewed successfully"))
|
||||
return redirect("car_detail", pk=reservation.car.pk)
|
||||
return redirect("car_detail", slug=reservation.car.slug)
|
||||
|
||||
elif action == "cancel":
|
||||
car = reservation.car
|
||||
@ -1772,7 +1777,7 @@ def manage_reservation(request, reservation_id):
|
||||
car.status = models.CarStatusChoices.AVAILABLE
|
||||
car.save()
|
||||
messages.success(request, _("Reservation canceled successfully"))
|
||||
return redirect("car_detail", pk=reservation.car.pk)
|
||||
return redirect("car_detail", slug=reservation.car.slug)
|
||||
|
||||
else:
|
||||
return JsonResponse(
|
||||
@ -1857,7 +1862,7 @@ class DealerUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||
success_message = _("Dealer updated successfully")
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("dealer_detail", kwargs={"pk": self.object.pk})
|
||||
return reverse("dealer_detail", kwargs={"slug": self.object.slug})
|
||||
|
||||
|
||||
class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
@ -2148,7 +2153,7 @@ class VendorListView(LoginRequiredMixin, ListView):
|
||||
|
||||
|
||||
@login_required
|
||||
def vendorDetailView(request, pk):
|
||||
def vendorDetailView(request, slug):
|
||||
"""
|
||||
Fetches and renders the detail view for a specific vendor.
|
||||
|
||||
@ -2163,7 +2168,7 @@ def vendorDetailView(request, pk):
|
||||
:return: An HttpResponse object containing the rendered vendor detail page.
|
||||
:rtype: HttpResponse
|
||||
"""
|
||||
vendor = get_object_or_404(models.Vendor, pk=pk)
|
||||
vendor = get_object_or_404(models.Vendor, slug=slug)
|
||||
return render(
|
||||
request, template_name="vendors/view_vendor.html", context={"vendor": vendor}
|
||||
)
|
||||
@ -2258,7 +2263,7 @@ class VendorUpdateView(
|
||||
|
||||
|
||||
@login_required
|
||||
def delete_vendor(request, pk):
|
||||
def delete_vendor(request, slug):
|
||||
"""
|
||||
Deletes an existing vendor record from the database.
|
||||
|
||||
@ -2273,7 +2278,7 @@ def delete_vendor(request, pk):
|
||||
:return: HttpResponseRedirect object for redirecting to the vendor list page.
|
||||
:rtype: HttpResponseRedirect
|
||||
"""
|
||||
vendor = get_object_or_404(models.Vendor, pk=pk)
|
||||
vendor = get_object_or_404(models.Vendor, slug=slug)
|
||||
vendor.active = False
|
||||
vendor.vendor_model.active = False
|
||||
vendor.save()
|
||||
@ -2367,7 +2372,7 @@ class GroupCreateView(
|
||||
def form_valid(self, form):
|
||||
dealer = get_user_type(self.request)
|
||||
instance = form.save(commit=False)
|
||||
group = Group.objects.create(name=f"{dealer.pk}_{instance.name}")
|
||||
group = Group.objects.create(name=f"{dealer.slug}_{instance.name}")
|
||||
instance.dealer = dealer
|
||||
instance.group = group
|
||||
instance.save()
|
||||
@ -2410,7 +2415,7 @@ class GroupUpdateView(
|
||||
dealer = get_user_type(self.request)
|
||||
instance = form.save(commit=False)
|
||||
instance.set_defualt_permissions()
|
||||
instance.group.name = f"{dealer.pk}_{instance.name}"
|
||||
instance.group.name = f"{dealer.slug}_{instance.name}"
|
||||
instance.save()
|
||||
return super().form_valid(form)
|
||||
|
||||
@ -4812,7 +4817,7 @@ def add_note_to_lead(request, pk):
|
||||
note.created_by = request.user
|
||||
note.save()
|
||||
messages.success(request, _("Note added successfully"))
|
||||
return redirect("lead_detail", pk=lead.pk)
|
||||
return redirect("lead_detail", slug=lead.slug)
|
||||
else:
|
||||
form = forms.NoteForm()
|
||||
return render(request, "crm/note_form.html", {"form": form, "lead": lead})
|
||||
@ -4865,6 +4870,7 @@ def update_note(request, pk):
|
||||
"""
|
||||
note = get_object_or_404(models.Notes, pk=pk, created_by=request.user)
|
||||
lead_pk = note.content_object.pk
|
||||
lead = models.Lead.objects.get(pk=lead_pk)
|
||||
|
||||
if request.method == "POST":
|
||||
form = forms.NoteForm(request.POST, instance=note)
|
||||
@ -4874,7 +4880,7 @@ def update_note(request, pk):
|
||||
updated_note.created_by = request.user
|
||||
updated_note.save()
|
||||
messages.success(request, _("Note updated successfully"))
|
||||
return redirect("lead_detail", pk=lead_pk)
|
||||
return redirect("lead_detail", slug=lead.slug)
|
||||
else:
|
||||
form = forms.NoteForm(instance=note)
|
||||
|
||||
@ -5035,7 +5041,7 @@ def lead_transfer(request, pk):
|
||||
|
||||
|
||||
@login_required
|
||||
def send_lead_email(request, pk, email_pk=None):
|
||||
def send_lead_email(request, slug, email_pk=None):
|
||||
"""
|
||||
Handles sending emails related to a lead. Supports creating drafts, sending emails, and rendering
|
||||
an email composition page. Changes on the lead or email objects, such as marking a lead as contacted
|
||||
@ -5055,15 +5061,15 @@ def send_lead_email(request, pk, email_pk=None):
|
||||
or email composition rendering, a response object is returned to render the respective page.
|
||||
Type: HttpResponse
|
||||
"""
|
||||
lead = get_object_or_404(models.Lead, pk=pk)
|
||||
lead = get_object_or_404(models.Lead, slug=slug)
|
||||
status = request.GET.get("status")
|
||||
dealer = get_user_type(request)
|
||||
if status == 'draft':
|
||||
models.Email.objects.create(content_object=lead, created_by=request.user,from_email="manager@tenhal.com", to_email=request.GET.get("to"), subject=request.GET.get("subject"), message=request.GET.get("message"),status=models.EmailStatus.DRAFT)
|
||||
models.Activity.objects.create(dealer=dealer,content_object=lead, notes="Email Draft",created_by=request.user,activity_type=models.ActionChoices.EMAIL)
|
||||
messages.success(request, _("Email Draft successfully"))
|
||||
response = HttpResponse(redirect("lead_detail", pk=lead.pk))
|
||||
response["HX-Redirect"] = reverse("lead_detail", args=[lead.pk])
|
||||
response = HttpResponse(redirect("lead_detail", slug=lead.slug))
|
||||
response["HX-Redirect"] = reverse("lead_detail", args=[lead.slug])
|
||||
return response
|
||||
|
||||
if request.method == "POST":
|
||||
@ -7028,7 +7034,7 @@ class CarListViewTable(LoginRequiredMixin, ExportMixin, SingleTableView):
|
||||
|
||||
|
||||
@login_required
|
||||
def DealerSettingsView(request, pk):
|
||||
def DealerSettingsView(request, slug):
|
||||
"""
|
||||
Handles dealer settings view where dealers can update their financial and
|
||||
payment account settings. This view ensures validation and reassigns form
|
||||
@ -7045,7 +7051,7 @@ def DealerSettingsView(request, pk):
|
||||
to the dealer detail view after successful form submission.
|
||||
:rtype: HttpResponse
|
||||
"""
|
||||
dealer_setting = get_object_or_404(models.DealerSettings, dealer__pk=pk)
|
||||
dealer_setting = get_object_or_404(models.DealerSettings, dealer__slug=slug)
|
||||
dealer = get_user_type(request)
|
||||
if request.method == "POST":
|
||||
form = forms.DealerSettingsForm(request.POST, instance=dealer_setting)
|
||||
@ -7054,7 +7060,7 @@ def DealerSettingsView(request, pk):
|
||||
instance.dealer = dealer
|
||||
instance.save()
|
||||
messages.success(request, _('Settings updated'))
|
||||
return redirect('dealer_detail', pk=dealer.pk)
|
||||
return redirect('dealer_detail', slug=dealer.slug)
|
||||
else:
|
||||
print(form.errors)
|
||||
form = forms.DealerSettingsForm(instance=dealer_setting, initial={"dealer": dealer})
|
||||
@ -7134,7 +7140,7 @@ def assign_car_makes(request):
|
||||
makes = form.cleaned_data["car_makes"]
|
||||
create_accounts_for_make(dealer, makes)
|
||||
form.save()
|
||||
return redirect("dealer_detail", pk=dealer.pk)
|
||||
return redirect("dealer_detail", slug=dealer.slug)
|
||||
else:
|
||||
print(form.errors)
|
||||
else:
|
||||
@ -7743,13 +7749,13 @@ def notifications_history(request):
|
||||
# )
|
||||
# return render(request, 'activity_history.html')
|
||||
|
||||
def add_activity(request,content_type,pk):
|
||||
def add_activity(request,content_type,slug):
|
||||
try:
|
||||
model = apps.get_model(f'inventory.{content_type}')
|
||||
except LookupError:
|
||||
raise Http404("Model not found")
|
||||
|
||||
obj = get_object_or_404(model, pk=pk)
|
||||
obj = get_object_or_404(model, slug=slug)
|
||||
dealer = get_user_type(request)
|
||||
if request.method == "POST":
|
||||
form = forms.ActivityForm(request.POST)
|
||||
@ -7765,14 +7771,14 @@ def add_activity(request,content_type,pk):
|
||||
messages.success(request, _("Activity added successfully"))
|
||||
else:
|
||||
messages.error(request, _("Activity form is not valid"))
|
||||
return redirect(f"{content_type}_detail", pk=pk)
|
||||
def add_task(request,content_type,pk):
|
||||
return redirect(f"{content_type}_detail", slug=slug)
|
||||
def add_task(request,content_type,slug):
|
||||
try:
|
||||
model = apps.get_model(f'inventory.{content_type}')
|
||||
except LookupError:
|
||||
raise Http404("Model not found")
|
||||
|
||||
obj = get_object_or_404(model, pk=pk)
|
||||
obj = get_object_or_404(model, slug=slug)
|
||||
dealer = get_user_type(request)
|
||||
if request.method == "POST":
|
||||
form = forms.StaffTaskForm(request.POST)
|
||||
@ -7788,16 +7794,22 @@ def add_task(request,content_type,pk):
|
||||
else:
|
||||
print(form.errors)
|
||||
messages.error(request, _("Task form is not valid"))
|
||||
return redirect(f"{content_type}_detail", pk=pk)
|
||||
return redirect(f"{content_type}_detail", slug=slug)
|
||||
|
||||
def update_task(request,pk):
|
||||
task = get_object_or_404(models.Tasks, pk=pk)
|
||||
lead = get_object_or_404(models.Lead, pk=task.content_object.id)
|
||||
|
||||
if request.method == "POST":
|
||||
task.completed = False if task.completed else True
|
||||
task.save()
|
||||
messages.success(request, _("Task updated successfully"))
|
||||
else:
|
||||
messages.error(request, _("Task form is not valid"))
|
||||
response = HttpResponse()
|
||||
response['HX-Refresh'] = 'true'
|
||||
return response
|
||||
# response = HttpResponse()
|
||||
# response['HX-Refresh'] = 'true'
|
||||
# return response
|
||||
tasks = models.Tasks.objects.filter(
|
||||
content_type__model="lead", object_id=lead.id
|
||||
)
|
||||
return render(request,'crm/leads/lead_detail.html',{'lead':lead,'tasks':tasks})
|
||||
@ -2,22 +2,22 @@
|
||||
echo "Loading initial data"
|
||||
|
||||
echo "Loading carmake"
|
||||
python3 manage.py loaddata --app carmake carmake_backup.json
|
||||
python3 manage.py loaddata --app carmake carmake_backup_output.json
|
||||
|
||||
echo "Loading carmodel"
|
||||
python3 manage.py loaddata --app carmodel carmodel_backup.json
|
||||
python3 manage.py loaddata --app carmodel carmodel_backup_output.json
|
||||
|
||||
echo "Loading carserie"
|
||||
python3 manage.py loaddata --app carserie carserie_backup.json
|
||||
python3 manage.py loaddata --app carserie carserie_backup_output.json
|
||||
|
||||
echo "Loading cartrim"
|
||||
python3 manage.py loaddata --app cartrim cartrim_backup.json
|
||||
python3 manage.py loaddata --app cartrim cartrim_backup_output.json
|
||||
|
||||
echo "Loading caroption"
|
||||
python3 manage.py loaddata --app caroption caroption_backup.json
|
||||
python3 manage.py loaddata --app caroption caroption_backup_output.json
|
||||
|
||||
echo "Loading carequipment"
|
||||
python3 manage.py loaddata --app carequipment carequipment_backup.json
|
||||
python3 manage.py loaddata --app carequipment carequipment_backup_output.json
|
||||
|
||||
echo "Populating colors"
|
||||
python3 manage.py populate_colors
|
||||
@ -26,4 +26,6 @@ python3 manage.py tenhal_plan
|
||||
|
||||
python3 manage.py set_vat
|
||||
|
||||
python3 manage.py initial_services_offered
|
||||
|
||||
echo "Done"
|
||||
56
slug_data.py
Normal file
@ -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)
|
||||
BIN
static/images/customers/image_UGmtPMg.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
static/images/logos/vendors/logo-for-the-word-daju-48a980_Bw4t8ED.jpg
vendored
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
static/images/logos/vendors/logo-for-the-word-daju-48a980_CfNtYr7.jpg
vendored
Normal file
|
After Width: | Height: | Size: 42 KiB |
@ -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;
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
BIN
staticfiles/images/customers/image.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
staticfiles/images/logos/vendors/image.png
vendored
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
staticfiles/images/logos/vendors/logo-for-the-word-daju-48a980.jpg
vendored
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
staticfiles/images/logos/vendors/logo-for-the-word-daju-48a980_Bw4t8ED.jpg
vendored
Normal file
|
After Width: | Height: | Size: 42 KiB |
@ -10,7 +10,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="{% url 'add_activity' content_type=content_type pk=pk %}" method="post" class="add_activity_form">
|
||||
<form action="{% url 'add_activity' content_type=content_type slug=slug %}" method="post" class="add_activity_form">
|
||||
{% csrf_token %}
|
||||
<div class="mb-2 form-group">
|
||||
<select class="form-select" name="activity_type" id="activity_type">
|
||||
|
||||
@ -352,7 +352,7 @@
|
||||
<div class="tab-pane fade" id="tab-emails" role="tabpanel" aria-labelledby="emails-tab">
|
||||
<div class="mb-1 d-flex justify-content-between align-items-center">
|
||||
<h3 class="mb-0" id="scrollspyEmails">{{ _("Emails") }}</h3>
|
||||
<a href="{% url 'send_lead_email' lead.pk %}">
|
||||
<a href="{% url 'send_lead_email' lead.slug %}">
|
||||
<button type="button" class="btn btn-sm btn-phoenix-primary">
|
||||
<span class="fas fa-plus me-1"></span>
|
||||
{% trans 'Send Email' %}
|
||||
@ -448,7 +448,7 @@
|
||||
</td>
|
||||
<td class="sent align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2">{{email.from_email}}</td>
|
||||
<td class="date align-middle white-space-nowrap text-body py-2">{{email.created}}</td>
|
||||
<td class="align-middle white-space-nowrap ps-3"><a class="text-body" href="{% url 'send_lead_email_with_template' lead.pk email.pk %}"><span class="fa-solid fa-email text-primary me-2"></span>Send</a></td>
|
||||
<td class="align-middle white-space-nowrap ps-3"><a class="text-body" href="{% url 'send_lead_email_with_template' lead.slug email.pk %}"><span class="fa-solid fa-email text-primary me-2"></span>Send</a></td>
|
||||
<td class="status align-middle fw-semibold text-end py-2"><span class="badge badge-phoenix fs-10 badge-phoenix-warning">draft</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@ -496,10 +496,10 @@
|
||||
</thead>
|
||||
<tbody class="list" id="all-email-table-body">
|
||||
{% for task in tasks %}
|
||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static {% if task.completed %}completed-task{% endif %}">
|
||||
<tr class="task-{{task.pk}} hover-actions-trigger btn-reveal-trigger position-static {% if task.completed %}completed-task{% endif %}">
|
||||
<td class="fs-9 align-middle px-0 py-3">
|
||||
<div class="form-check mb-0 fs-8">
|
||||
<input class="form-check-input" type="checkbox" hx-post="{% url 'update_task' task.pk %}" hx-trigger="change" hx-swap="none" />
|
||||
<input class="form-check-input" type="checkbox" hx-post="{% url 'update_task' task.pk %}" hx-trigger="change" hx-swap="outerHTML" hx-select=".task-{{task.pk}}" hx-target=".task-{{task.pk}}" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="subject order align-middle white-space-nowrap py-2 ps-0"><a class="fw-semibold text-primary" href="">{{task.title}}</a>
|
||||
@ -552,7 +552,7 @@
|
||||
</div>
|
||||
|
||||
<!-- activity Modal -->
|
||||
{% include "components/activity_modal.html" with content_type="lead" pk=lead.pk %}
|
||||
{% include "components/activity_modal.html" with content_type="lead" slug=lead.slug %}
|
||||
<!-- task Modal -->
|
||||
<div class="modal fade" id="taskModal" tabindex="-1" aria-labelledby="taskModalLabel" aria-hidden="true">
|
||||
|
||||
@ -565,7 +565,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="{% url 'add_task' 'lead' lead.pk %}" method="post" class="add_task_form">
|
||||
<form action="{% url 'add_task' 'lead' lead.slug %}" method="post" class="add_task_form">
|
||||
{% csrf_token %}
|
||||
{{ staff_task_form|crispy }}
|
||||
<button type="submit" class="btn btn-success w-100">{% trans 'Save' %}</button>
|
||||
|
||||
@ -148,7 +148,7 @@
|
||||
<p>{% trans "Are you sure you want to delete this lead?" %}</p>
|
||||
</div>
|
||||
<div class="modal-footer flex justify-content-center border-top-0">
|
||||
<a type="button" class="btn btn-sm btn-danger w-100" href="{% url 'lead_delete' lead.pk %}">
|
||||
<a type="button" class="btn btn-sm btn-danger w-100" href="{% url 'lead_delete' lead.slug %}">
|
||||
{% trans "Yes" %}
|
||||
</a>
|
||||
</div>
|
||||
@ -159,7 +159,7 @@
|
||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
||||
<td class="name align-middle white-space-nowrap ps-0">
|
||||
<div class="d-flex align-items-center">
|
||||
<div><a class="fs-8 fw-bold" href="{% url 'lead_detail' lead.pk %}">{{lead.full_name}}</a>
|
||||
<div><a class="fs-8 fw-bold" href="{% url 'lead_detail' lead.slug %}">{{lead.full_name}}</a>
|
||||
<div class="d-flex align-items-center">
|
||||
<p class="mb-0 text-body-highlight fw-semibold fs-9 me-2"></p>
|
||||
{% if lead.status == "new" %}
|
||||
@ -187,11 +187,11 @@
|
||||
<div class="accordion" id="accordionExample">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingTwo">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse{{lead.pk}}" aria-expanded="false" aria-controls="collapseTwo">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse{{lead.slug}}" aria-expanded="false" aria-controls="collapseTwo">
|
||||
View Schedules ({{lead.get_latest_schedules.count}})
|
||||
</button>
|
||||
</h2>
|
||||
<div class="accordion-collapse collapse" id="collapse{{lead.pk}}" aria-labelledby="headingTwo" data-bs-parent="#accordionExample">
|
||||
<div class="accordion-collapse collapse" id="collapse{{lead.slug}}" aria-labelledby="headingTwo" data-bs-parent="#accordionExample">
|
||||
<div class="accordion-body pt-0">
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<table><tbody>
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
<div class="card email-content">
|
||||
<h5 class="card-header">Send Mail</h5>
|
||||
<div class="card-body">
|
||||
<form class="d-flex flex-column h-100" action="{% url 'send_lead_email' lead.pk %}" method="post">
|
||||
<form class="d-flex flex-column h-100" action="{% url 'send_lead_email' lead.slug %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="row g-3 mb-2">
|
||||
<div class="col-12">
|
||||
@ -26,8 +26,8 @@
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'lead_detail' lead.pk %}" class="btn btn-link text-body fs-10 text-decoration-none">Discard</a>
|
||||
<a hx-boost="true" hx-push-url='false' hx-include="#message,#subject,#to" href="{% url 'send_lead_email' lead.pk %}?status=draft" class="btn btn-secondary text-white fs-10 text-decoration-none">Save as Draft</a>
|
||||
<a href="{% url 'lead_detail' lead.slug %}" class="btn btn-link text-body fs-10 text-decoration-none">Discard</a>
|
||||
<a hx-boost="true" hx-push-url='false' hx-include="#message,#subject,#to" href="{% url 'send_lead_email' lead.slug %}?status=draft" class="btn btn-secondary text-white fs-10 text-decoration-none">Save as Draft</a>
|
||||
<button class="btn btn-primary fs-10" type="submit">Send<span class="fa-solid fa-paper-plane ms-1"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -72,7 +72,7 @@
|
||||
<div class="kanban-column">
|
||||
<div class="kanban-header">New Leads ({{new|length}})</div>
|
||||
{% for lead in new %}
|
||||
<a href="{% url 'lead_detail' lead.id %}">
|
||||
<a href="{% url 'lead_detail' lead.slug %}">
|
||||
<div class="lead-card">
|
||||
<strong>{{lead.full_name|capfirst}}</strong><br>
|
||||
<small>{{lead.email}}</small><br>
|
||||
@ -88,7 +88,7 @@
|
||||
<div class="kanban-column">
|
||||
<div class="kanban-header">Follow Ups ({{follow_up|length}})</div>
|
||||
{% for lead in follow_up %}
|
||||
<a href="{% url 'lead_detail' lead.id %}">
|
||||
<a href="{% url 'lead_detail' lead.slug %}">
|
||||
<div class="lead-card">
|
||||
<strong>{{lead.full_name|capfirst}}</strong><br>
|
||||
<small>{{lead.email}}</small><br>
|
||||
@ -104,7 +104,7 @@
|
||||
<div class="kanban-column">
|
||||
<div class="kanban-header">Negotiation ({{negotiation|length}})</div>
|
||||
{% for lead in negotiation %}
|
||||
<a href="{% url 'lead_detail' lead.id %}">
|
||||
<a href="{% url 'lead_detail' lead.slug %}">
|
||||
<div class="lead-card">
|
||||
<strong>{{lead.full_name|capfirst}}</strong><br>
|
||||
<small>{{lead.email}}</small><br>
|
||||
@ -120,7 +120,7 @@
|
||||
<div class="kanban-column">
|
||||
<div class="kanban-header">Won ({{won|length}})</div>
|
||||
{% for lead in won %}
|
||||
<a href="{% url 'lead_detail' lead.id %}">
|
||||
<a href="{% url 'lead_detail' lead.slug %}">
|
||||
<div class="lead-card">
|
||||
<strong>{{lead.full_name|capfirst}}</strong><br>
|
||||
<small>{{lead.email}}</small><br>
|
||||
@ -136,7 +136,7 @@
|
||||
<div class="kanban-column">
|
||||
<div class="kanban-header">Lose ({{lose|length}})</div>
|
||||
{% for lead in lose %}
|
||||
<a href="{% url 'lead_detail' lead.id %}">
|
||||
<a href="{% url 'lead_detail' lead.slug %}">
|
||||
<div class="lead-card">
|
||||
<strong>{{lead.full_name|capfirst}}</strong><br>
|
||||
<small>{{lead.email}}</small><br>
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
</td>
|
||||
<td class="name align-middle white-space-nowrap ps-0">
|
||||
<div class="d-flex align-items-center">
|
||||
<div><a class="fs-8 fw-bold" href="{% url 'customer_detail' customer.pk %}">{{ customer.full_name }}</a>
|
||||
<div><a class="fs-8 fw-bold" href="{% url 'customer_detail' customer.slug %}">{{ customer.full_name }}</a>
|
||||
<div class="d-flex align-items-center">
|
||||
</div>
|
||||
</div>
|
||||
@ -91,13 +91,13 @@
|
||||
<td class="date align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 text-body-tertiary">{{ customer.created|date }}</td>
|
||||
<td class="align-middle white-space-nowrap text-end pe-0 ps-4">
|
||||
{% if perms.django_ledger.change_customermodel %}
|
||||
<a href="{% url 'customer_update' customer.pk %}" class="btn btn-sm btn-phoenix-primary me-2" data-url="{% url 'customer_update' customer.pk %}">
|
||||
<a href="{% url 'customer_update' customer.slug %}" class="btn btn-sm btn-phoenix-primary me-2" data-url="{% url 'customer_update' customer.slug %}">
|
||||
<i class="fas fa-pen"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.django_ledger.delete_customermodel %}
|
||||
<button class="btn btn-phoenix-danger btn-sm delete-btn"
|
||||
data-url="{% url 'customer_delete' customer.pk %}"
|
||||
data-url="{% url 'customer_delete' customer.slug %}"
|
||||
data-message="{{ _("Are you sure you want to delete this customer")}}"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal">
|
||||
<i class="fas fa-trash"></i>
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
<div class="col-auto">
|
||||
{% if perms.django_ledger.delete_customermodel %}
|
||||
<button class="btn btn-phoenix-danger btn-sm delete-btn"
|
||||
data-url="{% url 'customer_delete' customer.pk %}"
|
||||
data-url="{% url 'customer_delete' customer.slug %}"
|
||||
data-message="Are you sure you want to delete this customer?"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal">
|
||||
<i class="fas fa-trash me-1"> </i>{{ _("Delete") }}
|
||||
@ -27,7 +27,7 @@
|
||||
|
||||
<div class="col-auto">
|
||||
{% if perms.django_ledger.change_customermodel %}
|
||||
<a href="{% url 'customer_update' customer.pk %}" class="btn btn-sm btn-phoenix-warning"><span class="fa-solid fa-pen-to-square me-2"></span>{{_("Update")}}</a>
|
||||
<a href="{% url 'customer_update' customer.slug %}" class="btn btn-sm btn-phoenix-warning"><span class="fa-solid fa-pen-to-square me-2"></span>{{_("Update")}}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@ -81,7 +81,7 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center justify-content-end">
|
||||
<a id="addBtn" href="#" class="btn btn-sm btn-phoenix-primary mb-3" data-url="{% url 'add_note_to_customer' customer.pk %}" data-bs-toggle="modal" data-bs-target="#noteModal" data-note-title="{{ _("Add") }}<i class='fa fa-plus-circle text-success ms-2'></i>">
|
||||
<a id="addBtn" href="#" class="btn btn-sm btn-phoenix-primary mb-3" data-url="{% url 'add_note_to_customer' customer.slug %}" data-bs-toggle="modal" data-bs-target="#noteModal" data-note-title="{{ _("Add") }}<i class='fa fa-plus-circle text-success ms-2'></i>">
|
||||
<span class="fas fa-plus me-1"></span>
|
||||
{% trans 'Add Note' %}
|
||||
</a>
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<a href="{% url 'account_change_password' %}" class="btn btn-phoenix-danger"><span class="fas fa-key me-2"></span>{{ _("Change Password") }}</a>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a class="btn btn-phoenix-secondary" href="{% url 'dealer_update' dealer.pk %}"><span class="fas fa-edit me-2 text-body-quaternary"></span>{{ _("Edit") }} </a>
|
||||
<a class="btn btn-phoenix-secondary" href="{% url 'dealer_update' dealer.slug %}"><span class="fas fa-edit me-2 text-body-quaternary"></span>{{ _("Edit") }} </a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -415,7 +415,7 @@
|
||||
<ul class="nav d-flex flex-column mb-2 pb-1">
|
||||
{% if request.is_dealer %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link px-3 d-block" href="{% url 'dealer_detail' request.user.dealer.pk %}"> <span class="me-2 text-body align-bottom" data-feather="user"></span><span>{% translate 'profile'|capfirst %}</span></a>
|
||||
<a class="nav-link px-3 d-block" href="{% url 'dealer_detail' request.user.dealer.slug %}"> <span class="me-2 text-body align-bottom" data-feather="user"></span><span>{% translate 'profile'|capfirst %}</span></a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
@ -432,7 +432,7 @@
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
{% if request.is_dealer %}
|
||||
<a class="nav-link px-3 d-block" href="{% url 'dealer_settings' request.user.dealer.pk %}"> <span class="me-2 text-body align-bottom" data-feather="settings"></span>{{ _("Settings") }}</a>
|
||||
<a class="nav-link px-3 d-block" href="{% url 'dealer_settings' request.user.dealer.slug %}"> <span class="me-2 text-body align-bottom" data-feather="settings"></span>{{ _("Settings") }}</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_filters %}
|
||||
|
||||
<form method="post" id="customCardForm" action="{% url 'add_custom_card' car.pk %}">
|
||||
<form method="post" id="customCardForm" action="{% url 'add_custom_card' car.slug %}">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<div class="d-flex gap-1">
|
||||
|
||||
@ -155,16 +155,15 @@
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
|
||||
<th>{% trans 'Location'|capfirst %}</th>
|
||||
<td>
|
||||
{% if car.finances and not car.get_transfer %}
|
||||
{% if car.location %} {% if car.location.is_owner_showroom %} {% trans 'Our Showroom' %} {% else %} {{ car.location.showroom.get_local_name }} {% endif %}
|
||||
<a href="{% url 'update_car_location' car.pk car.location.pk%}" class="btn btn-phoenix-danger btn-sm">
|
||||
<a href="{% url 'update_car_location' car.slug car.location.pk%}" class="btn btn-phoenix-danger btn-sm">
|
||||
{% trans "transfer"|capfirst %}
|
||||
</a>
|
||||
{% else %} {% trans "No location available." %}
|
||||
<a href="{% url 'add_car_location' car.pk %}" class="btn btn-phoenix-success btn-sm ms-2">
|
||||
<a href="{% url 'add_car_location' car.slug %}" class="btn btn-phoenix-success btn-sm ms-2">
|
||||
{% trans "Add" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
@ -176,8 +175,8 @@
|
||||
<div>
|
||||
{% if not car.get_transfer %}
|
||||
{% if perms.inventory.change_car %}
|
||||
<a href="{% url 'car_update' car.pk %}" class="btn btn-phoenix-warning btn-sm mt-1">{% trans "Edit" %}</a>
|
||||
<a href="{% url 'transfer' car.pk %}" class="btn btn-phoenix-danger btn-sm">
|
||||
<a href="{% url 'car_update' car.slug %}" class="btn btn-phoenix-warning btn-sm mt-1">{% trans "Edit" %}</a>
|
||||
<a href="{% url 'transfer' car.slug %}" class="btn btn-phoenix-danger btn-sm">
|
||||
{% trans "Sell to another dealer"|capfirst %}
|
||||
</a>
|
||||
{% endif %}
|
||||
@ -242,7 +241,7 @@
|
||||
{% else %}
|
||||
<p>{% trans "No finance details available." %}</p>
|
||||
{% if perms.inventory.add_carfinance %}
|
||||
<a href="{% url 'car_finance_create' car.pk %}" class="btn btn-phoenix-success btn-sm mb-3">
|
||||
<a href="{% url 'car_finance_create' car.slug %}" class="btn btn-phoenix-success btn-sm mb-3">
|
||||
{% trans "Add" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
@ -288,7 +287,7 @@
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
{% if perms.inventory.change_carcolors %}
|
||||
<a href="{% url 'add_color' car.pk %}" class="btn btn-phoenix-success btn-sm">
|
||||
<a href="{% url 'add_color' car.slug %}" class="btn btn-phoenix-success btn-sm">
|
||||
{% trans "Add" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
@ -454,9 +453,7 @@
|
||||
<div class="modal-body">
|
||||
{% trans 'Are you sure you want to reserve this car?' %}
|
||||
</div>
|
||||
<form method="POST" action="{% url 'reserve_car' car.id %}" class="form ">
|
||||
|
||||
|
||||
<form method="POST" action="{% url 'reserve_car' car.slug %}" class="form ">
|
||||
{% csrf_token %}
|
||||
<div class="p-1">
|
||||
<div class="d-flex gap-1">
|
||||
@ -493,8 +490,6 @@
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
|
||||
const csrftoken = getCookie("csrftoken");
|
||||
const ajaxUrl = "{% url 'ajax_handler' %}";
|
||||
|
||||
@ -503,7 +498,7 @@
|
||||
|
||||
// When the modal is triggered, load the form
|
||||
customCardModal.addEventListener("show.bs.modal", function () {
|
||||
const url = "{% url 'add_custom_card' car.pk %}";
|
||||
const url = "{% url 'add_custom_card' car.slug %}";
|
||||
|
||||
fetch(url)
|
||||
.then((response) => response.text())
|
||||
@ -525,7 +520,7 @@
|
||||
|
||||
// When the modal is triggered, load the form
|
||||
registrationModal.addEventListener("show.bs.modal", function () {
|
||||
const url = "{% url 'add_registration' car.pk %}";
|
||||
const url = "{% url 'add_registration' car.slug %}";
|
||||
|
||||
fetch(url)
|
||||
.then((response) => response.text())
|
||||
@ -608,7 +603,7 @@
|
||||
document.querySelectorAll(".reserve-btn").forEach((button) => {
|
||||
button.addEventListener("click", async function () {
|
||||
try {
|
||||
const response = await fetch(`{% url 'reserve_car' car.pk %}`, {
|
||||
const response = await fetch(`{% url 'reserve_car' car.slug %}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-CSRFToken": csrfToken,
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
<span class="badge badge-phoenix badge-phoenix-info"><span class="badge-label">{{_("Used")}}</span></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap text-start"><a class="fs-9 fw-bold" href="{% url 'car_detail' car.pk %}">{{ car.vin }}</a></td>
|
||||
<td class="align-middle white-space-nowrap text-start"><a class="fs-9 fw-bold" href="{% url 'car_detail' car.slug %}">{{ car.vin }}</a></td>
|
||||
<td class="align-middle white-space-nowrap text-center fw-bold">{{ car.year }}</td>
|
||||
{% if car.colors %}
|
||||
<td class="align-middle white-space-nowrap text-body fs-9 text-start">
|
||||
@ -111,7 +111,7 @@
|
||||
<span class="fw-light">{{ car.receiving_date|timesince }}</span>
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap text-end pe-0 ps-4">
|
||||
<a class="btn btn-sm btn-phoenix-success" href="{% url 'car_detail' car.pk %}">{% trans "view"|capfirst %}</a>
|
||||
<a class="btn btn-sm btn-phoenix-success" href="{% url 'car_detail' car.slug %}">{% trans "view"|capfirst %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
|
||||
@ -117,10 +117,10 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="list" id="project-list-table-body">
|
||||
{% for car in page_obj %}
|
||||
{% for car in cars %}
|
||||
<tr class="position-static">
|
||||
<td class="align-middle white-space-nowrap ps-1">
|
||||
<a class="fw-bold" href="{% url 'car_detail' car.pk %}">{{car.vin}}</a>
|
||||
<a class="fw-bold" href="{% url 'car_detail' car.slug %}">{{car.vin}}</a>
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap">
|
||||
<p class="text-body mb-0">{{car.id_car_make.get_local_name|default:car.id_car_make.name}}</p>
|
||||
@ -164,7 +164,7 @@
|
||||
<div class="btn-reveal-trigger position-static">
|
||||
<button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal fs-10" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10"></span></button>
|
||||
<div class="dropdown-menu dropdown-menu-end py-2">
|
||||
<a class="dropdown-item" href="{% url 'car_detail' car.pk %}">{{ _("View") }}</a>
|
||||
<a class="dropdown-item" href="{% url 'car_detail' car.slug %}">{{ _("View") }}</a>
|
||||
<a class="dropdown-item" href="#!">{{ _("Export") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
{% load crispy_forms_filters %}
|
||||
|
||||
<div class="w-100 g-3">
|
||||
<form method="post" id="registrationForm" action="{% url 'add_registration' car.pk %}">
|
||||
<form method="post" id="registrationForm" action="{% url 'add_registration' car.slug %}">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<div class="d-flex gap-1">
|
||||
|
||||
@ -67,7 +67,7 @@
|
||||
<ul>
|
||||
{% for trim in model.trims %}
|
||||
<li>
|
||||
<a href="{% url 'car_inventory' make_id=make.make_id model_id=model.model_id trim_id=trim.trim_id %}">
|
||||
<a href="{% url 'car_inventory' make_id=make.slug model_id=model.slug trim_id=trim.slug %}">
|
||||
{{ trim.trim_name }}
|
||||
</a> - {% trans "Total" %}:
|
||||
<strong>{{ trim.total_cars }}</strong></li>
|
||||
|
||||
6
templates/vendors/vendors_list.html
vendored
@ -84,7 +84,7 @@
|
||||
<div class="avatar avatar-xl me-3"><img class="rounded-circle" src="{% static 'images/icons/picture.svg' %}" alt="" />
|
||||
{% endif %}
|
||||
</div>
|
||||
<div><a class="fs-8 fw-bold" href="{% url 'vendor_detail' vendor.pk%}">{{ vendor.arabic_name }}</a>
|
||||
<div><a class="fs-8 fw-bold" href="{% url 'vendor_detail' vendor.slug%}">{{ vendor.arabic_name }}</a>
|
||||
<div class="d-flex align-items-center">
|
||||
<p class="mb-0 text-body-highlight fw-semibold fs-9 me-2">{{ vendor.name}}</p><!--<span class="badge badge-phoenix badge-phoenix-primary">{{ vendor.vendor_model.uuid }}</span>-->
|
||||
</div>
|
||||
@ -101,12 +101,12 @@
|
||||
<div class="btn-reveal-trigger position-static">
|
||||
<button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal fs-10" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10"></span></button>
|
||||
<div class="dropdown-menu dropdown-menu-end py-2">
|
||||
<a href="{% url 'vendor_update' vendor.pk %}" class="dropdown-item text-success-dark">
|
||||
<a href="{% url 'vendor_update' vendor.slug %}" class="dropdown-item text-success-dark">
|
||||
{% trans "Edit" %}
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="delete-btn dropdown-item text-danger"
|
||||
data-url="{% url 'vendor_delete' vendor.pk %}"
|
||||
data-url="{% url 'vendor_delete' vendor.slug %}"
|
||||
data-message="{{ _("Are you sure you want to delete this vendor")}}?"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal">
|
||||
{{ _("Delete") }}
|
||||
|
||||
4
templates/vendors/view_vendor.html
vendored
@ -28,12 +28,12 @@
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-footer d-flex">
|
||||
<a class="btn btn-sm btn-phoenix-primary me-1" href="{% url 'vendor_update' vendor.id %}">
|
||||
<a class="btn btn-sm btn-phoenix-primary me-1" href="{% url 'vendor_update' vendor.slug %}">
|
||||
{% trans "Edit" %}
|
||||
<i class="fa fa-pencil"></i>
|
||||
</a>
|
||||
<button class="btn btn-phoenix-danger btn-sm delete-btn"
|
||||
data-url="{% url 'vendor_delete' vendor.pk %}"
|
||||
data-url="{% url 'vendor_delete' vendor.slug %}"
|
||||
data-message="{{ _("Are you sure you want to delete this vendor")}}?"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal">
|
||||
{{ _("Delete") }}
|
||||
|
||||