diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 94a25f7f..e7670d6e 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/car_inventory/__pycache__/settings.cpython-311.pyc b/car_inventory/__pycache__/settings.cpython-311.pyc index acef0142..7f3609bd 100644 Binary files a/car_inventory/__pycache__/settings.cpython-311.pyc and b/car_inventory/__pycache__/settings.cpython-311.pyc differ diff --git a/db.sqlite b/db.sqlite index 3a863764..65c69626 100644 Binary files a/db.sqlite and b/db.sqlite differ diff --git a/inventory/__pycache__/forms.cpython-311.pyc b/inventory/__pycache__/forms.cpython-311.pyc index ab2fc768..0f318649 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 b3a397af..6ab6cbe0 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 feff8b2f..bde268ca 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 8f304fb1..28159d72 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 b0a1af6a..5b3cce5e 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -25,7 +25,7 @@ from .models import ( SaleQuotationCar, AdditionalServices, Staff, - Opportunity, DealStatus, Priority, DealSource, + Opportunity, DealStatus, Priority, Sources, ) from django_ledger.models import ItemModel, InvoiceModel from django.forms import ModelMultipleChoiceField, ValidationError @@ -560,11 +560,6 @@ class OpportunityForm(forms.ModelForm): class Meta: model = Opportunity fields = [ - 'car', 'deal_name', 'deal_value', 'deal_status', - 'priority', 'source' + 'car', 'deal_name', 'deal_value', ] - widgets = { - 'deal_status': forms.Select(choices=DealStatus.choices), - 'priority': forms.Select(choices=Priority.choices), - 'source': forms.Select(choices=DealSource.choices), - } + diff --git a/inventory/migrations/0002_alter_subscriptionplan_options_and_more.py b/inventory/migrations/0002_alter_subscriptionplan_options_and_more.py deleted file mode 100644 index 56ecc382..00000000 --- a/inventory/migrations/0002_alter_subscriptionplan_options_and_more.py +++ /dev/null @@ -1,101 +0,0 @@ -# Generated by Django 5.1.4 on 2025-01-02 23:56 - -import django.db.models.deletion -import django.utils.timezone -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0001_initial'), - ] - - operations = [ - migrations.AlterModelOptions( - name='subscriptionplan', - options={'verbose_name': 'Subscription Plan', 'verbose_name_plural': 'Subscription Plans'}, - ), - migrations.RemoveField( - model_name='subscription', - name='max_users', - ), - migrations.AddField( - model_name='opportunity', - name='assigned_at', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Assigned At'), - preserve_default=False, - ), - migrations.AddField( - model_name='opportunity', - name='assigned_to', - field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='deals_assigned', to='inventory.staff'), - preserve_default=False, - ), - migrations.AddField( - model_name='subscription', - name='billing_cycle', - field=models.CharField(choices=[('monthly', 'Monthly'), ('annual', 'Annual')], default='monthly', help_text='Billing cycle for the subscription', max_length=10), - ), - migrations.AddField( - model_name='subscription', - name='last_payment_date', - field=models.DateField(blank=True, help_text='Date of the last payment made', null=True), - ), - migrations.AddField( - model_name='subscription', - name='next_payment_date', - field=models.DateField(blank=True, help_text='Date of the next payment due', null=True), - ), - migrations.AddField( - model_name='subscriptionplan', - name='created_at', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), - preserve_default=False, - ), - migrations.AddField( - model_name='subscriptionplan', - name='custom_features', - field=models.JSONField(blank=True, help_text='Additional features specific to this plan', null=True), - ), - migrations.AddField( - model_name='subscriptionplan', - name='max_inventory_size', - field=models.PositiveIntegerField(default=50, help_text='Maximum number of cars in inventory'), - ), - migrations.AddField( - model_name='subscriptionplan', - name='support_level', - field=models.CharField(choices=[('basic', 'Basic Support'), ('priority', 'Priority Support'), ('dedicated', 'Dedicated Support')], default='basic', help_text='Level of support provided', max_length=50), - ), - migrations.AddField( - model_name='subscriptionplan', - name='updated_at', - field=models.DateTimeField(auto_now=True), - ), - migrations.AlterField( - model_name='subscription', - name='end_date', - field=models.DateField(help_text='Date when the subscription ends'), - ), - migrations.AlterField( - model_name='subscription', - name='plan', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subscriptions', to='inventory.subscriptionplan'), - ), - migrations.AlterField( - model_name='subscription', - name='start_date', - field=models.DateField(help_text='Date when the subscription starts'), - ), - migrations.AlterField( - model_name='subscriptionplan', - name='max_users', - field=models.PositiveIntegerField(default=1, help_text='Maximum number of users allowed'), - ), - migrations.AlterField( - model_name='subscriptionplan', - name='name', - field=models.CharField(help_text='Name of the subscription plan', max_length=100, unique=True), - ), - ] diff --git a/inventory/models.py b/inventory/models.py index d69807bd..5a1fae58 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -632,13 +632,6 @@ class Dealer(models.Model, LocalizedNameMixin): # def get_root_dealer(self): # return self.parent_dealer if self.parent_dealer else self - -class StaffTypes(models.TextChoices): - MANAGER = "manager", _("Manager") - INVENTORY = "inventory", _("Inventory") - ACCOUNTANT = "accountant", _("Accountant") - SALES = "sales", _("Sales") - ############################## # Additional staff types for later @@ -650,6 +643,17 @@ class StaffTypes(models.TextChoices): ############################## + +class StaffTypes(models.TextChoices): + MANAGER = "manager", _("Manager") + INVENTORY = "inventory", _("Inventory") + ACCOUNTANT = "accountant", _("Accountant") + SALES = "sales", _("Sales") + COORDINATOR = "coordinator", _("Coordinator") + RECEPTIONIST = "receptionist", _("Receptionist") + AGENT = "agent", _("Agent") + + class Staff(models.Model, LocalizedNameMixin): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="staff") dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="staff") @@ -669,33 +673,89 @@ class Staff(models.Model, LocalizedNameMixin): return f"{self.name} - {self.get_staff_type_display()}" -class Vendor(models.Model, LocalizedNameMixin): - dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="vendors") - crn = models.CharField( - max_length=10, unique=True, verbose_name=_("Commercial Registration Number") - ) - vrn = models.CharField( - max_length=15, unique=True, verbose_name=_("VAT Registration Number") - ) - arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) - name = models.CharField(max_length=255, verbose_name=_("English Name")) - contact_person = models.CharField(max_length=100, verbose_name=_("Contact Person")) - phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number")) - email = models.EmailField(max_length=255, verbose_name=_("Email Address")) - address = models.CharField( - max_length=200, blank=True, null=True, verbose_name=_("Address") - ) - logo = models.ImageField( - upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo") - ) - created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) +class ActionChoices(models.TextChoices): + CREATE = "create", _("Create") + UPDATE = "update", _("Update") + DELETE = "delete", _("Delete") + STATUS_CHANGE = "status_change", _("Status Change") - class Meta: - verbose_name = _("Vendor") - verbose_name_plural = _("Vendors") - def __str__(self): - return self.name +class DealStatus(models.TextChoices): + NEW = "new", _("New") + PENDING = "pending", _("Pending") + CANCELED = "canceled", _("Canceled") + COMPLETED = "completed", _("Completed") + + +class Priority(models.TextChoices): + LOW = "low", _("Low") + MEDIUM = "medium", _("Medium") + HIGH = "high", _("High") + + +class Sources(models.TextChoices): + REFERRALS = "referrals", _("Referrals") + WALK_IN = "walk_in", _("Walk In") + TOLL_FREE = "toll_free", _("Toll Free") + WHATSAPP = "whatsapp", _("WhatsApp") + SHOWROOM = "showroom", _("Showroom") + WEBSITE = "website", _("Website") + TIKTOK = "tiktok", _("TikTok") + INSTAGRAM = "instagram", _("Instagram") + X = "x", _("X") + FACEBOOK = "facebook", _("Facebook") + MOTORY = "motory", _("Motory") + INFLUENCERS = "influencers", _("Influencers") + YOUTUBE = "youtube", _("Youtube") + EMAIL = "email", _("Email") + +class ContactStatus(models.TextChoices): + NEW = "new", _("New") + PENDING = "pending", _("Pending") + ASSIGNED = "assigned", _("Assigned") + CONTACTED = "contacted", _("Contacted") + ACCEPTED = "accepted", _("Accepted") + QUALIFIED = "qualified", _("Qualified") + CANCELED = "canceled", _("Canceled") + + +# class Contact(models.Model): +# AGE_RANGES = ( +# ('18-30', '18 - 30'), +# ('31-40', '31 - 40'), +# ('41-50', '41 - 50'), +# ('51-60', '51 - 60'), +# ('61-70', '61 - 70'), +# ('71-80', '71 - 80'), +# ('81-90', '81 - 90'), +# ) +# +# dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="contacts") +# first_name = models.CharField(max_length=50, verbose_name=_("First Name")) +# last_name = models.CharField(max_length=50, verbose_name=_("Last Name")) +# age = models.CharField(choices=AGE_RANGES, max_length=20, verbose_name=_("Age")) +# gender = models.CharField(choices=[('m', _('Male')), ('f', _('Female'))], max_length=1, verbose_name=_("Gender")) +# phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number")) +# email = models.EmailField(verbose_name=_("Email")) +# id_car_make = models.ForeignKey(CarMake, on_delete=models.DO_NOTHING, verbose_name=_("Make")) +# id_car_model = models.ForeignKey(CarModel, on_delete=models.DO_NOTHING, verbose_name=_("Model")) +# year = models.PositiveSmallIntegerField(verbose_name=_("Year")) +# status = models.CharField(choices=ContactStatus.choices, max_length=255, verbose_name=_("Status"), default=ContactStatus.NEW) +# created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) +# updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) +# enquiry_type = models.CharField(choices=[("quotation", _("Quote")),("testdrive", _("Test drive"))], max_length=50, verbose_name=_("Enquiry Type")) +# purchase_method = models.CharField(choices=[("c", _("Cash")),("f", _("Finance"))], max_length=1, verbose_name=_("Purchase Method")) +# source = models.CharField(max_length=100, choices=Sources.choices, verbose_name=_("Source")) +# salary = models.PositiveIntegerField(verbose_name=_("Salary")) +# obligations = models.PositiveIntegerField(verbose_name=_("Obligations")) +# +# class Meta: +# verbose_name = _("Contact") +# verbose_name_plural = _("Contacts") +# +# def __str__(self): +# return self.first_name + " " + self.last_name + class Customer(models.Model): @@ -733,42 +793,17 @@ class Customer(models.Model): return f"{self.first_name} {self.middle_name} {self.last_name}" -class DealStatus(models.TextChoices): - NEW = "new", _("New") - PENDING = "pending", _("Pending") - CANCELED = "canceled", _("Canceled") - COMPLETED = "completed", _("Completed") - - -class Priority(models.TextChoices): - LOW = "low", _("Low") - MEDIUM = "medium", _("Medium") - HIGH = "high", _("High") - - -class DealSource(models.TextChoices): - REFERRALS = "referrals", _("Referrals") - WALK_IN = "walk_in", _("Walk In") - TOLL_FREE = "toll_free", _("Toll Free") - WHATSAPP = "whatsapp", _("Whatsapp") - SHOWROOMS = "showrooms", _("Showrooms") - WEBSITE = "website", _("Website") - OTHER = "other", _("Other") - - class Opportunity(models.Model): 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")) deal_name = models.CharField(max_length=255, verbose_name=_("Deal Name")) deal_value = models.DecimalField(max_digits=10, decimal_places=2, verbose_name=_("Deal Value")) - deal_status = models.CharField(max_length=50, choices=DealStatus.choices, default=DealStatus.NEW, verbose_name=_("Deal Status")) - priority = models.CharField(max_length=50, choices=Priority.choices, default=Priority.LOW, verbose_name=_("Priority")) - source = models.CharField(max_length=255, choices=DealSource.choices, default=DealSource.SHOWROOMS, verbose_name=_("Source")) + deal_status = models.CharField(max_length=20, choices=DealStatus.choices, default=DealStatus.NEW, verbose_name=_("Deal Status")) + priority = models.CharField(max_length=10, choices=Priority.choices, default=Priority.LOW, verbose_name=_("Priority")) created_by = models.ForeignKey(Staff, on_delete=models.SET_NULL, null=True, related_name="deals_created") created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) - assigned_to = models.ForeignKey(Staff, on_delete=models.CASCADE, related_name="deals_assigned") - assigned_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Assigned At")) + class Meta: verbose_name = _("Opportunity") @@ -781,7 +816,7 @@ class Opportunity(models.Model): class Notes(models.Model): opportunity = models.ForeignKey(Opportunity, on_delete=models.CASCADE, related_name="notes") note = models.TextField(verbose_name=_("Note")) - created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name="notes_created") + created_by = models.ForeignKey(User, on_delete=models.DO_NOTHING, related_name="notes_created") created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) @@ -790,13 +825,6 @@ class Notes(models.Model): verbose_name_plural = _("Notes") -class ActionChoices(models.TextChoices): - CREATE = "create", _("Create") - UPDATE = "update", _("Update") - DELETE = "delete", _("Delete") - STATUS_CHANGE = "status_change", _("Status Change") - - class OpportunityLog(models.Model): opportunity = models.ForeignKey(Opportunity, on_delete=models.CASCADE, related_name="logs") action = models.CharField(max_length=50, choices=ActionChoices.choices, verbose_name=_("Action")) @@ -830,6 +858,35 @@ class Notification(models.Model): return self.message +class Vendor(models.Model, LocalizedNameMixin): + dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="vendors") + crn = models.CharField( + max_length=10, unique=True, verbose_name=_("Commercial Registration Number") + ) + vrn = models.CharField( + max_length=15, unique=True, verbose_name=_("VAT Registration Number") + ) + arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) + name = models.CharField(max_length=255, verbose_name=_("English Name")) + contact_person = models.CharField(max_length=100, verbose_name=_("Contact Person")) + phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number")) + email = models.EmailField(max_length=255, verbose_name=_("Email Address")) + address = models.CharField( + max_length=200, blank=True, null=True, verbose_name=_("Address") + ) + logo = models.ImageField( + upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo") + ) + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) + + class Meta: + verbose_name = _("Vendor") + verbose_name_plural = _("Vendors") + + def __str__(self): + return self.name + + class Organization(models.Model, LocalizedNameMixin): dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='organizations') name = models.CharField(max_length=255, verbose_name=_("Name")) diff --git a/inventory/urls.py b/inventory/urls.py index b87a0c53..ef31d36b 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -34,7 +34,7 @@ urlpatterns = [ path('dealers/activity/', views.UserActivityLogListView.as_view(), name='dealer_activity'), # path('dealers//delete/', views.DealerDeleteView.as_view(), name='dealer_delete'), - # Customer URLs + # CRM URLs path('customers/', views.CustomerListView.as_view(), name='customer_list'), path('customers//', views.CustomerDetailView.as_view(), name='customer_detail'), path('customers/create/', views.CustomerCreateView.as_view(), name='customer_create'), @@ -42,15 +42,17 @@ urlpatterns = [ 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'), - # CRM URLs - path('opportunities//', views.OpportunityDetailView.as_view(), name='opportunity_detail'), - path('opportunities//edit/', views.OpportunityUpdateView.as_view(), name='update_opportunity'), - path('opportunities/', views.OpportunityListView.as_view(), name='opportunity_list'), - path('opportunities//delete/', views.delete_opportunity, name='delete_opportunity'), - path('opportunities//logs/', views.OpportunityLogsView.as_view(), name='opportunity_logs'), - path('notifications/', views.NotificationListView.as_view(), name='notifications_history'), - path('fetch_notifications/', views.fetch_notifications, name='fetch_notifications'), - path('notifications//mark_as_read/', views.mark_notification_as_read, name='mark_notification_as_read'), + + + path('crm/leads/', views.LeadListView.as_view(), name='lead_list'), + 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'), + path('crm/opportunities//delete/', views.delete_opportunity, name='delete_opportunity'), + path('crm/opportunities//logs/', views.OpportunityLogsView.as_view(), name='opportunity_logs'), + path('crm/notifications/', views.NotificationListView.as_view(), name='notifications_history'), + path('crm/fetch_notifications/', views.fetch_notifications, name='fetch_notifications'), + path('crm/notifications//mark_as_read/', views.mark_notification_as_read, name='mark_notification_as_read'), #Vendor URLs path('vendors', views.VendorListView.as_view(), name='vendor_list'), @@ -71,8 +73,8 @@ urlpatterns = [ path('cars/add/', views.CarCreateView.as_view(), name='car_add'), path('ajax/', views.AjaxHandlerView.as_view(), name='ajax_handler'), path('cars//add-color/', views.CarColorCreate.as_view(), name='add_color'), - path('car//location/add/', views.CarLocationCreateView.as_view(), name='add_car_location'), - path('car//location/update/', views.CarLocationUpdateView.as_view(), name='transfer'), + path('cars//location/add/', views.CarLocationCreateView.as_view(), name='add_car_location'), + path('cars//location/update/', views.CarLocationUpdateView.as_view(), name='transfer'), # path('cars//colors//update/',views.CarColorUpdateView.as_view(),name='color_update'), path('cars/reserve//', views.reserve_car_view, name='reserve_car'), diff --git a/inventory/views.py b/inventory/views.py index 46cb1cde..ac13c3b0 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -170,6 +170,7 @@ class TestView(TemplateView): template_name = "test.html" + class AccountingDashboard(LoginRequiredMixin, TemplateView): template_name = "dashboards/accounting.html" @@ -714,7 +715,7 @@ class CustomerListView(LoginRequiredMixin, ListView): query = self.request.GET.get("q") dealer = get_user_type(self.request) - customers = models.Customer.objects.filter(dealer=dealer) + customers = models.Customer.objects.filter(dealer=dealer, is_lead=False) if query: customers = customers.filter( @@ -748,7 +749,7 @@ class CustomerCreateView( success_message = _("Customer 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) @@ -2219,6 +2220,7 @@ def send_email_view(request, pk): ) +# CRM RELATED VIEWS def create_lead(request, pk): customer = get_object_or_404(models.Customer, pk=pk) if customer.is_lead: @@ -2230,6 +2232,31 @@ def create_lead(request, pk): 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' + + def get_queryset(self): + query = self.request.GET.get("q") + dealer = get_user_type(self.request) + + 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 + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["query"] = self.request.GET.get("q", "") + return context + + class OpportunityCreateView(CreateView): model = models.Opportunity form_class = forms.OpportunityForm diff --git a/requirements.txt b/requirements.txt index 622e3e3d..501dfa5f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,7 @@ +aiohappyeyeballs==2.4.4 +aiohttp==3.11.11 +aiohttp-retry==2.8.3 +aiosignal==1.3.2 alabaster==1.0.0 annotated-types==0.7.0 anyio==4.7.0 @@ -45,6 +49,7 @@ django-phonenumber-field==8.0.0 django-prometheus==2.3.1 django-sekizai==4.1.0 django-silk==5.3.2 +django-sms==0.7.0 django-sslserver==0.22 django-tables2==2.7.4 django-treebeard==4.7.1 @@ -58,6 +63,7 @@ et_xmlfile==2.0.0 Faker==33.1.0 Flask==3.1.0 fonttools==4.55.3 +frozenlist==1.5.0 gprof2dot==2024.6.6 graphqlclient==0.2.4 h11==0.14.0 @@ -75,13 +81,16 @@ joblib==1.4.2 ledger==1.0.1 lxml==5.3.0 Markdown==3.7 +markdown-it-py==3.0.0 MarkupSafe==3.0.2 marshmallow==3.23.2 mccabe==0.7.0 +mdurl==0.1.2 MouseInfo==0.1.3 +multidict==6.1.0 mypy-extensions==1.0.0 newrelic==10.4.0 -libquadmath==2.2.1 +numpy==2.2.1 oauthlib==3.2.2 ofxtools==0.9.5 openai==1.58.1 @@ -94,6 +103,7 @@ phonenumbers==8.13.52 pillow==11.0.0 platformdirs==4.3.6 prometheus_client==0.21.1 +propcache==0.2.1 psycopg==3.2.3 psycopg-binary==3.2.3 psycopg-c==3.2.3 @@ -139,8 +149,8 @@ requests==2.32.3 requests-oauthlib==2.0.0 rich==13.9.4 rubicon-objc==0.4.9 -libomp runtime library==1.6.0 -libquadmath==1.14.1 +scikit-learn==1.6.0 +scipy==1.14.1 selenium==4.27.1 six==1.17.0 sniffio==1.3.1 @@ -157,6 +167,7 @@ tomlkit==0.13.2 tqdm==4.67.1 trio==0.28.0 trio-websocket==0.11.1 +twilio==9.4.1 typing-inspect==0.9.0 typing_extensions==4.12.2 tzdata==2024.2 @@ -174,4 +185,5 @@ Werkzeug==3.1.3 wikipedia==1.4.0 wsproto==1.2.0 xmlsec==1.3.14 +yarl==1.18.3 zopfli==0.2.3.post1 diff --git a/static/.DS_Store b/static/.DS_Store index c6753b28..adb0562c 100644 Binary files a/static/.DS_Store and b/static/.DS_Store differ diff --git a/static/css/custom.css b/static/css/custom.css index 60d60233..1017a420 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -2,9 +2,9 @@ width: 64px; height: 16px; padding: 2px 4px; - border-radius: 1px; + border-radius: 3px; border: 1px outset #CBD0DDFd; text-align: center; - vertical-align: middle; - line-height: 22px; + /*vertical-align: middle;*/ + /*line-height: 22px;*/ } \ No newline at end of file diff --git a/static/images/.DS_Store b/static/images/.DS_Store index 23a16b17..1a0c7164 100644 Binary files a/static/images/.DS_Store and b/static/images/.DS_Store differ diff --git a/static/images/logos/.DS_Store b/static/images/logos/.DS_Store index 72155161..840c82ed 100644 Binary files a/static/images/logos/.DS_Store and b/static/images/logos/.DS_Store differ diff --git a/static/images/logos/car_make/convertible.svg b/static/images/logos/car_make/convertible.svg new file mode 100644 index 00000000..277ed941 --- /dev/null +++ b/static/images/logos/car_make/convertible.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/images/logos/car_make/coupe.svg b/static/images/logos/car_make/coupe.svg new file mode 100644 index 00000000..8097cff5 --- /dev/null +++ b/static/images/logos/car_make/coupe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/images/logos/car_make/sedan.svg b/static/images/logos/car_make/sedan.svg new file mode 100644 index 00000000..b560e504 --- /dev/null +++ b/static/images/logos/car_make/sedan.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/images/logos/car_make/suv.svg b/static/images/logos/car_make/suv.svg new file mode 100644 index 00000000..cd8f1e9e --- /dev/null +++ b/static/images/logos/car_make/suv.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/images/spot-illustrations/.DS_Store b/static/images/spot-illustrations/.DS_Store new file mode 100644 index 00000000..da69a15d Binary files /dev/null and b/static/images/spot-illustrations/.DS_Store differ diff --git a/static/images/spot-illustrations/21.png b/static/images/spot-illustrations/21.png index 939645ba..6612d677 100644 Binary files a/static/images/spot-illustrations/21.png and b/static/images/spot-illustrations/21.png differ diff --git a/static/images/spot-illustrations/dark_21.png b/static/images/spot-illustrations/dark_21.png index 5c0f04a8..731b6a9f 100644 Binary files a/static/images/spot-illustrations/dark_21.png and b/static/images/spot-illustrations/dark_21.png differ diff --git a/templates/crm/lead_list.html b/templates/crm/lead_list.html new file mode 100644 index 00000000..4e3f7b03 --- /dev/null +++ b/templates/crm/lead_list.html @@ -0,0 +1,185 @@ +{% extends 'base.html' %} +{% load i18n %} +{% load static %} +{% block title %}{{ _('Leads')|capfirst }}{% endblock title %} +{% block vendors %}{{ _("Leads")|capfirst }}{% endblock %} + +{% block content %} +
+

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

