This commit is contained in:
Marwan Alwali 2025-02-04 22:37:12 +03:00
commit 3febd59d86
26 changed files with 537 additions and 132 deletions

View File

@ -5,4 +5,8 @@ class InventoryConfig(AppConfig):
name = 'inventory'
def ready(self):
import inventory.signals
import inventory.signals
from decimal import Decimal
from inventory.models import VatRate
VatRate.objects.get_or_create(rate=Decimal('0.15'), is_active=True)

View File

@ -17,6 +17,7 @@ from .models import (
Dealer,
# Branch,
Vendor,
Schedule,
Customer,
Car,
CarTransfer,
@ -38,7 +39,7 @@ from .models import (
SaleOrder
)
from django_ledger.models import ItemModel, InvoiceModel, BillModel,VendorModel
from django.forms import ModelMultipleChoiceField, ValidationError, DateInput
from django.forms import ModelMultipleChoiceField, ValidationError, DateInput,DateTimeInput
from django.utils.translation import gettext_lazy as _
import django_tables2 as tables
from django.forms import formset_factory
@ -651,8 +652,12 @@ class EmailForm(forms.Form):
class LeadForm(forms.ModelForm):
class Meta:
model = Lead
fields = ['customer',
'city',
fields = [
'first_name',
'last_name',
'email',
'phone_number',
'address',
'id_car_make',
'id_car_model',
'year',
@ -671,6 +676,11 @@ class LeadForm(forms.ModelForm):
(obj.id_car_make, obj.get_local_name()) for obj in queryset
]
class ScheduleForm(forms.ModelForm):
scheduled_at = forms.DateTimeField(widget=DateTimeInput(attrs={'type': 'datetime-local'}))
class Meta:
model = Schedule
fields = ['purpose','scheduled_type', 'scheduled_at', 'notes']
class NoteForm(forms.ModelForm):
class Meta:

View File

@ -4,6 +4,8 @@ from django.utils import timezone
from django.shortcuts import redirect
from django.urls import reverse
from inventory.utils import get_user_type
logger = logging.getLogger('user_activity')
@ -37,7 +39,8 @@ class InjectParamsMiddleware:
def __call__(self, request):
try:
request.entity = request.user.dealer.entity
# request.entity = request.user.dealer.entity
request.dealer = get_user_type(request)
except Exception as e:
pass

View File

@ -0,0 +1,21 @@
# Generated by Django 4.2.17 on 2025-02-04 09:34
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.DJANGO_LEDGER_CUSTOMER_MODEL),
('inventory', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='lead',
name='customer',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leads', to=settings.DJANGO_LEDGER_CUSTOMER_MODEL),
),
]

View File

