diff --git a/.gitignore b/.gitignore index 450418e5..3643e961 100644 --- a/.gitignore +++ b/.gitignore @@ -116,6 +116,7 @@ celerybeat-schedule.* .venv env/ venv/ +dev_venv/ ENV/ env.bak/ venv.bak/ diff --git a/haikalbot/migrations/0001_initial.py b/haikalbot/migrations/0001_initial.py new file mode 100644 index 00000000..31da4e22 --- /dev/null +++ b/haikalbot/migrations/0001_initial.py @@ -0,0 +1,46 @@ +# Generated by Django 5.1.7 on 2025-07-01 10:33 + +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', + }, + ), + ] diff --git a/haikalbot/migrations/0002_initial.py b/haikalbot/migrations/0002_initial.py new file mode 100644 index 00000000..32465ec6 --- /dev/null +++ b/haikalbot/migrations/0002_initial.py @@ -0,0 +1,34 @@ +# Generated by Django 5.1.7 on 2025-07-01 10:33 + +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'), + ), + ] diff --git a/inventory/management/commands/tenhal_plan.py b/inventory/management/commands/tenhal_plan.py index a301a6ad..b376d94d 100644 --- a/inventory/management/commands/tenhal_plan.py +++ b/inventory/management/commands/tenhal_plan.py @@ -43,8 +43,8 @@ class Command(BaseCommand): ) # Assign quotas to plans - PlanQuota.objects.create(plan=basic_plan, quota=users_quota, value=3) - PlanQuota.objects.create(plan=basic_plan, quota=cars_quota, value=3) + PlanQuota.objects.create(plan=basic_plan, quota=users_quota, value=4) + PlanQuota.objects.create(plan=basic_plan, quota=cars_quota, value=4) PlanQuota.objects.create(plan=pro_plan, quota=users_quota, value=5) PlanQuota.objects.create(plan=pro_plan, quota=cars_quota, value=5) diff --git a/inventory/migrations/0001_initial.py b/inventory/migrations/0001_initial.py new file mode 100644 index 00000000..fd3fd633 --- /dev/null +++ b/inventory/migrations/0001_initial.py @@ -0,0 +1,883 @@ +# Generated by Django 5.1.7 on 2025-07-01 10:33 + +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='PoItemsUploaded', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(blank=True, max_length=100, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('dealer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='inventory.dealer')), + ('item', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='po_items', to='django_ledger.itemtransactionmodel')), + ('po', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='items', to='django_ledger.purchaseordermodel')), + ], + ), + 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')], + }, + ), + ] diff --git a/inventory/models.py b/inventory/models.py index a46c3c22..d6cf5676 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -1187,6 +1187,9 @@ class Staff(models.Model, LocalizedNameMixin): return self.staff_member.user @property + def groups(self): + return [x.customgroup for x in self.user.groups.all()] + @property def groups(self): return [x.customgroup for x in self.user.groups.all()] diff --git a/inventory/templatetags/custom_filters.py b/inventory/templatetags/custom_filters.py index d5e2c41d..3390919a 100644 --- a/inventory/templatetags/custom_filters.py +++ b/inventory/templatetags/custom_filters.py @@ -661,4 +661,7 @@ def count_checked(permissions, group_permission_ids): # @register.filter # def count_checked(permissions, group_permission_ids): # """Count how many permissions are checked from the allowed list""" -# return sum(1 for perm in permissions if perm.id in group_permission_ids) \ No newline at end of file +# return sum(1 for perm in permissions if perm.id in group_permission_ids) + + + diff --git a/inventory/views.py b/inventory/views.py index 5cc65f95..b4c1ff56 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -1165,7 +1165,7 @@ def inventory_stats_view(request, dealer_slug): "inventory/inventory_stats.html" template. :rtype: HttpResponse """ - + # Base queryset for cars belonging to the dealer cars = models.Car.objects.filter(dealer=request.dealer) @@ -2306,6 +2306,7 @@ class CustomerCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView model = models.Customer form_class = forms.CustomerForm permission_required = ["inventory.add_customer"] + permission_required = ["inventory.add_customer"] template_name = "customers/customer_form.html" success_url = reverse_lazy("customer_list") success_message = "Customer created successfully" @@ -2373,6 +2374,7 @@ class CustomerUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView model = models.Customer form_class = forms.CustomerForm permission_required = ["inventory.change_customer"] + permission_required = ["inventory.change_customer"] template_name = "customers/customer_form.html" success_url = reverse_lazy("customer_list") success_message = "Customer updated successfully" @@ -2384,6 +2386,7 @@ class CustomerUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView def get_success_url(self): return reverse_lazy("customer_list", kwargs={"dealer_slug": self.kwargs["dealer_slug"]}) +@permission_required('inventory.delete_customer',raise_exception=True) @login_required def delete_customer(request, dealer_slug ,slug): """ @@ -2450,6 +2453,7 @@ class VendorListView(LoginRequiredMixin,PermissionRequiredMixin, ListView): @login_required +@permission_required('django_ledger.view_vendormodel',raise_exception=True) def vendorDetailView(request, dealer_slug,slug): """ Fetches and renders the detail view for a specific vendor. @@ -4897,7 +4901,7 @@ class DraftInvoiceModelUpdateFormView( form_class = DraftInvoiceModelUpdateForm template_name = "sales/invoices/draft_invoice_update.html" success_url = reverse_lazy("invoice_list") - permission_required = ["django_ledger.view_invoicemodel"] + permission_required = ["django_ledger.change_invoicemodel"] def get_form_kwargs(self): kwargs = super().get_form_kwargs() @@ -4939,7 +4943,7 @@ class ApprovedInvoiceModelUpdateFormView( form_class = ApprovedInvoiceModelUpdateForm template_name = "sales/invoices/approved_invoice_update.html" success_url = reverse_lazy("invoice_list") - permission_required = ["django_ledger.view_invoicemodel"] + permission_required = ["django_ledger.change_invoicemodel"] def get_form_kwargs(self): kwargs = super().get_form_kwargs() @@ -4987,7 +4991,7 @@ class PaidInvoiceModelUpdateFormView( form_class = PaidInvoiceModelUpdateForm template_name = "sales/invoices/paid_invoice_update.html" success_url = reverse_lazy("invoice_list") - permission_required = ["django_ledger.view_invoicemodel"] + permission_required = ["django_ledger.change_invoicemodel"] def get_form_kwargs(self): kwargs = super().get_form_kwargs() @@ -5169,7 +5173,7 @@ class InvoicePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailView @login_required -@permission_required("django_ledger.add_journalentrymodel", raise_exception=True) +@permission_required("inventory.add_payment", raise_exception=True) def PaymentCreateView(request, dealer_slug, pk): """ Handles the creation of a payment entry associated with an invoice or bill. Validates @@ -5244,7 +5248,7 @@ def PaymentCreateView(request, dealer_slug, pk): @login_required -@permission_required("django_ledger.view_journalentrymodel", raise_exception=True) +@permission_required("inventory.view_payment", raise_exception=True) def PaymentListView(request, dealer_slug): """ Handles the view for listing payment information associated with the journals of a specific @@ -5274,7 +5278,7 @@ def PaymentListView(request, dealer_slug): @login_required -@permission_required("django_ledger.view_journalentrymodel", raise_exception=True) +@permission_required("inventory.view_payment", raise_exception=True) def PaymentDetailView(request, dealer_slug, pk): """ This function handles the detail view for a payment by fetching a journal entry @@ -5304,7 +5308,7 @@ def PaymentDetailView(request, dealer_slug, pk): @login_required -@permission_required("django_ledger.change_journalentrymodel", raise_exception=True) +@permission_required("inventory.change_payment", raise_exception=True) def payment_mark_as_paid(request, dealer_slug, pk): """ Marks an invoice as paid if it meets the conditions of being fully paid and eligible @@ -5596,8 +5600,10 @@ def lead_create(request,dealer_slug): if hasattr(request.user.staffmember, "staff"): - form.initial["staff"] = request.user.staffmember.staff - form.fields["staff"].widget = HiddenInput() + staff = request.user.staffmember.staff + form.initial["staff"] = staff + form.fields["staff"].widget.attrs.update({"readonly":"true","required":"true"}) + form.fields["staff"].queryset = models.Staff.objects.filter(dealer=dealer,pk=staff.pk) form.fields["id_car_make"].queryset = qs form.fields["id_car_make"].choices = [ (obj.id_car_make, obj.get_local_name()) for obj in qs @@ -6175,13 +6181,6 @@ class OpportunityCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateVie initial["stage"] = models.Stage.QUALIFICATION return initial - def get_form(self, form_class=None): - form = super().get_form(form_class) - dealer = get_object_or_404(models.Dealer,slug=self.kwargs.get("dealer_slug")) - form.fields['car'].queryset = models.Car.objects.filter(dealer=dealer) - form.fields['lead'].queryset = models.Lead.objects.filter(dealer=dealer) - return form - def form_valid(self, form): dealer = get_object_or_404(models.Dealer,slug=self.kwargs.get("dealer_slug")) instance = form.save(commit=False) @@ -6317,6 +6316,7 @@ class OpportunityListView(LoginRequiredMixin,PermissionRequiredMixin, ListView): context_object_name = "opportunities" paginate_by = 30 permission_required = ["inventory.view_opportunity"] + permission_required = ["inventory.view_opportunity"] def get_queryset(self): dealer = get_user_type(self.request) @@ -6529,7 +6529,7 @@ class ItemServiceCreateView( template_name = "items/service/service_create.html" success_message = _("Service created successfully") context_object_name = "service" - permission_required = ["django_ledger.add_itemmodel"] + permission_required = ["inventory.add_additionalservices"] def form_valid(self, form): vat = models.VatRate.objects.get(is_active=True) @@ -6575,7 +6575,8 @@ class ItemServiceUpdateView( template_name = "items/service/service_create.html" success_message = _("Service updated successfully") context_object_name = "service" - permission_required = ["django_ledger.change_itemmodel"] + permission_required = ["inventory.change_additionalservices"] + def form_valid(self, form): vat = models.VatRate.objects.get(is_active=True) @@ -6613,7 +6614,8 @@ class ItemServiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView) template_name = "items/service/service_list.html" context_object_name = "services" paginate_by = 30 - permission_required = ["django_ledger.view_itemmodel"] + permission_required = ["inventory.view_additionalservices"] + def get_queryset(self): dealer = get_user_type(self.request) @@ -6778,6 +6780,7 @@ class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView): template_name = "bill/bill_create.html" PAGE_TITLE = _("Create Bill") + permission_required = ["django_ledger.add_billmodel"] extra_context = { "page_title": PAGE_TITLE, "header_title": PAGE_TITLE, @@ -6961,6 +6964,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView) class BillModelDetailViewView(BillModelDetailView): template_name = "bill/bill_detail.html" + permission_required = ["django_ledger.view_billmodel"] def get_context_data(self, **kwargs): context = super(BillModelDetailViewView, self).get_context_data(**kwargs) @@ -6970,6 +6974,8 @@ class BillModelDetailViewView(BillModelDetailView): class BillModelUpdateViewView(BillModelUpdateView): template_name = "bill/bill_update.html" + permission_required = ["django_ledger.change_billmodel"] + def post(self, request, dealer_slug, entity_slug, bill_pk, *args, **kwargs): if self.action_update_items: @@ -7240,7 +7246,7 @@ class OrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): # email @login_required -@permission_required("django_ledger.view_estimatemodel", raise_exception=True) +@permission_required("django_ledger.change_estimatemodel", raise_exception=True) def send_email_view(request, dealer_slug, pk): """ View function to send an email for an estimate. This function allows authenticated and @@ -8327,6 +8333,7 @@ class LedgerModelListView(LoginRequiredMixin,PermissionRequiredMixin, ListView, model = LedgerModel context_object_name = "ledgers" template_name = "ledger/ledger/ledger_list.html" + permission_required = ["django_ledger.view_ledgermodel"] date_field = "created" ordering = "-created" show_all = False @@ -8397,6 +8404,7 @@ class LedgerModelCreateView(LedgerModelCreateViewBase): """ template_name = "ledger/ledger/ledger_form.html" + permission_required = ["django_ledger.add_ledgermodel"] def get_form(self, form_class=None): dealer = get_user_type(self.request) @@ -8452,6 +8460,7 @@ class LedgerModelDeleteView(LedgerModelDeleteViewBase, SuccessMessageMixin): template_name = "ledger/ledger/ledger_delete.html" success_message = "Ledger deleted" + permission_required = ["django_ledger.delete_ledgermodel"] def get_success_url(self): return reverse("ledger_list", args=[self.kwargs["dealer_slug"]]) @@ -8500,6 +8509,7 @@ class JournalEntryListView(LoginRequiredMixin,PermissionRequiredMixin, ListView) model = JournalEntryModel context_object_name = "journal_entries" template_name = "ledger/journal_entry/journal_entry_list.html" + permission_required = ["django_ledger.view_journalentrymodel"] ordering = ["-timestamp"] permission_required = "ledger.view_ledger" @@ -8538,6 +8548,7 @@ class JournalEntryCreateView(LoginRequiredMixin, PermissionRequiredMixin,Success model = JournalEntryModel template_name = "ledger/journal_entry/journal_entry_form.html" + permission_required = ["django_ledger.add_journalentrymodel"] form_class = forms.JournalEntryModelCreateForm ledger_model = None success_message = _("Journal Entry created") @@ -9253,6 +9264,7 @@ def permenant_delete_account(request,dealer_slug, content_type, slug): def PurchaseOrderCreateView(request, dealer_slug,entity_slug): dealer = get_object_or_404(models.Dealer, slug=dealer_slug) entity = dealer.entity + if request.method == "POST": try: po = entity.create_purchase_order(po_title=request.POST.get("po_title")) diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index abd5fbfd..80ecd633 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -108,3 +108,4 @@ html[dir="rtl"] .form-icon-container .form-control { padding-right: 35px; padding-left: 10px; } + diff --git a/staticfiles/images/car-exterior.svg b/staticfiles/images/car-exterior.svg new file mode 100644 index 00000000..653dae0b --- /dev/null +++ b/staticfiles/images/car-exterior.svg @@ -0,0 +1,125 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + \ No newline at end of file diff --git a/staticfiles/images/car-interior.svg b/staticfiles/images/car-interior.svg new file mode 100644 index 00000000..cc77934e --- /dev/null +++ b/staticfiles/images/car-interior.svg @@ -0,0 +1,994 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/staticfiles/images/car_make/Alfa-Romeo-2_jj41jXd.png b/staticfiles/images/car_make/Alfa-Romeo-2_jj41jXd.png index 9c019410..e50a4b04 100644 Binary files a/staticfiles/images/car_make/Alfa-Romeo-2_jj41jXd.png and b/staticfiles/images/car_make/Alfa-Romeo-2_jj41jXd.png differ diff --git a/staticfiles/images/car_make/Avatr.png b/staticfiles/images/car_make/Avatr.png new file mode 100644 index 00000000..837969be Binary files /dev/null and b/staticfiles/images/car_make/Avatr.png differ diff --git a/staticfiles/images/car_make/BAIC.png b/staticfiles/images/car_make/BAIC.png index cde8f9b5..2efbafcf 100644 Binary files a/staticfiles/images/car_make/BAIC.png and b/staticfiles/images/car_make/BAIC.png differ diff --git a/staticfiles/images/car_make/BMW.png b/staticfiles/images/car_make/BMW.png index c9a745a5..5e547272 100644 Binary files a/staticfiles/images/car_make/BMW.png and b/staticfiles/images/car_make/BMW.png differ diff --git a/staticfiles/images/car_make/Changfeng.png b/staticfiles/images/car_make/Changfeng.png new file mode 100644 index 00000000..49e19c81 Binary files /dev/null and b/staticfiles/images/car_make/Changfeng.png differ diff --git a/staticfiles/images/car_make/Chery.png b/staticfiles/images/car_make/Chery.png index 41e33b1a..ebfd8e9e 100644 Binary files a/staticfiles/images/car_make/Chery.png and b/staticfiles/images/car_make/Chery.png differ diff --git a/staticfiles/images/car_make/Chevrolet_ZUJVQuH.png b/staticfiles/images/car_make/Chevrolet_ZUJVQuH.png index c7c6c798..fff175d9 100644 Binary files a/staticfiles/images/car_make/Chevrolet_ZUJVQuH.png and b/staticfiles/images/car_make/Chevrolet_ZUJVQuH.png differ diff --git a/staticfiles/images/car_make/Citroen.png b/staticfiles/images/car_make/Citroen.png index eb2d3d22..ec6352b2 100644 Binary files a/staticfiles/images/car_make/Citroen.png and b/staticfiles/images/car_make/Citroen.png differ diff --git a/staticfiles/images/car_make/Dongfeng.png b/staticfiles/images/car_make/Dongfeng.png index fa272b7b..e2dc1644 100644 Binary files a/staticfiles/images/car_make/Dongfeng.png and b/staticfiles/images/car_make/Dongfeng.png differ diff --git a/staticfiles/images/car_make/EXEED.png b/staticfiles/images/car_make/EXEED.png index c6c40273..fb4b9da3 100644 Binary files a/staticfiles/images/car_make/EXEED.png and b/staticfiles/images/car_make/EXEED.png differ diff --git a/staticfiles/images/car_make/Enovate.png b/staticfiles/images/car_make/Enovate.png new file mode 100644 index 00000000..ee909149 Binary files /dev/null and b/staticfiles/images/car_make/Enovate.png differ diff --git a/staticfiles/images/car_make/Forthing.png b/staticfiles/images/car_make/Forthing.png index 474a02a3..24dcc489 100644 Binary files a/staticfiles/images/car_make/Forthing.png and b/staticfiles/images/car_make/Forthing.png differ diff --git a/staticfiles/images/car_make/GWM.png b/staticfiles/images/car_make/GWM.png index df788792..cd7970f3 100644 Binary files a/staticfiles/images/car_make/GWM.png and b/staticfiles/images/car_make/GWM.png differ diff --git a/staticfiles/images/car_make/HiPhi.png b/staticfiles/images/car_make/HiPhi.png new file mode 100644 index 00000000..e4262128 Binary files /dev/null and b/staticfiles/images/car_make/HiPhi.png differ diff --git a/staticfiles/images/car_make/Hozon.png b/staticfiles/images/car_make/Hozon.png new file mode 100644 index 00000000..ff3383ef Binary files /dev/null and b/staticfiles/images/car_make/Hozon.png differ diff --git a/staticfiles/images/car_make/Huawei.png b/staticfiles/images/car_make/Huawei.png new file mode 100644 index 00000000..7c8652a3 Binary files /dev/null and b/staticfiles/images/car_make/Huawei.png differ diff --git a/staticfiles/images/car_make/Hyundai.png b/staticfiles/images/car_make/Hyundai.png index 59f9af60..f55ff589 100644 Binary files a/staticfiles/images/car_make/Hyundai.png and b/staticfiles/images/car_make/Hyundai.png differ diff --git a/staticfiles/images/car_make/IM-Motors.png b/staticfiles/images/car_make/IM-Motors.png new file mode 100644 index 00000000..e31c293f Binary files /dev/null and b/staticfiles/images/car_make/IM-Motors.png differ diff --git a/staticfiles/images/car_make/Jaguar.png b/staticfiles/images/car_make/Jaguar.png index 7f4bc6e9..9621c739 100644 Binary files a/staticfiles/images/car_make/Jaguar.png and b/staticfiles/images/car_make/Jaguar.png differ diff --git a/staticfiles/images/car_make/Jeep.png b/staticfiles/images/car_make/Jeep.png index 2e4b247e..37c47e98 100644 Binary files a/staticfiles/images/car_make/Jeep.png and b/staticfiles/images/car_make/Jeep.png differ diff --git a/staticfiles/images/car_make/Leapmotor.png b/staticfiles/images/car_make/Leapmotor.png new file mode 100644 index 00000000..9dee98b8 Binary files /dev/null and b/staticfiles/images/car_make/Leapmotor.png differ diff --git a/staticfiles/images/car_make/Lincoln.png b/staticfiles/images/car_make/Lincoln.png index a8b95d17..3fabda22 100644 Binary files a/staticfiles/images/car_make/Lincoln.png and b/staticfiles/images/car_make/Lincoln.png differ diff --git a/staticfiles/images/car_make/Lincoln1.png b/staticfiles/images/car_make/Lincoln1.png new file mode 100644 index 00000000..89a2360c Binary files /dev/null and b/staticfiles/images/car_make/Lincoln1.png differ diff --git a/staticfiles/images/car_make/LynkCo.png b/staticfiles/images/car_make/LynkCo.png index 2e90a6e2..d4e8bf58 100644 Binary files a/staticfiles/images/car_make/LynkCo.png and b/staticfiles/images/car_make/LynkCo.png differ diff --git a/staticfiles/images/car_make/Maserati.png b/staticfiles/images/car_make/Maserati.png index 7cb2e6e3..d89073ae 100644 Binary files a/staticfiles/images/car_make/Maserati.png and b/staticfiles/images/car_make/Maserati.png differ diff --git a/staticfiles/images/car_make/Renault.png b/staticfiles/images/car_make/Renault.png index b130c600..d9791024 100644 Binary files a/staticfiles/images/car_make/Renault.png and b/staticfiles/images/car_make/Renault.png differ diff --git a/staticfiles/images/car_make/Rover.png b/staticfiles/images/car_make/Rover.png index 7bb26736..b7aece62 100644 Binary files a/staticfiles/images/car_make/Rover.png and b/staticfiles/images/car_make/Rover.png differ diff --git a/staticfiles/images/car_make/Ssangyong.png b/staticfiles/images/car_make/Ssangyong.png index 8b8d2bef..e70b82b2 100644 Binary files a/staticfiles/images/car_make/Ssangyong.png and b/staticfiles/images/car_make/Ssangyong.png differ diff --git a/staticfiles/images/car_make/Stelato.png b/staticfiles/images/car_make/Stelato.png index 398919c2..856a2753 100644 Binary files a/staticfiles/images/car_make/Stelato.png and b/staticfiles/images/car_make/Stelato.png differ diff --git a/staticfiles/images/car_make/Toyota.png b/staticfiles/images/car_make/Toyota.png index 2220f0f0..5a564d91 100644 Binary files a/staticfiles/images/car_make/Toyota.png and b/staticfiles/images/car_make/Toyota.png differ diff --git a/staticfiles/images/car_make/Voyah.png b/staticfiles/images/car_make/Voyah.png index b8e0d3b1..5d8adbc0 100644 Binary files a/staticfiles/images/car_make/Voyah.png and b/staticfiles/images/car_make/Voyah.png differ diff --git a/staticfiles/images/car_make/Xiaomi.png b/staticfiles/images/car_make/Xiaomi.png new file mode 100644 index 00000000..4c7dfe31 Binary files /dev/null and b/staticfiles/images/car_make/Xiaomi.png differ diff --git a/staticfiles/images/car_make/Zeekr.png b/staticfiles/images/car_make/Zeekr.png new file mode 100644 index 00000000..6a2ecbe4 Binary files /dev/null and b/staticfiles/images/car_make/Zeekr.png differ diff --git a/staticfiles/images/car_make/Zotye.png b/staticfiles/images/car_make/Zotye.png index c86d94e0..d9ecefba 100644 Binary files a/staticfiles/images/car_make/Zotye.png and b/staticfiles/images/car_make/Zotye.png differ diff --git a/staticfiles/images/car_make/changan.png b/staticfiles/images/car_make/changan.png index 5761674e..1002e821 100644 Binary files a/staticfiles/images/car_make/changan.png and b/staticfiles/images/car_make/changan.png differ diff --git a/staticfiles/images/cars/1FM5K7B86EJA77428.png b/staticfiles/images/cars/1FM5K7B86EJA77428.png new file mode 100644 index 00000000..ab2a4983 Binary files /dev/null and b/staticfiles/images/cars/1FM5K7B86EJA77428.png differ diff --git a/staticfiles/images/cars/1g6a85sx8k0144230.png b/staticfiles/images/cars/1g6a85sx8k0144230.png index f27d5384..495d6b49 100644 Binary files a/staticfiles/images/cars/1g6a85sx8k0144230.png and b/staticfiles/images/cars/1g6a85sx8k0144230.png differ diff --git a/staticfiles/images/cars/2G1F93D33C9198388.png b/staticfiles/images/cars/2G1F93D33C9198388.png index f5f2e4a7..b6a1f75d 100644 Binary files a/staticfiles/images/cars/2G1F93D33C9198388.png and b/staticfiles/images/cars/2G1F93D33C9198388.png differ diff --git a/staticfiles/images/cars/3GCNY9EF5LG275234.png b/staticfiles/images/cars/3GCNY9EF5LG275234.png index 6ca9b1e5..585597d5 100644 Binary files a/staticfiles/images/cars/3GCNY9EF5LG275234.png and b/staticfiles/images/cars/3GCNY9EF5LG275234.png differ diff --git a/staticfiles/images/cars/5LMCJ2D93NUL03460.png b/staticfiles/images/cars/5LMCJ2D93NUL03460.png index b1ea45cf..b8f16f11 100644 Binary files a/staticfiles/images/cars/5LMCJ2D93NUL03460.png and b/staticfiles/images/cars/5LMCJ2D93NUL03460.png differ diff --git a/staticfiles/images/cars/JN8AY2NY9E9073687.png b/staticfiles/images/cars/JN8AY2NY9E9073687.png index 7a71f306..7687330c 100644 Binary files a/staticfiles/images/cars/JN8AY2NY9E9073687.png and b/staticfiles/images/cars/JN8AY2NY9E9073687.png differ diff --git a/staticfiles/images/cars/LJXCU3BB0RTF17235.png b/staticfiles/images/cars/LJXCU3BB0RTF17235.png new file mode 100644 index 00000000..4ab35847 Binary files /dev/null and b/staticfiles/images/cars/LJXCU3BB0RTF17235.png differ diff --git a/staticfiles/images/cars/VF3V1ZKX7RZ002134.png b/staticfiles/images/cars/VF3V1ZKX7RZ002134.png new file mode 100644 index 00000000..201190f0 Binary files /dev/null and b/staticfiles/images/cars/VF3V1ZKX7RZ002134.png differ diff --git a/staticfiles/images/logos/users/pexels-marieke-schonfeld-1309710-2514035.jpg b/staticfiles/images/logos/users/pexels-marieke-schonfeld-1309710-2514035.jpg new file mode 100644 index 00000000..9c8f2f26 Binary files /dev/null and b/staticfiles/images/logos/users/pexels-marieke-schonfeld-1309710-2514035.jpg differ diff --git a/templates/base.html b/templates/base.html index ddb99a7a..732ac9e6 100644 --- a/templates/base.html +++ b/templates/base.html @@ -89,10 +89,14 @@ + + + + diff --git a/templates/crm/leads/lead_detail.html b/templates/crm/leads/lead_detail.html index d7de8861..f3110d1e 100644 --- a/templates/crm/leads/lead_detail.html +++ b/templates/crm/leads/lead_detail.html @@ -172,6 +172,7 @@ + {% if perms.inventory.change_lead%} + {% endif %}

