This commit is contained in:
Marwan Alwali 2025-07-01 18:29:31 +03:00
parent 68f7e3fb2c
commit b22ef36524
24 changed files with 7140 additions and 1626 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -14,9 +14,10 @@
</component> </component>
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
<excludeFolder url="file://$MODULE_DIR$/venv" /> <excludeFolder url="file://$MODULE_DIR$/venv" />
</content> </content>
<orderEntry type="jdk" jdkName="Python 3.11 (car_inventory)" jdkType="Python SDK" /> <orderEntry type="jdk" jdkName="uv (car_inventory)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="jquery-3.5.1" level="application" /> <orderEntry type="library" name="jquery-3.5.1" level="application" />
<orderEntry type="library" name="sweetalert2" level="application" /> <orderEntry type="library" name="sweetalert2" level="application" />

5
.idea/misc.xml generated
View File

@ -3,8 +3,5 @@
<component name="Black"> <component name="Black">
<option name="sdkName" value="Python 3.11 (car_inventory)" /> <option name="sdkName" value="Python 3.11 (car_inventory)" />
</component> </component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (car_inventory)" project-jdk-type="Python SDK" /> <component name="ProjectRootManager" version="2" project-jdk-name="uv (car_inventory)" project-jdk-type="Python SDK" />
<component name="PyPackaging">
<option name="earlyReleasesAsUpgrades" value="true" />
</component>
</project> </project>

View File

@ -1,31 +0,0 @@
# Generated by Django 5.2.1 on 2025-05-25 23:01
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="CarVIN",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("vin", models.CharField(max_length=17, verbose_name="VIN")),
(
"created",
models.DateTimeField(auto_now_add=True, verbose_name="created"),
),
],
),
]

View File

@ -1,46 +0,0 @@
# Generated by Django 5.1.7 on 2025-06-22 17:22
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ChatLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user_message', models.TextField()),
('chatbot_response', models.TextField()),
('timestamp', models.DateTimeField(auto_now_add=True, db_index=True)),
],
options={
'ordering': ['-timestamp'],
},
),
migrations.CreateModel(
name='AnalysisCache',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('prompt_hash', models.CharField(db_index=True, max_length=64)),
('dealer_id', models.IntegerField(blank=True, db_index=True, null=True)),
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
('updated_at', models.DateTimeField(auto_now=True)),
('expires_at', models.DateTimeField(db_index=True)),
('result', models.JSONField()),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name_plural': 'Analysis caches',
},
),
]

View File

@ -1,34 +0,0 @@
# Generated by Django 5.1.7 on 2025-06-22 17:22
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('haikalbot', '0001_initial'),
('inventory', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='chatlog',
name='dealer',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chatlogs', to='inventory.dealer'),
),
migrations.AddIndex(
model_name='analysiscache',
index=models.Index(fields=['prompt_hash', 'dealer_id'], name='haikalbot_a_prompt__b98e1e_idx'),
),
migrations.AddIndex(
model_name='analysiscache',
index=models.Index(fields=['expires_at'], name='haikalbot_a_expires_e790cd_idx'),
),
migrations.AddIndex(
model_name='chatlog',
index=models.Index(fields=['dealer', 'timestamp'], name='haikalbot_c_dealer__6f8d63_idx'),
),
]

View File

@ -1,8 +1,5 @@
from django.conf import settings from django.conf import settings
from inventory.utils import get_user_type
def currency_context(request): def currency_context(request):
""" """
Provides a context dictionary containing the currency setting. This is typically Provides a context dictionary containing the currency setting. This is typically

View File

@ -114,6 +114,7 @@ class InjectDealerMiddleware:
# if request.user.is_authenticated and not request.session.get('otp_verified', False): # if request.user.is_authenticated and not request.session.get('otp_verified', False):
# return redirect(reverse('verify_otp')) # return redirect(reverse('verify_otp'))
# return self.get_response(request) # return self.get_response(request)
class DealerSlugMiddleware: class DealerSlugMiddleware:
def __init__(self, get_response): def __init__(self, get_response):
self.get_response = get_response self.get_response = get_response

View File

