From 9549213a09c500b9b749932d77861f4059fab6f9 Mon Sep 17 00:00:00 2001 From: Marwan Alwali Date: Sun, 12 Jan 2025 09:33:42 +0300 Subject: [PATCH] update --- .../__pycache__/0001_initial.cpython-311.pyc | Bin 1068 -> 1068 bytes inventory/__pycache__/forms.cpython-311.pyc | Bin 26721 -> 29110 bytes inventory/__pycache__/models.cpython-311.pyc | Bin 81683 -> 82948 bytes inventory/__pycache__/urls.cpython-311.pyc | Bin 19687 -> 20831 bytes inventory/__pycache__/views.cpython-311.pyc | Bin 128282 -> 134903 bytes inventory/forms.py | 50 +- .../0012_opportunity_probability.py | 20 + .../migrations/0013_lead_phone_number.py | 20 + ...ivity_created_by_alter_notes_created_by.py | 26 + inventory/migrations/0015_lead_city.py | 19 + inventory/migrations/0016_lead_address.py | 18 + .../migrations/0017_alter_lead_assigned.py | 19 + .../migrations/0018_alter_lead_priority.py | 18 + ...losed_opportunity_closing_date_and_more.py | 45 ++ inventory/models.py | 30 +- inventory/signals.py | 29 +- inventory/urls.py | 10 +- inventory/views.py | 194 +++++-- templates/account/2FA.html | 2 +- .../confirm_email_verification_code.html | 2 +- templates/account/confirm_login_code..html | 2 +- templates/account/email_confirm.html | 2 +- templates/account/lock-screen.html | 6 +- templates/account/login.html | 2 +- templates/account/logout.html | 2 +- templates/account/password_change.html | 2 +- templates/account/password_reset.html | 2 +- templates/account/request_login_code.html | 2 +- templates/account/signup-wizard.html | 2 +- templates/account/signup.html | 2 +- templates/auth_base.html | 32 +- templates/base.html | 6 +- templates/chat_support.html | 4 +- templates/crm/add_activity.html | 11 + templates/crm/add_note.html | 10 + templates/crm/lead_list.html | 185 ------ templates/crm/leads/lead_confirm_delete.html | 10 + templates/crm/leads/lead_detail.html | 549 ++++++++++++++++++ templates/crm/leads/lead_form.html | 222 +++++++ templates/crm/leads/lead_list.html | 200 +++++++ .../opportunity_confirm_delete.html | 0 .../opportunity_detail.html | 91 ++- .../crm/opportunities/opportunity_form.html | 82 +++ .../{ => opportunities}/opportunity_list.html | 130 ++--- .../{ => opportunities}/opportunity_logs.html | 0 templates/crm/opportunity_form.html | 6 - templates/customers/customer_form.html | 10 +- templates/customers/customer_list.html | 3 +- templates/customers/view_customer.html | 25 +- templates/dashboards/crm.html | 2 +- templates/dealers/activity_log.html | 4 +- templates/dealers/dealer_detail.html | 6 +- templates/dealers/dealer_form.html | 2 +- templates/errors/403.html | 4 +- templates/errors/404.html | 4 +- templates/errors/500.html | 4 +- templates/footer.html | 3 +- templates/haikalbot/chatbot.html | 2 +- templates/header.html | 78 ++- templates/index.html | 2 +- templates/inventory/add_colors.html | 2 +- templates/inventory/car_detail.html | 4 +- templates/inventory/car_edit.html | 2 +- templates/inventory/car_finance_form.html | 2 +- templates/inventory/car_form.html | 12 +- templates/inventory/car_inventory.html | 5 +- templates/inventory/car_list.html | 42 +- templates/inventory/car_location_form.html | 2 +- templates/inventory/color_palette.html | 2 +- templates/inventory/colors.html | 2 +- templates/inventory/inventory_stats.html | 2 +- templates/inventory/scan_vin.html | 2 +- templates/items/expenses/expense_create.html | 2 +- templates/items/expenses/expense_update.html | 2 +- templates/items/expenses/expenses_list.html | 2 +- templates/items/service/service_create.html | 2 +- templates/items/service/service_list.html | 2 +- .../bank_accounts/bank_account_detail.html | 2 +- .../bank_accounts/bank_account_form.html | 2 +- .../bank_accounts/bank_account_list.html | 2 +- .../ledger/coa_accounts/account_detail.html | 2 +- .../ledger/coa_accounts/account_form.html | 2 +- .../ledger/coa_accounts/account_list.html | 2 +- .../organizations/organization_detail.html | 2 +- .../organizations/organization_form.html | 2 +- .../organizations/organization_list.html | 2 +- .../representative_detail.html | 2 +- .../representatives/representative_form.html | 2 +- .../representatives/representative_list.html | 2 +- .../sales/estimates/estimate_detail.html | 4 +- templates/sales/estimates/estimate_form.html | 20 +- templates/sales/estimates/estimate_list.html | 2 +- .../sales/estimates/estimate_preview.html | 12 +- .../estimates/payment_request_detail.html | 4 +- .../invoices/approved_invoice_update.html | 2 +- .../sales/invoices/draft_invoice_update.html | 2 +- templates/sales/invoices/invoice_create.html | 2 +- templates/sales/invoices/invoice_detail.html | 2 +- templates/sales/invoices/invoice_list.html | 2 +- templates/sales/invoices/invoice_preview.html | 10 +- .../sales/invoices/paid_invoice_update.html | 2 +- templates/sales/journals/journal_form.html | 2 +- templates/sales/journals/journal_list.html | 2 +- templates/sales/payments/payment_create.html | 2 +- templates/sales/payments/payment_details.html | 2 +- templates/sales/payments/payment_form.html | 2 +- templates/sales/payments/payment_list.html | 2 +- templates/sales/quotation_form.html | 2 +- templates/sales/quotation_list.html | 2 +- templates/sales/sales_order_detail.html | 2 +- .../subscriptions/subscription_plan.html | 2 +- templates/test.html | 2 +- templates/users/user_detail.html | 2 +- templates/users/user_form.html | 2 +- templates/users/user_list.html | 2 +- templates/vendors/vendor_form.html | 2 +- templates/vendors/vendors_list.html | 2 +- templates/vendors/view_vendor.html | 2 +- templates/welcome.html | 18 +- 119 files changed, 1855 insertions(+), 587 deletions(-) create mode 100644 inventory/migrations/0012_opportunity_probability.py create mode 100644 inventory/migrations/0013_lead_phone_number.py create mode 100644 inventory/migrations/0014_alter_activity_created_by_alter_notes_created_by.py create mode 100644 inventory/migrations/0015_lead_city.py create mode 100644 inventory/migrations/0016_lead_address.py create mode 100644 inventory/migrations/0017_alter_lead_assigned.py create mode 100644 inventory/migrations/0018_alter_lead_priority.py create mode 100644 inventory/migrations/0019_opportunity_closed_opportunity_closing_date_and_more.py create mode 100644 templates/crm/add_activity.html create mode 100644 templates/crm/add_note.html delete mode 100644 templates/crm/lead_list.html create mode 100644 templates/crm/leads/lead_confirm_delete.html create mode 100644 templates/crm/leads/lead_detail.html create mode 100644 templates/crm/leads/lead_form.html create mode 100644 templates/crm/leads/lead_list.html rename templates/crm/{ => opportunities}/opportunity_confirm_delete.html (100%) rename templates/crm/{ => opportunities}/opportunity_detail.html (97%) create mode 100644 templates/crm/opportunities/opportunity_form.html rename templates/crm/{ => opportunities}/opportunity_list.html (82%) rename templates/crm/{ => opportunities}/opportunity_logs.html (100%) delete mode 100644 templates/crm/opportunity_form.html diff --git a/api/migrations/__pycache__/0001_initial.cpython-311.pyc b/api/migrations/__pycache__/0001_initial.cpython-311.pyc index c2ffaec92b0d3ccc721996a5b845eee183318530..88edabc7e63f4d6bce008f1d5aea590d2f0b8010 100644 GIT binary patch delta 20 acmZ3(v4(?tIWI340}vEDHEiToWB~v%XarRN delta 20 acmZ3(v4(?tIWI340}!aKuG`41$N~T{xdd+j diff --git a/inventory/__pycache__/forms.cpython-311.pyc b/inventory/__pycache__/forms.cpython-311.pyc index 28c7d4eea51891ce474ec38d65806fbb79f86fce..a774c93a9692dd4cb995e5f95766788171bd5cd3 100644 GIT binary patch delta 7621 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 Lz<-vYlSBJ|W&S%; diff --git a/inventory/__pycache__/models.cpython-311.pyc b/inventory/__pycache__/models.cpython-311.pyc index 63b3c4b810a28a867841c2c621a52f7ac73b6b55..7cc305530990a7b05f07f4c4ef5e717dada9f74e 100644 GIT binary patch delta 7580 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 b^kl3WymWY@kllV%Bib9L)ZOR}@zQ?)H{}aN delta 6498 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 Y@`cPdS3MwxN-^wP$P|Ck_wZNyKT<7hSO5S3 delta 2736 zcmZwIZA@F&83*tjd>!t!iAh3uX?#~ow~Gl7Gg87*62SP~V2r_rBqoFyUmFbOCALXI z5(v}MuHBM$=~MM(AEv0%G@@2YH}9>|T1~4oP1@F&&0SK`sjIZM>$(r#`eE8WY)}IvJ&1aR zwLws*&M7+&Ggv9rO7PuMJ8OV4QDFz3(I13yQD%)i+``br!>1UUd3ZqDoQg>b-wmF` z!a+WB8-o>!lEPXz9R?XbHCW-gL1wMIa2LZN9_ow<*2cq+j50fnflcst#A47@)(T3Q z%)Y{B=P?}N;WZ48!oSNDV+X%3{!&xgidlGdS1UZfOJ>J-0{c`$Fb(+EnnC81k%U(q{eR`dpWvKY)+so*u`e*6O(v4DXf`=Nw>6!%;8eOHH`V?f#_Ujf4Z!BliElkUw$kG#b2IT1Pn_$dx`9K}2QbVnac!TpXVHkT_s znr6oLa5cl9%vuFpJ$8wy&vaUt3Jsl&IEFe8;JDn`isS9hS{(0mK8mB%#YcxWE_XFy zzNL+Cck%xJYPqSK*N=BM<9M~3?{-_u?{-@;8*O~-usz(MmXwzi-E47%B)uMGw=tp5Fa$>tAwn3Y5yONLc+qYa zj}s#BGn^SEoPtkqW{hweOb)X+PEeuMp@?xp0!};X#Yw^xTyxY*Ny0RPFLx`_3@N2a zoF%29%4v}@ge(KMv%YzebZ+aSYj0K->W=2^p}Y`Y7s3T0Ts%)L7Z|+YRHP-+MFzh_ z;Z5RacA2zHT4C_8t6sc9vO&;g5qnCsMmt<_DWZemgjWzQf*an!H6DT&O5J$EgkETO zTl{^5V9Dm)QEo04ET6kOgVZs!)p2lbvC!iA)m&Z}TNlO(!dNjx9bveIyAKm0a1Sv` zI0dC1i#SF&4ecIF(>USG=XQ1H`_JTs=(-Rs2+?AcS|;EL?2i%Dt^Ve=kl!jTt93;F-^E@HD^*6LDb>w>^d0}*27%d2+#YJkl zSOP!seo4ASx?BQ(_A1gcX{7}A`V{dB$p+ns9^K|`O8NGS4uTt=@>xU=!3(b=e1u;3 z8={Zk2Xn7Q>?Z^?3=qPwgfqj05m-YUC!B!0h?9f}?CirkK{%yhjG%&wGjT!!05M6J zf?pt#glYIYVupan<~NJ8gavT;74a+~4GVt!7!a~LxaqH#7D?xHa0hjsbU_FI@$Z+G zNSAd`*N=xsTG7EC8$RPC8!Y#m#U5ev@NOa;1Sh=NZxLMtH~a(PA$XxKuwV2M{NM=G zi~WQEoDG=80YVU-4d7P_VF=y~SeinFu(0`2XnQqYXz$Ap#qxr>E~o`TEe=!52viQV ziN^^iU=VSV5P|ChZH=RZgs@|$VKtmLm|qw!eBsbfAI$5$>w0fN?=4PJ#}s@rfR86( z8cad6I73K5YtSss5-tlc9ITg?Nh<kA_ua PF#tV-`-MN^KZ5@Ocf$$H diff --git a/inventory/__pycache__/views.cpython-311.pyc b/inventory/__pycache__/views.cpython-311.pyc index 31ea8470422f65ed026f7c53ac12e65b4de3670f..4a3ef42fb4e9b557d722f8f49377c28a29273961 100644 GIT binary patch delta 18167 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 l_(ob-x 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 H;1c{lmm{$5 delta 13573 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 diff --git a/inventory/forms.py b/inventory/forms.py index 3458a5f0..a0b78a2b 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -28,7 +28,7 @@ from .models import ( SaleQuotationCar, AdditionalServices, Staff, - Opportunity, Priority, Sources, + Opportunity, Priority, Sources, Lead, Activity, Notes, CarModel ) from django_ledger.models import ItemModel, InvoiceModel from django.forms import ModelMultipleChoiceField, ValidationError, DateInput @@ -567,10 +567,50 @@ class EmailForm(forms.Form): from_email = forms.EmailField() to_email = forms.EmailField(label="To") +class LeadForm(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): + dealer = kwargs.pop("dealer", None) + 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 NoteForm(forms.ModelForm): + class Meta: + model = Notes + fields = ['note'] + +class ActivityForm(forms.ModelForm): + class Meta: + model = Activity + fields = ['activity_type', 'notes'] + + class OpportunityForm(forms.ModelForm): class Meta: model = Opportunity - fields = [ - 'car', 'customer', 'stage', - ] - + fields = ['customer', 'car', 'stage', 'probability', 'closing_date'] \ No newline at end of file diff --git a/inventory/migrations/0012_opportunity_probability.py b/inventory/migrations/0012_opportunity_probability.py new file mode 100644 index 00000000..b30a12ac --- /dev/null +++ b/inventory/migrations/0012_opportunity_probability.py @@ -0,0 +1,20 @@ +# 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 new file mode 100644 index 00000000..b9ac98ba --- /dev/null +++ b/inventory/migrations/0013_lead_phone_number.py @@ -0,0 +1,20 @@ +# 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 new file mode 100644 index 00000000..b25d5a2b --- /dev/null +++ b/inventory/migrations/0014_alter_activity_created_by_alter_notes_created_by.py @@ -0,0 +1,26 @@ +# 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 new file mode 100644 index 00000000..33f6c4b2 --- /dev/null +++ b/inventory/migrations/0015_lead_city.py @@ -0,0 +1,19 @@ +# 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 new file mode 100644 index 00000000..62d58179 --- /dev/null +++ b/inventory/migrations/0016_lead_address.py @@ -0,0 +1,18 @@ +# 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 new file mode 100644 index 00000000..1e7aaf8f --- /dev/null +++ b/inventory/migrations/0017_alter_lead_assigned.py @@ -0,0 +1,19 @@ +# 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 new file mode 100644 index 00000000..4ddbaa54 --- /dev/null +++ b/inventory/migrations/0018_alter_lead_priority.py @@ -0,0 +1,18 @@ +# 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 new file mode 100644 index 00000000..572ebea5 --- /dev/null +++ b/inventory/migrations/0019_opportunity_closed_opportunity_closing_date_and_more.py @@ -0,0 +1,45 @@ +# 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 80a2b891..c6a8330e 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -376,7 +376,10 @@ class CarFinance(models.Model): if vat: return (self.total_discount * Decimal(vat.rate)).quantize(Decimal('0.01')) return Decimal('0.00') - + + @property + def revenue(self): + return self.selling_price-self.cost_price def __str__(self): @@ -731,9 +734,7 @@ class Channel(models.TextChoices): class Status(models.TextChoices): NEW = "new", _("New") PENDING = "pending", _("Pending") - ASSIGNED = "assigned", _("Assigned") IN_PROGRESS = "in_progress", _("In Progress") - CONTACTED = "contacted", _("Contacted") QUALIFIED = "qualified", _("Qualified") CANCELED = "canceled", _("Canceled") @@ -783,6 +784,7 @@ class Lead(models.Model): 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(unique=True, verbose_name=_("Email"), db_index=True) + phone_number = PhoneNumberField(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")) id_car_make = models.ForeignKey(CarMake, on_delete=models.DO_NOTHING, blank=True, null=True, verbose_name=_("Make")) @@ -790,10 +792,12 @@ class Lead(models.Model): year = models.PositiveSmallIntegerField(verbose_name=_("Year"), blank=True, null=True) source = models.CharField(max_length=50, choices=Sources.choices, verbose_name=_("Source")) channel = models.CharField(max_length=50, choices=Channel.choices, verbose_name=_("Channel")) - assigned = models.ForeignKey(Staff, on_delete=models.SET_NULL, null=True, related_name="assigned", verbose_name=_("Assigned")) - priority = models.CharField(max_length=10, choices=Priority.choices, default=Priority.LOW, + address = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Address")) + city = models.CharField(max_length=50, verbose_name=_("City")) + assigned = models.ForeignKey(Staff, on_delete=models.SET_NULL, blank=True, null=True, related_name="assigned", verbose_name=_("Assigned")) + priority = models.CharField(max_length=10, choices=Priority.choices, default=Priority.MEDIUM, verbose_name=_("Priority")) - status = models.CharField(max_length=50, choices=Status.choices, verbose_name=_("Status"), db_index=True) + status = models.CharField(max_length=50, choices=Status.choices, verbose_name=_("Status"), db_index=True, default=Status.NEW) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"), db_index=True) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) @@ -852,15 +856,23 @@ class Customer(models.Model): def get_full_name(self): return f"{self.first_name} {self.middle_name} {self.last_name}" +def validate_probability(value): + if value < 0 or value > 100: + raise ValidationError(_("Probability must be between 0 and 100.")) + class Opportunity(models.Model): dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="opportunities") customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name="opportunities") car = models.ForeignKey(Car, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Car")) stage = models.CharField(max_length=20, choices=Stage.choices, verbose_name=_("Stage")) + status = models.CharField(max_length=20, choices=Status.choices, verbose_name=_("Status"), default=Status.NEW) staff = models.ForeignKey(Staff, on_delete=models.SET_NULL, null=True, related_name="owner", verbose_name=_("Owner")) + probability = models.PositiveIntegerField(validators=[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")) class Meta: verbose_name = _("Opportunity") @@ -875,7 +887,7 @@ class Notes(models.Model): object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') note = models.TextField(verbose_name=_("Note")) - created_by = models.ForeignKey(Staff, on_delete=models.DO_NOTHING, related_name="notes_created") + created_by = models.ForeignKey(User, on_delete=models.DO_NOTHING, related_name="notes_created") created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) @@ -884,7 +896,7 @@ class Notes(models.Model): verbose_name_plural = _("Notes") def __str__(self): - return f"Note by {self.created_by.name} on {self.content_object}" + return f"Note by {self.created_by.first_name} on {self.content_object}" class Activity(models.Model): @@ -893,7 +905,7 @@ class Activity(models.Model): content_object = GenericForeignKey('content_type', 'object_id') activity_type = models.CharField(max_length=50, choices=ActionChoices.choices, verbose_name=_("Activity Type")) notes = models.TextField(blank=True, null=True, verbose_name=_("Notes")) - created_by = models.ForeignKey(Staff, on_delete=models.DO_NOTHING, related_name="activities_created") + created_by = models.ForeignKey(User, on_delete=models.DO_NOTHING, related_name="activities_created") created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) diff --git a/inventory/signals.py b/inventory/signals.py index 7c6805cc..0f7aafa3 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -432,8 +432,8 @@ def update_item_model_cost(sender, instance, created, **kwargs): def notify_staff_on_deal_stage_change(sender, instance, **kwargs): if instance.pk: previous = models.Opportunity.objects.get(pk=instance.pk) - if previous.stage != instance.deal_status: - message = f"Deal '{instance.deal_name}' status changed from {previous.stage} to {instance.stage}." + if previous.stage != instance.stage: + message = f"Opportunity '{instance.pk}' status changed from {previous.stage} to {instance.stage}." models.Notification.objects.create( staff=instance.created_by, message=message ) @@ -493,3 +493,28 @@ def create_item_service(sender, instance, created, **kwargs): ) instance.item = service_model instance.save() + + +@receiver(post_save, sender=models.Lead) +def track_lead_status_change(sender, instance, **kwargs): + if instance.pk: # Ensure the instance is being updated, not created + try: + old_lead = models.Lead.objects.get(pk=instance.pk) + if old_lead.status != instance.status: # Check if status has changed + models.LeadStatusHistory.objects.create( + lead=instance, + old_status=old_lead.status, + new_status=instance.status, + changed_by=instance.assigned # Assuming the assigned staff made the change + ) + except models.Lead.DoesNotExist: + pass # Ignore if the lead doesn't exist (e.g., during initial creation) + + +@receiver(post_save, sender=models.Lead) +def notify_assigned_staff(sender, instance, created, **kwargs): + if instance.assigned: # Check if the lead is assigned + models.Notification.objects.create( + user=instance.assigned.user, + message=f"You have been assigned a new lead: {instance.first_name} {instance.last_name}." + ) \ No newline at end of file diff --git a/inventory/urls.py b/inventory/urls.py index 05d0cb03..0ae7a21c 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -40,11 +40,17 @@ urlpatterns = [ path('customers/create/', views.CustomerCreateView.as_view(), name='customer_create'), path('customers//update/', views.CustomerUpdateView.as_view(), name='customer_update'), path('customers//delete/', views.delete_customer, name='customer_delete'), - path('customers//create_lead/', views.create_lead, name='create_lead'), path('customers//opportunities/create/', views.OpportunityCreateView.as_view(), name='create_opportunity'), - + path('customers//add-note/', views.add_note_to_customer, name='add_note_to_customer'), path('crm/leads/', views.LeadListView.as_view(), name='lead_list'), + path('crm/leads//view/', views.LeadDetailView.as_view(), name='lead_detail'), + path('crm/leads/create/', views.LeadCreateView.as_view(), name='lead_create'), + path('crm/leads//update/', views.LeadUpdateView.as_view(), name='lead_update'), + path('crm/leads//delete/', views.LeadDeleteView.as_view(), name='lead_delete'), + path('crm/leads//add-note/', views.add_note_to_lead, name='add_note'), + path('crm/leads//add-activity/', views.add_activity_to_lead, name='add_activity'), + path('crm/opportunities/create/', views.OpportunityCreateView.as_view(), name='opportunity_create'), path('crm/opportunities//', views.OpportunityDetailView.as_view(), name='opportunity_detail'), path('crm/opportunities//edit/', views.OpportunityUpdateView.as_view(), name='update_opportunity'), path('crm/opportunities/', views.OpportunityListView.as_view(), name='opportunity_list'), diff --git a/inventory/views.py b/inventory/views.py index 533d1a81..d0f60a61 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -207,8 +207,8 @@ class AccountingDashboard(LoginRequiredMixin, TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - - total_cars = models.Car.objects.filter(dealer=self.request.user.dealer).count() + dealer = get_user_type(self.request) + total_cars = models.Car.objects.filter(dealer=dealer).count() total_reservations = models.CarReservation.objects.filter( reserved_until__gte=timezone.now() ).count() @@ -220,7 +220,7 @@ class AccountingDashboard(LoginRequiredMixin, TemplateView): total_selling_price = stats["total_selling_price"] or 0 total_profit = total_selling_price - total_cost_price - context["dealer"] = self.request.user.dealer + context["dealer"] = dealer context["total_cars"] = total_cars context["total_reservations"] = total_reservations context["total_cost_price"] = total_cost_price @@ -558,7 +558,7 @@ class CarFinanceCreateView(LoginRequiredMixin, CreateView): def get_form(self, form_class=None): form = super().get_form(form_class) - dealer = get_user_type(self.request.user.dealer) + dealer = get_user_type(self.request) form.fields[ "additional_finances" ].queryset = models.AdditionalServices.objects.filter(dealer=dealer) @@ -590,14 +590,14 @@ class CarFinanceUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): def get_initial(self): initial = super().get_initial() instance = self.get_object() - dealer = get_user_type(self.request.user.dealer) + dealer = get_user_type(self.request) selected_items = instance.additional_services.filter(dealer=dealer) initial["additional_finances"] = selected_items return initial def get_form(self, form_class=None): form = super().get_form(form_class) - dealer = get_user_type(self.request.user.dealer) + dealer = get_user_type(self.request) form.fields[ "additional_finances" ].queryset = models.AdditionalServices.objects.filter(dealer=dealer) @@ -634,7 +634,8 @@ class CarLocationCreateView(CreateView): def form_valid(self, form): form.instance.car = get_object_or_404(models.Car, pk=self.kwargs["car_pk"]) - form.instance.owner = self.request.user.dealer + dealer = get_user_type(self.request) + form.instance.owner = dealer form.save() messages.success(self.request, "Car saved successfully.") return super().form_valid(form) @@ -784,14 +785,48 @@ class CustomerDetailView(LoginRequiredMixin, DetailView): context_object_name = "customer" def get_context_data(self, **kwargs): + dealer = get_user_type(self.request) + entity = dealer.entity context = super().get_context_data(**kwargs) name = f"{context['customer'].first_name} {context['customer'].middle_name} {context['customer'].last_name}" - context["estimates"] = self.request.entity.get_estimates().filter( + context["estimates"] = entity.get_estimates().filter( customer__customer_name=name ) + context['notes'] = models.Notes.objects.filter(content_type__model='customer', object_id=self.object.id) + context['activities'] = models.Activity.objects.filter(content_type__model='customer', object_id=self.object.id) return context +def add_note_to_customer(request, pk): + customer = get_object_or_404(models.Customer, pk=pk) + if request.method == 'POST': + form = forms.NoteForm(request.POST) + if form.is_valid(): + note = form.save(commit=False) + note.content_object = customer + + note.created_by = request.user + note.save() + return redirect('customer_detail', pk=pk) + else: + form = forms.NoteForm() + return render(request, 'crm/add_note.html', {'form': form, 'customer': customer}) + +def add_activity_to_customer(request, pk): + customer = get_object_or_404(models.Customer, pk=pk) + if request.method == 'POST': + form = forms.ActivityForm(request.POST) + if form.is_valid(): + activity = form.save(commit=False) + activity.content_object = customer + activity.created_by = request.user + activity.save() + return redirect('customer_detail', pk=pk) + else: + form = forms.ActivityForm() + return render(request, 'crm/add_activity.html', {'form': form, 'customer': customer}) + + class CustomerCreateView( LoginRequiredMixin, SuccessMessageMixin, @@ -852,7 +887,7 @@ class VendorCreateView( success_message = _("Vendor created successfully.") def form_valid(self, form): - form.instance.dealer = self.request.user.dealer + form.instance.dealer = get_user_type(self.request) return super().form_valid(form) @@ -882,8 +917,7 @@ class QuotationCreateView(LoginRequiredMixin, CreateView): template_name = "sales/quotation_form.html" def form_valid(self, form): - dealer = self.request.user.dealer - form.instance.dealer = dealer + form.instance.dealer = get_user_type(self.request) quotation = form.save() selected_cars = form.cleaned_data.get("cars") for car in selected_cars: @@ -906,7 +940,8 @@ class QuotationListView(LoginRequiredMixin, ListView): def get_queryset(self): status = self.request.GET.get("status") - queryset = self.request.user.dealer.sales.all() + dealer = get_user_type(self.request) + queryset = dealer.sales.all() if status: queryset = queryset.filter(status=status) return queryset @@ -1092,7 +1127,7 @@ def generate_invoice(request, pk): @login_required def post_quotation(request, pk): qoutation = get_object_or_404(models.SaleQuotation, pk=pk) - dealer = request.user.dealer + dealer = get_user_type(request) entity = dealer.entity if qoutation.posted: messages.error(request, "Quotation is already posted") @@ -1260,6 +1295,11 @@ class UserListView(LoginRequiredMixin, ListView): paginate_by = 10 template_name = "users/user_list.html" + def get_queryset(self): + dealer = get_user_type(self.request) + staff = models.Staff.objects.filter(dealer=dealer).all() + return staff + class UserDetailView(LoginRequiredMixin, DetailView): model = models.Staff @@ -1339,6 +1379,11 @@ class OrganizationListView(LoginRequiredMixin, ListView): context_object_name = "organizations" paginate_by = 10 + def get_queryset(self): + dealer = get_user_type(self.request) + data = models.Organization.objects.filter(dealer=dealer).all() + return data + class OrganizationDetailView(DetailView): model = models.Organization @@ -1382,6 +1427,10 @@ class RepresentativeListView(LoginRequiredMixin, ListView): template_name = "representatives/representative_list.html" context_object_name = "representatives" + def get_queryset(self): + dealer = get_user_type(self.request) + data = models.Representative.objects.filter(dealer=dealer).all() + return data class RepresentativeDetailView(DetailView): model = models.Representative @@ -1604,8 +1653,9 @@ class BankAccountListView(LoginRequiredMixin, ListView): context_object_name = "bank_accounts" def get_queryset(self): + dealer = get_user_type(self.request) return BankAccountModel.objects.filter( - entity_model=self.request.user.dealer.entity + entity_model=dealer.entity ) @@ -2308,58 +2358,99 @@ class UserActivityLogListView(ListView): # CRM RELATED VIEWS -def create_lead(request, pk): - customer = get_object_or_404(models.Customer, pk=pk) - if customer.is_lead: - messages.warning(request, _("Customer is already a lead.")) - else: - customer.is_lead = True - customer.save() - messages.success(request, _("Customer successfully marked as a lead.")) - return redirect(reverse("customer_detail", kwargs={"pk": customer.pk})) - - class LeadListView(ListView): - model = models.Customer - template_name = "crm/lead_list.html" - context_object_name = "customers" + model = models.Lead + template_name = 'crm/leads/lead_list.html' + context_object_name = 'leads' + paginate_by = 10 def get_queryset(self): - query = self.request.GET.get("q") dealer = get_user_type(self.request) + leads = models.Lead.objects.filter(dealer=dealer).all() + return leads - customers = models.Customer.objects.filter(dealer=dealer, is_lead=True) - - if query: - customers = customers.filter( - Q(national_id__icontains=query) - | Q(first_name__icontains=query) - | Q(last_name__icontains=query) - ) - return customers +class LeadDetailView(DetailView): + model = models.Lead + template_name = 'crm/leads/lead_detail.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["query"] = self.request.GET.get("q", "") + context['notes'] = models.Notes.objects.filter(content_type__model='lead', object_id=self.object.id) + context['activities'] = models.Activity.objects.filter(content_type__model='lead', object_id=self.object.id) + context['status_history'] = models.LeadStatusHistory.objects.filter(lead=self.object) return context +class LeadCreateView(CreateView): + model = models.Lead + 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): + dealer = get_user_type(self.request) + form.instance.dealer = dealer + return super().form_valid(form) + + +class LeadUpdateView(UpdateView): + model = models.Lead + form_class = forms.LeadForm + template_name = 'crm/leads/lead_form.html' + success_url = reverse_lazy('lead_list') + +class LeadDeleteView(DeleteView): + model = models.Lead + template_name = 'crm/leads/lead_confirm_delete.html' + success_url = reverse_lazy('lead_list') + + +def add_note_to_lead(request, pk): + lead = get_object_or_404(models.Lead, pk=pk) + if request.method == 'POST': + form = forms.NoteForm(request.POST) + if form.is_valid(): + note = form.save(commit=False) + note.content_object = lead + + note.created_by = request.user + note.save() + return redirect('lead_detail', pk=pk) + else: + form = forms.NoteForm() + return render(request, 'crm/add_note.html', {'form': form, 'lead': lead}) + +def add_activity_to_lead(request, pk): + lead = get_object_or_404(models.Lead, pk=pk) + if request.method == 'POST': + form = forms.ActivityForm(request.POST) + if form.is_valid(): + activity = form.save(commit=False) + activity.content_object = lead + activity.created_by = request.user + activity.save() + return redirect('lead_detail', pk=pk) + else: + form = forms.ActivityForm() + return render(request, 'crm/add_activity.html', {'form': form, 'lead': lead}) + class OpportunityCreateView(CreateView): model = models.Opportunity form_class = forms.OpportunityForm - template_name = "crm/opportunity_form.html" + template_name = "crm/opportunities/opportunity_form.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["customer"] = models.Customer.objects.get(pk=self.kwargs["customer_id"]) - context["cars"] = models.Car.objects.all() + dealer = get_user_type(self.request) + context["customer"] = models.Customer.objects.filter(dealer=dealer) + context["cars"] = models.Car.objects.filter(dealer=dealer) return context def form_valid(self, form): - form.instance.customer = models.Customer.objects.get( - pk=self.kwargs["customer_id"] - ) - form.instance.created_by = self.request.user.staff + dealer = get_user_type(self.request) + form.instance.dealer = dealer + form.instance.staff = dealer.staff return super().form_valid(form) def get_success_url(self): @@ -2369,7 +2460,7 @@ class OpportunityCreateView(CreateView): class OpportunityUpdateView(UpdateView): model = models.Opportunity form_class = forms.OpportunityForm - template_name = "crm/opportunity_form.html" + template_name = "crm/opportunities/opportunity_form.html" def get_success_url(self): return reverse_lazy("opportunity_detail", kwargs={"pk": self.object.pk}) @@ -2377,15 +2468,20 @@ class OpportunityUpdateView(UpdateView): class OpportunityDetailView(DetailView): model = models.Opportunity - template_name = "crm/opportunity_detail.html" + template_name = "crm/opportunities/opportunity_detail.html" context_object_name = "opportunity" class OpportunityListView(ListView): model = models.Opportunity - template_name = "crm/opportunity_list.html" + template_name = "crm/opportunities/opportunity_list.html" context_object_name = "opportunities" + def get_queryset(self): + dealer = get_user_type(self.request) + data = models.Opportunity.objects.filter(dealer=dealer).all() + return data + @login_required def delete_opportunity(request, pk): diff --git a/templates/account/2FA.html b/templates/account/2FA.html index 517707ed..e05e6297 100644 --- a/templates/account/2FA.html +++ b/templates/account/2FA.html @@ -2,7 +2,7 @@ {% load i18n static %} {% block content %} -
+
phoenix diff --git a/templates/account/confirm_email_verification_code.html b/templates/account/confirm_email_verification_code.html index d99adc58..1244e348 100644 --- a/templates/account/confirm_email_verification_code.html +++ b/templates/account/confirm_email_verification_code.html @@ -3,7 +3,7 @@ {% block title %}Confirm Email Verification Code{% endblock %} {% block content %} -
+

Confirm Your Email

Please enter the verification code sent to your email.

diff --git a/templates/account/confirm_login_code..html b/templates/account/confirm_login_code..html index 9cc246e4..c296ba7a 100644 --- a/templates/account/confirm_login_code..html +++ b/templates/account/confirm_login_code..html @@ -3,7 +3,7 @@ {% block title %}Confirm Login Code{% endblock %} {% block content %} -
+

Confirm Login Code

Please enter the login code sent to your email or phone.

diff --git a/templates/account/email_confirm.html b/templates/account/email_confirm.html index c662cd44..182ab9a9 100644 --- a/templates/account/email_confirm.html +++ b/templates/account/email_confirm.html @@ -3,7 +3,7 @@ {% block title %}Email Confirmation{% endblock %} {% block content %} -
+

Email Confirmation

Your email has been successfully confirmed.

Go to Login diff --git a/templates/account/lock-screen.html b/templates/account/lock-screen.html index aae47a86..00c873ef 100644 --- a/templates/account/lock-screen.html +++ b/templates/account/lock-screen.html @@ -63,7 +63,7 @@
-
+
@@ -91,8 +91,8 @@ navbarVertical.setAttribute('data-navbar-appearance', 'darker'); } -
-
+
+
Demo widget
diff --git a/templates/account/login.html b/templates/account/login.html index fc48a2fd..48361558 100644 --- a/templates/account/login.html +++ b/templates/account/login.html @@ -6,7 +6,7 @@ {% block title %}{{ _("Sign In") }}{% endblock title %} {% block content %} -
+
diff --git a/templates/account/logout.html b/templates/account/logout.html index 8d70e023..cf289974 100644 --- a/templates/account/logout.html +++ b/templates/account/logout.html @@ -3,7 +3,7 @@ {% block title %}{{ _("Sign Out") }}{% endblock title %} {% block content %} -
+
diff --git a/templates/account/password_change.html b/templates/account/password_change.html index 8b344bbf..f19f2bcc 100644 --- a/templates/account/password_change.html +++ b/templates/account/password_change.html @@ -3,7 +3,7 @@ {% block title %}{{ _("Change Password") }}{% endblock title %} {% block content %} -
+
diff --git a/templates/account/password_reset.html b/templates/account/password_reset.html index e510467f..a84a25fc 100644 --- a/templates/account/password_reset.html +++ b/templates/account/password_reset.html @@ -4,7 +4,7 @@ {% block title %}{{ _("Password Reset") }}{% endblock title %} {% block content %} -
+
diff --git a/templates/account/request_login_code.html b/templates/account/request_login_code.html index 3c27d170..93ee3c13 100644 --- a/templates/account/request_login_code.html +++ b/templates/account/request_login_code.html @@ -3,7 +3,7 @@ {% block title %}Request Login Code{% endblock %} {% block content %} -
+

Request a Login Code

Enter your email address to receive a login code.

diff --git a/templates/account/signup-wizard.html b/templates/account/signup-wizard.html index 66399b65..ee4cc980 100644 --- a/templates/account/signup-wizard.html +++ b/templates/account/signup-wizard.html @@ -4,7 +4,7 @@ {% block content %} -
+
diff --git a/templates/account/signup.html b/templates/account/signup.html index 0966a9c0..d1a8ad2c 100644 --- a/templates/account/signup.html +++ b/templates/account/signup.html @@ -5,7 +5,7 @@ {% block title %}{{ _("Sign Up") }}{% endblock title %} {% block content %} -
+
diff --git a/templates/auth_base.html b/templates/auth_base.html index 284d9b76..9c63dab3 100644 --- a/templates/auth_base.html +++ b/templates/auth_base.html @@ -85,31 +85,31 @@ +{% endblock %} \ No newline at end of file diff --git a/templates/crm/leads/lead_list.html b/templates/crm/leads/lead_list.html new file mode 100644 index 00000000..b6bce936 --- /dev/null +++ b/templates/crm/leads/lead_list.html @@ -0,0 +1,200 @@ +{% extends 'base.html' %} +{% load i18n static %} +{% block title %}{{ _('Leads')|capfirst }}{% endblock title %} + +{% block content %} +
+

{{ _("Leads")|capfirst }}

+
+ +
+
+ +
+
+
+
+
+ {% if page_obj.object_list %} +
+ + + + + + + + + + + + + + + {% for lead in leads %} + + + + + + + + + + + + + {% endfor %} + + {% endif %} +
{{ _("Status")|capfirst }} +
+
+ {{ _("Name")|capfirst }} +
+ +
+
+
+ {{ _("email")|capfirst }} +
+
+
+
+ {{ _("Phone Number") }} +
+
+
+
+ {{ _("Source")|capfirst }} +
+
+
+
+ {{ _("Channel")|capfirst }} +
+
+ {{ _("Create date") }} +
+
+ {% if lead.status == "new" %} + {{_("New")}} + {% elif lead.status == "pending" %} + {{_("Pending")}} + {% elif lead.status == "in_progress" %} + {{_("In Progress")}} + {% elif lead.status == "qualified" %} + {{_("Qualified")}} + {% elif lead.status == "canceled" %} + {{_("Canceled")}} + {% endif %} +
+
+ + {{ lead.email }}{{ lead.phone_number }}{{ lead.source|upper }}{{ lead.channel|upper }}{{ lead.created|date }} +
+ + +
+
+
+
+
+
+
+ + +{% endblock %} diff --git a/templates/crm/opportunity_confirm_delete.html b/templates/crm/opportunities/opportunity_confirm_delete.html similarity index 100% rename from templates/crm/opportunity_confirm_delete.html rename to templates/crm/opportunities/opportunity_confirm_delete.html diff --git a/templates/crm/opportunity_detail.html b/templates/crm/opportunities/opportunity_detail.html similarity index 97% rename from templates/crm/opportunity_detail.html rename to templates/crm/opportunities/opportunity_detail.html index 42f16529..6b8767fb 100644 --- a/templates/crm/opportunity_detail.html +++ b/templates/crm/opportunities/opportunity_detail.html @@ -4,22 +4,21 @@ {% block content %} -
-
+
-

Deal details

+

O{{ _("pportunity details")}}

@@ -31,30 +30,21 @@
-

Start-Up Growth Suite

+

{{ opportunity.car.id_car_make.get_local_name }} - {{ opportunity.car.id_car_model.get_local_name }} - {{ opportunity.car.year }}

-
USD $12,000.00
-
Financial
+
{{ opportunity.car.finances.total }} {{ _("SAR") }}
-
+
-
Ansolo Lazinatov
+
{{ opportunity.staff.get_local_name}}
@@ -152,8 +142,8 @@
-

Deal Amount

-

$12,000.00

+

{{ _("Amount") }}

+

{{ opportunity.car.finances.total }}

@@ -161,7 +151,7 @@
-

Deal Code

+

Code

PHO1234

@@ -170,7 +160,7 @@
-

Deal Type

+

Type

New Business

@@ -196,19 +186,19 @@ : -

12.5

+

{{ opportunity.probability }}

-

Revenue

+

{{ _("Revenue") }}

: -

$1,500.00

+

{{ opportunity.car.finances.revenue }}

@@ -224,21 +214,21 @@
-

Phone

+

{{ _("Phone Number") }}

: - +11 123 456 789 + {{ opportunity.customer.phone_number }}
-

Email

+

{{ _("Email") }}

: - jacksonpol@email.com + {{ opportunity.customer.email}}
@@ -253,24 +243,24 @@
-

Contact Name

+

{{ _("Contact Name")}}

: -
Jackson Pollock
+
{{ opportunity.customer.get_full_name}}
-

Modified By

+

{{ _("Staff") }}

: -
Ansolo Lazinatov
+
{{ opportunity.staff.get_local_name}}
@@ -286,24 +276,24 @@
-

Create Date

+

{{ _("Create Date")}}

: -
Nov 30, 2022
+
{{ opportunity.created|date}}
-

Closing Date

+

{{ _("Closing Date")}}

: -
Dec 15, 2022
+
{{ opportunity.closing_date|date}}
@@ -1424,17 +1414,4 @@
-
-

{{ opportunity.deal_name }}

-

Customer: {{ opportunity.customer.get_full_name }}

-

Car: {{ opportunity.car }}

-

Deal Value: {{ opportunity.deal_value }}

-

Deal Status: {{ opportunity.get_deal_status_display }}

-

Priority: {{ opportunity.get_priority_display }}

-

Source: {{ opportunity.get_source_display }}

-

Created By: {{ opportunity.created_by.name }}

-

Created At: {{ opportunity.created_at }}

-

Updated At: {{ opportunity.updated_at }}

-Edit -
{% endblock %} \ No newline at end of file diff --git a/templates/crm/opportunities/opportunity_form.html b/templates/crm/opportunities/opportunity_form.html new file mode 100644 index 00000000..ff97f447 --- /dev/null +++ b/templates/crm/opportunities/opportunity_form.html @@ -0,0 +1,82 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} + +
+
+

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

