diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py index de2ca279..b1728ba5 100644 --- a/api/migrations/0001_initial.py +++ b/api/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.4 on 2025-01-07 22:27 +# Generated by Django 5.1.4 on 2025-01-12 17:20 from django.db import migrations, models diff --git a/api/migrations/__pycache__/0001_initial.cpython-311.pyc b/api/migrations/__pycache__/0001_initial.cpython-311.pyc index 92bbc91c..bc610fab 100644 Binary files a/api/migrations/__pycache__/0001_initial.cpython-311.pyc and b/api/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/haikalbot/migrations/0001_initial.py b/haikalbot/migrations/0001_initial.py index 0f9e2667..b85002cb 100644 --- a/haikalbot/migrations/0001_initial.py +++ b/haikalbot/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.4 on 2025-01-07 22:27 +# Generated by Django 5.1.4 on 2025-01-12 17:20 from django.db import migrations, models diff --git a/haikalbot/migrations/0002_initial.py b/haikalbot/migrations/0002_initial.py index cd9aa54f..7ac265cf 100644 --- a/haikalbot/migrations/0002_initial.py +++ b/haikalbot/migrations/0002_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.4 on 2025-01-07 22:27 +# Generated by Django 5.1.4 on 2025-01-12 17:20 import django.db.models.deletion from django.db import migrations, models diff --git a/inventory/__pycache__/admin.cpython-311.pyc b/inventory/__pycache__/admin.cpython-311.pyc index 74f16682..442e6d37 100644 Binary files a/inventory/__pycache__/admin.cpython-311.pyc and b/inventory/__pycache__/admin.cpython-311.pyc differ diff --git a/inventory/__pycache__/forms.cpython-311.pyc b/inventory/__pycache__/forms.cpython-311.pyc index ee9a7a39..422785da 100644 Binary files a/inventory/__pycache__/forms.cpython-311.pyc and b/inventory/__pycache__/forms.cpython-311.pyc differ diff --git a/inventory/__pycache__/models.cpython-311.pyc b/inventory/__pycache__/models.cpython-311.pyc index 5dc558dc..b3240499 100644 Binary files a/inventory/__pycache__/models.cpython-311.pyc and b/inventory/__pycache__/models.cpython-311.pyc differ diff --git a/inventory/__pycache__/services.cpython-311.pyc b/inventory/__pycache__/services.cpython-311.pyc index 3af68731..6d167297 100644 Binary files a/inventory/__pycache__/services.cpython-311.pyc and b/inventory/__pycache__/services.cpython-311.pyc differ diff --git a/inventory/__pycache__/urls.cpython-311.pyc b/inventory/__pycache__/urls.cpython-311.pyc index 23222acb..e5d0ce97 100644 Binary files a/inventory/__pycache__/urls.cpython-311.pyc and b/inventory/__pycache__/urls.cpython-311.pyc differ diff --git a/inventory/__pycache__/utils.cpython-311.pyc b/inventory/__pycache__/utils.cpython-311.pyc index a6ad92f4..83420281 100644 Binary files a/inventory/__pycache__/utils.cpython-311.pyc and b/inventory/__pycache__/utils.cpython-311.pyc differ diff --git a/inventory/__pycache__/views.cpython-311.pyc b/inventory/__pycache__/views.cpython-311.pyc index 81eaefb1..a1f04797 100644 Binary files a/inventory/__pycache__/views.cpython-311.pyc and b/inventory/__pycache__/views.cpython-311.pyc differ diff --git a/inventory/forms.py b/inventory/forms.py index 0f786bc0..41e5bb3a 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -567,36 +567,7 @@ class EmailForm(forms.Form): from_email = forms.EmailField() to_email = forms.EmailField(label="To") -class LeadCreateForm(forms.ModelForm): - class Meta: - model = Lead - fields = ['title', - 'first_name', - 'last_name', - 'email', - 'phone_number', - 'city', - 'salary', - 'obligations', - 'id_car_make', - 'id_car_model', - 'year', - 'source', - 'channel', - 'assigned', - 'priority', - ] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - if "id_car_make" in self.fields: - queryset = self.fields["id_car_make"].queryset.filter(is_sa_import=True) - self.fields["id_car_make"].choices = [ - (obj.id_car_make, obj.get_local_name()) for obj in queryset - ] - -class LeadUpdateForm(forms.ModelForm): +class LeadForm(forms.ModelForm): class Meta: model = Lead fields = ['title', diff --git a/inventory/migrations/0001_initial.py b/inventory/migrations/0001_initial.py index 9be8205c..a8b172e7 100644 --- a/inventory/migrations/0001_initial.py +++ b/inventory/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.4 on 2025-01-07 22:27 +# Generated by Django 5.1.4 on 2025-01-12 17:20 import django.db.models.deletion import inventory.mixins @@ -14,6 +14,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), ('django_ledger', '0017_alter_accountmodel_unique_together_and_more'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] @@ -57,7 +58,7 @@ class Migration(migrations.Migration): ('arabic_name', models.CharField(blank=True, max_length=255, null=True)), ('logo', models.ImageField(blank=True, null=True, upload_to='car_make', verbose_name='logo')), ('is_sa_import', models.BooleanField(default=False)), - ('car_type', models.SmallIntegerField(choices=[(1, 'Car'), (2, 'light commercial'), (3, 'truck trailer'), (4, 'trailer'), (5, 'truck'), (6, 'bus')])), + ('car_type', models.SmallIntegerField(choices=[(1, 'Car'), (2, 'Light Commercial'), (3, 'Heavy-Duty Tractors'), (4, 'Trailers'), (5, 'Medium Trucks'), (6, 'Buses'), (20, 'Motorcycles'), (21, 'Buggy'), (22, 'Moto ATV'), (23, 'Scooters'), (24, 'Karting'), (25, 'ATV'), (26, 'Snowmobiles')])), ], options={ 'verbose_name': 'Make', @@ -134,6 +135,41 @@ class Migration(migrations.Migration): ('created_at', models.DateTimeField(auto_now_add=True)), ], ), + migrations.CreateModel( + name='Activity', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.PositiveIntegerField()), + ('activity_type', models.CharField(choices=[('call', 'Call'), ('sms', 'SMS'), ('email', 'Email'), ('whatsapp', 'WhatsApp'), ('visit', 'Visit'), ('add_car', 'Add Car'), ('reserve_car', 'Reserve Car'), ('remove_car', 'Remove Car'), ('create_quotation', 'Create Quotation'), ('cancel_quotation', 'Cancel Quotation'), ('create_order', 'Create Order'), ('cancel_order', 'Cancel Order'), ('create_invoice', 'Create Invoice'), ('cancel_invoice', 'Cancel Invoice')], max_length=50, verbose_name='Activity Type')), + ('notes', models.TextField(blank=True, null=True, verbose_name='Notes')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='activities_created', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Activity', + 'verbose_name_plural': 'Activities', + }, + ), + migrations.CreateModel( + name='AdditionalServices', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Name')), + ('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')), + ('description', models.TextField(verbose_name='Description')), + ('price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Price')), + ('taxable', models.BooleanField(default=False, verbose_name='taxable')), + ('uom', models.CharField(choices=[('EA', 'Each'), ('PR', 'Pair'), ('SET', 'Set'), ('GAL', 'Gallon'), ('L', 'Liter'), ('M', 'Meter'), ('KG', 'Kilogram'), ('HR', 'Hour'), ('BX', 'Box'), ('RL', 'Roll'), ('PKG', 'Package'), ('DZ', 'Dozen'), ('SQ_M', 'Square Meter'), ('PC', 'Piece'), ('BDL', 'Bundle')], max_length=10, verbose_name='Unit of Measurement')), + ('item', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='django_ledger.itemmodel', verbose_name='Item')), + ], + options={ + 'verbose_name': 'Additional Services', + 'verbose_name_plural': 'Additional Services', + }, + bases=(models.Model, inventory.mixins.LocalizedNameMixin), + ), migrations.CreateModel( name='CarFinance', fields=[ @@ -141,7 +177,7 @@ class Migration(migrations.Migration): ('cost_price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Cost Price')), ('selling_price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Selling Price')), ('discount_amount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=14, verbose_name='Discount Amount')), - ('additional_services', models.ManyToManyField(blank=True, related_name='additional_finances', to='django_ledger.itemmodel')), + ('additional_services', models.ManyToManyField(blank=True, related_name='additional_finances', to='inventory.additionalservices')), ('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='finances', to='inventory.car')), ], options={ @@ -325,26 +361,6 @@ class Migration(migrations.Migration): ('objects', inventory.models.DealerUserManager()), ], ), - migrations.CreateModel( - name='Customer', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('first_name', models.CharField(max_length=50, verbose_name='First Name')), - ('middle_name', models.CharField(blank=True, max_length=50, null=True, verbose_name='Middle Name')), - ('last_name', models.CharField(max_length=50, verbose_name='Last Name')), - ('email', models.EmailField(max_length=254, unique=True, verbose_name='Email')), - ('national_id', models.CharField(max_length=10, unique=True, verbose_name='National ID')), - ('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', unique=True, verbose_name='Phone Number')), - ('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')), - ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), - ('is_lead', models.BooleanField(default=False, verbose_name='Is Lead')), - ('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='customers', to='inventory.dealer')), - ], - options={ - 'verbose_name': 'Customer', - 'verbose_name_plural': 'Customers', - }, - ), migrations.CreateModel( name='CarLocation', fields=[ @@ -366,23 +382,80 @@ class Migration(migrations.Migration): name='dealer', field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='cars', to='inventory.dealer', verbose_name='Dealer'), ), + migrations.AddField( + model_name='additionalservices', + name='dealer', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.dealer', verbose_name='Dealer'), + ), migrations.CreateModel( - name='AdditionalServices', + name='Lead', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255, verbose_name='Name')), - ('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')), - ('description', models.TextField(verbose_name='Description')), - ('price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Price')), - ('taxable', models.BooleanField(default=False, verbose_name='taxable')), - ('uom', models.CharField(choices=[('EA', 'Each'), ('PR', 'Pair'), ('SET', 'Set'), ('GAL', 'Gallon'), ('L', 'Liter'), ('M', 'Meter'), ('KG', 'Kilogram'), ('HR', 'Hour'), ('BX', 'Box'), ('RL', 'Roll'), ('PKG', 'Package'), ('DZ', 'Dozen'), ('SQ_M', 'Square Meter'), ('PC', 'Piece'), ('BDL', 'Bundle')], max_length=10, verbose_name='Unit of Measurement')), - ('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.dealer', verbose_name='Dealer')), + ('title', models.CharField(choices=[('mr', 'Mr'), ('mrs', 'Mrs'), ('ms', 'Ms'), ('miss', 'Miss'), ('dr', 'Dr'), ('prof', 'Prof'), ('prince', 'Prince'), ('princess', 'Princess'), ('company', 'Company'), ('na', 'N/A')], max_length=20, verbose_name='Title')), + ('first_name', models.CharField(max_length=50, verbose_name='First Name')), + ('last_name', models.CharField(max_length=50, verbose_name='Last Name')), + ('email', models.EmailField(db_index=True, max_length=254, unique=True, verbose_name='Email')), + ('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')), + ('salary', models.PositiveIntegerField(blank=True, null=True, verbose_name='Salary')), + ('obligations', models.PositiveIntegerField(blank=True, null=True, verbose_name='Obligations')), + ('year', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Year')), + ('source', models.CharField(choices=[('referrals', 'Referrals'), ('whatsapp', 'WhatsApp'), ('showroom', 'Showroom'), ('tiktok', 'TikTok'), ('instagram', 'Instagram'), ('x', 'X'), ('facebook', 'Facebook'), ('motory', 'Motory'), ('influencers', 'Influencers'), ('youtube', 'Youtube'), ('campaign', 'Campaign')], max_length=50, verbose_name='Source')), + ('channel', models.CharField(choices=[('walk_in', 'Walk In'), ('toll_free', 'Toll Free'), ('website', 'Website'), ('email', 'Email'), ('form', 'Form')], max_length=50, verbose_name='Channel')), + ('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')), + ('city', models.CharField(max_length=50, verbose_name='City')), + ('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')], default='medium', max_length=10, verbose_name='Priority')), + ('status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], db_index=True, default='new', max_length=50, verbose_name='Status')), + ('created', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Created')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')), + ('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leads', to='inventory.dealer')), + ('id_car_make', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake', verbose_name='Make')), + ('id_car_model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel', verbose_name='Model')), ], options={ - 'verbose_name': 'Additional Services', - 'verbose_name_plural': 'Additional Services', + 'verbose_name': 'Lead', + 'verbose_name_plural': 'Leads', + }, + ), + migrations.CreateModel( + name='Customer', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(choices=[('mr', 'Mr'), ('mrs', 'Mrs'), ('ms', 'Ms'), ('miss', 'Miss'), ('dr', 'Dr'), ('prof', 'Prof'), ('prince', 'Prince'), ('princess', 'Princess'), ('company', 'Company'), ('na', 'N/A')], default='na', max_length=10, verbose_name='Title')), + ('first_name', models.CharField(max_length=50, verbose_name='First Name')), + ('middle_name', models.CharField(blank=True, max_length=50, null=True, verbose_name='Middle Name')), + ('last_name', models.CharField(max_length=50, verbose_name='Last Name')), + ('gender', models.CharField(choices=[('m', 'Male'), ('f', 'Female')], max_length=1, verbose_name='Gender')), + ('dob', models.DateField(verbose_name='Date of Birth')), + ('email', models.EmailField(max_length=254, unique=True, verbose_name='Email')), + ('national_id', models.CharField(max_length=10, unique=True, verbose_name='National ID')), + ('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', unique=True, verbose_name='Phone Number')), + ('city', models.CharField(blank=True, max_length=255, verbose_name='City')), + ('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')), + ('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='customers', to='inventory.dealer')), + ('lead', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='converted', to='inventory.lead', verbose_name='Lead')), + ], + options={ + 'verbose_name': 'Customer', + 'verbose_name_plural': 'Customers', + }, + ), + migrations.CreateModel( + name='Notes', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.PositiveIntegerField()), + ('note', models.TextField(verbose_name='Note')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='notes_created', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Note', + 'verbose_name_plural': 'Notes', }, - bases=(models.Model, inventory.mixins.LocalizedNameMixin), ), migrations.CreateModel( name='Notification', @@ -390,46 +463,13 @@ class Migration(migrations.Migration): ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('message', models.CharField(max_length=255, verbose_name='Message')), ('is_read', models.BooleanField(default=False, verbose_name='Is Read')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)), ], options={ 'verbose_name': 'Notification', 'verbose_name_plural': 'Notifications', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='Opportunity', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('deal_name', models.CharField(max_length=255, verbose_name='Deal Name')), - ('deal_value', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Deal Value')), - ('deal_status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('canceled', 'Canceled'), ('completed', 'Completed')], default='new', max_length=20, verbose_name='Deal Status')), - ('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')], default='low', max_length=10, verbose_name='Priority')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')), - ('car', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.car', verbose_name='Car')), - ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='inventory.customer')), - ], - options={ - 'verbose_name': 'Opportunity', - 'verbose_name_plural': 'Opportunities', - }, - ), - migrations.CreateModel( - name='Notes', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('note', models.TextField(verbose_name='Note')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')), - ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='notes_created', to=settings.AUTH_USER_MODEL)), - ('opportunity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='inventory.opportunity')), - ], - options={ - 'verbose_name': 'Notes', - 'verbose_name_plural': 'Notes', + 'ordering': ['-created'], }, ), migrations.CreateModel( @@ -540,8 +580,8 @@ class Migration(migrations.Migration): ('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')), ('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')), ('staff_type', models.CharField(choices=[('manager', 'Manager'), ('inventory', 'Inventory'), ('accountant', 'Accountant'), ('sales', 'Sales'), ('coordinator', 'Coordinator'), ('receptionist', 'Receptionist'), ('agent', 'Agent')], max_length=255, verbose_name='Staff Type')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')), ('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='staff', to='inventory.dealer')), ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='staff', to=settings.AUTH_USER_MODEL)), ], @@ -551,29 +591,55 @@ class Migration(migrations.Migration): 'permissions': [], }, bases=(models.Model, inventory.mixins.LocalizedNameMixin), + managers=[ + ('objects', inventory.models.StaffUserManager()), + ], ), migrations.CreateModel( - name='OpportunityLog', + name='Opportunity', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('action', models.CharField(choices=[('create', 'Create'), ('update', 'Update'), ('delete', 'Delete'), ('status_change', 'Status Change')], max_length=50, verbose_name='Action')), - ('old_status', models.CharField(blank=True, choices=[('new', 'New'), ('pending', 'Pending'), ('canceled', 'Canceled'), ('completed', 'Completed')], max_length=50, null=True, verbose_name='Old Status')), - ('new_status', models.CharField(blank=True, choices=[('new', 'New'), ('pending', 'Pending'), ('canceled', 'Canceled'), ('completed', 'Completed')], max_length=50, null=True, verbose_name='New Status')), - ('details', models.TextField(blank=True, null=True, verbose_name='Details')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('opportunity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='inventory.opportunity')), - ('staff', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.staff', verbose_name='Staff')), + ('stage', models.CharField(choices=[('prospect', 'Prospect'), ('proposal', 'Proposal'), ('negotiation', 'Negotiation'), ('closed_won', 'Closed Won'), ('closed_lost', 'Closed Lost')], max_length=20, verbose_name='Stage')), + ('status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], default='new', max_length=20, verbose_name='Status')), + ('probability', models.PositiveIntegerField(validators=[inventory.models.validate_probability])), + ('closing_date', models.DateField(verbose_name='Closing Date')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')), + ('closed', models.BooleanField(default=False, verbose_name='Closed')), + ('car', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.car', verbose_name='Car')), + ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='inventory.customer')), + ('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='inventory.dealer')), + ('staff', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owner', to='inventory.staff', verbose_name='Owner')), ], options={ - 'verbose_name': 'Log', - 'verbose_name_plural': 'Logs', - 'ordering': ['-created_at'], + 'verbose_name': 'Opportunity', + 'verbose_name_plural': 'Opportunities', + }, + ), + migrations.CreateModel( + name='LeadStatusHistory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('old_status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], max_length=50, verbose_name='Old Status')), + ('new_status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], max_length=50, verbose_name='New Status')), + ('changed_at', models.DateTimeField(auto_now_add=True, verbose_name='Changed At')), + ('lead', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='status_history', to='inventory.lead')), + ('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='status_changes', to='inventory.staff')), + ], + options={ + 'verbose_name': 'Lead Status History', + 'verbose_name_plural': 'Lead Status Histories', }, ), migrations.AddField( - model_name='opportunity', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deals_created', to='inventory.staff'), + model_name='lead', + name='assigned', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned', to='inventory.staff', verbose_name='Assigned'), + ), + migrations.AddField( + model_name='customer', + name='staff', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customer_staff', to='inventory.staff', verbose_name='Staff'), ), migrations.CreateModel( name='Subscription', diff --git a/inventory/migrations/0004_additionalservices_item_and_more.py b/inventory/migrations/0004_additionalservices_item_and_more.py deleted file mode 100644 index 2241b959..00000000 --- a/inventory/migrations/0004_additionalservices_item_and_more.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 4.2.17 on 2025-01-08 08:42 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('django_ledger', '0017_alter_accountmodel_unique_together_and_more'), - ('inventory', '0003_alter_caroptionvalue_is_base'), - ] - - operations = [ - migrations.AddField( - model_name='additionalservices', - name='item', - field=models.OneToOneField(default='', on_delete=django.db.models.deletion.CASCADE, to='django_ledger.itemmodel', verbose_name='Item'), - preserve_default=False, - ), - migrations.AlterField( - model_name='carfinance', - name='additional_services', - field=models.ManyToManyField(blank=True, related_name='additional_finances', to='inventory.additionalservices'), - ), - ] diff --git a/inventory/migrations/0005_alter_additionalservices_item.py b/inventory/migrations/0005_alter_additionalservices_item.py deleted file mode 100644 index bf542c58..00000000 --- a/inventory/migrations/0005_alter_additionalservices_item.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 4.2.17 on 2025-01-08 08:43 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('django_ledger', '0017_alter_accountmodel_unique_together_and_more'), - ('inventory', '0004_additionalservices_item_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='additionalservices', - name='item', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='django_ledger.itemmodel', verbose_name='Item'), - ), - ] diff --git a/inventory/migrations/0006_remove_customer_is_lead_customer_dob_customer_gender_and_more.py b/inventory/migrations/0006_remove_customer_is_lead_customer_dob_customer_gender_and_more.py deleted file mode 100644 index 3e6592a1..00000000 --- a/inventory/migrations/0006_remove_customer_is_lead_customer_dob_customer_gender_and_more.py +++ /dev/null @@ -1,59 +0,0 @@ -# Generated by Django 5.1.4 on 2025-01-08 19:03 - -import django.utils.timezone -import django_countries.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0005_alter_additionalservices_item'), - ] - - operations = [ - migrations.RemoveField( - model_name='customer', - name='is_lead', - ), - migrations.AddField( - model_name='customer', - name='dob', - field=models.DateField(default=django.utils.timezone.now, verbose_name='Date of Birth'), - preserve_default=False, - ), - migrations.AddField( - model_name='customer', - name='gender', - field=models.CharField(choices=[('m', 'Male'), ('f', 'Female')], default='m', max_length=1, verbose_name='Gender'), - preserve_default=False, - ), - migrations.AddField( - model_name='customer', - name='nationality', - field=django_countries.fields.CountryField(blank=True, max_length=2, verbose_name='Nationality'), - ), - migrations.AddField( - model_name='customer', - name='obligations', - field=models.PositiveIntegerField(default=1000, verbose_name='Obligations'), - preserve_default=False, - ), - migrations.AddField( - model_name='customer', - name='salary', - field=models.PositiveIntegerField(default=10000, verbose_name='Salary'), - preserve_default=False, - ), - migrations.AddField( - model_name='customer', - name='title', - field=models.CharField(choices=[('mr', 'Mr'), ('mrs', 'Mrs'), ('ms', 'Ms'), ('miss', 'Miss'), ('dr', 'Dr'), ('prof', 'Prof'), ('prince', 'Prince'), ('princess', 'Princess'), ('company', 'Company')], default='mr', max_length=10, verbose_name='Title'), - preserve_default=False, - ), - migrations.AddField( - model_name='customer', - name='updated', - field=models.DateTimeField(auto_now=True, verbose_name='Updated'), - ), - ] diff --git a/inventory/migrations/0007_remove_customer_nationality_customer_country_and_more.py b/inventory/migrations/0007_remove_customer_nationality_customer_country_and_more.py deleted file mode 100644 index f9e6d93f..00000000 --- a/inventory/migrations/0007_remove_customer_nationality_customer_country_and_more.py +++ /dev/null @@ -1,43 +0,0 @@ -# Generated by Django 5.1.4 on 2025-01-09 05:46 - -import django_countries.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0006_remove_customer_is_lead_customer_dob_customer_gender_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='customer', - name='nationality', - ), - migrations.AddField( - model_name='customer', - name='country', - field=django_countries.fields.CountryField(blank=True, max_length=2, verbose_name='Country'), - ), - migrations.AlterField( - model_name='customer', - name='title', - field=models.CharField(choices=[('mr', 'Mr'), ('mrs', 'Mrs'), ('ms', 'Ms'), ('miss', 'Miss'), ('dr', 'Dr'), ('prof', 'Prof'), ('prince', 'Prince'), ('princess', 'Princess'), ('company', 'Company'), ('na', 'N/A')], default='na', max_length=10, verbose_name='Title'), - ), - migrations.AlterField( - model_name='opportunity', - name='deal_status', - field=models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('canceled', 'Canceled'), ('lost', 'Lost'), ('won', 'Won')], default='new', max_length=20, verbose_name='Deal Status'), - ), - migrations.AlterField( - model_name='opportunitylog', - name='new_status', - field=models.CharField(blank=True, choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('canceled', 'Canceled'), ('lost', 'Lost'), ('won', 'Won')], max_length=50, null=True, verbose_name='New Status'), - ), - migrations.AlterField( - model_name='opportunitylog', - name='old_status', - field=models.CharField(blank=True, choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('canceled', 'Canceled'), ('lost', 'Lost'), ('won', 'Won')], max_length=50, null=True, verbose_name='Old Status'), - ), - ] diff --git a/inventory/migrations/0008_alter_notes_options_alter_notification_options_and_more.py b/inventory/migrations/0008_alter_notes_options_alter_notification_options_and_more.py deleted file mode 100644 index 47e0ef38..00000000 --- a/inventory/migrations/0008_alter_notes_options_alter_notification_options_and_more.py +++ /dev/null @@ -1,224 +0,0 @@ -# Generated by Django 5.1.4 on 2025-01-09 09:19 - -import django.db.models.deletion -import django.utils.timezone -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ('inventory', '0007_remove_customer_nationality_customer_country_and_more'), - ] - - operations = [ - migrations.AlterModelOptions( - name='notes', - options={'verbose_name': 'Note', 'verbose_name_plural': 'Notes'}, - ), - migrations.AlterModelOptions( - name='notification', - options={'ordering': ['-created'], 'verbose_name': 'Notification', 'verbose_name_plural': 'Notifications'}, - ), - migrations.RemoveField( - model_name='customer', - name='obligations', - ), - migrations.RemoveField( - model_name='customer', - name='salary', - ), - migrations.RemoveField( - model_name='notes', - name='created_at', - ), - migrations.RemoveField( - model_name='notes', - name='opportunity', - ), - migrations.RemoveField( - model_name='notes', - name='updated_at', - ), - migrations.RemoveField( - model_name='notification', - name='created_at', - ), - migrations.RemoveField( - model_name='opportunity', - name='created_at', - ), - migrations.RemoveField( - model_name='opportunity', - name='created_by', - ), - migrations.RemoveField( - model_name='opportunity', - name='deal_name', - ), - migrations.RemoveField( - model_name='opportunity', - name='deal_status', - ), - migrations.RemoveField( - model_name='opportunity', - name='deal_value', - ), - migrations.RemoveField( - model_name='opportunity', - name='priority', - ), - migrations.RemoveField( - model_name='opportunity', - name='updated_at', - ), - migrations.RemoveField( - model_name='staff', - name='created_at', - ), - migrations.RemoveField( - model_name='staff', - name='updated_at', - ), - migrations.AddField( - model_name='notes', - name='content_type', - field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'), - preserve_default=False, - ), - migrations.AddField( - model_name='notes', - name='created', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created'), - preserve_default=False, - ), - migrations.AddField( - model_name='notes', - name='object_id', - field=models.PositiveIntegerField(default=1), - preserve_default=False, - ), - migrations.AddField( - model_name='notes', - name='updated', - field=models.DateTimeField(auto_now=True, verbose_name='Updated'), - ), - migrations.AddField( - model_name='notification', - name='created', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created'), - preserve_default=False, - ), - migrations.AddField( - model_name='opportunity', - name='created', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created'), - preserve_default=False, - ), - migrations.AddField( - model_name='opportunity', - name='dealer', - field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='inventory.dealer'), - preserve_default=False, - ), - migrations.AddField( - model_name='opportunity', - name='staff', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owner', to='inventory.staff', verbose_name='Owner'), - ), - migrations.AddField( - model_name='opportunity', - name='stage', - field=models.CharField(choices=[('prospect', 'Prospect'), ('proposal', 'Proposal'), ('negotiation', 'Negotiation'), ('closed_won', 'Closed Won'), ('closed_lost', 'Closed Lost')], default='prospect', max_length=20, verbose_name='Stage'), - preserve_default=False, - ), - migrations.AddField( - model_name='opportunity', - name='updated', - field=models.DateTimeField(auto_now=True, verbose_name='Updated'), - ), - migrations.AddField( - model_name='staff', - name='created', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created'), - preserve_default=False, - ), - migrations.AddField( - model_name='staff', - name='updated', - field=models.DateTimeField(auto_now=True, verbose_name='Updated'), - ), - migrations.AlterField( - model_name='notes', - name='created_by', - field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='notes_created', to='inventory.staff'), - ), - migrations.CreateModel( - name='Activity', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('object_id', models.PositiveIntegerField()), - ('activity_type', models.CharField(choices=[('call', 'Call'), ('sms', 'SMS'), ('email', 'Email'), ('whatsapp', 'WhatsApp'), ('visit', 'Visit'), ('add_car', 'Add Car'), ('reserve_car', 'Reserve Car'), ('remove_car', 'Remove Car'), ('create_quotation', 'Create Quotation'), ('cancel_quotation', 'Cancel Quotation'), ('create_order', 'Create Order'), ('cancel_order', 'Cancel Order'), ('create_invoice', 'Create Invoice'), ('cancel_invoice', 'Cancel Invoice')], max_length=50, verbose_name='Activity Type')), - ('notes', models.TextField(blank=True, null=True, verbose_name='Notes')), - ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), - ('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')), - ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), - ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='activities_created', to='inventory.staff')), - ], - options={ - 'verbose_name': 'Activity', - 'verbose_name_plural': 'Activities', - }, - ), - migrations.CreateModel( - name='Lead', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(choices=[('mr', 'Mr'), ('mrs', 'Mrs'), ('ms', 'Ms'), ('miss', 'Miss'), ('dr', 'Dr'), ('prof', 'Prof'), ('prince', 'Prince'), ('princess', 'Princess'), ('company', 'Company'), ('na', 'N/A')], max_length=20, verbose_name='Title')), - ('first_name', models.CharField(max_length=50, verbose_name='First Name')), - ('last_name', models.CharField(max_length=50, verbose_name='Last Name')), - ('email', models.EmailField(db_index=True, max_length=254, unique=True, verbose_name='Email')), - ('salary', models.PositiveIntegerField(blank=True, null=True, verbose_name='Salary')), - ('obligations', models.PositiveIntegerField(blank=True, null=True, verbose_name='Obligations')), - ('year', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Year')), - ('source', models.CharField(choices=[('referrals', 'Referrals'), ('whatsapp', 'WhatsApp'), ('showroom', 'Showroom'), ('tiktok', 'TikTok'), ('instagram', 'Instagram'), ('x', 'X'), ('facebook', 'Facebook'), ('motory', 'Motory'), ('influencers', 'Influencers'), ('youtube', 'Youtube'), ('campaign', 'Campaign')], max_length=50, verbose_name='Source')), - ('channel', models.CharField(choices=[('walk_in', 'Walk In'), ('toll_free', 'Toll Free'), ('website', 'Website'), ('email', 'Email'), ('form', 'Form')], max_length=50, verbose_name='Channel')), - ('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')], default='low', max_length=10, verbose_name='Priority')), - ('status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('assigned', 'Assigned'), ('in_progress', 'In Progress'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], db_index=True, max_length=50, verbose_name='Status')), - ('created', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Created')), - ('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')), - ('assigned', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned', to='inventory.staff', verbose_name='Assigned')), - ('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leads', to='inventory.dealer')), - ('id_car_make', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake', verbose_name='Make')), - ('id_car_model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel', verbose_name='Model')), - ], - options={ - 'verbose_name': 'Lead', - 'verbose_name_plural': 'Leads', - }, - ), - migrations.AddField( - model_name='customer', - name='lead', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='converted', to='inventory.lead', verbose_name='Lead'), - ), - migrations.CreateModel( - name='LeadStatusHistory', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('old_status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('assigned', 'Assigned'), ('in_progress', 'In Progress'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], max_length=50, verbose_name='Old Status')), - ('new_status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('assigned', 'Assigned'), ('in_progress', 'In Progress'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], max_length=50, verbose_name='New Status')), - ('changed_at', models.DateTimeField(auto_now_add=True, verbose_name='Changed At')), - ('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='status_changes', to='inventory.staff')), - ('lead', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='status_history', to='inventory.lead')), - ], - options={ - 'verbose_name': 'Lead Status History', - 'verbose_name_plural': 'Lead Status Histories', - }, - ), - migrations.DeleteModel( - name='OpportunityLog', - ), - ] diff --git a/inventory/migrations/0009_alter_staff_managers.py b/inventory/migrations/0009_alter_staff_managers.py deleted file mode 100644 index 1b94fe19..00000000 --- a/inventory/migrations/0009_alter_staff_managers.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.1.4 on 2025-01-09 09:57 - -import inventory.models -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0008_alter_notes_options_alter_notification_options_and_more'), - ] - - operations = [ - migrations.AlterModelManagers( - name='staff', - managers=[ - ('objects', inventory.models.StaffUserManager()), - ], - ), - ] diff --git a/inventory/migrations/0010_customer_staff.py b/inventory/migrations/0010_customer_staff.py deleted file mode 100644 index ab0bdd30..00000000 --- a/inventory/migrations/0010_customer_staff.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.1.4 on 2025-01-09 11:36 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0009_alter_staff_managers'), - ] - - operations = [ - migrations.AddField( - model_name='customer', - name='staff', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customer_staff', to='inventory.staff', verbose_name='Staff'), - ), - ] diff --git a/inventory/migrations/0011_remove_customer_country_customer_city.py b/inventory/migrations/0011_remove_customer_country_customer_city.py deleted file mode 100644 index 29473352..00000000 --- a/inventory/migrations/0011_remove_customer_country_customer_city.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 5.1.4 on 2025-01-09 20:54 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0010_customer_staff'), - ] - - operations = [ - migrations.RemoveField( - model_name='customer', - name='country', - ), - migrations.AddField( - model_name='customer', - name='city', - field=models.CharField(blank=True, max_length=255, verbose_name='City'), - ), - ] diff --git a/inventory/migrations/0012_opportunity_probability.py b/inventory/migrations/0012_opportunity_probability.py deleted file mode 100644 index b30a12ac..00000000 --- a/inventory/migrations/0012_opportunity_probability.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.1.4 on 2025-01-11 10:32 - -import inventory.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0011_remove_customer_country_customer_city'), - ] - - operations = [ - migrations.AddField( - model_name='opportunity', - name='probability', - field=models.PositiveIntegerField(default=70, validators=[inventory.models.validate_probability]), - preserve_default=False, - ), - ] diff --git a/inventory/migrations/0013_lead_phone_number.py b/inventory/migrations/0013_lead_phone_number.py deleted file mode 100644 index b9ac98ba..00000000 --- a/inventory/migrations/0013_lead_phone_number.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.1.4 on 2025-01-11 11:09 - -import phonenumber_field.modelfields -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0012_opportunity_probability'), - ] - - operations = [ - migrations.AddField( - model_name='lead', - name='phone_number', - field=phonenumber_field.modelfields.PhoneNumberField(default='0535521547', max_length=128, region='SA', verbose_name='Phone Number'), - preserve_default=False, - ), - ] diff --git a/inventory/migrations/0014_alter_activity_created_by_alter_notes_created_by.py b/inventory/migrations/0014_alter_activity_created_by_alter_notes_created_by.py deleted file mode 100644 index b25d5a2b..00000000 --- a/inventory/migrations/0014_alter_activity_created_by_alter_notes_created_by.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 5.1.4 on 2025-01-11 12:12 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0013_lead_phone_number'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AlterField( - model_name='activity', - name='created_by', - field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='activities_created', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='notes', - name='created_by', - field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='notes_created', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/inventory/migrations/0015_lead_city.py b/inventory/migrations/0015_lead_city.py deleted file mode 100644 index 33f6c4b2..00000000 --- a/inventory/migrations/0015_lead_city.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.1.4 on 2025-01-11 12:28 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0014_alter_activity_created_by_alter_notes_created_by'), - ] - - operations = [ - migrations.AddField( - model_name='lead', - name='city', - field=models.CharField(default='Riyadh', max_length=50, verbose_name='City'), - preserve_default=False, - ), - ] diff --git a/inventory/migrations/0016_lead_address.py b/inventory/migrations/0016_lead_address.py deleted file mode 100644 index 62d58179..00000000 --- a/inventory/migrations/0016_lead_address.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.1.4 on 2025-01-11 12:31 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0015_lead_city'), - ] - - operations = [ - migrations.AddField( - model_name='lead', - name='address', - field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Address'), - ), - ] diff --git a/inventory/migrations/0017_alter_lead_assigned.py b/inventory/migrations/0017_alter_lead_assigned.py deleted file mode 100644 index 1e7aaf8f..00000000 --- a/inventory/migrations/0017_alter_lead_assigned.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.1.4 on 2025-01-11 19:20 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0016_lead_address'), - ] - - operations = [ - migrations.AlterField( - model_name='lead', - name='assigned', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned', to='inventory.staff', verbose_name='Assigned'), - ), - ] diff --git a/inventory/migrations/0018_alter_lead_priority.py b/inventory/migrations/0018_alter_lead_priority.py deleted file mode 100644 index 4ddbaa54..00000000 --- a/inventory/migrations/0018_alter_lead_priority.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.1.4 on 2025-01-11 23:36 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0017_alter_lead_assigned'), - ] - - operations = [ - migrations.AlterField( - model_name='lead', - name='priority', - field=models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')], default='medium', max_length=10, verbose_name='Priority'), - ), - ] diff --git a/inventory/migrations/0019_opportunity_closed_opportunity_closing_date_and_more.py b/inventory/migrations/0019_opportunity_closed_opportunity_closing_date_and_more.py deleted file mode 100644 index 572ebea5..00000000 --- a/inventory/migrations/0019_opportunity_closed_opportunity_closing_date_and_more.py +++ /dev/null @@ -1,45 +0,0 @@ -# Generated by Django 5.1.4 on 2025-01-12 01:43 - -import django.utils.timezone -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0018_alter_lead_priority'), - ] - - operations = [ - migrations.AddField( - model_name='opportunity', - name='closed', - field=models.BooleanField(default=False, verbose_name='Closed'), - ), - migrations.AddField( - model_name='opportunity', - name='closing_date', - field=models.DateField(default=django.utils.timezone.now, verbose_name='Closing Date'), - preserve_default=False, - ), - migrations.AddField( - model_name='opportunity', - name='status', - field=models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], default='new', max_length=20, verbose_name='Status'), - ), - migrations.AlterField( - model_name='lead', - name='status', - field=models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], db_index=True, default='new', max_length=50, verbose_name='Status'), - ), - migrations.AlterField( - model_name='leadstatushistory', - name='new_status', - field=models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], max_length=50, verbose_name='New Status'), - ), - migrations.AlterField( - model_name='leadstatushistory', - name='old_status', - field=models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], max_length=50, verbose_name='Old Status'), - ), - ] diff --git a/inventory/models.py b/inventory/models.py index ca265bf9..61ca451b 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -75,6 +75,22 @@ class VatRate(models.Model): def __str__(self): return f"Rate: {self.rate}%" +class CarType(models.IntegerChoices): + CAR = 1, _('Car') + LIGHT_COMMERCIAL = 2, _('Light Commercial') + HEAVY_DUTY_TRACTORS = 3, _('Heavy-Duty Tractors') + TRAILERS = 4, _('Trailers') + MEDIUM_TRUCKS = 5, _('Medium Trucks') + BUSES = 6, _('Buses') + MOTORCYCLES = 20, _('Motorcycles') + BUGGY = 21, _('Buggy') + MOTO_ATV = 22, _('Moto ATV') + SCOOTERS = 23, _('Scooters') + KARTING = 24, _('Karting') + ATV = 25, _('ATV') + SNOWMOBILES = 26, _('Snowmobiles') + + class CarMake(models.Model, LocalizedNameMixin): id_car_make = models.AutoField(primary_key=True) diff --git a/inventory/urls.py b/inventory/urls.py index 0ae7a21c..218b75f7 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -78,9 +78,11 @@ urlpatterns = [ path('cars/finance//update/', views.CarFinanceUpdateView.as_view(), name='car_finance_update'), path('cars/add/', views.CarCreateView.as_view(), name='car_add'), path('ajax/', views.AjaxHandlerView.as_view(), name='ajax_handler'), + path('cars/get-car-models/', views.get_car_models, name='get_car_models'), path('cars//add-color/', views.CarColorCreate.as_view(), name='add_color'), path('cars//location/add/', views.CarLocationCreateView.as_view(), name='add_car_location'), path('cars//location/update/', views.CarLocationUpdateView.as_view(), name='transfer'), + path('cars/inventory/search/', views.SearchCodeView.as_view(), name='car_search'), # path('cars//colors//update/',views.CarColorUpdateView.as_view(),name='color_update'), path('cars/reserve//', views.reserve_car_view, name='reserve_car'), @@ -183,6 +185,6 @@ handler500 = 'inventory.views.custom_error_view' handler403 = 'inventory.views.custom_permission_denied_view' handler400 = 'inventory.views.custom_bad_request_view' - + diff --git a/inventory/views.py b/inventory/views.py index 85c76335..bbdca039 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -1,5 +1,7 @@ from decimal import Decimal from django.core.paginator import Paginator +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt from django_ledger.models import ( EntityModel, InvoiceModel, @@ -81,6 +83,10 @@ from django.contrib.auth.models import User from allauth.account import views from django.db.models import Count, F, Value from django.contrib.auth import authenticate +import cv2 +import numpy as np +from pyzbar.pyzbar import decode + logger = logging.getLogger(__name__) @@ -427,6 +433,38 @@ class AjaxHandlerView(LoginRequiredMixin, View): return JsonResponse(serialized_options, safe=False) +@method_decorator(csrf_exempt, name='dispatch') +class SearchCodeView(View): + template_name = "inventory/scan_vin.html" + + def get(self, request, *args, **kwargs): + """Render the form page.""" + return render(request, self.template_name) + + def post(self, request, *args, **kwargs): + image_file = request.FILES.get('image') + + if image_file: + print("image received!") + image = cv2.imdecode(np.frombuffer(image_file.read(), np.uint8), cv2.IMREAD_COLOR) + decoded_objects = decode(image) + if decoded_objects: + print("image decoded!") + print(decoded_objects[0]) + code = decoded_objects[0].data.decode('utf-8') + print("code received!") + print(code) + car = get_object_or_404(models.Car, vin=code) + name = car.id_car_make.get_local_name + print(name) + return redirect('car_detail', pk=car.pk) + else: + print("back to else statement") + return JsonResponse({'success': False, 'error': 'No code detected'}) + else: + return JsonResponse({'success': False, 'error': 'No image provided'}) + + class CarInventory(LoginRequiredMixin, ListView): model = models.Car home_label = _("inventory") @@ -594,9 +632,7 @@ class CarFinanceCreateView(LoginRequiredMixin, CreateView): def get_form(self, form_class=None): form = super().get_form(form_class) dealer = get_user_type(self.request) - form.fields[ - "additional_finances" - ].queryset = models.AdditionalServices.objects.filter(dealer=dealer) + form.fields["additional_finances"].queryset = models.AdditionalServices.objects.filter(dealer=dealer) return form # def get_initial(self): @@ -1702,17 +1738,16 @@ class BankAccountCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView) success_message = "Bank account created successfully." def form_valid(self, form): - form.instance.entity_model = self.request.user.dealer.entity + dealer = get_user_type(self.request) + form.instance.entity_model = dealer.entity return super().form_valid(form) def get_form_kwargs(self): - """ - Override this method to pass additional keyword arguments to the form. - """ - entity = self.request.user.dealer.entity + dealer = get_user_type(self.request) + entity = dealer.entity kwargs = super().get_form_kwargs() - kwargs["entity_slug"] = entity.slug # Get entity_slug from URL - kwargs["user_model"] = entity.admin # Get user_model from the request + kwargs["entity_slug"] = entity.slug + kwargs["user_model"] = entity.admin return kwargs @@ -1730,10 +1765,8 @@ class BankAccountUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView) success_message = "Bank account updated successfully." def get_form_kwargs(self): - """ - Override this method to pass additional keyword arguments to the form. - """ - entity = self.request.user.dealer.entity + dealer = get_user_type(self.request) + entity = dealer.entity kwargs = super().get_form_kwargs() kwargs["entity_slug"] = entity.slug # Get entity_slug from URL kwargs["user_model"] = entity.admin # Get user_model from the request @@ -1764,7 +1797,8 @@ class AccountListView(LoginRequiredMixin, ListView): paginate_by = 10 def get_queryset(self): - entity = self.request.user.dealer.entity + dealer = get_user_type(self.request) + entity = dealer.entity qs = entity.get_all_accounts() paginator = Paginator(qs, 20) page_number = self.request.GET.get("page", 1) # Default to page 1 @@ -2417,12 +2451,11 @@ class LeadDetailView(DetailView): class LeadCreateView(CreateView, SuccessMessageMixin, LoginRequiredMixin): model = models.Lead - form_class = forms.LeadCreateForm - template_name = 'crm/leads/lead_create_form.html' - success_message = "Lead created successfully!" + form_class = forms.LeadForm + template_name = 'crm/leads/lead_form.html' + # success_message = "Lead created successfully!" success_url = reverse_lazy('lead_list') - def form_valid(self, form): print("Form data:", form.cleaned_data) # Debug form data dealer = get_user_type(self.request) @@ -2430,10 +2463,18 @@ class LeadCreateView(CreateView, SuccessMessageMixin, LoginRequiredMixin): return super().form_valid(form) +def get_car_models(request): + make_id = request.GET.get('id_car_make') + if make_id: + car_models = models.CarModel.objects.filter(id_car_make=make_id).values('id_car_model', 'name', 'arabic_name') + return JsonResponse(list(car_models), safe=False) + return JsonResponse([], safe=False) + + class LeadUpdateView(UpdateView): model = models.Lead - form_class = forms.LeadUpdateForm - template_name = 'crm/leads/lead_update_form.html' + form_class = forms.LeadForm + template_name = 'crm/leads/lead_form.html' success_url = reverse_lazy('lead_list') class LeadDeleteView(DeleteView): diff --git a/requirements.txt b/requirements.txt index 02c91b3a..3839fa37 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,26 +1,43 @@ +aiohappyeyeballs==2.4.4 +aiohttp==3.11.11 +aiohttp-retry==2.8.3 +aiosignal==1.3.2 alabaster==1.0.0 +albucore==0.0.13 +albumentations==1.4.10 annotated-types==0.7.0 anyio==4.6.2.post1 +arabic-reshaper==3.0.0 asgiref==3.8.1 +astor==0.8.1 astroid==3.3.5 attrs==23.2.0 autopep8==2.3.1 babel==2.16.0 beautifulsoup4==4.12.3 +bleach==6.2.0 +blinker==1.9.0 Brotli==1.1.0 certifi==2024.8.30 cffi==1.17.1 chardet==5.2.0 charset-normalizer==3.4.0 -click==8.1.7 +click==8.1.8 +colorama==0.4.6 +commonmark==0.9.1 +contourpy==1.3.1 crispy-bootstrap5==2024.10 cryptography==44.0.0 cssselect2==0.7.0 +cycler==0.12.1 +Cython==3.0.11 +decorator==5.1.1 +desert==2020.11.18 dill==0.3.9 distro==1.9.0 dj-rest-auth==7.0.0 dj-shop-cart==7.1.1 -Django==4.2.17 +Django==5.1.4 django-allauth==65.3.0 django-autoslug==1.9.9 django-bootstrap5==24.3 @@ -34,10 +51,13 @@ django-filter==24.3 django-formtools==2.5.1 django-ledger==0.7.0 django-money==3.5.3 +django-nine==0.2.7 +django-nonefield==0.4 django-phonenumber-field==8.0.0 django-prometheus==2.3.1 django-sekizai==4.1.0 django-silk==5.3.1 +django-sms==0.7.0 django-sslserver==0.22 django-tables2==2.7.0 django-treebeard==4.7.1 @@ -47,9 +67,13 @@ djangorestframework==3.15.2 djangorestframework-simplejwt==5.3.1 djangoviz==0.1.1 docutils==0.21.2 +easy-thumbnails==2.10 et_xmlfile==2.0.0 Faker==33.1.0 +fire==0.7.0 +Flask==3.1.0 fonttools==4.55.3 +frozenlist==1.5.0 gprof2dot==2024.6.6 graphqlclient==0.2.4 greenlet==3.1.1 @@ -57,80 +81,143 @@ h11==0.14.0 httpcore==1.0.7 httpx==0.28.0 idna==3.10 +imageio==2.36.1 imagesize==1.4.1 +imgaug==0.4.0 iso4217==1.12.20240625 isodate==0.7.2 isort==5.13.2 +itsdangerous==2.2.0 Jinja2==3.1.4 jiter==0.8.0 joblib==1.4.2 +kiwisolver==1.4.8 +lazy_loader==0.4 ledger==1.0.1 +lmdb==1.6.2 lxml==5.3.0 Markdown==3.7 markdown-it-py==3.0.0 MarkupSafe==3.0.2 +marshmallow==3.23.2 +matplotlib==3.10.0 mccabe==0.7.0 mdurl==0.1.2 +MouseInfo==0.1.3 +multidict==6.1.0 +mypy-extensions==1.0.0 +networkx==3.4.2 newrelic==10.3.1 nltk==3.9.1 +numpy==1.26.4 oauthlib==3.2.2 ofxtools==0.9.5 openai==1.56.2 +opencv-contrib-python==4.10.0.84 +opencv-python==4.10.0.84 +opencv-python-headless==4.10.0.84 openpyxl==3.1.5 +opt-einsum==3.3.0 +outcome==1.3.0.post0 packaging==24.2 +pandas==2.2.3 pango==0.0.1 pdfkit==1.0.0 phonenumbers==8.13.51 pillow==11.0.0 platformdirs==4.3.6 prometheus_client==0.21.1 +propcache==0.2.1 +protobuf==5.29.3 psycopg==3.2.3 psycopg-binary==3.2.3 +psycopg-c==3.2.3 py-moneyed==3.0 +PyAutoGUI==0.9.54 +pyclipper==1.3.0.post6 pycodestyle==2.12.1 pycparser==2.22 pydantic==2.10.3 pydantic_core==2.27.1 pydotplus==2.0.2 pydyf==0.11.0 +PyGetWindow==0.0.9 Pygments==2.18.0 PyJWT==2.10.1 pylint==3.3.2 +PyMsgBox==1.0.9 PyMySQL==1.1.1 +pyobjc-core==10.3.2 +pyobjc-framework-Cocoa==10.3.2 +pyobjc-framework-Quartz==10.3.2 pyparsing==3.2.0 +pyperclip==1.9.0 pyphen==0.17.0 pypng==0.20220715.0 +PyRect==0.2.0 +PyScreeze==1.0.1 pyserial==3.5 +PySocks==1.7.1 +python-bidi==0.6.3 python-dateutil==2.9.0.post0 +python-docx==1.1.2 python-openid==2.2.5 python3-saml==1.16.0 +pytweening==1.2.0 pytz==2024.2 pyvin==0.0.2 +pywa==2.4.0 +pywhat==5.1.0 +pywhatkit==5.4 +PyYAML==6.0.2 qrcode==8.0 +RapidFuzz==3.11.0 regex==2024.11.6 reportlab==4.2.5 requests==2.32.3 requests-oauthlib==2.0.0 rich==13.9.4 -setuptools==75.6.0 +rubicon-objc==0.4.9 +scikit-image==0.25.0 +scikit-learn==1.6.0 +scipy==1.14.1 +selenium==4.27.1 +shapely==2.0.6 six==1.16.0 sniffio==1.3.1 snowballstemmer==2.2.0 +sortedcontainers==2.4.0 soupsieve==2.6 SQLAlchemy==2.0.36 sqlparse==0.5.2 tablib==3.7.0 +termcolor==2.5.0 +threadpoolctl==3.5.0 +tifffile==2025.1.10 tinycss2==1.4.0 tinyhtml5==2.0.0 +tomli==2.2.1 tomlkit==0.13.2 tqdm==4.67.1 +trio==0.28.0 +trio-websocket==0.11.1 +twilio==9.4.1 +typing-inspect==0.9.0 typing_extensions==4.12.2 +tzdata==2024.2 +Unidecode==1.3.8 upgrade-requirements==1.7.0 urllib3==2.2.3 vin==0.6.2 vininfo==1.8.0 +vishap==0.1.5 +vpic-api==0.7.4 weasyprint==63.1 webencodings==0.5.1 +websocket-client==1.8.0 Werkzeug==3.1.3 +wikipedia==1.4.0 +wsproto==1.2.0 xmlsec==1.3.14 +yarl==1.18.3 zopfli==0.2.3.post1 diff --git a/templates/crm/leads/lead_create_form.html b/templates/crm/leads/lead_create_form.html deleted file mode 100644 index 4cd1f04a..00000000 --- a/templates/crm/leads/lead_create_form.html +++ /dev/null @@ -1,388 +0,0 @@ -{% extends "base.html" %} -{% load i18n static %} - -{% block content %} -
-
-