@ -1,871 +0,0 @@
# Generated by Django 5.1.7 on 2025-06-22 17:22
import datetime
import django.core.validators
import django.db.models.deletion
import django.utils.timezone
import inventory.mixins
import inventory.models
import phonenumber_field.modelfields
import uuid
from decimal import Decimal
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('appointment', '0001_initial'),
('auth', '0012_alter_user_first_name_max_length'),
('contenttypes', '0002_remove_content_type_name'),
('django_ledger', '0021_alter_bankaccountmodel_account_model_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='CarEquipment',
fields=[
('id_car_equipment', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=255, null=True)),
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('year_begin', models.IntegerField(blank=True, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
],
options={
'verbose_name': 'Equipment',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='CarMake',
fields=[
('id_car_make', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=255, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('logo', models.ImageField(blank=True, null=True, upload_to='car_make', verbose_name='logo')),
('is_sa_import', models.BooleanField(default=False)),
('car_type', models.SmallIntegerField(blank=True, 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')], null=True)),
],
options={
'verbose_name': 'Make',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='ExteriorColors',
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')),
('rgb', models.CharField(blank=True, max_length=24, null=True, verbose_name='RGB')),
],
options={
'verbose_name': 'Exterior Colors',
'verbose_name_plural': 'Exterior Colors',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='InteriorColors',
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')),
('rgb', models.CharField(blank=True, max_length=24, null=True, verbose_name='RGB')),
],
options={
'verbose_name': 'Interior Colors',
'verbose_name_plural': 'Interior Colors',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='Payment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='amount')),
('payment_method', models.CharField(choices=[('cash', 'cash'), ('credit', 'credit'), ('transfer', 'transfer'), ('debit', 'debit'), ('sadad', 'SADAD')], max_length=50, verbose_name='method')),
('reference_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='reference number')),
('payment_date', models.DateField(auto_now_add=True, verbose_name='date')),
],
options={
'verbose_name': 'payment',
'verbose_name_plural': 'payments',
},
),
migrations.CreateModel(
name='VatRate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('rate', models.DecimalField(decimal_places=2, default=Decimal('0.15'), max_digits=5)),
('is_active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
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='Car',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='Primary Key')),
('slug', models.SlugField(blank=True, help_text='Slug for the object. If not provided, it will be generated automatically.', null=True, unique=True, verbose_name='Slug')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
('vin', models.CharField(max_length=17, unique=True, verbose_name='VIN')),
('year', models.IntegerField(verbose_name='Year')),
('status', models.CharField(choices=[('available', 'Available'), ('sold', 'Sold'), ('hold', 'Hold'), ('damaged', 'Damaged'), ('reserved', 'Reserved'), ('transfer', 'Transfer')], default='available', max_length=10, verbose_name='Status')),
('stock_type', models.CharField(choices=[('new', 'New'), ('used', 'Used')], default='new', max_length=10, verbose_name='Stock Type')),
('remarks', models.TextField(blank=True, null=True, verbose_name='Remarks')),
('mileage', models.IntegerField(blank=True, null=True, verbose_name='Mileage')),
('receiving_date', models.DateTimeField(verbose_name='Receiving Date')),
('hash', models.CharField(blank=True, max_length=64, null=True, verbose_name='Hash')),
('item_model', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='django_ledger.itemmodel', verbose_name='Item Model')),
('id_car_make', models.ForeignKey(blank=True, db_column='id_car_make', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake', verbose_name='Make')),
],
options={
'verbose_name': 'Car',
'verbose_name_plural': 'Cars',
},
),
migrations.CreateModel(
name='CarFinance',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('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')),
('is_sold', models.BooleanField(default=False)),
('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={
'verbose_name': 'Car Financial Details',
'verbose_name_plural': 'Car Financial Details',
},
),
migrations.CreateModel(
name='CarModel',
fields=[
('id_car_model', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=255, null=True)),
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('id_car_make', models.ForeignKey(db_column='id_car_make', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake')),
],
options={
'verbose_name': 'Model',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.AddField(
model_name='car',
name='id_car_model',
field=models.ForeignKey(blank=True, db_column='id_car_model', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel', verbose_name='Model'),
),
migrations.CreateModel(
name='CarOption',
fields=[
('id_car_option', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=255, null=True)),
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('id_parent', models.ForeignKey(blank=True, db_column='id_parent', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.caroption')),
],
options={
'verbose_name': 'Option',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='CarOptionValue',
fields=[
('id_car_option_value', models.AutoField(primary_key=True, serialize=False)),
('value', models.CharField(max_length=500)),
('unit', models.CharField(blank=True, max_length=255, null=True)),
('is_base', models.IntegerField()),
('id_car_equipment', models.ForeignKey(db_column='id_car_equipment', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carequipment')),
('id_car_option', models.ForeignKey(db_column='id_car_option', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.caroption')),
],
options={
'verbose_name': 'Option Value',
},
),
migrations.CreateModel(
name='CarRegistration',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('plate_number', models.IntegerField(verbose_name='Plate Number')),
('text1', models.CharField(max_length=1, verbose_name='Text 1')),
('text2', models.CharField(blank=True, max_length=1, null=True, verbose_name='Text 2')),
('text3', models.CharField(blank=True, max_length=1, null=True, verbose_name='Text 3')),
('registration_date', models.DateTimeField(verbose_name='Registration Date')),
('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='registrations', to='inventory.car', verbose_name='Car')),
],
options={
'verbose_name': 'Registration',
'verbose_name_plural': 'Registrations',
},
),
migrations.CreateModel(
name='CarSerie',
fields=[
('id_car_serie', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=255, null=True)),
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('year_begin', models.IntegerField(blank=True, null=True)),
('year_end', models.IntegerField(blank=True, null=True)),
('generation_name', models.CharField(blank=True, max_length=255, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('id_car_model', models.ForeignKey(db_column='id_car_model', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel')),
],
options={
'verbose_name': 'Series',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.AddField(
model_name='car',
name='id_car_serie',
field=models.ForeignKey(blank=True, db_column='id_car_serie', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carserie', verbose_name='Series'),
),
migrations.CreateModel(
name='CarSpecification',
fields=[
('id_car_specification', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)),
('arabic_name', models.CharField(max_length=255)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('id_parent', models.ForeignKey(blank=True, db_column='id_parent', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carspecification')),
],
options={
'verbose_name': 'Specification',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='CarTrim',
fields=[
('id_car_trim', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=255, null=True)),
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('start_production_year', models.IntegerField(blank=True, null=True)),
('end_production_year', models.IntegerField(blank=True, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('id_car_serie', models.ForeignKey(db_column='id_car_serie', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carserie')),
],
options={
'verbose_name': 'Trim',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='CarSpecificationValue',
fields=[
('id_car_specification_value', models.AutoField(primary_key=True, serialize=False)),
('value', models.CharField(max_length=500)),
('unit', models.CharField(blank=True, max_length=255, null=True)),
('id_car_specification', models.ForeignKey(db_column='id_car_specification', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carspecification')),
('id_car_trim', models.ForeignKey(db_column='id_car_trim', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.cartrim')),
],
options={
'verbose_name': 'Specification Value',
},
),
migrations.AddField(
model_name='carequipment',
name='id_car_trim',
field=models.ForeignKey(db_column='id_car_trim', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.cartrim'),
),
migrations.AddField(
model_name='car',
name='id_car_trim',
field=models.ForeignKey(blank=True, db_column='id_car_trim', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.cartrim', verbose_name='Trim'),
),
migrations.CreateModel(
name='CustomCard',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('custom_number', models.CharField(max_length=255, verbose_name='Custom Number')),
('custom_date', models.DateField(verbose_name='Custom Date')),
('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='custom_cards', to='inventory.car', verbose_name='Car')),
],
options={
'verbose_name': 'Custom Card',
'verbose_name_plural': 'Custom Cards',
},
),
migrations.CreateModel(
name='Dealer',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('crn', models.CharField(blank=True, max_length=10, null=True, verbose_name='Commercial Registration Number')),
('vrn', models.CharField(blank=True, max_length=15, null=True, verbose_name='VAT Registration Number')),
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
('name', models.CharField(max_length=255, verbose_name='English Name')),
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')),
('logo', models.ImageField(blank=True, null=True, upload_to='logos/users', verbose_name='Logo')),
('joined_at', models.DateTimeField(auto_now_add=True, verbose_name='Joined At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('entity', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_ledger.entitymodel')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='dealer', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Dealer',
'verbose_name_plural': 'Dealers',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
managers=[
('objects', inventory.models.DealerUserManager()),
],
),
migrations.CreateModel(
name='CustomGroup',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('group', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='auth.group', verbose_name='Group')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='groups', to='inventory.dealer')),
],
),
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')),
('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(blank=True, null=True, verbose_name='Date of Birth')),
('email', models.EmailField(max_length=254, unique=True, verbose_name='Email')),
('national_id', models.CharField(blank=True, max_length=10, null=True, unique=True, verbose_name='National ID')),
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', unique=True, verbose_name='Phone Number')),
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')),
('active', models.BooleanField(default=True, verbose_name='Active')),
('image', models.ImageField(blank=True, null=True, upload_to='customers/', verbose_name='Image')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('slug', models.SlugField(blank=True, editable=False, max_length=255, null=True, unique=True)),
('customer_model', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_ledger.customermodel')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='customer_profile', to=settings.AUTH_USER_MODEL)),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='customers', to='inventory.dealer')),
],
options={
'verbose_name': 'Customer',
'verbose_name_plural': 'Customers',
},
),
migrations.CreateModel(
name='CarTransfer',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('transfer_date', models.DateTimeField(auto_now_add=True, verbose_name='Transfer Date')),
('quantity', models.IntegerField(default=1, verbose_name='Quantity')),
('remarks', models.TextField(blank=True, null=True, verbose_name='Remarks')),
('status', models.CharField(default='draft', max_length=10, verbose_name=[('draft', 'Draft'), ('approved', 'Approved'), ('pending', 'Pending'), ('accepted', 'Accepted'), ('success', 'Success'), ('reject', 'Reject'), ('cancelled', 'Cancelled')])),
('is_approved', models.BooleanField(default=False)),
('active', models.BooleanField(default=True)),
('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(on_delete=django.db.models.deletion.CASCADE, related_name='transfer_logs', to='inventory.car', verbose_name='Car')),
('from_dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transfers_out', to='inventory.dealer', verbose_name='From Dealer')),
('to_dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transfers_in', to='inventory.dealer', verbose_name='To Dealer')),
],
options={
'verbose_name': 'Car Transfer Log',
'verbose_name_plural': 'Car Transfer Logs',
},
),
migrations.CreateModel(
name='CarLocation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('description', models.TextField(blank=True, help_text='Optional description about the showroom placement.', null=True, verbose_name='Description')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Updated')),
('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='location', to='inventory.car', verbose_name='Car')),
('owner', models.ForeignKey(help_text='Dealer who owns the car.', on_delete=django.db.models.deletion.CASCADE, related_name='owned_cars', to='inventory.dealer', verbose_name='Owner')),
('showroom', models.ForeignKey(help_text='Dealer where the car is displayed (can be the owner).', on_delete=django.db.models.deletion.CASCADE, related_name='showroom_cars', to='inventory.dealer', verbose_name='Showroom')),
],
options={
'verbose_name': 'Car Location',
'verbose_name_plural': 'Car Locations',
},
),
migrations.AddField(
model_name='car',
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='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'), ('meeting', 'Meeting'), ('whatsapp', 'WhatsApp'), ('visit', 'Visit'), ('negotiation', 'Negotiation'), ('follow_up', 'Follow Up'), ('won', 'Won'), ('lost', 'Lost'), ('closed', 'Closed'), ('converted', 'Converted'), ('transfer', 'Transfer'), ('add_car', 'Add Car'), ('sale_car', 'Sale Car'), ('reserve_car', 'Reserve Car'), ('transfer_car', 'Transfer Car'), ('remove_car', 'Remove Car'), ('create_quotation', 'Create Quotation'), ('cancel_quotation', 'Cancel Quotation'), ('create_order', 'Create Order'), ('cancel_order', 'Cancel Order'), ('create_invoice', 'Create Invoice'), ('cancel_invoice', 'Cancel Invoice')], max_length=50, verbose_name='Activity Type')),
('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.DO_NOTHING, to='contenttypes.contenttype')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='activities_created_by', to=settings.AUTH_USER_MODEL)),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activities', to='inventory.dealer')),
],
options={
'verbose_name': 'Activity',
'verbose_name_plural': 'Activities',
},
),
migrations.CreateModel(
name='DealerSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('additional_info', models.JSONField(blank=True, default=dict, null=True)),
('bill_cash_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bill_cash', to='django_ledger.accountmodel')),
('bill_prepaid_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bill_prepaid', to='django_ledger.accountmodel')),
('bill_unearned_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bill_unearned', to='django_ledger.accountmodel')),
('dealer', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='settings', to='inventory.dealer')),
('invoice_cash_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_cash', to='django_ledger.accountmodel')),
('invoice_prepaid_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_prepaid', to='django_ledger.accountmodel')),
('invoice_unearned_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_unearned', to='django_ledger.accountmodel')),
],
),
migrations.CreateModel(
name='Email',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.UUIDField()),
('from_email', models.TextField(blank=True, null=True, verbose_name='From Email')),
('to_email', models.TextField(blank=True, null=True, verbose_name='To Email')),
('subject', models.TextField(blank=True, null=True, verbose_name='Subject')),
('message', models.TextField(blank=True, null=True, verbose_name='Message')),
('status', models.CharField(choices=[('SENT', 'Sent'), ('FAILED', 'Failed'), ('DELIVERED', 'Delivered'), ('OPEN', 'Open'), ('DRAFT', 'Draft')], default='OPEN', max_length=20, verbose_name='Status')),
('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='emails_created', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Email',
'verbose_name_plural': 'Emails',
},
),
migrations.CreateModel(
name='Lead',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=50, verbose_name='First Name')),
('last_name', models.CharField(max_length=50, verbose_name='Last Name')),
('email', models.EmailField(max_length=254, verbose_name='Email')),
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')),
('lead_type', models.CharField(choices=[('customer', 'Customer'), ('organization', 'Organization')], default='customer', max_length=50, verbose_name='Lead Type')),
('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')),
('status', models.CharField(choices=[('new', 'New'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('unqualified', 'Unqualified'), ('converted', 'Converted')], db_index=True, default='new', max_length=50, verbose_name='Status')),
('next_action', models.CharField(blank=True, max_length=255, null=True, verbose_name='Next Action')),
('next_action_date', models.DateTimeField(blank=True, null=True, verbose_name='Next Action Date')),
('is_converted', models.BooleanField(default=False)),
('converted_at', models.DateTimeField(blank=True, null=True)),
('created', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('slug', models.SlugField(blank=True, null=True, unique=True)),
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='customer_leads', to='inventory.customer')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leads', to='inventory.dealer')),
('id_car_make', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake', verbose_name='Make')),
('id_car_model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel', verbose_name='Model')),
],
options={
'verbose_name': 'Lead',
'verbose_name_plural': 'Leads',
},
),
migrations.CreateModel(
name='Notes',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.UUIDField()),
('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)),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='inventory.dealer')),
],
options={
'verbose_name': 'Note',
'verbose_name_plural': 'Notes',
},
),
migrations.CreateModel(
name='Notification',
fields=[
('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', 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'],
},
),
migrations.CreateModel(
name='Organization',
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')),
('crn', models.CharField(max_length=15, verbose_name='Commercial Registration Number')),
('vrn', models.CharField(max_length=15, verbose_name='VAT Registration Number')),
('email', models.EmailField(max_length=254, verbose_name='Email')),
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')),
('logo', models.ImageField(blank=True, null=True, upload_to='logos', verbose_name='Logo')),
('active', models.BooleanField(default=True, verbose_name='Active')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('slug', models.SlugField(blank=True, editable=False, max_length=255, null=True, unique=True)),
('customer_model', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_ledger.customermodel')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='organizations', to='inventory.dealer')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='organization_profile', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Organization',
'verbose_name_plural': 'Organizations',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='Opportunity',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('crn', models.CharField(blank=True, max_length=20, null=True, verbose_name='CRN')),
('vrn', models.CharField(blank=True, max_length=20, null=True, verbose_name='VRN')),
('salary', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Salary')),
('priority', models.CharField(choices=[('high', 'High'), ('medium', 'Medium'), ('low', 'Low')], default='medium', max_length=20, verbose_name='Priority')),
('stage', models.CharField(choices=[('qualification', 'Qualification'), ('test_drive', 'Test Drive'), ('quotation', 'Quotation'), ('negotiation', 'Negotiation'), ('financing', 'Financing'), ('closed_won', 'Closed Won'), ('closed_lost', 'Closed Lost'), ('on_hold', 'On Hold')], max_length=20, verbose_name='Stage')),
('probability', models.PositiveIntegerField(validators=[inventory.models.validate_probability])),
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Amount')),
('expected_revenue', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Expected Revenue')),
('vehicle_of_interest_make', models.CharField(blank=True, max_length=50, null=True)),
('vehicle_of_interest_model', models.CharField(blank=True, max_length=100, null=True)),
('expected_close_date', models.DateField(blank=True, null=True)),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('slug', models.SlugField(blank=True, help_text='Unique slug for the opportunity.', null=True, unique=True, verbose_name='Slug')),
('loss_reason', models.CharField(blank=True, max_length=255, null=True)),
('car', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.car', verbose_name='Car')),
('customer', models.ForeignKey(blank=True, null=True, 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')),
('estimate', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='opportunity', to='django_ledger.estimatemodel')),
('lead', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='opportunity', to='inventory.lead')),
('organization', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.organization', verbose_name='Organization')),
],
options={
'verbose_name': 'Opportunity',
'verbose_name_plural': 'Opportunities',
},
),
migrations.AddField(
model_name='lead',
name='organization',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='organization_leads', to='inventory.organization'),
),
migrations.CreateModel(
name='Refund',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='amount')),
('reason', models.TextField(blank=True, verbose_name='reason')),
('refund_date', models.DateField(auto_now_add=True, verbose_name='refund date')),
('payment', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='refund', to='inventory.payment')),
],
options={
'verbose_name': 'refund',
'verbose_name_plural': 'refunds',
},
),
migrations.CreateModel(
name='Representative',
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')),
('id_number', models.CharField(max_length=10, unique=True, verbose_name='ID Number')),
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
('email', models.EmailField(max_length=255, verbose_name='Email Address')),
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='representatives', to='inventory.dealer')),
('organization', models.ManyToManyField(related_name='representatives', to='inventory.organization')),
],
options={
'verbose_name': 'Representative',
'verbose_name_plural': 'Representatives',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='SaleOrder',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('comments', models.TextField(blank=True, null=True)),
('formatted_order_id', models.CharField(editable=False, max_length=10, unique=True)),
('status', models.CharField(choices=[('PENDING_APPROVAL', 'Pending Approval'), ('APPROVED', 'Approved'), ('IN_FINANCING', 'In Financing'), ('PARTIALLY_PAID', 'Partially Paid'), ('FULLY_PAID', 'Fully Paid'), ('PENDING_DELIVERY', 'Pending Delivery'), ('DELIVERED', 'Delivered'), ('CANCELLED', 'Cancelled')], default='PENDING_APPROVAL', help_text='Current status of the sales order.', max_length=20)),
('order_date', models.DateTimeField(default=django.utils.timezone.now, help_text='The date and time the sales order was created.')),
('expected_delivery_date', models.DateField(blank=True, help_text='The planned date for vehicle delivery.', null=True)),
('actual_delivery_date', models.DateTimeField(blank=True, help_text='The actual date and time the vehicle was delivered.', null=True)),
('cancelled_date', models.DateTimeField(blank=True, help_text='The date and time the order was cancelled, if applicable.', null=True)),
('cancellation_reason', models.TextField(blank=True, help_text='Reason for cancellation, if applicable.', null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_by', models.ForeignKey(blank=True, help_text='The user who created this sales order.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_sales_orders', to=settings.AUTH_USER_MODEL)),
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sale_orders', to='inventory.customer', verbose_name='Customer')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sale_orders', to='inventory.dealer')),
('estimate', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sale_orders', to='django_ledger.estimatemodel', verbose_name='Estimate')),
('invoice', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sale_orders', to='django_ledger.invoicemodel', verbose_name='Invoice')),
('last_modified_by', models.ForeignKey(blank=True, help_text='The user who last modified this sales order.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modified_sales_orders', to=settings.AUTH_USER_MODEL)),
('opportunity', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sale_orders', to='inventory.opportunity', verbose_name='Opportunity')),
],
options={
'verbose_name': 'Sales Order',
'verbose_name_plural': 'Sales Orders',
'ordering': ['-order_date'],
},
),
migrations.CreateModel(
name='Schedule',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('purpose', models.CharField(choices=[('product_demo', 'Product Demo'), ('follow_up_call', 'Follow-Up Call'), ('contract_discussion', 'Contract Discussion'), ('sales_meeting', 'Sales Meeting'), ('support_call', 'Support Call'), ('other', 'Other')], max_length=200)),
('scheduled_at', models.DateTimeField()),
('scheduled_type', models.CharField(choices=[('call', 'Call'), ('meeting', 'Meeting'), ('email', 'Email')], default='Call', max_length=200)),
('duration', models.DurationField(default=datetime.timedelta(seconds=300))),
('notes', models.TextField(blank=True, null=True)),
('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('completed', 'Completed'), ('canceled', 'Canceled')], default='Scheduled', max_length=200)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='django_ledger.customermodel')),
('lead', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='inventory.lead')),
('scheduled_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-scheduled_at'],
},
),
migrations.CreateModel(
name='Staff',
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')),
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
('staff_type', models.CharField(choices=[('inventory', 'Inventory'), ('accountant', 'Accountant'), ('sales', 'Sales')], max_length=255, verbose_name='Staff Type')),
('active', models.BooleanField(default=True, verbose_name='Active')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('slug', models.SlugField(blank=True, editable=False, max_length=255, null=True, unique=True)),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='staff', to='inventory.dealer')),
('staff_member', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='staff', to='appointment.staffmember')),
],
options={
'verbose_name': 'Staff',
'verbose_name_plural': 'Staff',
'permissions': [],
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
managers=[
('objects', inventory.models.StaffUserManager()),
],
),
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.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'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('unqualified', 'Unqualified'), ('converted', 'Converted')], max_length=50, verbose_name='Old Status')),
('new_status', models.CharField(choices=[('new', 'New'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('unqualified', 'Unqualified'), ('converted', 'Converted')], 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='lead',
name='staff',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned', to='inventory.staff', verbose_name='Assigned'),
),
migrations.CreateModel(
name='Tasks',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.UUIDField()),
('title', models.CharField(max_length=255, verbose_name='Title')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('due_date', models.DateField(verbose_name='Due Date')),
('completed', models.BooleanField(default=False, verbose_name='Completed')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('assigned_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='tasks_assigned', to=settings.AUTH_USER_MODEL)),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='tasks_created', to=settings.AUTH_USER_MODEL)),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='inventory.dealer')),
],
options={
'verbose_name': 'Task',
'verbose_name_plural': 'Tasks',
},
),
migrations.CreateModel(
name='UserActivityLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('action', models.TextField()),
('timestamp', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'User Activity Log',
'verbose_name_plural': 'User Activity Logs',
'ordering': ['-timestamp'],
},
),
migrations.CreateModel(
name='Vendor',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('crn', models.CharField(max_length=10, unique=True, verbose_name='Commercial Registration Number')),
('vrn', models.CharField(max_length=15, unique=True, verbose_name='VAT Registration Number')),
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
('name', models.CharField(max_length=255, verbose_name='English Name')),
('contact_person', models.CharField(max_length=100, verbose_name='Contact Person')),
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
('email', models.EmailField(max_length=255, verbose_name='Email Address')),
('address', models.CharField(max_length=200, verbose_name='Address')),
('logo', models.ImageField(blank=True, null=True, upload_to='logos/vendors', verbose_name='Logo')),
('active', models.BooleanField(default=True, verbose_name='Active')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True, verbose_name='Slug')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vendors', to='inventory.dealer')),
('vendor_model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='django_ledger.vendormodel', verbose_name='Vendor Model')),
],
options={
'verbose_name': 'Vendor',
'verbose_name_plural': 'Vendors',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.AddField(
model_name='car',
name='vendor',
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='cars', to='inventory.vendor', verbose_name='Vendor'),
),
migrations.CreateModel(
name='CarReservation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reserved_at', models.DateTimeField(auto_now_add=True, verbose_name='Reserved At')),
('reserved_until', models.DateTimeField(verbose_name='Reserved Until')),
('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='inventory.car', verbose_name='Car')),
('reserved_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to=settings.AUTH_USER_MODEL, verbose_name='Reserved By')),
],
options={
'verbose_name': 'Car Reservation',
'verbose_name_plural': 'Car Reservations',
'ordering': ['-reserved_at'],
'unique_together': {('car', 'reserved_until')},
},
),
migrations.CreateModel(
name='DealersMake',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('added_at', models.DateTimeField(auto_now_add=True)),
('car_make', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='car_dealers', to='inventory.carmake')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dealer_makes', to='inventory.dealer')),
],
options={
'unique_together': {('dealer', 'car_make')},
},
),
migrations.CreateModel(
name='CarColors',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='colors', to='inventory.car')),
('exterior', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='colors', to='inventory.exteriorcolors')),
('interior', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='colors', to='inventory.interiorcolors')),
],
options={
'verbose_name': 'Color',
'verbose_name_plural': 'Colors',
'unique_together': {('car', 'exterior', 'interior')},
},
),
migrations.CreateModel(
name='PaymentHistory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user_data', models.JSONField(blank=True, null=True)),
('amount', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0.01)])),
('currency', models.CharField(default='SAR', max_length=3)),
('payment_date', models.DateTimeField(default=django.utils.timezone.now)),
('status', models.CharField(choices=[('initiated', 'initiated'), ('pending', 'Pending'), ('completed', 'Completed'), ('paid', 'Paid'), ('failed', 'Failed'), ('refunded', 'Refunded'), ('cancelled', 'Cancelled')], default='pending', max_length=10)),
('payment_method', models.CharField(choices=[('credit_card', 'Credit Card'), ('debit_card', 'Debit Card'), ('paypal', 'PayPal'), ('bank_transfer', 'Bank Transfer'), ('crypto', 'Cryptocurrency'), ('other', 'Other')], max_length=20)),
('transaction_id', models.CharField(blank=True, max_length=100, null=True, unique=True)),
('invoice_number', models.CharField(blank=True, max_length=50, null=True)),
('order_reference', models.CharField(blank=True, max_length=100, null=True)),
('gateway_response', models.JSONField(blank=True, null=True)),
('gateway_name', models.CharField(blank=True, max_length=50, null=True)),
('description', models.TextField(blank=True, null=True)),
('is_recurring', models.BooleanField(default=False)),
('billing_email', models.EmailField(blank=True, max_length=254, null=True)),
('billing_address', models.TextField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Payment History',
'verbose_name_plural': 'Payment Histories',
'ordering': ['-payment_date'],
'indexes': [models.Index(fields=['transaction_id'], name='inventory_p_transac_9469f3_idx'), models.Index(fields=['user'], name='inventory_p_user_id_c31626_idx'), models.Index(fields=['status'], name='inventory_p_status_abcb77_idx'), models.Index(fields=['payment_date'], name='inventory_p_payment_b3068c_idx')],
},
),
]

View File

@ -41,7 +41,7 @@ urlpatterns = [
path("dashboards/sales/", views.SalesDashboard.as_view(), name="sales_dashboard"), path("dashboards/sales/", views.SalesDashboard.as_view(), name="sales_dashboard"),
path("test/", views.TestView.as_view(), name="test"), path("test/", views.TestView.as_view(), name="test"),
path("cars/inventory/table/", views.CarListViewTable.as_view(), name="car_table"), path("cars/inventory/table/", views.CarListViewTable.as_view(), name="car_table"),
path("export/format/", TableExport, name="export"), path("export/format/", TableExport.export, name="export"),
# Dealer URLs # Dealer URLs
path( path(
"<slug:slug>/dealers/", views.DealerDetailView.as_view(), name="dealer_detail" "<slug:slug>/dealers/", views.DealerDetailView.as_view(), name="dealer_detail"
@ -440,63 +440,23 @@ urlpatterns = [
path("<slug:dealer_slug>/user/create/", views.UserCreateView.as_view(), name="user_create"), path("<slug:dealer_slug>/user/create/", views.UserCreateView.as_view(), name="user_create"),
path("<slug:dealer_slug>/user/<slug:slug>/", views.UserDetailView.as_view(), name="user_detail"), path("<slug:dealer_slug>/user/<slug:slug>/", views.UserDetailView.as_view(), name="user_detail"),
path("<slug:dealer_slug>/user/<slug:slug>/groups/", views.UserGroupView, name="user_groups"), path("<slug:dealer_slug>/user/<slug:slug>/groups/", views.UserGroupView, name="user_groups"),
path( path("<slug:dealer_slug>/user/<slug:slug>/update/", views.UserUpdateView.as_view(), name="user_update"),
"<slug:dealer_slug>/user/<slug:slug>/update/", views.UserUpdateView.as_view(), name="user_update"
),
path("<slug:dealer_slug>/user/<slug:slug>/confirm/", views.UserDeleteview, name="user_delete"), path("<slug:dealer_slug>/user/<slug:slug>/confirm/", views.UserDeleteview, name="user_delete"),
# Group URLs
path("<slug:dealer_slug>/group/create/", views.GroupCreateView.as_view(), name="group_create"), path("<slug:dealer_slug>/group/create/", views.GroupCreateView.as_view(), name="group_create"),
path( path("<slug:dealer_slug>/group/<int:pk>/update/", views.GroupUpdateView.as_view(), name="group_update"),
"<slug:dealer_slug>/group/<int:pk>/update/", views.GroupUpdateView.as_view(), name="group_update"
),
path("<slug:dealer_slug>/group/<int:pk>/", views.GroupDetailView.as_view(), name="group_detail"), path("<slug:dealer_slug>/group/<int:pk>/", views.GroupDetailView.as_view(), name="group_detail"),
path("<slug:dealer_slug>/group/", views.GroupListView.as_view(), name="group_list"), path("<slug:dealer_slug>/group/", views.GroupListView.as_view(), name="group_list"),
path("<slug:dealer_slug>/group/<int:pk>/confirm/", views.GroupDeleteview, name="group_delete"), path("<slug:dealer_slug>/group/<int:pk>/confirm/", views.GroupDeleteview, name="group_delete"),
path( path("<slug:dealer_slug>/group/<int:pk>/permission/", views.GroupPermissionView, name="group_permission"),
"<slug:dealer_slug>/group/<int:pk>/permission/", views.GroupPermissionView, name="group_permission" path("<slug:dealer_slug>/organizations/create/", views.OrganizationCreateView.as_view(), name="organization_create"),
), path("<slug:dealer_slug>/organizations/", views.OrganizationListView.as_view(), name="organization_list"),
# Organization URLs path("<slug:dealer_slug>/organizations/<slug:slug>/", views.OrganizationDetailView.as_view(), name="organization_detail"),
path( path("<slug:dealer_slug>/organizations/<slug:slug>/update/", views.OrganizationUpdateView.as_view(), name="organization_update"),
"<slug:dealer_slug>/organizations/create/", path("<slug:dealer_slug>/organizations/<slug:slug>/delete/", views.OrganizationDeleteView, name="organization_delete"),
views.OrganizationCreateView.as_view(), path("representatives/", views.RepresentativeListView.as_view(), name="representative_list"),
name="organization_create", path("representatives/<int:pk>/", views.RepresentativeDetailView.as_view(), name="representative_detail"),
), path("representatives/create/", views.RepresentativeCreateView.as_view(),name="representative_create"),
path( path("representatives/<int:pk>/update/",
"<slug:dealer_slug>/organizations/", views.OrganizationListView.as_view(), name="organization_list"
),
path(
"<slug:dealer_slug>/organizations/<slug:slug>/",
views.OrganizationDetailView.as_view(),
name="organization_detail",
),
path(
"<slug:dealer_slug>/organizations/<slug:slug>/update/",
views.OrganizationUpdateView.as_view(),
name="organization_update",
),
path(
"<slug:dealer_slug>/organizations/<slug:slug>/delete/",
views.OrganizationDeleteView,
name="organization_delete",
),
# Representative URLs
path(
"representatives/",
views.RepresentativeListView.as_view(),
name="representative_list",
),
path(
"representatives/<int:pk>/",
views.RepresentativeDetailView.as_view(),
name="representative_detail",
),
path(
"representatives/create/",
views.RepresentativeCreateView.as_view(),
name="representative_create",
),
path(
"representatives/<int:pk>/update/",
views.RepresentativeUpdateView.as_view(), views.RepresentativeUpdateView.as_view(),
name="representative_update", name="representative_update",
), ),
@ -505,9 +465,6 @@ urlpatterns = [
views.RepresentativeDeleteView.as_view(), views.RepresentativeDeleteView.as_view(),
name="representative_delete", name="representative_delete",
), ),
#####################################################################
# Ledger
#####################################################################
path("<slug:dealer_slug>/ledgers/", views.LedgerModelListView.as_view(), name="ledger_list"), path("<slug:dealer_slug>/ledgers/", views.LedgerModelListView.as_view(), name="ledger_list"),
path( path(
"<slug:dealer_slug>/ledgers/create/", views.LedgerModelCreateView.as_view(), name="ledger_create" "<slug:dealer_slug>/ledgers/create/", views.LedgerModelCreateView.as_view(), name="ledger_create"
@ -613,9 +570,6 @@ urlpatterns = [
views.LedgerModelDeleteView.as_view(), views.LedgerModelDeleteView.as_view(),
name="ledger-delete", name="ledger-delete",
), ),
##############################################################
# Bank Account
##############################################################
path( path(
"<slug:dealer_slug>/bank_accounts/", "<slug:dealer_slug>/bank_accounts/",
views.BankAccountListView.as_view(), views.BankAccountListView.as_view(),
@ -641,7 +595,6 @@ urlpatterns = [
views.bank_account_delete, views.bank_account_delete,
name="bank_account_delete", name="bank_account_delete",
), ),
# Account
path( path(
"<slug:dealer_slug>/coa_accounts/", "<slug:dealer_slug>/coa_accounts/",
views.AccountListView.as_view(), views.AccountListView.as_view(),

View File

@ -624,8 +624,9 @@ class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
return super().form_valid(form) return super().form_valid(form)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
dealer = get_user_type(self.request)
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["vendor_exists"] = self.request.dealer.vendors.exists() context["vendor_exists"] = dealer.vendors.exists()
return context return context

357
pyproject.toml Normal file
View File

@ -0,0 +1,357 @@
[project]
name = "car-inventory"
version = "0.1.0"
description = "Add your description here"
requires-python = ">=3.12"
dependencies = [
"aiohappyeyeballs>=2.6.1",
"aiohttp>=3.12.13",
"aiohttp-retry>=2.9.1",
"aiosignal>=1.3.2",
"alabaster>=1.0.0",
"albucore>=0.0.24",
"albumentations>=2.0.8",
"annotated-types>=0.7.0",
"anthropic>=0.55.0",
"anyio>=4.9.0",
"arabic-reshaper>=3.0.0",
"argcomplete>=3.6.2",
"arrow>=1.3.0",
"asgiref>=3.8.1",
"astor>=0.8.1",
"astroid>=3.3.10",
"attrs>=25.3.0",
"autopep8>=2.3.2",
"babel>=2.17.0",
"beautifulsoup4>=4.13.4",
"bleach>=6.2.0",
"blessed>=1.21.0",
"blinker>=1.9.0",
"boto3>=1.38.44",
"botocore>=1.38.44",
"brotli>=1.1.0",
"cachetools>=5.5.2",
"cattrs>=25.1.1",
"certifi>=2025.6.15",
"cffi>=1.17.1",
"chardet>=5.2.0",
"charset-normalizer>=3.4.2",
"click>=7.1.2",
"cohere>=5.15.0",
"colorama>=0.4.6",
"commonmark>=0.9.1",
"contourpy>=1.3.2",
"crispy-bootstrap5>=2025.6",
"cryptography>=45.0.4",
"cssselect2>=0.8.0",
"ctranslate2>=4.6.0",
"cycler>=0.12.1",
"cython>=3.1.2",
"dataclasses-json>=0.6.7",
"decorator>=5.2.1",
"defusedxml>=0.7.1",
"desert>=2020.11.18",
"diff-match-patch>=20241021",
"dill>=0.4.0",
"distro>=1.9.0",
"dj-rest-auth>=7.0.1",
"django>=5.2.3",
"django-allauth>=65.9.0",
"django-appointment>=3.6.0",
"django-autoslug>=1.9.9",
"django-background-tasks>=1.2.8",
"django-bootstrap5>=25.1",
"django-ckeditor>=6.7.3",
"django-classy-tags>=4.1.0",
"django-cors-headers>=4.7.0",
"django-countries>=7.6.1",
"django-crispy-forms>=2.4",
"django-debug-toolbar>=5.2.0",
"django-easy-audit>=1.3.7",
"django-extensions>=4.1",
"django-filter>=25.1",
"django-formtools>=2.5.1",
"django-import-export>=4.3.8",
"django-js-asset>=3.1.2",
"django-ledger==0.7.8",
"django-model-utils>=5.0.0",
"django-money>=3.5.4",
"django-next-url-mixin>=0.4.0",
"django-nine>=0.2.7",
"django-nonefield>=0.4",
"django-ordered-model>=3.7.4",
"django-pdf-actions>=0.1.52",
"django-phonenumber-field>=8.1.0",
"django-picklefield>=3.3",
"django-plans>=2.0.0",
"django-prometheus>=2.4.1",
"django-q2>=1.8.0",
"django-schema-graph>=3.1.0",
"django-sekizai>=4.1.0",
"django-sequences>=3.0",
"django-silk>=5.4.0",
"django-simple-history>=3.10.1",
"django-sms>=0.7.0",
"django-sslserver-v2>=1.0",
"django-tables2>=2.7.5",
"django-treebeard>=4.7.1",
"django-view-breadcrumbs>=2.5.1",
"django-widget-tweaks>=1.5.0",
"djangocms-admin-style>=3.3.1",
"djangorestframework>=3.16.0",
"djangorestframework-simplejwt>=5.5.0",
"djangoviz>=0.1.1",
"djhtml>=3.0.8",
"docopt>=0.6.2",
"docutils>=0.21.2",
"easy-thumbnails>=2.10",
"emoji>=2.14.1",
"et-xmlfile>=2.0.0",
"eval-type-backport>=0.2.2",
"executing>=2.2.0",
"faker>=37.4.0",
"fasta2a>=0.3.4",
"fastavro>=1.11.1",
"filelock>=3.18.0",
"fire>=0.7.0",
"fonttools>=4.58.4",
"fpdf>=1.7.2",
"fpdf2>=2.8.3",
"frozenlist>=1.7.0",
"fsspec>=2025.5.1",
"google-auth>=2.40.3",
"google-genai>=1.22.0",
"googleapis-common-protos>=1.70.0",
"gprof2dot>=2025.4.14",
"graphqlclient>=0.2.4",
"greenlet>=3.2.3",
"griffe>=1.7.3",
"groq>=0.29.0",
"h11>=0.16.0",
"h2>=4.2.0",
"hf-xet>=1.1.5",
"hpack>=4.1.0",
"hstspreload>=2025.1.1",
"httpcore>=1.0.9",
"httpx>=0.28.1",
"httpx-sse>=0.4.0",
"huggingface-hub>=0.33.1",
"hyperframe>=6.1.0",
"icalendar>=6.3.1",
"idna>=3.10",
"imageio>=2.37.0",
"imagesize>=1.4.1",
"imgaug>=0.4.0",
"importlib-metadata>=8.7.0",
"iso4217>=1.14.20250512",
"isodate>=0.7.2",
"isort>=6.0.1",
"itsdangerous>=2.2.0",
"jinja2>=3.1.6",
"jiter>=0.10.0",
"jmespath>=1.0.1",
"joblib>=1.5.1",
"jsonpatch>=1.33",
"jsonpointer>=3.0.0",
"jwt>=1.4.0",
"kiwisolver>=1.4.8",
"langchain>=0.3.26",
"langchain-community>=0.3.26",
"langchain-core>=0.3.66",
"langchain-ollama>=0.3.3",
"langchain-text-splitters>=0.3.8",
"langsmith>=0.4.2",
"lazy-loader>=0.4",
"ledger>=1.0.1",
"libretranslatepy>=2.1.4",
"lmdb>=1.6.2",
"logfire>=3.21.1",
"logfire-api>=3.21.1",
"luhnchecker>=0.0.12",
"lxml>=5.4.0",
"markdown>=3.8.2",
"markdown-it-py>=3.0.0",
"markupsafe>=3.0.2",
"marshmallow>=3.26.1",
"matplotlib>=3.10.3",
"mccabe>=0.7.0",
"mcp>=1.9.4",
"mdurl>=0.1.2",
"mistralai>=1.8.2",
"mouseinfo>=0.1.3",
"mpmath>=1.3.0",
"multidict>=6.5.1",
"mypy-extensions>=1.1.0",
"networkx>=3.5",
"newrelic>=10.14.0",
"nltk>=3.9.1",
"num2words>=0.5.14",
"numpy>=2.3.1",
"oauthlib>=3.3.1",
"ofxtools>=0.9.5",
"ollama>=0.5.1",
"openai>=1.91.0",
"opencv-contrib-python>=4.11.0.86",
"opencv-python>=4.11.0.86",
"opencv-python-headless>=4.11.0.86",
"openpyxl>=3.1.5",
"opentelemetry-api>=1.34.1",
"opentelemetry-exporter-otlp-proto-common>=1.34.1",
"opentelemetry-exporter-otlp-proto-http>=1.34.1",
"opentelemetry-instrumentation>=0.55b1",
"opentelemetry-proto>=1.34.1",
"opentelemetry-sdk>=1.34.1",
"opentelemetry-semantic-conventions>=0.55b1",
"opt-einsum>=3.4.0",
"orjson>=3.10.18",
"outcome>=1.3.0.post0",
"packaging>=24.2",
"pandas>=2.3.0",
"pango>=0.0.1",
"pdfkit>=1.0.0",
"phonenumbers>=9.0.8",
"pillow>=11.2.1",
"platformdirs>=4.3.8",
"prometheus-client>=0.22.1",
"prompt-toolkit>=3.0.51",
"propcache>=0.3.2",
"protobuf>=5.29.5",
"psycopg>=3.2.9",
"psycopg-binary>=3.2.9",
"psycopg-c>=3.2.9",
"psycopg2-binary>=2.9.10",
"py-moneyed>=3.0",
"pyasn1>=0.6.1",
"pyasn1-modules>=0.4.2",
"pyautogui>=0.9.54",
"pyclipper>=1.3.0.post6",
"pycodestyle>=2.14.0",
"pycparser>=2.22",
"pydantic>=2.11.7",
"pydantic-ai>=0.3.4",
"pydantic-ai-slim>=0.3.4",
"pydantic-core>=2.33.2",
"pydantic-evals>=0.3.4",
"pydantic-graph>=0.3.4",
"pydantic-settings>=2.10.1",
"pydotplus>=2.0.2",
"pydyf>=0.11.0",
"pygetwindow>=0.0.9",
"pygments>=2.19.2",
"pyjwt>=2.9.0",
"pylint>=3.3.7",
"pymsgbox>=1.0.9",
"pymysql>=1.1.1",
"pyobjc-core>=11.1",
"pyobjc-framework-cocoa>=11.1",
"pyobjc-framework-quartz>=11.1",
"pyparsing>=3.2.3",
"pypdf>=5.6.1",
"pyperclip>=1.9.0",
"pyphen>=0.17.2",
"pypng>=0.20220715.0",
"pyrect>=0.2.0",
"pyscreeze>=1.0.1",
"pyserial>=3.5",
"pysocks>=1.7.1",
"python-bidi>=0.6.6",
"python-dateutil>=2.9.0.post0",
"python-docx>=1.2.0",
"python-dotenv>=1.1.1",
"python-multipart>=0.0.20",
"python-openid>=2.2.5",
"python-slugify>=8.0.4",
"python-stdnum>=2.1",
"python3-saml>=1.16.0",
"pytweening>=1.2.0",
"pytz>=2025.2",
"pyvin>=0.0.2",
"pywa>=2.11.0",
"pywhat>=1.0.0",
"pywhatkit>=5.4",
"pyyaml>=6.0.2",
"pyzbar>=0.1.9",
"qrcode>=8.2",
"rapidfuzz>=3.13.0",
"redis>=6.2.0",
"regex>=2024.11.6",
"reportlab>=4.4.2",
"requests>=2.32.4",
"requests-oauthlib>=2.0.0",
"requests-toolbelt>=1.0.0",
"rfc3986>=2.0.0",
"rich>=14.0.0",
"rsa>=4.9.1",
"rubicon-objc>=0.5.1",
"s3transfer>=0.13.0",
"sacremoses>=0.1.1",
"safetensors>=0.5.3",
"scikit-image>=0.25.2",
"scikit-learn>=1.7.0",
"scipy>=1.16.0",
"selenium>=4.32.0",
"sentence-transformers>=4.1.0",
"sentencepiece>=0.2.0",
"shapely>=2.1.1",
"simsimd>=6.4.9",
"six>=1.17.0",
"slugify>=0.0.1",
"sniffio>=1.3.1",
"snowballstemmer>=3.0.1",
"sortedcontainers>=2.4.0",
"soupsieve>=2.7",
"sqlalchemy>=2.0.41",
"sqlparse>=0.5.3",
"sse-starlette>=2.3.6",
"stanza>=1.10.1",
"starlette>=0.47.1",
"stringzilla>=3.12.5",
"suds>=1.2.0",
"swapper>=1.3.0",
"sympy>=1.14.0",
"tablib>=3.8.0",
"tenacity>=8.5.0",
"termcolor>=3.1.0",
"text-unidecode>=1.3",
"threadpoolctl>=3.6.0",
"tifffile>=2025.6.11",
"tinycss2>=1.4.0",
"tinyhtml5>=2.0.0",
"tokenizers>=0.21.2",
"tomli>=2.2.1",
"tomlkit>=0.13.3",
"torch>=2.7.1",
"tqdm>=4.67.1",
"transformers>=4.52.4",
"trio>=0.30.0",
"trio-websocket>=0.12.2",
"twilio>=9.6.3",
"types-python-dateutil>=2.9.0.20250516",
"types-requests>=2.32.4.20250611",
"typing-extensions>=4.14.0",
"typing-inspect>=0.9.0",
"typing-inspection>=0.4.1",
"tzdata>=2025.2",
"unidecode>=1.4.0",
"upgrade-requirements>=1.7.0",
"urllib3>=2.5.0",
"uvicorn>=0.34.3",
"vin>=0.6.2",
"vininfo>=1.9.1",
"vishap>=0.1.5",
"vpic-api>=0.7.4",
"wcwidth>=0.2.13",
"weasyprint>=65.1",
"webencodings>=0.5.1",
"websocket-client>=1.8.0",
"websockets>=15.0.1",
"werkzeug>=3.1.3",
"wikipedia>=1.4.0",
"wrapt>=1.17.2",
"wsproto>=1.2.0",
"xmlsec>=1.3.15",
"yarl>=1.20.1",
"zipp>=3.23.0",
"zopfli>=0.2.3.post1",
"zstandard>=0.23.0",
]

View File

@ -1,350 +1,350 @@
aiohappyeyeballs==2.6.1 aiohappyeyeballs
aiohttp==3.12.0 aiohttp
aiohttp-retry==2.9.1 aiohttp-retry
aiosignal==1.3.2 aiosignal
alabaster==1.0.0 alabaster
albucore==0.0.24 albucore
albumentations==2.0.7 albumentations
annotated-types==0.7.0 annotated-types
anthropic==0.52.2 anthropic
anyio==4.9.0 anyio
arabic-reshaper==3.0.0 arabic-reshaper
argcomplete==3.6.2 argcomplete
arrow==1.3.0 arrow
asgiref==3.8.1 asgiref
astor==0.8.1 astor
astroid==3.3.10 astroid
attrs==25.3.0 attrs
autopep8==2.3.2 autopep8
Babel==2.15.0 Babel
beautifulsoup4==4.13.4 beautifulsoup4
bleach==6.2.0 bleach
blessed==1.21.0 blessed
blinker==1.9.0 blinker
boto3==1.38.29 boto3
botocore==1.38.29 botocore
Brotli==1.1.0 Brotli
cachetools==5.5.2 cachetools
cattrs==24.1.3 cattrs
certifi==2025.4.26 certifi
cffi==1.17.1 cffi
chardet==5.2.0 chardet
charset-normalizer==3.4.2 charset-normalizer
click==8.2.1 click
cohere==5.15.0 cohere
colorama==0.4.6 colorama
commonmark==0.9.1 commonmark
contourpy==1.3.2 contourpy
crispy-bootstrap5==2025.4 crispy-bootstrap5
cryptography==45.0.3 cryptography
cssselect2==0.8.0 cssselect2
ctranslate2==4.6.0 ctranslate2
cycler==0.12.1 cycler
Cython==3.1.1 Cython
dataclasses-json==0.6.7 dataclasses-json
decorator==5.2.1 decorator
defusedxml==0.7.1 defusedxml
desert==2020.11.18 desert
diff-match-patch==20241021 diff-match-patch
dill==0.4.0 dill
distro==1.9.0 distro
dj-rest-auth==7.0.1 dj-rest-auth
Django==5.2.1 Django
django-allauth==65.8.1 django-allauth
django-appointment==3.8.0 django-appointment
django-autoslug==1.9.9 django-autoslug
django-background-tasks==1.2.8 django-background-tasks
django-bootstrap5==25.1 django-bootstrap5
django-ckeditor==6.7.2 django-ckeditor
django-classy-tags==4.1.0 django-classy-tags
django-cors-headers==4.7.0 django-cors-headers
django-countries==7.6.1 django-countries
django-crispy-forms==2.4 django-crispy-forms
django-debug-toolbar==5.2.0 django-debug-toolbar
django-easy-audit==1.3.7 django-easy-audit
django-extensions==4.1 django-extensions
django-filter==25.1 django-filter
django-formtools==2.5.1 django-formtools
django-import-export==4.3.7 django-import-export
django-js-asset==3.1.2 django-js-asset
django-ledger==0.7.7 django-ledger
django-model-utils==5.0.0 django-model-utils
django-money==3.5.4 django-money
django-next-url-mixin==0.4.0 django-next-url-mixin
django-nine==0.2.7 django-nine
django-nonefield==0.4 django-nonefield
django-ordered-model==3.7.4 django-ordered-model
django-pdf-actions==0.1.49 django-pdf-actions
django-phonenumber-field==8.0.0 django-phonenumber-field
django-picklefield==3.3 django-picklefield
django-plans==2.0.0 django-plans
django-prometheus==2.3.1 django-prometheus
django-q2==1.8.0 django-q2
django-schema-graph==3.1.0 django-schema-graph
django-sekizai==4.1.0 django-sekizai
django-sequences==3.0 django-sequences
django-silk==5.3.2 django-silk
django-simple-history==3.8.0 django-simple-history
django-sms==0.7.0 django-sms
django-sslserver==0.22 django-sslserver
django-tables2==2.7.5 django-tables2
django-treebeard==4.7.1 django-treebeard
django-view-breadcrumbs==2.5.1 django-view-breadcrumbs
django-widget-tweaks==1.5.0 django-widget-tweaks
djangocms-admin-style==3.3.1 djangocms-admin-style
djangorestframework==3.16.0 djangorestframework
djangorestframework_simplejwt==5.5.0 djangorestframework_simplejwt
djangoviz==0.1.1 djangoviz
djhtml==3.0.8 djhtml
docopt==0.6.2 docopt
docutils==0.21.2 docutils
easy-thumbnails==2.10 easy-thumbnails
emoji==2.14.1 emoji
et_xmlfile==2.0.0 et_xmlfile
eval_type_backport==0.2.2 eval_type_backport
executing==2.2.0 executing
Faker==37.3.0 Faker
fasta2a==0.2.14 fasta2a
fastavro==1.11.1 fastavro
filelock==3.18.0 filelock
fire==0.7.0 fire
fonttools==4.58.0 fonttools
fpdf==1.7.2 fpdf
fpdf2==2.8.3 fpdf2
frozenlist==1.6.0 frozenlist
fsspec==2025.5.1 fsspec
google-auth==2.40.2 google-auth
google-genai==1.18.0 google-genai
googleapis-common-protos==1.70.0 googleapis-common-protos
gprof2dot==2025.4.14 gprof2dot
graphqlclient==0.2.4 graphqlclient
greenlet==3.2.2 greenlet
griffe==1.7.3 griffe
groq==0.26.0 groq
h11==0.16.0 h11
h2==4.2.0 h2
hf-xet==1.1.3 hf-xet
hpack==4.1.0 hpack
hstspreload==2025.1.1 hstspreload
httpcore==1.0.9 httpcore
httpx==0.28.1 httpx
httpx-sse==0.4.0 httpx-sse
huggingface-hub==0.32.4 huggingface-hub
hyperframe==6.1.0 hyperframe
icalendar==6.3.1 icalendar
idna==3.10 idna
imageio==2.37.0 imageio
imagesize==1.4.1 imagesize
imgaug==0.4.0 imgaug
importlib_metadata==8.7.0 importlib_metadata
iso4217==1.12.20240625 iso4217
isodate==0.7.2 isodate
isort==6.0.1 isort
itsdangerous==2.2.0 itsdangerous
Jinja2==3.1.6 Jinja2
jiter==0.10.0 jiter
jmespath==1.0.1 jmespath
joblib==1.5.1 joblib
jsonpatch==1.33 jsonpatch
jsonpointer==3.0.0 jsonpointer
jwt==1.3.1 jwt
kiwisolver==1.4.8 kiwisolver
langchain==0.3.25 langchain
langchain-community==0.3.24 langchain-community
langchain-core==0.3.61 langchain-core
langchain-ollama==0.3.3 langchain-ollama
langchain-text-splitters==0.3.8 langchain-text-splitters
langsmith==0.3.42 langsmith
lazy_loader==0.4 lazy_loader
ledger==1.0.1 ledger
libretranslatepy==2.1.4 libretranslatepy
lmdb==1.6.2 lmdb
logfire==3.18.0 logfire
logfire-api==3.17.0 logfire-api
luhnchecker==0.0.12 luhnchecker
lxml==5.4.0 lxml
Markdown==3.8 Markdown
markdown-it-py==3.0.0 markdown-it-py
MarkupSafe==3.0.2 MarkupSafe
marshmallow==3.26.1 marshmallow
matplotlib==3.10.3 matplotlib
mccabe==0.7.0 mccabe
mcp==1.9.2 mcp
mdurl==0.1.2 mdurl
mistralai==1.8.1 mistralai
MouseInfo==0.1.3 MouseInfo
mpmath==1.3.0 mpmath
multidict==6.4.4 multidict
mypy_extensions==1.1.0 mypy_extensions
networkx==3.4.2 networkx
newrelic==10.12.0 newrelic
nltk==3.9.1 nltk
num2words==0.5.14 num2words
numpy==2.2.6 numpy
oauthlib==3.2.2 oauthlib
ofxtools==0.9.5 ofxtools
ollama==0.4.8 ollama
openai==1.82.0 openai
opencv-contrib-python==4.11.0.86 opencv-contrib-python
opencv-python==4.11.0.86 opencv-python
opencv-python-headless==4.11.0.86 opencv-python-headless
openpyxl==3.1.5 openpyxl
opentelemetry-api==1.34.0 opentelemetry-api
opentelemetry-exporter-otlp-proto-common==1.34.0 opentelemetry-exporter-otlp-proto-common
opentelemetry-exporter-otlp-proto-http==1.34.0 opentelemetry-exporter-otlp-proto-http
opentelemetry-instrumentation==0.55b0 opentelemetry-instrumentation
opentelemetry-proto==1.34.0 opentelemetry-proto
opentelemetry-sdk==1.34.0 opentelemetry-sdk
opentelemetry-semantic-conventions==0.55b0 opentelemetry-semantic-conventions
opt_einsum==3.4.0 opt_einsum
orjson==3.10.18 orjson
outcome==1.3.0.post0 outcome
packaging==24.2 packaging
pandas==2.2.3 pandas
pango==0.0.1 pango
pdfkit==1.0.0 pdfkit
phonenumbers==8.13.42 phonenumbers
pillow==10.4.0 pillow
platformdirs==4.3.8 platformdirs
prometheus_client==0.22.0 prometheus_client
prompt_toolkit==3.0.51 prompt_toolkit
propcache==0.3.1 propcache
protobuf==5.29.5 protobuf
psycopg==3.2.9 psycopg
psycopg-binary==3.2.9 psycopg-binary
psycopg-c==3.2.9 psycopg-c
psycopg2-binary==2.9.10 psycopg2-binary
py-moneyed==3.0 py-moneyed
pyasn1==0.6.1 pyasn1
pyasn1_modules==0.4.2 pyasn1_modules
PyAutoGUI==0.9.54 PyAutoGUI
pyclipper==1.3.0.post6 pyclipper
pycodestyle==2.13.0 pycodestyle
pycparser==2.22 pycparser
pydantic==2.11.5 pydantic
pydantic-ai==0.2.14 pydantic-ai
pydantic-ai-slim==0.2.14 pydantic-ai-slim
pydantic-evals==0.2.14 pydantic-evals
pydantic-graph==0.2.14 pydantic-graph
pydantic-settings==2.9.1 pydantic-settings
pydantic_core==2.33.2 pydantic_core
pydotplus==2.0.2 pydotplus
pydyf==0.11.0 pydyf
PyGetWindow==0.0.9 PyGetWindow
Pygments==2.19.1 Pygments
PyJWT==2.10.1 PyJWT
pylint==3.3.7 pylint
PyMsgBox==1.0.9 PyMsgBox
PyMySQL==1.1.1 PyMySQL
pyobjc-core==11.0 pyobjc-core
pyobjc-framework-Cocoa==11.0 pyobjc-framework-Cocoa
pyobjc-framework-Quartz==11.0 pyobjc-framework-Quartz
pyparsing==3.2.3 pyparsing
pypdf==5.5.0 pypdf
pyperclip==1.9.0 pyperclip
pyphen==0.17.2 pyphen
pypng==0.20220715.0 pypng
PyRect==0.2.0 PyRect
PyScreeze==1.0.1 PyScreeze
pyserial==3.5 pyserial
PySocks==1.7.1 PySocks
python-bidi==0.6.6 python-bidi
python-dateutil==2.9.0.post0 python-dateutil
python-docx==1.1.2 python-docx
python-dotenv==1.1.0 python-dotenv
python-multipart==0.0.20 python-multipart
python-openid==2.2.5 python-openid
python-slugify==8.0.4 python-slugify
python-stdnum==2.1 python-stdnum
python3-saml==1.16.0 python3-saml
pytweening==1.2.0 pytweening
pytz==2025.2 pytz
pyvin==0.0.2 pyvin
pywa==2.10.0 pywa
pywhat==5.1.0 pywhat
pywhatkit==5.4 pywhatkit
PyYAML==6.0.2 PyYAML
pyzbar==0.1.9 pyzbar
qrcode==8.2 qrcode
RapidFuzz==3.13.0 RapidFuzz
redis==6.1.0 redis
regex==2024.11.6 regex
reportlab==4.4.1 reportlab
requests==2.32.3 requests
requests-oauthlib==2.0.0 requests-oauthlib
requests-toolbelt==1.0.0 requests-toolbelt
rfc3986==2.0.0 rfc3986
rich==14.0.0 rich
rsa==4.9.1 rsa
rubicon-objc==0.5.0 rubicon-objc
s3transfer==0.13.0 s3transfer
sacremoses==0.1.1 sacremoses
safetensors==0.5.3 safetensors
scikit-image==0.25.2 scikit-image
scikit-learn==1.6.1 scikit-learn
scipy==1.15.3 scipy
selenium==4.33.0 selenium
sentence-transformers==4.1.0 sentence-transformers
sentencepiece==0.2.0 sentencepiece
shapely==2.1.1 shapely
simsimd==6.2.1 simsimd
six==1.17.0 six
slugify==0.0.1 slugify
sniffio==1.3.1 sniffio
snowballstemmer==3.0.1 snowballstemmer
sortedcontainers==2.4.0 sortedcontainers
soupsieve==2.7 soupsieve
SQLAlchemy==2.0.41 SQLAlchemy
sqlparse==0.5.3 sqlparse
sse-starlette==2.3.6 sse-starlette
stanza==1.10.1 stanza
starlette==0.47.0 starlette
stringzilla==3.12.5 stringzilla
suds==1.2.0 suds
swapper==1.3.0 swapper
sympy==1.14.0 sympy
tablib==3.8.0 tablib
tenacity==9.1.2 tenacity
termcolor==3.1.0 termcolor
text-unidecode==1.3 text-unidecode
threadpoolctl==3.6.0 threadpoolctl
tifffile==2025.5.24 tifffile
tinycss2==1.4.0 tinycss2
tinyhtml5==2.0.0 tinyhtml5
tokenizers==0.21.1 tokenizers
tomli==2.2.1 tomli
tomlkit==0.13.2 tomlkit
torch==2.7.0 torch
tqdm==4.67.1 tqdm
transformers==4.52.4 transformers
trio==0.30.0 trio
trio-websocket==0.12.2 trio-websocket
twilio==9.6.1 twilio
types-python-dateutil==2.9.0.20250516 types-python-dateutil
types-requests==2.32.0.20250602 types-requests
typing-inspect==0.9.0 typing-inspect
typing-inspection==0.4.1 typing-inspection
typing_extensions==4.13.2 typing_extensions
tzdata==2025.2 tzdata
Unidecode==1.4.0 Unidecode
upgrade-requirements==1.7.0 upgrade-requirements
urllib3==2.4.0 urllib3
uvicorn==0.34.3 uvicorn
vin==0.6.2 vin
vininfo==1.8.0 vininfo
vishap==0.1.5 vishap
vpic-api==0.7.4 vpic-api
wcwidth==0.2.13 wcwidth
weasyprint==65.1 weasyprint
webencodings==0.5.1 webencodings
websocket-client==1.8.0 websocket-client
websockets==15.0.1 websockets
Werkzeug==3.1.3 Werkzeug
wikipedia==1.4.0 wikipedia
wrapt==1.17.2 wrapt
wsproto==1.2.0 wsproto
xmlsec==1.3.15 xmlsec
yarl==1.20.0 yarl
zipp==3.22.0 zipp
zopfli==0.2.3.post1 zopfli
zstandard==0.23.0 zstandard

BIN
static/.DS_Store vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -19,6 +19,3 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,90 +1,64 @@
{% load i18n static %} {% load i18n static %}
<div class="d-flex justify-content-between align-items-center mt-4 mb-3"> <div class="d-flex justify-content-between align-items-center mt-4 mb-3">
<div class="text-body-secondary"> <div class="text-body-secondary fw-bold fs-10">
{{ _("Showing") }} {{ page_obj.start_index }} {{ _("to") }} {{ page_obj.end_index }} {{ _("Showing") }} {{ page_obj.start_index }} {{ _("to") }} {{ page_obj.end_index }}
{{ _("of") }} {{ page_obj.paginator.count }} {{ _("results") }} {{ _("of") }} {{ page_obj.paginator.count }} {{ _("results") }}
</div> </div>
<nav aria-label="Page navigation"> <nav aria-label="Page navigation">
<ul class="pagination mb-0"> <ul class="pagination mb-0">
{# First Page Link #} {# First Page Link #}
{% if page_obj.has_previous %} {% if page_obj.has_previous %}
<li class="page-item rounded-md overflow-hidden"> <li class="page-item">
<a class="page-link px-3 py-2 border border-gray-300 bg-white text-blue-600 hover:bg-gray-100 transition-colors duration-200" href="?page=1{% if q %}&q={{q}}{% endif %}" aria-label="{% trans 'First' %}"> <a class="page-link" href="?page=1{% if q %}&q={{q}}{% endif %}">
<span class="fas fa-angle-double-left" aria-hidden="true"></span> <span class="fas fa-angle-double-{% if LANGUAGE_CODE == 'ar' %}right{% else %}left{% endif %}"> </span>
</a>
</a> </li>
</li> {% else %}
{% else %} <li class="page-item">
<li class="page-item disabled rounded-md overflow-hidden"> <span class="page-link">
<span class="page-link px-3 py-2 border border-gray-200 bg-gray-50 text-gray-400 cursor-not-allowed"> <span class="fas fa-chevron-{% if LANGUAGE_CODE == 'ar' %}right{% else %}left{% endif %}"> </span>
<span class="fas fa-angle-double-left" aria-hidden="true"></span> </span>
</li>
</span> {% endif %}
</li>
{% endif %}
{# Previous Page Link #} {# Previous Page Link #}
{% if page_obj.has_previous %} {% if page_obj.has_previous %}
<li class="page-item rounded-md overflow-hidden"> <li class="page-item">
<a class="page-link px-3 py-2 border border-gray-300 bg-white text-blue-600 hover:bg-gray-100 transition-colors duration-200" href="?page={{ page_obj.previous_page_number }}{% if q %}&q={{q}}{% endif %}" aria-label="{% trans 'Previous' %}"> <a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if q %}&q={{q}}{% endif %}">
<span class="fas fa-chevron-left" aria-hidden="true"></span> <span class="fas fa-chevron-{% if LANGUAGE_CODE == 'ar' %}right{% else %}left{% endif %}"></span>
</a>
</a> </li>
</li> {% endif %}
{% else %}
<li class="page-item disabled rounded-md overflow-hidden">
<span class="page-link px-3 py-2 border border-gray-200 bg-gray-50 text-gray-400 cursor-not-allowed">
<span class="fas fa-chevron-left" aria-hidden="true"></span>
</span>
</li>
{% endif %}
{# Page Numbers #} {# Page Numbers #}
{% for num in page_obj.paginator.page_range %} {% for num in page_obj.paginator.page_range %}
{% if num == 1 or num == page_obj.paginator.num_pages or num >= page_obj.number|add:-2 and num <= page_obj.number|add:2 %} {% if num == 1 or num == page_obj.paginator.num_pages or num >= page_obj.number|add:-2 and num <= page_obj.number|add:2 %}
<li class="page-item {% if num == page_obj.number %}active{% endif %}"> <li class="page-item {% if num == page_obj.number %}active{% endif %}">
<a class="page-link" {% if num == page_obj.number %}aria-current="page"{% endif %} <a class="page-link" {% if num == page_obj.number %}aria-current="page"{% endif %}
href="?page={{ num }}{% if q %}&q={{q}}{% endif %}"> href="?page={{ num }}{% if q %}&q={{q}}{% endif %}">
{{ num }} {{ num }}
</a> </a>
</li> </li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{# Next Page Link #} {# Next Page Link #}
{% if page_obj.has_next %} {% if page_obj.has_next %}
<li class="page-item rounded-md overflow-hidden"> <li class="page-item">
<a class="page-link px-3 py-2 border border-gray-300 bg-white text-blue-600 hover:bg-gray-100 transition-colors duration-200" href="?page={{ page_obj.next_page_number }}{% if q %}&q={{q}}{% endif %}" aria-label="{% trans 'Next' %}"> <a class="page-link" href="?page={{ page_obj.next_page_number }}{% if q %}&q={{q}}{% endif %}">
<span class="fas fa-chevron-right" aria-hidden="true"></span> <span class="fas fa-chevron-{% if LANGUAGE_CODE == 'ar' %}left{% else %}right{% endif %}"></span>
</a>
</a> </li>
</li> {% endif %}
{% else %}
<li class="page-item disabled rounded-md overflow-hidden">
<span class="page-link px-3 py-2 border border-gray-200 bg-gray-50 text-gray-400 cursor-not-allowed">
<span class="fas fa-chevron-right" aria-hidden="true"></span>
</span>
</li>
{% endif %}
{# Last Page Link #} {# Last Page Link #}
{% if page_obj.has_next %} {% if page_obj.has_next %}
<li class="page-item rounded-md overflow-hidden"> <li class="page-item">
<a class="page-link px-3 py-2 border border-gray-300 bg-white text-blue-600 hover:bg-gray-100 transition-colors duration-200" href="?page={{ page_obj.paginator.num_pages }}{% if q %}&q={{q}}{% endif %}" aria-label="{% trans 'Last' %}"> <a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if q %}&q={{q}}{% endif %}">
<span class="fas fa-angle-double-right" aria-hidden="true"></span> <span class="fas fa-angle-double-{% if LANGUAGE_CODE == 'ar' %}left{% else %}right{% endif %}"></span>
</a>
</a> </li>
</li> {% endif %}
{% else %} </ul>
<li class="page-item disabled rounded-md overflow-hidden"> </nav>
<span class="page-link px-3 py-2 border border-gray-200 bg-gray-50 text-gray-400 cursor-not-allowed">
<span class="fas fa-angle-double-right" aria-hidden="true"></span>
</span>
</li>
{% endif %}
</ul>
</nav>
</div> </div>

View File

@ -2,5 +2,5 @@ from django.contrib import admin
from . import models from . import models
# Register your models here. # Register your models here.
admin.site.register(models.Tour) # admin.site.register(models.Tour)
admin.site.register(models.TourCompletion) # admin.site.register(models.TourCompletion)

View File

@ -1,66 +0,0 @@
# Generated by Django 5.2.1 on 2025-06-06 14:05
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Tour",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
("description", models.TextField(blank=True)),
("slug", models.SlugField(unique=True)),
("tour_file", models.CharField(max_length=255)),
("is_active", models.BooleanField(default=True)),
],
),
migrations.CreateModel(
name="TourCompletion",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("completed_on", models.DateTimeField(auto_now_add=True)),
(
"tour",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="tours.tour"
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"unique_together": {("tour", "user")},
},
),
]

View File

@ -1,21 +1,21 @@
from django.db import models from django.db import models
class Tour(models.Model): # class Tour(models.Model):
name = models.CharField(max_length=100) # name = models.CharField(max_length=100)
description = models.TextField(blank=True) # description = models.TextField(blank=True)
slug = models.SlugField(unique=True) # slug = models.SlugField(unique=True)
tour_file = models.CharField(max_length=255) # tour_file = models.CharField(max_length=255)
is_active = models.BooleanField(default=True) # is_active = models.BooleanField(default=True)
#
def __str__(self): # def __str__(self):
return self.name # return self.name
#
#
class TourCompletion(models.Model): # class TourCompletion(models.Model):
tour = models.ForeignKey(Tour, on_delete=models.CASCADE) # tour = models.ForeignKey(Tour, on_delete=models.CASCADE)
user = models.ForeignKey("auth.User", on_delete=models.CASCADE) # user = models.ForeignKey("auth.User", on_delete=models.CASCADE)
completed_on = models.DateTimeField(auto_now_add=True) # completed_on = models.DateTimeField(auto_now_add=True)
#
class Meta: # class Meta:
unique_together = ("tour", "user") # unique_together = ("tour", "user")

View File

@ -2,8 +2,8 @@ from django.urls import path
from . import views from . import views
urlpatterns = [ urlpatterns = [
path("", views.tour_list, name="tour_list"), # path("", views.tour_list, name="tour_list"),
path("data/<slug:slug>/", views.get_tour_data, name="get_tour_data"), # path("data/<slug:slug>/", views.get_tour_data, name="get_tour_data"),
path("complete/<slug:slug>/", views.mark_tour_completed, name="mark_tour_complete"), # path("complete/<slug:slug>/", views.mark_tour_completed, name="mark_tour_complete"),
path("start/<slug:slug>/", views.start_tour_view, name="start_tour"), # path("start/<slug:slug>/", views.start_tour_view, name="start_tour"),
] ]

View File

@ -1,54 +1,54 @@
import os # import os
import json # import json
from django.shortcuts import render, get_object_or_404 # from django.shortcuts import render, get_object_or_404
from django.http import JsonResponse # from django.http import JsonResponse
from django.contrib.auth.decorators import login_required # from django.contrib.auth.decorators import login_required
from django.conf import settings # from django.conf import settings
from .models import Tour, TourCompletion # from .models import Tour, TourCompletion
#
#
@login_required # @login_required
def tour_list(request): # def tour_list(request):
tours = Tour.objects.filter(is_active=True) # tours = Tour.objects.filter(is_active=True)
return render(request, "tours/tour_list.html", {"tours": tours}) # return render(request, "tours/tour_list.html", {"tours": tours})
#
#
@login_required # @login_required
def get_tour_data(request, slug): # def get_tour_data(request, slug):
tour = get_object_or_404(Tour, slug=slug, is_active=True) # tour = get_object_or_404(Tour, slug=slug, is_active=True)
#
# Check if user has already completed this tour # # Check if user has already completed this tour
completed = TourCompletion.objects.filter(tour=tour, user=request.user).exists() # completed = TourCompletion.objects.filter(tour=tour, user=request.user).exists()
#
# Load the tour data from JSON file # # Load the tour data from JSON file
tour_file_path = os.path.join( # tour_file_path = os.path.join(
settings.BASE_DIR, "static", "js", "tours", tour.tour_file # settings.BASE_DIR, "static", "js", "tours", tour.tour_file
) # )
#
try: # try:
with open(tour_file_path, "r") as f: # with open(tour_file_path, "r") as f:
tour_data = json.load(f) # tour_data = json.load(f)
except (FileNotFoundError, json.JSONDecodeError): # except (FileNotFoundError, json.JSONDecodeError):
return JsonResponse({"error": "Tour data not found or invalid"}, status=404) # return JsonResponse({"error": "Tour data not found or invalid"}, status=404)
#
return JsonResponse({"tour": tour_data, "completed": completed}) # return JsonResponse({"tour": tour_data, "completed": completed})
#
#
@login_required # @login_required
def mark_tour_completed(request, slug): # def mark_tour_completed(request, slug):
if request.method != "POST": # if request.method != "POST":
return JsonResponse({"error": "Method not allowed"}, status=405) # return JsonResponse({"error": "Method not allowed"}, status=405)
#
tour = get_object_or_404(Tour, slug=slug, is_active=True) # tour = get_object_or_404(Tour, slug=slug, is_active=True)
#
# Mark the tour as completed for this user # # Mark the tour as completed for this user
TourCompletion.objects.get_or_create(tour=tour, user=request.user) # TourCompletion.objects.get_or_create(tour=tour, user=request.user)
#
return JsonResponse({"status": "success"}) # return JsonResponse({"status": "success"})
#
#
@login_required # @login_required
def start_tour_view(request, slug): # def start_tour_view(request, slug):
tour = get_object_or_404(Tour, slug=slug, is_active=True) # tour = get_object_or_404(Tour, slug=slug, is_active=True)
# Redirect to the page where the tour should start # # Redirect to the page where the tour should start
return render(request, "tours/start_tour.html", {"tour": tour}) # return render(request, "tours/start_tour.html", {"tour": tour})

6284
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff