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 name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
</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="library" name="jquery-3.5.1" level="application" />
<orderEntry type="library" name="sweetalert2" level="application" />

5
.idea/misc.xml generated
View File

@ -3,8 +3,5 @@
<component name="Black">
<option name="sdkName" value="Python 3.11 (car_inventory)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (car_inventory)" project-jdk-type="Python SDK" />
<component name="PyPackaging">
<option name="earlyReleasesAsUpgrades" value="true" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="uv (car_inventory)" project-jdk-type="Python SDK" />
</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 inventory.utils import get_user_type
def currency_context(request):
"""
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):
# return redirect(reverse('verify_otp'))
# return self.get_response(request)
class DealerSlugMiddleware:
def __init__(self, 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("test/", views.TestView.as_view(), name="test"),
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
path(
"<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/<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>/update/", views.UserUpdateView.as_view(), name="user_update"
),
path("<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"),
# Group URLs
path("<slug:dealer_slug>/group/create/", views.GroupCreateView.as_view(), name="group_create"),
path(
"<slug:dealer_slug>/group/<int:pk>/update/", views.GroupUpdateView.as_view(), name="group_update"
),
path("<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/", 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>/permission/", views.GroupPermissionView, name="group_permission"
),
# Organization URLs
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"
),
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/",
path("<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"),
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"),
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(),
name="representative_update",
),
@ -505,9 +465,6 @@ urlpatterns = [
views.RepresentativeDeleteView.as_view(),
name="representative_delete",
),
#####################################################################
# Ledger
#####################################################################
path("<slug:dealer_slug>/ledgers/", views.LedgerModelListView.as_view(), name="ledger_list"),
path(
"<slug:dealer_slug>/ledgers/create/", views.LedgerModelCreateView.as_view(), name="ledger_create"
@ -613,9 +570,6 @@ urlpatterns = [
views.LedgerModelDeleteView.as_view(),
name="ledger-delete",
),
##############################################################
# Bank Account
##############################################################
path(
"<slug:dealer_slug>/bank_accounts/",
views.BankAccountListView.as_view(),
@ -641,7 +595,6 @@ urlpatterns = [
views.bank_account_delete,
name="bank_account_delete",
),
# Account
path(
"<slug:dealer_slug>/coa_accounts/",
views.AccountListView.as_view(),

View File

@ -624,8 +624,9 @@ class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
return super().form_valid(form)
def get_context_data(self, **kwargs):
dealer = get_user_type(self.request)
context = super().get_context_data(**kwargs)
context["vendor_exists"] = self.request.dealer.vendors.exists()
context["vendor_exists"] = dealer.vendors.exists()
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
aiohttp==3.12.0
aiohttp-retry==2.9.1
aiosignal==1.3.2
alabaster==1.0.0
albucore==0.0.24
albumentations==2.0.7
annotated-types==0.7.0
anthropic==0.52.2
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.15.0
beautifulsoup4==4.13.4
bleach==6.2.0
blessed==1.21.0
blinker==1.9.0
boto3==1.38.29
botocore==1.38.29
Brotli==1.1.0
cachetools==5.5.2
cattrs==24.1.3
certifi==2025.4.26
cffi==1.17.1
chardet==5.2.0
charset-normalizer==3.4.2
click==8.2.1
cohere==5.15.0
colorama==0.4.6
commonmark==0.9.1
contourpy==1.3.2
crispy-bootstrap5==2025.4
cryptography==45.0.3
cssselect2==0.8.0
ctranslate2==4.6.0
cycler==0.12.1
Cython==3.1.1
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.1
django-allauth==65.8.1
django-appointment==3.8.0
django-autoslug==1.9.9
django-background-tasks==1.2.8
django-bootstrap5==25.1
django-ckeditor==6.7.2
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.7
django-js-asset==3.1.2
django-ledger==0.7.7
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.49
django-phonenumber-field==8.0.0
django-picklefield==3.3
django-plans==2.0.0
django-prometheus==2.3.1
django-q2==1.8.0
django-schema-graph==3.1.0
django-sekizai==4.1.0
django-sequences==3.0
django-silk==5.3.2
django-simple-history==3.8.0
django-sms==0.7.0
django-sslserver==0.22
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.3.0
fasta2a==0.2.14
fastavro==1.11.1
filelock==3.18.0
fire==0.7.0
fonttools==4.58.0
fpdf==1.7.2
fpdf2==2.8.3
frozenlist==1.6.0
fsspec==2025.5.1
google-auth==2.40.2
google-genai==1.18.0
googleapis-common-protos==1.70.0
gprof2dot==2025.4.14
graphqlclient==0.2.4
greenlet==3.2.2
griffe==1.7.3
groq==0.26.0
h11==0.16.0
h2==4.2.0
hf-xet==1.1.3
hpack==4.1.0
hstspreload==2025.1.1
httpcore==1.0.9
httpx==0.28.1
httpx-sse==0.4.0
huggingface-hub==0.32.4
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.12.20240625
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.3.1
kiwisolver==1.4.8
langchain==0.3.25
langchain-community==0.3.24
langchain-core==0.3.61
langchain-ollama==0.3.3
langchain-text-splitters==0.3.8
langsmith==0.3.42
lazy_loader==0.4
ledger==1.0.1
libretranslatepy==2.1.4
lmdb==1.6.2
logfire==3.18.0
logfire-api==3.17.0
luhnchecker==0.0.12
lxml==5.4.0
Markdown==3.8
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.2
mdurl==0.1.2
mistralai==1.8.1
MouseInfo==0.1.3
mpmath==1.3.0
multidict==6.4.4
mypy_extensions==1.1.0
networkx==3.4.2
newrelic==10.12.0
nltk==3.9.1
num2words==0.5.14
numpy==2.2.6
oauthlib==3.2.2
ofxtools==0.9.5
ollama==0.4.8
openai==1.82.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.0
opentelemetry-exporter-otlp-proto-common==1.34.0
opentelemetry-exporter-otlp-proto-http==1.34.0
opentelemetry-instrumentation==0.55b0
opentelemetry-proto==1.34.0
opentelemetry-sdk==1.34.0
opentelemetry-semantic-conventions==0.55b0
opt_einsum==3.4.0
orjson==3.10.18
outcome==1.3.0.post0
packaging==24.2
pandas==2.2.3
pango==0.0.1
pdfkit==1.0.0
phonenumbers==8.13.42
pillow==10.4.0
platformdirs==4.3.8
prometheus_client==0.22.0
prompt_toolkit==3.0.51
propcache==0.3.1
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.13.0
pycparser==2.22
pydantic==2.11.5
pydantic-ai==0.2.14
pydantic-ai-slim==0.2.14
pydantic-evals==0.2.14
pydantic-graph==0.2.14
pydantic-settings==2.9.1
pydantic_core==2.33.2
pydotplus==2.0.2
pydyf==0.11.0
PyGetWindow==0.0.9
Pygments==2.19.1
PyJWT==2.10.1
pylint==3.3.7
PyMsgBox==1.0.9
PyMySQL==1.1.1
pyobjc-core==11.0
pyobjc-framework-Cocoa==11.0
pyobjc-framework-Quartz==11.0
pyparsing==3.2.3
pypdf==5.5.0
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.1.2
python-dotenv==1.1.0
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.10.0
pywhat==5.1.0
pywhatkit==5.4
PyYAML==6.0.2
pyzbar==0.1.9
qrcode==8.2
RapidFuzz==3.13.0
redis==6.1.0
regex==2024.11.6
reportlab==4.4.1
requests==2.32.3
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.0
s3transfer==0.13.0
sacremoses==0.1.1
safetensors==0.5.3
scikit-image==0.25.2
scikit-learn==1.6.1
scipy==1.15.3
selenium==4.33.0
sentence-transformers==4.1.0
sentencepiece==0.2.0
shapely==2.1.1
simsimd==6.2.1
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.0
stringzilla==3.12.5
suds==1.2.0
swapper==1.3.0
sympy==1.14.0
tablib==3.8.0
tenacity==9.1.2
termcolor==3.1.0
text-unidecode==1.3
threadpoolctl==3.6.0
tifffile==2025.5.24
tinycss2==1.4.0
tinyhtml5==2.0.0
tokenizers==0.21.1
tomli==2.2.1
tomlkit==0.13.2
torch==2.7.0
tqdm==4.67.1
transformers==4.52.4
trio==0.30.0
trio-websocket==0.12.2
twilio==9.6.1
types-python-dateutil==2.9.0.20250516
types-requests==2.32.0.20250602
typing-inspect==0.9.0
typing-inspection==0.4.1
typing_extensions==4.13.2
tzdata==2025.2
Unidecode==1.4.0
upgrade-requirements==1.7.0
urllib3==2.4.0
uvicorn==0.34.3
vin==0.6.2
vininfo==1.8.0
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.0
zipp==3.22.0
zopfli==0.2.3.post1
zstandard==0.23.0
aiohappyeyeballs
aiohttp
aiohttp-retry
aiosignal
alabaster
albucore
albumentations
annotated-types
anthropic
anyio
arabic-reshaper
argcomplete
arrow
asgiref
astor
astroid
attrs
autopep8
Babel
beautifulsoup4
bleach
blessed
blinker
boto3
botocore
Brotli
cachetools
cattrs
certifi
cffi
chardet
charset-normalizer
click
cohere
colorama
commonmark
contourpy
crispy-bootstrap5
cryptography
cssselect2
ctranslate2
cycler
Cython
dataclasses-json
decorator
defusedxml
desert
diff-match-patch
dill
distro
dj-rest-auth
Django
django-allauth
django-appointment
django-autoslug
django-background-tasks
django-bootstrap5
django-ckeditor
django-classy-tags
django-cors-headers
django-countries
django-crispy-forms
django-debug-toolbar
django-easy-audit
django-extensions
django-filter
django-formtools
django-import-export
django-js-asset
django-ledger
django-model-utils
django-money
django-next-url-mixin
django-nine
django-nonefield
django-ordered-model
django-pdf-actions
django-phonenumber-field
django-picklefield
django-plans
django-prometheus
django-q2
django-schema-graph
django-sekizai
django-sequences
django-silk
django-simple-history
django-sms
django-sslserver
django-tables2
django-treebeard
django-view-breadcrumbs
django-widget-tweaks
djangocms-admin-style
djangorestframework
djangorestframework_simplejwt
djangoviz
djhtml
docopt
docutils
easy-thumbnails
emoji
et_xmlfile
eval_type_backport
executing
Faker
fasta2a
fastavro
filelock
fire
fonttools
fpdf
fpdf2
frozenlist
fsspec
google-auth
google-genai
googleapis-common-protos
gprof2dot
graphqlclient
greenlet
griffe
groq
h11
h2
hf-xet
hpack
hstspreload
httpcore
httpx
httpx-sse
huggingface-hub
hyperframe
icalendar
idna
imageio
imagesize
imgaug
importlib_metadata
iso4217
isodate
isort
itsdangerous
Jinja2
jiter
jmespath
joblib
jsonpatch
jsonpointer
jwt
kiwisolver
langchain
langchain-community
langchain-core
langchain-ollama
langchain-text-splitters
langsmith
lazy_loader
ledger
libretranslatepy
lmdb
logfire
logfire-api
luhnchecker
lxml
Markdown
markdown-it-py
MarkupSafe
marshmallow
matplotlib
mccabe
mcp
mdurl
mistralai
MouseInfo
mpmath
multidict
mypy_extensions
networkx
newrelic
nltk
num2words
numpy
oauthlib
ofxtools
ollama
openai
opencv-contrib-python
opencv-python
opencv-python-headless
openpyxl
opentelemetry-api
opentelemetry-exporter-otlp-proto-common
opentelemetry-exporter-otlp-proto-http
opentelemetry-instrumentation
opentelemetry-proto
opentelemetry-sdk
opentelemetry-semantic-conventions
opt_einsum
orjson
outcome
packaging
pandas
pango
pdfkit
phonenumbers
pillow
platformdirs
prometheus_client
prompt_toolkit
propcache
protobuf
psycopg
psycopg-binary
psycopg-c
psycopg2-binary
py-moneyed
pyasn1
pyasn1_modules
PyAutoGUI
pyclipper
pycodestyle
pycparser
pydantic
pydantic-ai
pydantic-ai-slim
pydantic-evals
pydantic-graph
pydantic-settings
pydantic_core
pydotplus
pydyf
PyGetWindow
Pygments
PyJWT
pylint
PyMsgBox
PyMySQL
pyobjc-core
pyobjc-framework-Cocoa
pyobjc-framework-Quartz
pyparsing
pypdf
pyperclip
pyphen
pypng
PyRect
PyScreeze
pyserial
PySocks
python-bidi
python-dateutil
python-docx
python-dotenv
python-multipart
python-openid
python-slugify
python-stdnum
python3-saml
pytweening
pytz
pyvin
pywa
pywhat
pywhatkit
PyYAML
pyzbar
qrcode
RapidFuzz
redis
regex
reportlab
requests
requests-oauthlib
requests-toolbelt
rfc3986
rich
rsa
rubicon-objc
s3transfer
sacremoses
safetensors
scikit-image
scikit-learn
scipy
selenium
sentence-transformers
sentencepiece
shapely
simsimd
six
slugify
sniffio
snowballstemmer
sortedcontainers
soupsieve
SQLAlchemy
sqlparse
sse-starlette
stanza
starlette
stringzilla
suds
swapper
sympy
tablib
tenacity
termcolor
text-unidecode
threadpoolctl
tifffile
tinycss2
tinyhtml5
tokenizers
tomli
tomlkit
torch
tqdm
transformers
trio
trio-websocket
twilio
types-python-dateutil
types-requests
typing-inspect
typing-inspection
typing_extensions
tzdata
Unidecode
upgrade-requirements
urllib3
uvicorn
vin
vininfo
vishap
vpic-api
wcwidth
weasyprint
webencodings
websocket-client
websockets
Werkzeug
wikipedia
wrapt
wsproto
xmlsec
yarl
zipp
zopfli
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>
{% endblock %}

View File

@ -1,90 +1,64 @@
{% load i18n static %}
<div class="d-flex justify-content-between align-items-center mt-4 mb-3">
<div class="text-body-secondary">
{{ _("Showing") }} {{ page_obj.start_index }} {{ _("to") }} {{ page_obj.end_index }}
{{ _("of") }} {{ page_obj.paginator.count }} {{ _("results") }}
</div>
<nav aria-label="Page navigation">
<ul class="pagination mb-0">
<div class="text-body-secondary fw-bold fs-10">
{{ _("Showing") }} {{ page_obj.start_index }} {{ _("to") }} {{ page_obj.end_index }}
{{ _("of") }} {{ page_obj.paginator.count }} {{ _("results") }}
</div>
<nav aria-label="Page navigation">
<ul class="pagination mb-0">
{# First Page Link #}
{% if page_obj.has_previous %}
<li class="page-item rounded-md overflow-hidden">
<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' %}">
<span class="fas fa-angle-double-left" aria-hidden="true"></span>
</a>
</li>
{% 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-angle-double-left" aria-hidden="true"></span>
</span>
</li>
{% endif %}
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1{% if q %}&q={{q}}{% endif %}">
<span class="fas fa-angle-double-{% if LANGUAGE_CODE == 'ar' %}right{% else %}left{% endif %}"> </span>
</a>
</li>
{% else %}
<li class="page-item">
<span class="page-link">
<span class="fas fa-chevron-{% if LANGUAGE_CODE == 'ar' %}right{% else %}left{% endif %}"> </span>
</span>
</li>
{% endif %}
{# Previous Page Link #}
{% if page_obj.has_previous %}
<li class="page-item rounded-md overflow-hidden">
<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' %}">
<span class="fas fa-chevron-left" aria-hidden="true"></span>
</a>
</li>
{% 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 %}
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if q %}&q={{q}}{% endif %}">
<span class="fas fa-chevron-{% if LANGUAGE_CODE == 'ar' %}right{% else %}left{% endif %}"></span>
</a>
</li>
{% endif %}
{# Page Numbers #}
{% 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 %}
<li class="page-item {% if num == page_obj.number %}active{% endif %}">
<a class="page-link" {% if num == page_obj.number %}aria-current="page"{% endif %}
href="?page={{ num }}{% if q %}&q={{q}}{% endif %}">
{{ num }}
</a>
</li>
{% endif %}
{% endfor %}
{% 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 %}
<li class="page-item {% if num == page_obj.number %}active{% endif %}">
<a class="page-link" {% if num == page_obj.number %}aria-current="page"{% endif %}
href="?page={{ num }}{% if q %}&q={{q}}{% endif %}">
{{ num }}
</a>
</li>
{% endif %}
{% endfor %}
{# Next Page Link #}
{% if page_obj.has_next %}
<li class="page-item rounded-md overflow-hidden">
<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' %}">
<span class="fas fa-chevron-right" aria-hidden="true"></span>
</a>
</li>
{% 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 %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if q %}&q={{q}}{% endif %}">
<span class="fas fa-chevron-{% if LANGUAGE_CODE == 'ar' %}left{% else %}right{% endif %}"></span>
</a>
</li>
{% endif %}
{# Last Page Link #}
{% if page_obj.has_next %}
<li class="page-item rounded-md overflow-hidden">
<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' %}">
<span class="fas fa-angle-double-right" aria-hidden="true"></span>
</a>
</li>
{% 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-angle-double-right" aria-hidden="true"></span>
</span>
</li>
{% endif %}
</ul>
</nav>
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if q %}&q={{q}}{% endif %}">
<span class="fas fa-angle-double-{% if LANGUAGE_CODE == 'ar' %}left{% else %}right{% endif %}"></span>
</a>
</li>
{% endif %}
</ul>
</nav>
</div>

View File

@ -2,5 +2,5 @@ from django.contrib import admin
from . import models
# Register your models here.
admin.site.register(models.Tour)
admin.site.register(models.TourCompletion)
# admin.site.register(models.Tour)
# 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
class Tour(models.Model):
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)
def __str__(self):
return self.name
class TourCompletion(models.Model):
tour = models.ForeignKey(Tour, on_delete=models.CASCADE)
user = models.ForeignKey("auth.User", on_delete=models.CASCADE)
completed_on = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ("tour", "user")
# class Tour(models.Model):
# 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)
#
# def __str__(self):
# return self.name
#
#
# class TourCompletion(models.Model):
# tour = models.ForeignKey(Tour, on_delete=models.CASCADE)
# user = models.ForeignKey("auth.User", on_delete=models.CASCADE)
# completed_on = models.DateTimeField(auto_now_add=True)
#
# class Meta:
# unique_together = ("tour", "user")

View File

@ -2,8 +2,8 @@ from django.urls import path
from . import views
urlpatterns = [
path("", views.tour_list, name="tour_list"),
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("start/<slug:slug>/", views.start_tour_view, name="start_tour"),
# path("", views.tour_list, name="tour_list"),
# 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("start/<slug:slug>/", views.start_tour_view, name="start_tour"),
]

View File

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