+
+
+
+ {% csrf_token %} + + +
+
+ + +
+ {{ form.customer.errors }} +
+ + +
+
+ + +
+ {{ form.car.errors }} +
+ + +
+
+ + +
+ {{ form.stage.errors }} +
+ + +
+
+ + +
+ {{ form.probability.errors }} +
+ + +
+
+ + + {{ form.closing_date.errors }} +
+ {{ form.closing_date.errors }} +
+ + +
+
+ + {{ _("Cancel") }} +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/crm/opportunity_list.html b/templates/crm/opportunities/opportunity_list.html similarity index 82% rename from templates/crm/opportunity_list.html rename to templates/crm/opportunities/opportunity_list.html index c205e602..0825ad44 100644 --- a/templates/crm/opportunity_list.html +++ b/templates/crm/opportunities/opportunity_list.html @@ -2,30 +2,26 @@ {% load i18n static %} {% block content %} -
-
-
+

{{ _("Opportunities") }}

- + {{ _("Add Opportunity") }}
+
{% for opportunity in opportunities %} -
+
-
-

{{ _("Revenue") }}:

-

{{ opportunity.car.finances.total }}

+
{{ opportunity.car.id_car_make.get_local_name }} - {{ opportunity.car.id_car_model.get_local_name }} - {{ opportunity.car.year }}
-
-
+
-
+
+
-
-