@ -0,0 +1,46 @@
# Generated by Django 4.2.17 on 2025-02-04 11:38
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import phonenumber_field.modelfields
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.DJANGO_LEDGER_CUSTOMER_MODEL),
('inventory', '0002_alter_lead_customer'),
]
operations = [
migrations.AddField(
model_name='lead',
name='email',
field=models.EmailField(default='x@tenhal.sa', max_length=254, verbose_name='Email'),
preserve_default=False,
),
migrations.AddField(
model_name='lead',
name='first_name',
field=models.CharField(default='test', max_length=50, verbose_name='First Name'),
preserve_default=False,
),
migrations.AddField(
model_name='lead',
name='last_name',
field=models.CharField(default='test', max_length=50, verbose_name='Last Name'),
preserve_default=False,
),
migrations.AddField(
model_name='lead',
name='phone_number',
field=phonenumber_field.modelfields.PhoneNumberField(default='056523656', max_length=128, region='SA', verbose_name='Phone Number'),
preserve_default=False,
),
migrations.AlterField(
model_name='lead',
name='customer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='leads', to=settings.DJANGO_LEDGER_CUSTOMER_MODEL),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.17 on 2025-02-04 14:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0003_lead_email_lead_first_name_lead_last_name_and_more'),
]
operations = [
migrations.RemoveField(
model_name='lead',
name='city',
),
migrations.AddField(
model_name='lead',
name='address',
field=models.CharField(default='', max_length=50, verbose_name='address'),
preserve_default=False,
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 4.2.17 on 2025-02-04 15:15
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.DJANGO_LEDGER_CUSTOMER_MODEL),
('inventory', '0004_remove_lead_city_lead_address'),
]
operations = [
migrations.CreateModel(
name='Schedule',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('purpose', models.CharField(max_length=200)),
('scheduled_at', models.DateTimeField()),
('notes', models.TextField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to=settings.DJANGO_LEDGER_CUSTOMER_MODEL)),
('lead', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='inventory.lead')),
('scheduled_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.staff')),
],
options={
'ordering': ['-scheduled_at'],
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.17 on 2025-02-04 15:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0005_schedule'),
]
operations = [
migrations.AlterField(
model_name='schedule',
name='purpose',
field=models.CharField(choices=[('Product Demo', 'Product Demo'), ('Follow-Up Call', 'Follow-Up Call'), ('Contract Discussion', 'Contract Discussion'), ('Sales Meeting', 'Sales Meeting'), ('Support Call', 'Support Call'), ('Other', 'Other')], max_length=200),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.17 on 2025-02-04 15:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0006_alter_schedule_purpose'),
]
operations = [
migrations.AddField(
model_name='schedule',
name='scheduled_type',
field=models.CharField(choices=[('Call', 'Call'), ('Meeting', 'Meeting'), ('Email', 'Email')], default='Call', max_length=200),
),
]

View File

@ -1,3 +1,4 @@
from datetime import timezone
import itertools
from uuid import uuid4
from django.conf import settings
@ -1087,8 +1088,13 @@ class Representative(models.Model, LocalizedNameMixin):
class Lead(models.Model):
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="leads")
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(verbose_name=_("Email"))
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
customer = models.ForeignKey(
Customer, on_delete=models.CASCADE, related_name="leads"
CustomerModel, on_delete=models.CASCADE, related_name="leads",
null=True,blank=True
)
id_car_make = models.ForeignKey(
CarMake,
@ -1113,7 +1119,7 @@ class Lead(models.Model):
channel = models.CharField(
max_length=50, choices=Channel.choices, verbose_name=_("Channel")
)
city = models.CharField(max_length=50, verbose_name=_("City"))
address = models.CharField(max_length=50, verbose_name=_("address"))
staff = models.ForeignKey(
Staff,
on_delete=models.SET_NULL,
@ -1146,7 +1152,80 @@ class Lead(models.Model):
def __str__(self):
return f"{self.first_name} {self.last_name}"
@property
def is_converted(self):
return bool(self.customer)
def to_dict(self):
return {
"first_name": str(self.first_name),
"last_name": str(self.last_name),
"email": str(self.email),
"address": str(self.address),
"phone_number": str(self.phone_number),
"id_car_make": str(self.id_car_make.name),
"id_car_model": str(self.id_car_model.name),
"year": str(self.year),
"created_at": str(self.created.strftime("%Y-%m-%d")),
}
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
def convert_to_customer(self,entity):
if entity and not CustomerModel.objects.filter(email=self.email).exists():
customer = entity.create_customer(
customer_model_kwargs={
"customer_name": self.full_name,
"address_1": self.address,
"phone": self.phone_number,
"email": self.email,
}
)
customer.additional_info = {}
customer.additional_info.update({"info":self.to_dict()})
customer.save()
self.customer = customer
self.save()
def get_latest_schedule(self):
return self.schedules.order_by('-scheduled_at').first()
class Schedule(models.Model):
PURPOSE_CHOICES = [
('Product Demo', 'Product Demo'),
('Follow-Up Call', 'Follow-Up Call'),
('Contract Discussion', 'Contract Discussion'),
('Sales Meeting', 'Sales Meeting'),
('Support Call', 'Support Call'),
('Other', 'Other'),
]
ScheduledType = [
('Call', 'Call'),
('Meeting', 'Meeting'),
('Email', 'Email'),
]
lead = models.ForeignKey(Lead, on_delete=models.CASCADE, related_name='schedules')
customer = models.ForeignKey(CustomerModel, on_delete=models.CASCADE, related_name='schedules')
scheduled_by = models.ForeignKey(Staff, on_delete=models.CASCADE)
purpose = models.CharField(max_length=200, choices=PURPOSE_CHOICES)
scheduled_at = models.DateTimeField()
scheduled_type = models.CharField(max_length=200, choices=ScheduledType,default='Call')
notes = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"Scheduled {self.purpose} with {self.customer.customer_name} on {self.scheduled_at}"
def schedule_past_date(self):
if self.scheduled_at < timezone.now():
return True
return False
class Meta:
ordering = ['-scheduled_at']
class LeadStatusHistory(models.Model):
lead = models.ForeignKey(
@ -1181,7 +1260,7 @@ class Opportunity(models.Model):
Dealer, on_delete=models.CASCADE, related_name="opportunities"
)
customer = models.ForeignKey(
Customer, on_delete=models.CASCADE, related_name="opportunities"
Customer, on_delete=models.CASCADE, related_name="opportunities"
)
car = models.ForeignKey(
Car, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Car")
@ -1213,7 +1292,7 @@ class Opportunity(models.Model):
verbose_name_plural = _("Opportunities")
def __str__(self):
return f"{self.car.id_car_make.name} - {self.car.id_car_model.name} : {self.customer.get_full_name}"
return f"{self.car.id_car_make.name} - {self.car.id_car_model.name} : {self.customer}"
class Notes(models.Model):

View File

@ -811,7 +811,7 @@ def notify_assigned_staff(sender, instance, created, **kwargs):
if instance.staff: # Check if the lead is assigned
models.Notification.objects.create(
user=instance.staff.user,
message=f"You have been assigned a new lead: {instance.customer.get_full_name}."
message=f"You have been assigned a new lead: {instance.full_name}."
)

View File

@ -92,15 +92,25 @@ urlpatterns = [
path(
"crm/leads/<int:pk>/update/", views.LeadUpdateView.as_view(), name="lead_update"
),
path(
"crm/leads/<int:pk>/delete/", views.LeadDeleteView.as_view(), name="lead_delete"
),
path("crm/leads/<int:pk>/delete/", views.LeadDeleteView, name="lead_delete"),
path("crm/leads/<int:pk>/add-note/", views.add_note_to_lead, name="add_note"),
path("crm/leads/<int:pk>/lead-convert/", views.lead_convert, name="lead_convert"),
path(
"crm/leads/<int:pk>/add-activity/",
views.add_activity_to_lead,
name="add_activity",
),
path(
"crm/leads/<int:pk>/send_lead_email/",
views.send_lead_email,
name="send_lead_email",
),
path(
"crm/leads/<int:pk>/schedule/",
views.schedule_lead,
name="schedule_lead",
),
path(
"crm/opportunities/create/",
views.OpportunityCreateView.as_view(),

View File

@ -350,7 +350,7 @@ def set_bill_payment(dealer, entity, bill, amount, payment_method):
account=cash_account, # Debit Cash
amount=amount, # Payment amount
tx_type="debit",
description="Payment Received",
description="Bill Payment Received",
)
TransactionModel.objects.create(
@ -358,7 +358,7 @@ def set_bill_payment(dealer, entity, bill, amount, payment_method):
account=account_payable, # Credit Accounts Receivable
amount=amount, # Payment amount
tx_type="credit",
description="Payment Received",
description="Bill Payment Received",
)
bill.make_payment(amount)

View File

@ -1804,7 +1804,7 @@ class OrganizationListView(LoginRequiredMixin, ListView):
class OrganizationDetailView(DetailView):
model = models.Organization
model = CustomerModel
template_name = "organizations/organization_detail.html"
context_object_name = "organization"
@ -1812,12 +1812,8 @@ class OrganizationDetailView(DetailView):
def OrganizationCreateView(request):
if request.method == "POST":
form = forms.OrganizationForm(request.POST)
# upload logo
image = request.FILES.get("logo")
file_name = default_storage.save("images/{}".format(image.name), image)
file_url = default_storage.url(file_name)
organization_dict = {
x: request.POST[x] for x in request.POST if x != "csrfmiddlewaretoken"
}
@ -1831,7 +1827,12 @@ def OrganizationCreateView(request):
"email": organization_dict["email"],
}
)
organization_dict["logo"] = file_url
image = request.FILES.get("logo")
if image:
file_name = default_storage.save("images/{}".format(image.name), image)
file_url = default_storage.url(file_name)
organization_dict["logo"] = file_url
organization_dict["pk"] = str(instance.pk)
instance.additional_info["organization_info"] = organization_dict
instance.additional_info["type"] = "organization"
@ -2475,6 +2476,7 @@ class EstimateDetailView(LoginRequiredMixin, DetailView):
if estimate.get_itemtxs_data():
calculator = CarFinanceCalculator(estimate)
finance_data = calculator.get_finance_data()
print(finance_data.get("cars"))
kwargs["data"] = finance_data
kwargs["invoice"] = (
InvoiceModel.objects.all().filter(ce_model=estimate).first()
@ -2609,11 +2611,7 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView):
if invoice.get_itemtxs_data():
calculator = CarFinanceCalculator(invoice)
finance_data = calculator.get_finance_data()
print(
(finance_data["total_vat_amount"] + finance_data["total_price"])
== finance_data["grand_total"]
)
finance_data = calculator.get_finance_data()
kwargs["data"] = finance_data
kwargs["payments"] = JournalEntryModel.objects.filter(
ledger=invoice.ledger
@ -2954,11 +2952,10 @@ class LeadCreateView(CreateView, SuccessMessageMixin, LoginRequiredMixin):
model = models.Lead
form_class = forms.LeadForm
template_name = "crm/leads/lead_form.html"
# success_message = "Lead created successfully!"
success_message = "Lead created successfully!"
success_url = reverse_lazy("lead_list")
def form_valid(self, form):
print("Form data:", form.cleaned_data) # Debug form data
def form_valid(self, form):
dealer = get_user_type(self.request)
form.instance.dealer = dealer
return super().form_valid(form)
@ -2981,10 +2978,11 @@ class LeadUpdateView(UpdateView):
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 LeadDeleteView(request,pk):
lead = get_object_or_404(models.Lead, pk=pk)
lead.delete()
messages.success(request, "Lead deleted successfully!")
return redirect("lead_list")
def add_note_to_lead(request, pk):
@ -3002,6 +3000,74 @@ def add_note_to_lead(request, pk):
form = forms.NoteForm()
return render(request, "crm/add_note.html", {"form": form, "lead": lead})
def lead_convert(request, pk):
lead = get_object_or_404(models.Lead, pk=pk)
dealer = get_user_type(request)
lead.convert_to_customer(dealer.entity)
messages.success(request, "Lead converted to customer successfully!")
return redirect("lead_list")
def schedule_lead(request, pk):
lead = get_object_or_404(models.Lead, pk=pk)
if request.method == "POST":
form = forms.ScheduleForm(request.POST)
if form.is_valid():
instance = form.save(commit=False)
instance.lead = lead
instance.customer = lead.customer
if hasattr(request.user, "staff"):
instance.scheduled_by = request.user.staff
instance.save()
messages.success(request, "Lead scheduled successfully!")
return redirect("lead_list")
else:
messages.error(request, f"Invalid form data: {str(form.errors)}")
return redirect("lead_list")
form = forms.ScheduleForm()
return render(request, "crm/leads/schedule_lead.html", {"lead": lead, "form": form})
def send_lead_email(request, pk):
lead = get_object_or_404(models.Lead, pk=pk)
dealer = get_user_type(request)
lead.convert_to_customer(dealer.entity)
if request.method == "POST":
send_email(
"manager@tenhal.com",
request.POST.get("to"),
request.POST.get("subject"),
request.POST.get("message"),
)
messages.success(request, "Email sent successfully!")
return redirect("lead_list")
msg = f"""
السلام عليكم
Dear {lead.full_name},
أود أن أشارككم تقدير المشروع الذي ناقشناه. يرجى العثور على الوثيقة التفصيلية للمقترح المرفقة.
I hope this email finds you well. I wanted to share with you the estimate for the project we discussed. Please find the detailed estimate document attached.
يرجى مراجعة المقترح وإعلامي إذا كانت لديك أي أسئلة أو مخاوف. إذا كانت كل شيء يبدو جيدًا، يمكننا المضي قدمًا في المشروع.
Please review the estimate and let me know if you have any questions or concerns. If everything looks good, we can proceed with the project.
شكراً لاهتمامكم بهذا الأمر.
Thank you for your attention to this matter.
تحياتي,
Best regards,
[Your Name]
[Your Position]
[Your Company]
[Your Contact Information]
"""
return render(
request,
"crm/leads/lead_send.html",
{"lead": lead, "message": msg},
)
def add_activity_to_lead(request, pk):
lead = get_object_or_404(models.Lead, pk=pk)
@ -3244,29 +3310,23 @@ class BillDetailView(LoginRequiredMixin, DetailView):
if bill.get_itemtxs_data():
txs = bill.get_itemtxs_data()[0]
car_and_item_info = [
transactions = [
{
"car": models.Car.objects.get(vin=x.item_model.name),
"total": models.Car.objects.get(
vin=x.item_model.name
).finances.cost_price
* Decimal(x.quantity),
"itemmodel": x,
"item": x,
"total": Decimal(x.unit_cost) * Decimal(x.quantity),
}
for x in txs
]
grand_total = sum(
Decimal(
models.Car.objects.get(vin=x.item_model.name).finances.cost_price
)
* Decimal(x.quantity)
Decimal(x.unit_cost) * Decimal(x.quantity)
for x in txs
)
vat = models.VatRate.objects.filter(is_active=True).first()
if vat:
grand_total += round(Decimal(grand_total) * Decimal(vat.rate), 2)
kwargs["car_and_item_info"] = car_and_item_info
# vat = models.VatRate.objects.filter(is_active=True).first()
# if vat:
# grand_total += round(Decimal(grand_total) * Decimal(vat.rate), 2)
kwargs["transactions"] = transactions
kwargs["grand_total"] = grand_total
print(dir(txs[0]))
return super().get_context_data(**kwargs)
@ -3345,7 +3405,7 @@ def bill_mark_as_paid(request, pk):
bill.mark_as_paid(user_model=dealer.entity.admin)
bill.save()
bill.ledger.lock_journal_entries()
bill.ledger.post_journal_entries()
bill.ledger.post_journal_entries()
bill.ledger.post()
bill.ledger.save()
messages.success(request, _("Bill marked as paid successfully."))
@ -3488,9 +3548,7 @@ def bill_create(request):
),
}
)
car_list = models.Car.objects.filter(
dealer=dealer, finances__selling_price__gt=0, status="available"
)
car_list = models.Car.objects.filter(dealer=dealer)
context = {
"form": form,
"items": [

View File

@ -52,7 +52,6 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"
integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
{% block customCSS %}
@ -135,10 +134,8 @@ function notify(tag,msg){
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<script src="{% static 'vendors/swiper/swiper-bundle.min.js' %}"></script>
<script src="{% static 'vendors/flatpickr/flatpickr.min.js' %}"></script>
<script>
<script>
{% if entity_slug %}
let entitySlug = "{{ view.kwargs.entity_slug }}"
{% endif %}

View File

@ -1,8 +1,6 @@
{% extends 'base.html' %}
{% load i18n static %}
{% block content %}
<div class="row g-3">
<div class="col-12">
<div class="row align-items-center justify-content-between g-3 mb-3">

View File

@ -1,5 +1,6 @@
{% extends 'base.html' %}
{% load i18n static crispy_forms_filters %}
{% block content %}
<h1>{% if object %}Update{% else %}Create{% endif %}</h1>
<form method="post">

View File

@ -36,13 +36,12 @@
<table class="table fs-9 mb-0 border-top border-translucent">
<thead>
<tr>
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">{{ _("Status")|capfirst }}</th>
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 25%;">
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 20%;">{{ _("Lead Name")|capfirst }}</th>
<th class="align-middle white-space-nowrap text-uppercase" scope="col" 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="user"></span></div>
<span>{{ _("Name")|capfirst }}</span>
<div class="d-flex align-items-center px-1 py-1 bg-success-subtle rounded me-2"><i class="text-success-dark fas fa-car"></i></div>
<span>{{ _("Car")|capfirst }}</span>
</div>
</th>
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 15%;">
<div class="d-inline-flex flex-center">
@ -56,12 +55,24 @@
<span>{{ _("Phone Number") }}</span>
</div>
</th>
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 15%;">
<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>{{ _("Schedule") }}</span>
</div>
</th>
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center bg-info-subtle rounded me-2"><span class="text-info-dark" data-feather="database"></span></div>
<span>{{ _("Assigned To")|capfirst }}</span>
</div>
</th>
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center bg-info-subtle rounded me-2"><span class="text-info-dark" data-feather="database"></span></div>
<span>{{ _("Source")|capfirst }}</span>
</div>
</th>
</th>``
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center bg-warning-subtle rounded me-2"><span class="text-warning-dark" data-feather="grid"></span></div>
@ -101,29 +112,39 @@
</div>
</div>
</div>
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="align-middle white-space-nowrap fw-semibold">
<div class="d-flex align-items-center">
{% if lead.status == "new" %}
<span class="badge badge-phoenix badge-phoenix-primary"><span class="badge-label">{{_("New")}}</span><span class="fa fa-bell ms-1"></span></span>
{% elif lead.status == "pending" %}
<span class="badge badge-phoenix badge-phoenix-warning"><span class="badge-label">{{_("Pending")}}</span><span class="fa fa-clock-o ms-1"></span></span>
{% elif lead.status == "in_progress" %}
<span class="badge badge-phoenix badge-phoenix-info"><span class="badge-label">{{_("In Progress")}}</span><span class="fa fa-wrench ms-1"></span></span>
{% elif lead.status == "qualified" %}
<span class="badge badge-phoenix badge-phoenix-success"><span class="badge-label">{{_("Qualified")}}</span><span class="fa fa-check ms-1"></span></span>
{% elif lead.status == "canceled" %}
<span class="badge badge-phoenix badge-phoenix-danger"><span class="badge-label">{{_("Canceled")}}</span><span class="fa fa-times ms-1"></span></span>
{% endif %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="name align-middle white-space-nowrap ps-0">
<div class="d-flex align-items-center"><a href="#!">
</a>
<div><a class="fs-8 fw-bold" href="{% url 'lead_detail' lead.pk %}">{{lead.full_name}}</a>
<div class="d-flex align-items-center">
<p class="mb-0 text-body-highlight fw-semibold fs-9 me-2"></p>
{% if lead.status == "new" %}
<span class="badge badge-phoenix badge-phoenix-primary"><span class="badge-label">{{_("New")}}</span><span class="fa fa-bell ms-1"></span></span>
{% elif lead.status == "pending" %}
<span class="badge badge-phoenix badge-phoenix-warning"><span class="badge-label">{{_("Pending")}}</span><span class="fa fa-clock-o ms-1"></span></span>
{% elif lead.status == "in_progress" %}
<span class="badge badge-phoenix badge-phoenix-info"><span class="badge-label">{{_("In Progress")}}</span><span class="fa fa-wrench ms-1"></span></span>
{% elif lead.status == "qualified" %}
<span class="badge badge-phoenix badge-phoenix-success"><span class="badge-label">{{_("Qualified")}}</span><span class="fa fa-check ms-1"></span></span>
{% elif lead.status == "canceled" %}
<span class="badge badge-phoenix badge-phoenix-danger"><span class="badge-label">{{_("Canceled")}}</span><span class="fa fa-times ms-1"></span></span>
{% endif %}
</div>
</div>
</div>
</td>
</td>
<td class="align-middle white-space-nowrap fw-semibold"><a class="text-body-highlight" href="">{{ lead.id_car_make.get_local_name }} - {{ lead.id_car_model.get_local_name }} {{ lead.year }}</a></td>
<td class="align-middle white-space-nowrap fw-semibold"><a class="text-body-highlight" href="">{{ lead.email }}</a></td>
<td class="align-middle white-space-nowrap fw-semibold"><a class="text-body-highlight" href="tel:{{ lead.phone_number }}">{{ lead.phone_number }}</a></td>
<td class="align-middle white-space-nowrap fw-semibold">
<div class="d-flex align-items-center">
<a class="fs-8 fw-bold" href="{% url 'lead_detail' lead.pk %}">{{ lead.customer.get_full_name }}</a>
</div>
</td>
<td class="align-middle white-space-nowrap fw-semibold"><a class="text-body-highlight" href="">{{ lead.customer.email }}</a></td>
<td class="align-middle white-space-nowrap fw-semibold"><a class="text-body-highlight" href="tel:{{ lead.customer.phone_number }}">{{ lead.customer.phone_number }}</a></td>
{% if lead.get_latest_schedule %}
<span class="text-success" data-feather="calendar"></span> <span class="badge badge-phoenix badge-phoenix-primary text-success fw-semibold">{{ lead.get_latest_schedule.scheduled_type }}</span> <br>
<span class="text-success" data-feather="clock"></span> <span class="badge badge-phoenix badge-phoenix-primary text-success fw-semibold">{{ lead.get_latest_schedule.scheduled_at }}</span>
{% endif %}
</td>
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{{ lead.staff|upper }}</td>
<td class="align-middle white-space-nowrap fw-semibold text-body-highlight">{{ lead.source|upper }}</td>
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{{ lead.channel|upper }}</td>
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 text-body-tertiary">{{ lead.created|date }}</td>
@ -141,6 +162,11 @@
</button>
<div class="dropdown-menu dropdown-menu-end py-2">
<a href="{% url 'lead_update' lead.id %}" class="dropdown-item text-success-dark">{% trans "Edit" %}</a>
<a href="{% url 'send_lead_email' lead.id %}" class="dropdown-item text-success-dark">{% trans "Send Email" %}</a>
<a href="{% url 'schedule_lead' lead.id %}" class="dropdown-item text-success-dark">{% trans "Set Schedule" %}</a>
{% if not lead.is_converted %}
<a href="{% url 'lead_convert' lead.id %}" class="dropdown-item text-success-dark">{% trans "Convert To Customer" %}</a>
{% endif %}
<div class="dropdown-divider"></div>
<button class="dropdown-item text-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">{% trans "Delete" %}</button>
</div>

View File

@ -0,0 +1,36 @@
{% extends "base.html" %}
{% load crispy_forms_filters %}
{% load i18n static %}
{% block title %}{{ _("Leads") }}{% endblock title %}
{% block content %}
<div class="card email-content">
<h5 class="card-header">Send Mail</h5>
<div class="card-body">
<form class="d-flex flex-column h-100" action="{% url 'send_lead_email' lead.pk %}" method="post">
{% csrf_token %}
<div class="row g-3 mb-2">
<div class="col-12">
<input class="form-control" name="to" type="text" placeholder="To" value="{{lead.email}}" />
</div>
<div class="col-12">
<input class="form-control" name="subject" type="text" placeholder="Subject" value="" />
</div>
</div>
<div class="mb-3 flex-1">
<textarea class="form-control" name="message" rows="15" placeholder="Message">{{message}}</textarea>
</div>
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex">
<a href="{% url 'lead_detail' lead.pk %}" class="btn btn-link text-body fs-10 text-decoration-none">Discard</a>
<button class="btn btn-primary fs-10" type="submit">Send<span class="fa-solid fa-paper-plane ms-1"></span></button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,21 @@
{% extends 'base.html' %}
{% load i18n static crispy_forms_filters %}
{% block content %}
<h1>{% if object %}Update{% else %}Create{% endif %}</h1>
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button
type="submit"
name="schedule_lead"
id="lead-save"
class="btn btn-phoenix-primary"
>
{{ _("Save") }}
</button>
<a href="{% url 'lead_list' %}" class="btn btn-phoenix-secondary">
{{ _("Cancel") }}
</a>
</form>
{% endblock %}

View File

@ -56,7 +56,7 @@
<!-- ============================================-->
<!-- <section> begin ============================-->
<section class="pt-5 pb-9 bg-body-emphasis dark__bg-gray-1200 border-top">
<div class="row-small mt-3">
<div class="row-small mt-3 mx-3">
<div class="d-flex justify-content-between align-items-end mb-4">
<h2 class="mb-0">{% trans 'Bill' %}</h2>
<div class="d-flex align-items-center gap-2">
@ -217,13 +217,13 @@
</tr>
</thead>
<tbody>
{% for item in car_and_item_info %}
{% for transaction in transactions %}
<tr>
<td class="">{{forloop.counter}}</td>
<td class="">{{item.car.id_car_model}}</td>
<td class="align-middle">{{item.itemmodel.quantity}}</td>
<td class="align-middle ps-5">{{item.car.finances.cost_price}}</td>
<td class="align-middle text-body-tertiary fw-semibold">{{item.total}}</td>
<td class=""></td>
<td class="">{{transaction.item.item_model.name}}</td>
<td class="align-middle">{{transaction.item.quantity}}</td>
<td class="align-middle ps-5">{{transaction.item.unit_cost}}</td>
<td class="align-middle text-body-tertiary fw-semibold">{{transaction.total}}</td>
</tr>
{% endfor %}
<tr class="bg-body-secondary total-sum">
@ -235,7 +235,7 @@
<tr class="bg-body-secondary total-sum">
<td class="align-middle ps-4 fw-bolder text-body-highlight" colspan="4">{% trans "Grand Total" %}</td>
<td class="align-middle text-start fw-bolder">
<span id="grand-total">{{bill.additional_info.car_finance.total_vat}}</span>
<span id="grand-total">{{grand_total}}</span>
</td>
</tr>
</tbody>

View File

@ -27,11 +27,6 @@
<span class="fas fa-chevron-right"> </span>
<span class="fas fa-chevron-right"> </span>
</a>
<span class="fw-bold">{% trans 'Year' %}:</span>
<a href="{{ previous_year_url }}" class="text-decoration-none me-2"><< {{ previous_year }}</a>
<a href="{{ current_year_url }}" class="text-decoration-none me-2">{{ year }}</a>
<a href="{{ next_year_url }}" class="text-decoration-none">{{ next_year }} >></a>
</p>
</div>

View File

@ -5,14 +5,14 @@
<div class="row my-4">
<h2>{{ organization.get_local_name }}</h2>
<ul class="list-group mb-4">
<li class="list-group-item"><strong>{% trans "CRN" %}:</strong> {{ organization.crn }}</li>
<li class="list-group-item"><strong>{% trans "VRN" %}:</strong> {{ organization.vrn }}</li>
<li class="list-group-item"><strong>{% trans "Phone" %}:</strong> {{ organization.phone_number }}</li>
<li class="list-group-item"><strong>{% trans "Address" %}:</strong> {{ organization.address }}</li>
<li class="list-group-item"><strong>{% trans "CRN" %}:</strong> {{ organization.additional_info.organization_info.crn }}</li>
<li class="list-group-item"><strong>{% trans "VRN" %}:</strong> {{ organization.additional_info.organization_info.vrn }}</li>
<li class="list-group-item"><strong>{% trans "Phone" %}:</strong> {{ organization.additional_info.organization_info.phone_number }}</li>
<li class="list-group-item"><strong>{% trans "Address" %}:</strong> {{ organization.additional_info.organization_info.address }}</li>
</ul>
<div class="d-flex">
<a href="{% url 'organization_update' organization.id %}" class="btn btn-sm btn-warning me-2">{% trans "Edit" %}</a>
<form method="post" action="{% url 'organization_delete' organization.id %}">
<a href="{% url 'organization_update' organization.pk %}" class="btn btn-sm btn-warning me-2">{% trans "Edit" %}</a>
<form method="post" action="{% url 'organization_delete' organization.pk %}">
{% csrf_token %}
<button type="submit" class="btn btn-sm btn-danger">{% trans "Delete" %}</button>
</form>

View File

@ -108,9 +108,9 @@
<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 'organization_detail' org.pk %}">{{ org.name }}</a>
<div><a class="fs-8 fw-bold" href="{% url 'organization_detail' org.pk %}">{{ org.customer_name }}</a>
<div class="d-flex align-items-center">
<p class="mb-0 text-body-highlight fw-semibold fs-9 me-2">{{ org.arabic_name }}</p><span class="badge badge-phoenix badge-phoenix-primary">{{ org.pk}}</span>
<p class="mb-0 text-body-highlight fw-semibold fs-9 me-2"></p><span class="badge badge-phoenix badge-phoenix-primary">{{ org.customer_name}}</span>
</div>
</div>
</div>

View File

@ -33,7 +33,7 @@
<!-- <section> begin ============================-->
<section class="pt-5 pb-9 bg-body-emphasis dark__bg-gray-1200 border-top">
<div class="row-small mt-3">
<div class="d-flex justify-content-between align-items-end mb-4">
<div class="d-flex justify-content-between align-items-end mb-4 mx-3">
<h2 class="mb-0">{% trans 'Estimate' %}</h2>
<div class="d-flex align-items-center gap-2">
{% if estimate.status == 'draft' %}
@ -46,7 +46,7 @@
<a href="{% url 'invoice_create' estimate.pk %}" class="btn btn-phoenix-primary"><span class="d-none d-sm-inline-block">{% trans 'Create Invoice' %}</span></a>
{% else %}
<a href="{% url 'create_sale_order' estimate.pk %}" class="btn btn-phoenix-primary"><span class="d-none d-sm-inline-block">{% trans 'Create Sale Order' %}</span></a>
<a href="{% url 'preview_sale_order' estimate.pk %}" class="btn btn-phoenix-primary"><span class="d-none d-sm-inline-block">{% trans 'Preview Sale Order' %}</span></a>
{% comment %} <a href="{% url 'preview_sale_order' estimate.pk %}" class="btn btn-phoenix-primary"><span class="d-none d-sm-inline-block">{% trans 'Preview Sale Order' %}</span></a> {% endcomment %}
{% endif %}
{% elif estimate.status == 'in_review' %}
<a href="{% url 'estimate_preview' estimate.pk %}" class="btn btn-phoenix-primary"><span class="d-none d-sm-inline-block">{% trans 'Preview' %}</span></a>
@ -83,7 +83,7 @@
<div class="row align-items-center g-0">
<div class="col-auto col-lg-6 col-xl-5">
<h6 class="mb-2 me-3">{% trans 'Customer' %} :</h6>
<p class="fs-9 text-body-secondary fw-semibold mb-0">{{estimate.customer.customer_name}}</p>
<p class="fs-9 text-body-secondary fw-semibold mb-0">{{estimate.customer.customer_name}}</p>
</div>
<div class="col-12 col-lg-4">
<h6 class="mb-2"> {% trans 'Email' %} :</h6>
@ -119,7 +119,9 @@
<thead class="bg-body-secondary">
<tr>
<th scope="col" style="width: 24px;">#</th>
<th scope="col" style="min-width: 260px;">{% trans "Item" %}</th>
<th scope="col" style="min-width: 260px;">{% trans "Make" %}</th>
<th scope="col" style="min-width: 260px;">{% trans "Model" %}</th>
<th scope="col" style="min-width: 260px;">{% trans "Year" %}</th>
<th scope="col" style="min-width: 60px;">{% trans "Quantity" %}</th>
<th scope="col" style="min-width: 60px;">{% trans "Unit Price" %}</th>
<th scope="col" style="min-width: 60px;">{% trans "Total" %}</th>
@ -129,27 +131,29 @@
<tbody>
{% for item in data.cars %}
<tr>
<td class="">{{forloop.counter}}</td>
<td class="">{{item.make}}</td>
<td class="align-middle"></td>
<td class="align-middle">{{item.make}}</td>
<td class="align-middle">{{item.model}}</td>
<td class="align-middle">{{item.year}}</td>
<td class="align-middle">{{item.quantity}}</td>
<td class="align-middle ps-5">{{item.unit_price}}</td>
<td class="align-middle text-body-tertiary fw-semibold">{{item.total}}</td>
</tr>
{% endfor %}
<tr class="bg-body-secondary total-sum">
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="4">{% trans "Vat" %} ({{data.vat}}%)</td>
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="6">{% trans "Vat" %} ({{data.vat}}%)</td>
<td class="align-middle text-start fw-semibold">
<span id="grand-total">+ {{data.total_vat_amount}}</span>
</td>
</tr>
<tr class="bg-body-secondary total-sum">
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="4">{% trans "Discount Amount" %}</td>
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="6">{% trans "Discount Amount" %}</td>
<td class="align-middle text-start text-danger fw-semibold ">
<span id="grand-total">- {{data.total_discount}}</span>
</td>
</tr>
<tr class="bg-body-secondary total-sum">
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="4">{% trans "Additional Services" %}</td>
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="6">{% trans "Additional Services" %}</td>
<td class="align-middle text-start fw-semibold">
{% for service in data.additionals %}
<small><span class="fw-semibold">+ {{service.name}} - {{service.total}}</span></small><br>
@ -157,7 +161,7 @@
</td>
</tr>
<tr class="bg-body-secondary total-sum">
<td class="align-middle ps-4 fw-bolder text-body-highlight" colspan="4">{% trans "Grand Total" %}</td>
<td class="align-middle ps-4 fw-bolder text-body-highlight" colspan="6">{% trans "Grand Total" %}</td>
<td class="align-middle text-start fw-bolder">
<span id="grand-total">{{data.grand_total}}</span>
</td>

View File

@ -55,9 +55,9 @@
</div>
<!-- ============================================-->
<!-- <section> begin ============================-->
<section class="pt-5 pb-9">
<div class="row-small mt-3">
<div class="d-flex justify-content-between align-items-end mb-4">
<section class="pt-5 pb-9 bg-body-emphasis dark__bg-gray-1200 border-top">
<div class="row-small mt-3 mx-3">
<div class="d-flex justify-content-between align-items-end mb-4 mx-3">
<h2 class="mb-0">{% trans 'Invoice' %}</h2>
<div class="d-flex align-items-center gap-2">
{% if invoice.invoice_status == 'in_review' %}
@ -212,46 +212,50 @@
<thead class="bg-body-secondary">
<tr>
<th scope="col" style="width: 24px;">#</th>
<th scope="col" style="min-width: 260px;">{% trans "Item" %}</th>
<th scope="col" style="min-width: 260px;">{% trans "Make" %}</th>
<th scope="col" style="min-width: 260px;">{% trans "Model" %}</th>
<th scope="col" style="min-width: 260px;">{% trans "Year" %}</th>
<th scope="col" style="min-width: 60px;">{% trans "Quantity" %}</th>
<th scope="col" style="min-width: 60px;">{% trans "Unit Price" %}</th>
<th scope="col" style="min-width: 60px;">{% trans "Total" %}</th>
</tr>
</thead>
<tbody>
{% for item in car_and_item_info %}
{% for item in data.cars %}
<tr>
<td class="">{{forloop.counter}}</td>
<td class="">{{item.info.make}}</td>
<td class="align-middle"></td>
<td class="align-middle">{{item.make}}</td>
<td class="align-middle">{{item.model}}</td>
<td class="align-middle">{{item.year}}</td>
<td class="align-middle">{{item.quantity}}</td>
<td class="align-middle ps-5">{{item.finances.selling_price}}</td>
<td class="align-middle ps-5">{{item.total}}</td>
<td class="align-middle text-body-tertiary fw-semibold">{{item.total}}</td>
</tr>
{% endfor %}
<tr class="bg-body-secondary total-sum">
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="4">{% trans "Discount Amount" %}</td>
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="6">{% trans "Discount Amount" %}</td>
<td class="align-middle text-start fw-semibold">
<span id="grand-total">- {{discount_amount}}</span>
<span id="grand-total">- {{data.total_discount}}</span>
</td>
</tr>
<tr class="bg-body-secondary total-sum">
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="4">{% trans "VAT" %} ({{vat}}%)</td>
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="6">{% trans "VAT" %} ({{data.vat}}%)</td>
<td class="align-middle text-start fw-semibold">
<span id="grand-total">+ {{data.total_vat_amount}}</span>
</td>
</tr>
<tr class="bg-body-secondary total-sum">
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="4">{% trans "Additional Services" %}</td>
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="6">{% trans "Additional Services" %}</td>
<td class="align-middle text-start fw-bold">
{% for service in additional_services %}
{% for service in data.additionals %}
<small><span class="fw-bold">+ {{service.name}} - {{service.price}}</span></small><br>
{% endfor %}
</td>
</tr>
<tr class="bg-body-secondary total-sum">
<td class="align-middle ps-4 fw-bolder text-body-highlight" colspan="4">{% trans "Grand Total" %}</td>
<td class="align-middle ps-4 fw-bolder text-body-highlight" colspan="6">{% trans "Grand Total" %}</td>
<td class="align-middle text-start fw-bolder">
<span id="grand-total">{{total}}</span>
<span id="grand-total">{{data.grand_total}}</span>
</td>
</tr>
</tbody>