{{ _("Activities") }} ({{ activities.count}})

+ {% if perms.inventory.change_lead%} + {% endif %}
@@ -252,7 +256,9 @@

{{ _("Opportunities") }} ({{ lead.get_opportunities.count}})

+ {% if perms.inventory.add_opportunity%} {{ _("Add Opportunity") }} + {% endif%}
@@ -284,7 +290,9 @@

{{ _("Notes") }}

+ {% if perms.inventory.change_lead%} + {% endif %}
@@ -340,12 +348,14 @@

{{ _("Emails") }}

+ {% if perms.inventory.change_lead%} + {% endif %}
@@ -462,7 +472,9 @@

{{ _("Tasks") }}

+ {% if perms.inventory.change_lead%} + {% endif %}
diff --git a/templates/crm/leads/lead_list.html b/templates/crm/leads/lead_list.html index 148d1a71..dfa111e5 100644 --- a/templates/crm/leads/lead_list.html +++ b/templates/crm/leads/lead_list.html @@ -212,18 +212,23 @@ {% if perms.inventory.change_lead %} {% trans "Edit" %} {% endif %} + {% if perms.inventory.change_lead%} {% trans "Send Email" %} {% trans "Schedule Event" %} + {% endif %} {% if not lead.opportunity %} + {% if perms.inventory.add_opportunity%} {% trans "Convert to Opportunity" %} + {% endif %} {% endif %} - + {% if perms.inventory.delete_lead %} + - {% endif %} + {%endif%}
{% endif %} diff --git a/templates/crm/opportunities/opportunity_detail.html b/templates/crm/opportunities/opportunity_detail.html index 3531048d..2e03856e 100644 --- a/templates/crm/opportunities/opportunity_detail.html +++ b/templates/crm/opportunities/opportunity_detail.html @@ -13,14 +13,22 @@
@@ -348,9 +356,11 @@
+ {%if perms.inventory.change_opportunity%}
+ {% endif %}
{% for activity in opportunity.get_activities %}
@@ -392,11 +402,13 @@