- {% if form.instance.pk %} - {{ _("Edit Lead") }} - {% else %} - {{ _("Add New Lead") }} - {% endif %} -

-
- -
- -
- {% csrf_token %} - - -
-
- - -
- {{ form.title.errors }} -
- - -
-
- - -
- {{ form.first_name.errors }} -
- - -
-
- - -
- {{ form.last_name.errors }} -
- - -
-
- - -
- {{ form.email.errors }} -
- - -
-
- - -
- {{ form.phone_number.errors }} -
- - -
-
- - -
- {{ form.salary.errors }} -
- - -
-
- - -
- {{ form.obligations.errors }} -
- - -
-
- - -
- -
- - -
-
- - -
-
- - -
-
- - -
- {{ form.year.errors }} -
- - -
-
- - -
- {{ form.source.errors }} -
- - -
-
- - -
- {{ form.channel.errors }} -
- - -
-
- - -
- {{ form.assigned.errors }} -
- - -
-
- - -
- {{ form.priority.errors }} -
- - -
- - - {{ _("Cancel") }} - -
-
-
-
- - -{% endblock %} \ No newline at end of file diff --git a/templates/crm/leads/lead_form.html b/templates/crm/leads/lead_form.html new file mode 100644 index 00000000..525b683f --- /dev/null +++ b/templates/crm/leads/lead_form.html @@ -0,0 +1,20 @@ +{% extends 'base.html' %} +{% load i18n static crispy_forms_filters %} +{% block content %} +