+
+ +
+
+ +
+
+
+ {% if page_obj.object_list %} +
+ + + + + + + + + + + + + + + + + {% for customer in customers %} + + + + + + + + + + + + + {% endfor %} + + {% endif %} +
{{ _("Name")|capfirst }} +
+
{{ _("email")|capfirst }} +
+
+
+
{{ _("Phone Number") }} +
+
+
+
{{ _("National ID")|capfirst }} +
+
+
+
{{ _("Address")|capfirst }} +
+
+ {{ _("Create date") }}
+ + + {{ _("Lead") }} + + + + + + {{ customer.phone_number }}{{ customer.national_id }} + {{ customer.address }}{{ customer.created|date }} +
+ + +
+
+
+
+ + {% if is_paginated %} + + {% endif %} +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/customers/customer_list.html b/templates/customers/customer_list.html index c4b631dc..ed29c636 100644 --- a/templates/customers/customer_list.html +++ b/templates/customers/customer_list.html @@ -65,7 +65,7 @@ - + {% for customer in customers %} @@ -102,12 +102,7 @@ - {% if customer.is_lead %} - - {{ _("Lead") }} - - - {% endif %} +
@@ -128,11 +123,7 @@
diff --git a/templates/dealers/dealer_detail.html b/templates/dealers/dealer_detail.html index 05c95c16..85615749 100644 --- a/templates/dealers/dealer_detail.html +++ b/templates/dealers/dealer_detail.html @@ -38,7 +38,8 @@ {% if dealer.logo %} {{ dealer.get_local_name }} {% else %} - {{ dealer.get_local_name }} + + {{ dealer.get_local_name }} {% endif %} @@ -65,7 +66,6 @@ {% else %} {% trans 'Expired' %} {% endif %} - diff --git a/templates/header.html b/templates/header.html index 338392cb..a88240df 100644 --- a/templates/header.html +++ b/templates/header.html @@ -77,11 +77,7 @@ - - - -