cleanup
1
.idea/vcs.xml
generated
@ -2,5 +2,6 @@
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/django-crm" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@ -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
|
||||
@ -572,11 +572,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),
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-02 23:56
|
||||
# Generated by Django 5.1.4 on 2025-01-05 09:01
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@ -9,29 +10,30 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='subscription',
|
||||
options={'verbose_name': 'Subscription', 'verbose_name_plural': 'Subscriptions'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='subscriptionplan',
|
||||
options={'verbose_name': 'Subscription Plan', 'verbose_name_plural': 'Subscription Plans'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='subscriptionuser',
|
||||
options={'verbose_name': 'Subscription User', 'verbose_name_plural': 'Subscription Users'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='opportunity',
|
||||
name='source',
|
||||
),
|
||||
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',
|
||||
@ -73,6 +75,27 @@ class Migration(migrations.Migration):
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notes',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.DO_NOTHING, related_name='notes_created', to=settings.AUTH_USER_MODEL),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='opportunity',
|
||||
name='deal_status',
|
||||
field=models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('canceled', 'Canceled'), ('completed', 'Completed')], default='new', max_length=20, verbose_name='Deal Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='opportunity',
|
||||
name='priority',
|
||||
field=models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')], default='low', max_length=10, verbose_name='Priority'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='staff',
|
||||
name='staff_type',
|
||||
field=models.CharField(choices=[('manager', 'Manager'), ('inventory', 'Inventory'), ('accountant', 'Accountant'), ('sales', 'Sales'), ('coordinator', 'Coordinator'), ('receptionist', 'Receptionist'), ('agent', 'Agent')], max_length=255, verbose_name='Staff Type'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='subscription',
|
||||
name='end_date',
|
||||
@ -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"))
|
||||
|
||||
@ -6,6 +6,7 @@ from django_ledger.io import roles
|
||||
from django_ledger.models import EntityModel,AccountModel,ItemModel,ItemModelAbstract,UnitOfMeasureModel, VendorModel
|
||||
from . import models
|
||||
from .models import OpportunityLog
|
||||
from .utils import get_dealer_from_instance
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@ -319,14 +320,14 @@ def create_item_model(sender, instance, created, **kwargs):
|
||||
#
|
||||
#
|
||||
# # update price - CarFinance
|
||||
# @receiver(post_save, sender=CarFinance)
|
||||
# def update_item_model_cost(sender, instance, created, **kwargs):
|
||||
#
|
||||
# ItemModel.objects.filter(item_id=instance.car.vin).update(
|
||||
# inventory_received_value=instance.cost_price,
|
||||
# default_amount=instance.cost_price,
|
||||
# )
|
||||
# print(f"Inventory item updated with CarFinance data for Car: {instance.car}")
|
||||
@receiver(post_save, sender=models.CarFinance)
|
||||
def update_item_model_cost(sender, instance, created, **kwargs):
|
||||
|
||||
ItemModel.objects.filter(item_id=instance.car.vin).update(
|
||||
inventory_received_value=instance.cost_price,
|
||||
default_amount=instance.cost_price,
|
||||
)
|
||||
print(f"Inventory item updated with CarFinance data for Car: {instance.car}")
|
||||
|
||||
|
||||
|
||||
|
||||
@ -34,7 +34,7 @@ urlpatterns = [
|
||||
path('dealers/activity/', views.UserActivityLogListView.as_view(), name='dealer_activity'),
|
||||
# path('dealers/<int:pk>/delete/', views.DealerDeleteView.as_view(), name='dealer_delete'),
|
||||
|
||||
# Customer URLs
|
||||
# CRM URLs
|
||||
path('customers/', views.CustomerListView.as_view(), name='customer_list'),
|
||||
path('customers/<int:pk>/', views.CustomerDetailView.as_view(), name='customer_detail'),
|
||||
path('customers/create/', views.CustomerCreateView.as_view(), name='customer_create'),
|
||||
@ -42,15 +42,17 @@ urlpatterns = [
|
||||
path('customers/<int:pk>/delete/', views.delete_customer, name='customer_delete'),
|
||||
path('customers/<int:pk>/create_lead/', views.create_lead, name='create_lead'),
|
||||
path('customers/<int:customer_id>/opportunities/create/', views.OpportunityCreateView.as_view(), name='create_opportunity'),
|
||||
# CRM URLs
|
||||
path('opportunities/<int:pk>/', views.OpportunityDetailView.as_view(), name='opportunity_detail'),
|
||||
path('opportunities/<int:pk>/edit/', views.OpportunityUpdateView.as_view(), name='update_opportunity'),
|
||||
path('opportunities/', views.OpportunityListView.as_view(), name='opportunity_list'),
|
||||
path('opportunities/<int:pk>/delete/', views.delete_opportunity, name='delete_opportunity'),
|
||||
path('opportunities/<int:pk>/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/<int:pk>/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/<int:pk>/', views.OpportunityDetailView.as_view(), name='opportunity_detail'),
|
||||
path('crm/opportunities/<int:pk>/edit/', views.OpportunityUpdateView.as_view(), name='update_opportunity'),
|
||||
path('crm/opportunities/', views.OpportunityListView.as_view(), name='opportunity_list'),
|
||||
path('crm/opportunities/<int:pk>/delete/', views.delete_opportunity, name='delete_opportunity'),
|
||||
path('crm/opportunities/<int:pk>/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/<int:pk>/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/<int:car_pk>/add-color/', views.CarColorCreate.as_view(), name='add_color'),
|
||||
path('car/<int:car_pk>/location/add/', views.CarLocationCreateView.as_view(), name='add_car_location'),
|
||||
path('car/<int:pk>/location/update/', views.CarLocationUpdateView.as_view(), name='transfer'),
|
||||
path('cars/<int:car_pk>/location/add/', views.CarLocationCreateView.as_view(), name='add_car_location'),
|
||||
path('cars/<int:pk>/location/update/', views.CarLocationUpdateView.as_view(), name='transfer'),
|
||||
# path('cars/<int:car_pk>/colors/<int:pk>/update/',views.CarColorUpdateView.as_view(),name='color_update'),
|
||||
|
||||
path('cars/reserve/<int:car_id>/', views.reserve_car_view, name='reserve_car'),
|
||||
|
||||
@ -61,8 +61,15 @@ def send_email(from_, to_, subject, message):
|
||||
|
||||
|
||||
def get_user_type(request):
|
||||
dealer = ""
|
||||
if hasattr(request.user, 'dealer'):
|
||||
dealer = request.user.dealer
|
||||
elif hasattr(request.user, 'staff'):
|
||||
dealer = request.user.staff.dealer
|
||||
return dealer
|
||||
|
||||
def get_dealer_from_instance(instance):
|
||||
if instance.dealer.staff:
|
||||
return instance.dealer
|
||||
else:
|
||||
return instance.dealer
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.paginator import Paginator
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
@ -180,6 +181,7 @@ class TestView(TemplateView):
|
||||
template_name = "test.html"
|
||||
|
||||
|
||||
|
||||
class AccountingDashboard(LoginRequiredMixin, TemplateView):
|
||||
template_name = "dashboards/accounting.html"
|
||||
|
||||
@ -233,7 +235,8 @@ class CarCreateView(LoginRequiredMixin, CreateView):
|
||||
return reverse("inventory_stats")
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.dealer = self.request.user.dealer
|
||||
dealer = get_user_type(self.request)
|
||||
form.instance.dealer = dealer
|
||||
form.save()
|
||||
messages.success(self.request, "Car saved successfully.")
|
||||
return super().form_valid(form)
|
||||
@ -257,10 +260,13 @@ class AjaxHandlerView(LoginRequiredMixin, View):
|
||||
|
||||
def decode_vin(self, request):
|
||||
vin_no = request.GET.get("vin_no")
|
||||
car_existed = models.Car.objects.filter(vin=vin_no).exists()
|
||||
|
||||
if car_existed:
|
||||
return JsonResponse({"error": _("VIN number exists")}, status=400)
|
||||
|
||||
if not vin_no or len(vin_no.strip()) != 17:
|
||||
return JsonResponse(
|
||||
{"success": False, "error": "Invalid VIN number provided."}, status=400
|
||||
)
|
||||
return JsonResponse({"success": False, "error": "Invalid VIN number provided."}, status=400)
|
||||
|
||||
vin_no = vin_no.strip()
|
||||
vin_data = {}
|
||||
@ -727,7 +733,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(
|
||||
@ -761,7 +767,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)
|
||||
|
||||
|
||||
@ -2236,6 +2242,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:
|
||||
@ -2247,6 +2254,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
|
||||
|
||||
@ -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
|
||||
|
||||
BIN
static/.DS_Store
vendored
@ -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;*/
|
||||
}
|
||||
BIN
static/images/.DS_Store
vendored
BIN
static/images/car_make/maserati.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
static/images/logos/.DS_Store
vendored
1
static/images/logos/car_make/convertible.svg
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
1
static/images/logos/car_make/coupe.svg
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
static/images/logos/car_make/maserati.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
1
static/images/logos/car_make/sedan.svg
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
1
static/images/logos/car_make/suv.svg
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
static/images/spot-illustrations/.DS_Store
vendored
Normal file
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 12 KiB |
185
templates/crm/lead_list.html
Normal file
@ -0,0 +1,185 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% block title %}{{ _('Leads')|capfirst }}{% endblock title %}
|
||||
{% block vendors %}<a class="nav-link active">{{ _("Leads")|capfirst }}</a>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h2 class="mb-4">{{ _("Leads")|capfirst }}</h2>
|
||||
<div class="row g-3 justify-content-between mb-4">
|
||||
<div class="col-auto">
|
||||
<div class="d-md-flex justify-content-between">
|
||||
<div>
|
||||
<a href="{% url 'customer_create' %}" class="btn btn-primary me-4"><span class="fas fa-plus me-2"></span>{{ _("Add Customer") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="d-flex">
|
||||
<div class="search-box me-2">
|
||||
<form method="get" class="d-inline-block position-relative">
|
||||
<div class="input-group">
|
||||
<button type="submit" class="btn btn-phoenix-primary"><span class="fas fa-search search-box-icon"></span></button>
|
||||
<input name="q" class="form-control search-input search" type="search" placeholder="{{ _('Enter customer name') }}" value="{{ request.GET.q }}"/>
|
||||
{% if request.GET.q %}
|
||||
<a href="{% url request.resolver_match.view_name %}" class="btn btn-close"></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if page_obj.object_list %}
|
||||
<div class="table-responsive scrollbar mx-n1 px-1">
|
||||
|
||||
<table class="table fs-9 mb-0 border-top border-translucent">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="sort white-space-nowrap align-middle text-uppercase ps-0" scope="col" data-sort="name" style="width:25%;">{{ _("Name")|capfirst }}</th>
|
||||
<th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent" scope="col" data-sort="email" style="width:15%;">
|
||||
<div class="d-inline-flex flex-center">
|
||||
<div class="d-flex align-items-center px-1 py-1 bg-success-subtle rounded me-2"><span class="text-success-dark" data-feather="mail"></span></div><span>{{ _("email")|capfirst }}</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent" scope="col" data-sort="phone" style="width:15%; min-width: 180px;">
|
||||
<div class="d-inline-flex flex-center">
|
||||
<div class="d-flex align-items-center px-1 py-1 bg-primary-subtle rounded me-2"><span class="text-primary-dark" data-feather="phone"></span></div><span>{{ _("Phone Number") }}</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent" scope="col" data-sort="contact" style="width:15%;">
|
||||
<div class="d-inline-flex flex-center">
|
||||
<div class="d-flex align-items-center px-1 py-1 bg-info-subtle rounded me-2"><span class="text-info-dark" data-feather="user"></span></div><span>{{ _("National ID")|capfirst }}</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent" scope="col" data-sort="company" style="width:15%;">
|
||||
<div class="d-inline-flex flex-center">
|
||||
<div class="d-flex align-items-center px-1 py-1 bg-warning-subtle rounded me-2"><span class="text-warning-dark" data-feather="grid"></span></div><span>{{ _("Address")|capfirst }}</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="sort align-middle ps-4 pe-5 text-uppercase" scope="col" data-sort="date" style="width:15%;">
|
||||
{{ _("Create date") }}</th>
|
||||
<th class="sort text-end align-middle pe-0 ps-4" scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="list" id="lead-tables-body">
|
||||
|
||||
{% for customer in customers %}
|
||||
<!-- Delete Modal -->
|
||||
<div class="modal fade" id="deleteModal"
|
||||
data-bs-backdrop="static"
|
||||
data-bs-keyboard="false"
|
||||
tabindex="-1"
|
||||
aria-labelledby="deleteModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteModalLabel">
|
||||
|
||||
{% trans "Delete Customer" %}
|
||||
<span data-feather="alert-circle"></span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<p class="mb-0 text-danger fw-bold">
|
||||
{% trans "Are you sure you want to delete this customer?" %}
|
||||
</p>
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">
|
||||
{% trans "No" %}
|
||||
</button>
|
||||
<a type="button" class="btn btn-danger btn-sm" href="{% url 'customer_delete' customer.id %}">
|
||||
{% trans "Yes" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
||||
<td>
|
||||
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-success">
|
||||
<span class="badge-label">{{ _("Lead") }}</span>
|
||||
<span class="ms-1" data-feather="check" style="height:13px;width:13px;"></span>
|
||||
</span>
|
||||
|
||||
</td>
|
||||
<td class="name align-middle white-space-nowrap ps-0">
|
||||
<div class="d-flex align-items-center">
|
||||
<div><a class="fs-8 fw-bold" href="{% url 'customer_detail' customer.id %}">{{ customer.first_name }} {{ customer.middle_name }} {{ customer.last_name }}</a>
|
||||
<div class="d-flex align-items-center">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="email align-middle white-space-nowrap fw-semibold ps-4 border-end border-translucent"><a class="text-body-highlight" href="">{{ customer.email }}</a></td>
|
||||
<td class="phone align-middle white-space-nowrap fw-semibold ps-4 border-end border-translucent"><a class="text-body-highlight" href="tel:{{ customer.phone_number }}">{{ customer.phone_number }}</a></td>
|
||||
<td class="contact align-middle white-space-nowrap ps-4 border-end border-translucent fw-semibold text-body-highlight">{{ customer.national_id }}</td>
|
||||
<td class="company align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 border-end border-translucent fw-semibold text-body-highlight">
|
||||
{{ customer.address }}</td>
|
||||
<td class="date align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 text-body-tertiary">{{ customer.created|date }}</td>
|
||||
<td class="align-middle white-space-nowrap text-end pe-0 ps-4">
|
||||
<div class="btn-reveal-trigger position-static">
|
||||
<button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal fs-10" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10"></span></button>
|
||||
<div class="dropdown-menu dropdown-menu-end py-2">
|
||||
<a href="{% url 'customer_update' customer.id %}" class="dropdown-item text-success-dark">{% trans "Edit" %}</a>
|
||||
<a href="{% url 'create_opportunity' customer.pk %}" class="dropdown-item text-warning-dark">{{_("Opportunity")}}</a>
|
||||
<div class="dropdown-divider"></div><button class="dropdown-item text-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">{% trans "Delete" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
<div class="row align-items-center justify-content-end py-4 pe-0 fs-9">
|
||||
<!-- Optional: Pagination -->
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination pagination-sm justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item py-0">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active"><a class="page-link" href="?page={{ num }}">{{ num }}</a></li>
|
||||
{% else %}
|
||||
<li class="page-item"><a class="page-link" href="?page={{ num }}">{{ num }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %} {% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
@ -65,7 +65,7 @@
|
||||
<th class="sort text-end align-middle pe-0 ps-4" scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="list" id="leal-tables-body">
|
||||
<tbody class="list" id="lead-tables-body">
|
||||
|
||||
{% for customer in customers %}
|
||||
<!-- Delete Modal -->
|
||||
@ -102,12 +102,7 @@
|
||||
</div>
|
||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
||||
<td>
|
||||
{% if customer.is_lead %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-success">
|
||||
<span class="badge-label">{{ _("Lead") }}</span>
|
||||
<span class="ms-1" data-feather="check" style="height:13px;width:13px;"></span>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
</td>
|
||||
<td class="name align-middle white-space-nowrap ps-0">
|
||||
<div class="d-flex align-items-center">
|
||||
@ -128,11 +123,7 @@
|
||||
<button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal fs-10" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10"></span></button>
|
||||
<div class="dropdown-menu dropdown-menu-end py-2">
|
||||
<a href="{% url 'customer_update' customer.id %}" class="dropdown-item text-success-dark">{% trans "Edit" %}</a>
|
||||
{% if customer.is_lead %}
|
||||
<a href="{% url 'create_opportunity' customer.pk %}" class="dropdown-item text-warning-dark">{{_("Opportunity")}}</a>
|
||||
{% else %}
|
||||
<a href="{% url 'create_lead' customer.pk %}" class="dropdown-item text-success-dark">{{ _("Mark as Lead")}}</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'create_lead' customer.pk %}" class="dropdown-item text-success-dark">{{ _("Mark as Lead")}}</a>
|
||||
<div class="dropdown-divider"></div><button class="dropdown-item text-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">{% trans "Delete" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -38,7 +38,8 @@
|
||||
{% if dealer.logo %}
|
||||
<img src="{{ dealer.logo.url }}" alt="{{ dealer.get_local_name }}" class="rounded-circle" style="max-width: 150px;">
|
||||
{% else %}
|
||||
<img src="{% static 'images/logos/logo.png' %}" alt="{{ dealer.get_local_name }}" class="rounded-circle" style="max-width: 150px;">
|
||||
<span class="rounded-circle feather feather-user text-body-tertiary" style="max-width: 150px;"></span>
|
||||
<img src="{% static 'images/logos/logo.png' %}" alt="{{ dealer.get_local_name }}" class="" style="max-width: 150px;">
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
@ -65,7 +66,6 @@
|
||||
{% else %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-danger"><span class="badge-label">{% trans 'Expired' %}</span><span class="ms-1" data-feather="times" style="height:12.8px;width:12.8px;"></span></span>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -77,11 +77,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- parent pages-->
|
||||
|
||||
<!-- parent pages-->
|
||||
|
||||
<!-- parent pages-->
|
||||
<div class="nav-item-wrapper">
|
||||
<a class="nav-link dropdown-indicator label-1" href="#nv-sales" role="button" data-bs-toggle="collapse" aria-expanded="false" aria-controls="nv-sales">
|
||||
<div class="d-flex align-items-center">
|
||||
@ -210,8 +206,7 @@
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="left"
|
||||
data-bs-title="Switch theme"
|
||||
style="height: 32px; width: 32px;"
|
||||
>
|
||||
style="height: 32px; width: 32px;">
|
||||
<span class="icon" data-feather="moon"></span>
|
||||
</label>
|
||||
<label
|
||||
@ -221,8 +216,7 @@
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="left"
|
||||
data-bs-title="Switch theme"
|
||||
style="height: 32px; width: 32px;"
|
||||
>
|
||||
style="height: 32px; width: 32px;">
|
||||
<span class="icon" data-feather="sun"></span>
|
||||
</label>
|
||||
</div>
|
||||
@ -261,6 +255,10 @@
|
||||
<div class="avatar avatar-l">
|
||||
{% if user.dealer.logo %}
|
||||
<img class="rounded-circle" src="{{ user.dealer.logo.url }}" alt="" />
|
||||
{% elif user.staff.dealer.logo %}
|
||||
<img class="rounded-circle" src="{{ user.staff.dealer.logo.url }}" alt="" />
|
||||
{% else %}
|
||||
<span class="fa fa-user text-body-tertiary" style="width: 32px;"></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
@ -268,9 +266,14 @@
|
||||
<div class="card position-relative border-0">
|
||||
<div class="card-body p-0">
|
||||
<div class="text-center pt-4 pb-3">
|
||||
|
||||
<div class="avatar avatar-xl">
|
||||
{% if user.dealer.logo %}
|
||||
{% if user.dealer.logo %}
|
||||
<img class="rounded-circle" src="{{ user.dealer.logo.url }}" alt="" />
|
||||
{% elif user.staff.dealer.logo %}
|
||||
<img class="rounded-circle" src="{{ user.staff.dealer.logo.url }}" alt="" />
|
||||
{% else %}
|
||||
<span class="fa fa-user text-body-tertiary" style="width: 32px;"></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h6 class="mt-2 text-body-emphasis">{{ user.dealer.get_local_name }}</h6>
|
||||
@ -294,18 +297,18 @@
|
||||
<a class="nav-link px-3 d-block" href="{% url 'dealer_activity' %}"> <span class="me-2 text-body align-bottom" data-feather="lock"></span>{{ _("Activity") }}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link px-3 d-block" href="#!"> <span class="me-2 text-body align-bottom" data-feather="settings"></span>Settings & Privacy </a>
|
||||
<a class="nav-link px-3 d-block" href=""> <span class="me-2 text-body align-bottom" data-feather="settings"></span>Settings & Privacy </a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link px-3 d-block" href="#!"> <span class="me-2 text-body align-bottom" data-feather="help-circle"></span>Help Center</a>
|
||||
<a class="nav-link px-3 d-block" href=""> <span class="me-2 text-body align-bottom" data-feather="help-circle"></span>Help Center</a>
|
||||
</li>
|
||||
<li class="nav-item"><a class="nav-link px-3 d-block" href="#!"> Language</a></li>
|
||||
<li class="nav-item"><a class="nav-link px-3 d-block" href=""> Language</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-footer p-0 border-top border-translucent">
|
||||
<ul class="nav d-flex flex-column my-3">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link px-3 d-block" href="#!"> <span class="me-2 text-body align-bottom" data-feather="user-plus"></span>Add another account</a>
|
||||
<a class="nav-link px-3 d-block" href=""> <span class="me-2 text-body align-bottom" data-feather="user-plus"></span>Add another account</a>
|
||||
</li>
|
||||
</ul>
|
||||
<hr />
|
||||
@ -313,7 +316,7 @@
|
||||
<a class="btn btn-phoenix-secondary d-flex flex-center w-100" href="{% url 'account_logout' %}"> <span class="me-2" data-feather="log-out"> </span>{% trans 'Sign Out' %}</a>
|
||||
</div>
|
||||
<div class="my-2 text-center fw-bold fs-10 text-body-quaternary">
|
||||
<a class="text-body-quaternary me-1" href="#!">Privacy policy</a>•<a class="text-body-quaternary mx-1" href="#!">Terms</a>•<a class="text-body-quaternary ms-1" href="#!">Cookies</a>
|
||||
<a class="text-body-quaternary me-1" href="">Privacy policy</a>•<a class="text-body-quaternary mx-1" href="">Terms</a>•<a class="text-body-quaternary ms-1" href="">Cookies</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="px-3">
|
||||
|
||||
@ -235,7 +235,7 @@
|
||||
<tbody>
|
||||
{% for reservation in car.reservations.all %}
|
||||
<tr>
|
||||
<td>{{ reservation.reserved_by.dealer.staff }}</td>
|
||||
<td>{{ reservation.reserved_by.dealer }}</td>
|
||||
<td>{{ reservation.reserved_until }}</td>
|
||||
<td>
|
||||
{% if reservation.is_active %}
|
||||
|
||||
@ -206,30 +206,23 @@
|
||||
</div>
|
||||
<!-- Receiving Date Field -->
|
||||
<div class="col-lg-4 col-xl-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.receiving_date.id_for_label }}" class="form-label">
|
||||
{% trans 'Receiving Date' %}:
|
||||
</label>
|
||||
{{ form.receiving_date }}
|
||||
<input
|
||||
class="form-control form-control-sm datetimepicker flatpickr-input"
|
||||
id="{{ form.receiving_date.id_for_label }}"
|
||||
type="text"
|
||||
placeholder="dd/mm/yyyy hour : minute"
|
||||
data-options='{"enableTime":true,"dateFormat":"d/m/y H:i","disableMobile":false}'
|
||||
readonly="readonly"
|
||||
/>
|
||||
{% if form.receiving_date.errors %}
|
||||
<div class="text-danger small">
|
||||
{{ form.receiving_date.errors|striptags }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card h-100 border-1 rounded shadow">
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.receiving_date.id_for_label }}"
|
||||
class="form-label">
|
||||
{% trans 'Receiving Date' %}:
|
||||
</label>
|
||||
{{ form.receiving_date|add_class:"form-control form-control-sm" }}
|
||||
{% if form.receiving_date.errors %}
|
||||
<div class="text-danger small">
|
||||
{{ form.receiving_date.errors|striptags }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Remarks Card -->
|
||||
<div class="col-lg-4 col-xl-3">
|
||||
<div class="card h-100">
|
||||
@ -369,7 +362,7 @@
|
||||
async function decodeVin() {
|
||||
const vinNumber = vinInput.value.trim();
|
||||
if (vinNumber.length !== 17) {
|
||||
notify("error", "{% trans 'Please enter a valid VIN.' %}");
|
||||
Swal.fire("error", "{% trans 'Please enter a valid VIN.' %}");
|
||||
/*alert("{% trans 'Please enter a valid VIN.' %}");*/
|
||||
return;
|
||||
}
|
||||
@ -387,12 +380,12 @@
|
||||
await updateFields(data.data);
|
||||
} else {
|
||||
hideLoading();
|
||||
notify("error", data.error);
|
||||
Swal.fire("{% trans 'error' %}", data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error decoding VIN:", error);
|
||||
hideLoading();
|
||||
notify("error", "{% trans 'An error occurred while decoding the VIN.' %}");
|
||||
Swal.fire("error", "{% trans 'An error occurred while decoding the VIN.' %}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -16,12 +16,12 @@
|
||||
<div class="card border h-100 w-100 overflow-hidden">
|
||||
<div class="bg-holder d-block bg-card" style="background-image:url({% static 'images/spot-illustrations/32.png' %});background-position: top right;">
|
||||
</div>
|
||||
<div class="d-dark-none">
|
||||
<div class="bg-holder d-none d-sm-block d-xl-none d-xxl-block bg-card" style="background-image:url({% static 'images/spot-illustrations/dark_21.png' %});background-position: bottom right; background-size: auto;">
|
||||
<div class="d-dark-none me-5">
|
||||
<div class="bg-holder d-none d-sm-block d-xl-none d-xxl-block bg-card" style="background-image:url({% static 'images/spot-illustrations/dark_21.png' %}); background-position: bottom right; background-size: auto;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-light-none">
|
||||
<div class="bg-holder d-none d-sm-block d-xl-none d-xxl-block bg-card" style="background-image:url({% static 'images/spot-illustrations/21.png' %});background-position: bottom right; background-size: auto;">
|
||||
<div class="d-light-none me-5">
|
||||
<div class="bg-holder d-none d-sm-block d-xl-none d-xxl-block bg-card" style="background-image:url({% static 'images/spot-illustrations/21.png' %}); background-position: bottom right; background-size: auto;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body px-lg-5 position-relative">
|
||||
@ -30,7 +30,9 @@
|
||||
<div class="col-12 col-sm-auto ">
|
||||
<div class="avatar avatar-3xl avatar-bordered mb-3">
|
||||
{% if cars.first.id_car_make.logo %}
|
||||
<img class="rounded-circle" src="{{ cars.first.id_car_make.logo.url }}" alt="">
|
||||
<img class="rounded-circle" src="{{ cars.first.id_car_make.logo.url }}" alt="">
|
||||
{% else %}
|
||||
<img class="rounded-circle" src="{% static 'images/logos/car_make/sedan.svg' %}" alt="">
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@ -76,12 +78,12 @@
|
||||
{% if car.colors.exists %}
|
||||
<td class="align-middle white-space-nowrap text-body fs-9 text-start">
|
||||
<div class="d-flex flex-column align-items-center">
|
||||
<span class="color-div" style="background: linear-gradient(90deg, rgba({{ car.colors.first.exterior.rgb }},1) 40%, rgba({{ car.colors.first.exterior.rgb }},0.35) 100%);" title="{{ car.colors.first.exterior.get_local_name }}"></span><span>{{ car.colors.first.exterior.get_local_name }}</span>
|
||||
<span class="color-div" style="background: linear-gradient(90deg, rgba({{ car.colors.first.exterior.rgb }},1) 10%, rgba({{ car.colors.first.exterior.rgb }},0.10) 100%);" title="{{ car.colors.first.exterior.get_local_name }}"></span><span>{{ car.colors.first.exterior.get_local_name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap text-body fs-9 text-start">
|
||||
<div class="d-flex flex-column align-items-center">
|
||||
<span class="color-div" style="background: linear-gradient(90deg, rgba({{ car.colors.first.interior.rgb }},1) 40%, rgba({{ car.colors.first.interior.rgb }},0.35) 100%);" title="{{ car.colors.first.interior.get_local_name }}"></span><span>{{ car.colors.first.interior.get_local_name }}</span>
|
||||
<span class="color-div" style="background: linear-gradient(90deg, rgba({{ car.colors.first.interior.rgb }},1) 10%, rgba({{ car.colors.first.interior.rgb }},0.10) 100%);" title="{{ car.colors.first.interior.get_local_name }}"></span><span>{{ car.colors.first.interior.get_local_name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
{% else %}
|
||||
|
||||
@ -27,26 +27,26 @@
|
||||
<td class="align-middle product white-space-nowrap">{{ estimate.customer }}</td>
|
||||
<td class="align-middle product white-space-nowrap">
|
||||
{% if estimate.status == 'draft' %}
|
||||
<span class="badge bg-warning text-light">{% trans "Draft" %}</span>
|
||||
<span class="badge badge-phoenix badge-phoenix-warning">{% trans "Draft" %}</span>
|
||||
{% elif estimate.status == 'in_review' %}
|
||||
<span class="badge bg-info text-light">{% trans "In Review" %}</span>
|
||||
<span class="badge badge-phoenix badge-phoenix-info">{% trans "In Review" %}</span>
|
||||
{% elif estimate.status == 'approved' %}
|
||||
<span class="badge bg-success text-light">{% trans "Approved" %}</span>
|
||||
<span class="badge badge-phoenix badge-phoenix-success">{% trans "Approved" %}</span>
|
||||
{% elif estimate.status == 'declined' %}
|
||||
<span class="badge bg-danger text-light">{% trans "Declined" %}</span>
|
||||
<span class="badge badge-phoenix badge-phoenix-danger">{% trans "Declined" %}</span>
|
||||
{% elif estimate.status == 'canceled' %}
|
||||
<span class="badge bg-danger text-light">{% trans "Canceled" %}</span>
|
||||
<span class="badge badge-phoenix badge-phoenix-danger">{% trans "Canceled" %}</span>
|
||||
{% elif estimate.status == 'completed' %}
|
||||
<span class="badge bg-success text-light">{% trans "Completed" %}</span>
|
||||
<span class="badge badge-phoenix badge-phoenix-success">{% trans "Completed" %}</span>
|
||||
{% elif estimate.status == 'void' %}
|
||||
<span class="badge bg-secondary text-light">{% trans "Void" %}</span>
|
||||
<span class="badge badge-phoenix badge-phoenix-secondary">{% trans "Void" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="align-middle product white-space-nowrap">{{ estimate.get_status_action_date }}</td>
|
||||
<td class="align-middle product white-space-nowrap">{{ estimate.created }}</td>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'estimate_detail' estimate.pk %}"
|
||||
class="btn btn-sm btn-success">
|
||||
class="btn btn-sm btn-phoenix-success">
|
||||
{% trans "view" %}
|
||||
</a>
|
||||
</td>
|
||||
|
||||
@ -56,7 +56,7 @@
|
||||
<td class="align-middle product white-space-nowrap">{{ invoice.created }}</td>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'invoice_detail' invoice.pk %}"
|
||||
class="btn btn-sm btn-success">
|
||||
class="btn btn-sm btn-phoenix-success">
|
||||
{% trans "View" %}
|
||||
</a>
|
||||
</td>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block title %}{{ _("Tranactions") }}{% endblock title %}
|
||||
{% block title %}{{ _("Transactions") }}{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h3 class="text-center">{% trans "Tranactions" %}</h3>
|
||||
<h3 class="text-center">{% trans "Transactions" %}</h3>
|
||||
<div class="mx-n4 px-4 mx-lg-n6 px-lg-6 bg-body-emphasis pt-7 border-y">
|
||||
|
||||
<div class="table-responsive mx-n1 px-1 scrollbar">
|
||||
@ -34,7 +34,7 @@
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">{% trans "No Tranactions Found" %}</td>
|
||||
<td colspan="6" class="text-center">{% trans "No Transactions Found" %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
53
test_sms.py
Normal file
@ -0,0 +1,53 @@
|
||||
# from django.conf import settings
|
||||
# settings.configure()
|
||||
#
|
||||
# from sms import Message
|
||||
#
|
||||
# message = Message(
|
||||
# 'Here is the message',
|
||||
# '+12065550100',
|
||||
# ['+441134960000']
|
||||
# )
|
||||
# print(message)
|
||||
|
||||
from twilio.rest import Client
|
||||
|
||||
account_sid = 'AC988fcc2cce8ae4f36ffc58bf897bcb1d'
|
||||
auth_token = '8e5a85e1fe8d48d73b8247bdee3ec65a'
|
||||
client = Client(account_sid, auth_token)
|
||||
|
||||
message = client.messages.create(
|
||||
from_='+12313103856', #twilio phone number
|
||||
body='أول رسالة من هيكل .. السلام عليكم',
|
||||
to='+966535521547',
|
||||
)
|
||||
|
||||
print(message.sid)
|
||||
|
||||
# from twilio.rest import Client
|
||||
#
|
||||
# account_sid = 'AC988fcc2cce8ae4f36ffc58bf897bcb1d'
|
||||
# auth_token = '8e5a85e1fe8d48d73b8247bdee3ec65a'
|
||||
# client = Client(account_sid, auth_token)
|
||||
#
|
||||
# message = client.messages.create(
|
||||
# from_='+12313103856',
|
||||
# to='+966535521547'
|
||||
# )
|
||||
#
|
||||
# print(message.sid)
|
||||
|
||||
|
||||
# from twilio.rest import Client
|
||||
#
|
||||
# account_sid = settings.TWILIO_ACCOUNT_SID
|
||||
# auth_token = settings.TWILIO_AUTH_TOKEN
|
||||
# client = Client(account_sid, auth_token)
|
||||
#
|
||||
# verification_check = client.verify \
|
||||
# .v2 \
|
||||
# .services('VA4a22a04749e04800cc89f687df8732d3') \
|
||||
# .verification_checks \
|
||||
# .create(to='+966535521547', code='[Code]')
|
||||
#
|
||||
# print(verification_check.status)
|
||||