Notes

+ {%if perms.inventory.change_opportunity%}
{% csrf_token %}
+ {% endif %}
{% for note in opportunity.get_notes %} @@ -427,9 +439,11 @@

23 tasks

+ {% if perms.inventory.change_opportunity%} + {% endif %}
{% for metting in opportunity.lead.get_meetings %} @@ -454,10 +468,12 @@

Call

- + {% if perms.inventory.change_opportunity%} + {% endif %} +
{{opportunity.get_all_notes}}
@@ -503,6 +519,7 @@

Emails

+ {% if perms.inventory.change_opportunity%} + {% endif %}
- + {% if perms.inventory.add_opportunity %} + {% endif %}
diff --git a/templates/crm/opportunities/partials/opportunity_grid.html b/templates/crm/opportunities/partials/opportunity_grid.html index 5a371564..ca6a4c9a 100644 --- a/templates/crm/opportunities/partials/opportunity_grid.html +++ b/templates/crm/opportunities/partials/opportunity_grid.html @@ -119,12 +119,16 @@
+ {% if perms.inventory.view_opportunity%} {{ _("View") }} + {% endif %} + {%if perms.inventory.change_opportunity%} {{ _("Update") }} + {% endif %}
diff --git a/templates/customers/customer_list.html b/templates/customers/customer_list.html index b8f27306..20916698 100644 --- a/templates/customers/customer_list.html +++ b/templates/customers/customer_list.html @@ -10,7 +10,7 @@
- {% if perms.django_ledger.add_customermodel %} + {% if perms.inventory.add_customer %} @@ -68,6 +68,7 @@ + {% if perms.inventory.view_customer%}
{{ customer.full_name }} @@ -76,6 +77,7 @@
+ {% endif %} {{ customer.email }} {{ customer.phone_number }} {{ customer.national_id }} @@ -90,12 +92,12 @@ {{ customer.created|date }} - {% if perms.django_ledger.change_customermodel %} + {% if perms.inventory.change_customer %} {% endif %} - {% if perms.django_ledger.delete_customermodel %} + {% if perms.inventory.delete_customer %} - {% endif %}
+ {% endif %} -
- {% if perms.django_ledger.change_customermodel %} - {{_("Update")}} - {% endif %} -
@@ -84,16 +87,14 @@
+ {% if perms.inventory.change_customer%} + {% endif %} diff --git a/templates/groups/group_list.html b/templates/groups/group_list.html index dafbbb28..6e52f69f 100644 --- a/templates/groups/group_list.html +++ b/templates/groups/group_list.html @@ -1,5 +1,6 @@ {% extends "base.html" %} {% load i18n %} +{% load custom_filters %} {% load render_table from django_tables2 %} {% block title %}{% trans "Groups" %}{% endblock title %} @@ -32,7 +33,7 @@ {% for group in groups %} - + + {% if perms.inventory.change_carfinance%} + + {% endif %} {% else %} -

