diff --git a/api/migrations/__pycache__/0001_initial.cpython-311.pyc b/api/migrations/__pycache__/0001_initial.cpython-311.pyc index c2ffaec9..88edabc7 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/inventory/__pycache__/forms.cpython-311.pyc b/inventory/__pycache__/forms.cpython-311.pyc index 28c7d4ee..a774c93a 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 63b3c4b8..7cc30553 100644 Binary files a/inventory/__pycache__/models.cpython-311.pyc and b/inventory/__pycache__/models.cpython-311.pyc differ diff --git a/inventory/__pycache__/urls.cpython-311.pyc b/inventory/__pycache__/urls.cpython-311.pyc index 45965654..7df5f846 100644 Binary files a/inventory/__pycache__/urls.cpython-311.pyc and b/inventory/__pycache__/urls.cpython-311.pyc differ diff --git a/inventory/__pycache__/views.cpython-311.pyc b/inventory/__pycache__/views.cpython-311.pyc index 31ea8470..4a3ef42f 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 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"> - -