This commit is contained in:
Marwan Alwali 2025-01-13 21:38:18 +03:00
parent 00f87799d0
commit 1c668d3f0a
43 changed files with 748 additions and 1770 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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',

View File

@ -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',

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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',
),
]

View File

@ -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()),
],
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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,
),
]

View File

@ -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,
),
]

View File

@ -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),
),
]

View File

@ -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,
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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)

View File

@ -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'

View File

@ -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):

View File

@ -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

View File

@ -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 %}

View 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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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", () => {

View File

@ -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">&laquo;&laquo;</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}" aria-label="Previous">&laquo;</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">&raquo;</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}" aria-label="Last">&raquo;&raquo;</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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link" aria-hidden="true">&laquo;</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">&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link" aria-hidden="true">&raquo;</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 %}

View 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">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link" aria-hidden="true">&laquo;</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">&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link" aria-hidden="true">&raquo;</span>
</li>
{% endif %}
</ul>
</nav>
</div>