{% trans "No finance details available." %}

- {% if perms.inventory.add_carfinance %} +

{% trans "No finance details available." %}

+ {% if perms.inventory.add_carfinance %} {% trans "Add" %} {% endif %} - - {% endif %}
{{ _("Note") }}
{{ group.name }} {{ group.users.count }} {{ group.users.count}} {{ group.permissions.count }} -
+ {% csrf_token %} -
@@ -43,86 +42,83 @@
- - -
- {% for app_label, models in grouped_permissions.items %} -
-
-
-
-
- - {{ app_label|capfirst }} -
- - {{ models|length }} {% trans "categories" %} - -
-
-
-
- {% for model, perms in models.items %} -
-
- -
-
-
-
- {% for perm in perms %} -
+
+
+
+ {% for model, perms in models.items %} +
+
+ +
+
+
+
+ {% for perm in perms %} + + {% endfor %} +
+
+
+ {% endfor %}
- {% endfor %} -
+
+ {% endfor %}
-
-
-{% endfor %} -
- -
-
+
@@ -134,12 +130,9 @@
- - {% trans "Cancel" %} - -
@@ -197,7 +190,7 @@ document.addEventListener('DOMContentLoaded', function() { if (button) { const hasVisible = collapse.querySelector('.list-group-item[style=""]'); const bsCollapse = bootstrap.Collapse.getInstance(collapse) || - new bootstrap.Collapse(collapse); + new bootstrap.Collapse(collapse); if (hasVisible) { bsCollapse.show(); diff --git a/templates/header.html b/templates/header.html index bda00b6d..b9e8e152 100644 --- a/templates/header.html +++ b/templates/header.html @@ -17,7 +17,7 @@ {% trans "Inventory"|capfirst %}
- +
+ {% endif %} -{% endif %} +
{% trans "Total"|capfirst %} {{ car.finances.total_vat|floatformat:2 }}
{% if not car.get_transfer %} - {% trans "Edit" %} + {% trans "Edit" %} {% else %} {% trans "Cannot Edit, Car in Transfer." %} - {% endif %} + {% endif %} +
+ {% endif %} + + {% if perms.inventory.view_carcolors %}

{% trans 'Colors Details' %}

@@ -332,7 +342,8 @@ style="background-color: rgb({{ car.colors.interior.rgb }})">
- + + {% if perms.inventory.change_carcolors%} {% if not car.get_transfer %} @@ -343,8 +354,9 @@ {% endif %} + {% endif %} {% else %} - +

{% trans "No color details available." %}

@@ -352,7 +364,7 @@ {{ _("Add Color") }} {% endif %} - + {% endif %} @@ -360,7 +372,9 @@
+ {% endif %} {% if car.status != 'transfer' %} + {%if perms.inventory.view_carreservation%}

{% trans 'Reservations Details' %}

@@ -379,6 +393,7 @@ {{ reservation.reserved_by.dealer }} {{ reservation.reserved_until }} + {% if perms.inventory.change_carreservation %} {% if reservation.is_active %} @@ -402,27 +417,30 @@ {% trans "Expired" %} {% endif %} + {% endif %} {% endfor %} {% else %} + {% if perms.inventory.add_carreservation %} - {% if perms.inventory.change_carreservation %} - {% endif %} - - {% endif %} + {% endif %} + + + {% endif %}
+ {% endif %} {% endif %} {% if car.status == 'transfer' and car.get_transfer %} @@ -483,6 +501,7 @@ alt=""> {% endif %} + + {% endif %}