{{ opportunity.created_at|date }} . {{ opportunity.created_at|time }}

+
+ +
+
+ +

{{ opportunity.created|date }} . {{ opportunity.created|time}}

-
{{ opportunity.deal_name }} -

{{ opportunity.get_source_display }}

+
+ {{ _("View") }} +

{{ opportunity.get_stage_display }}

{{ opportunity.car.finances.total }}

@@ -52,76 +61,69 @@

{{ opportunity.customer.get_full_name }}

-

{{ opportunity.created_by.name }}

+

{{ opportunity.staff.name }}

-
{{ opportunity.get_deal_status_display }}{{ opportunity.get_priority_display }}
+
{{ opportunity.get_stage_display }}{{ opportunity.get_status_display }}
- - + + - - - - - - - - - - - +
{{ _("Details") }}:
+

{{ _("Expected Revenue")}}

: +

{{ opportunity.car.finances.total }}

+
-

{{ _("Name") }}

+

{{ _("Contact") }}

: -

{{ opportunity.customer.first_name }}

+
+

+
-

{{ _("Closing Date and Time")}}

+

{{ _("Closing Date")}}

: -

{{ opportunity.created_at }}

-
-
-

Assigned Agent

-
-
: - + +

{{ opportunity.closing_date }}

-

{{ _("Probability") }}:

-
-
+

{{ _("Probability") }}: %

+
+ {% if opportunity.probability >= 25 and opportunity.probability < 49 %} +
+ {{ opportunity.probability }} +
+ {% elif opportunity.probability >= 50 and opportunity.probability <= 74 %} +
+ {{ opportunity.probability }} +
+ {% elif opportunity.probability >= 75 and opportunity.probability <= 100 %} +
+ {{ opportunity.probability }} +
+ {% endif %}
+
@@ -135,13 +137,13 @@ tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true"> - -