update
This commit is contained in:
parent
00f87799d0
commit
1c668d3f0a
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-07 22:27
|
||||
# Generated by Django 5.1.4 on 2025-01-12 17:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
Binary file not shown.
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-07 22:27
|
||||
# Generated by Django 5.1.4 on 2025-01-12 17:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-07 22:27
|
||||
# Generated by Django 5.1.4 on 2025-01-12 17:20
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -567,36 +567,7 @@ class EmailForm(forms.Form):
|
||||
from_email = forms.EmailField()
|
||||
to_email = forms.EmailField(label="To")
|
||||
|
||||
class LeadCreateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Lead
|
||||
fields = ['title',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'email',
|
||||
'phone_number',
|
||||
'city',
|
||||
'salary',
|
||||
'obligations',
|
||||
'id_car_make',
|
||||
'id_car_model',
|
||||
'year',
|
||||
'source',
|
||||
'channel',
|
||||
'assigned',
|
||||
'priority',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if "id_car_make" in self.fields:
|
||||
queryset = self.fields["id_car_make"].queryset.filter(is_sa_import=True)
|
||||
self.fields["id_car_make"].choices = [
|
||||
(obj.id_car_make, obj.get_local_name()) for obj in queryset
|
||||
]
|
||||
|
||||
class LeadUpdateForm(forms.ModelForm):
|
||||
class LeadForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Lead
|
||||
fields = ['title',
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-07 22:27
|
||||
# Generated by Django 5.1.4 on 2025-01-12 17:20
|
||||
|
||||
import django.db.models.deletion
|
||||
import inventory.mixins
|
||||
@ -14,6 +14,7 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('django_ledger', '0017_alter_accountmodel_unique_together_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
@ -57,7 +58,7 @@ class Migration(migrations.Migration):
|
||||
('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)),
|
||||
('car_type', models.SmallIntegerField(choices=[(1, 'Car'), (2, 'light commercial'), (3, 'truck trailer'), (4, 'trailer'), (5, 'truck'), (6, 'bus')])),
|
||||
('car_type', models.SmallIntegerField(choices=[(1, 'Car'), (2, 'Light Commercial'), (3, 'Heavy-Duty Tractors'), (4, 'Trailers'), (5, 'Medium Trucks'), (6, 'Buses'), (20, 'Motorcycles'), (21, 'Buggy'), (22, 'Moto ATV'), (23, 'Scooters'), (24, 'Karting'), (25, 'ATV'), (26, 'Snowmobiles')])),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Make',
|
||||
@ -134,6 +135,41 @@ class Migration(migrations.Migration):
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Activity',
|
||||
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'), ('reserve_car', 'Reserve 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')),
|
||||
('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='activities_created', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Activity',
|
||||
'verbose_name_plural': 'Activities',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AdditionalServices',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Name')),
|
||||
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
|
||||
('description', models.TextField(verbose_name='Description')),
|
||||
('price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Price')),
|
||||
('taxable', models.BooleanField(default=False, verbose_name='taxable')),
|
||||
('uom', models.CharField(choices=[('EA', 'Each'), ('PR', 'Pair'), ('SET', 'Set'), ('GAL', 'Gallon'), ('L', 'Liter'), ('M', 'Meter'), ('KG', 'Kilogram'), ('HR', 'Hour'), ('BX', 'Box'), ('RL', 'Roll'), ('PKG', 'Package'), ('DZ', 'Dozen'), ('SQ_M', 'Square Meter'), ('PC', 'Piece'), ('BDL', 'Bundle')], max_length=10, verbose_name='Unit of Measurement')),
|
||||
('item', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='django_ledger.itemmodel', verbose_name='Item')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Additional Services',
|
||||
'verbose_name_plural': 'Additional Services',
|
||||
},
|
||||
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CarFinance',
|
||||
fields=[
|
||||
@ -141,7 +177,7 @@ class Migration(migrations.Migration):
|
||||
('cost_price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Cost Price')),
|
||||
('selling_price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Selling Price')),
|
||||
('discount_amount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=14, verbose_name='Discount Amount')),
|
||||
('additional_services', models.ManyToManyField(blank=True, related_name='additional_finances', to='django_ledger.itemmodel')),
|
||||
('additional_services', models.ManyToManyField(blank=True, related_name='additional_finances', to='inventory.additionalservices')),
|
||||
('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='finances', to='inventory.car')),
|
||||
],
|
||||
options={
|
||||
@ -325,26 +361,6 @@ class Migration(migrations.Migration):
|
||||
('objects', inventory.models.DealerUserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Customer',
|
||||
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')),
|
||||
('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')),
|
||||
('email', models.EmailField(max_length=254, unique=True, verbose_name='Email')),
|
||||
('national_id', models.CharField(max_length=10, 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')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('is_lead', models.BooleanField(default=False, verbose_name='Is Lead')),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='customers', to='inventory.dealer')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Customer',
|
||||
'verbose_name_plural': 'Customers',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CarLocation',
|
||||
fields=[
|
||||
@ -366,23 +382,80 @@ class Migration(migrations.Migration):
|
||||
name='dealer',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='cars', to='inventory.dealer', verbose_name='Dealer'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='additionalservices',
|
||||
name='dealer',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.dealer', verbose_name='Dealer'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AdditionalServices',
|
||||
name='Lead',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Name')),
|
||||
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
|
||||
('description', models.TextField(verbose_name='Description')),
|
||||
('price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Price')),
|
||||
('taxable', models.BooleanField(default=False, verbose_name='taxable')),
|
||||
('uom', models.CharField(choices=[('EA', 'Each'), ('PR', 'Pair'), ('SET', 'Set'), ('GAL', 'Gallon'), ('L', 'Liter'), ('M', 'Meter'), ('KG', 'Kilogram'), ('HR', 'Hour'), ('BX', 'Box'), ('RL', 'Roll'), ('PKG', 'Package'), ('DZ', 'Dozen'), ('SQ_M', 'Square Meter'), ('PC', 'Piece'), ('BDL', 'Bundle')], max_length=10, verbose_name='Unit of Measurement')),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.dealer', verbose_name='Dealer')),
|
||||
('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')], max_length=20, verbose_name='Title')),
|
||||
('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(db_index=True, max_length=254, unique=True, verbose_name='Email')),
|
||||
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
|
||||
('salary', models.PositiveIntegerField(blank=True, null=True, verbose_name='Salary')),
|
||||
('obligations', models.PositiveIntegerField(blank=True, null=True, verbose_name='Obligations')),
|
||||
('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')),
|
||||
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')),
|
||||
('city', models.CharField(max_length=50, verbose_name='City')),
|
||||
('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'), ('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')),
|
||||
('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': 'Additional Services',
|
||||
'verbose_name_plural': 'Additional Services',
|
||||
'verbose_name': 'Lead',
|
||||
'verbose_name_plural': 'Leads',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Customer',
|
||||
fields=[
|
||||
('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')),
|
||||
('email', models.EmailField(max_length=254, unique=True, verbose_name='Email')),
|
||||
('national_id', models.CharField(max_length=10, unique=True, verbose_name='National ID')),
|
||||
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', unique=True, verbose_name='Phone Number')),
|
||||
('city', models.CharField(blank=True, max_length=255, verbose_name='City')),
|
||||
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='customers', to='inventory.dealer')),
|
||||
('lead', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='converted', to='inventory.lead', verbose_name='Lead')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Customer',
|
||||
'verbose_name_plural': 'Customers',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Notes',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('object_id', models.PositiveIntegerField()),
|
||||
('note', models.TextField(verbose_name='Note')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
||||
('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='notes_created', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Note',
|
||||
'verbose_name_plural': 'Notes',
|
||||
},
|
||||
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Notification',
|
||||
@ -390,46 +463,13 @@ class Migration(migrations.Migration):
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('message', models.CharField(max_length=255, verbose_name='Message')),
|
||||
('is_read', models.BooleanField(default=False, verbose_name='Is Read')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Notification',
|
||||
'verbose_name_plural': 'Notifications',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Opportunity',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('deal_name', models.CharField(max_length=255, verbose_name='Deal Name')),
|
||||
('deal_value', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Deal Value')),
|
||||
('deal_status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('canceled', 'Canceled'), ('completed', 'Completed')], default='new', max_length=20, verbose_name='Deal Status')),
|
||||
('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')], default='low', max_length=10, verbose_name='Priority')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
|
||||
('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='inventory.customer')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Opportunity',
|
||||
'verbose_name_plural': 'Opportunities',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Notes',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('note', models.TextField(verbose_name='Note')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='notes_created', to=settings.AUTH_USER_MODEL)),
|
||||
('opportunity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='inventory.opportunity')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Notes',
|
||||
'verbose_name_plural': 'Notes',
|
||||
'ordering': ['-created'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
@ -540,8 +580,8 @@ class Migration(migrations.Migration):
|
||||
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
|
||||
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
|
||||
('staff_type', models.CharField(choices=[('manager', 'Manager'), ('inventory', 'Inventory'), ('accountant', 'Accountant'), ('sales', 'Sales'), ('coordinator', 'Coordinator'), ('receptionist', 'Receptionist'), ('agent', 'Agent')], max_length=255, verbose_name='Staff Type')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='staff', to='inventory.dealer')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='staff', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
@ -551,29 +591,55 @@ class Migration(migrations.Migration):
|
||||
'permissions': [],
|
||||
},
|
||||
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
|
||||
managers=[
|
||||
('objects', inventory.models.StaffUserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OpportunityLog',
|
||||
name='Opportunity',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('action', models.CharField(choices=[('create', 'Create'), ('update', 'Update'), ('delete', 'Delete'), ('status_change', 'Status Change')], max_length=50, verbose_name='Action')),
|
||||
('old_status', models.CharField(blank=True, choices=[('new', 'New'), ('pending', 'Pending'), ('canceled', 'Canceled'), ('completed', 'Completed')], max_length=50, null=True, verbose_name='Old Status')),
|
||||
('new_status', models.CharField(blank=True, choices=[('new', 'New'), ('pending', 'Pending'), ('canceled', 'Canceled'), ('completed', 'Completed')], max_length=50, null=True, verbose_name='New Status')),
|
||||
('details', models.TextField(blank=True, null=True, verbose_name='Details')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('opportunity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='inventory.opportunity')),
|
||||
('staff', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.staff', verbose_name='Staff')),
|
||||
('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'), ('canceled', 'Canceled')], default='new', max_length=20, verbose_name='Status')),
|
||||
('probability', models.PositiveIntegerField(validators=[inventory.models.validate_probability])),
|
||||
('closing_date', models.DateField(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='inventory.customer')),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='inventory.dealer')),
|
||||
('staff', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owner', to='inventory.staff', verbose_name='Owner')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Log',
|
||||
'verbose_name_plural': 'Logs',
|
||||
'ordering': ['-created_at'],
|
||||
'verbose_name': 'Opportunity',
|
||||
'verbose_name_plural': 'Opportunities',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
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'), ('canceled', 'Canceled')], max_length=50, verbose_name='Old Status')),
|
||||
('new_status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], 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')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Lead Status History',
|
||||
'verbose_name_plural': 'Lead Status Histories',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deals_created', to='inventory.staff'),
|
||||
model_name='lead',
|
||||
name='assigned',
|
||||
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.AddField(
|
||||
model_name='customer',
|
||||
name='staff',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customer_staff', to='inventory.staff', verbose_name='Staff'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Subscription',
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
# Generated by Django 4.2.17 on 2025-01-08 08:42
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('django_ledger', '0017_alter_accountmodel_unique_together_and_more'),
|
||||
('inventory', '0003_alter_caroptionvalue_is_base'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='additionalservices',
|
||||
name='item',
|
||||
field=models.OneToOneField(default='', on_delete=django.db.models.deletion.CASCADE, to='django_ledger.itemmodel', verbose_name='Item'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='carfinance',
|
||||
name='additional_services',
|
||||
field=models.ManyToManyField(blank=True, related_name='additional_finances', to='inventory.additionalservices'),
|
||||
),
|
||||
]
|
||||
@ -1,20 +0,0 @@
|
||||
# Generated by Django 4.2.17 on 2025-01-08 08:43
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('django_ledger', '0017_alter_accountmodel_unique_together_and_more'),
|
||||
('inventory', '0004_additionalservices_item_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='additionalservices',
|
||||
name='item',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='django_ledger.itemmodel', verbose_name='Item'),
|
||||
),
|
||||
]
|
||||
@ -1,59 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-08 19:03
|
||||
|
||||
import django.utils.timezone
|
||||
import django_countries.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0005_alter_additionalservices_item'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='customer',
|
||||
name='is_lead',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='dob',
|
||||
field=models.DateField(default=django.utils.timezone.now, verbose_name='Date of Birth'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='gender',
|
||||
field=models.CharField(choices=[('m', 'Male'), ('f', 'Female')], default='m', max_length=1, verbose_name='Gender'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='nationality',
|
||||
field=django_countries.fields.CountryField(blank=True, max_length=2, verbose_name='Nationality'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='obligations',
|
||||
field=models.PositiveIntegerField(default=1000, verbose_name='Obligations'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='salary',
|
||||
field=models.PositiveIntegerField(default=10000, verbose_name='Salary'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='title',
|
||||
field=models.CharField(choices=[('mr', 'Mr'), ('mrs', 'Mrs'), ('ms', 'Ms'), ('miss', 'Miss'), ('dr', 'Dr'), ('prof', 'Prof'), ('prince', 'Prince'), ('princess', 'Princess'), ('company', 'Company')], default='mr', max_length=10, verbose_name='Title'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='updated',
|
||||
field=models.DateTimeField(auto_now=True, verbose_name='Updated'),
|
||||
),
|
||||
]
|
||||
@ -1,43 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-09 05:46
|
||||
|
||||
import django_countries.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0006_remove_customer_is_lead_customer_dob_customer_gender_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='customer',
|
||||
name='nationality',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='country',
|
||||
field=django_countries.fields.CountryField(blank=True, max_length=2, verbose_name='Country'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customer',
|
||||
name='title',
|
||||
field=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'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='opportunity',
|
||||
name='deal_status',
|
||||
field=models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('canceled', 'Canceled'), ('lost', 'Lost'), ('won', 'Won')], default='new', max_length=20, verbose_name='Deal Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='opportunitylog',
|
||||
name='new_status',
|
||||
field=models.CharField(blank=True, choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('canceled', 'Canceled'), ('lost', 'Lost'), ('won', 'Won')], max_length=50, null=True, verbose_name='New Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='opportunitylog',
|
||||
name='old_status',
|
||||
field=models.CharField(blank=True, choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('canceled', 'Canceled'), ('lost', 'Lost'), ('won', 'Won')], max_length=50, null=True, verbose_name='Old Status'),
|
||||
),
|
||||
]
|
||||
@ -1,224 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-09 09:19
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('inventory', '0007_remove_customer_nationality_customer_country_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='notes',
|
||||
options={'verbose_name': 'Note', 'verbose_name_plural': 'Notes'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='notification',
|
||||
options={'ordering': ['-created'], 'verbose_name': 'Notification', 'verbose_name_plural': 'Notifications'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='customer',
|
||||
name='obligations',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='customer',
|
||||
name='salary',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='notes',
|
||||
name='created_at',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='notes',
|
||||
name='opportunity',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='notes',
|
||||
name='updated_at',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='notification',
|
||||
name='created_at',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='opportunity',
|
||||
name='created_at',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='opportunity',
|
||||
name='created_by',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='opportunity',
|
||||
name='deal_name',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='opportunity',
|
||||
name='deal_status',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='opportunity',
|
||||
name='deal_value',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='opportunity',
|
||||
name='priority',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='opportunity',
|
||||
name='updated_at',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='staff',
|
||||
name='created_at',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='staff',
|
||||
name='updated_at',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='notes',
|
||||
name='content_type',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='notes',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='notes',
|
||||
name='object_id',
|
||||
field=models.PositiveIntegerField(default=1),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='notes',
|
||||
name='updated',
|
||||
field=models.DateTimeField(auto_now=True, verbose_name='Updated'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='notification',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='dealer',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='inventory.dealer'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='staff',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owner', to='inventory.staff', verbose_name='Owner'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='stage',
|
||||
field=models.CharField(choices=[('prospect', 'Prospect'), ('proposal', 'Proposal'), ('negotiation', 'Negotiation'), ('closed_won', 'Closed Won'), ('closed_lost', 'Closed Lost')], default='prospect', max_length=20, verbose_name='Stage'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='updated',
|
||||
field=models.DateTimeField(auto_now=True, verbose_name='Updated'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='staff',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='staff',
|
||||
name='updated',
|
||||
field=models.DateTimeField(auto_now=True, verbose_name='Updated'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notes',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='notes_created', to='inventory.staff'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Activity',
|
||||
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'), ('reserve_car', 'Reserve 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')),
|
||||
('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='activities_created', to='inventory.staff')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Activity',
|
||||
'verbose_name_plural': 'Activities',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Lead',
|
||||
fields=[
|
||||
('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')], max_length=20, verbose_name='Title')),
|
||||
('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(db_index=True, max_length=254, unique=True, verbose_name='Email')),
|
||||
('salary', models.PositiveIntegerField(blank=True, null=True, verbose_name='Salary')),
|
||||
('obligations', models.PositiveIntegerField(blank=True, null=True, verbose_name='Obligations')),
|
||||
('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')),
|
||||
('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')], default='low', max_length=10, verbose_name='Priority')),
|
||||
('status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('assigned', 'Assigned'), ('in_progress', 'In Progress'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], db_index=True, 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')),
|
||||
('assigned', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned', to='inventory.staff', verbose_name='Assigned')),
|
||||
('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.AddField(
|
||||
model_name='customer',
|
||||
name='lead',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='converted', to='inventory.lead', verbose_name='Lead'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
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'), ('assigned', 'Assigned'), ('in_progress', 'In Progress'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], max_length=50, verbose_name='Old Status')),
|
||||
('new_status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('assigned', 'Assigned'), ('in_progress', 'In Progress'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], max_length=50, verbose_name='New Status')),
|
||||
('changed_at', models.DateTimeField(auto_now_add=True, verbose_name='Changed At')),
|
||||
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='status_changes', to='inventory.staff')),
|
||||
('lead', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='status_history', to='inventory.lead')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Lead Status History',
|
||||
'verbose_name_plural': 'Lead Status Histories',
|
||||
},
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='OpportunityLog',
|
||||
),
|
||||
]
|
||||
@ -1,20 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-09 09:57
|
||||
|
||||
import inventory.models
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0008_alter_notes_options_alter_notification_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelManagers(
|
||||
name='staff',
|
||||
managers=[
|
||||
('objects', inventory.models.StaffUserManager()),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-09 11:36
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0009_alter_staff_managers'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='staff',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customer_staff', to='inventory.staff', verbose_name='Staff'),
|
||||
),
|
||||
]
|
||||
@ -1,22 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-09 20:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0010_customer_staff'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='customer',
|
||||
name='country',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='city',
|
||||
field=models.CharField(blank=True, max_length=255, verbose_name='City'),
|
||||
),
|
||||
]
|
||||
@ -1,20 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-11 10:32
|
||||
|
||||
import inventory.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0011_remove_customer_country_customer_city'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='probability',
|
||||
field=models.PositiveIntegerField(default=70, validators=[inventory.models.validate_probability]),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@ -1,20 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-11 11:09
|
||||
|
||||
import phonenumber_field.modelfields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0012_opportunity_probability'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='lead',
|
||||
name='phone_number',
|
||||
field=phonenumber_field.modelfields.PhoneNumberField(default='0535521547', max_length=128, region='SA', verbose_name='Phone Number'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@ -1,26 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-11 12:12
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0013_lead_phone_number'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='activity',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='activities_created', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notes',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='notes_created', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-11 12:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0014_alter_activity_created_by_alter_notes_created_by'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='lead',
|
||||
name='city',
|
||||
field=models.CharField(default='Riyadh', max_length=50, verbose_name='City'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-11 12:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0015_lead_city'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='lead',
|
||||
name='address',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Address'),
|
||||
),
|
||||
]
|
||||
@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-11 19:20
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0016_lead_address'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='lead',
|
||||
name='assigned',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned', to='inventory.staff', verbose_name='Assigned'),
|
||||
),
|
||||
]
|
||||
@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-11 23:36
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0017_alter_lead_assigned'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='lead',
|
||||
name='priority',
|
||||
field=models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')], default='medium', max_length=10, verbose_name='Priority'),
|
||||
),
|
||||
]
|
||||
@ -1,45 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-12 01:43
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0018_alter_lead_priority'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='closed',
|
||||
field=models.BooleanField(default=False, verbose_name='Closed'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='closing_date',
|
||||
field=models.DateField(default=django.utils.timezone.now, verbose_name='Closing Date'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='opportunity',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], default='new', max_length=20, verbose_name='Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='lead',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], 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'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], max_length=50, verbose_name='New Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='leadstatushistory',
|
||||
name='old_status',
|
||||
field=models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], max_length=50, verbose_name='Old Status'),
|
||||
),
|
||||
]
|
||||
@ -75,6 +75,22 @@ class VatRate(models.Model):
|
||||
def __str__(self):
|
||||
return f"Rate: {self.rate}%"
|
||||
|
||||
class CarType(models.IntegerChoices):
|
||||
CAR = 1, _('Car')
|
||||
LIGHT_COMMERCIAL = 2, _('Light Commercial')
|
||||
HEAVY_DUTY_TRACTORS = 3, _('Heavy-Duty Tractors')
|
||||
TRAILERS = 4, _('Trailers')
|
||||
MEDIUM_TRUCKS = 5, _('Medium Trucks')
|
||||
BUSES = 6, _('Buses')
|
||||
MOTORCYCLES = 20, _('Motorcycles')
|
||||
BUGGY = 21, _('Buggy')
|
||||
MOTO_ATV = 22, _('Moto ATV')
|
||||
SCOOTERS = 23, _('Scooters')
|
||||
KARTING = 24, _('Karting')
|
||||
ATV = 25, _('ATV')
|
||||
SNOWMOBILES = 26, _('Snowmobiles')
|
||||
|
||||
|
||||
|
||||
class CarMake(models.Model, LocalizedNameMixin):
|
||||
id_car_make = models.AutoField(primary_key=True)
|
||||
|
||||
@ -78,9 +78,11 @@ urlpatterns = [
|
||||
path('cars/finance/<int:pk>/update/', 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/get-car-models/', views.get_car_models, name='get_car_models'),
|
||||
path('cars/<int:car_pk>/add-color/', views.CarColorCreate.as_view(), name='add_color'),
|
||||
path('cars/<int:car_pk>/location/add/', views.CarLocationCreateView.as_view(), name='add_car_location'),
|
||||
path('cars/<int:pk>/location/update/', views.CarLocationUpdateView.as_view(), name='transfer'),
|
||||
path('cars/inventory/search/', 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>/', views.reserve_car_view, name='reserve_car'),
|
||||
@ -183,6 +185,6 @@ handler500 = 'inventory.views.custom_error_view'
|
||||
handler403 = 'inventory.views.custom_permission_denied_view'
|
||||
handler400 = 'inventory.views.custom_bad_request_view'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
from decimal import Decimal
|
||||
from django.core.paginator import Paginator
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django_ledger.models import (
|
||||
EntityModel,
|
||||
InvoiceModel,
|
||||
@ -81,6 +83,10 @@ from django.contrib.auth.models import User
|
||||
from allauth.account import views
|
||||
from django.db.models import Count, F, Value
|
||||
from django.contrib.auth import authenticate
|
||||
import cv2
|
||||
import numpy as np
|
||||
from pyzbar.pyzbar import decode
|
||||
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -427,6 +433,38 @@ class AjaxHandlerView(LoginRequiredMixin, View):
|
||||
return JsonResponse(serialized_options, safe=False)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class SearchCodeView(View):
|
||||
template_name = "inventory/scan_vin.html"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Render the form page."""
|
||||
return render(request, self.template_name)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
image_file = request.FILES.get('image')
|
||||
|
||||
if image_file:
|
||||
print("image received!")
|
||||
image = cv2.imdecode(np.frombuffer(image_file.read(), np.uint8), cv2.IMREAD_COLOR)
|
||||
decoded_objects = decode(image)
|
||||
if decoded_objects:
|
||||
print("image decoded!")
|
||||
print(decoded_objects[0])
|
||||
code = decoded_objects[0].data.decode('utf-8')
|
||||
print("code received!")
|
||||
print(code)
|
||||
car = get_object_or_404(models.Car, vin=code)
|
||||
name = car.id_car_make.get_local_name
|
||||
print(name)
|
||||
return redirect('car_detail', pk=car.pk)
|
||||
else:
|
||||
print("back to else statement")
|
||||
return JsonResponse({'success': False, 'error': 'No code detected'})
|
||||
else:
|
||||
return JsonResponse({'success': False, 'error': 'No image provided'})
|
||||
|
||||
|
||||
class CarInventory(LoginRequiredMixin, ListView):
|
||||
model = models.Car
|
||||
home_label = _("inventory")
|
||||
@ -594,9 +632,7 @@ class CarFinanceCreateView(LoginRequiredMixin, CreateView):
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
dealer = get_user_type(self.request)
|
||||
form.fields[
|
||||
"additional_finances"
|
||||
].queryset = models.AdditionalServices.objects.filter(dealer=dealer)
|
||||
form.fields["additional_finances"].queryset = models.AdditionalServices.objects.filter(dealer=dealer)
|
||||
return form
|
||||
|
||||
# def get_initial(self):
|
||||
@ -1702,17 +1738,16 @@ class BankAccountCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView)
|
||||
success_message = "Bank account created successfully."
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.entity_model = self.request.user.dealer.entity
|
||||
dealer = get_user_type(self.request)
|
||||
form.instance.entity_model = dealer.entity
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
"""
|
||||
Override this method to pass additional keyword arguments to the form.
|
||||
"""
|
||||
entity = self.request.user.dealer.entity
|
||||
dealer = get_user_type(self.request)
|
||||
entity = dealer.entity
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs["entity_slug"] = entity.slug # Get entity_slug from URL
|
||||
kwargs["user_model"] = entity.admin # Get user_model from the request
|
||||
kwargs["entity_slug"] = entity.slug
|
||||
kwargs["user_model"] = entity.admin
|
||||
return kwargs
|
||||
|
||||
|
||||
@ -1730,10 +1765,8 @@ class BankAccountUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView)
|
||||
success_message = "Bank account updated successfully."
|
||||
|
||||
def get_form_kwargs(self):
|
||||
"""
|
||||
Override this method to pass additional keyword arguments to the form.
|
||||
"""
|
||||
entity = self.request.user.dealer.entity
|
||||
dealer = get_user_type(self.request)
|
||||
entity = dealer.entity
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs["entity_slug"] = entity.slug # Get entity_slug from URL
|
||||
kwargs["user_model"] = entity.admin # Get user_model from the request
|
||||
@ -1764,7 +1797,8 @@ class AccountListView(LoginRequiredMixin, ListView):
|
||||
paginate_by = 10
|
||||
|
||||
def get_queryset(self):
|
||||
entity = self.request.user.dealer.entity
|
||||
dealer = get_user_type(self.request)
|
||||
entity = dealer.entity
|
||||
qs = entity.get_all_accounts()
|
||||
paginator = Paginator(qs, 20)
|
||||
page_number = self.request.GET.get("page", 1) # Default to page 1
|
||||
@ -2417,12 +2451,11 @@ class LeadDetailView(DetailView):
|
||||
|
||||
class LeadCreateView(CreateView, SuccessMessageMixin, LoginRequiredMixin):
|
||||
model = models.Lead
|
||||
form_class = forms.LeadCreateForm
|
||||
template_name = 'crm/leads/lead_create_form.html'
|
||||
success_message = "Lead created successfully!"
|
||||
form_class = forms.LeadForm
|
||||
template_name = 'crm/leads/lead_form.html'
|
||||
# success_message = "Lead created successfully!"
|
||||
success_url = reverse_lazy('lead_list')
|
||||
|
||||
|
||||
def form_valid(self, form):
|
||||
print("Form data:", form.cleaned_data) # Debug form data
|
||||
dealer = get_user_type(self.request)
|
||||
@ -2430,10 +2463,18 @@ class LeadCreateView(CreateView, SuccessMessageMixin, LoginRequiredMixin):
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
def get_car_models(request):
|
||||
make_id = request.GET.get('id_car_make')
|
||||
if make_id:
|
||||
car_models = models.CarModel.objects.filter(id_car_make=make_id).values('id_car_model', 'name', 'arabic_name')
|
||||
return JsonResponse(list(car_models), safe=False)
|
||||
return JsonResponse([], safe=False)
|
||||
|
||||
|
||||
class LeadUpdateView(UpdateView):
|
||||
model = models.Lead
|
||||
form_class = forms.LeadUpdateForm
|
||||
template_name = 'crm/leads/lead_update_form.html'
|
||||
form_class = forms.LeadForm
|
||||
template_name = 'crm/leads/lead_form.html'
|
||||
success_url = reverse_lazy('lead_list')
|
||||
|
||||
class LeadDeleteView(DeleteView):
|
||||
|
||||
@ -1,26 +1,43 @@
|
||||
aiohappyeyeballs==2.4.4
|
||||
aiohttp==3.11.11
|
||||
aiohttp-retry==2.8.3
|
||||
aiosignal==1.3.2
|
||||
alabaster==1.0.0
|
||||
albucore==0.0.13
|
||||
albumentations==1.4.10
|
||||
annotated-types==0.7.0
|
||||
anyio==4.6.2.post1
|
||||
arabic-reshaper==3.0.0
|
||||
asgiref==3.8.1
|
||||
astor==0.8.1
|
||||
astroid==3.3.5
|
||||
attrs==23.2.0
|
||||
autopep8==2.3.1
|
||||
babel==2.16.0
|
||||
beautifulsoup4==4.12.3
|
||||
bleach==6.2.0
|
||||
blinker==1.9.0
|
||||
Brotli==1.1.0
|
||||
certifi==2024.8.30
|
||||
cffi==1.17.1
|
||||
chardet==5.2.0
|
||||
charset-normalizer==3.4.0
|
||||
click==8.1.7
|
||||
click==8.1.8
|
||||
colorama==0.4.6
|
||||
commonmark==0.9.1
|
||||
contourpy==1.3.1
|
||||
crispy-bootstrap5==2024.10
|
||||
cryptography==44.0.0
|
||||
cssselect2==0.7.0
|
||||
cycler==0.12.1
|
||||
Cython==3.0.11
|
||||
decorator==5.1.1
|
||||
desert==2020.11.18
|
||||
dill==0.3.9
|
||||
distro==1.9.0
|
||||
dj-rest-auth==7.0.0
|
||||
dj-shop-cart==7.1.1
|
||||
Django==4.2.17
|
||||
Django==5.1.4
|
||||
django-allauth==65.3.0
|
||||
django-autoslug==1.9.9
|
||||
django-bootstrap5==24.3
|
||||
@ -34,10 +51,13 @@ django-filter==24.3
|
||||
django-formtools==2.5.1
|
||||
django-ledger==0.7.0
|
||||
django-money==3.5.3
|
||||
django-nine==0.2.7
|
||||
django-nonefield==0.4
|
||||
django-phonenumber-field==8.0.0
|
||||
django-prometheus==2.3.1
|
||||
django-sekizai==4.1.0
|
||||
django-silk==5.3.1
|
||||
django-sms==0.7.0
|
||||
django-sslserver==0.22
|
||||
django-tables2==2.7.0
|
||||
django-treebeard==4.7.1
|
||||
@ -47,9 +67,13 @@ djangorestframework==3.15.2
|
||||
djangorestframework-simplejwt==5.3.1
|
||||
djangoviz==0.1.1
|
||||
docutils==0.21.2
|
||||
easy-thumbnails==2.10
|
||||
et_xmlfile==2.0.0
|
||||
Faker==33.1.0
|
||||
fire==0.7.0
|
||||
Flask==3.1.0
|
||||
fonttools==4.55.3
|
||||
frozenlist==1.5.0
|
||||
gprof2dot==2024.6.6
|
||||
graphqlclient==0.2.4
|
||||
greenlet==3.1.1
|
||||
@ -57,80 +81,143 @@ h11==0.14.0
|
||||
httpcore==1.0.7
|
||||
httpx==0.28.0
|
||||
idna==3.10
|
||||
imageio==2.36.1
|
||||
imagesize==1.4.1
|
||||
imgaug==0.4.0
|
||||
iso4217==1.12.20240625
|
||||
isodate==0.7.2
|
||||
isort==5.13.2
|
||||
itsdangerous==2.2.0
|
||||
Jinja2==3.1.4
|
||||
jiter==0.8.0
|
||||
joblib==1.4.2
|
||||
kiwisolver==1.4.8
|
||||
lazy_loader==0.4
|
||||
ledger==1.0.1
|
||||
lmdb==1.6.2
|
||||
lxml==5.3.0
|
||||
Markdown==3.7
|
||||
markdown-it-py==3.0.0
|
||||
MarkupSafe==3.0.2
|
||||
marshmallow==3.23.2
|
||||
matplotlib==3.10.0
|
||||
mccabe==0.7.0
|
||||
mdurl==0.1.2
|
||||
MouseInfo==0.1.3
|
||||
multidict==6.1.0
|
||||
mypy-extensions==1.0.0
|
||||
networkx==3.4.2
|
||||
newrelic==10.3.1
|
||||
nltk==3.9.1
|
||||
numpy==1.26.4
|
||||
oauthlib==3.2.2
|
||||
ofxtools==0.9.5
|
||||
openai==1.56.2
|
||||
opencv-contrib-python==4.10.0.84
|
||||
opencv-python==4.10.0.84
|
||||
opencv-python-headless==4.10.0.84
|
||||
openpyxl==3.1.5
|
||||
opt-einsum==3.3.0
|
||||
outcome==1.3.0.post0
|
||||
packaging==24.2
|
||||
pandas==2.2.3
|
||||
pango==0.0.1
|
||||
pdfkit==1.0.0
|
||||
phonenumbers==8.13.51
|
||||
pillow==11.0.0
|
||||
platformdirs==4.3.6
|
||||
prometheus_client==0.21.1
|
||||
propcache==0.2.1
|
||||
protobuf==5.29.3
|
||||
psycopg==3.2.3
|
||||
psycopg-binary==3.2.3
|
||||
psycopg-c==3.2.3
|
||||
py-moneyed==3.0
|
||||
PyAutoGUI==0.9.54
|
||||
pyclipper==1.3.0.post6
|
||||
pycodestyle==2.12.1
|
||||
pycparser==2.22
|
||||
pydantic==2.10.3
|
||||
pydantic_core==2.27.1
|
||||
pydotplus==2.0.2
|
||||
pydyf==0.11.0
|
||||
PyGetWindow==0.0.9
|
||||
Pygments==2.18.0
|
||||
PyJWT==2.10.1
|
||||
pylint==3.3.2
|
||||
PyMsgBox==1.0.9
|
||||
PyMySQL==1.1.1
|
||||
pyobjc-core==10.3.2
|
||||
pyobjc-framework-Cocoa==10.3.2
|
||||
pyobjc-framework-Quartz==10.3.2
|
||||
pyparsing==3.2.0
|
||||
pyperclip==1.9.0
|
||||
pyphen==0.17.0
|
||||
pypng==0.20220715.0
|
||||
PyRect==0.2.0
|
||||
PyScreeze==1.0.1
|
||||
pyserial==3.5
|
||||
PySocks==1.7.1
|
||||
python-bidi==0.6.3
|
||||
python-dateutil==2.9.0.post0
|
||||
python-docx==1.1.2
|
||||
python-openid==2.2.5
|
||||
python3-saml==1.16.0
|
||||
pytweening==1.2.0
|
||||
pytz==2024.2
|
||||
pyvin==0.0.2
|
||||
pywa==2.4.0
|
||||
pywhat==5.1.0
|
||||
pywhatkit==5.4
|
||||
PyYAML==6.0.2
|
||||
qrcode==8.0
|
||||
RapidFuzz==3.11.0
|
||||
regex==2024.11.6
|
||||
reportlab==4.2.5
|
||||
requests==2.32.3
|
||||
requests-oauthlib==2.0.0
|
||||
rich==13.9.4
|
||||
setuptools==75.6.0
|
||||
rubicon-objc==0.4.9
|
||||
scikit-image==0.25.0
|
||||
scikit-learn==1.6.0
|
||||
scipy==1.14.1
|
||||
selenium==4.27.1
|
||||
shapely==2.0.6
|
||||
six==1.16.0
|
||||
sniffio==1.3.1
|
||||
snowballstemmer==2.2.0
|
||||
sortedcontainers==2.4.0
|
||||
soupsieve==2.6
|
||||
SQLAlchemy==2.0.36
|
||||
sqlparse==0.5.2
|
||||
tablib==3.7.0
|
||||
termcolor==2.5.0
|
||||
threadpoolctl==3.5.0
|
||||
tifffile==2025.1.10
|
||||
tinycss2==1.4.0
|
||||
tinyhtml5==2.0.0
|
||||
tomli==2.2.1
|
||||
tomlkit==0.13.2
|
||||
tqdm==4.67.1
|
||||
trio==0.28.0
|
||||
trio-websocket==0.11.1
|
||||
twilio==9.4.1
|
||||
typing-inspect==0.9.0
|
||||
typing_extensions==4.12.2
|
||||
tzdata==2024.2
|
||||
Unidecode==1.3.8
|
||||
upgrade-requirements==1.7.0
|
||||
urllib3==2.2.3
|
||||
vin==0.6.2
|
||||
vininfo==1.8.0
|
||||
vishap==0.1.5
|
||||
vpic-api==0.7.4
|
||||
weasyprint==63.1
|
||||
webencodings==0.5.1
|
||||
websocket-client==1.8.0
|
||||
Werkzeug==3.1.3
|
||||
wikipedia==1.4.0
|
||||
wsproto==1.2.0
|
||||
xmlsec==1.3.14
|
||||
yarl==1.18.3
|
||||
zopfli==0.2.3.post1
|
||||
|
||||
@ -1,388 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row g-3">
|
||||
<div class="col-sm-12 col-md-8">
|
||||
<h2>
|
||||
{% if form.instance.pk %}
|
||||
{{ _("Edit Lead") }}
|
||||
{% else %}
|
||||
{{ _("Add New Lead") }}
|
||||
{% endif %}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-md-8">
|
||||
<!-- The 'novalidate' attribute is removed to allow HTML5 validation.
|
||||
If you prefer manual validation, you can add 'novalidate'. -->
|
||||
<form class="row g-3 form needs-validation" method="post" action="{% url 'lead_create' %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Title -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<select
|
||||
class="form-control"
|
||||
id="{{ form.title.id_for_label }}"
|
||||
name="{{ form.title.name }}"
|
||||
aria-label="{{ _('Title') }}"
|
||||
required
|
||||
>
|
||||
<option value="">{{ _('Select Title') }}</option>
|
||||
{% for value, label in form.title.field.choices %}
|
||||
<option value="{{ value }}" {% if form.title.value == value %}selected{% endif %}>
|
||||
{{ label }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label for="{{ form.title.id_for_label }}">{{ _("Title") }}</label>
|
||||
</div>
|
||||
<span class="invalid-feedback">{{ form.title.errors }}</span>
|
||||
</div>
|
||||
|
||||
<!-- First Name -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="{{ form.first_name.id_for_label }}"
|
||||
name="{{ form.first_name.name }}"
|
||||
value="{{ form.first_name.value|default:'' }}"
|
||||
placeholder="{{ _('Enter first name') }}"
|
||||
aria-label="{{ _('First Name') }}"
|
||||
required
|
||||
>
|
||||
<label for="{{ form.first_name.id_for_label }}">{{ _("First Name") }}</label>
|
||||
</div>
|
||||
<span class="invalid-feedback">{{ form.first_name.errors }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Last Name -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="{{ form.last_name.id_for_label }}"
|
||||
name="{{ form.last_name.name }}"
|
||||
value="{{ form.last_name.value|default:'' }}"
|
||||
placeholder="{{ _('Enter last name') }}"
|
||||
aria-label="{{ _('Last Name') }}"
|
||||
required
|
||||
>
|
||||
<label for="{{ form.last_name.id_for_label }}">{{ _("Last Name") }}</label>
|
||||
</div>
|
||||
<span class="invalid-feedback">{{ form.last_name.errors }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<input
|
||||
type="email"
|
||||
class="form-control"
|
||||
id="{{ form.email.id_for_label }}"
|
||||
name="{{ form.email.name }}"
|
||||
value="{{ form.email.value|default:'' }}"
|
||||
placeholder="{{ _('Enter email') }}"
|
||||
aria-label="{{ _('Email') }}"
|
||||
required
|
||||
>
|
||||
<label for="{{ form.email.id_for_label }}">{{ _("Email") }}</label>
|
||||
</div>
|
||||
<span class="invalid-feedback">{{ form.email.errors }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Phone Number -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="{{ form.phone_number.id_for_label }}"
|
||||
name="{{ form.phone_number.name }}"
|
||||
value="{{ form.phone_number.value|default:'' }}"
|
||||
placeholder="{{ _('Enter phone number') }}"
|
||||
aria-label="{{ _('Phone Number') }}"
|
||||
pattern="^[+]?[\d\s-]{5,}$"
|
||||
required>
|
||||
<label for="{{ form.phone_number.id_for_label }}">{{ _("Phone Number") }}</label>
|
||||
</div>
|
||||
<span class="invalid-feedback">{{ form.phone_number.errors }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Salary -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
id="{{ form.salary.id_for_label }}"
|
||||
name="{{ form.salary.name }}"
|
||||
value="{{ form.salary.value|default:'' }}"
|
||||
placeholder="{{ _('Enter salary') }}"
|
||||
aria-label="{{ _('Salary') }}"
|
||||
required
|
||||
>
|
||||
<label for="{{ form.salary.id_for_label }}">{{ _("Salary") }}</label>
|
||||
</div>
|
||||
<span class="invalid-feedback">{{ form.salary.errors }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Obligations -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
id="{{ form.obligations.id_for_label }}"
|
||||
name="{{ form.obligations.name }}"
|
||||
value="{{ form.obligations.value|default:'' }}"
|
||||
placeholder="{{ _('Enter obligations') }}"
|
||||
aria-label="{{ _('Obligations') }}"
|
||||
required
|
||||
>
|
||||
<label for="{{ form.obligations.id_for_label }}">{{ _("Obligations") }}</label>
|
||||
</div>
|
||||
<span class="invalid-feedback">{{ form.obligations.errors }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Car Make -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<select
|
||||
class="form-select"
|
||||
id="{{ form.id_car_make.id_for_label }}"
|
||||
name="{{ form.id_car_make.name }}"
|
||||
aria-label="{{ _('Car Make') }}">
|
||||
<option value="">{{ _('Select Make') }}</option>
|
||||
{% for value, label in form.id_car_make.field.choices %}
|
||||
<option value="{{ value }}" {% if form.id_car_make.value == value %}selected{% endif %}>
|
||||
{{ label }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label for="{{ form.id_car_make.id_for_label }}">{{ _("Car Make") }}</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Car Model -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<select
|
||||
class="form-select"
|
||||
id="{{ form.id_car_model.id_for_label }}"
|
||||
name="{{ form.id_car_model.name }}"
|
||||
aria-label="{{ _('Car Model') }}">
|
||||
<option value="">{{ _('Select Model') }}</option>
|
||||
{% for value, label in form.id_car_model.field.choices %}
|
||||
<option value="{{ value }}" {% if form.id_car_model.value == value %}selected{% endif %}>
|
||||
{{ label }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label for="{{ form.id_car_model.id_for_label }}">{{ _("Car Model") }}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Year -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
id="{{ form.year.id_for_label }}"
|
||||
name="{{ form.year.name }}"
|
||||
value="{{ form.year.value|default:'' }}"
|
||||
placeholder="{{ _('Enter year') }}"
|
||||
aria-label="{{ _('Year') }}"
|
||||
required
|
||||
>
|
||||
<label for="{{ form.year.id_for_label }}">{{ _("Year") }}</label>
|
||||
</div>
|
||||
<span class="invalid-feedback">{{ form.year.errors }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Source -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<select
|
||||
class="form-control"
|
||||
id="{{ form.source.id_for_label }}"
|
||||
name="{{ form.source.name }}"
|
||||
aria-label="{{ _('Source') }}"
|
||||
required
|
||||
>
|
||||
<option value="">{{ _('Select Source') }}</option>
|
||||
{% for value, label in form.source.field.choices %}
|
||||
<option value="{{ value }}" {% if form.source.value == value %}selected{% endif %}>
|
||||
{{ label }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label for="{{ form.source.id_for_label }}">{{ _("Source") }}</label>
|
||||
</div>
|
||||
<span class="invalid-feedback">{{ form.source.errors }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Channel -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<select
|
||||
class="form-control"
|
||||
id="{{ form.channel.id_for_label }}"
|
||||
name="{{ form.channel.name }}"
|
||||
aria-label="{{ _('Channel') }}"
|
||||
required
|
||||
>
|
||||
<option value="">{{ _('Select Channel') }}</option>
|
||||
{% for value, label in form.channel.field.choices %}
|
||||
<option value="{{ value }}" {% if form.channel.value == value %}selected{% endif %}>
|
||||
{{ label }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label for="{{ form.channel.id_for_label }}">{{ _("Channel") }}</label>
|
||||
</div>
|
||||
<span class="invalid-feedback">{{ form.channel.errors }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Assigned -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<select
|
||||
class="form-control"
|
||||
id="{{ form.assigned.id_for_label }}"
|
||||
name="{{ form.assigned.name }}"
|
||||
aria-label="{{ _('Assigned To') }}"
|
||||
required
|
||||
>
|
||||
<option value="">{{ _('Select Assignee') }}</option>
|
||||
{% for value, label in form.assigned.field.choices %}
|
||||
<option value="{{ value }}" {% if form.assigned.value == value %}selected{% endif %}>
|
||||
{{ label }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label for="{{ form.assigned.id_for_label }}">{{ _("Assigned To") }}</label>
|
||||
</div>
|
||||
<span class="invalid-feedback">{{ form.assigned.errors }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Priority -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<select
|
||||
class="form-control"
|
||||
id="{{ form.priority.id_for_label }}"
|
||||
name="{{ form.priority.name }}"
|
||||
aria-label="{{ _('Priority') }}"
|
||||
required
|
||||
>
|
||||
<option value="">{{ _('Select Priority') }}</option>
|
||||
{% for value, label in form.priority.field.choices %}
|
||||
<option value="{{ value }}" {% if form.priority.value == value %}selected{% endif %}>
|
||||
{{ label }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label for="{{ form.priority.id_for_label }}">{{ _("Priority") }}</label>
|
||||
</div>
|
||||
<span class="invalid-feedback">{{ form.priority.errors }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="submit"
|
||||
name="add_lead"
|
||||
id="lead-save"
|
||||
class="btn btn-phoenix-primary"
|
||||
>
|
||||
{{ _("Save") }}
|
||||
</button>
|
||||
<a href="{% url 'lead_list' %}" class="btn btn-phoenix-secondary">
|
||||
{{ _("Cancel") }}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Reusable function to get the CSRF cookie
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== "") {
|
||||
const cookies = document.cookie.split(";");
|
||||
for (let cookie of cookies) {
|
||||
cookie = cookie.trim();
|
||||
if (cookie.substring(0, name.length + 1) === name + "=") {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const ajaxUrl = "{% url 'ajax_handler' %}";
|
||||
const makeSelect = document.getElementById("{{ form.id_car_make.id_for_label }}");
|
||||
const modelSelect = document.getElementById("{{ form.id_car_model.id_for_label }}");
|
||||
const csrfToken = getCookie("csrftoken");
|
||||
|
||||
// Helper function to reset the dropdown
|
||||
function resetDropdown(dropdown, placeholder) {
|
||||
dropdown.innerHTML = `<option value="">${placeholder}</option>`;
|
||||
}
|
||||
|
||||
// Load car models based on make
|
||||
async function loadModels(makeId) {
|
||||
// Show a temporary loading message
|
||||
resetDropdown(modelSelect, "{{ _('Loading...') }}");
|
||||
try {
|
||||
const response = await fetch(`${ajaxUrl}?action=get_models&make_id=${makeId}`, {
|
||||
headers: {
|
||||
"X-CSRFToken": csrfToken,
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
// Reset dropdown before populating
|
||||
resetDropdown(modelSelect, "{{ _('Select Model') }}");
|
||||
data.forEach((model) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = model.id_car_model;
|
||||
// Display name based on language
|
||||
option.textContent = document.documentElement.lang === "en" ? model.name : model.arabic_name;
|
||||
modelSelect.appendChild(option);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error loading models:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// When car make changes, load corresponding models
|
||||
makeSelect.addEventListener("change", () => {
|
||||
const make_id = makeSelect.value;
|
||||
if (make_id) {
|
||||
loadModels(make_id);
|
||||
} else {
|
||||
resetDropdown(modelSelect, "{{ _('Select Model') }}");
|
||||
}
|
||||
});
|
||||
|
||||
// Debug normal form submission
|
||||
document.querySelector("form").addEventListener("submit", (event) => {
|
||||
// If you'd like to handle the form manually, uncomment the next line:
|
||||
// event.preventDefault();
|
||||
console.log("Form submitted normally");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
20
templates/crm/leads/lead_form.html
Normal file
20
templates/crm/leads/lead_form.html
Normal file
@ -0,0 +1,20 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n static crispy_forms_filters %}
|
||||
{% block content %}
|
||||
<h1>{% if object %}Update{% else %}Create{% endif %}</h1>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button
|
||||
type="submit"
|
||||
name="add_lead"
|
||||
id="lead-save"
|
||||
class="btn btn-phoenix-primary"
|
||||
>
|
||||
{{ _("Save") }}
|
||||
</button>
|
||||
<a href="{% url 'lead_list' %}" class="btn btn-phoenix-secondary">
|
||||
{{ _("Cancel") }}
|
||||
</a>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@ -1,232 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n static%}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-sm-6 col-md-8">
|
||||
<h2>{% if form.instance.pk %}{{ _("Edit Lead") }}{% else %}{{ _("Add New Lead") }}{% endif %}</h2>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-8">
|
||||
<form class="row g-3 form" method="post" action="{% url 'lead_create' %}">
|
||||
{% csrf_token %}
|
||||
<!-- Title -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<select class="form-control" id="{{ form.title.id_for_label }}" name="{{ form.title.name }}">
|
||||
{% for value, label in form.title.field.choices %}
|
||||
<option value="{{ value }}" {% if form.title.value == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label for="{{ form.title.id_for_label }}">{{ _("Title") }}</label>
|
||||
</div>
|
||||
{{ form.title.errors }}
|
||||
</div>
|
||||
|
||||
<!-- First Name -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="{{ form.first_name.id_for_label }}" name="{{ form.first_name.name }}" value="{{ form.first_name.value|default:'' }}" placeholder="{{ _('Enter first name') }}">
|
||||
<label for="{{ form.first_name.id_for_label }}">{{ _("First Name") }}</label>
|
||||
</div>
|
||||
{{ form.first_name.errors }}
|
||||
</div>
|
||||
|
||||
<!-- Last Name -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="{{ form.last_name.id_for_label }}" name="{{ form.last_name.name }}" value="{{ form.last_name.value|default:'' }}" placeholder="{{ _('Enter last name') }}">
|
||||
<label for="{{ form.last_name.id_for_label }}">{{ _("Last Name") }}</label>
|
||||
</div>
|
||||
{{ form.last_name.errors }}
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<input type="email" class="form-control" id="{{ form.email.id_for_label }}" name="{{ form.email.name }}" value="{{ form.email.value|default:'' }}" placeholder="{{ _('Enter email') }}">
|
||||
<label for="{{ form.email.id_for_label }}">{{ _("Email") }}</label>
|
||||
</div>
|
||||
{{ form.email.errors }}
|
||||
</div>
|
||||
|
||||
<!-- Phone Number -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="{{ form.phone_number.id_for_label }}" name="{{ form.phone_number.name }}" value="{{ form.phone_number.value|default:'' }}" placeholder="{{ _('Enter phone number') }}">
|
||||
<label for="{{ form.phone_number.id_for_label }}">{{ _("Phone Number") }}</label>
|
||||
</div>
|
||||
{{ form.phone_number.errors }}
|
||||
</div>
|
||||
|
||||
<!-- Salary -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<input type="number" class="form-control" id="{{ form.salary.id_for_label }}" name="{{ form.salary.name }}" value="{{ form.salary.value|default:'' }}" placeholder="{{ _('Enter salary') }}">
|
||||
<label for="{{ form.salary.id_for_label }}">{{ _("Salary") }}</label>
|
||||
</div>
|
||||
{{ form.salary.errors }}
|
||||
</div>
|
||||
|
||||
<!-- Obligations -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<input type="number" class="form-control" id="{{ form.obligations.id_for_label }}" name="{{ form.obligations.name }}" value="{{ form.obligations.value|default:'' }}" placeholder="{{ _('Enter obligations') }}">
|
||||
<label for="{{ form.obligations.id_for_label }}">{{ _("Obligations") }}</label>
|
||||
</div>
|
||||
{{ form.obligations.errors }}
|
||||
</div>
|
||||
|
||||
<!-- Car Make -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<select class="form-select form-select-sm" id="{{ form.id_car_make.id_for_label }}" name="{{ form.id_car_make.name }}">
|
||||
{% for value, label in form.id_car_make.field.choices %}
|
||||
<option value="{{ value }}" {% if form.id_car_make.value == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label for="{{ form.id_car_make.id_for_label }}">{{ _("Car Make") }}</label>
|
||||
</div>
|
||||
{{ form.id_car_make.errors }}
|
||||
</div>
|
||||
|
||||
<!-- Car Model -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<select class="form-select form-select-sm" id="{{ form.id_car_model.id_for_label }}" name="{{ form.id_car_model.name }}">
|
||||
{% for value, label in form.id_car_model.field.choices %}
|
||||
<option value="{{ value }}" {% if form.id_car_model.value == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label for="{{ form.id_car_model.id_for_label }}">{{ _("Car Make") }}</label>
|
||||
</div>
|
||||
{{ form.id_car_model.errors }}
|
||||
</div>
|
||||
|
||||
<!-- Year -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<input type="number" class="form-control" id="{{ form.year.id_for_label }}" name="{{ form.year.name }}" value="{{ form.year.value|default:'' }}" placeholder="{{ _('Enter year') }}">
|
||||
<label for="{{ form.year.id_for_label }}">{{ _("Year") }}</label>
|
||||
</div>
|
||||
{{ form.year.errors }}
|
||||
</div>
|
||||
|
||||
<!-- Source -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<select class="form-control" id="{{ form.source.id_for_label }}" name="{{ form.source.name }}">
|
||||
{% for value, label in form.source.field.choices %}
|
||||
<option value="{{ value }}" {% if form.source.value == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label for="{{ form.source.id_for_label }}">{{ _("Source") }}</label>
|
||||
</div>
|
||||
{{ form.source.errors }}
|
||||
</div>
|
||||
|
||||
<!-- Channel -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<select class="form-control" id="{{ form.channel.id_for_label }}" name="{{ form.channel.name }}">
|
||||
{% for value, label in form.channel.field.choices %}
|
||||
<option value="{{ value }}" {% if form.channel.value == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label for="{{ form.channel.id_for_label }}">{{ _("Channel") }}</label>
|
||||
</div>
|
||||
{{ form.channel.errors }}
|
||||
</div>
|
||||
|
||||
<!-- Assigned -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<select class="form-control" id="{{ form.assigned.id_for_label }}" name="{{ form.assigned.name }}">
|
||||
{% for value, label in form.assigned.field.choices %}
|
||||
<option value="{{ value }}" {% if form.assigned.value == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label for="{{ form.assigned.id_for_label }}">{{ _("Assigned To") }}</label>
|
||||
</div>
|
||||
{{ form.assigned.errors }}
|
||||
</div>
|
||||
|
||||
<!-- Priority -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<select class="form-control" id="{{ form.priority.id_for_label }}" name="{{ form.priority.name }}">
|
||||
{% for value, label in form.priority.field.choices %}
|
||||
<option value="{{ value }}" {% if form.priority.value == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label for="{{ form.priority.id_for_label }}">{{ _("Priority") }}</label>
|
||||
</div>
|
||||
{{ form.priority.errors }}
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<button type="submit" name="add_lead" id="lead-save" class="btn btn-phoenix-primary">{{ _("Save") }}</button>
|
||||
<a href="{% url 'lead_list' %}" class="btn btn-phoenix-secondary">{{ _("Cancel") }}</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== "") {
|
||||
const cookies = document.cookie.split(";");
|
||||
for (let cookie of cookies) {
|
||||
cookie = cookie.trim();
|
||||
if (cookie.substring(0, name.length + 1) === name + "=") {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const ajaxUrl = "{% url 'ajax_handler' %}";
|
||||
const makeSelect = document.getElementById("{{ form.id_car_make.id_for_label }}");
|
||||
const modelSelect = document.getElementById("{{ form.id_car_model.id_for_label }}");
|
||||
const csrfToken = getCookie("csrftoken");
|
||||
|
||||
function resetDropdown(dropdown, placeholder) {
|
||||
dropdown.innerHTML = `<option value="">${placeholder}</option>`;
|
||||
}
|
||||
|
||||
async function loadModels(makeId) {
|
||||
resetDropdown(modelSelect, '{% trans "Select" %}');
|
||||
const response = await fetch(`${ajaxUrl}?action=get_models&make_id=${makeId}`, {
|
||||
headers: {
|
||||
"X-CSRFToken": csrfToken,
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
data.forEach((model) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = model.id_car_model;
|
||||
option.textContent = document.documentElement.lang === "en" ? model.name : model.arabic_name;
|
||||
modelSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// AJAX logic: Load models when car make changes
|
||||
makeSelect.addEventListener("change", () => {
|
||||
const make_id = makeSelect.value;
|
||||
if (make_id) loadModels(make_id);
|
||||
});
|
||||
|
||||
// Ensure the form submits normally
|
||||
document.querySelector("form").addEventListener("submit", (event) => {
|
||||
// Do not call event.preventDefault() here unless you handle form submission manually
|
||||
console.log("Form submitted normally"); // Debugging
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -10,31 +10,31 @@
|
||||
<a class="nav-link dropdown-indicator label-1" href="#nv-dashboards" role="button" data-bs-toggle="collapse" aria-expanded="false" aria-controls="nv-dashboards">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="dropdown-indicator-icon-wrapper"><span class="fas fa-caret-right dropdown-indicator-icon"></span></div>
|
||||
<span class="nav-link-icon"><span data-feather="pie-chart"></span></span><span class="nav-link-text">Dashboards</span>
|
||||
<span class="nav-link-icon"><span data-feather="pie-chart"></span></span><span class="nav-link-text">{{ _("Dashboards") }}</span>
|
||||
</div>
|
||||
</a>
|
||||
<div class="parent-wrapper label-1">
|
||||
<ul class="nav collapse parent" data-bs-parent="#navbarVerticalCollapse" id="nv-dashboards">
|
||||
<li class="collapsed-nav-item-title d-none">Dashboards</li>
|
||||
<li class="collapsed-nav-item-title d-none">{{ _("Dashboards") }}</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'accounting' %}">
|
||||
<div class="d-flex align-items-center"><span class="nav-link-text">Accounting</span></div>
|
||||
<div class="d-flex align-items-center"><span class="nav-link-text">{{ _("Accounting") }}</span></div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">
|
||||
<div class="d-flex align-items-center"><span class="nav-link-text">Inventory</span></div>
|
||||
<div class="d-flex align-items-center"><span class="nav-link-text">{{ _("Inventory") }}</span></div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">
|
||||
<div class="d-flex align-items-center"><span class="nav-link-text">Sales</span></div>
|
||||
<div class="d-flex align-items-center"><span class="nav-link-text">{{ _("Sales") }}</span></div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">
|
||||
<div class="d-flex align-items-center"><span class="nav-link-text">CRM</span></div>
|
||||
<div class="d-flex align-items-center"><span class="nav-link-text">{{ _("CRM") }}</span></div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@ -139,7 +139,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item-wrapper">
|
||||
<a class="nav-link dropdown-indicator label-1" href="#nv-crm" role="button" data-bs-toggle="collapse" aria-expanded="false" aria-controls="nv-sales">
|
||||
<a class="nav-link dropdown-indicator label-1" href="#nv-crm" role="button" data-bs-toggle="collapse" aria-expanded="false" aria-controls="nv-crm">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="dropdown-indicator-icon-wrapper"><span class="fas fa-caret-right dropdown-indicator-icon"></span></div>
|
||||
<span class="nav-link-icon"><span data-feather="phone"></span></span><span class="nav-link-text">{% trans 'crm'|upper %}</span>
|
||||
@ -189,6 +189,34 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item-wrapper">
|
||||
<a class="nav-link dropdown-indicator label-1" href="#nv-accounting" role="button" data-bs-toggle="collapse" aria-expanded="false" aria-controls="nv-accounting">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="dropdown-indicator-icon-wrapper"><span class="fas fa-caret-right dropdown-indicator-icon"></span></div>
|
||||
<span class="nav-link-icon"><span data-feather="book"></span></span><span class="nav-link-text">{% trans 'Accounting' %}</span>
|
||||
</div>
|
||||
</a>
|
||||
<div class="parent-wrapper label-1">
|
||||
<ul class="nav collapse parent" data-bs-parent="#navbarVerticalCollapse" id="nv-accounting">
|
||||
<li class="collapsed-nav-item-title d-none">{% trans 'Accounting' %}</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'account_list' %}">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="nav-link-icon"><span data-feather="book-open"></span></span><span class="nav-link-text">{% trans 'Chart of Accounts' %}</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'bank_account_list' %}">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="nav-link-icon"><span data-feather="users"></span></span><span class="nav-link-text">{% trans 'Banks' %}</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!--Add Above -->
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@ -282,7 +282,7 @@
|
||||
<div class="modal-content rounded-top-2">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="equipmentOptionsModalLabel">
|
||||
{% trans 'specifications'|capfirst %}
|
||||
{% trans 'Options'|capfirst %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
|
||||
</div>
|
||||
@ -617,6 +617,7 @@
|
||||
showSpecificationButton.disabled = !trimId
|
||||
showEquipmentButton.disabled = !trimId
|
||||
if (trimId) loadSpecifications(trimId)
|
||||
loadEquipment(trimId)
|
||||
})
|
||||
|
||||
equipmentSelect.addEventListener("change", () => {
|
||||
|
||||
@ -7,165 +7,134 @@
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="row g-3 justify-content-between">
|
||||
<div class="col-sm-12">
|
||||
<div class="card border h-100 w-100 overflow-hidden">
|
||||
<div class="bg-holder d-block bg-card" style="background-image:url({% static 'images/spot-illustrations/32.png' %});background-position: top right;">
|
||||
</div>
|
||||
<div class="d-dark-none me-5">
|
||||
<div class="bg-holder d-none d-sm-block d-xl-none d-xxl-block bg-card" style="background-image:url({% static 'images/spot-illustrations/dark_21.png' %}); background-position: bottom right; background-size: auto;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-light-none me-5">
|
||||
<div class="bg-holder d-none d-sm-block d-xl-none d-xxl-block bg-card" style="background-image:url({% static 'images/spot-illustrations/21.png' %}); background-position: bottom right; background-size: auto;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body px-lg-5 position-relative">
|
||||
<br>
|
||||
<div class="row align-items-center g-3 g-sm-5 text-start text-sm-start">
|
||||
<div class="col-12 col-sm-auto ">
|
||||
<div class="avatar avatar-3xl avatar-bordered mb-3">
|
||||
{% if cars.first.id_car_make.logo %}
|
||||
<img class="rounded" src="{{ cars.first.id_car_make.logo.url }}" alt="">
|
||||
{% else %}
|
||||
<img class="rounded" src="{% static 'images/logos/car_make/sedan.svg' %}" alt="">
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-sm-auto flex-1">
|
||||
<h3 class="mb-2">{{ cars.first.id_car_make.get_local_name }}<span class="ms-2 text-body-tertiary fw-semibold">{{ cars.first.id_car_model.get_local_name }}</span></h3>
|
||||
<p class="text-body-tertiary fw-semibold">{{ cars.first.id_car_serie.name }}, <span class="fs-10">{{ cars.first.id_car_trim.name }}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 justify-content-between">
|
||||
<div class="col-sm-12">
|
||||
<div class="card border h-100 w-100 overflow-hidden">
|
||||
<div class="bg-holder d-block bg-card" style="background-image:url({% static 'images/spot-illustrations/32.png' %});background-position: top right;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 justify-content-between mt-4">
|
||||
<div class="col-sm-12">
|
||||
<div class="table-list" id="inventoryTable">
|
||||
<div class="table-responsive scrollbar mb-3">
|
||||
<table class="table table-sm fs-9 mb-0 overflow-hidden">
|
||||
<thead class="text-body">
|
||||
<tr>
|
||||
<th class="pe-1 align-middle white-space-nowrap text-center" data-sort="stock">{% trans 'Stock' %}</th>
|
||||
<th class="pe-1 align-middle white-space-nowrap text-start" data-sort="vin" style="min-width: 4.5rem;">{% trans "VIN" %}</th>
|
||||
<th class="pe-1 align-middle white-space-nowrap text-center" data-sort="date">{% trans "Year"|upper %}</th>
|
||||
<th class="pe-1 align-middle white-space-nowrap text-center" data-sort="extColor" style="min-width: 8.5rem">{% trans 'Exterior Color'|upper %}</th>
|
||||
<th class="pe-1 align-middle white-space-nowrap text-center" data-sort="intColor" style="min-width: 8.5rem">{% trans 'Interior Color'|upper %}</th>
|
||||
<th class="pe-1 align-middle white-space-nowrap text-center" data-sort="location" style="min-width: 12.5rem;">{% trans "Showroom Location"|upper %}</th>
|
||||
<th class="pe-1 align-middle white-space-nowrap text-center" data-sort="status">{% trans 'Status'|upper %}</th>
|
||||
<th class="pe-1 align-middle white-space-nowrap text-center" data-sort="age" style="min-width: 7rem">{% trans 'Age'|upper %}</th>
|
||||
<th class="no-sort align-middle white-space-nowrap text-center"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for car in cars %}
|
||||
<tr>
|
||||
<td class="align-middle white-space-nowrap text-center fw-bold text-body-tertiary">
|
||||
{% if car.stock_type == "new" %}
|
||||
<span class="badge badge-phoenix badge-phoenix-success"><span class="badge-label">{{_("New")}}</span></span>
|
||||
{% elif car.status == "used" %}
|
||||
<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 fw-bold">{{ car.vin }}</td>
|
||||
<td class="align-middle white-space-nowrap text-center fw-bold">{{ car.year }}</td>
|
||||
{% if car.colors.exists %}
|
||||
<td class="align-middle white-space-nowrap text-body fs-9 text-start">
|
||||
<div class="d-flex flex-column align-items-center">
|
||||
<span class="color-div" style="background: linear-gradient(90deg, rgba({{ car.colors.first.exterior.rgb }},1) 10%, rgba({{ car.colors.first.exterior.rgb }},0.10) 100%);" title="{{ car.colors.first.exterior.get_local_name }}"></span><span>{{ car.colors.first.exterior.get_local_name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap text-body fs-9 text-start">
|
||||
<div class="d-flex flex-column align-items-center">
|
||||
<span class="color-div" style="background: linear-gradient(90deg, rgba({{ car.colors.first.interior.rgb }},1) 10%, rgba({{ car.colors.first.interior.rgb }},0.10) 100%);" title="{{ car.colors.first.interior.get_local_name }}"></span><span>{{ car.colors.first.interior.get_local_name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
{% else %}
|
||||
<td class="align-middle white-space-nowrap text-body fs-9 text-center"><span class="color-div">{% trans 'No Color' %}</span></td>
|
||||
<td class="align-middle white-space-nowrap text-body fs-9 text-center"><span class="color-div">{% trans 'No Color' %}</span></td>
|
||||
{% endif %}
|
||||
<td class="align-middle white-space-nowrap text-body fs-9 text-center">
|
||||
{% if car.location.is_owner_showroom %}
|
||||
{% trans 'Our Showroom' %}
|
||||
{% else %}
|
||||
{{ car.location.showroom.get_local_name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="status align-middle white-space-nowrap text-center fw-bold text-body-tertiary">
|
||||
{% if car.status == "available" %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-success"><span class="badge-label">{{_("Available")}}</span><span class="ms-1" data-feather="check" style="height:13px;width:13px;"></span></span>
|
||||
{% elif car.status == "sold" %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-primary"><span class="badge-label">{{_("Sold")}}</span><span class="ms-1" data-feather="package" style="height:13px;width:13px;"></span></span>
|
||||
{% elif car.status == "hold" %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-warning"><span class="badge-label">{{ _("Hold") }}</span><span class="ms-1" data-feather="alert-octagon" style="height:13px;width:13px;"></span></span>
|
||||
{% elif car.status == "reserved" %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-danger"><span class="badge-label">{{ _("Reserved") }}</span><span class="ms-1" data-feather="info" style="height:13px;width:13px;"></span></span>
|
||||
{% elif car.status == "damaged" %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-secondary"><span class="badge-label">{{ _("Damaged") }}</span><span class="ms-1" data-feather="x" style="height:13px;width:13px;"></span></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="date align-middle white-space-nowrap text-body-tertiary fs-9 ps-4 text-start">
|
||||
<span class="fw-light">{{ car.receiving_date|timesince }}</span>
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap text-end pe-0 ps-4">
|
||||
<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 %}">{% trans "view"|capfirst %}</a>
|
||||
<div class="dropdown-divider"></div><a class="dropdown-item text-danger" href="#!">Remove</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" class="d-flex flex-column align-items-center">
|
||||
<p class="text-muted">{% trans "No cars available." %}</p>
|
||||
<a href="{% url 'add_car' %}" class="btn btn-primary">{% trans "Add a Car" %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination pagination-sm justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1" aria-label="First">««</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}" aria-label="Previous">«</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ num }}</span>
|
||||
</li>
|
||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}" aria-label="Next">»</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}" aria-label="Last">»»</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="d-dark-none me-5">
|
||||
<div class="bg-holder d-none d-sm-block d-xl-none d-xxl-block bg-card" style="background-image:url({% static 'images/spot-illustrations/dark_21.png' %}); background-position: bottom right; background-size: auto;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-light-none me-5">
|
||||
<div class="bg-holder d-none d-sm-block d-xl-none d-xxl-block bg-card" style="background-image:url({% static 'images/spot-illustrations/21.png' %}); background-position: bottom right; background-size: auto;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body px-lg-5 position-relative">
|
||||
<br>
|
||||
<div class="row align-items-center g-3 g-sm-5 text-start text-sm-start">
|
||||
|
||||
<div class="col-12 col-sm-auto ">
|
||||
<div class="avatar avatar-3xl avatar-bordered mb-3">
|
||||
{% if cars.first.id_car_make.logo %}
|
||||
<img class="rounded" src="{{ cars.first.id_car_make.logo.url }}" alt="">
|
||||
{% else %}
|
||||
<img class="rounded" src="{% static 'images/logos/car_make/sedan.svg' %}" alt="">
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-sm-auto flex-1">
|
||||
<h3 class="mb-2">{{ cars.first.id_car_make.get_local_name }}<span class="ms-2 text-body-tertiary fw-semibold">{{ cars.first.id_car_model.get_local_name }}</span></h3>
|
||||
<p class="text-body-tertiary fw-semibold">{{ cars.first.id_car_serie.name }}, <span class="fs-10">{{ cars.first.id_car_trim.name }}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 justify-content-between mt-4">
|
||||
<div class="col-sm-12">
|
||||
<div class="table-list" id="inventoryTable">
|
||||
<div class="table-responsive scrollbar mb-3">
|
||||
<table class="table table-sm fs-9 mb-0 overflow-hidden">
|
||||
<thead class="text-body">
|
||||
<tr>
|
||||
<th class="pe-1 align-middle white-space-nowrap text-center" data-sort="stock">{% trans 'Stock'|upper %}</th>
|
||||
<th class="pe-1 align-middle white-space-nowrap text-start" data-sort="vin" style="min-width: 4.5rem;">{% trans "VIN" %}</th>
|
||||
<th class="pe-1 align-middle white-space-nowrap text-center" data-sort="date">{% trans "Year"|upper %}</th>
|
||||
<th class="pe-1 align-middle white-space-nowrap text-center" data-sort="extColor" style="min-width: 8.5rem">{% trans 'Exterior Color'|upper %}</th>
|
||||
<th class="pe-1 align-middle white-space-nowrap text-center" data-sort="intColor" style="min-width: 8.5rem">{% trans 'Interior Color'|upper %}</th>
|
||||
<th class="pe-1 align-middle white-space-nowrap text-center" data-sort="location" style="min-width: 12.5rem;">{% trans "Showroom Location"|upper %}</th>
|
||||
<th class="pe-1 align-middle white-space-nowrap text-center" data-sort="status">{% trans 'Status'|upper %}</th>
|
||||
<th class="pe-1 align-middle white-space-nowrap text-center" data-sort="age" style="min-width: 7rem">{% trans 'Age'|upper %}</th>
|
||||
<th class="no-sort align-middle white-space-nowrap text-center"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for car in cars %}
|
||||
<tr>
|
||||
<td class="align-middle white-space-nowrap text-center fw-bold text-body-tertiary">
|
||||
{% if car.stock_type == "new" %}
|
||||
<span class="badge badge-phoenix badge-phoenix-success"><span class="badge-label">{{_("New")}}</span></span>
|
||||
{% elif car.status == "used" %}
|
||||
<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-8 fw-bold" href="{% url 'car_detail' car.pk %}">{{ car.vin }}</a></td>
|
||||
<td class="align-middle white-space-nowrap text-center fw-bold">{{ car.year }}</td>
|
||||
{% if car.colors.exists %}
|
||||
<td class="align-middle white-space-nowrap text-body fs-9 text-start">
|
||||
<div class="d-flex flex-column align-items-center">
|
||||
<span class="color-div" style="background: linear-gradient(90deg, rgba({{ car.colors.first.exterior.rgb }},1) 10%, rgba({{ car.colors.first.exterior.rgb }},0.10) 100%);" title="{{ car.colors.first.exterior.get_local_name }}"></span><span>{{ car.colors.first.exterior.get_local_name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap text-body fs-9 text-start">
|
||||
<div class="d-flex flex-column align-items-center">
|
||||
<span class="color-div" style="background: linear-gradient(90deg, rgba({{ car.colors.first.interior.rgb }},1) 10%, rgba({{ car.colors.first.interior.rgb }},0.10) 100%);" title="{{ car.colors.first.interior.get_local_name }}"></span><span>{{ car.colors.first.interior.get_local_name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
{% else %}
|
||||
<td class="align-middle white-space-nowrap text-body fs-9 text-center"><span class="color-div">{% trans 'No Color' %}</span></td>
|
||||
<td class="align-middle white-space-nowrap text-body fs-9 text-center"><span class="color-div">{% trans 'No Color' %}</span></td>
|
||||
{% endif %}
|
||||
<td class="align-middle white-space-nowrap text-body fs-9 text-center">
|
||||
{% if car.location.is_owner_showroom %}
|
||||
{% trans 'Our Showroom' %}
|
||||
{% else %}
|
||||
{{ car.location.showroom.get_local_name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="status align-middle white-space-nowrap text-center fw-bold text-body-tertiary">
|
||||
{% if car.status == "available" %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-success"><span class="badge-label">{{_("Available")}}</span><span class="ms-1" data-feather="check" style="height:13px;width:13px;"></span></span>
|
||||
{% elif car.status == "sold" %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-primary"><span class="badge-label">{{_("Sold")}}</span><span class="ms-1" data-feather="package" style="height:13px;width:13px;"></span></span>
|
||||
{% elif car.status == "hold" %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-warning"><span class="badge-label">{{ _("Hold") }}</span><span class="ms-1" data-feather="alert-octagon" style="height:13px;width:13px;"></span></span>
|
||||
{% elif car.status == "reserved" %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-danger"><span class="badge-label">{{ _("Reserved") }}</span><span class="ms-1" data-feather="info" style="height:13px;width:13px;"></span></span>
|
||||
{% elif car.status == "damaged" %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-secondary"><span class="badge-label">{{ _("Damaged") }}</span><span class="ms-1" data-feather="x" style="height:13px;width:13px;"></span></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="date align-middle white-space-nowrap text-body-tertiary fs-9 ps-4 text-start">
|
||||
<span class="fw-light">{{ car.receiving_date|timesince }}</span>
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap text-end pe-0 ps-4">
|
||||
<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 %}">{% trans "view"|capfirst %}</a>
|
||||
<div class="dropdown-divider"></div><a class="dropdown-item text-danger" href="#!">Remove</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" class="d-flex flex-column align-items-center">
|
||||
<p class="text-muted">{% trans "No cars available." %}</p>
|
||||
<a href="{% url 'add_car' %}" class="btn btn-primary">{% trans "Add a Car" %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
{% include 'partials/pagination.html' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -6,15 +6,38 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="px-3 mb-6">
|
||||
<div class="row justify-content-between">
|
||||
<div class="col-6 col-md-4 col-xxl-2 text-center border-translucent border-start-xxl border-end-xxl-0 border-bottom-xxl-0 border-end border-bottom pb-4 pb-xxl-0 "><span class="uil fs-5 lh-1 uil-car-sideview text-primary"></span>
|
||||
<h1 class="fs-5 pt-3">{{ inventory.total_cars }}</h1>
|
||||
<p class="fs-9 mb-0">{% trans "Total Cars in Inventory" %}</p>
|
||||
</div>
|
||||
<div class="col-6 col-md-4 col-xxl-2 text-center border-translucent border-start-xxl border-end-xxl-0 border-bottom-xxl-0 border-end-md border-bottom pb-4 pb-xxl-0"><span class="uil fs-5 lh-1 uil-envelope-upload text-info"></span>
|
||||
<h1 class="fs-5 pt-3">1,866</h1>
|
||||
<p class="fs-9 mb-0">Emails Sent</p>
|
||||
</div>
|
||||
<div class="col-6 col-md-4 col-xxl-2 text-center border-translucent border-start-xxl border-bottom-xxl-0 border-bottom border-end border-end-md-0 pb-4 pb-xxl-0 pt-4 pt-md-0"><span class="uil fs-5 lh-1 uil-envelopes text-primary"></span>
|
||||
<h1 class="fs-5 pt-3">1,366</h1>
|
||||
<p class="fs-9 mb-0">Emails Delivered</p>
|
||||
</div>
|
||||
<div class="col-6 col-md-4 col-xxl-2 text-center border-translucent border-start-xxl border-end-md border-end-xxl-0 border-bottom border-bottom-md-0 pb-4 pb-xxl-0 pt-4 pt-xxl-0"><span class="uil fs-5 lh-1 uil-envelope-open text-info"></span>
|
||||
<h1 class="fs-5 pt-3">1,200</h1>
|
||||
<p class="fs-9 mb-0">Emails Opened</p>
|
||||
</div>
|
||||
<div class="col-6 col-md-4 col-xxl-2 text-center border-translucent border-start-xxl border-end border-end-xxl-0 pb-md-4 pb-xxl-0 pt-4 pt-xxl-0"><span class="uil fs-5 lh-1 uil-envelope-check text-success"></span>
|
||||
<h1 class="fs-5 pt-3">900</h1>
|
||||
<p class="fs-9 mb-0">Emails Clicked</p>
|
||||
</div>
|
||||
<div class="col-6 col-md-4 col-xxl-2 text-center border-translucent border-start-xxl border-end-xxl pb-md-4 pb-xxl-0 pt-4 pt-xxl-0"><span class="uil fs-5 lh-1 uil-envelope-block text-danger"></span>
|
||||
<h1 class="fs-5 pt-3">500</h1>
|
||||
<p class="fs-9 mb-0">Emails Bounce</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-4">
|
||||
|
||||
<!-- Total Cars -->
|
||||
<div class="alert alert-phoenix-primary">
|
||||
<div class="d-flex justify-content-between">
|
||||
<strong class="fs-6">{% trans "Total Cars in Inventory" %}</strong>
|
||||
<strong class="fs-6">{{ inventory.total_cars }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Inventory by Makes -->
|
||||
<div class="accordion" id="makesAccordion">
|
||||
{% for make in inventory.makes %}
|
||||
|
||||
@ -1,118 +1,114 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load custom_filters %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
<div class="row-fluid p-2">
|
||||
<form id="car-form">
|
||||
<!-- Other form fields -->
|
||||
<div>
|
||||
<label for="vin_no">VIN Number:</label>
|
||||
<input type="text" id="vin_no" name="vin_no" readonly>
|
||||
<button type="button" id="scan-vin-btn">Scan VIN</button>
|
||||
<form id="car-form">
|
||||
<div>
|
||||
<label for="vin_no">VIN/Barcode/QR Code:</label>
|
||||
<input type="text" id="vin_no" name="vin_no" readonly>
|
||||
<button type="button" id="capture-btn">Capture Code</button>
|
||||
</div>
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
|
||||
<!-- Camera Stream -->
|
||||
<div id="camera-container" style="display:none;">
|
||||
<video id="camera" autoplay playsinline width="400"></video>
|
||||
<button id="toggle-btn">Toggle Camera</button>
|
||||
<button id="scan-btn">Scan</button>
|
||||
</div>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
<!-- Scanner Modal -->
|
||||
<div id="scanner-modal" style="display:none;">
|
||||
<div id="video"></div>
|
||||
<button id="stop-scanner">Stop Scanner</button>
|
||||
<p id="result"></p>
|
||||
</div>
|
||||
|
||||
<p id="result"></p>
|
||||
</div>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/quagga/0.12.1/quagga.min.js"></script>
|
||||
<script>
|
||||
const scanVinBtn = document.getElementById('scan-vin-btn');
|
||||
const vinInput = document.getElementById('vin_no');
|
||||
const scannerModal = document.getElementById('scanner-modal');
|
||||
const videoElement = document.getElementById('video');
|
||||
const stopScannerBtn = document.getElementById('stop-scanner');
|
||||
const resultElement = document.getElementById('result');
|
||||
let captureBtn = document.getElementById('capture-btn');
|
||||
let cameraContainer = document.getElementById('camera-container');
|
||||
let videoElement = document.getElementById('camera');
|
||||
let toggleBtn = document.getElementById('toggle-btn');
|
||||
let scanBtn = document.getElementById('scan-btn');
|
||||
let vinInput = document.getElementById('vin_no');
|
||||
let resultElement = document.getElementById('result');
|
||||
let mediaStream = null;
|
||||
let currentCameraIndex = 0;
|
||||
let cameras = [];
|
||||
|
||||
// Open the scanner modal
|
||||
scanVinBtn.addEventListener('click', () => {
|
||||
scannerModal.style.display = 'block';
|
||||
startScanner();
|
||||
});
|
||||
|
||||
// Stop the scanner
|
||||
stopScannerBtn.addEventListener('click', () => {
|
||||
stopScanner();
|
||||
scannerModal.style.display = 'none';
|
||||
});
|
||||
|
||||
function startScanner() {
|
||||
Quagga.init({
|
||||
inputStream: {
|
||||
name: "Live",
|
||||
type: "LiveStream",
|
||||
target: videoElement,
|
||||
},
|
||||
decoder: {
|
||||
readers: ["code_128_reader"], // Use appropriate barcode reader
|
||||
}
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
resultElement.textContent = 'Error initializing scanner.';
|
||||
return;
|
||||
}
|
||||
Quagga.start();
|
||||
});
|
||||
|
||||
Quagga.onDetected(function(data) {
|
||||
const vin = data.codeResult.code;
|
||||
vinInput.value = vin; // Set the scanned VIN to the input field
|
||||
stopScanner();
|
||||
scannerModal.style.display = 'none';
|
||||
|
||||
// Send the VIN via AJAX to the backend
|
||||
decodeVin(vin);
|
||||
});
|
||||
// List available cameras
|
||||
async function getCameras() {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
cameras = devices.filter(device => device.kind === 'videoinput');
|
||||
}
|
||||
|
||||
function stopScanner() {
|
||||
Quagga.stop();
|
||||
}
|
||||
|
||||
function decodeVin(vin) {
|
||||
fetch('/api/decode-vin/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCSRFToken()
|
||||
},
|
||||
body: JSON.stringify({ vin_no: vin })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
resultElement.textContent = `VIN decoded: ${JSON.stringify(data.data)}`;
|
||||
} else {
|
||||
resultElement.textContent = `Error: ${data.error}`;
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
resultElement.textContent = 'Error processing VIN.';
|
||||
});
|
||||
}
|
||||
|
||||
// Function to get CSRF token
|
||||
function getCSRFToken() {
|
||||
const name = 'csrftoken';
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
if (cookie.startsWith(name + '=')) {
|
||||
return cookie.substring(name.length + 1);
|
||||
}
|
||||
// Start the camera with the given device ID
|
||||
async function startCamera(deviceId) {
|
||||
if (mediaStream) {
|
||||
mediaStream.getTracks().forEach(track => track.stop()); // Stop the current stream
|
||||
}
|
||||
return '';
|
||||
|
||||
mediaStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { deviceId: { exact: deviceId } }
|
||||
});
|
||||
|
||||
videoElement.srcObject = mediaStream;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
// Toggle between cameras
|
||||
toggleBtn.addEventListener('click', async () => {
|
||||
currentCameraIndex = (currentCameraIndex + 1) % cameras.length;
|
||||
await startCamera(cameras[currentCameraIndex].deviceId);
|
||||
});
|
||||
|
||||
// Capture image and send to backend
|
||||
scanBtn.addEventListener('click', () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = videoElement.videoWidth;
|
||||
canvas.height = videoElement.videoHeight;
|
||||
const context = canvas.getContext('2d');
|
||||
context.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
canvas.toBlob((blob) => {
|
||||
const formData = new FormData();
|
||||
formData.append('image', blob, 'code_image.jpg');
|
||||
|
||||
fetch('{% url 'car_search' %}', {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRFToken': '{% csrf_token %}' },
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
vinInput.value = data.code;
|
||||
resultElement.textContent = `Code found: ${data.code}`;
|
||||
} else {
|
||||
resultElement.textContent = `Error: ${data.error}`;
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error processing code:', err);
|
||||
resultElement.textContent = 'Error processing code.';
|
||||
})
|
||||
.finally(() => {
|
||||
stopCamera();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Open camera and start video stream
|
||||
captureBtn.addEventListener('click', async () => {
|
||||
cameraContainer.style.display = 'block';
|
||||
await getCameras();
|
||||
if (cameras.length > 0) {
|
||||
await startCamera(cameras[currentCameraIndex].deviceId);
|
||||
} else {
|
||||
resultElement.textContent = 'No cameras found.';
|
||||
}
|
||||
});
|
||||
|
||||
// Stop the camera stream
|
||||
function stopCamera() {
|
||||
if (mediaStream) {
|
||||
mediaStream.getTracks().forEach(track => track.stop());
|
||||
mediaStream = null;
|
||||
}
|
||||
cameraContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
</script>
|
||||
@ -1,126 +1,82 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Bank Accounts" %}{% endblock title %}
|
||||
{% block customers %}
|
||||
{% block bank_accounts %}
|
||||
<a class="nav-link active fw-bold">
|
||||
{% trans "Customers"|capfirst %}
|
||||
{% trans "Bank Accounts" %}
|
||||
<span class="visually-hidden">(current)</span>
|
||||
</a>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="d-flex flex-column min-vh-100">
|
||||
<div class="d-flex flex-column flex-sm-grow-1 ms-sm-14 p-4">
|
||||
<main class="d-grid gap-4 p-1">
|
||||
<!-- Search Bar -->
|
||||
<div class="row g-4">
|
||||
<div class="col-12">
|
||||
<div class="row-fluid p-2">
|
||||
<form method="get">
|
||||
<div class="input-group input-group-sm">
|
||||
<button class="btn btn-sm btn-secondary rounded-start" type="submit">
|
||||
{% trans "search" %}
|
||||
</button>
|
||||
<input type="text"
|
||||
name="q"
|
||||
class="form-control form-control-sm rounded-end"
|
||||
value="{{ request.GET.q }}"
|
||||
placeholder="{% trans 'Search accounts...' %}" />
|
||||
{% if request.GET.q %}
|
||||
<a href="{% url request.resolver_match.view_name %}"
|
||||
class="btn btn-sm btn-outline-danger ms-1 rounded">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Customer Table -->
|
||||
<div class="row g-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">{% trans "Customers List" %}</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th class="text-center">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for account in bank_accounts %}
|
||||
<tr>
|
||||
<td>{{ account.name }}</td>
|
||||
<td>{{ account.cash_account }}</td>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'bank_account_detail' account.pk %}"
|
||||
class="btn btn-sm btn-success">
|
||||
{% trans "view" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-muted">
|
||||
{% trans "No customers found." %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<div class="card-footer bg-light">
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination pagination-sm justify-content-center mb-0">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}" aria-label="{% trans 'Previous' %}">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link" aria-hidden="true">«</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ num }}</span>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}" aria-label="{% trans 'Next' %}">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link" aria-hidden="true">»</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Search Bar -->
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<form method="get">
|
||||
<div class="input-group input-group-sm">
|
||||
<button class="btn btn-sm btn-secondary rounded-start" type="submit">
|
||||
{% trans "search" %}
|
||||
</button>
|
||||
<input type="text"
|
||||
name="q"
|
||||
class="form-control form-control-sm rounded-end"
|
||||
value="{{ request.GET.q }}"
|
||||
placeholder="{% trans 'Search accounts...' %}" />
|
||||
{% if request.GET.q %}
|
||||
<a href="{% url request.resolver_match.view_name %}"
|
||||
class="btn btn-sm btn-outline-danger ms-1 rounded">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Bank Accounts Table -->
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">{% trans "Bank Accounts" %}</h5>
|
||||
</div>
|
||||
</main>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th class="text-center">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for account in bank_accounts %}
|
||||
<tr>
|
||||
<td>{{ account.name }}</td>
|
||||
<td>{{ account.cash_account }}</td>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'bank_account_detail' account.pk %}"
|
||||
class="btn btn-sm btn-success">
|
||||
{% trans "view" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-muted">
|
||||
{% trans "No customers found." %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
{% include 'partials/pagination.html' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
40
templates/partials/pagination.html
Normal file
40
templates/partials/pagination.html
Normal file
@ -0,0 +1,40 @@
|
||||
{% load i18n static %}
|
||||
<div class="card-footer bg-light">
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination pagination-sm justify-content-center mb-0">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}" aria-label="{% trans 'Previous' %}">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link" aria-hidden="true">«</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ num }}</span>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}" aria-label="{% trans 'Next' %}">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link" aria-hidden="true">»</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
Loading…
x
Reference in New Issue
Block a user