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 f488685d..186847c1 100644 --- a/haikalbot/migrations/0001_initial.py +++ b/haikalbot/migrations/0001_initial.py @@ -1,4 +1,8 @@ +<<<<<<< HEAD # Generated by Django 4.2.17 on 2025-01-13 08:40 +======= +# Generated by Django 5.1.4 on 2025-01-12 17:20 +>>>>>>> 3889fed781038764cbb5e2e895a933edb4ebb451 from django.db import migrations, models diff --git a/haikalbot/migrations/0002_initial.py b/haikalbot/migrations/0002_initial.py index 5bc0e0bb..46ca49a3 100644 --- a/haikalbot/migrations/0002_initial.py +++ b/haikalbot/migrations/0002_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.17 on 2025-01-13 08:40 +# Generated by Django 5.1.4 on 2025-01-12 17:20 from django.db import migrations, models import django.db.models.deletion 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 ce595921..1d9083b4 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -580,36 +580,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 acb0f9ac..94b54d6e 100644 --- a/inventory/migrations/0001_initial.py +++ b/inventory/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.17 on 2025-01-13 08:40 +# Generated by Django 5.1.4 on 2025-01-12 17:20 from decimal import Decimal from django.conf import settings @@ -14,7 +14,6 @@ class Migration(migrations.Migration): initial = True dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('contenttypes', '0002_remove_content_type_name'), ('django_ledger', '0017_alter_accountmodel_unique_together_and_more'), ] @@ -75,13 +74,138 @@ 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=[])), + ('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', }, 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='SubscriptionPlan', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='Name of the subscription plan', max_length=100, unique=True)), + ('description', models.TextField()), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('max_users', models.PositiveIntegerField(default=1, help_text='Maximum number of users allowed')), + ('max_inventory_size', models.PositiveIntegerField(default=50, help_text='Maximum number of cars in inventory')), + ('support_level', models.CharField(choices=[('basic', 'Basic Support'), ('priority', 'Priority Support'), ('dedicated', 'Dedicated Support')], default='basic', help_text='Level of support provided', max_length=50)), + ('custom_features', models.JSONField(blank=True, help_text='Additional features specific to this plan', null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'verbose_name': 'Subscription Plan', + 'verbose_name_plural': 'Subscription Plans', + }, + ), + 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='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=[ + ('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')), + ('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.AddField( + model_name='car', + name='id_car_make', + field=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'), + ), migrations.CreateModel( name='CarModel', fields=[ @@ -186,32 +310,30 @@ class Migration(migrations.Migration): ], ), migrations.CreateModel( - name='ExteriorColors', + name='CarLocation', 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')), + ('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': 'Exterior Colors', - 'verbose_name_plural': 'Exterior Colors', + 'verbose_name': 'Car Location', + 'verbose_name_plural': 'Car Locations', }, - 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.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='Lead', @@ -233,12 +355,71 @@ class Migration(migrations.Migration): ('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': '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', + }, + ), + 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=[ @@ -297,6 +478,98 @@ class Migration(migrations.Migration): ('dealer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sales', to='inventory.dealer')), ], ), + migrations.AddField( + model_name='payment', + name='quotation', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='inventory.salequotation'), + ), + migrations.CreateModel( + name='SaleQuotationCar', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(default=1, verbose_name='Quantity')), + ('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.car', verbose_name='Car')), + ('quotation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quotation_cars', to='inventory.salequotation', verbose_name='Quotation')), + ], + ), + migrations.CreateModel( + name='SalesOrder', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('total_amount', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Total Amount')), + ('quotation', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='sales_order', to='inventory.salequotation', verbose_name='Quotation')), + ], + ), + 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=[('manager', 'Manager'), ('inventory', 'Inventory'), ('accountant', 'Accountant'), ('sales', 'Sales'), ('coordinator', 'Coordinator'), ('receptionist', 'Receptionist'), ('agent', 'Agent')], max_length=255, verbose_name='Staff Type')), + ('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)), + ], + options={ + 'verbose_name': 'Staff', + 'verbose_name_plural': 'Staff', + 'permissions': [], + }, + bases=(models.Model, inventory.mixins.LocalizedNameMixin), + managers=[ + ('objects', inventory.models.StaffUserManager()), + ], + ), + migrations.CreateModel( + name='Opportunity', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('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': '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='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', fields=[ diff --git a/inventory/models.py b/inventory/models.py index 53401533..a920d053 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -77,6 +77,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/signals.py b/inventory/signals.py index 543db67b..fca8c26d 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -107,20 +107,11 @@ def create_ledger_entity(sender, instance, created, **kwargs): ) asset_ca_cash.role_default = True asset_ca_cash.save() - - asset_ca_cash_bank = entity.create_account( - coa_model=coa, - code="1102", - role=roles.ASSET_CA_CASH, - name=_("Cash in Bank"), - balance_type="debit", - active=True, - ) # Accounts Receivable Account asset_ca_receivables = entity.create_account( coa_model=coa, - code="1103", + code="1102", role=roles.ASSET_CA_RECEIVABLES, name=_("Accounts Receivable"), balance_type="debit", @@ -128,6 +119,19 @@ def create_ledger_entity(sender, instance, created, **kwargs): ) asset_ca_receivables.role_default = True asset_ca_receivables.save() + + # Inventory Account + asset_ca_inventory = entity.create_account( + coa_model=coa, + code="1103", + role=roles.ASSET_CA_INVENTORY, + name=_("Inventory"), + balance_type="debit", + active=True, + ) + asset_ca_inventory.role_default = True + asset_ca_inventory.save() + # Prepaid Expenses Account asset_ca_prepaid = entity.create_account( @@ -247,16 +251,6 @@ def create_ledger_entity(sender, instance, created, **kwargs): asset_ppe_buildings.save() - asset_ppe_buildings_accum_depreciation = entity.create_account( - coa_model=coa, - code="1303", - role=roles.ASSET_PPE_EQUIPMENT, - name=_("Property, Plant, and Equipment"), - balance_type="credit", - active=True, - ) - asset_ppe_buildings_accum_depreciation.role_default = True - asset_ppe_buildings_accum_depreciation.save() # Accounts Payable Account liability_cl_acc_payable = entity.create_account( @@ -269,7 +263,19 @@ def create_ledger_entity(sender, instance, created, **kwargs): ) liability_cl_acc_payable.role_default = True liability_cl_acc_payable.save() - + + # Deferred Revenue Account + liability_cl_def_rev = entity.create_account( + coa_model=coa, + code="2103", + role=roles.LIABILITY_CL_DEFERRED_REVENUE, + name=_("Deferred Revenue"), + balance_type="credit", + active=True, + ) + liability_cl_def_rev.role_default = True + liability_cl_def_rev.save() + # Wages Payable Account liability_cl_wages_payable = entity.create_account( coa_model=coa, @@ -282,34 +288,10 @@ def create_ledger_entity(sender, instance, created, **kwargs): liability_cl_wages_payable.role_default = True liability_cl_wages_payable.save() - # Deferred Revenue Account - liability_cl_def_rev = entity.create_account( - coa_model=coa, - code="2103", - role=roles.LIABILITY_CL_DEFERRED_REVENUE, - name=_("Deferred Revenue"), - balance_type="credit", - active=True, - ) - liability_cl_def_rev.role_default = True - liability_cl_def_rev.save() - - # Short-Term Notes Payable Account - liability_ltl_notes_payable = entity.create_account( - coa_model=coa, - code="2104", - role=roles.LIABILITY_CL_ST_NOTES_PAYABLE, - name=_("Short-Term Notes Payable"), - balance_type="credit", - active=True, - ) - liability_ltl_notes_payable.role_default = True - liability_ltl_notes_payable.save() - # Long-Term Notes Payable Account liability_ltl_notes_payable = entity.create_account( coa_model=coa, - code="2105", + code="2201", role=roles.LIABILITY_LTL_NOTES_PAYABLE, name=_("Long-Term Notes Payable"), balance_type="credit", @@ -354,7 +336,7 @@ def create_ledger_entity(sender, instance, created, **kwargs): # Mortgage Payable Account liability_ltl_mortgage_payable = entity.create_account( coa_model=coa, - code="2203", + code="2202", role=roles.LIABILITY_LTL_MORTGAGE_PAYABLE, name=_("Mortgage Payable"), balance_type="credit", diff --git a/inventory/urls.py b/inventory/urls.py index 8ab137e7..6886d26d 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'), @@ -184,6 +186,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 97ebe964..e73d4e9e 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -1,6 +1,8 @@ from decimal import Decimal from django.core.paginator import Paginator from django.forms import DateField, DateInput, HiddenInput, TextInput +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt from django_ledger.models import ( EntityModel, InvoiceModel, @@ -82,6 +84,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__) @@ -433,6 +439,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") @@ -600,9 +638,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): @@ -1714,17 +1750,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 @@ -1742,10 +1777,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 @@ -1776,7 +1809,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 @@ -2509,10 +2543,10 @@ 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!" - success_url = reverse_lazy("lead_list") + 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 @@ -2521,12 +2555,19 @@ 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" - success_url = reverse_lazy("lead_list") - + form_class = forms.LeadForm + template_name = 'crm/leads/lead_form.html' + success_url = reverse_lazy('lead_list') class LeadDeleteView(DeleteView): model = models.Lead 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 9006cb7d..43efe394 100644 --- a/templates/header.html +++ b/templates/header.html @@ -10,31 +10,31 @@