{% if object %}Update{% else %}Create{% endif %}

+
+ {% csrf_token %} + {{ form|crispy }} + + + {{ _("Cancel") }} + +
+{% endblock %} \ No newline at end of file diff --git a/templates/crm/leads/lead_update_form.html b/templates/crm/leads/lead_update_form.html deleted file mode 100644 index 3adc6662..00000000 --- a/templates/crm/leads/lead_update_form.html +++ /dev/null @@ -1,232 +0,0 @@ -{% extends "base.html" %} -{% load i18n static%} - -{% block content %} - -
-
-

{% if form.instance.pk %}{{ _("Edit Lead") }}{% else %}{{ _("Add New Lead") }}{% endif %}

-
-
-
- {% csrf_token %} - -
-
- - -
- {{ form.title.errors }} -
- - -
-
- - -
- {{ form.first_name.errors }} -
- - -
-
- - -
- {{ form.last_name.errors }} -
- - -
-
- - -
- {{ form.email.errors }} -
- - -
-
- - -
- {{ form.phone_number.errors }} -
- - -
-
- - -
- {{ form.salary.errors }} -
- - -
-
- - -
- {{ form.obligations.errors }} -
- - -
-
- - -
- {{ form.id_car_make.errors }} -
- - -
-
- - -
- {{ form.id_car_model.errors }} -
- - -
-
- - -
- {{ form.year.errors }} -
- - -
-
- - -
- {{ form.source.errors }} -
- - -
-
- - -
- {{ form.channel.errors }} -
- - -
-
- - -
- {{ form.assigned.errors }} -
- - -
-
- - -
- {{ form.priority.errors }} -
-
- - {{ _("Cancel") }} -
-
-
-
- - -{% endblock %} \ No newline at end of file diff --git a/templates/header.html b/templates/header.html index a3091b95..44b60608 100644 --- a/templates/header.html +++ b/templates/header.html @@ -10,31 +10,31 @@ diff --git a/templates/inventory/car_form.html b/templates/inventory/car_form.html index 197859b0..609a647a 100644 --- a/templates/inventory/car_form.html +++ b/templates/inventory/car_form.html @@ -282,7 +282,7 @@